393 lines
9.7 KiB
Plaintext
393 lines
9.7 KiB
Plaintext
// 引入颜色处理库
|
|
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<string>;
|
|
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<UniElement | null>) : UseLoadingReturn {
|
|
|
|
const tick = ref<TickType>('pause')
|
|
|
|
const state = reactive<UseLoadingReturn>({
|
|
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<DrawableContext | null>(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
|
|
} |