|
|
- // 引入颜色处理库
- import { tinyColor } from '@/uni_modules/lime-color';
-
-
- // ===================== 类型定义 =====================
- /**
- * 加载动画类型
- * circular: 环形加载动画
- * spinner: 旋转器加载动画
- * failed: 失败状态动画
- */
- export type LoadingType = 'circular' | 'spinner' | 'failed';
-
- /**
- * 操作类型
- * play: 开始动画
- * failed: 显示失败状态
- * clear: 清除动画
- * destroy: 销毁实例
- */
- export type TickType = 'play' | 'failed' | 'clear' | 'destroy'
-
- /**
- * 加载组件配置选项
- * @property type - 初始动画类型
- * @property strokeColor - 线条颜色
- * @property ratio - 尺寸比例
- * @property immediate - 是否立即启动
- */
- export type UseLoadingOptions = {
- type : LoadingType;
- strokeColor : string;
- ratio : number;
- immediate ?: boolean;
- };
-
- /**
- * 加载组件返回接口
- */
- export type UseLoadingReturn = {
- // state : Ref<boolean>;
- // setOptions: (options: UseLoadingOptions) => void
- ratio : 1;
- type : LoadingType;
- color : string;//Ref<string>;
- play : () => void;
- failed : () => void;
- clear : () => void;
- destroy : () => void;
- }
-
- /**
- * 画布尺寸信息
- */
- export type Dimensions = {
- width : number;
- height : number;
- size : number
- }
-
- /**
- * 线段坐标点
- */
- type Point = {
- x1 : number
- y1 : number
- x2 : number
- y2 : number
- }
-
- /**
- * 画布上下文信息
- */
- type LoadingCanvasContext = {
- ctx : Ref<DrawableContext | null>;
- dimensions : Ref<Dimensions>;
- updateDimensions : (el : UniElement) => void;
- };
-
- /**
- * 动画参数配置
- */
- type AnimationParams = {
- width : number
- height : number
- center : number[] // 元组类型,明确表示两个数值的坐标
- color : string // 使用Ref类型包裹字符串
- size : number // 数值类型尺寸
- }
-
- // ===================== 动画管理器 =====================
- type AnimationFrameHandler = () => boolean;
-
- /**
- * 动画管理类
- * 封装动画的启动/停止逻辑
- */
- export class AnimationManager {
- time : number = 1000 / 60 // 默认帧率60fps
- private timer : number = -1;// 定时器ID
- private isDestroyed : boolean = false; // 销毁状态
- private drawFrame : AnimationFrameHandler// 帧绘制函数
- constructor(drawFrame : AnimationFrameHandler) {
- this.drawFrame = drawFrame
- }
- /** 启动动画循环 */
- start() {
- let animate : (() => void) | null = null
- animate = () => {
- if (this.isDestroyed) return;
- const shouldContinue : boolean = this.drawFrame();
- if (shouldContinue && animate != null) {
- this.timer = setTimeout(animate!, this.time);
- }
- };
- animate();
- }
- /** 停止动画并清理资源 */
- stop() {
- clearTimeout(this.timer);
- this.isDestroyed = true;
- }
- }
-
- // ===================== 工具函数 =====================
- /**
- * 缓动函数 - 三次缓入缓出
- * @param t 时间系数 (0-1)
- * @returns 计算后的进度值
- */
- function easeInOutCubic(t : number) : number {
- return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
- }
-
- // ===================== 画布管理 =====================
- /**
- * 获取画布上下文信息
- * @param element 画布元素引用
- * @returns 包含画布上下文和尺寸信息的对象
- */
- //_element : Ref<UniElement | null>
- export function useCanvas() : LoadingCanvasContext {
- const ctx = shallowRef<DrawableContext | null>(null);
- const dimensions = ref<Dimensions>({
- width: 0,
- height: 0,
- size: 0
- });
-
- const updateDimensions = (el: UniElement) => {
- const rect = el.getBoundingClientRect();
- ctx.value = el.getDrawableContext() as DrawableContext;
- dimensions.value.width = rect.width;
- dimensions.value.height = rect.height;
- dimensions.value.size = Math.min(rect.width, rect.height);
- };
-
- return {
- ctx,
- dimensions,
- updateDimensions
- } as LoadingCanvasContext
- }
-
- // ===================== 动画创建函数 =====================
- /**
- * 创建环形加载动画
- * @param ctx 画布上下文
- * @param animationParams 动画参数
- * @returns 动画管理器实例
- */
- function createCircularAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
- const { size, color, width, height } = animationParams
- let startAngle = 0; // 起始角度
- let endAngle = 0; // 结束角度
- let rotate = 0; // 旋转角度
-
- // 动画参数配置
- const MIN_ANGLE = 5; // 最小保持角度
- const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
- const PI = Math.PI / 180 // 角度转弧度系数
- const SPEED = 0.018 // 动画速度
- const ROTATE_INTERVAL = 0.09 // 旋转增量
- const lineWidth = size / 10; // 线宽计算
- const x = width / 2 // 中心点X
- const y = height / 2 // 中心点Y
- const radius = size / 2 - lineWidth // 实际绘制半径
-
- /** 帧绘制函数 */
- const drawFrame = () : boolean => {
- ctx.reset();
-
- // 绘制圆弧
- ctx.beginPath();
- ctx.arc(
- x,
- y,
- radius,
- startAngle * PI + rotate,
- endAngle * PI + rotate
- );
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = 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;
- }
-
- rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
- ctx.update()
- return true
- }
-
- return new AnimationManager(drawFrame)
- }
-
- /**
- * 创建旋转器动画
- * @param ctx 画布上下文
- * @param animationParams 动画参数
- * @returns 动画管理器实例
- */
- function createSpinnerAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
- const { size, color, center } = animationParams
- const steps = 12; // 旋转线条数量
- let step = 0; // 当前步数
- const lineWidth = size / 10; // 线宽
- const length = size / 4 - lineWidth; // 线长
- const offset = size / 4; // 距中心偏移
- const [x, y] = center // 中心坐标
-
- /** 生成颜色渐变数组 */
- 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, steps))
-
- /** 帧绘制函数 */
- const drawFrame = () : boolean => {
- 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 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();
- }
- step += 1
- ctx.update()
- return true
- }
- return new AnimationManager(drawFrame)
- }
-
- /**
- * 计算圆周上指定角度的点的坐标
- * @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]
- }
-
- /**
- * 创建失败状态动画(包含X图标和外围圆圈)
- * @param ctx 画布上下文
- * @param animationParams 动画参数
- * @returns 动画管理器实例
- */
- function createFailedAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
-
- const { width, height, size, color } = animationParams
- const innerSize = size * 0.8 // 内圈尺寸
- const lineWidth = innerSize / 10; // 线宽
- const lineLength = (size - lineWidth) / 2 // X长度
- const centerX = width / 2;
- const centerY = height / 2;
-
- const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
- const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
- const angleRadians1 = 45 * Math.PI / 180
- const angleRadians2 = (45 - 90) * Math.PI / 180
-
- const radius = (size - lineWidth) / 2
- const totalSteps = 36; // 总动画步数
-
- function generateSteps(stepsCount : number) : Point[][] {
-
- const halfStepsCount = stepsCount / 2;
- const step = lineLength / halfStepsCount
- 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(angleRadians1) * step * index + startX1
- const y2 = Math.cos(angleRadians1) * 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(angleRadians2) * step * index + startX2
- const y2 = Math.cos(angleRadians2) * step * index + startY
-
- const start2 = {
- x1: startX2,
- y1: startY,
- x2,
- y2,
- } as Point
- sub.push(start2)
- }
- steps.push(sub)
- }
-
- return steps
- }
- const steps = generateSteps(totalSteps);
-
- const drawFrame = () : boolean => {
- const drawStep = steps.shift()!
- ctx.reset()
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = color;
-
- // 绘制逐渐显示的圆
- ctx.beginPath();
- ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = color;
- 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()
- return steps.length != 0
- }
- return new AnimationManager(drawFrame)
- }
-
-
- // ===================== 主Hook函数 =====================
- /**
- * 加载动画组合式函数
- * @param element 画布元素引用
- * @returns 加载控制器实例
- */
- export function useLoading(
- element : Ref<UniElement | null>,
- // options : UseLoadingOptions
- ) : UseLoadingReturn {
- const ticks = ref<TickType[]>([]);
- const currentTick = ref<TickType>('clear');
-
- const state = reactive<UseLoadingReturn>({
- color: '#000',
- type: 'circular',
- ratio: 1,
- play: () => {
- ticks.value.length = 0
- ticks.value.push('play')
- },
- failed: () => {
- ticks.value.length = 0
- ticks.value.push('failed')
- },
- clear: () => {
- ticks.value.length = 0
- ticks.value.push('clear')
- },
- destroy: () => {
- ticks.value.length = 0
- ticks.value.push('destroy')
- },
- })
-
-
- const { ctx, dimensions, updateDimensions } = useCanvas();
- const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[])=>{
- updateDimensions(element.value!)
- });
- const currentAnimation = shallowRef<AnimationManager | null>(null);
-
- // 计算动画参数
- const animationParams = computed(() : AnimationParams => {
- return {
- width: dimensions.value.width,
- height: dimensions.value.height,
- center: [dimensions.value.width / 2, dimensions.value.height / 2],
- color: state.color,
- size: state.ratio > 1 ? state.ratio : dimensions.value.size * state.ratio
- } as AnimationParams
- })
-
- const startAnimation = (type : LoadingType) => {
- currentAnimation.value?.stop();
- if (type == 'circular') {
- currentAnimation.value = createCircularAnimation(ctx.value!, animationParams.value)
- currentAnimation.value!.time = 1000 / 30
- currentAnimation.value!.start()
- return
- }
- if (type == 'spinner') {
- currentAnimation.value = createSpinnerAnimation(ctx.value!, animationParams.value)
- currentAnimation.value!.time = 1000 / 10
- currentAnimation.value!.start()
- return
- }
- if (type == 'failed') {
- currentAnimation.value = createFailedAnimation(ctx.value!, animationParams.value)
- currentAnimation.value?.start()
- return
- }
- }
-
-
- const failed = () => {
- startAnimation('failed')
- }
- const play = () => {
- startAnimation(state.type)
- }
- const clear = () => {
- currentAnimation.value?.stop();
- ctx.value?.reset();
- ctx.value?.update();
- }
- const destroy = () => {
- clear();
- resizeObserver.disconnect();
- }
-
-
- watch(animationParams, () => {
- if (['clear', 'destroy'].includes(currentTick.value)) return
- startAnimation(state.type)
- })
-
- watchEffect(() => {
- if (ctx.value == null) return
- const tick = ticks.value.pop()
- if(tick != null) {
- currentTick.value = tick
- }
- if (tick == 'play') {
- play()
- return
- }
- if (tick == 'failed') {
- failed()
- return
- }
- if (tick == 'clear') {
- clear()
- return
- }
- if (tick == 'destroy') {
- destroy()
- return
- }
- })
-
-
- watch(element, (el : UniElement | null) => {
- if (el == null) return
- resizeObserver.observe(el);
- // #ifdef APP-IOS
- setTimeout(()=>{
- updateDimensions(el)
- },50)
- // #endif
- });
- onUnmounted(destroy);
-
-
- return state
- }
|