wired-elements 的 wired-spinner 手绘风加载指示器:API 详解与源码级动画原理
【免费下载链接】wired-elementsCollection of custom elements that appear hand drawn. Great for wireframes or a fun look.项目地址: https://gitcode.com/gh_mirrors/wi/wired-elements
wired-spinner 是 wired-elements 组件库中一个"手绘素描风"的加载指示器(loading spinner),用于展示进度或任务进行中的状态。本文以 docs/wired-spinner.md 为主线,结合 src/wired-spinner.ts 源码与 examples/spinner.html 示例,完整讲解其安装、HTML 用法、两个核心属性(spinning、duration)以及 CSS 着色方式,并深入剖析其基于 SVG 与requestAnimationFrame的动画实现原理,帮助你既能快速上手,也能理解其内部机制。
组件简介
wired-spinner是一个继承自WiredBase的 Web Component(自定义元素),渲染为一个 76×76 的 SVG 画布:外圈是一个粗糙手绘风格的椭圆描边,内部是一个通过斜线填充(hachure)绘制的小"旋钮"(knob),旋钮沿椭圆轨道匀速旋转,形成加载动画。它的定位是给线框图(wireframe)或趣味界面提供一个符合整体手绘风格的加载反馈。
完整的 wired-elements 组件集合与在线演示可在 wiredjs.com 查看;本组件与库内其他元素(如 wired-button、wired-progress)配合使用效果最佳。
安装与引入
方式一:npm 安装(推荐)
在你的 JavaScript 项目中安装 wired-elements 包:
npm i wired-elements然后在代码中按需导入模块(两种写法等价,均从包名导出):
import { WiredSpinner } from 'wired-elements'; // 或 import { WiredSpinner } from 'wired-elements/lib/wired-spinner.js';第一种写法利用 src/wired-elements.ts 中的统一出口(export * from './wired-spinner')批量导入;第二种写法直接指向单文件模块,适合只想引入单个组件、减小打包体积的场景。当前仓库package.json声明版本为3.0.0-rc.7,采用 ESM("type": "module"),依赖lit(v2 系列)与roughjs(v4.3.1),请确保你的构建工具支持 ES Modules 与自定义元素。
方式二:CDN 直接加载
不经过构建工具时,可以在 HTML 中通过<script type="module">直接加载:
<script type="module" src="https://unpkg.com/wired-elements/lib/wired-spinner.js?module"></script>方式三:本地构建后引用
仓库根目录执行npm run build(即rm -rf lib && tsc,见 package.json)后,会生成lib/目录,示例页面 examples/spinner.html 便是这样引用本地产物的:
<script type="module" src="../lib/wired-spinner.js"></script>基本用法
在 HTML 中直接书写<wired-spinner>标签即可:
<wired-spinner id="sp"></wired-spinner> <wired-spinner spinning duration="1000"></wired-spinner>- 第一个 spinner 未设置任何属性,默认静止(
spinning默认为false),只显示一个手绘椭圆环与静态旋钮。 - 第二个 spinner 设置了
spinning布尔属性(HTML 中布尔属性只要出现即为true)与duration="1000",因此会以每圈 1000ms 的速度旋转。
交互示例:点击按钮切换旋转状态
参考 examples/spinner.html,可以用一个按钮动态切换spinning属性:
<wired-spinner id="sp"></wired-spinner> <wired-button>Toggle</wired-button> <script> document.querySelector('wired-button').addEventListener('click', () => { const sp = document.getElementById('sp'); sp.spinning = !sp.spinning; }); </script>由于spinning是响应式属性(见下文属性表),直接通过 JavaScript 修改sp.spinning即可驱动动画启停,无需手动操作 DOM。
属性(Properties)
wired-spinner 只有两个对外属性,均定义在 src/wired-spinner.ts:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
spinning | Boolean | false | 是否处于旋转状态。为true时启动动画,为false时停止。 |
duration | Number | 1500 | 完成一整圈旋转所需的时间(毫秒)。 |
spinning 的响应式行为
源码中通过@property({ type: Boolean }) spinning = false;声明,并在生命周期钩子updated()中监听:
updated() { super.updated(); if (this.spinning) { this.startSpinner(); } else { this.stopSpinner(); } }也就是说,只要spinning变为true,组件立即调用startSpinner()重置计时并从第 0 帧开始旋转;变为false时调用stopSpinner()取消动画帧。因此同一时刻只会有一个动画循环在运行,反复切换也不会叠加多个定时器(startSpinner()内部先调用stopSpinner()做防重入处理)。
duration 的作用边界
duration表示旋钮走完一整圈(旋转 360°)的毫秒数,数值越小转速越快。需要说明的是:它只影响旋转速度,不改变旋钮的轨道半径、组件尺寸或椭圆形状;即使duration设置为 0,单圈耗时也会被钳制在至少一帧(value通过Math.min(1, ...)截断)内完成。从源码看,duration未设置上下限校验,请按实际需要传入合理的正整数。
样式定制
wired-spinner 的配色非常简单:修改元素自身的color样式即可改变其颜色。
wired-spinner { color: red; /* 手绘描边与旋钮将变为红色 */ } wired-spinner.custom { color: #2c7be5; /* 也可以按类名单独指定 */ }这一机制依赖于 src/wired-base.ts 中的BaseCSS(path { stroke: currentColor; ... })以及 src/wired-spinner.ts 内的组件样式。组件内所有path的描边颜色均取currentColor,因此只需覆盖color即可整体换色:
- 外圈椭圆描边:
stroke-opacity: 0.65、stroke-width: 1.5,营造淡而细腻的手绘笔触; - 旋钮(
.knob类):stroke-width: 2.8、stroke-opacity: 1,比外圈更粗更实,突出旋转主体; - 组件宿主:
display: inline-block; position: relative,便于在文档流中内联布局。
color支持任何合法的 CSS 颜色值,例如#ff6600、rgb(...)、var(--primary)等。除此之外组件不暴露尺寸属性,画布固定为 76×76(见 src/wired-spinner.ts 的canvasSize()返回值),如需缩放可对宿主元素使用 CSStransform: scale(...)。
源码级原理解析
1. 绘制阶段:椭圆环 + 斜线填充旋钮
组件渲染逻辑为render()返回一个空<svg>,真正的内容在draw()中绘制(src/wired-spinner.ts):
protected draw(svg: SVGSVGElement, size: Point) { ellipse(svg, size[0] / 2, size[1] / 2, Math.floor(size[0] * 0.8), Math.floor(0.8 * size[1]), this.seed); this.knob = hachureEllipseFill(0, 0, 20, 20, this.seed); this.knob.classList.add('knob'); svg.appendChild(this.knob); this.updateCursor(); }- 外圈:调用 src/wired-lib.ts 的
ellipse(),圆心在 (38, 38),宽高约为画布的 80%(约 60.8px),通过 roughjs 生成带随机抖动的"手绘感"椭圆路径(内部options()使用roughness: 1、bowing: 0.85、maxRandomnessOffset: 2等参数,见 src/wired-lib.ts)。 - 旋钮:调用
hachureEllipseFill(0, 0, 20, 20, this.seed)(src/wired-lib.ts)生成一个 20×20、以斜线(hachure)填充的小椭圆,挂上.knob类并追加到 SVG。 - seed:每个实例在构造时都会生成一个随机
seed(Math.floor(Math.random() * 2 ** 31),见 src/wired-base.ts),保证每个 spinner 的笔触抖动、填充疏密都是独一无二的"手绘痕迹",这也是所有 wired 元素"每次都不一样"的原因。
draw()由WiredBase.wiredRender()统一驱动(src/wired-base.ts):当尺寸未变化时不会重复重绘,首次渲染后会给宿主添加wired-rendered类,把透明度从0过渡到1(BaseCSS中:host { opacity: 0 }→:host(.wired-rendered) { opacity: 1 }),实现柔和的淡入效果。
2. 动画阶段:requestAnimationFrame 驱动
动画的核心在startSpinner()/stopSpinner()/nextTick()/tick()这组私有方法(src/wired-spinner.ts):
private nextTick() { this.frame = window.requestAnimationFrame((t) => this.tick(t)); } private tick(t: number) { if (this.spinning) { if (!this.timerstart) this.timerstart = t; this.value = Math.min(1, (t - this.timerstart) / this.duration); this.updateCursor(); if (this.value >= 1) { this.value = 0; this.timerstart = 0; } this.nextTick(); } else { this.frame = 0; } }运行逻辑:
nextTick()通过window.requestAnimationFrame注册下一帧回调,并把句柄存入this.frame;tick(t)收到浏览器提供的时间戳t,以首次回调时间作为timerstart,用(t - timerstart) / duration计算本圈进度value(范围 0~1);updateCursor()根据value计算旋钮的新位置;- 当
value达到 1 时归零并重置计时起点,开启下一圈,从而实现无缝循环; - 只要
spinning仍为true就继续注册下一帧;一旦为false,循环自然终止(this.frame = 0)。
stopSpinner()则会立即调用window.cancelAnimationFrame(this.frame)取消尚未执行的帧,保证停止响应即时生效。
3. 旋钮定位:极坐标圆周运动
updateCursor()(src/wired-spinner.ts)用极坐标把旋钮放到椭圆轨道上:
const position: Point = [ Math.round(38 + 25 * Math.cos(this.value * Math.PI * 2)), Math.round(38 + 25 * Math.sin(this.value * Math.PI * 2)) ]; this.knob.style.transform = `translate3d(${position[0]}px, ${position[1]}px, 0) rotateZ(${Math.round(this.value * 360 * 2)}deg)`;- 圆心 (38, 38),轨道半径 25px,
value从 0 到 1 对应角度 0 到 2π,Math.cos/Math.sin给出圆周上的坐标; translate3d使用 GPU 合成层,旋转过程顺滑;同时叠加rotateZ(value * 720)让旋钮自身每圈额外自转 720°(两圈),强化"滚动"的视觉动感;- 动画是纯 CSS
transform更新,不触发回流/重排,性能友好。
4. 与 wired-elements 体系的协作
wired-spinner通过@customElement('wired-spinner')注册(src/wired-spinner.ts),并与其他组件一同从 src/wired-elements.ts 统一导出。它复用了WiredBase的canvasSize/draw抽象与seed机制,是理解整个 wired 手绘体系(roughjs 绘制 + lit 响应式 + 随机 seed)的最佳入门组件之一;同类的进度类组件还有 wired-progress 与 wired-progress-ring,可对比阅读。
常见问题与提示
- 为什么我的 spinner 不动?默认
spinning为false,请确认已设置spinning属性或通过 JS 将sp.spinning置为true;同时确认脚本为type="module"且组件已正确加载(未加载成功时自定义元素不会被注册)。 - 如何调整速度?修改
duration属性即可,单位为毫秒,默认 1500ms/圈;duration="1000"比默认快约 1/3。 - 如何换颜色?设置
colorCSS 属性,例如wired-spinner { color: tomato; }。 - 如何停止动画?将
spinning置为false,组件会通过cancelAnimationFrame立即停止,不会在后台空转消耗资源。
结语
wired-spinner以极简的 API(两个属性 + 一个颜色样式)提供了与 wired-elements 手绘风格完全统一的加载指示能力:外圈手绘椭圆 + 斜线填充旋钮 + 圆周旋转动画。通过阅读 src/wired-spinner.ts 源码,你还能复用其"极坐标定位 + requestAnimationFrame 循环 + CSS transform 更新"的动画范式,为自己的组件实现类似的轻量动效。无论是快速搭建线框图原型,还是为趣味界面补充加载反馈,它都是一个开箱即用、风格统一的选择。
【免费下载链接】wired-elementsCollection of custom elements that appear hand drawn. Great for wireframes or a fun look.项目地址: https://gitcode.com/gh_mirrors/wi/wired-elements
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考