index.uts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. // 引入颜色处理库
  2. import { tinyColor } from '@/uni_modules/lime-color';
  3. /**
  4. * 操作类型
  5. * play: 开始动画
  6. * failed: 显示失败状态
  7. * clear: 清除动画
  8. * destroy: 销毁实例
  9. */
  10. export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
  11. /**
  12. * 加载动画类型
  13. * circular: 环形加载动画
  14. * spinner: 旋转器加载动画
  15. * failed: 失败状态动画
  16. */
  17. export type LoadingType = 'circular' | 'spinner' | 'failed';
  18. /**
  19. * 加载组件返回接口
  20. */
  21. export type UseLoadingReturn = {
  22. ratio : 1;
  23. type : LoadingType;
  24. mode : 'raf' | 'animate'; //
  25. color : string;//Ref<string>;
  26. play : () => void;
  27. failed : () => void;
  28. clear : () => void;
  29. destroy : () => void;
  30. pause : () => void;
  31. }
  32. /**
  33. * 计算圆周上指定角度的点的坐标
  34. * @param centerX 圆心的 X 坐标
  35. * @param centerY 圆心的 Y 坐标
  36. * @param radius 圆的半径
  37. * @param angleDegrees 角度(以度为单位)
  38. * @returns 包含 X 和 Y 坐标的对象
  39. */
  40. function getPointOnCircle(
  41. centerX : number,
  42. centerY : number,
  43. radius : number,
  44. angleDegrees : number
  45. ) : number[] {
  46. // 将角度转换为弧度
  47. const angleRadians = (angleDegrees * Math.PI) / 180;
  48. // 计算点的 X 和 Y 坐标
  49. const x = centerX + radius * Math.cos(angleRadians);
  50. const y = centerY + radius * Math.sin(angleRadians);
  51. return [x, y]
  52. }
  53. export function useLoading(element : Ref<UniElement | null>) : UseLoadingReturn {
  54. const tick = ref<TickType>('pause')
  55. const state = reactive<UseLoadingReturn>({
  56. color: '#000',
  57. type: 'circular',
  58. ratio: 1,
  59. mode: 'raf',
  60. play: () => {
  61. tick.value = 'play'
  62. },
  63. failed: () => {
  64. tick.value = 'failed'
  65. },
  66. clear: () => {
  67. tick.value = 'clear'
  68. },
  69. destroy: () => {
  70. tick.value = 'destroy'
  71. },
  72. pause: () => {
  73. tick.value = 'pause'
  74. }
  75. })
  76. const context = shallowRef<DrawableContext | null>(null);
  77. // let ctx:DrawableContext|null = null
  78. // let rotation = 0
  79. let isPlaying = false
  80. let canvasWidth = ref(0)
  81. let canvasHeight = ref(0)
  82. let canvasSize = ref(0)
  83. let animationFrameId = -1
  84. let animation : UniAnimation | null = null
  85. let drawFrame : (() => void) | null = null
  86. const size = computed(() : number => state.ratio > 1 ? state.ratio : canvasSize.value * state.ratio)
  87. // 绘制圆形加载
  88. const drawCircular = () => {
  89. let startAngle = 0; // 起始角度
  90. let endAngle = 0; // 结束角度
  91. let rotate = 0; // 旋转角度
  92. // const ctx = context.value!
  93. // 动画参数配置
  94. const MIN_ANGLE = 5; // 最小保持角度
  95. const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
  96. const PI = Math.PI / 180 // 角度转弧度系数
  97. const SPEED = 0.018 / 4 // 动画速度
  98. const ROTATE_INTERVAL = 0.09 / 4 // 旋转增量
  99. const lineWidth = size.value / 10; // 线宽计算
  100. const x = canvasWidth.value / 2 // 中心点X
  101. const y = canvasHeight.value / 2 // 中心点Y
  102. const radius = size.value / 2 - lineWidth // 实际绘制半径
  103. try {
  104. drawFrame = () => {
  105. if (context.value == null || !isPlaying) return
  106. let ctx = context.value!
  107. // console.log('radius', radius, size.value)
  108. ctx.reset();
  109. // 绘制圆弧
  110. ctx.beginPath();
  111. ctx.arc(
  112. x,
  113. y,
  114. radius,
  115. startAngle * PI + rotate,
  116. endAngle * PI + rotate
  117. );
  118. ctx.lineWidth = lineWidth;
  119. ctx.strokeStyle = state.color;
  120. ctx.stroke();
  121. // 角度更新逻辑
  122. if (endAngle < ARC_LENGTH) {
  123. endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
  124. } else if (startAngle < ARC_LENGTH) {
  125. startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
  126. } else {
  127. // 重置时保留最小可见角度
  128. startAngle = 0;
  129. endAngle = MIN_ANGLE;
  130. }
  131. ctx.update()
  132. if (state.mode == 'raf') {
  133. rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
  134. if (isPlaying && drawFrame != null) {
  135. animationFrameId = requestAnimationFrame(drawFrame!)
  136. }
  137. }
  138. }
  139. } catch(err) {
  140. }
  141. }
  142. let lastTime = Date.now();
  143. const drawSpinner = () => {
  144. const steps = 12; // 旋转线条数量
  145. // const size = state.ratio > 1 ? state.ratio : canvasSize.value
  146. const lineWidth = size.value / 10; // 线宽
  147. const x = canvasWidth.value / 2 // 中心坐标
  148. const y = canvasHeight.value / 2
  149. let step = 0; // 当前步数
  150. // #ifdef APP-HARMONY
  151. const length = size.value / 3.4 - lineWidth; // 线长
  152. // #endif
  153. // #ifndef APP-HARMONY
  154. const length = size.value / 3.6 - lineWidth; // 线长
  155. // #endif
  156. const offset = size.value / 4; // 距中心偏移
  157. /** 生成颜色渐变数组 */
  158. function generateColorGradient(hex : string, steps : number) : string[] {
  159. const colors : string[] = []
  160. const _color = tinyColor(hex)
  161. for (let i = 1; i <= steps; i++) {
  162. _color.setAlpha(i / steps);
  163. colors.push(_color.toRgbString());
  164. }
  165. return colors
  166. }
  167. // 计算颜色渐变
  168. let colors = computed(() : string[] => generateColorGradient(state.color, steps))
  169. /** 帧绘制函数 */
  170. drawFrame = () => {
  171. if (context.value == null || !isPlaying) return
  172. const delta = Date.now() - lastTime;
  173. if (delta >= 1000 / 10) {
  174. lastTime = Date.now();
  175. let ctx = context.value!
  176. ctx.reset();
  177. for (let i = 0; i < steps; i++) {
  178. const stepAngle = 360 / steps; // 单步角度
  179. const angle = stepAngle * i; // 当前角度
  180. const index = (steps + i - step) % steps // 颜色索引
  181. // 计算线段坐标
  182. const radian = angle * Math.PI / 180;
  183. const cos = Math.cos(radian);
  184. const sin = Math.sin(radian);
  185. // 绘制线段
  186. ctx.beginPath();
  187. ctx.moveTo(x + offset * cos, y + offset * sin);
  188. ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
  189. ctx.lineWidth = lineWidth;
  190. ctx.lineCap = 'round';
  191. ctx.strokeStyle = colors.value[index];
  192. ctx.stroke();
  193. }
  194. ctx.update()
  195. if(state.mode == 'raf') {
  196. // step += 1
  197. step = (step + 1) % steps; // 限制step范围
  198. }
  199. }
  200. if (state.mode == 'raf') {
  201. if (isPlaying && drawFrame != null) {
  202. animationFrameId = requestAnimationFrame(drawFrame!)
  203. }
  204. }
  205. }
  206. }
  207. const drwaFailed = () => {
  208. if (context.value == null) return
  209. let ctx = context.value!
  210. // const size = state.ratio > 1 ? state.ratio : canvasSize.value
  211. const innerSize = size.value * 0.8 // 内圈尺寸
  212. const lineWidth = innerSize / 10; // 线宽
  213. const lineLength = (size.value - lineWidth) / 2 // X长度
  214. const centerX = canvasWidth.value / 2;
  215. const centerY = canvasHeight.value / 2;
  216. const radius = (size.value - lineWidth) / 2
  217. const angleRadians1 = 45 * Math.PI / 180
  218. const angleRadians2 = (45 - 90) * Math.PI / 180
  219. ctx.reset()
  220. ctx.lineWidth = lineWidth;
  221. ctx.strokeStyle = state.color;
  222. // 绘制逐渐显示的圆
  223. ctx.beginPath();
  224. ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
  225. ctx.lineWidth = lineWidth;
  226. ctx.strokeStyle = state.color;
  227. ctx.stroke();
  228. const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
  229. const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
  230. const x2 = Math.sin(angleRadians1) * lineLength + startX1
  231. const y2 = Math.cos(angleRadians1) * lineLength + startY
  232. ctx.beginPath();
  233. ctx.moveTo(startX1, startY)
  234. ctx.lineTo(x2, y2)
  235. ctx.stroke();
  236. const x3 = Math.sin(angleRadians2) * lineLength + startX2
  237. const y3 = Math.cos(angleRadians2) * lineLength + startY
  238. ctx.beginPath();
  239. ctx.moveTo(startX2, startY)
  240. ctx.lineTo(x3, y3)
  241. ctx.stroke();
  242. ctx.update()
  243. }
  244. let currentType : LoadingType | null = null
  245. const useMode = () => {
  246. if (state.mode != 'raf') {
  247. const keyframes = [{ transform: 'rotate(0)' }, { transform: 'rotate(360)' }]
  248. animation = element.value!.animate(keyframes, {
  249. duration: 80000,
  250. easing: 'linear',
  251. // fill: 'forwards',
  252. iterations: Infinity
  253. })
  254. }
  255. }
  256. const startAnimation = (type : string) => {
  257. if (context.value == null || element.value == null) return
  258. animation?.pause()
  259. if (currentType == type) {
  260. isPlaying = true
  261. animation?.play()
  262. drawFrame?.()
  263. return
  264. }
  265. if (type == 'circular') {
  266. currentType = 'circular'
  267. drawCircular()
  268. useMode()
  269. }
  270. if (type == 'spinner') {
  271. currentType = 'spinner'
  272. drawSpinner()
  273. useMode()
  274. }
  275. isPlaying = true
  276. drawFrame?.()
  277. }
  278. // 监听元素尺寸
  279. let manualCheckTimer:number|null = null;
  280. const getBoundingClientRect = () => {
  281. if(manualCheckTimer != null){
  282. clearTimeout(manualCheckTimer!);
  283. }
  284. requestAnimationFrame(()=> {
  285. element.value?.getBoundingClientRectAsync()?.then(rect => {
  286. if (rect.width == 0 || rect.height == 0) return
  287. context.value = element.value!.getDrawableContext() as DrawableContext;
  288. canvasWidth.value = rect.width;
  289. canvasHeight.value = rect.height;
  290. canvasSize.value = Math.min(rect.width, rect.height);
  291. // startAnimation(state.type)
  292. })
  293. })
  294. }
  295. const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
  296. getBoundingClientRect()
  297. });
  298. watchEffect(() => {
  299. if (element.value == null) return
  300. resizeObserver.observe(element.value!);
  301. // #ifdef APP-IOS
  302. // nextTick(getBoundingClientRect)
  303. manualCheckTimer = setTimeout(() => {
  304. getBoundingClientRect();
  305. }, 50);
  306. // #endif
  307. })
  308. watchEffect(() => {
  309. if (context.value == null) return
  310. if (tick.value == 'play') {
  311. animation?.pause()
  312. isPlaying = false
  313. cancelAnimationFrame(animationFrameId)
  314. startAnimation(state.type)
  315. }
  316. if (tick.value == 'failed') {
  317. cancelAnimationFrame(animationFrameId)
  318. animation?.pause()
  319. animation?.cancel()
  320. drwaFailed()
  321. return
  322. }
  323. if (tick.value == 'clear') {
  324. cancelAnimationFrame(animationFrameId)
  325. animation?.pause()
  326. animation?.cancel()
  327. context.value?.reset();
  328. context.value?.update();
  329. isPlaying = false
  330. return
  331. }
  332. if (tick.value == 'destroy') {
  333. cancelAnimationFrame(animationFrameId)
  334. animation?.pause()
  335. animation?.cancel()
  336. context.value?.reset();
  337. context.value?.update();
  338. context.value = null
  339. animation = null
  340. isPlaying = false
  341. return
  342. }
  343. if (tick.value == 'pause') {
  344. // 首次需要绘制一帧
  345. if(animation == null) {
  346. startAnimation(state.type)
  347. }
  348. cancelAnimationFrame(animationFrameId)
  349. isPlaying = false
  350. animation?.pause()
  351. return
  352. }
  353. })
  354. watchEffect(()=>{
  355. if(state.color == '') return
  356. // #ifdef APP-HARMONY
  357. isPlaying = false
  358. cancelAnimationFrame(animationFrameId)
  359. // #endif
  360. })
  361. return state
  362. }