Initial commit of akmon project
This commit is contained in:
595
uni_modules/lime-loading/index_old.uts
Normal file
595
uni_modules/lime-loading/index_old.uts
Normal file
@@ -0,0 +1,595 @@
|
||||
// 引入颜色处理库
|
||||
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' | 'pause'
|
||||
|
||||
/**
|
||||
* 加载组件配置选项
|
||||
* @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;
|
||||
pause : () => 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// 帧绘制函数
|
||||
private lastTime : number = 0;
|
||||
private isRunning : boolean = false;
|
||||
type : LoadingType;
|
||||
constructor(drawFrame : AnimationFrameHandler, type : LoadingType = 'circular') {
|
||||
this.type = type
|
||||
this.drawFrame = drawFrame
|
||||
}
|
||||
/** 启动动画循环 */
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = Date.now();
|
||||
let animate : ((task : number) => void) | null = null
|
||||
|
||||
animate = (task : number) => {
|
||||
if (!this.isRunning) return;
|
||||
if (this.isDestroyed) return;
|
||||
const delta = Date.now() - this.lastTime;
|
||||
if (delta >= this.time || delta < 10) {
|
||||
const shouldContinue : boolean = this.drawFrame();
|
||||
this.lastTime = Date.now()
|
||||
if (!shouldContinue) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.timer = requestAnimationFrame(animate!);
|
||||
};
|
||||
|
||||
animate(Date.now());
|
||||
}
|
||||
/** 停止动画并清理资源 */
|
||||
stop() {
|
||||
cancelAnimationFrame(this.timer)
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
cancelAnimationFrame(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 工具函数 =====================
|
||||
/**
|
||||
* 缓动函数 - 三次缓入缓出
|
||||
* @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();
|
||||
// // 鸿蒙尺寸为0时 再次尺寸变化会不渲染
|
||||
// if(rect.width == 0) return
|
||||
// if(ctx.value == null) {
|
||||
// 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);
|
||||
el.getBoundingClientRectAsync()?.then(rect => {
|
||||
if (rect.width == 0 || rect.height == 0) return
|
||||
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; // 线宽
|
||||
// #ifdef APP-HARMONY
|
||||
const length = size / 3.4 - lineWidth; // 线长
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
const length = size / 3.6 - lineWidth; // 线长
|
||||
// #endif
|
||||
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 // 颜色索引
|
||||
// const index = (i + step) % steps;
|
||||
// console.log('index', index)
|
||||
// 计算线段坐标
|
||||
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
|
||||
step = (step + 1) % steps; // 限制step范围
|
||||
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 currentAnimation = shallowRef<AnimationManager | null>(null);
|
||||
const state = reactive<UseLoadingReturn>({
|
||||
color: '#000',
|
||||
type: 'circular',
|
||||
ratio: 1,
|
||||
play: () => {
|
||||
// if (currentTick.value == 'pause' && currentAnimation.value != null) {
|
||||
// // 从暂停状态恢复时直接启动动画管理器
|
||||
// currentAnimation.value?.start()
|
||||
// } else {
|
||||
// // 首次播放或类型变化时重新初始化
|
||||
// ticks.value.length = 0
|
||||
// ticks.value.push('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')
|
||||
},
|
||||
pause: () => {
|
||||
ticks.value.push('pause')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const { ctx, dimensions, updateDimensions } = useCanvas();
|
||||
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
|
||||
updateDimensions(element.value!)
|
||||
});
|
||||
|
||||
|
||||
// 计算动画参数
|
||||
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?.pause();
|
||||
if (currentAnimation.value?.type == type) {
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
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', 'pause'].includes(currentTick.value)) return
|
||||
startAnimation(state.type)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (ctx.value == null) return
|
||||
|
||||
const tick = ticks.value.pop()
|
||||
if (ticks.value.length > 0) { }
|
||||
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
|
||||
}
|
||||
if (tick == 'pause') {
|
||||
if (currentAnimation.value == null) {
|
||||
play()
|
||||
}
|
||||
currentAnimation.value?.pause();
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value == null) return
|
||||
resizeObserver.observe(element.value!);
|
||||
// #ifdef APP-IOS || APP-HARMONY
|
||||
// APP-HARMONY 在下拉组件中使用居中会影响绘制 故先获取一次
|
||||
updateDimensions(element.value!)
|
||||
// #endif
|
||||
})
|
||||
|
||||
onUnmounted(destroy);
|
||||
|
||||
|
||||
return state
|
||||
}
|
||||
Reference in New Issue
Block a user