@xingwangzhe/tags-cloud 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 王兴家
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @xingwangzhe/tags-cloud
2
+
3
+ [中文文档](README_CN.md) | [English](#)
4
+
5
+ > Pure math 3D tag cloud engine
6
+
7
+ ## Features
8
+
9
+ - **Zero DOM overhead** — pure math output, render via callback
10
+ - **Fibonacci sphere distribution** — tags evenly placed on 3D sphere
11
+ - **Arcball interaction** — drag to rotate with quaternion-based Shoemake arcball
12
+ - **Auto-spin** — configurable per-axis spin (X/Y) with independent speed and direction
13
+ - **TypeScript** — fully typed
14
+ - **~2.3KB** gzipped
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ bun add @xingwangzhe/tags-cloud
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { TagCloud } from "@xingwangzhe/tags-cloud";
26
+
27
+ // Zero config — auto-creates Canvas and renders text
28
+ new TagCloud(document.getElementById("cloud"), {
29
+ tags: ["TypeScript", "Canvas", "3D"],
30
+ radius: 300,
31
+ });
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### `new TagCloud(container, options)`
37
+
38
+ | Option | Type | Default | Description |
39
+ | ----------------- | ---------------- | --------------- | ------------------------------- |
40
+ | `tags` | `string[]` | — | Tag text list |
41
+ | `radius` | `number` | `300` | Sphere radius (px) |
42
+ | `spinY` | `number` | `0` | Y-axis spin: +right -left 0=off |
43
+ | `spinX` | `number` | `0` | X-axis spin: +down -up 0=off |
44
+ | `reverse` | `boolean` | `false` | Reverse both drag axes |
45
+ | `reverseX` | `boolean` | `false` | Reverse X-axis drag only |
46
+ | `reverseY` | `boolean` | `false` | Reverse Y-axis drag only |
47
+ | `inertiaDecay` | `number` | `0.96` | Inertia decay per frame |
48
+ | `dragSensitivity` | `number` | `3` | Drag sensitivity multiplier |
49
+ | `fontFamily` | `string` | `system-ui` | Font family |
50
+ | `fontSize` | `number` | `14` | Base font size (px) |
51
+ | `color` | `string` | `#fff` | Text color |
52
+ | `onRender` | `(tags) => void` | built-in Canvas | Custom render callback |
53
+
54
+ ### Instance Methods
55
+
56
+ ```ts
57
+ cloud.setTags(["new", "tags"]); // Update tags
58
+ cloud.pause(); // Pause
59
+ cloud.resume(); // Resume
60
+ cloud.destroy(); // Destroy (cleanup events + rAF)
61
+ ```
62
+
63
+ ## Development
64
+
65
+ ```bash
66
+ bun install
67
+ bun run build # vite build
68
+ bun run lint # oxlint
69
+ bun run fmt # oxfmt
70
+ ```
71
+
72
+ ## Credits
73
+
74
+ Core algorithm ported from [cong-min/TagCloud](https://github.com/cong-min/TagCloud)
75
+
76
+ ## License
77
+
78
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,239 @@
1
+ //#region src/core/distribution.ts
2
+ function e(e, t) {
3
+ let n = [];
4
+ for (let r = 0; r < e; r++) {
5
+ let i = Math.acos(-1 + (2 * r + 1) / e), a = Math.sqrt(e * Math.PI) * i;
6
+ n.push({
7
+ x: t * Math.cos(a) * Math.sin(i),
8
+ y: t * Math.sin(a) * Math.sin(i),
9
+ z: t * Math.cos(i)
10
+ });
11
+ }
12
+ return n;
13
+ }
14
+ //#endregion
15
+ //#region src/TagCloud.ts
16
+ var t = {
17
+ radius: 300,
18
+ spinY: 0,
19
+ spinX: 0,
20
+ reverse: !1,
21
+ reverseX: !1,
22
+ reverseY: !1,
23
+ inertiaDecay: .96,
24
+ dragSensitivity: 3,
25
+ fontFamily: "system-ui, sans-serif",
26
+ fontSize: 14,
27
+ color: "#ffffff"
28
+ }, n = class {
29
+ #e;
30
+ #t = [];
31
+ #n;
32
+ #r;
33
+ #i = {
34
+ w: 1,
35
+ x: 0,
36
+ y: 0,
37
+ z: 0
38
+ };
39
+ #a = {
40
+ w: 1,
41
+ x: 0,
42
+ y: 0,
43
+ z: 0
44
+ };
45
+ #o = 0;
46
+ #s = 0;
47
+ #c = !1;
48
+ #l = !1;
49
+ #u = {
50
+ x: 0,
51
+ y: 0,
52
+ z: 0
53
+ };
54
+ #d = 0;
55
+ #f;
56
+ #p;
57
+ #m;
58
+ #h;
59
+ constructor(e, n) {
60
+ this.#f = e, this.#e = {
61
+ ...t,
62
+ ...n
63
+ }, this.#n = this.#e.radius, this.#r = 2 * this.#n, this.#e.onRender || (this.#e.onRender = this.#g), this.#v(this.#e.tags), this.#y(), this.#x();
64
+ }
65
+ #g = (e) => {
66
+ if (!this.#m) {
67
+ let e = document.createElement("canvas");
68
+ e.style.width = "100%", e.style.height = "100%", this.#f.appendChild(e), this.#m = e, this.#h = e.getContext("2d"), this.#_();
69
+ }
70
+ let { width: t, height: n } = this.#m.getBoundingClientRect(), r = this.#h;
71
+ r.clearRect(0, 0, t, n);
72
+ let { fontFamily: i, fontSize: a, color: o } = this.#e;
73
+ for (let t of e) r.save(), r.globalAlpha = t.alpha, r.font = `${a + t.scale * 5}px ${i}`, r.fillStyle = o, r.textAlign = "center", r.textBaseline = "middle", r.fillText(t.text, t.x, t.y), r.restore();
74
+ };
75
+ #_() {
76
+ let e = this.#m;
77
+ if (!e) return;
78
+ let t = window.devicePixelRatio || 1, { width: n, height: r } = e.getBoundingClientRect();
79
+ e.width = n * t, e.height = r * t, this.#h.setTransform(t, 0, 0, t, 0, 0);
80
+ }
81
+ setTags(e) {
82
+ this.#v(e);
83
+ }
84
+ pause() {
85
+ this.#c = !0;
86
+ }
87
+ resume() {
88
+ this.#c = !1;
89
+ }
90
+ destroy() {
91
+ cancelAnimationFrame(this.#d);
92
+ let e = this.#p;
93
+ this.#f.removeEventListener("pointerdown", e.down), window.removeEventListener("pointermove", e.move), window.removeEventListener("pointerup", e.up), this.#m && this.#m.remove();
94
+ }
95
+ #v(t) {
96
+ let n = 1.5 * this.#n, r = e(t.length, n / 2);
97
+ this.#t = r.map((e, n) => ({
98
+ ...e,
99
+ text: t[n]
100
+ }));
101
+ }
102
+ #y() {
103
+ this.#f.style.cursor = "grab";
104
+ let e = () => this.#f.getBoundingClientRect();
105
+ this.#p = {
106
+ down: ((t) => {
107
+ this.#l = !0, this.#f.style.cursor = "grabbing", this.#a = { ...this.#i };
108
+ let n = e();
109
+ this.#u = this.#b(t.clientX - n.left, t.clientY - n.top, n.width, n.height), this.#o = 0, this.#s = 0;
110
+ }),
111
+ move: ((t) => {
112
+ if (!this.#l) return;
113
+ let n = e(), r = this.#b(t.clientX - n.left, t.clientY - n.top, n.width, n.height), i = this.#u, a = i.x * r.x + i.y * r.y + i.z * r.z, o = this.#e.reverse || this.#e.reverseX ? -1 : 1, s = this.#e.reverse || this.#e.reverseY ? -1 : 1, c = {
114
+ w: 1 + a,
115
+ x: (i.y * r.z - i.z * r.y) * o,
116
+ y: (i.x * r.z - i.z * r.x) * s,
117
+ z: (i.x * r.y - i.y * r.x) * o * s
118
+ }, l = Math.sqrt(c.w ** 2 + c.x ** 2 + c.y ** 2 + c.z ** 2);
119
+ c.w /= l, c.x /= l, c.y /= l, c.z /= l;
120
+ let u = this.#a;
121
+ this.#i = {
122
+ w: c.w * u.w - c.x * u.x - c.y * u.y - c.z * u.z,
123
+ x: c.w * u.x + c.x * u.w + c.y * u.z - c.z * u.y,
124
+ y: c.w * u.y - c.x * u.z + c.y * u.w + c.z * u.x,
125
+ z: c.w * u.z + c.x * u.y - c.y * u.x + c.z * u.w
126
+ };
127
+ let d = this.#e.dragSensitivity;
128
+ this.#o = c.y / l * d, this.#s = c.x / l * d;
129
+ }),
130
+ up: () => {
131
+ this.#l = !1, this.#f.style.cursor = "grab";
132
+ }
133
+ }, this.#f.addEventListener("pointerdown", this.#p.down), window.addEventListener("pointermove", this.#p.move), window.addEventListener("pointerup", this.#p.up);
134
+ }
135
+ #b(e, t, n, r) {
136
+ let i = e / n * 2 - 1, a = -(t / r * 2 - 1), o = i * i + a * a;
137
+ if (o > 1) {
138
+ let e = 1 / Math.sqrt(o);
139
+ return {
140
+ x: i * e,
141
+ y: a * e,
142
+ z: 0
143
+ };
144
+ }
145
+ return {
146
+ x: i,
147
+ y: a,
148
+ z: Math.sqrt(1 - o)
149
+ };
150
+ }
151
+ #x = () => {
152
+ this.#c || this.#w(), this.#d = requestAnimationFrame(this.#x);
153
+ };
154
+ #S(e) {
155
+ let t = e * Math.PI / 360, n = {
156
+ w: Math.cos(t),
157
+ x: 0,
158
+ y: Math.sin(t),
159
+ z: 0
160
+ }, r = this.#i;
161
+ this.#i = {
162
+ w: n.w * r.w - n.y * r.y,
163
+ x: n.w * r.x + n.y * r.z,
164
+ y: n.w * r.y + n.y * r.w,
165
+ z: n.w * r.z - n.y * r.x
166
+ };
167
+ }
168
+ #C(e) {
169
+ let t = e * Math.PI / 360, n = {
170
+ w: Math.cos(t),
171
+ x: Math.sin(t),
172
+ y: 0,
173
+ z: 0
174
+ }, r = this.#i;
175
+ this.#i = {
176
+ w: n.w * r.w - n.x * r.x,
177
+ x: n.w * r.x + n.x * r.w,
178
+ y: n.w * r.y - n.x * r.z,
179
+ z: n.w * r.z + n.x * r.y
180
+ };
181
+ }
182
+ #w() {
183
+ let e = this.#f.getBoundingClientRect(), t = e.width / 2, n = e.height / 2, r = this.#e.reverse || this.#e.reverseY ? -1 : 1, i = this.#e.reverse || this.#e.reverseX ? -1 : 1, a = this.#e.inertiaDecay;
184
+ this.#l || (this.#S((this.#e.spinY + this.#o) * r), this.#C((this.#e.spinX + this.#s) * i), this.#o *= a, this.#s *= a);
185
+ let { w: o, x: s, y: c, z: l } = this.#i, u = 1 - 2 * (c * c + l * l), d = 2 * (s * c - o * l), f = 2 * (s * l + o * c), p = 2 * (s * c + o * l), m = 1 - 2 * (s * s + l * l), h = 2 * (c * l - o * s), g = 2 * (s * l - o * c), _ = 2 * (c * l + o * s), v = this.#r * 2, y = [];
186
+ for (let e of this.#t) {
187
+ let r = u * e.x + d * e.y + f * e.z, i = p * e.x + m * e.y + h * e.z, a = g * e.x + _ * e.y + (1 - 2 * (s * s + c * c)) * e.z, o = v / (v + a), l = Math.min(1, Math.max(0, o * o - .25));
188
+ y.push({
189
+ text: e.text,
190
+ x: t + r * o,
191
+ y: n + i * o,
192
+ z: a,
193
+ scale: o,
194
+ alpha: l
195
+ });
196
+ }
197
+ this.#e.onRender(y.sort((e, t) => t.z - e.z));
198
+ }
199
+ };
200
+ //#endregion
201
+ //#region src/core/rotation.ts
202
+ function r(e, t, n) {
203
+ let r = Math.sin(t), i = Math.cos(t), a = Math.sin(n), o = Math.cos(n), s = e.y * i + e.z * -r, c = e.y * r + e.z * i;
204
+ return {
205
+ x: e.x * o + c * a,
206
+ y: s,
207
+ z: c * o - e.x * a
208
+ };
209
+ }
210
+ function i(e, t, n) {
211
+ let r = Math.sin(t), i = Math.cos(t), a = Math.sin(n), o = Math.cos(n);
212
+ return e.map((e) => {
213
+ let t = e.y * i + e.z * -r, n = e.y * r + e.z * i;
214
+ return {
215
+ x: e.x * o + n * a,
216
+ y: t,
217
+ z: n * o - e.x * a
218
+ };
219
+ });
220
+ }
221
+ //#endregion
222
+ //#region src/core/projection.ts
223
+ function a(e, t) {
224
+ let n = 2 * t;
225
+ return e.map((e) => {
226
+ let t = n / (n + e.z), r = Math.min(1, Math.max(0, t * t - .25));
227
+ return {
228
+ x: e.x,
229
+ y: e.y,
230
+ z: e.z,
231
+ scale: t,
232
+ alpha: r
233
+ };
234
+ });
235
+ }
236
+ //#endregion
237
+ export { n as TagCloud, e as fibonacciSphere, a as project, r as rotatePoint, i as rotatePoints };
238
+
239
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#container","#opts","#radius","#depth","#canvasRender","#initTags","#bindEvents","#loop","#canvas","#ctx","#resizeCanvas","#paused","#raf","#handlers","#points","#dragging","#qDown","#qNow","#vDown","#screenToSphere","#velY","#velX","#tick","#rotateY","#rotateX"],"sources":["../src/core/distribution.ts","../src/TagCloud.ts","../src/core/rotation.ts","../src/core/projection.ts"],"sourcesContent":["/**\n * 斐波那契球面分布\n * Fibonacci sphere distribution\n *\n * 将 N 个点均匀分布在球面上,避免两极聚集\n * Evenly distributes N points on a sphere, avoiding polar clustering\n *\n * ported from TagCloud.js _computePosition\n */\n\nexport interface Vec3 {\n x: number;\n y: number;\n z: number;\n}\n\n/**\n * 生成球面上均匀分布的 N 个点\n * Generate N evenly distributed points on a sphere of radius R\n */\nexport function fibonacciSphere(n: number, R: number): Vec3[] {\n const points: Vec3[] = [];\n for (let i = 0; i < n; i++) {\n // φ = acos(1 - 2(i+0.5)/N) — 纬度均匀分布\n // φ = acos(1 - 2(i+0.5)/N) — uniform latitude\n const phi = Math.acos(-1 + (2 * i + 1) / n);\n // θ = √(Nπ) × φ — 经度黄金比例螺旋\n // θ = √(Nπ) × φ — golden ratio spiral for longitude\n const theta = Math.sqrt(n * Math.PI) * phi;\n points.push({\n x: R * Math.cos(theta) * Math.sin(phi),\n y: R * Math.sin(theta) * Math.sin(phi),\n z: R * Math.cos(phi),\n });\n }\n return points;\n}\n","/**\n * 3D 标签云 — 纯数学引擎\n * 3D Tag Cloud — Pure Math Engine\n *\n * 零 DOM 渲染,每帧通过 onRender 回调输出投影坐标\n * Zero DOM rendering, outputs projected coords via onRender callback each frame\n *\n * 基于 cong-min/TagCloud 算法\n * Based on cong-min/TagCloud algorithm\n */\nimport { fibonacciSphere } from \"./core/distribution\";\n\n// ── 类型\n// ── Types\n\n/** 投影后的标签数据 */\n/** Projected tag data */\nexport interface TagData {\n text: string;\n /** 容器内 X 坐标(像素) */\n /** X coordinate in container (px) */\n x: number;\n /** 容器内 Y 坐标(像素) */\n /** Y coordinate in container (px) */\n y: number;\n /** Z 深度(-radius ~ +radius) */\n /** Z depth */\n z: number;\n /** 缩放比例 (0 ~ 1+) */\n /** scale factor */\n scale: number;\n /** 透明度 (0 ~ 1) */\n /** opacity */\n alpha: number;\n}\n\nexport interface TagCloudOptions {\n /** 标签文本数组 */\n /** tag text array */\n tags: string[];\n /** 球面半径(px) */\n /** sphere radius (px) (default 300) */\n radius?: number;\n /** 绕 Y 轴自旋速度(°/帧): +右转 -左转 0=关 */\n /** Y-axis auto-spin speed (°/frame): +right -left 0=off (default 0) */\n spinY?: number;\n /** 绕 X 轴自旋速度(°/帧): +下转 -上转 0=关 */\n /** X-axis auto-spin speed (°/frame): +down -up 0=off (default 0) */\n spinX?: number;\n /** 反转方向(X+Y 同时) */\n /** reverse both axes (default false) */\n reverse?: boolean;\n /** 单独反转 X 轴(上下拖拽) */\n /** reverse X-axis drag only (default false) */\n reverseX?: boolean;\n /** 反转 Y 轴拖拽方向 */\n /** reverse Y-axis drag direction (default false) */\n reverseY?: boolean;\n /** 惯性衰减系数(每帧乘以此值) */\n /** inertia decay per frame (default 0.96) */\n inertiaDecay?: number;\n /** 拖拽灵敏度(松手后惯性速度倍率) */\n /** drag sensitivity for release velocity (default 3) */\n dragSensitivity?: number;\n /** 字体 */\n /** font family (default \"system-ui, sans-serif\") */\n fontFamily?: string;\n /** 基础字号(px) */\n /** base font size in px (default 14) */\n fontSize?: number;\n /** 文字颜色 */\n /** text color (default \"#ffffff\") */\n color?: string;\n /** 自定义渲染回调(如不提供则用内置 Canvas) */\n /** custom render callback (built-in Canvas if omitted) */\n onRender?: (tags: TagData[]) => void;\n}\n\n// ── 内部类型\n// ── Internal Types\n\ninterface SpherePoint {\n x: number;\n y: number;\n z: number;\n text: string;\n}\n\ntype ResolvedOptions = TagCloudOptions & Required<Omit<TagCloudOptions, \"onRender\">>;\n\nconst DEFAULTS: Omit<ResolvedOptions, \"tags\" | \"onRender\"> = {\n radius: 300,\n spinY: 0,\n spinX: 0,\n reverse: false,\n reverseX: false,\n reverseY: false,\n inertiaDecay: 0.96,\n dragSensitivity: 3,\n fontFamily: \"system-ui, sans-serif\",\n fontSize: 14,\n color: \"#ffffff\",\n};\n\n// ── 主类\n// ── Main Class\n\nexport class TagCloud {\n #opts: ResolvedOptions;\n #points: SpherePoint[] = [];\n #radius: number;\n #depth: number;\n\n // 旋转状态 — 四元数\n // rotation state as quaternion\n #qNow = { w: 1, x: 0, y: 0, z: 0 };\n #qDown = { w: 1, x: 0, y: 0, z: 0 };\n #velY = 0;\n #velX = 0;\n #paused = false;\n\n // 拖拽状态\n // arcball drag state\n #dragging = false;\n #vDown = { x: 0, y: 0, z: 0 };\n\n // 动画\n // animation\n #raf = 0;\n #container: HTMLElement;\n #handlers!: { down: EventListener; move: EventListener; up: EventListener };\n\n // 内置 Canvas(仅当 onRender 未提供时创建)\n // built-in Canvas (only when onRender is not provided)\n #canvas?: HTMLCanvasElement;\n #ctx?: CanvasRenderingContext2D;\n\n constructor(container: HTMLElement, options: TagCloudOptions) {\n this.#container = container;\n this.#opts = { ...DEFAULTS, ...options } as ResolvedOptions;\n this.#radius = this.#opts.radius;\n this.#depth = 2 * this.#radius;\n\n // 内置 Canvas 渲染器\n // built-in Canvas renderer\n if (!this.#opts.onRender) {\n this.#opts.onRender = this.#canvasRender;\n }\n\n this.#initTags(this.#opts.tags);\n this.#bindEvents();\n this.#loop();\n }\n\n /** 内置 Canvas 渲染器 */\n /** Built-in Canvas renderer */\n #canvasRender = (tags: TagData[]): void => {\n if (!this.#canvas) {\n const c = document.createElement(\"canvas\");\n c.style.width = \"100%\";\n c.style.height = \"100%\";\n this.#container.appendChild(c);\n this.#canvas = c;\n this.#ctx = c.getContext(\"2d\")!;\n this.#resizeCanvas();\n }\n const { width, height } = this.#canvas.getBoundingClientRect();\n const ctx = this.#ctx!;\n ctx.clearRect(0, 0, width, height);\n const { fontFamily, fontSize, color } = this.#opts;\n for (const t of tags) {\n ctx.save();\n ctx.globalAlpha = t.alpha;\n ctx.font = `${fontSize + t.scale * 5}px ${fontFamily}`;\n ctx.fillStyle = color;\n ctx.textAlign = \"center\";\n ctx.textBaseline = \"middle\";\n ctx.fillText(t.text, t.x, t.y);\n ctx.restore();\n }\n };\n\n #resizeCanvas(): void {\n const c = this.#canvas;\n if (!c) return;\n const dpr = window.devicePixelRatio || 1;\n const { width, height } = c.getBoundingClientRect();\n c.width = width * dpr;\n c.height = height * dpr;\n this.#ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);\n }\n\n // ── 公开 API\n // ── Public API\n\n setTags(tags: string[]): void {\n this.#initTags(tags);\n }\n pause(): void {\n this.#paused = true;\n }\n resume(): void {\n this.#paused = false;\n }\n\n destroy(): void {\n cancelAnimationFrame(this.#raf);\n const h = this.#handlers;\n this.#container.removeEventListener(\"pointerdown\", h.down);\n window.removeEventListener(\"pointermove\", h.move);\n window.removeEventListener(\"pointerup\", h.up);\n if (this.#canvas) this.#canvas.remove();\n }\n\n // ── 内部方法\n // ── Internal\n\n #initTags(tags: string[]): void {\n const size = 1.5 * this.#radius;\n const positions = fibonacciSphere(tags.length, size / 2);\n this.#points = positions.map((p, i) => ({ ...p, text: tags[i]! }));\n }\n\n #bindEvents(): void {\n this.#container.style.cursor = \"grab\";\n\n const rect = () => this.#container.getBoundingClientRect();\n\n this.#handlers = {\n down: ((e: PointerEvent) => {\n this.#dragging = true;\n this.#container.style.cursor = \"grabbing\";\n this.#qDown = { ...this.#qNow };\n const r = rect();\n this.#vDown = this.#screenToSphere(\n e.clientX - r.left,\n e.clientY - r.top,\n r.width,\n r.height,\n );\n this.#velY = 0;\n this.#velX = 0;\n }) as EventListener,\n move: ((e: PointerEvent) => {\n if (!this.#dragging) return;\n const r = rect();\n const vCur = this.#screenToSphere(e.clientX - r.left, e.clientY - r.top, r.width, r.height);\n const vA = this.#vDown;\n const dot = vA.x * vCur.x + vA.y * vCur.y + vA.z * vCur.z;\n // Shoemake arcball 四元数\n // Shoemake arcball quaternion\n const revX = this.#opts.reverse || this.#opts.reverseX ? -1 : 1;\n const revY = this.#opts.reverse || this.#opts.reverseY ? -1 : 1;\n const qDrag = {\n w: 1 + dot,\n x: (vA.y * vCur.z - vA.z * vCur.y) * revX,\n y: (vA.x * vCur.z - vA.z * vCur.x) * revY,\n z: (vA.x * vCur.y - vA.y * vCur.x) * revX * revY,\n };\n const len = Math.sqrt(qDrag.w ** 2 + qDrag.x ** 2 + qDrag.y ** 2 + qDrag.z ** 2);\n qDrag.w /= len;\n qDrag.x /= len;\n qDrag.y /= len;\n qDrag.z /= len;\n // 组合\n // compose\n const qD = this.#qDown;\n this.#qNow = {\n w: qDrag.w * qD.w - qDrag.x * qD.x - qDrag.y * qD.y - qDrag.z * qD.z,\n x: qDrag.w * qD.x + qDrag.x * qD.w + qDrag.y * qD.z - qDrag.z * qD.y,\n y: qDrag.w * qD.y - qDrag.x * qD.z + qDrag.y * qD.w + qDrag.z * qD.x,\n z: qDrag.w * qD.z + qDrag.x * qD.y - qDrag.y * qD.x + qDrag.z * qD.w,\n };\n // 拖拽速度\n // drag velocity\n const sens = this.#opts.dragSensitivity;\n this.#velY = (qDrag.y / len) * sens;\n this.#velX = (qDrag.x / len) * sens;\n }) as EventListener,\n up: () => {\n this.#dragging = false;\n this.#container.style.cursor = \"grab\";\n },\n };\n\n this.#container.addEventListener(\"pointerdown\", this.#handlers.down);\n window.addEventListener(\"pointermove\", this.#handlers.move);\n window.addEventListener(\"pointerup\", this.#handlers.up);\n }\n\n /** 屏幕坐标 → 球面 3D 点 */\n /** screen coords → sphere 3D point */\n #screenToSphere(\n sx: number,\n sy: number,\n w: number,\n h: number,\n ): { x: number; y: number; z: number } {\n const x = (sx / w) * 2 - 1;\n const y = -((sy / h) * 2 - 1);\n const r2 = x * x + y * y;\n if (r2 > 1) {\n // 球外 → 投影到球边缘\n // outside sphere → project to edge\n const inv = 1 / Math.sqrt(r2);\n return { x: x * inv, y: y * inv, z: 0 };\n }\n return { x, y, z: Math.sqrt(1 - r2) };\n }\n\n #loop = (): void => {\n if (!this.#paused) this.#tick();\n this.#raf = requestAnimationFrame(this.#loop);\n };\n\n /** 绕 Y 轴旋转 */\n /** rotate around Y axis */\n #rotateY(deg: number): void {\n const half = (deg * Math.PI) / 360;\n const qY = { w: Math.cos(half), x: 0, y: Math.sin(half), z: 0 };\n const q = this.#qNow;\n this.#qNow = {\n w: qY.w * q.w - qY.y * q.y,\n x: qY.w * q.x + qY.y * q.z,\n y: qY.w * q.y + qY.y * q.w,\n z: qY.w * q.z - qY.y * q.x,\n };\n }\n\n /** 绕 X 轴旋转 */\n /** rotate around X axis */\n #rotateX(deg: number): void {\n const half = (deg * Math.PI) / 360;\n const qX = { w: Math.cos(half), x: Math.sin(half), y: 0, z: 0 };\n const q = this.#qNow;\n this.#qNow = {\n w: qX.w * q.w - qX.x * q.x,\n x: qX.w * q.x + qX.x * q.w,\n y: qX.w * q.y - qX.x * q.z,\n z: qX.w * q.z + qX.x * q.y,\n };\n }\n\n #tick(): void {\n const rect = this.#container.getBoundingClientRect();\n const cx = rect.width / 2;\n const cy = rect.height / 2;\n\n // 自旋 + 惯性\n // auto-spin + inertia\n const revY = this.#opts.reverse || this.#opts.reverseY ? -1 : 1;\n const revX = this.#opts.reverse || this.#opts.reverseX ? -1 : 1;\n const decay = this.#opts.inertiaDecay;\n if (!this.#dragging) {\n this.#rotateY((this.#opts.spinY + this.#velY) * revY);\n this.#rotateX((this.#opts.spinX + this.#velX) * revX);\n this.#velY *= decay;\n this.#velX *= decay;\n }\n\n // 四元数构造 3×3 旋转矩阵\n // build 3×3 rotation matrix from quaternion\n const { w, x, y, z } = this.#qNow;\n const m00 = 1 - 2 * (y * y + z * z);\n const m01 = 2 * (x * y - w * z);\n const m02 = 2 * (x * z + w * y);\n const m10 = 2 * (x * y + w * z);\n const m11 = 1 - 2 * (x * x + z * z);\n const m12 = 2 * (y * z - w * x);\n const m20 = 2 * (x * z - w * y);\n const m21 = 2 * (y * z + w * x);\n\n const d2 = this.#depth * 2;\n const projected: TagData[] = [];\n\n for (const p of this.#points) {\n // 矩阵 × 点\n // matrix × point\n const rx = m00 * p.x + m01 * p.y + m02 * p.z;\n const ry = m10 * p.x + m11 * p.y + m12 * p.z;\n const rz = m20 * p.x + m21 * p.y + (1 - 2 * (x * x + y * y)) * p.z;\n\n const per = d2 / (d2 + rz);\n const alpha = Math.min(1, Math.max(0, per * per - 0.25));\n\n projected.push({\n text: p.text,\n x: cx + rx * per,\n y: cy + ry * per,\n z: rz,\n scale: per,\n alpha,\n });\n }\n\n this.#opts.onRender(projected.sort((a, b) => b.z - a.z));\n }\n}\n\nexport default TagCloud;\n","/**\n * 3D 旋转变换\n * 3D rotation transforms\n *\n * 绕 Y 轴和 X 轴旋转球面上的所有点\n * Rotates all points around Y and X axes\n *\n * ported from TagCloud.js _next() rotation logic\n */\n\nimport type { Vec3 } from \"./distribution\";\n\n/**\n * 旋转单个 3D 点\n * Rotate a single 3D point\n *\n * @param p - 待旋转的点\n * @param p - point to rotate\n * @param a - 绕 Y 轴的旋转角(弧度)\n * @param a - Y-axis rotation angle (radians)\n * @param b - 绕 X 轴的旋转角(弧度)\n * @param b - X-axis rotation angle (radians)\n */\nexport function rotatePoint(p: Vec3, a: number, b: number): Vec3 {\n const sinA = Math.sin(a);\n const cosA = Math.cos(a);\n const sinB = Math.sin(b);\n const cosB = Math.cos(b);\n\n // Y 轴旋转\n // Y-axis rotation\n const y1 = p.y * cosA + p.z * -sinA;\n const z1 = p.y * sinA + p.z * cosA;\n\n // X 轴旋转\n // X-axis rotation\n const x2 = p.x * cosB + z1 * sinB;\n const z2 = z1 * cosB - p.x * sinB;\n\n return { x: x2, y: y1, z: z2 };\n}\n\n/**\n * 批量旋转所有点\n * Rotate all points in batch\n */\nexport function rotatePoints(points: Vec3[], a: number, b: number): Vec3[] {\n const sinA = Math.sin(a);\n const cosA = Math.cos(a);\n const sinB = Math.sin(b);\n const cosB = Math.cos(b);\n\n return points.map((p) => {\n const y1 = p.y * cosA + p.z * -sinA;\n const z1 = p.y * sinA + p.z * cosA;\n const x2 = p.x * cosB + z1 * sinB;\n return { x: x2, y: y1, z: z1 * cosB - p.x * sinB };\n });\n}\n","/**\n * 透视投影\n * Perspective projection\n *\n * 根据 Z 深度计算每个标签的缩放比例和透明度\n * Calculates scale and alpha for each tag based on Z depth\n *\n * per = (2 × depth) / (2 × depth + z)\n * scale = per\n * alpha = per² − 0.25 → clamped [0, 1]\n *\n * ported from TagCloud.js _next() projection logic\n */\n\nexport interface ProjectedTag {\n /** 原始 X 坐标 */\n /** original X coordinate */\n x: number;\n /** 原始 Y 坐标 */\n /** original Y coordinate */\n y: number;\n /** 原始 Z 深度 */\n /** original Z depth */\n z: number;\n /** 缩放比例 (1 = 最近, 0 = 最远) */\n /** scale factor (1 = nearest, 0 = farthest) */\n scale: number;\n /** 透明度 (1 = 最前, 0 = 最后) */\n /** opacity (1 = front, 0 = back) */\n alpha: number;\n}\n\n/**\n * 对旋转后的点做透视投影\n * Apply perspective projection to rotated points\n *\n * @param points — 旋转后的 3D 点\n * @param points — rotated 3D points\n * @param depth — 透视深度 = 2 × 球半径\n * @param depth — perspective depth = 2 × sphere radius\n */\nexport function project(\n points: { x: number; y: number; z: number }[],\n depth: number,\n): ProjectedTag[] {\n const d2 = 2 * depth;\n return points.map((p) => {\n // 透视缩放\n // perspective scale\n const per = d2 / (d2 + p.z);\n // 透明度从 per² − 0.25 计算\n // alpha derived from per² − 0.25\n const alpha = Math.min(1, Math.max(0, per * per - 0.25));\n return { x: p.x, y: p.y, z: p.z, scale: per, alpha };\n });\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@xingwangzhe/tags-cloud",
3
+ "version": "0.5.0",
4
+ "description": "Canvas-driven 3D tag cloud engine",
5
+ "keywords": ["3d", "canvas", "sphere", "tag-cloud", "tags"],
6
+ "license": "MIT",
7
+ "files": ["dist", "src/core", "src/index.ts", "src/TagCloud.ts"],
8
+ "type": "module",
9
+ "main": "./dist/index.js",
10
+ "module": "./dist/index.js",
11
+ "types": "./src/index.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "vite build",
21
+ "fmt": "oxfmt",
22
+ "fmt:check": "oxfmt --check",
23
+ "lint": "oxlint",
24
+ "prepublishOnly": "bun run build"
25
+ },
26
+ "devDependencies": {
27
+ "oxfmt": "^0.54.0",
28
+ "oxlint": "^1.69.0",
29
+ "vite": "^8.0.16"
30
+ }
31
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * 3D 标签云 — 纯数学引擎
3
+ * 3D Tag Cloud — Pure Math Engine
4
+ *
5
+ * 零 DOM 渲染,每帧通过 onRender 回调输出投影坐标
6
+ * Zero DOM rendering, outputs projected coords via onRender callback each frame
7
+ *
8
+ * 基于 cong-min/TagCloud 算法
9
+ * Based on cong-min/TagCloud algorithm
10
+ */
11
+ import { fibonacciSphere } from "./core/distribution";
12
+
13
+ // ── 类型
14
+ // ── Types
15
+
16
+ /** 投影后的标签数据 */
17
+ /** Projected tag data */
18
+ export interface TagData {
19
+ text: string;
20
+ /** 容器内 X 坐标(像素) */
21
+ /** X coordinate in container (px) */
22
+ x: number;
23
+ /** 容器内 Y 坐标(像素) */
24
+ /** Y coordinate in container (px) */
25
+ y: number;
26
+ /** Z 深度(-radius ~ +radius) */
27
+ /** Z depth */
28
+ z: number;
29
+ /** 缩放比例 (0 ~ 1+) */
30
+ /** scale factor */
31
+ scale: number;
32
+ /** 透明度 (0 ~ 1) */
33
+ /** opacity */
34
+ alpha: number;
35
+ }
36
+
37
+ export interface TagCloudOptions {
38
+ /** 标签文本数组 */
39
+ /** tag text array */
40
+ tags: string[];
41
+ /** 球面半径(px) */
42
+ /** sphere radius (px) (default 300) */
43
+ radius?: number;
44
+ /** 绕 Y 轴自旋速度(°/帧): +右转 -左转 0=关 */
45
+ /** Y-axis auto-spin speed (°/frame): +right -left 0=off (default 0) */
46
+ spinY?: number;
47
+ /** 绕 X 轴自旋速度(°/帧): +下转 -上转 0=关 */
48
+ /** X-axis auto-spin speed (°/frame): +down -up 0=off (default 0) */
49
+ spinX?: number;
50
+ /** 反转方向(X+Y 同时) */
51
+ /** reverse both axes (default false) */
52
+ reverse?: boolean;
53
+ /** 单独反转 X 轴(上下拖拽) */
54
+ /** reverse X-axis drag only (default false) */
55
+ reverseX?: boolean;
56
+ /** 反转 Y 轴拖拽方向 */
57
+ /** reverse Y-axis drag direction (default false) */
58
+ reverseY?: boolean;
59
+ /** 惯性衰减系数(每帧乘以此值) */
60
+ /** inertia decay per frame (default 0.96) */
61
+ inertiaDecay?: number;
62
+ /** 拖拽灵敏度(松手后惯性速度倍率) */
63
+ /** drag sensitivity for release velocity (default 3) */
64
+ dragSensitivity?: number;
65
+ /** 字体 */
66
+ /** font family (default "system-ui, sans-serif") */
67
+ fontFamily?: string;
68
+ /** 基础字号(px) */
69
+ /** base font size in px (default 14) */
70
+ fontSize?: number;
71
+ /** 文字颜色 */
72
+ /** text color (default "#ffffff") */
73
+ color?: string;
74
+ /** 自定义渲染回调(如不提供则用内置 Canvas) */
75
+ /** custom render callback (built-in Canvas if omitted) */
76
+ onRender?: (tags: TagData[]) => void;
77
+ }
78
+
79
+ // ── 内部类型
80
+ // ── Internal Types
81
+
82
+ interface SpherePoint {
83
+ x: number;
84
+ y: number;
85
+ z: number;
86
+ text: string;
87
+ }
88
+
89
+ type ResolvedOptions = TagCloudOptions & Required<Omit<TagCloudOptions, "onRender">>;
90
+
91
+ const DEFAULTS: Omit<ResolvedOptions, "tags" | "onRender"> = {
92
+ radius: 300,
93
+ spinY: 0,
94
+ spinX: 0,
95
+ reverse: false,
96
+ reverseX: false,
97
+ reverseY: false,
98
+ inertiaDecay: 0.96,
99
+ dragSensitivity: 3,
100
+ fontFamily: "system-ui, sans-serif",
101
+ fontSize: 14,
102
+ color: "#ffffff",
103
+ };
104
+
105
+ // ── 主类
106
+ // ── Main Class
107
+
108
+ export class TagCloud {
109
+ #opts: ResolvedOptions;
110
+ #points: SpherePoint[] = [];
111
+ #radius: number;
112
+ #depth: number;
113
+
114
+ // 旋转状态 — 四元数
115
+ // rotation state as quaternion
116
+ #qNow = { w: 1, x: 0, y: 0, z: 0 };
117
+ #qDown = { w: 1, x: 0, y: 0, z: 0 };
118
+ #velY = 0;
119
+ #velX = 0;
120
+ #paused = false;
121
+
122
+ // 拖拽状态
123
+ // arcball drag state
124
+ #dragging = false;
125
+ #vDown = { x: 0, y: 0, z: 0 };
126
+
127
+ // 动画
128
+ // animation
129
+ #raf = 0;
130
+ #container: HTMLElement;
131
+ #handlers!: { down: EventListener; move: EventListener; up: EventListener };
132
+
133
+ // 内置 Canvas(仅当 onRender 未提供时创建)
134
+ // built-in Canvas (only when onRender is not provided)
135
+ #canvas?: HTMLCanvasElement;
136
+ #ctx?: CanvasRenderingContext2D;
137
+
138
+ constructor(container: HTMLElement, options: TagCloudOptions) {
139
+ this.#container = container;
140
+ this.#opts = { ...DEFAULTS, ...options } as ResolvedOptions;
141
+ this.#radius = this.#opts.radius;
142
+ this.#depth = 2 * this.#radius;
143
+
144
+ // 内置 Canvas 渲染器
145
+ // built-in Canvas renderer
146
+ if (!this.#opts.onRender) {
147
+ this.#opts.onRender = this.#canvasRender;
148
+ }
149
+
150
+ this.#initTags(this.#opts.tags);
151
+ this.#bindEvents();
152
+ this.#loop();
153
+ }
154
+
155
+ /** 内置 Canvas 渲染器 */
156
+ /** Built-in Canvas renderer */
157
+ #canvasRender = (tags: TagData[]): void => {
158
+ if (!this.#canvas) {
159
+ const c = document.createElement("canvas");
160
+ c.style.width = "100%";
161
+ c.style.height = "100%";
162
+ this.#container.appendChild(c);
163
+ this.#canvas = c;
164
+ this.#ctx = c.getContext("2d")!;
165
+ this.#resizeCanvas();
166
+ }
167
+ const { width, height } = this.#canvas.getBoundingClientRect();
168
+ const ctx = this.#ctx!;
169
+ ctx.clearRect(0, 0, width, height);
170
+ const { fontFamily, fontSize, color } = this.#opts;
171
+ for (const t of tags) {
172
+ ctx.save();
173
+ ctx.globalAlpha = t.alpha;
174
+ ctx.font = `${fontSize + t.scale * 5}px ${fontFamily}`;
175
+ ctx.fillStyle = color;
176
+ ctx.textAlign = "center";
177
+ ctx.textBaseline = "middle";
178
+ ctx.fillText(t.text, t.x, t.y);
179
+ ctx.restore();
180
+ }
181
+ };
182
+
183
+ #resizeCanvas(): void {
184
+ const c = this.#canvas;
185
+ if (!c) return;
186
+ const dpr = window.devicePixelRatio || 1;
187
+ const { width, height } = c.getBoundingClientRect();
188
+ c.width = width * dpr;
189
+ c.height = height * dpr;
190
+ this.#ctx!.setTransform(dpr, 0, 0, dpr, 0, 0);
191
+ }
192
+
193
+ // ── 公开 API
194
+ // ── Public API
195
+
196
+ setTags(tags: string[]): void {
197
+ this.#initTags(tags);
198
+ }
199
+ pause(): void {
200
+ this.#paused = true;
201
+ }
202
+ resume(): void {
203
+ this.#paused = false;
204
+ }
205
+
206
+ destroy(): void {
207
+ cancelAnimationFrame(this.#raf);
208
+ const h = this.#handlers;
209
+ this.#container.removeEventListener("pointerdown", h.down);
210
+ window.removeEventListener("pointermove", h.move);
211
+ window.removeEventListener("pointerup", h.up);
212
+ if (this.#canvas) this.#canvas.remove();
213
+ }
214
+
215
+ // ── 内部方法
216
+ // ── Internal
217
+
218
+ #initTags(tags: string[]): void {
219
+ const size = 1.5 * this.#radius;
220
+ const positions = fibonacciSphere(tags.length, size / 2);
221
+ this.#points = positions.map((p, i) => ({ ...p, text: tags[i]! }));
222
+ }
223
+
224
+ #bindEvents(): void {
225
+ this.#container.style.cursor = "grab";
226
+
227
+ const rect = () => this.#container.getBoundingClientRect();
228
+
229
+ this.#handlers = {
230
+ down: ((e: PointerEvent) => {
231
+ this.#dragging = true;
232
+ this.#container.style.cursor = "grabbing";
233
+ this.#qDown = { ...this.#qNow };
234
+ const r = rect();
235
+ this.#vDown = this.#screenToSphere(
236
+ e.clientX - r.left,
237
+ e.clientY - r.top,
238
+ r.width,
239
+ r.height,
240
+ );
241
+ this.#velY = 0;
242
+ this.#velX = 0;
243
+ }) as EventListener,
244
+ move: ((e: PointerEvent) => {
245
+ if (!this.#dragging) return;
246
+ const r = rect();
247
+ const vCur = this.#screenToSphere(e.clientX - r.left, e.clientY - r.top, r.width, r.height);
248
+ const vA = this.#vDown;
249
+ const dot = vA.x * vCur.x + vA.y * vCur.y + vA.z * vCur.z;
250
+ // Shoemake arcball 四元数
251
+ // Shoemake arcball quaternion
252
+ const revX = this.#opts.reverse || this.#opts.reverseX ? -1 : 1;
253
+ const revY = this.#opts.reverse || this.#opts.reverseY ? -1 : 1;
254
+ const qDrag = {
255
+ w: 1 + dot,
256
+ x: (vA.y * vCur.z - vA.z * vCur.y) * revX,
257
+ y: (vA.x * vCur.z - vA.z * vCur.x) * revY,
258
+ z: (vA.x * vCur.y - vA.y * vCur.x) * revX * revY,
259
+ };
260
+ const len = Math.sqrt(qDrag.w ** 2 + qDrag.x ** 2 + qDrag.y ** 2 + qDrag.z ** 2);
261
+ qDrag.w /= len;
262
+ qDrag.x /= len;
263
+ qDrag.y /= len;
264
+ qDrag.z /= len;
265
+ // 组合
266
+ // compose
267
+ const qD = this.#qDown;
268
+ this.#qNow = {
269
+ w: qDrag.w * qD.w - qDrag.x * qD.x - qDrag.y * qD.y - qDrag.z * qD.z,
270
+ x: qDrag.w * qD.x + qDrag.x * qD.w + qDrag.y * qD.z - qDrag.z * qD.y,
271
+ y: qDrag.w * qD.y - qDrag.x * qD.z + qDrag.y * qD.w + qDrag.z * qD.x,
272
+ z: qDrag.w * qD.z + qDrag.x * qD.y - qDrag.y * qD.x + qDrag.z * qD.w,
273
+ };
274
+ // 拖拽速度
275
+ // drag velocity
276
+ const sens = this.#opts.dragSensitivity;
277
+ this.#velY = (qDrag.y / len) * sens;
278
+ this.#velX = (qDrag.x / len) * sens;
279
+ }) as EventListener,
280
+ up: () => {
281
+ this.#dragging = false;
282
+ this.#container.style.cursor = "grab";
283
+ },
284
+ };
285
+
286
+ this.#container.addEventListener("pointerdown", this.#handlers.down);
287
+ window.addEventListener("pointermove", this.#handlers.move);
288
+ window.addEventListener("pointerup", this.#handlers.up);
289
+ }
290
+
291
+ /** 屏幕坐标 → 球面 3D 点 */
292
+ /** screen coords → sphere 3D point */
293
+ #screenToSphere(
294
+ sx: number,
295
+ sy: number,
296
+ w: number,
297
+ h: number,
298
+ ): { x: number; y: number; z: number } {
299
+ const x = (sx / w) * 2 - 1;
300
+ const y = -((sy / h) * 2 - 1);
301
+ const r2 = x * x + y * y;
302
+ if (r2 > 1) {
303
+ // 球外 → 投影到球边缘
304
+ // outside sphere → project to edge
305
+ const inv = 1 / Math.sqrt(r2);
306
+ return { x: x * inv, y: y * inv, z: 0 };
307
+ }
308
+ return { x, y, z: Math.sqrt(1 - r2) };
309
+ }
310
+
311
+ #loop = (): void => {
312
+ if (!this.#paused) this.#tick();
313
+ this.#raf = requestAnimationFrame(this.#loop);
314
+ };
315
+
316
+ /** 绕 Y 轴旋转 */
317
+ /** rotate around Y axis */
318
+ #rotateY(deg: number): void {
319
+ const half = (deg * Math.PI) / 360;
320
+ const qY = { w: Math.cos(half), x: 0, y: Math.sin(half), z: 0 };
321
+ const q = this.#qNow;
322
+ this.#qNow = {
323
+ w: qY.w * q.w - qY.y * q.y,
324
+ x: qY.w * q.x + qY.y * q.z,
325
+ y: qY.w * q.y + qY.y * q.w,
326
+ z: qY.w * q.z - qY.y * q.x,
327
+ };
328
+ }
329
+
330
+ /** 绕 X 轴旋转 */
331
+ /** rotate around X axis */
332
+ #rotateX(deg: number): void {
333
+ const half = (deg * Math.PI) / 360;
334
+ const qX = { w: Math.cos(half), x: Math.sin(half), y: 0, z: 0 };
335
+ const q = this.#qNow;
336
+ this.#qNow = {
337
+ w: qX.w * q.w - qX.x * q.x,
338
+ x: qX.w * q.x + qX.x * q.w,
339
+ y: qX.w * q.y - qX.x * q.z,
340
+ z: qX.w * q.z + qX.x * q.y,
341
+ };
342
+ }
343
+
344
+ #tick(): void {
345
+ const rect = this.#container.getBoundingClientRect();
346
+ const cx = rect.width / 2;
347
+ const cy = rect.height / 2;
348
+
349
+ // 自旋 + 惯性
350
+ // auto-spin + inertia
351
+ const revY = this.#opts.reverse || this.#opts.reverseY ? -1 : 1;
352
+ const revX = this.#opts.reverse || this.#opts.reverseX ? -1 : 1;
353
+ const decay = this.#opts.inertiaDecay;
354
+ if (!this.#dragging) {
355
+ this.#rotateY((this.#opts.spinY + this.#velY) * revY);
356
+ this.#rotateX((this.#opts.spinX + this.#velX) * revX);
357
+ this.#velY *= decay;
358
+ this.#velX *= decay;
359
+ }
360
+
361
+ // 四元数构造 3×3 旋转矩阵
362
+ // build 3×3 rotation matrix from quaternion
363
+ const { w, x, y, z } = this.#qNow;
364
+ const m00 = 1 - 2 * (y * y + z * z);
365
+ const m01 = 2 * (x * y - w * z);
366
+ const m02 = 2 * (x * z + w * y);
367
+ const m10 = 2 * (x * y + w * z);
368
+ const m11 = 1 - 2 * (x * x + z * z);
369
+ const m12 = 2 * (y * z - w * x);
370
+ const m20 = 2 * (x * z - w * y);
371
+ const m21 = 2 * (y * z + w * x);
372
+
373
+ const d2 = this.#depth * 2;
374
+ const projected: TagData[] = [];
375
+
376
+ for (const p of this.#points) {
377
+ // 矩阵 × 点
378
+ // matrix × point
379
+ const rx = m00 * p.x + m01 * p.y + m02 * p.z;
380
+ const ry = m10 * p.x + m11 * p.y + m12 * p.z;
381
+ const rz = m20 * p.x + m21 * p.y + (1 - 2 * (x * x + y * y)) * p.z;
382
+
383
+ const per = d2 / (d2 + rz);
384
+ const alpha = Math.min(1, Math.max(0, per * per - 0.25));
385
+
386
+ projected.push({
387
+ text: p.text,
388
+ x: cx + rx * per,
389
+ y: cy + ry * per,
390
+ z: rz,
391
+ scale: per,
392
+ alpha,
393
+ });
394
+ }
395
+
396
+ this.#opts.onRender(projected.sort((a, b) => b.z - a.z));
397
+ }
398
+ }
399
+
400
+ export default TagCloud;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 斐波那契球面分布
3
+ * Fibonacci sphere distribution
4
+ *
5
+ * 将 N 个点均匀分布在球面上,避免两极聚集
6
+ * Evenly distributes N points on a sphere, avoiding polar clustering
7
+ *
8
+ * ported from TagCloud.js _computePosition
9
+ */
10
+
11
+ export interface Vec3 {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ }
16
+
17
+ /**
18
+ * 生成球面上均匀分布的 N 个点
19
+ * Generate N evenly distributed points on a sphere of radius R
20
+ */
21
+ export function fibonacciSphere(n: number, R: number): Vec3[] {
22
+ const points: Vec3[] = [];
23
+ for (let i = 0; i < n; i++) {
24
+ // φ = acos(1 - 2(i+0.5)/N) — 纬度均匀分布
25
+ // φ = acos(1 - 2(i+0.5)/N) — uniform latitude
26
+ const phi = Math.acos(-1 + (2 * i + 1) / n);
27
+ // θ = √(Nπ) × φ — 经度黄金比例螺旋
28
+ // θ = √(Nπ) × φ — golden ratio spiral for longitude
29
+ const theta = Math.sqrt(n * Math.PI) * phi;
30
+ points.push({
31
+ x: R * Math.cos(theta) * Math.sin(phi),
32
+ y: R * Math.sin(theta) * Math.sin(phi),
33
+ z: R * Math.cos(phi),
34
+ });
35
+ }
36
+ return points;
37
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 透视投影
3
+ * Perspective projection
4
+ *
5
+ * 根据 Z 深度计算每个标签的缩放比例和透明度
6
+ * Calculates scale and alpha for each tag based on Z depth
7
+ *
8
+ * per = (2 × depth) / (2 × depth + z)
9
+ * scale = per
10
+ * alpha = per² − 0.25 → clamped [0, 1]
11
+ *
12
+ * ported from TagCloud.js _next() projection logic
13
+ */
14
+
15
+ export interface ProjectedTag {
16
+ /** 原始 X 坐标 */
17
+ /** original X coordinate */
18
+ x: number;
19
+ /** 原始 Y 坐标 */
20
+ /** original Y coordinate */
21
+ y: number;
22
+ /** 原始 Z 深度 */
23
+ /** original Z depth */
24
+ z: number;
25
+ /** 缩放比例 (1 = 最近, 0 = 最远) */
26
+ /** scale factor (1 = nearest, 0 = farthest) */
27
+ scale: number;
28
+ /** 透明度 (1 = 最前, 0 = 最后) */
29
+ /** opacity (1 = front, 0 = back) */
30
+ alpha: number;
31
+ }
32
+
33
+ /**
34
+ * 对旋转后的点做透视投影
35
+ * Apply perspective projection to rotated points
36
+ *
37
+ * @param points — 旋转后的 3D 点
38
+ * @param points — rotated 3D points
39
+ * @param depth — 透视深度 = 2 × 球半径
40
+ * @param depth — perspective depth = 2 × sphere radius
41
+ */
42
+ export function project(
43
+ points: { x: number; y: number; z: number }[],
44
+ depth: number,
45
+ ): ProjectedTag[] {
46
+ const d2 = 2 * depth;
47
+ return points.map((p) => {
48
+ // 透视缩放
49
+ // perspective scale
50
+ const per = d2 / (d2 + p.z);
51
+ // 透明度从 per² − 0.25 计算
52
+ // alpha derived from per² − 0.25
53
+ const alpha = Math.min(1, Math.max(0, per * per - 0.25));
54
+ return { x: p.x, y: p.y, z: p.z, scale: per, alpha };
55
+ });
56
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 3D 旋转变换
3
+ * 3D rotation transforms
4
+ *
5
+ * 绕 Y 轴和 X 轴旋转球面上的所有点
6
+ * Rotates all points around Y and X axes
7
+ *
8
+ * ported from TagCloud.js _next() rotation logic
9
+ */
10
+
11
+ import type { Vec3 } from "./distribution";
12
+
13
+ /**
14
+ * 旋转单个 3D 点
15
+ * Rotate a single 3D point
16
+ *
17
+ * @param p - 待旋转的点
18
+ * @param p - point to rotate
19
+ * @param a - 绕 Y 轴的旋转角(弧度)
20
+ * @param a - Y-axis rotation angle (radians)
21
+ * @param b - 绕 X 轴的旋转角(弧度)
22
+ * @param b - X-axis rotation angle (radians)
23
+ */
24
+ export function rotatePoint(p: Vec3, a: number, b: number): Vec3 {
25
+ const sinA = Math.sin(a);
26
+ const cosA = Math.cos(a);
27
+ const sinB = Math.sin(b);
28
+ const cosB = Math.cos(b);
29
+
30
+ // Y 轴旋转
31
+ // Y-axis rotation
32
+ const y1 = p.y * cosA + p.z * -sinA;
33
+ const z1 = p.y * sinA + p.z * cosA;
34
+
35
+ // X 轴旋转
36
+ // X-axis rotation
37
+ const x2 = p.x * cosB + z1 * sinB;
38
+ const z2 = z1 * cosB - p.x * sinB;
39
+
40
+ return { x: x2, y: y1, z: z2 };
41
+ }
42
+
43
+ /**
44
+ * 批量旋转所有点
45
+ * Rotate all points in batch
46
+ */
47
+ export function rotatePoints(points: Vec3[], a: number, b: number): Vec3[] {
48
+ const sinA = Math.sin(a);
49
+ const cosA = Math.cos(a);
50
+ const sinB = Math.sin(b);
51
+ const cosB = Math.cos(b);
52
+
53
+ return points.map((p) => {
54
+ const y1 = p.y * cosA + p.z * -sinA;
55
+ const z1 = p.y * sinA + p.z * cosA;
56
+ const x2 = p.x * cosB + z1 * sinB;
57
+ return { x: x2, y: y1, z: z1 * cosB - p.x * sinB };
58
+ });
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @xingwangzhe/tags-cloud
3
+ *
4
+ * 纯数学 3D 标签云引擎
5
+ * Pure math 3D tag cloud engine
6
+ *
7
+ * 用法
8
+ * Usage:
9
+ *
10
+ * ```ts
11
+ * import { TagCloud } from "@xingwangzhe/tags-cloud";
12
+ *
13
+ * const cloud = new TagCloud(container, {
14
+ * tags: ["TS", "Canvas", "3D"],
15
+ * radius: 300,
16
+ * });
17
+ * ```
18
+ *
19
+ * 底层数学模块
20
+ * Low-level math modules:
21
+ *
22
+ * ```ts
23
+ * import { fibonacciSphere, rotatePoints, project } from "@xingwangzhe/tags-cloud";
24
+ * ```
25
+ */
26
+
27
+ export { TagCloud } from "./TagCloud";
28
+ export type { TagCloudOptions, TagData } from "./TagCloud";
29
+ export { fibonacciSphere } from "./core/distribution";
30
+ export type { Vec3 } from "./core/distribution";
31
+ export { rotatePoint, rotatePoints } from "./core/rotation";
32
+ export { project } from "./core/projection";
33
+ export type { ProjectedTag } from "./core/projection";