3d-spinner 0.9.1 → 0.9.3
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/README.md +96 -11
- package/dist/animation-label.d.ts +8 -0
- package/dist/animation-label.js +47 -0
- package/dist/animation.d.ts +2 -0
- package/dist/animations/charged-orb.d.ts +55 -0
- package/dist/animations/charged-orb.js +229 -0
- package/dist/animations/grid-assembly.d.ts +67 -0
- package/dist/animations/grid-assembly.js +268 -0
- package/dist/animations/object-motion.d.ts +19 -7
- package/dist/animations/object-motion.js +38 -32
- package/dist/animations/particles.d.ts +110 -0
- package/dist/animations/particles.js +209 -0
- package/dist/cjs/animations/charged-orb.cjs +1334 -0
- package/dist/cjs/animations/grid-assembly.cjs +1498 -0
- package/dist/cjs/animations/object-motion.cjs +1449 -0
- package/dist/cjs/animations/particles.cjs +1940 -0
- package/dist/cjs/animations/spin.cjs +1300 -0
- package/dist/cjs/composite-animation.cjs +58 -0
- package/dist/cjs/engines/little-3d-engine/little-3d-engine.cjs +1433 -0
- package/dist/cjs/engines/little-3d-engine/loaders/obj.cjs +87 -0
- package/dist/cjs/engines/little-3d-engine/renderers/canvas2d-textured.cjs +345 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgl-textured.cjs +528 -0
- package/dist/cjs/engines/little-3d-engine/renderers/webgpu-textured.cjs +712 -0
- package/dist/cjs/engines/little-tween-engine/little-tween-engine.cjs +279 -0
- package/dist/cjs/index.cjs +120 -0
- package/dist/cjs/motion/motion.cjs +140 -0
- package/dist/cjs/motion/transitions.cjs +80 -0
- package/dist/cjs/prefabs/prefabs.cjs +3450 -0
- package/dist/composite-animation.d.ts +18 -0
- package/dist/composite-animation.js +39 -0
- package/dist/engines/little-3d-engine/little-3d-engine.d.ts +17 -12
- package/dist/engines/little-3d-engine/little-3d-engine.js +13 -8
- package/dist/engines/little-3d-engine/loaders/obj.d.ts +12 -4
- package/dist/engines/little-3d-engine/loaders/obj.js +44 -5
- package/dist/engines/little-3d-engine/renderer.d.ts +7 -1
- package/dist/engines/little-3d-engine/renderer.js +2 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.d.ts +20 -0
- package/dist/engines/little-3d-engine/renderers/canvas2d-textured.js +135 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.d.ts +9 -0
- package/dist/engines/little-3d-engine/renderers/textured-helpers.js +35 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.d.ts +43 -0
- package/dist/engines/little-3d-engine/renderers/webgl-textured.js +211 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.d.ts +45 -0
- package/dist/engines/little-3d-engine/renderers/webgpu-textured.js +300 -0
- package/dist/engines/little-3d-engine/renderers/webgpu.d.ts +11 -6
- package/dist/engines/little-3d-engine/shapes/complex/plane.d.ts +3 -0
- package/dist/engines/little-3d-engine/shapes/complex/plane.js +33 -0
- package/dist/engines/little-3d-engine/shapes/{cube.d.ts → primitives/cube.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octahedron.d.ts → primitives/octahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{pyramid.d.ts → primitives/pyramid.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/primitives/quad.d.ts +9 -0
- package/dist/engines/little-3d-engine/shapes/primitives/quad.js +18 -0
- package/dist/engines/little-3d-engine/shapes/{cube-sphere.d.ts → primitives/spheres/cube-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.d.ts → primitives/spheres/icosphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{icosphere.js → primitives/spheres/icosphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.d.ts → primitives/spheres/octa-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{octa-sphere.js → primitives/spheres/octa-sphere.js} +1 -1
- package/dist/engines/little-3d-engine/shapes/{uv-sphere.d.ts → primitives/spheres/uv-sphere.d.ts} +1 -1
- package/dist/engines/little-3d-engine/shapes/{tetrahedron.d.ts → primitives/tetrahedron.d.ts} +1 -1
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.d.ts +1 -0
- package/dist/engines/little-3d-engine/textures/dynamic/canvas-texture.js +8 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.d.ts +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/shine.js +14 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.d.ts +2 -0
- package/dist/engines/little-3d-engine/textures/dynamic/star.js +16 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.d.ts +5 -0
- package/dist/engines/little-3d-engine/textures/dynamic/streak.js +20 -0
- package/dist/index.d.ts +2 -1
- package/dist/prefabs/charged-orb.d.ts +17 -0
- package/dist/prefabs/charged-orb.js +37 -0
- package/dist/prefabs/crystal-comet.d.ts +4 -0
- package/dist/prefabs/crystal-comet.js +41 -0
- package/dist/prefabs/ghost-train.d.ts +9 -0
- package/dist/prefabs/ghost-train.js +44 -0
- package/dist/prefabs/grid-assembly.d.ts +13 -0
- package/dist/prefabs/grid-assembly.js +15 -0
- package/dist/prefabs/monochrome-streak.d.ts +4 -0
- package/dist/prefabs/monochrome-streak.js +25 -0
- package/dist/prefabs/plane-star-trail.d.ts +4 -0
- package/dist/prefabs/plane-star-trail.js +40 -0
- package/dist/prefabs/prefabs.d.ts +10 -0
- package/dist/prefabs/prefabs.js +9 -0
- package/dist/prefabs/pulsing-starfield.d.ts +4 -0
- package/dist/prefabs/pulsing-starfield.js +27 -0
- package/dist/prefabs/rocket-launch.d.ts +8 -0
- package/dist/prefabs/rocket-launch.js +59 -0
- package/dist/prefabs/spinner.d.ts +7 -0
- package/dist/prefabs/spinner.js +17 -0
- package/dist/prefabs/star-swarm.d.ts +4 -0
- package/dist/prefabs/star-swarm.js +27 -0
- package/dist/prefabs/types.d.ts +46 -0
- package/dist/prefabs/types.js +1 -0
- package/dist/umd/spinner.global.js +4212 -0
- package/dist/umd/spinner.global.min.js +113 -0
- package/package.json +67 -11
- /package/dist/engines/little-3d-engine/shapes/{cube.js → primitives/cube.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{octahedron.js → primitives/octahedron.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{pyramid.js → primitives/pyramid.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{cube-sphere.js → primitives/spheres/cube-sphere.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{uv-sphere.js → primitives/spheres/uv-sphere.js} +0 -0
- /package/dist/engines/little-3d-engine/shapes/{tetrahedron.js → primitives/tetrahedron.js} +0 -0
|
@@ -0,0 +1,1498 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __esm = (fn, res, err) => function __init() {
|
|
7
|
+
if (err) throw err[0];
|
|
8
|
+
try {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
} catch (e) {
|
|
11
|
+
throw err = [e], e;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
|
|
28
|
+
// src/engines/little-3d-engine/core/math.ts
|
|
29
|
+
function subtract(a, b) {
|
|
30
|
+
return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
|
|
31
|
+
}
|
|
32
|
+
function cross(a, b) {
|
|
33
|
+
return {
|
|
34
|
+
x: a.y * b.z - a.z * b.y,
|
|
35
|
+
y: a.z * b.x - a.x * b.z,
|
|
36
|
+
z: a.x * b.y - a.y * b.x
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function dot(a, b) {
|
|
40
|
+
return a.x * b.x + a.y * b.y + a.z * b.z;
|
|
41
|
+
}
|
|
42
|
+
function scale(v, s) {
|
|
43
|
+
return { x: v.x * s, y: v.y * s, z: v.z * s };
|
|
44
|
+
}
|
|
45
|
+
function normalize(v) {
|
|
46
|
+
const length = Math.hypot(v.x, v.y, v.z);
|
|
47
|
+
if (length === 0) return { x: 0, y: 0, z: 0 };
|
|
48
|
+
return { x: v.x / length, y: v.y / length, z: v.z / length };
|
|
49
|
+
}
|
|
50
|
+
function multiply(a, b) {
|
|
51
|
+
const out = new Array(16);
|
|
52
|
+
for (let col = 0; col < 4; col++) {
|
|
53
|
+
for (let row = 0; row < 4; row++) {
|
|
54
|
+
let sum = 0;
|
|
55
|
+
for (let k = 0; k < 4; k++) {
|
|
56
|
+
sum += a[k * 4 + row] * b[col * 4 + k];
|
|
57
|
+
}
|
|
58
|
+
out[col * 4 + row] = sum;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function translation(x, y, z) {
|
|
64
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, x, y, z, 1];
|
|
65
|
+
}
|
|
66
|
+
function scaleMatrix(s) {
|
|
67
|
+
return [s, 0, 0, 0, 0, s, 0, 0, 0, 0, s, 0, 0, 0, 0, 1];
|
|
68
|
+
}
|
|
69
|
+
function rotationX(rad) {
|
|
70
|
+
const c = Math.cos(rad);
|
|
71
|
+
const s = Math.sin(rad);
|
|
72
|
+
return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1];
|
|
73
|
+
}
|
|
74
|
+
function rotationY(rad) {
|
|
75
|
+
const c = Math.cos(rad);
|
|
76
|
+
const s = Math.sin(rad);
|
|
77
|
+
return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1];
|
|
78
|
+
}
|
|
79
|
+
function rotationZ(rad) {
|
|
80
|
+
const c = Math.cos(rad);
|
|
81
|
+
const s = Math.sin(rad);
|
|
82
|
+
return [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
83
|
+
}
|
|
84
|
+
function perspective(fovY, aspect, near, far) {
|
|
85
|
+
const f = 1 / Math.tan(fovY / 2);
|
|
86
|
+
const nf = 1 / (near - far);
|
|
87
|
+
return [
|
|
88
|
+
f / aspect,
|
|
89
|
+
0,
|
|
90
|
+
0,
|
|
91
|
+
0,
|
|
92
|
+
0,
|
|
93
|
+
f,
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
0,
|
|
97
|
+
0,
|
|
98
|
+
(far + near) * nf,
|
|
99
|
+
-1,
|
|
100
|
+
0,
|
|
101
|
+
0,
|
|
102
|
+
2 * far * near * nf,
|
|
103
|
+
0
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
function transformPoint(m, p) {
|
|
107
|
+
const x = m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12];
|
|
108
|
+
const y = m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13];
|
|
109
|
+
const z = m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14];
|
|
110
|
+
const w = m[3] * p.x + m[7] * p.y + m[11] * p.z + m[15] || 1;
|
|
111
|
+
return { x: x / w, y: y / w, z: z / w };
|
|
112
|
+
}
|
|
113
|
+
function transformAffine(m, p) {
|
|
114
|
+
return {
|
|
115
|
+
x: m[0] * p.x + m[4] * p.y + m[8] * p.z + m[12],
|
|
116
|
+
y: m[1] * p.x + m[5] * p.y + m[9] * p.z + m[13],
|
|
117
|
+
z: m[2] * p.x + m[6] * p.y + m[10] * p.z + m[14]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var init_math = __esm({
|
|
121
|
+
"src/engines/little-3d-engine/core/math.ts"() {
|
|
122
|
+
"use strict";
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// src/engines/little-3d-engine/core/geometry.ts
|
|
127
|
+
function parseColor(color) {
|
|
128
|
+
const hex = color.trim().replace("#", "");
|
|
129
|
+
const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
|
|
130
|
+
const n = parseInt(full, 16);
|
|
131
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
132
|
+
}
|
|
133
|
+
function expandToTriangles(mesh) {
|
|
134
|
+
let triangles = 0;
|
|
135
|
+
for (const face of mesh.faces) triangles += Math.max(0, face.indices.length - 2);
|
|
136
|
+
const positions = new Float32Array(triangles * 9);
|
|
137
|
+
const normals = new Float32Array(triangles * 9);
|
|
138
|
+
const colors = new Float32Array(triangles * 9);
|
|
139
|
+
let o = 0;
|
|
140
|
+
for (const face of mesh.faces) {
|
|
141
|
+
const v0 = mesh.vertices[face.indices[0]];
|
|
142
|
+
const v1 = mesh.vertices[face.indices[1]];
|
|
143
|
+
const v2 = mesh.vertices[face.indices[2]];
|
|
144
|
+
const normal = normalize(cross(subtract(v1, v0), subtract(v2, v0)));
|
|
145
|
+
const [r, g, b] = parseColor(face.color);
|
|
146
|
+
const cr = r / 255;
|
|
147
|
+
const cg = g / 255;
|
|
148
|
+
const cb = b / 255;
|
|
149
|
+
for (let k = 1; k < face.indices.length - 1; k++) {
|
|
150
|
+
const tri = [face.indices[0], face.indices[k], face.indices[k + 1]];
|
|
151
|
+
for (const index of tri) {
|
|
152
|
+
const v = mesh.vertices[index];
|
|
153
|
+
positions[o] = v.x;
|
|
154
|
+
positions[o + 1] = v.y;
|
|
155
|
+
positions[o + 2] = v.z;
|
|
156
|
+
normals[o] = normal.x;
|
|
157
|
+
normals[o + 1] = normal.y;
|
|
158
|
+
normals[o + 2] = normal.z;
|
|
159
|
+
colors[o] = cr;
|
|
160
|
+
colors[o + 1] = cg;
|
|
161
|
+
colors[o + 2] = cb;
|
|
162
|
+
o += 3;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { positions, normals, colors, count: positions.length / 3 };
|
|
167
|
+
}
|
|
168
|
+
function midpoint(a, b) {
|
|
169
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2 };
|
|
170
|
+
}
|
|
171
|
+
function sphereFromTriangles(seedVertices, seedFaces, size, detail, colors) {
|
|
172
|
+
const radius = size / 2;
|
|
173
|
+
let triangles = seedFaces.map((f) => f.map((i) => normalize(seedVertices[i])));
|
|
174
|
+
const levels = Math.max(0, Math.floor(detail) - 1);
|
|
175
|
+
for (let level = 0; level < levels; level++) {
|
|
176
|
+
const next = [];
|
|
177
|
+
for (const [a, b, c] of triangles) {
|
|
178
|
+
const ab = normalize(midpoint(a, b));
|
|
179
|
+
const bc = normalize(midpoint(b, c));
|
|
180
|
+
const ca = normalize(midpoint(c, a));
|
|
181
|
+
next.push([a, ab, ca], [b, bc, ab], [c, ca, bc], [ab, bc, ca]);
|
|
182
|
+
}
|
|
183
|
+
triangles = next;
|
|
184
|
+
}
|
|
185
|
+
const vertices = [];
|
|
186
|
+
const faces = triangles.map((tri, i) => {
|
|
187
|
+
const base = vertices.length;
|
|
188
|
+
vertices.push(scale(tri[0], radius), scale(tri[1], radius), scale(tri[2], radius));
|
|
189
|
+
return { indices: [base, base + 1, base + 2], color: colors[i % colors.length] };
|
|
190
|
+
});
|
|
191
|
+
return { vertices, faces };
|
|
192
|
+
}
|
|
193
|
+
var init_geometry = __esm({
|
|
194
|
+
"src/engines/little-3d-engine/core/geometry.ts"() {
|
|
195
|
+
"use strict";
|
|
196
|
+
init_math();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// src/engines/little-3d-engine/core/light.ts
|
|
201
|
+
function clamp01(value) {
|
|
202
|
+
return Math.min(1, Math.max(0, value));
|
|
203
|
+
}
|
|
204
|
+
function shadeColor(normal, color, light) {
|
|
205
|
+
const lambert = Math.max(0, dot(normal, light.toLight));
|
|
206
|
+
const brightness = clamp01(light.ambient + light.intensity * lambert);
|
|
207
|
+
const [r, g, b] = parseColor(color);
|
|
208
|
+
return `rgb(${Math.round(r * brightness)}, ${Math.round(g * brightness)}, ${Math.round(
|
|
209
|
+
b * brightness
|
|
210
|
+
)})`;
|
|
211
|
+
}
|
|
212
|
+
var DEFAULTS2, Light;
|
|
213
|
+
var init_light = __esm({
|
|
214
|
+
"src/engines/little-3d-engine/core/light.ts"() {
|
|
215
|
+
"use strict";
|
|
216
|
+
init_math();
|
|
217
|
+
init_geometry();
|
|
218
|
+
DEFAULTS2 = {
|
|
219
|
+
direction: { x: -0.4, y: -0.7, z: -0.6 },
|
|
220
|
+
intensity: 0.85,
|
|
221
|
+
ambient: 0.25
|
|
222
|
+
};
|
|
223
|
+
Light = class {
|
|
224
|
+
constructor(options) {
|
|
225
|
+
this.options = { ...DEFAULTS2, ...options };
|
|
226
|
+
this.params = {
|
|
227
|
+
toLight: normalize(scale(this.options.direction, -1)),
|
|
228
|
+
intensity: this.options.intensity,
|
|
229
|
+
ambient: this.options.ambient
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Convenience wrapper around {@link shadeColor} using this light. */
|
|
233
|
+
shade(normal, color) {
|
|
234
|
+
return shadeColor(normal, color, this.params);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// src/engines/little-3d-engine/renderers/webgl.ts
|
|
241
|
+
var webgl_exports = {};
|
|
242
|
+
__export(webgl_exports, {
|
|
243
|
+
WebGLRenderer: () => WebGLRenderer
|
|
244
|
+
});
|
|
245
|
+
function compile(gl, type, source) {
|
|
246
|
+
const shader = gl.createShader(type);
|
|
247
|
+
gl.shaderSource(shader, source);
|
|
248
|
+
gl.compileShader(shader);
|
|
249
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
250
|
+
throw new Error(`3d-spinner: shader compile failed: ${gl.getShaderInfoLog(shader)}`);
|
|
251
|
+
}
|
|
252
|
+
return shader;
|
|
253
|
+
}
|
|
254
|
+
function link(gl) {
|
|
255
|
+
const program = gl.createProgram();
|
|
256
|
+
gl.attachShader(program, compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER));
|
|
257
|
+
gl.attachShader(program, compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER));
|
|
258
|
+
gl.linkProgram(program);
|
|
259
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
260
|
+
throw new Error(`3d-spinner: program link failed: ${gl.getProgramInfoLog(program)}`);
|
|
261
|
+
}
|
|
262
|
+
return program;
|
|
263
|
+
}
|
|
264
|
+
var VERTEX_SHADER, FRAGMENT_SHADER, WebGLRenderer;
|
|
265
|
+
var init_webgl = __esm({
|
|
266
|
+
"src/engines/little-3d-engine/renderers/webgl.ts"() {
|
|
267
|
+
"use strict";
|
|
268
|
+
init_geometry();
|
|
269
|
+
init_renderer();
|
|
270
|
+
VERTEX_SHADER = `#version 300 es
|
|
271
|
+
in vec3 aPos;
|
|
272
|
+
in vec3 aNormal;
|
|
273
|
+
in vec3 aColor;
|
|
274
|
+
uniform mat4 uViewProj;
|
|
275
|
+
uniform mat4 uModel;
|
|
276
|
+
out vec3 vNormal;
|
|
277
|
+
out vec3 vColor;
|
|
278
|
+
void main() {
|
|
279
|
+
vNormal = mat3(uModel) * aNormal;
|
|
280
|
+
vColor = aColor;
|
|
281
|
+
gl_Position = uViewProj * uModel * vec4(aPos, 1.0);
|
|
282
|
+
}`;
|
|
283
|
+
FRAGMENT_SHADER = `#version 300 es
|
|
284
|
+
precision mediump float;
|
|
285
|
+
in vec3 vNormal;
|
|
286
|
+
in vec3 vColor;
|
|
287
|
+
uniform vec3 uToLight;
|
|
288
|
+
uniform float uIntensity;
|
|
289
|
+
uniform float uAmbient;
|
|
290
|
+
uniform float uOpacity;
|
|
291
|
+
out vec4 fragColor;
|
|
292
|
+
void main() {
|
|
293
|
+
float lambert = max(dot(normalize(vNormal), normalize(uToLight)), 0.0);
|
|
294
|
+
float brightness = clamp(uAmbient + uIntensity * lambert, 0.0, 1.0);
|
|
295
|
+
fragColor = vec4(vColor * brightness, uOpacity);
|
|
296
|
+
}`;
|
|
297
|
+
WebGLRenderer = class {
|
|
298
|
+
constructor(options = {}) {
|
|
299
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
300
|
+
if (options.background) {
|
|
301
|
+
const [r, g, b] = parseColor(options.background);
|
|
302
|
+
this.clearColor = [r / 255, g / 255, b / 255, 1];
|
|
303
|
+
} else {
|
|
304
|
+
this.clearColor = [0, 0, 0, 0];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
init(canvas) {
|
|
308
|
+
const gl = canvas.getContext("webgl2");
|
|
309
|
+
if (!gl) throw new Error("3d-spinner: WebGL2 is not supported in this browser.");
|
|
310
|
+
this.gl = gl;
|
|
311
|
+
this.program = link(gl);
|
|
312
|
+
this.locations = {
|
|
313
|
+
aPos: gl.getAttribLocation(this.program, "aPos"),
|
|
314
|
+
aNormal: gl.getAttribLocation(this.program, "aNormal"),
|
|
315
|
+
aColor: gl.getAttribLocation(this.program, "aColor"),
|
|
316
|
+
uViewProj: gl.getUniformLocation(this.program, "uViewProj"),
|
|
317
|
+
uModel: gl.getUniformLocation(this.program, "uModel"),
|
|
318
|
+
uToLight: gl.getUniformLocation(this.program, "uToLight"),
|
|
319
|
+
uIntensity: gl.getUniformLocation(this.program, "uIntensity"),
|
|
320
|
+
uAmbient: gl.getUniformLocation(this.program, "uAmbient"),
|
|
321
|
+
uOpacity: gl.getUniformLocation(this.program, "uOpacity")
|
|
322
|
+
};
|
|
323
|
+
gl.enable(gl.DEPTH_TEST);
|
|
324
|
+
gl.enable(gl.CULL_FACE);
|
|
325
|
+
gl.cullFace(gl.BACK);
|
|
326
|
+
gl.frontFace(gl.CCW);
|
|
327
|
+
}
|
|
328
|
+
resize() {
|
|
329
|
+
const gl = this.gl;
|
|
330
|
+
if (!gl) return;
|
|
331
|
+
const canvas = gl.canvas;
|
|
332
|
+
gl.viewport(0, 0, canvas.width, canvas.height);
|
|
333
|
+
}
|
|
334
|
+
buffers(mesh) {
|
|
335
|
+
const cached = this.cache.get(mesh);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
const gl = this.gl;
|
|
338
|
+
const loc = this.locations;
|
|
339
|
+
const data = expandToTriangles(mesh);
|
|
340
|
+
const vao = gl.createVertexArray();
|
|
341
|
+
gl.bindVertexArray(vao);
|
|
342
|
+
const attribute = (location, array) => {
|
|
343
|
+
if (location < 0) return;
|
|
344
|
+
const buffer = gl.createBuffer();
|
|
345
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
346
|
+
gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);
|
|
347
|
+
gl.enableVertexAttribArray(location);
|
|
348
|
+
gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
|
|
349
|
+
};
|
|
350
|
+
attribute(loc.aPos, data.positions);
|
|
351
|
+
attribute(loc.aNormal, data.normals);
|
|
352
|
+
attribute(loc.aColor, data.colors);
|
|
353
|
+
gl.bindVertexArray(null);
|
|
354
|
+
const result = { vao, count: data.count };
|
|
355
|
+
this.cache.set(mesh, result);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
render(frame) {
|
|
359
|
+
const gl = this.gl;
|
|
360
|
+
const loc = this.locations;
|
|
361
|
+
if (!gl || !this.program || !loc) return;
|
|
362
|
+
gl.clearColor(...this.clearColor);
|
|
363
|
+
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
|
364
|
+
gl.useProgram(this.program);
|
|
365
|
+
gl.uniformMatrix4fv(loc.uViewProj, false, new Float32Array(frame.viewProjection));
|
|
366
|
+
gl.uniform3f(loc.uToLight, frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z);
|
|
367
|
+
gl.uniform1f(loc.uIntensity, frame.light.intensity);
|
|
368
|
+
gl.uniform1f(loc.uAmbient, frame.light.ambient);
|
|
369
|
+
gl.disable(gl.BLEND);
|
|
370
|
+
gl.depthMask(true);
|
|
371
|
+
gl.cullFace(gl.BACK);
|
|
372
|
+
for (const item of frame.items) {
|
|
373
|
+
if (item.transparency) continue;
|
|
374
|
+
const mesh = this.buffers(item.mesh);
|
|
375
|
+
gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
|
|
376
|
+
gl.uniform1f(loc.uOpacity, 1);
|
|
377
|
+
gl.bindVertexArray(mesh.vao);
|
|
378
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
379
|
+
}
|
|
380
|
+
gl.enable(gl.BLEND);
|
|
381
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
382
|
+
gl.depthMask(false);
|
|
383
|
+
for (const item of frame.items) {
|
|
384
|
+
const transparency = item.transparency;
|
|
385
|
+
if (!transparency) continue;
|
|
386
|
+
const mesh = this.buffers(item.mesh);
|
|
387
|
+
gl.uniformMatrix4fv(loc.uModel, false, new Float32Array(item.model));
|
|
388
|
+
gl.bindVertexArray(mesh.vao);
|
|
389
|
+
if (transparency.mode === "two-sided") {
|
|
390
|
+
const resolved = resolveTwoSidedOpacity(transparency);
|
|
391
|
+
gl.cullFace(gl.FRONT);
|
|
392
|
+
gl.uniform1f(loc.uOpacity, resolved.back);
|
|
393
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
394
|
+
gl.cullFace(gl.BACK);
|
|
395
|
+
gl.uniform1f(loc.uOpacity, resolved.front);
|
|
396
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
397
|
+
} else {
|
|
398
|
+
gl.cullFace(gl.BACK);
|
|
399
|
+
gl.uniform1f(loc.uOpacity, opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY));
|
|
400
|
+
gl.drawArrays(gl.TRIANGLES, 0, mesh.count);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
gl.depthMask(true);
|
|
404
|
+
gl.disable(gl.BLEND);
|
|
405
|
+
gl.cullFace(gl.BACK);
|
|
406
|
+
gl.bindVertexArray(null);
|
|
407
|
+
}
|
|
408
|
+
destroy() {
|
|
409
|
+
const gl = this.gl;
|
|
410
|
+
if (gl) {
|
|
411
|
+
for (const mesh of this.cache.values()) gl.deleteVertexArray(mesh.vao);
|
|
412
|
+
if (this.program) gl.deleteProgram(this.program);
|
|
413
|
+
}
|
|
414
|
+
this.cache.clear();
|
|
415
|
+
this.gl = void 0;
|
|
416
|
+
this.program = void 0;
|
|
417
|
+
this.locations = void 0;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// src/engines/little-3d-engine/renderers/webgpu.ts
|
|
424
|
+
var webgpu_exports = {};
|
|
425
|
+
__export(webgpu_exports, {
|
|
426
|
+
WebGPURenderer: () => WebGPURenderer
|
|
427
|
+
});
|
|
428
|
+
var WGSL, CLIP_Z_FIX, UNIFORM_STRIDE, WebGPURenderer;
|
|
429
|
+
var init_webgpu = __esm({
|
|
430
|
+
"src/engines/little-3d-engine/renderers/webgpu.ts"() {
|
|
431
|
+
"use strict";
|
|
432
|
+
init_geometry();
|
|
433
|
+
init_math();
|
|
434
|
+
init_renderer();
|
|
435
|
+
WGSL = `
|
|
436
|
+
struct Uniforms {
|
|
437
|
+
viewProj: mat4x4<f32>,
|
|
438
|
+
model: mat4x4<f32>,
|
|
439
|
+
toLight: vec4<f32>,
|
|
440
|
+
params: vec4<f32>,
|
|
441
|
+
};
|
|
442
|
+
@group(0) @binding(0) var<uniform> u: Uniforms;
|
|
443
|
+
|
|
444
|
+
struct VSOut {
|
|
445
|
+
@builtin(position) position: vec4<f32>,
|
|
446
|
+
@location(0) normal: vec3<f32>,
|
|
447
|
+
@location(1) color: vec3<f32>,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
@vertex
|
|
451
|
+
fn vs(@location(0) pos: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) color: vec3<f32>) -> VSOut {
|
|
452
|
+
var out: VSOut;
|
|
453
|
+
let m = mat3x3<f32>(u.model[0].xyz, u.model[1].xyz, u.model[2].xyz);
|
|
454
|
+
out.normal = m * normal;
|
|
455
|
+
out.color = color;
|
|
456
|
+
out.position = u.viewProj * u.model * vec4<f32>(pos, 1.0);
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@fragment
|
|
461
|
+
fn fs(in: VSOut) -> @location(0) vec4<f32> {
|
|
462
|
+
let lambert = max(dot(normalize(in.normal), normalize(u.toLight.xyz)), 0.0);
|
|
463
|
+
let brightness = clamp(u.params.y + u.params.x * lambert, 0.0, 1.0);
|
|
464
|
+
return vec4<f32>(in.color * brightness, u.params.z);
|
|
465
|
+
}
|
|
466
|
+
`;
|
|
467
|
+
CLIP_Z_FIX = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1];
|
|
468
|
+
UNIFORM_STRIDE = 256;
|
|
469
|
+
WebGPURenderer = class {
|
|
470
|
+
constructor(options = {}) {
|
|
471
|
+
this.uniformCapacity = 0;
|
|
472
|
+
this.depthSize = "";
|
|
473
|
+
this.destroyed = false;
|
|
474
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
475
|
+
if (options.background) {
|
|
476
|
+
const [r, g, b] = parseColor(options.background);
|
|
477
|
+
this.clearValue = { r: r / 255, g: g / 255, b: b / 255, a: 1 };
|
|
478
|
+
this.alphaMode = "opaque";
|
|
479
|
+
} else {
|
|
480
|
+
this.clearValue = { r: 0, g: 0, b: 0, a: 0 };
|
|
481
|
+
this.alphaMode = "premultiplied";
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async init(canvas) {
|
|
485
|
+
const gpu = navigator.gpu;
|
|
486
|
+
if (!gpu) throw new Error("3d-spinner: WebGPU is not supported in this browser.");
|
|
487
|
+
const adapter = await gpu.requestAdapter();
|
|
488
|
+
if (!adapter) throw new Error("3d-spinner: no WebGPU adapter is available.");
|
|
489
|
+
const device = await adapter.requestDevice();
|
|
490
|
+
if (this.destroyed) {
|
|
491
|
+
device.destroy?.();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const context = canvas.getContext("webgpu");
|
|
495
|
+
if (!context) throw new Error("3d-spinner: could not get a WebGPU canvas context.");
|
|
496
|
+
const format = gpu.getPreferredCanvasFormat();
|
|
497
|
+
context.configure({ device, format, alphaMode: this.alphaMode });
|
|
498
|
+
const module2 = device.createShaderModule({ code: WGSL });
|
|
499
|
+
const stage = globalThis.GPUShaderStage;
|
|
500
|
+
const layout = device.createBindGroupLayout({
|
|
501
|
+
entries: [
|
|
502
|
+
{
|
|
503
|
+
binding: 0,
|
|
504
|
+
visibility: stage.VERTEX | stage.FRAGMENT,
|
|
505
|
+
buffer: { type: "uniform", hasDynamicOffset: true, minBindingSize: 160 }
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
});
|
|
509
|
+
const vertexBuffer = (location) => ({
|
|
510
|
+
arrayStride: 12,
|
|
511
|
+
attributes: [{ shaderLocation: location, offset: 0, format: "float32x3" }]
|
|
512
|
+
});
|
|
513
|
+
const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [layout] });
|
|
514
|
+
const blend = {
|
|
515
|
+
color: {
|
|
516
|
+
srcFactor: "src-alpha",
|
|
517
|
+
dstFactor: "one-minus-src-alpha",
|
|
518
|
+
operation: "add"
|
|
519
|
+
},
|
|
520
|
+
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
521
|
+
};
|
|
522
|
+
const pipeline = (cullMode, transparent) => device.createRenderPipeline({
|
|
523
|
+
layout: pipelineLayout,
|
|
524
|
+
vertex: {
|
|
525
|
+
module: module2,
|
|
526
|
+
entryPoint: "vs",
|
|
527
|
+
buffers: [vertexBuffer(0), vertexBuffer(1), vertexBuffer(2)]
|
|
528
|
+
},
|
|
529
|
+
fragment: {
|
|
530
|
+
module: module2,
|
|
531
|
+
entryPoint: "fs",
|
|
532
|
+
targets: [{ format, ...transparent ? { blend } : {} }]
|
|
533
|
+
},
|
|
534
|
+
primitive: { topology: "triangle-list", cullMode, frontFace: "ccw" },
|
|
535
|
+
depthStencil: {
|
|
536
|
+
format: "depth24plus",
|
|
537
|
+
depthWriteEnabled: !transparent,
|
|
538
|
+
depthCompare: "less"
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
this.pipeline = pipeline("back", false);
|
|
542
|
+
this.transparentBackPipeline = pipeline("front", true);
|
|
543
|
+
this.transparentFrontPipeline = pipeline("back", true);
|
|
544
|
+
this.canvas = canvas;
|
|
545
|
+
this.device = device;
|
|
546
|
+
this.context = context;
|
|
547
|
+
}
|
|
548
|
+
resize() {
|
|
549
|
+
this.ensureDepth();
|
|
550
|
+
}
|
|
551
|
+
ensureDepth() {
|
|
552
|
+
const canvas = this.canvas;
|
|
553
|
+
if (!this.device || !canvas) return;
|
|
554
|
+
const width = Math.max(1, canvas.width);
|
|
555
|
+
const height = Math.max(1, canvas.height);
|
|
556
|
+
const key = `${width}x${height}`;
|
|
557
|
+
if (key === this.depthSize && this.depthTexture) return;
|
|
558
|
+
this.depthTexture?.destroy?.();
|
|
559
|
+
this.depthTexture = this.device.createTexture({
|
|
560
|
+
size: { width, height },
|
|
561
|
+
format: "depth24plus",
|
|
562
|
+
usage: globalThis.GPUTextureUsage.RENDER_ATTACHMENT
|
|
563
|
+
});
|
|
564
|
+
this.depthSize = key;
|
|
565
|
+
}
|
|
566
|
+
buffers(mesh) {
|
|
567
|
+
const cached = this.cache.get(mesh);
|
|
568
|
+
if (cached) return cached;
|
|
569
|
+
const data = expandToTriangles(mesh);
|
|
570
|
+
const usage = globalThis.GPUBufferUsage.VERTEX | globalThis.GPUBufferUsage.COPY_DST;
|
|
571
|
+
const upload = (array) => {
|
|
572
|
+
const buffer = this.device.createBuffer({ size: array.byteLength, usage });
|
|
573
|
+
this.device.queue.writeBuffer(buffer, 0, array);
|
|
574
|
+
return buffer;
|
|
575
|
+
};
|
|
576
|
+
const result = {
|
|
577
|
+
position: upload(data.positions),
|
|
578
|
+
normal: upload(data.normals),
|
|
579
|
+
color: upload(data.colors),
|
|
580
|
+
count: data.count
|
|
581
|
+
};
|
|
582
|
+
this.cache.set(mesh, result);
|
|
583
|
+
return result;
|
|
584
|
+
}
|
|
585
|
+
ensureUniformCapacity(draws) {
|
|
586
|
+
if (draws <= this.uniformCapacity && this.uniformBuffer) return;
|
|
587
|
+
this.uniformBuffer?.destroy?.();
|
|
588
|
+
this.uniformBuffer = this.device.createBuffer({
|
|
589
|
+
size: Math.max(1, draws) * UNIFORM_STRIDE,
|
|
590
|
+
usage: globalThis.GPUBufferUsage.UNIFORM | globalThis.GPUBufferUsage.COPY_DST
|
|
591
|
+
});
|
|
592
|
+
this.uniformCapacity = draws;
|
|
593
|
+
}
|
|
594
|
+
render(frame) {
|
|
595
|
+
if (this.destroyed || !this.device || !this.context || !this.pipeline) return;
|
|
596
|
+
if (frame.width === 0 || frame.height === 0 || frame.items.length === 0) return;
|
|
597
|
+
this.ensureDepth();
|
|
598
|
+
const draws = [];
|
|
599
|
+
for (const item of frame.items) {
|
|
600
|
+
if (!item.transparency) draws.push({ item, opacity: 1, pipeline: this.pipeline });
|
|
601
|
+
}
|
|
602
|
+
for (const item of frame.items) {
|
|
603
|
+
const transparency = item.transparency;
|
|
604
|
+
if (!transparency) continue;
|
|
605
|
+
if (transparency.mode === "two-sided") {
|
|
606
|
+
const resolved = resolveTwoSidedOpacity(transparency);
|
|
607
|
+
draws.push({
|
|
608
|
+
item,
|
|
609
|
+
opacity: resolved.back,
|
|
610
|
+
pipeline: this.transparentBackPipeline
|
|
611
|
+
});
|
|
612
|
+
draws.push({
|
|
613
|
+
item,
|
|
614
|
+
opacity: resolved.front,
|
|
615
|
+
pipeline: this.transparentFrontPipeline
|
|
616
|
+
});
|
|
617
|
+
} else {
|
|
618
|
+
draws.push({
|
|
619
|
+
item,
|
|
620
|
+
opacity: opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY),
|
|
621
|
+
pipeline: this.transparentFrontPipeline
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
this.ensureUniformCapacity(draws.length);
|
|
626
|
+
const viewProj = multiply(CLIP_Z_FIX, frame.viewProjection);
|
|
627
|
+
const layout = this.pipeline.getBindGroupLayout(0);
|
|
628
|
+
const bindGroup = this.device.createBindGroup({
|
|
629
|
+
layout,
|
|
630
|
+
entries: [
|
|
631
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer, offset: 0, size: 160 } }
|
|
632
|
+
]
|
|
633
|
+
});
|
|
634
|
+
draws.forEach((draw, i) => {
|
|
635
|
+
const data = new Float32Array(UNIFORM_STRIDE / 4);
|
|
636
|
+
data.set(viewProj, 0);
|
|
637
|
+
data.set(draw.item.model, 16);
|
|
638
|
+
data.set([frame.light.toLight.x, frame.light.toLight.y, frame.light.toLight.z, 0], 32);
|
|
639
|
+
data.set([frame.light.intensity, frame.light.ambient, draw.opacity, 0], 36);
|
|
640
|
+
this.device.queue.writeBuffer(this.uniformBuffer, i * UNIFORM_STRIDE, data);
|
|
641
|
+
});
|
|
642
|
+
const encoder = this.device.createCommandEncoder();
|
|
643
|
+
const pass = encoder.beginRenderPass({
|
|
644
|
+
colorAttachments: [
|
|
645
|
+
{
|
|
646
|
+
view: this.context.getCurrentTexture().createView(),
|
|
647
|
+
clearValue: this.clearValue,
|
|
648
|
+
loadOp: "clear",
|
|
649
|
+
storeOp: "store"
|
|
650
|
+
}
|
|
651
|
+
],
|
|
652
|
+
depthStencilAttachment: {
|
|
653
|
+
view: this.depthTexture.createView(),
|
|
654
|
+
depthClearValue: 1,
|
|
655
|
+
depthLoadOp: "clear",
|
|
656
|
+
depthStoreOp: "store"
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
draws.forEach((draw, i) => {
|
|
660
|
+
const mesh = this.buffers(draw.item.mesh);
|
|
661
|
+
pass.setPipeline(draw.pipeline);
|
|
662
|
+
pass.setBindGroup(0, bindGroup, [i * UNIFORM_STRIDE]);
|
|
663
|
+
pass.setVertexBuffer(0, mesh.position);
|
|
664
|
+
pass.setVertexBuffer(1, mesh.normal);
|
|
665
|
+
pass.setVertexBuffer(2, mesh.color);
|
|
666
|
+
pass.draw(mesh.count);
|
|
667
|
+
});
|
|
668
|
+
pass.end();
|
|
669
|
+
this.device.queue.submit([encoder.finish()]);
|
|
670
|
+
}
|
|
671
|
+
destroy() {
|
|
672
|
+
this.destroyed = true;
|
|
673
|
+
for (const mesh of this.cache.values()) {
|
|
674
|
+
mesh.position.destroy?.();
|
|
675
|
+
mesh.normal.destroy?.();
|
|
676
|
+
mesh.color.destroy?.();
|
|
677
|
+
}
|
|
678
|
+
this.cache.clear();
|
|
679
|
+
this.uniformBuffer?.destroy?.();
|
|
680
|
+
this.depthTexture?.destroy?.();
|
|
681
|
+
this.device?.destroy?.();
|
|
682
|
+
this.device = void 0;
|
|
683
|
+
this.context = void 0;
|
|
684
|
+
this.pipeline = void 0;
|
|
685
|
+
this.transparentBackPipeline = void 0;
|
|
686
|
+
this.transparentFrontPipeline = void 0;
|
|
687
|
+
this.uniformBuffer = void 0;
|
|
688
|
+
this.depthTexture = void 0;
|
|
689
|
+
this.canvas = void 0;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// src/engines/little-3d-engine/renderers/canvas2d.ts
|
|
696
|
+
var canvas2d_exports = {};
|
|
697
|
+
__export(canvas2d_exports, {
|
|
698
|
+
Canvas2DRenderer: () => Canvas2DRenderer
|
|
699
|
+
});
|
|
700
|
+
var Canvas2DRenderer;
|
|
701
|
+
var init_canvas2d = __esm({
|
|
702
|
+
"src/engines/little-3d-engine/renderers/canvas2d.ts"() {
|
|
703
|
+
"use strict";
|
|
704
|
+
init_light();
|
|
705
|
+
init_math();
|
|
706
|
+
init_renderer();
|
|
707
|
+
Canvas2DRenderer = class {
|
|
708
|
+
constructor(options = {}) {
|
|
709
|
+
this.options = options;
|
|
710
|
+
this.dpr = 1;
|
|
711
|
+
}
|
|
712
|
+
init(canvas) {
|
|
713
|
+
this.ctx = canvas.getContext("2d") ?? void 0;
|
|
714
|
+
}
|
|
715
|
+
resize(_cssWidth, _cssHeight, dpr) {
|
|
716
|
+
this.dpr = dpr;
|
|
717
|
+
this.ctx?.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
718
|
+
}
|
|
719
|
+
render(frame) {
|
|
720
|
+
const ctx = this.ctx;
|
|
721
|
+
if (!ctx) return;
|
|
722
|
+
if (this.options.background) {
|
|
723
|
+
ctx.fillStyle = this.options.background;
|
|
724
|
+
ctx.fillRect(0, 0, frame.width, frame.height);
|
|
725
|
+
} else {
|
|
726
|
+
ctx.clearRect(0, 0, frame.width, frame.height);
|
|
727
|
+
}
|
|
728
|
+
const polygons = [];
|
|
729
|
+
for (const item of frame.items) {
|
|
730
|
+
const world = item.mesh.vertices.map((v) => transformAffine(item.model, v));
|
|
731
|
+
const twoSidedOpacity = item.transparency?.mode === "two-sided" ? resolveTwoSidedOpacity(item.transparency) : void 0;
|
|
732
|
+
for (const face of item.mesh.faces) {
|
|
733
|
+
const a = world[face.indices[0]];
|
|
734
|
+
const b = world[face.indices[1]];
|
|
735
|
+
const c = world[face.indices[2]];
|
|
736
|
+
const normal = normalize(cross(subtract(b, a), subtract(c, a)));
|
|
737
|
+
const frontFacing = dot(normal, subtract(frame.eye, a)) > 0;
|
|
738
|
+
const transparency = item.transparency;
|
|
739
|
+
if (!frontFacing && transparency?.mode !== "two-sided") continue;
|
|
740
|
+
let faceOpacity = 1;
|
|
741
|
+
if (transparency?.mode === "one-sided") {
|
|
742
|
+
faceOpacity = opacity(transparency.opacity, DEFAULT_ONE_SIDED_OPACITY);
|
|
743
|
+
} else if (twoSidedOpacity) {
|
|
744
|
+
faceOpacity = frontFacing ? twoSidedOpacity.front : twoSidedOpacity.back;
|
|
745
|
+
}
|
|
746
|
+
const points = face.indices.map((i) => {
|
|
747
|
+
const ndc = transformPoint(frame.viewProjection, world[i]);
|
|
748
|
+
return {
|
|
749
|
+
x: (ndc.x * 0.5 + 0.5) * frame.width,
|
|
750
|
+
y: (1 - (ndc.y * 0.5 + 0.5)) * frame.height
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
let depth = 0;
|
|
754
|
+
for (const i of face.indices) {
|
|
755
|
+
const d = subtract(world[i], frame.eye);
|
|
756
|
+
depth += dot(d, d);
|
|
757
|
+
}
|
|
758
|
+
depth /= face.indices.length;
|
|
759
|
+
polygons.push({
|
|
760
|
+
points,
|
|
761
|
+
color: shadeColor(normal, face.color, frame.light),
|
|
762
|
+
depth,
|
|
763
|
+
opacity: faceOpacity
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
polygons.sort((p, q) => q.depth - p.depth);
|
|
768
|
+
for (const poly of polygons) {
|
|
769
|
+
if (poly.points.length < 3) continue;
|
|
770
|
+
ctx.beginPath();
|
|
771
|
+
ctx.moveTo(poly.points[0].x, poly.points[0].y);
|
|
772
|
+
for (let i = 1; i < poly.points.length; i++) {
|
|
773
|
+
ctx.lineTo(poly.points[i].x, poly.points[i].y);
|
|
774
|
+
}
|
|
775
|
+
ctx.closePath();
|
|
776
|
+
ctx.fillStyle = poly.color;
|
|
777
|
+
ctx.strokeStyle = poly.color;
|
|
778
|
+
ctx.lineWidth = 1;
|
|
779
|
+
ctx.globalAlpha = poly.opacity;
|
|
780
|
+
ctx.fill();
|
|
781
|
+
ctx.stroke();
|
|
782
|
+
}
|
|
783
|
+
ctx.globalAlpha = 1;
|
|
784
|
+
}
|
|
785
|
+
destroy() {
|
|
786
|
+
this.ctx = void 0;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// src/engines/little-3d-engine/renderer.ts
|
|
793
|
+
function opacity(value, fallback) {
|
|
794
|
+
return Math.max(0, Math.min(1, value ?? fallback));
|
|
795
|
+
}
|
|
796
|
+
function resolveTwoSidedOpacity(transparency) {
|
|
797
|
+
const front = opacity(
|
|
798
|
+
transparency.frontOpacity ?? transparency.opacity,
|
|
799
|
+
DEFAULT_FRONT_OPACITY
|
|
800
|
+
);
|
|
801
|
+
const backFallback = transparency.opacity === void 0 ? DEFAULT_BACK_OPACITY : front * (2 / 3);
|
|
802
|
+
return {
|
|
803
|
+
front,
|
|
804
|
+
back: opacity(transparency.backOpacity, backFallback)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function orderRenderItems(items, eye) {
|
|
808
|
+
const opaque = [];
|
|
809
|
+
const transparent = [];
|
|
810
|
+
for (const item of items) {
|
|
811
|
+
(item.transparency ? transparent : opaque).push(item);
|
|
812
|
+
}
|
|
813
|
+
transparent.sort((a, b) => {
|
|
814
|
+
const ax = a.model[12] - eye.x;
|
|
815
|
+
const ay = a.model[13] - eye.y;
|
|
816
|
+
const az = a.model[14] - eye.z;
|
|
817
|
+
const bx = b.model[12] - eye.x;
|
|
818
|
+
const by = b.model[13] - eye.y;
|
|
819
|
+
const bz = b.model[14] - eye.z;
|
|
820
|
+
return bx * bx + by * by + bz * bz - (ax * ax + ay * ay + az * az);
|
|
821
|
+
});
|
|
822
|
+
return opaque.concat(transparent);
|
|
823
|
+
}
|
|
824
|
+
async function createRenderer(backend, options = {}) {
|
|
825
|
+
if (typeof backend === "function") return backend(options);
|
|
826
|
+
switch (backend) {
|
|
827
|
+
case "webgl":
|
|
828
|
+
return new (await Promise.resolve().then(() => (init_webgl(), webgl_exports))).WebGLRenderer(options);
|
|
829
|
+
case "webgpu":
|
|
830
|
+
return new (await Promise.resolve().then(() => (init_webgpu(), webgpu_exports))).WebGPURenderer(options);
|
|
831
|
+
case "canvas2d":
|
|
832
|
+
default:
|
|
833
|
+
return new (await Promise.resolve().then(() => (init_canvas2d(), canvas2d_exports))).Canvas2DRenderer(options);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
var DEFAULT_ONE_SIDED_OPACITY, DEFAULT_BACK_OPACITY, DEFAULT_FRONT_OPACITY;
|
|
837
|
+
var init_renderer = __esm({
|
|
838
|
+
"src/engines/little-3d-engine/renderer.ts"() {
|
|
839
|
+
"use strict";
|
|
840
|
+
DEFAULT_ONE_SIDED_OPACITY = 0.35;
|
|
841
|
+
DEFAULT_BACK_OPACITY = 0.84;
|
|
842
|
+
DEFAULT_FRONT_OPACITY = 0.56;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// src/animations/grid-assembly.ts
|
|
847
|
+
var grid_assembly_exports = {};
|
|
848
|
+
__export(grid_assembly_exports, {
|
|
849
|
+
GridAssemblyAnimation: () => GridAssemblyAnimation
|
|
850
|
+
});
|
|
851
|
+
module.exports = __toCommonJS(grid_assembly_exports);
|
|
852
|
+
|
|
853
|
+
// src/animation-label.ts
|
|
854
|
+
var LABEL_STYLE = [
|
|
855
|
+
"position:absolute",
|
|
856
|
+
"inset:0",
|
|
857
|
+
"display:flex",
|
|
858
|
+
"align-items:center",
|
|
859
|
+
"justify-content:center",
|
|
860
|
+
"pointer-events:none",
|
|
861
|
+
"font:700 1.6rem/1 system-ui,sans-serif",
|
|
862
|
+
"letter-spacing:0.02em",
|
|
863
|
+
"color:rgba(255,255,255,0.9)",
|
|
864
|
+
"text-shadow:0 1px 10px rgba(0,0,0,0.6)",
|
|
865
|
+
"z-index:1"
|
|
866
|
+
].join(";");
|
|
867
|
+
function animationLabelOpacity(now, enterAt, introDurationMs, exitAt, outroDurationMs) {
|
|
868
|
+
if (enterAt === Infinity) return 0;
|
|
869
|
+
const intro = introDurationMs <= 0 ? 1 : Math.max(0, Math.min(1, (now - enterAt) / introDurationMs));
|
|
870
|
+
const outro = exitAt === Infinity ? 1 : outroDurationMs <= 0 ? 0 : Math.max(0, Math.min(1, 1 - (now - exitAt) / outroDurationMs));
|
|
871
|
+
return Math.min(intro, outro);
|
|
872
|
+
}
|
|
873
|
+
function mountAnimationLabel(target, content) {
|
|
874
|
+
var _a;
|
|
875
|
+
const container = document.createElement("div");
|
|
876
|
+
container.style.cssText = LABEL_STYLE;
|
|
877
|
+
container.setAttribute("role", "status");
|
|
878
|
+
if (typeof content === "string") container.textContent = content;
|
|
879
|
+
else if (content) {
|
|
880
|
+
(_a = content.style).pointerEvents || (_a.pointerEvents = "auto");
|
|
881
|
+
container.appendChild(content);
|
|
882
|
+
}
|
|
883
|
+
target.appendChild(container);
|
|
884
|
+
return {
|
|
885
|
+
container,
|
|
886
|
+
setText(value) {
|
|
887
|
+
if (typeof content !== "object") container.textContent = value;
|
|
888
|
+
},
|
|
889
|
+
setOpacity(value) {
|
|
890
|
+
container.style.opacity = String(value);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/engines/little-3d-engine/core/camera.ts
|
|
896
|
+
init_math();
|
|
897
|
+
var DEFAULTS = {
|
|
898
|
+
position: { x: 0, y: 0, z: 4 },
|
|
899
|
+
fov: 55 * Math.PI / 180,
|
|
900
|
+
near: 0.1,
|
|
901
|
+
far: 100
|
|
902
|
+
};
|
|
903
|
+
var Camera = class {
|
|
904
|
+
constructor(options) {
|
|
905
|
+
this.options = { ...DEFAULTS, ...options };
|
|
906
|
+
}
|
|
907
|
+
/** Transform a world-space point into view (camera) space. */
|
|
908
|
+
toView(p) {
|
|
909
|
+
const { position } = this.options;
|
|
910
|
+
return transformAffine(translation(-position.x, -position.y, -position.z), p);
|
|
911
|
+
}
|
|
912
|
+
/** Combined view-projection matrix for the given viewport aspect ratio. */
|
|
913
|
+
viewProjection(aspect) {
|
|
914
|
+
const { position, fov, near, far } = this.options;
|
|
915
|
+
const view = translation(-position.x, -position.y, -position.z);
|
|
916
|
+
const projection = perspective(fov, aspect, near, far);
|
|
917
|
+
return multiply(projection, view);
|
|
918
|
+
}
|
|
919
|
+
/** Convert a normalized device coordinate (-1..1) to a pixel position. */
|
|
920
|
+
toScreen(ndc, width, height) {
|
|
921
|
+
return {
|
|
922
|
+
x: (ndc.x * 0.5 + 0.5) * width,
|
|
923
|
+
y: (1 - (ndc.y * 0.5 + 0.5)) * height
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
929
|
+
init_light();
|
|
930
|
+
init_math();
|
|
931
|
+
|
|
932
|
+
// src/engines/little-3d-engine/core/mesh.ts
|
|
933
|
+
function transform(init) {
|
|
934
|
+
return {
|
|
935
|
+
position: init?.position ?? { x: 0, y: 0, z: 0 },
|
|
936
|
+
rotation: init?.rotation ?? { x: 0, y: 0, z: 0 },
|
|
937
|
+
scale: init?.scale ?? 1
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
942
|
+
init_renderer();
|
|
943
|
+
init_light();
|
|
944
|
+
|
|
945
|
+
// src/engines/little-3d-engine/shapes/primitives/cube.ts
|
|
946
|
+
var DEFAULT_COLORS = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
947
|
+
function cube(size = 1, colors = DEFAULT_COLORS) {
|
|
948
|
+
const h = size / 2;
|
|
949
|
+
const vertices = [
|
|
950
|
+
{ x: -h, y: -h, z: h },
|
|
951
|
+
{ x: h, y: -h, z: h },
|
|
952
|
+
{ x: h, y: h, z: h },
|
|
953
|
+
{ x: -h, y: h, z: h },
|
|
954
|
+
{ x: -h, y: -h, z: -h },
|
|
955
|
+
{ x: h, y: -h, z: -h },
|
|
956
|
+
{ x: h, y: h, z: -h },
|
|
957
|
+
{ x: -h, y: h, z: -h }
|
|
958
|
+
];
|
|
959
|
+
const faces = [
|
|
960
|
+
{ indices: [0, 1, 2, 3], color: colors[0 % colors.length] },
|
|
961
|
+
{ indices: [5, 4, 7, 6], color: colors[1 % colors.length] },
|
|
962
|
+
{ indices: [3, 2, 6, 7], color: colors[2 % colors.length] },
|
|
963
|
+
{ indices: [4, 5, 1, 0], color: colors[3 % colors.length] },
|
|
964
|
+
{ indices: [1, 5, 6, 2], color: colors[4 % colors.length] },
|
|
965
|
+
{ indices: [4, 0, 3, 7], color: colors[5 % colors.length] }
|
|
966
|
+
];
|
|
967
|
+
return { vertices, faces };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// src/engines/little-3d-engine/shapes/primitives/tetrahedron.ts
|
|
971
|
+
var DEFAULT_COLORS2 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b"];
|
|
972
|
+
function tetrahedron(size = 1, colors = DEFAULT_COLORS2) {
|
|
973
|
+
const s = size / 2;
|
|
974
|
+
const vertices = [
|
|
975
|
+
{ x: s, y: s, z: s },
|
|
976
|
+
{ x: s, y: -s, z: -s },
|
|
977
|
+
{ x: -s, y: s, z: -s },
|
|
978
|
+
{ x: -s, y: -s, z: s }
|
|
979
|
+
];
|
|
980
|
+
const faces = [
|
|
981
|
+
{ indices: [0, 1, 2], color: colors[0 % colors.length] },
|
|
982
|
+
{ indices: [0, 3, 1], color: colors[1 % colors.length] },
|
|
983
|
+
{ indices: [0, 2, 3], color: colors[2 % colors.length] },
|
|
984
|
+
{ indices: [1, 3, 2], color: colors[3 % colors.length] }
|
|
985
|
+
];
|
|
986
|
+
return { vertices, faces };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/engines/little-3d-engine/shapes/primitives/octahedron.ts
|
|
990
|
+
var DEFAULT_COLORS3 = [
|
|
991
|
+
"#3b82f6",
|
|
992
|
+
"#8b5cf6",
|
|
993
|
+
"#ec4899",
|
|
994
|
+
"#f59e0b",
|
|
995
|
+
"#10b981",
|
|
996
|
+
"#ef4444",
|
|
997
|
+
"#06b6d4",
|
|
998
|
+
"#eab308"
|
|
999
|
+
];
|
|
1000
|
+
function octahedron(size = 1, colors = DEFAULT_COLORS3) {
|
|
1001
|
+
const r = size / 2;
|
|
1002
|
+
const vertices = [
|
|
1003
|
+
{ x: r, y: 0, z: 0 },
|
|
1004
|
+
{ x: -r, y: 0, z: 0 },
|
|
1005
|
+
{ x: 0, y: r, z: 0 },
|
|
1006
|
+
{ x: 0, y: -r, z: 0 },
|
|
1007
|
+
{ x: 0, y: 0, z: r },
|
|
1008
|
+
{ x: 0, y: 0, z: -r }
|
|
1009
|
+
];
|
|
1010
|
+
const faces = [
|
|
1011
|
+
{ indices: [4, 0, 2], color: colors[0 % colors.length] },
|
|
1012
|
+
{ indices: [4, 2, 1], color: colors[1 % colors.length] },
|
|
1013
|
+
{ indices: [4, 1, 3], color: colors[2 % colors.length] },
|
|
1014
|
+
{ indices: [4, 3, 0], color: colors[3 % colors.length] },
|
|
1015
|
+
{ indices: [5, 2, 0], color: colors[4 % colors.length] },
|
|
1016
|
+
{ indices: [5, 1, 2], color: colors[5 % colors.length] },
|
|
1017
|
+
{ indices: [5, 3, 1], color: colors[6 % colors.length] },
|
|
1018
|
+
{ indices: [5, 0, 3], color: colors[7 % colors.length] }
|
|
1019
|
+
];
|
|
1020
|
+
return { vertices, faces };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/engines/little-3d-engine/shapes/primitives/pyramid.ts
|
|
1024
|
+
var DEFAULT_COLORS4 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981"];
|
|
1025
|
+
function pyramid(size = 1, colors = DEFAULT_COLORS4) {
|
|
1026
|
+
const h = size / 2;
|
|
1027
|
+
const vertices = [
|
|
1028
|
+
{ x: -h, y: -h, z: h },
|
|
1029
|
+
{ x: h, y: -h, z: h },
|
|
1030
|
+
{ x: h, y: -h, z: -h },
|
|
1031
|
+
{ x: -h, y: -h, z: -h },
|
|
1032
|
+
{ x: 0, y: h, z: 0 }
|
|
1033
|
+
];
|
|
1034
|
+
const faces = [
|
|
1035
|
+
{ indices: [0, 3, 2, 1], color: colors[0 % colors.length] },
|
|
1036
|
+
{ indices: [4, 0, 1], color: colors[1 % colors.length] },
|
|
1037
|
+
{ indices: [4, 1, 2], color: colors[2 % colors.length] },
|
|
1038
|
+
{ indices: [4, 2, 3], color: colors[3 % colors.length] },
|
|
1039
|
+
{ indices: [4, 3, 0], color: colors[4 % colors.length] }
|
|
1040
|
+
];
|
|
1041
|
+
return { vertices, faces };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/engines/little-3d-engine/shapes/primitives/spheres/icosphere.ts
|
|
1045
|
+
init_geometry();
|
|
1046
|
+
var DEFAULT_COLORS5 = ["#3b82f6", "#8b5cf6", "#ec4899", "#f59e0b", "#10b981", "#ef4444"];
|
|
1047
|
+
var T = (1 + Math.sqrt(5)) / 2;
|
|
1048
|
+
var SEED_VERTICES = [
|
|
1049
|
+
{ x: -1, y: T, z: 0 },
|
|
1050
|
+
{ x: 1, y: T, z: 0 },
|
|
1051
|
+
{ x: -1, y: -T, z: 0 },
|
|
1052
|
+
{ x: 1, y: -T, z: 0 },
|
|
1053
|
+
{ x: 0, y: -1, z: T },
|
|
1054
|
+
{ x: 0, y: 1, z: T },
|
|
1055
|
+
{ x: 0, y: -1, z: -T },
|
|
1056
|
+
{ x: 0, y: 1, z: -T },
|
|
1057
|
+
{ x: T, y: 0, z: -1 },
|
|
1058
|
+
{ x: T, y: 0, z: 1 },
|
|
1059
|
+
{ x: -T, y: 0, z: -1 },
|
|
1060
|
+
{ x: -T, y: 0, z: 1 }
|
|
1061
|
+
];
|
|
1062
|
+
var SEED_FACES = [
|
|
1063
|
+
[0, 11, 5],
|
|
1064
|
+
[0, 5, 1],
|
|
1065
|
+
[0, 1, 7],
|
|
1066
|
+
[0, 7, 10],
|
|
1067
|
+
[0, 10, 11],
|
|
1068
|
+
[1, 5, 9],
|
|
1069
|
+
[5, 11, 4],
|
|
1070
|
+
[11, 10, 2],
|
|
1071
|
+
[10, 7, 6],
|
|
1072
|
+
[7, 1, 8],
|
|
1073
|
+
[3, 9, 4],
|
|
1074
|
+
[3, 4, 2],
|
|
1075
|
+
[3, 2, 6],
|
|
1076
|
+
[3, 6, 8],
|
|
1077
|
+
[3, 8, 9],
|
|
1078
|
+
[4, 9, 5],
|
|
1079
|
+
[2, 4, 11],
|
|
1080
|
+
[6, 2, 10],
|
|
1081
|
+
[8, 6, 7],
|
|
1082
|
+
[9, 8, 1]
|
|
1083
|
+
];
|
|
1084
|
+
function icosphere(size = 1, detail = 1, colors = DEFAULT_COLORS5) {
|
|
1085
|
+
return sphereFromTriangles(SEED_VERTICES, SEED_FACES, size, detail, colors);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/engines/little-3d-engine/shapes/primitives/spheres/octa-sphere.ts
|
|
1089
|
+
init_geometry();
|
|
1090
|
+
|
|
1091
|
+
// src/engines/little-3d-engine/little-3d-engine.ts
|
|
1092
|
+
init_geometry();
|
|
1093
|
+
init_renderer();
|
|
1094
|
+
init_math();
|
|
1095
|
+
function modelMatrix(t) {
|
|
1096
|
+
const rotation = multiply(
|
|
1097
|
+
rotationZ(t.rotation.z),
|
|
1098
|
+
multiply(rotationY(t.rotation.y), rotationX(t.rotation.x))
|
|
1099
|
+
);
|
|
1100
|
+
return multiply(
|
|
1101
|
+
translation(t.position.x, t.position.y, t.position.z),
|
|
1102
|
+
multiply(rotation, scaleMatrix(t.scale))
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
var Little3dEngine = class {
|
|
1106
|
+
constructor(options = {}) {
|
|
1107
|
+
this.scene = [];
|
|
1108
|
+
this.cssWidth = 0;
|
|
1109
|
+
this.cssHeight = 0;
|
|
1110
|
+
this.ready = false;
|
|
1111
|
+
this.generation = 0;
|
|
1112
|
+
this.rafId = 0;
|
|
1113
|
+
this.running = false;
|
|
1114
|
+
this.camera = new Camera(options.camera);
|
|
1115
|
+
this.light = new Light(options.light);
|
|
1116
|
+
this.backend = options.backend ?? "canvas2d";
|
|
1117
|
+
this.background = options.background;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Create the canvas inside `target`, load the selected backend, and start
|
|
1121
|
+
* tracking size. Resolves once the renderer is ready; rejects if the backend
|
|
1122
|
+
* is unavailable. Drawing is a no-op until it resolves.
|
|
1123
|
+
*/
|
|
1124
|
+
async mount(target) {
|
|
1125
|
+
const canvas = document.createElement("canvas");
|
|
1126
|
+
canvas.style.display = "block";
|
|
1127
|
+
canvas.style.width = "100%";
|
|
1128
|
+
canvas.style.height = "100%";
|
|
1129
|
+
target.appendChild(canvas);
|
|
1130
|
+
this.canvas = canvas;
|
|
1131
|
+
this.observer = new ResizeObserver(() => this.resize());
|
|
1132
|
+
this.observer.observe(canvas);
|
|
1133
|
+
this.resize();
|
|
1134
|
+
const generation = this.generation;
|
|
1135
|
+
const dropCanvas = () => {
|
|
1136
|
+
if (this.canvas !== canvas) return;
|
|
1137
|
+
this.observer?.disconnect();
|
|
1138
|
+
this.observer = void 0;
|
|
1139
|
+
canvas.remove();
|
|
1140
|
+
this.canvas = void 0;
|
|
1141
|
+
};
|
|
1142
|
+
try {
|
|
1143
|
+
const renderer = await createRenderer(this.backend, { background: this.background });
|
|
1144
|
+
if (generation !== this.generation) {
|
|
1145
|
+
renderer.destroy();
|
|
1146
|
+
dropCanvas();
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
await renderer.init(canvas);
|
|
1150
|
+
if (generation !== this.generation) {
|
|
1151
|
+
renderer.destroy();
|
|
1152
|
+
dropCanvas();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
this.renderer = renderer;
|
|
1156
|
+
this.resize();
|
|
1157
|
+
this.ready = true;
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
dropCanvas();
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
/** Add a mesh to the scene and return a handle for animating it. */
|
|
1164
|
+
add(mesh, init) {
|
|
1165
|
+
const entry = {
|
|
1166
|
+
mesh,
|
|
1167
|
+
transform: transform(init),
|
|
1168
|
+
transparency: init?.transparency,
|
|
1169
|
+
remove: () => {
|
|
1170
|
+
const i = this.scene.indexOf(entry);
|
|
1171
|
+
if (i >= 0) this.scene.splice(i, 1);
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
this.scene.push(entry);
|
|
1175
|
+
return entry;
|
|
1176
|
+
}
|
|
1177
|
+
resize() {
|
|
1178
|
+
const canvas = this.canvas;
|
|
1179
|
+
if (!canvas) return;
|
|
1180
|
+
const dpr = window.devicePixelRatio || 1;
|
|
1181
|
+
this.cssWidth = canvas.clientWidth || canvas.parentElement?.clientWidth || 0;
|
|
1182
|
+
this.cssHeight = canvas.clientHeight || canvas.parentElement?.clientHeight || 0;
|
|
1183
|
+
canvas.width = Math.max(1, Math.round(this.cssWidth * dpr));
|
|
1184
|
+
canvas.height = Math.max(1, Math.round(this.cssHeight * dpr));
|
|
1185
|
+
this.renderer?.resize(this.cssWidth, this.cssHeight, dpr);
|
|
1186
|
+
}
|
|
1187
|
+
/** Draw a single frame from the current scene state. */
|
|
1188
|
+
render() {
|
|
1189
|
+
if (!this.ready || !this.renderer) return;
|
|
1190
|
+
const width = this.cssWidth;
|
|
1191
|
+
const height = this.cssHeight;
|
|
1192
|
+
if (width === 0 || height === 0) return;
|
|
1193
|
+
const items = this.scene.map((entry) => ({
|
|
1194
|
+
mesh: entry.mesh,
|
|
1195
|
+
model: modelMatrix(entry.transform),
|
|
1196
|
+
transparency: entry.transparency
|
|
1197
|
+
}));
|
|
1198
|
+
const eye = this.camera.options.position;
|
|
1199
|
+
this.renderer.render({
|
|
1200
|
+
items: orderRenderItems(items, eye),
|
|
1201
|
+
viewProjection: this.camera.viewProjection(width / height),
|
|
1202
|
+
eye,
|
|
1203
|
+
light: this.light.params,
|
|
1204
|
+
width,
|
|
1205
|
+
height
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
/** Start an internal animation loop that calls {@link render} each frame. */
|
|
1209
|
+
start() {
|
|
1210
|
+
if (this.running) return;
|
|
1211
|
+
this.running = true;
|
|
1212
|
+
const loop = () => {
|
|
1213
|
+
if (!this.running) return;
|
|
1214
|
+
this.render();
|
|
1215
|
+
this.rafId = requestAnimationFrame(loop);
|
|
1216
|
+
};
|
|
1217
|
+
this.rafId = requestAnimationFrame(loop);
|
|
1218
|
+
}
|
|
1219
|
+
/** Stop the internal animation loop started by {@link start}. */
|
|
1220
|
+
stop() {
|
|
1221
|
+
this.running = false;
|
|
1222
|
+
if (this.rafId) cancelAnimationFrame(this.rafId);
|
|
1223
|
+
this.rafId = 0;
|
|
1224
|
+
}
|
|
1225
|
+
/** Stop animating, release the renderer, and remove the canvas. */
|
|
1226
|
+
destroy() {
|
|
1227
|
+
this.generation++;
|
|
1228
|
+
this.ready = false;
|
|
1229
|
+
this.stop();
|
|
1230
|
+
this.observer?.disconnect();
|
|
1231
|
+
this.observer = void 0;
|
|
1232
|
+
this.renderer?.destroy();
|
|
1233
|
+
this.renderer = void 0;
|
|
1234
|
+
this.canvas?.remove();
|
|
1235
|
+
this.canvas = void 0;
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
// src/engines/little-tween-engine/core/tweens.ts
|
|
1240
|
+
function input(value, overextend) {
|
|
1241
|
+
if (Number.isNaN(value)) return 0;
|
|
1242
|
+
if (overextend) return value;
|
|
1243
|
+
return Math.min(1, Math.max(0, value));
|
|
1244
|
+
}
|
|
1245
|
+
function easeInCubic(value, overextend = false) {
|
|
1246
|
+
const x = input(value, overextend);
|
|
1247
|
+
return x * x * x;
|
|
1248
|
+
}
|
|
1249
|
+
function easeOutCubic(value, overextend = false) {
|
|
1250
|
+
const x = input(value, overextend);
|
|
1251
|
+
return 1 - Math.pow(1 - x, 3);
|
|
1252
|
+
}
|
|
1253
|
+
function easeInOutCubic(value, overextend = false) {
|
|
1254
|
+
const x = input(value, overextend);
|
|
1255
|
+
return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/animations/grid-assembly.ts
|
|
1259
|
+
var GRID = 5;
|
|
1260
|
+
var COUNT = GRID * GRID;
|
|
1261
|
+
var CAMERA_Z = 4;
|
|
1262
|
+
var FOV = 55 * Math.PI / 180;
|
|
1263
|
+
var TWO_PI = Math.PI * 2;
|
|
1264
|
+
var INTRO_MS = 900;
|
|
1265
|
+
var INTRO_STAGGER_MS = 60;
|
|
1266
|
+
var GATE_DOCK = 0.35;
|
|
1267
|
+
var GATE_UNDOCK = 0.65;
|
|
1268
|
+
var EXIT_HURRY = 2.5;
|
|
1269
|
+
var SPIN_EVERY_MS = 2e3;
|
|
1270
|
+
var SPIN_MS = 380;
|
|
1271
|
+
var SPIN_STAGGER_MS = 80;
|
|
1272
|
+
var HOLD_MS = 1e3;
|
|
1273
|
+
var COLLAPSE_MS = 700;
|
|
1274
|
+
var POP_MS = 170;
|
|
1275
|
+
var LABEL_FADE_MS = 600;
|
|
1276
|
+
var DEFAULT_MESHES = [
|
|
1277
|
+
() => cube(1, ["#60a5fa", "#3b82f6", "#2563eb", "#38bdf8", "#0ea5e9", "#1d4ed8"]),
|
|
1278
|
+
() => tetrahedron(1, ["#f472b6", "#ec4899", "#db2777", "#f9a8d4"]),
|
|
1279
|
+
() => octahedron(1, ["#34d399", "#10b981", "#059669", "#6ee7b7", "#a7f3d0", "#047857", "#4ade80", "#065f46"]),
|
|
1280
|
+
() => pyramid(1, ["#fbbf24", "#f59e0b", "#d97706", "#fde68a", "#fcd34d"]),
|
|
1281
|
+
() => icosphere(1, 1, ["#a78bfa", "#8b5cf6", "#7c3aed", "#c4b5fd"])
|
|
1282
|
+
];
|
|
1283
|
+
function clamp012(value) {
|
|
1284
|
+
return Math.max(0, Math.min(1, value));
|
|
1285
|
+
}
|
|
1286
|
+
function smooth01(value) {
|
|
1287
|
+
const x = clamp012(value);
|
|
1288
|
+
return x * x * (3 - 2 * x);
|
|
1289
|
+
}
|
|
1290
|
+
function resolveMesh(mesh) {
|
|
1291
|
+
return typeof mesh === "function" ? mesh() : mesh;
|
|
1292
|
+
}
|
|
1293
|
+
function wrapAngle(angle) {
|
|
1294
|
+
const wrapped = angle - TWO_PI * Math.floor(angle / TWO_PI);
|
|
1295
|
+
return wrapped > Math.PI ? wrapped - TWO_PI : wrapped;
|
|
1296
|
+
}
|
|
1297
|
+
function hash01(index, salt) {
|
|
1298
|
+
let h = (Math.imul(index + 1, 2654435769) ^ Math.imul(salt + 1, 2246822507)) >>> 0;
|
|
1299
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
1300
|
+
h ^= h >>> 16;
|
|
1301
|
+
return (h >>> 0) / 4294967296;
|
|
1302
|
+
}
|
|
1303
|
+
var GridAssemblyAnimation = class {
|
|
1304
|
+
constructor(options = {}) {
|
|
1305
|
+
this.handles = [];
|
|
1306
|
+
this.blends = new Array(COUNT).fill(0);
|
|
1307
|
+
this.dockedAt = new Array(COUNT).fill(Infinity);
|
|
1308
|
+
this.tumbleX = [];
|
|
1309
|
+
this.tumbleY = [];
|
|
1310
|
+
this.fades = [];
|
|
1311
|
+
this.slots = [];
|
|
1312
|
+
this.aspect = 16 / 9;
|
|
1313
|
+
this.enterAt = Infinity;
|
|
1314
|
+
this.exitAt = Infinity;
|
|
1315
|
+
this.allDockedAt = Infinity;
|
|
1316
|
+
this.collapseAt = Infinity;
|
|
1317
|
+
this.lastNow = 0;
|
|
1318
|
+
this.popFading = false;
|
|
1319
|
+
this.finished = false;
|
|
1320
|
+
const sources = options.meshes && options.meshes.length > 0 ? options.meshes : DEFAULT_MESHES;
|
|
1321
|
+
this.meshes = sources.map(resolveMesh);
|
|
1322
|
+
this.size = options.size ?? 0.34;
|
|
1323
|
+
this.orbitPeriodMs = options.orbitPeriodMs ?? 9e3;
|
|
1324
|
+
this.dockMs = options.dockMs ?? 800;
|
|
1325
|
+
this.backend = options.backend;
|
|
1326
|
+
this.labelContent = options.label;
|
|
1327
|
+
this.fadeLabel = options.fadeLabel ?? true;
|
|
1328
|
+
const spacing = this.size + (options.gap ?? 0.12);
|
|
1329
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1330
|
+
const row = Math.floor(i / GRID);
|
|
1331
|
+
const col = i % GRID;
|
|
1332
|
+
this.slots.push({ x: (col - 2) * spacing, y: (2 - row) * spacing, z: 0 });
|
|
1333
|
+
this.tumbleX.push(TWO_PI * hash01(i, 2) - Math.PI);
|
|
1334
|
+
this.tumbleY.push(TWO_PI * hash01(i, 4) - Math.PI);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
mount(target) {
|
|
1338
|
+
if (!target.style.position) target.style.position = "relative";
|
|
1339
|
+
const engine = new Little3dEngine({
|
|
1340
|
+
backend: this.backend,
|
|
1341
|
+
camera: { position: { x: 0, y: 0, z: CAMERA_Z }, fov: FOV }
|
|
1342
|
+
});
|
|
1343
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1344
|
+
this.fades.push({ mode: "one-sided", opacity: 1 });
|
|
1345
|
+
this.handles.push(engine.add(this.meshes[i % this.meshes.length], { scale: 0 }));
|
|
1346
|
+
}
|
|
1347
|
+
this.engine = engine;
|
|
1348
|
+
engine.mount(target).catch((error) => {
|
|
1349
|
+
target.textContent = error instanceof Error ? error.message : String(error);
|
|
1350
|
+
});
|
|
1351
|
+
const measure = () => {
|
|
1352
|
+
if (target.clientWidth > 0 && target.clientHeight > 0) {
|
|
1353
|
+
this.aspect = target.clientWidth / target.clientHeight;
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
measure();
|
|
1357
|
+
this.observer = new ResizeObserver(measure);
|
|
1358
|
+
this.observer.observe(target);
|
|
1359
|
+
this.label = mountAnimationLabel(target, this.labelContent);
|
|
1360
|
+
if (this.fadeLabel) this.label.setOpacity(0);
|
|
1361
|
+
}
|
|
1362
|
+
enter(now) {
|
|
1363
|
+
if (this.enterAt === Infinity) this.enterAt = now;
|
|
1364
|
+
}
|
|
1365
|
+
exit(now) {
|
|
1366
|
+
if (this.exitAt === Infinity) this.exitAt = now;
|
|
1367
|
+
}
|
|
1368
|
+
isFinished() {
|
|
1369
|
+
return this.finished;
|
|
1370
|
+
}
|
|
1371
|
+
render(now, frame) {
|
|
1372
|
+
if (!this.engine || !this.label) return;
|
|
1373
|
+
if (this.enterAt === Infinity) {
|
|
1374
|
+
for (const handle of this.handles) handle.transform.scale = 0;
|
|
1375
|
+
this.engine.render();
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
const dt = this.lastNow === 0 ? 16 : Math.min(50, now - this.lastNow);
|
|
1379
|
+
this.lastNow = now;
|
|
1380
|
+
this.updateBlends(dt, frame.progress, now);
|
|
1381
|
+
if (this.exitAt !== Infinity && this.allDockedAt !== Infinity && this.collapseAt === Infinity) {
|
|
1382
|
+
this.collapseAt = Math.max(this.exitAt, this.allDockedAt) + HOLD_MS;
|
|
1383
|
+
}
|
|
1384
|
+
if (now >= this.collapseAt) this.renderCollapse(now);
|
|
1385
|
+
else this.renderStory(now, dt);
|
|
1386
|
+
this.label.setText(frame.indeterminate ? typeof this.labelContent === "string" ? this.labelContent : "" : `${Math.round(frame.progress * 100)}%`);
|
|
1387
|
+
if (this.fadeLabel) {
|
|
1388
|
+
this.label.setOpacity(animationLabelOpacity(
|
|
1389
|
+
now,
|
|
1390
|
+
this.enterAt,
|
|
1391
|
+
LABEL_FADE_MS,
|
|
1392
|
+
this.collapseAt,
|
|
1393
|
+
COLLAPSE_MS
|
|
1394
|
+
));
|
|
1395
|
+
}
|
|
1396
|
+
if (this.collapseAt !== Infinity && now >= this.collapseAt + COLLAPSE_MS + POP_MS) {
|
|
1397
|
+
this.finished = true;
|
|
1398
|
+
}
|
|
1399
|
+
this.engine.render();
|
|
1400
|
+
}
|
|
1401
|
+
destroy() {
|
|
1402
|
+
this.observer?.disconnect();
|
|
1403
|
+
this.observer = void 0;
|
|
1404
|
+
this.label?.container.remove();
|
|
1405
|
+
this.label = void 0;
|
|
1406
|
+
this.engine?.destroy();
|
|
1407
|
+
this.engine = void 0;
|
|
1408
|
+
this.handles.length = 0;
|
|
1409
|
+
this.fades.length = 0;
|
|
1410
|
+
}
|
|
1411
|
+
updateBlends(dt, progress, now) {
|
|
1412
|
+
const exiting = this.exitAt !== Infinity;
|
|
1413
|
+
const want = exiting ? COUNT : Math.min(COUNT, Math.floor(progress * COUNT + 1e-9));
|
|
1414
|
+
const rate = dt / this.dockMs * (exiting ? EXIT_HURRY : 1);
|
|
1415
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1416
|
+
const target = i < want ? 1 : 0;
|
|
1417
|
+
const blend = this.blends[i];
|
|
1418
|
+
if (target > blend && (i === 0 || this.blends[i - 1] >= GATE_DOCK)) {
|
|
1419
|
+
this.blends[i] = Math.min(1, blend + rate);
|
|
1420
|
+
} else if (target < blend && (i === COUNT - 1 || this.blends[i + 1] <= GATE_UNDOCK)) {
|
|
1421
|
+
this.blends[i] = Math.max(0, blend - rate);
|
|
1422
|
+
}
|
|
1423
|
+
if (this.blends[i] >= 1) {
|
|
1424
|
+
if (this.dockedAt[i] === Infinity) this.dockedAt[i] = now;
|
|
1425
|
+
} else {
|
|
1426
|
+
this.dockedAt[i] = Infinity;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
const allDocked = want === COUNT && this.blends.every((blend) => blend >= 1);
|
|
1430
|
+
if (allDocked) {
|
|
1431
|
+
if (this.allDockedAt === Infinity) this.allDockedAt = now;
|
|
1432
|
+
} else if (!exiting) {
|
|
1433
|
+
this.allDockedAt = Infinity;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
renderStory(now, dt) {
|
|
1437
|
+
const t = now - this.enterAt;
|
|
1438
|
+
const halfHeight = Math.tan(FOV / 2) * CAMERA_Z;
|
|
1439
|
+
const halfWidth = halfHeight * this.aspect;
|
|
1440
|
+
const radius = Math.max(0.6, Math.min(halfWidth, halfHeight) - this.size * 0.55);
|
|
1441
|
+
const spawnFactor = (Math.max(halfWidth, halfHeight) * 1.25 + this.size * 2) / radius;
|
|
1442
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1443
|
+
const transform2 = this.handles[i].transform;
|
|
1444
|
+
const eased = smooth01(this.blends[i]);
|
|
1445
|
+
const introT = clamp012((t - i * INTRO_STAGGER_MS) / INTRO_MS);
|
|
1446
|
+
const reach = 1 + (spawnFactor - 1) * (1 - easeOutCubic(introT));
|
|
1447
|
+
const angle = Math.PI / 2 - i / COUNT * TWO_PI - t / this.orbitPeriodMs * TWO_PI;
|
|
1448
|
+
const orbitX = Math.cos(angle) * radius * reach;
|
|
1449
|
+
const orbitY = Math.sin(angle) * radius * reach;
|
|
1450
|
+
const slot = this.slots[i];
|
|
1451
|
+
transform2.position.x = orbitX + (slot.x - orbitX) * eased;
|
|
1452
|
+
transform2.position.y = orbitY + (slot.y - orbitY) * eased;
|
|
1453
|
+
transform2.position.z = 0;
|
|
1454
|
+
transform2.scale = this.size;
|
|
1455
|
+
if (this.blends[i] === 0) {
|
|
1456
|
+
this.tumbleX[i] = wrapAngle(this.tumbleX[i] + (4e-4 + 8e-4 * hash01(i, 1)) * dt);
|
|
1457
|
+
this.tumbleY[i] = wrapAngle(this.tumbleY[i] + (6e-4 + 1e-3 * hash01(i, 3)) * dt);
|
|
1458
|
+
}
|
|
1459
|
+
transform2.rotation.z = 0;
|
|
1460
|
+
if (this.blends[i] >= 1) {
|
|
1461
|
+
const phase = (t + i * SPIN_STAGGER_MS) % SPIN_EVERY_MS;
|
|
1462
|
+
const spinning = phase < SPIN_MS && now - phase >= this.dockedAt[i];
|
|
1463
|
+
transform2.rotation.x = 0;
|
|
1464
|
+
transform2.rotation.y = spinning ? TWO_PI * easeInOutCubic(phase / SPIN_MS) : 0;
|
|
1465
|
+
} else {
|
|
1466
|
+
const free = 1 - eased;
|
|
1467
|
+
transform2.rotation.x = this.tumbleX[i] * free;
|
|
1468
|
+
transform2.rotation.y = this.tumbleY[i] * free;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
renderCollapse(now) {
|
|
1473
|
+
if (!this.captured) {
|
|
1474
|
+
this.captured = this.handles.map((handle) => ({ ...handle.transform.position }));
|
|
1475
|
+
}
|
|
1476
|
+
const u = clamp012((now - this.collapseAt) / COLLAPSE_MS);
|
|
1477
|
+
const pull = easeInCubic(u);
|
|
1478
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1479
|
+
const transform2 = this.handles[i].transform;
|
|
1480
|
+
const from = this.captured[i];
|
|
1481
|
+
transform2.position.x = from.x * (1 - pull);
|
|
1482
|
+
transform2.position.y = from.y * (1 - pull);
|
|
1483
|
+
transform2.position.z = from.z * (1 - pull);
|
|
1484
|
+
transform2.scale = this.size * (1 - 0.99 * pull);
|
|
1485
|
+
}
|
|
1486
|
+
if (u >= 1) {
|
|
1487
|
+
if (!this.popFading) {
|
|
1488
|
+
this.popFading = true;
|
|
1489
|
+
for (let i = 0; i < COUNT; i++) this.handles[i].transparency = this.fades[i];
|
|
1490
|
+
}
|
|
1491
|
+
const v = clamp012((now - this.collapseAt - COLLAPSE_MS) / POP_MS);
|
|
1492
|
+
for (let i = 0; i < COUNT; i++) {
|
|
1493
|
+
this.fades[i].opacity = 1 - v;
|
|
1494
|
+
this.handles[i].transform.scale = v >= 1 ? 0 : this.size * 0.01 * (1 + 1.6 * Math.sin(Math.PI * v));
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
};
|