// 引入颜色处理库 import { tinyColor } from '@/uni_modules/lime-color'; /** * 操作类型 * play: 开始动画 * failed: 显示失败状态 * clear: 清除动画 * destroy: 销毁实例 */ export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause' /** * 加载动画类型 * circular: 环形加载动画 * spinner: 旋转器加载动画 * failed: 失败状态动画 */ export type LoadingType = 'circular' | 'spinner' | 'failed'; /** * 加载组件返回接口 */ export type UseLoadingReturn = { ratio : 1; type : LoadingType; mode : 'raf' | 'animate'; // color : string;//Ref; play : () => void; failed : () => void; clear : () => void; destroy : () => void; pause : () => void; } /** * 计算圆周上指定角度的点的坐标 * @param centerX 圆心的 X 坐标 * @param centerY 圆心的 Y 坐标 * @param radius 圆的半径 * @param angleDegrees 角度(以度为单位) * @returns 包含 X 和 Y 坐标的对象 */ function getPointOnCircle( centerX : number, centerY : number, radius : number, angleDegrees : number ) : number[] { // 将角度转换为弧度 const angleRadians = (angleDegrees * Math.PI) / 180; // 计算点的 X 和 Y 坐标 const x = centerX + radius * Math.cos(angleRadians); const y = centerY + radius * Math.sin(angleRadians); return [x, y] } export function useLoading(element : Ref) : UseLoadingReturn { const tick = ref('pause') const state = reactive({ color: '#000', type: 'circular', ratio: 1, mode: 'raf', play: () => { tick.value = 'play' }, failed: () => { tick.value = 'failed' }, clear: () => { tick.value = 'clear' }, destroy: () => { tick.value = 'destroy' }, pause: () => { tick.value = 'pause' } }) const context = shallowRef(null); // let ctx:DrawableContext|null = null // let rotation = 0 let isPlaying = false let canvasWidth = ref(0) let canvasHeight = ref(0) let canvasSize = ref(0) let animationFrameId = -1 let animation : UniAnimation | null = null let drawFrame : (() => void) | null = null const size = computed(() : number => state.ratio > 1 ? state.ratio : canvasSize.value * state.ratio) // 绘制圆形加载 const drawCircular = () => { let startAngle = 0; // 起始角度 let endAngle = 0; // 结束角度 let rotate = 0; // 旋转角度 // const ctx = context.value! // 动画参数配置 const MIN_ANGLE = 5; // 最小保持角度 const ARC_LENGTH = 359.5 // 最大弧长(避免闭合) const PI = Math.PI / 180 // 角度转弧度系数 const SPEED = 0.018 / 4 // 动画速度 const ROTATE_INTERVAL = 0.09 / 4 // 旋转增量 const lineWidth = size.value / 10; // 线宽计算 const x = canvasWidth.value / 2 // 中心点X const y = canvasHeight.value / 2 // 中心点Y const radius = size.value / 2 - lineWidth // 实际绘制半径 drawFrame = () => { if (context.value == null || !isPlaying) return let ctx = context.value! // console.log('radius', radius, size.value) ctx.reset(); // 绘制圆弧 ctx.beginPath(); ctx.arc( x, y, radius, startAngle * PI + rotate, endAngle * PI + rotate ); ctx.lineWidth = lineWidth; ctx.strokeStyle = state.color; ctx.stroke(); // 角度更新逻辑 if (endAngle < ARC_LENGTH) { endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED); } else if (startAngle < ARC_LENGTH) { startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED); } else { // 重置时保留最小可见角度 startAngle = 0; endAngle = MIN_ANGLE; } ctx.update() if (state.mode == 'raf') { rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围 if (isPlaying && drawFrame != null) { animationFrameId = requestAnimationFrame(drawFrame!) } } } } let lastTime = Date.now(); const drawSpinner = () => { const steps = 12; // 旋转线条数量 // const size = state.ratio > 1 ? state.ratio : canvasSize.value const lineWidth = size.value / 10; // 线宽 const x = canvasWidth.value / 2 // 中心坐标 const y = canvasHeight.value / 2 let step = 0; // 当前步数 // #ifdef APP-HARMONY const length = size.value / 3.4 - lineWidth; // 线长 // #endif // #ifndef APP-HARMONY const length = size.value / 3.6 - lineWidth; // 线长 // #endif const offset = size.value / 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(state.color, steps)) /** 帧绘制函数 */ drawFrame = () => { if (context.value == null || !isPlaying) return const delta = Date.now() - lastTime; if (delta >= 1000 / 10) { lastTime = Date.now(); let ctx = context.value! ctx.reset(); for (let i = 0; i < steps; i++) { const stepAngle = 360 / steps; // 单步角度 const angle = stepAngle * i; // 当前角度 const index = (steps + i - step) % steps // 颜色索引 // 计算线段坐标 const radian = angle * Math.PI / 180; const cos = Math.cos(radian); const sin = Math.sin(radian); // 绘制线段 ctx.beginPath(); ctx.moveTo(x + offset * cos, y + offset * sin); ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin); ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.strokeStyle = colors.value[index]; ctx.stroke(); } ctx.update() if(state.mode == 'raf') { // step += 1 step = (step + 1) % steps; // 限制step范围 } } if (state.mode == 'raf') { if (isPlaying && drawFrame != null) { animationFrameId = requestAnimationFrame(drawFrame!) } } } } const drwaFailed = () => { if (context.value == null) return let ctx = context.value! // const size = state.ratio > 1 ? state.ratio : canvasSize.value const innerSize = size.value * 0.8 // 内圈尺寸 const lineWidth = innerSize / 10; // 线宽 const lineLength = (size.value - lineWidth) / 2 // X长度 const centerX = canvasWidth.value / 2; const centerY = canvasHeight.value / 2; const radius = (size.value - lineWidth) / 2 const angleRadians1 = 45 * Math.PI / 180 const angleRadians2 = (45 - 90) * Math.PI / 180 ctx.reset() ctx.lineWidth = lineWidth; ctx.strokeStyle = state.color; // 绘制逐渐显示的圆 ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.lineWidth = lineWidth; ctx.strokeStyle = state.color; ctx.stroke(); const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45) const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45) const x2 = Math.sin(angleRadians1) * lineLength + startX1 const y2 = Math.cos(angleRadians1) * lineLength + startY ctx.beginPath(); ctx.moveTo(startX1, startY) ctx.lineTo(x2, y2) ctx.stroke(); const x3 = Math.sin(angleRadians2) * lineLength + startX2 const y3 = Math.cos(angleRadians2) * lineLength + startY ctx.beginPath(); ctx.moveTo(startX2, startY) ctx.lineTo(x3, y3) ctx.stroke(); ctx.update() } let currentType : LoadingType | null = null const useMode = () => { if (state.mode != 'raf') { const keyframes = [{ transform: 'rotate(0)' }, { transform: 'rotate(360)' }] animation = element.value!.animate(keyframes, { duration: 80000, easing: 'linear', // fill: 'forwards', iterations: Infinity }) } } const startAnimation = (type : string) => { if (context.value == null || element.value == null) return animation?.pause() if (currentType == type) { isPlaying = true animation?.play() drawFrame?.() return } if (type == 'circular') { currentType = 'circular' drawCircular() useMode() } if (type == 'spinner') { currentType = 'spinner' drawSpinner() useMode() } isPlaying = true drawFrame?.() } // 监听元素尺寸 const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => { requestAnimationFrame(()=> { element.value?.getBoundingClientRectAsync()?.then(rect => { if (rect.width == 0 || rect.height == 0) return context.value = element.value!.getDrawableContext() as DrawableContext; canvasWidth.value = rect.width; canvasHeight.value = rect.height; canvasSize.value = Math.min(rect.width, rect.height); // startAnimation(state.type) }) }) }); watchEffect(() => { if (element.value == null) return resizeObserver.observe(element.value!); }) watchEffect(() => { if (context.value == null) return if (tick.value == 'play') { startAnimation(state.type) } if (tick.value == 'failed') { clearTimeout(animationFrameId) animation?.pause() animation?.cancel() drwaFailed() return } if (tick.value == 'clear') { clearTimeout(animationFrameId) animation?.pause() animation?.cancel() context.value?.reset(); context.value?.update(); isPlaying = false return } if (tick.value == 'destroy') { clearTimeout(animationFrameId) animation?.pause() animation?.cancel() context.value?.reset(); context.value?.update(); context.value = null animation = null isPlaying = false return } if (tick.value == 'pause') { clearTimeout(animationFrameId) isPlaying = false animation?.pause() return } }) return state }