// type UseLoadingOtions = { // type: string, // color: string, // el: UniElement // } import {tinyColor} from '@/uni_modules/lime-color' function easeInOutCubic(t : number) : number { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; } type useLoadingReturnType = { state : Ref color : Ref play: () => void failed: () => void clear : () => void destroy : () => void } type Point = { x1: number y1: number x2: number y2: number } export function useLoading( element : Ref, type : 'circular' | 'spinner', strokeColor : string, ratio : number, immediate: boolean = false, ) : useLoadingReturnType { const state = ref(false) const color = ref(strokeColor) let tick = 0 // 0 不绘制 | 1 旋转 | 2 错误 let init = false let isDestroy = ref(false) let width = 0 let height = 0 let size = 0 let x = 0 let y = 0 let ctx : DrawableContext | null = null let timer = -1 let isClear = false; let drawing = false; const updateSize = () => { if (element.value == null) return const rect = element.value!.getBoundingClientRect(); ctx = element.value!.getDrawableContext()! as DrawableContext width = rect.width height = rect.height size = ratio > 1 ? ratio : Math.floor(Math.min(width, height) * ratio) x = width / 2 y = height / 2 } const circular = () => { if (ctx == null) return let _ctx = ctx! let startAngle = 0; let endAngle = 0; let startSpeed = 0; let endSpeed = 0; let rotate = 0; // 不使用360的原因是加上rotate后,会导致闪烁 const ARC_LENGTH = 359.5 const PI = Math.PI / 180 const SPEED = 0.018 const ROTATE_INTERVAL = 0.09 const center = size / 2 const lineWidth = size / 10; function draw() { if(isClear) return _ctx.reset(); _ctx.beginPath(); _ctx.arc( x, y, center - lineWidth, startAngle * PI + rotate, endAngle * PI + rotate); _ctx.lineWidth = lineWidth; _ctx.strokeStyle = color.value; _ctx.stroke(); if (endAngle < ARC_LENGTH && startAngle == 0) { endSpeed += SPEED endAngle = Math.min(ARC_LENGTH, easeInOutCubic(endSpeed) * ARC_LENGTH) } else if (endAngle == ARC_LENGTH && startAngle < ARC_LENGTH) { startSpeed += SPEED startAngle = Math.min(ARC_LENGTH, easeInOutCubic(startSpeed) * ARC_LENGTH); } else if (endAngle >= ARC_LENGTH && startAngle >= ARC_LENGTH) { endSpeed = 0 startSpeed = 0 startAngle = 0; endAngle = 0; } rotate += ROTATE_INTERVAL; _ctx.update() // clearTimeout(timer) timer = setTimeout(() => draw(), 24) } draw() } const spinner = () => { if (ctx == null) return let _ctx = ctx! const steps = 12; let step = 0; const lineWidth = size / 10; // 线长度和距离圆心距离 const length = size / 4 - lineWidth; const offset = size / 4; function generateColorGradient(hex: string, steps: number):string[]{ const colors:string[] = [] const _color = tinyColor(hex) for (let i = 1; i <= steps; i++) { _color.setAlpha(i/steps); colors.push(_color.toRgbString()); } return colors } let colors = computed(():string[]=> generateColorGradient(color.value, steps)) function draw() { if(tick == 0) return _ctx.reset(); for (let i = 0; i < steps; i++) { const stepAngle = 360 / steps const angle = stepAngle * i; const index =(steps + i - (step % steps)) % steps // 正余弦 const sin = Math.sin(angle / 180 * Math.PI); const cos = Math.cos(angle / 180 * Math.PI); // 开始绘制 _ctx.lineWidth = lineWidth; _ctx.lineCap = 'round'; _ctx.beginPath(); _ctx.moveTo(size / 2 + offset * cos, size / 2 + offset * sin); _ctx.lineTo(size / 2 + (offset + length) * cos, size / 2 + (offset + length) * sin); _ctx.strokeStyle = colors.value[index] _ctx.stroke(); } step += 1 _ctx.update() timer = setTimeout(() => draw(), 1000/10) } draw() } const clear = () => { clearTimeout(timer) drawing = false tick = 0 if(ctx == null) return // ctx?.reset() // ctx?.update() setTimeout(()=>{ ctx!.reset() ctx!.update() },1000) } const failed = () => { if(tick == 1) { drawing = false } clearTimeout(timer) tick = 2 if (ctx == null || drawing) return let _ctx = ctx! const _size = size * 0.61 const _sizeX = _size * 0.65 const lineWidth = _size / 6; const lineLength = Math.ceil(Math.sqrt(Math.pow(_sizeX, 2) * 2)) const startX1 = (width - _sizeX) * 0.5 const startY = (height - _sizeX) * 0.5 const startX2 = startX1 + _sizeX // 添加圆的参数 const centerX = width / 2; const centerY = height / 2; const radius = (_size * Math.sqrt(2)) / 2 + lineWidth / 2; const totalSteps = 36; function generateSteps(stepsCount: number):Point[][] { const halfStepsCount = stepsCount / 2; const step = lineLength / halfStepsCount //Math.floor(lineLength / 18); const steps:Point[][] = [] for (let i = 0; i < stepsCount; i++) { const sub:Point[] = [] const index = i % 18 + 1 if(i < halfStepsCount) { const x2 = Math.sin(45 * Math.PI / 180) * step * index + startX1 const y2 = Math.cos(45 * Math.PI / 180) * step * index + startY const start1 = { x1: startX1, y1: startY, x2, y2, } as Point sub.push(start1) } else { sub.push(steps[halfStepsCount-1][0]) const x2 = Math.sin((45 - 90) * Math.PI / 180) * step * index + startX2 const y2 = Math.cos((45 - 90) * Math.PI / 180) * step * index + startY const start2 = { x1: startX2, y1: startY, x2, y2, } as Point sub.push(start2) } steps.push(sub) } return steps } const steps = generateSteps(36); function draw(){ if(steps.length == 0 || tick == 0) { clearTimeout(timer) return } const drawStep = steps.shift()! _ctx.reset() _ctx.lineWidth = lineWidth; _ctx.strokeStyle = color.value; // 绘制逐渐显示的圆 _ctx.beginPath(); _ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps); _ctx.lineWidth = lineWidth; _ctx.strokeStyle = color.value; _ctx.stroke(); // 绘制X _ctx.beginPath(); drawStep.forEach(item => { _ctx.beginPath(); _ctx.moveTo(item.x1, item.y1) _ctx.lineTo(item.x2, item.y2) _ctx.stroke(); }) _ctx.update() timer = setTimeout(() => draw(), 1000/30) } draw() } const destroy = () => { isDestroy.value = true; clear() } const play = () => { if(tick == 2) { drawing = false } if(drawing) return tick = 1 if(width == 0 || height == 0) return if (type == 'circular') { circular() } else if (type == 'spinner') { spinner() } drawing = true } const _watch = (v:boolean) => { if(isDestroy.value) return if (v) { play() } else { failed() } } const stopWatchState = watch(state, _watch) const ob = new UniResizeObserver((entries: UniResizeObserverEntry[])=>{ if(isDestroy.value) return entries.forEach(entry => { if(isDestroy.value) return const rect = entry.target.getBoundingClientRect(); if(rect.width > 0 && rect.height > 0) { updateSize(); if(tick == 1) { play() state.value = true } else if(tick == 2) { failed() state.value = false } else if(immediate && !init) { _watch(state.value) init = true } } }) }) const stopWatchElement = watch(element, (el:UniElement|null) => { if(el == null || isDestroy.value) return ob.observe(el) }) onUnmounted(()=>{ stopWatchState() stopWatchElement() clear() ob.disconnect() }) return { state, play, failed, clear, color, destroy } as useLoadingReturnType }