330 lines
7.6 KiB
Plaintext
330 lines
7.6 KiB
Plaintext
// 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
|
||
} |