| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- // 引入颜色处理库
- 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 // 实际绘制半径
- try {
- 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!)
- }
- }
- }
- } catch(err) {
-
- }
- }
- 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?.()
- }
- // 监听元素尺寸
- let manualCheckTimer:number|null = null;
- const getBoundingClientRect = () => {
- if(manualCheckTimer != null){
- clearTimeout(manualCheckTimer!);
- }
- 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)
- })
- })
- }
-
- const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
- getBoundingClientRect()
- });
- watchEffect(() => {
- if (element.value == null) return
- resizeObserver.observe(element.value!);
- // #ifdef APP-IOS
- // nextTick(getBoundingClientRect)
- manualCheckTimer = setTimeout(() => {
- getBoundingClientRect();
- }, 50);
- // #endif
- })
- watchEffect(() => {
- if (context.value == null) return
- if (tick.value == 'play') {
- animation?.pause()
- isPlaying = false
- cancelAnimationFrame(animationFrameId)
- startAnimation(state.type)
- }
- if (tick.value == 'failed') {
- cancelAnimationFrame(animationFrameId)
- animation?.pause()
- animation?.cancel()
- drwaFailed()
- return
- }
- if (tick.value == 'clear') {
- cancelAnimationFrame(animationFrameId)
- animation?.pause()
- animation?.cancel()
- context.value?.reset();
- context.value?.update();
- isPlaying = false
- return
- }
- if (tick.value == 'destroy') {
- cancelAnimationFrame(animationFrameId)
- animation?.pause()
- animation?.cancel()
- context.value?.reset();
- context.value?.update();
- context.value = null
- animation = null
- isPlaying = false
- return
- }
- if (tick.value == 'pause') {
- // 首次需要绘制一帧
- if(animation == null) {
- startAnimation(state.type)
- }
- cancelAnimationFrame(animationFrameId)
- isPlaying = false
- animation?.pause()
- return
- }
- })
-
- watchEffect(()=>{
- if(state.color == '') return
- // #ifdef APP-HARMONY
- isPlaying = false
- cancelAnimationFrame(animationFrameId)
- // #endif
- })
-
- return state
- }
|