7021f2fd创建于 2025年5月14日历史提交
// type UseLoadingOtions = {
// 	type: string,
// 	color: string,
// 	el: UniElement
// }
import {tinyColor} from '@/uni_modules/lime-color'
function easeInOutCubic(t : number) : number {
	return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
}

type useLoadingReturnType = {
	state : Ref<boolean>
	color : Ref<string>
	play: () => void
	failed: () => void
	clear : () => void
	destroy : () => void
}
type Point = {
	x1: number
	y1: number
	x2: number
	y2: number
}
export function useLoading(
	element : Ref<UniElement | null>, 
	type : 'circular' | 'spinner', 
	strokeColor : string, 
	ratio : number,
	immediate: boolean = false,
	) : useLoadingReturnType {
	const state = ref(false)
	const color = ref(strokeColor)
	let tick = 0 // 0 不绘制 | 1 旋转 | 2 错误
	let init = false
	let isDestroy = ref(false)
	let width = 0
	let height = 0
	let size = 0
	let x = 0
	let y = 0
	let ctx : DrawableContext | null = null
	let timer = -1
	let isClear = false;
	let drawing = false;
	const updateSize = () => {
		if (element.value == null) return
		
		const rect = element.value!.getBoundingClientRect();
		ctx = element.value!.getDrawableContext()! as DrawableContext
		width = rect.width
		height = rect.height
		size = ratio > 1 ? ratio : Math.floor(Math.min(width, height) * ratio)
		x = width / 2
		y = height / 2
	}
	const circular = () => {
		if (ctx == null) return
		let _ctx = ctx!
		let startAngle = 0;
		let endAngle = 0;
		let startSpeed = 0;
		let endSpeed = 0;
		let rotate = 0;

		// 不使用360的原因是加上rotate后,会导致闪烁
		const ARC_LENGTH = 359.5
		const PI = Math.PI / 180
		const SPEED = 0.018
		const ROTATE_INTERVAL = 0.09
		const center = size / 2
		const lineWidth = size / 10;

		function draw() {
			if(isClear) return
			_ctx.reset();
			_ctx.beginPath();
			_ctx.arc(
				x,
				y,
				center - lineWidth,
				startAngle * PI + rotate,
				endAngle * PI + rotate);
			_ctx.lineWidth = lineWidth;
			_ctx.strokeStyle = color.value;
			_ctx.stroke();

			if (endAngle < ARC_LENGTH && startAngle == 0) {
				endSpeed += SPEED
				endAngle = Math.min(ARC_LENGTH, easeInOutCubic(endSpeed) * ARC_LENGTH)
			} else if (endAngle == ARC_LENGTH && startAngle < ARC_LENGTH) {
				startSpeed += SPEED
				startAngle = Math.min(ARC_LENGTH, easeInOutCubic(startSpeed) * ARC_LENGTH);
			} else if (endAngle >= ARC_LENGTH && startAngle >= ARC_LENGTH) {
				endSpeed = 0
				startSpeed = 0
				startAngle = 0;
				endAngle = 0;
			}
			rotate += ROTATE_INTERVAL;
			_ctx.update()
			// clearTimeout(timer)
			
			timer = setTimeout(() => draw(), 24)
		}
		draw()
	}
	const spinner = () =>  {
		if (ctx == null) return
		let _ctx = ctx!
		const steps = 12;
		let step = 0;
		const lineWidth = size / 10;
		// 线长度和距离圆心距离
		const length = size / 4 - lineWidth;
		const offset = size / 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(color.value, steps)) 
		
		function draw() {
			if(tick == 0) return
			_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 sin = Math.sin(angle / 180 * Math.PI);
				const cos = Math.cos(angle / 180 * Math.PI);
				// 开始绘制
				_ctx.lineWidth = lineWidth;
				_ctx.lineCap = 'round';
				_ctx.beginPath();
				_ctx.moveTo(size / 2 + offset * cos, size / 2 + offset * sin);
				_ctx.lineTo(size / 2 + (offset + length) * cos, size / 2 + (offset + length) * sin);
				_ctx.strokeStyle = colors.value[index]
				_ctx.stroke();
			}
			step += 1
			_ctx.update()
			timer = setTimeout(() => draw(), 1000/10)
		}
		draw()
	}
	const clear = () => {
		clearTimeout(timer)
		drawing = false
		tick = 0
		if(ctx == null) return
		// ctx?.reset()
		// ctx?.update()
		setTimeout(()=>{
			ctx!.reset()
			ctx!.update()
		},1000)
		
	}
	const failed = () =>  {
		if(tick == 1) {
			drawing = false
		}
		clearTimeout(timer)
		tick = 2
		if (ctx == null || drawing) return
		let _ctx = ctx!
		const _size = size * 0.61
		const _sizeX = _size * 0.65
		const lineWidth = _size / 6;
		const lineLength = Math.ceil(Math.sqrt(Math.pow(_sizeX, 2) * 2))
		
		const startX1 = (width - _sizeX) * 0.5
		const startY = (height - _sizeX) * 0.5
		const startX2 = startX1 + _sizeX

		// 添加圆的参数
		const centerX = width / 2;
		const centerY = height / 2;
		const radius = (_size * Math.sqrt(2)) / 2 + lineWidth / 2;
		const totalSteps = 36;
		
		function generateSteps(stepsCount: number):Point[][] {
			
			const halfStepsCount = stepsCount / 2;
			const step = lineLength / halfStepsCount //Math.floor(lineLength / 18);
			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(45 * Math.PI / 180) * step * index + startX1
					const y2 = Math.cos(45 * Math.PI / 180) * 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((45 - 90) * Math.PI / 180) * step * index + startX2
					const y2 = Math.cos((45 - 90) * Math.PI / 180) * step * index + startY
					
					const start2 = {
						x1: startX2,
						y1: startY,
						x2,
						y2,
					} as Point
					sub.push(start2)
				}
				steps.push(sub)
			}	
			
			return steps
		}
		const steps = generateSteps(36);
		function draw(){
			if(steps.length == 0 || tick == 0) {
				clearTimeout(timer)
				return
			}
			const drawStep = steps.shift()!
			_ctx.reset()
			_ctx.lineWidth = lineWidth;
			_ctx.strokeStyle = color.value;
			
			// 绘制逐渐显示的圆
			_ctx.beginPath();
			_ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
			_ctx.lineWidth = lineWidth;
			_ctx.strokeStyle = color.value;
			_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()
			timer = setTimeout(() => draw(), 1000/30)
		}
		draw()
	}
	const destroy = () => {
		isDestroy.value = true;
		clear()
	}
	const play = () => {
		if(tick == 2) {
			drawing = false
		}
		if(drawing) return
		tick = 1
		if(width == 0 || height == 0) return
		if (type == 'circular') {
			circular()
		} else if (type == 'spinner') {
			spinner()
		}
		drawing = true
	}
	
	const _watch = (v:boolean) => {
		if(isDestroy.value) return
		if (v) {
			play() 
		} else {
			failed()
		}
	}
	const stopWatchState = watch(state, _watch)
	
	const ob = new UniResizeObserver((entries: UniResizeObserverEntry[])=>{
		if(isDestroy.value) return
		entries.forEach(entry => {
			if(isDestroy.value) return
			const rect = entry.target.getBoundingClientRect();
			if(rect.width > 0 && rect.height > 0) {
				updateSize();
				if(tick == 1) {
					play()
					state.value = true
				} else if(tick == 2) {
					failed()
					state.value = false
				} else if(immediate && !init) {
					_watch(state.value)
					init = true
				}
			}
		})
	})
	
	const stopWatchElement = watch(element, (el:UniElement|null) => {
		if(el == null || isDestroy.value) return
		ob.observe(el)
	})

	onUnmounted(()=>{
		stopWatchState()
		stopWatchElement()
		clear()
		ob.disconnect()
	})
	return {
		state,
		play,
		failed,
		clear,
		color,
		destroy
	} as useLoadingReturnType
}