// 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
|
|
}
|