@vectojs/three 0.1.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/dist/index.d.mts +123 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +711 -0
- package/dist/index.mjs +675 -0
- package/package.json +35 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/ThreeRenderer.ts
|
|
2
|
+
import { parseColorToRGBA } from "@vectojs/core";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
var WebGLGradient = class {
|
|
5
|
+
constructor(x0, y0, x1, y1, colorStops) {
|
|
6
|
+
this.x0 = x0;
|
|
7
|
+
this.y0 = y0;
|
|
8
|
+
this.x1 = x1;
|
|
9
|
+
this.y1 = y1;
|
|
10
|
+
this.colorStops = colorStops;
|
|
11
|
+
}
|
|
12
|
+
x0;
|
|
13
|
+
y0;
|
|
14
|
+
x1;
|
|
15
|
+
y1;
|
|
16
|
+
colorStops;
|
|
17
|
+
type = "linear";
|
|
18
|
+
};
|
|
19
|
+
var ThreePath = class {
|
|
20
|
+
shapes = [];
|
|
21
|
+
currentShape = null;
|
|
22
|
+
constructor() {
|
|
23
|
+
this.moveTo(0, 0);
|
|
24
|
+
}
|
|
25
|
+
getShape() {
|
|
26
|
+
if (!this.currentShape) {
|
|
27
|
+
this.currentShape = new THREE.Shape();
|
|
28
|
+
this.shapes.push(this.currentShape);
|
|
29
|
+
}
|
|
30
|
+
return this.currentShape;
|
|
31
|
+
}
|
|
32
|
+
moveTo(x, y) {
|
|
33
|
+
if (this.currentShape && this.currentShape.curves.length > 0) {
|
|
34
|
+
this.currentShape = new THREE.Shape();
|
|
35
|
+
this.shapes.push(this.currentShape);
|
|
36
|
+
}
|
|
37
|
+
this.getShape().moveTo(x, y);
|
|
38
|
+
}
|
|
39
|
+
lineTo(x, y) {
|
|
40
|
+
this.getShape().lineTo(x, y);
|
|
41
|
+
}
|
|
42
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
43
|
+
this.getShape().bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
|
44
|
+
}
|
|
45
|
+
closePath() {
|
|
46
|
+
this.currentShape?.closePath();
|
|
47
|
+
}
|
|
48
|
+
arc(x, y, radius, startAngle, endAngle, counterclockwise) {
|
|
49
|
+
this.getShape().absarc(x, y, radius, startAngle, endAngle, counterclockwise);
|
|
50
|
+
}
|
|
51
|
+
toShapes() {
|
|
52
|
+
return this.shapes;
|
|
53
|
+
}
|
|
54
|
+
getPoints() {
|
|
55
|
+
const allPoints = [];
|
|
56
|
+
for (const shape of this.shapes) {
|
|
57
|
+
allPoints.push(...shape.getPoints());
|
|
58
|
+
}
|
|
59
|
+
return allPoints;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var ThreeRenderer = class {
|
|
63
|
+
scene;
|
|
64
|
+
camera;
|
|
65
|
+
renderer;
|
|
66
|
+
width;
|
|
67
|
+
height;
|
|
68
|
+
matrix;
|
|
69
|
+
stack = [];
|
|
70
|
+
globalAlpha = 1;
|
|
71
|
+
alphaStack = [];
|
|
72
|
+
currentPath = null;
|
|
73
|
+
activeObjects = [];
|
|
74
|
+
scissorStack = [];
|
|
75
|
+
constructor(canvas) {
|
|
76
|
+
this.width = window.innerWidth;
|
|
77
|
+
this.height = window.innerHeight;
|
|
78
|
+
this.scene = new THREE.Scene();
|
|
79
|
+
this.camera = new THREE.OrthographicCamera(0, this.width, 0, this.height, 0.1, 1e3);
|
|
80
|
+
this.camera.position.z = 1;
|
|
81
|
+
this.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true });
|
|
82
|
+
this.renderer.setSize(this.width, this.height);
|
|
83
|
+
this.renderer.setPixelRatio(window.devicePixelRatio || 1);
|
|
84
|
+
this.matrix = new THREE.Matrix4().identity();
|
|
85
|
+
}
|
|
86
|
+
resize(width, height) {
|
|
87
|
+
this.width = width;
|
|
88
|
+
this.height = height;
|
|
89
|
+
this.renderer.setSize(width, height);
|
|
90
|
+
this.camera.right = width;
|
|
91
|
+
this.camera.bottom = height;
|
|
92
|
+
this.camera.updateProjectionMatrix();
|
|
93
|
+
}
|
|
94
|
+
clear() {
|
|
95
|
+
for (const obj of this.activeObjects) {
|
|
96
|
+
this.scene.remove(obj);
|
|
97
|
+
if (obj instanceof THREE.Mesh || obj instanceof THREE.Line) {
|
|
98
|
+
obj.geometry.dispose();
|
|
99
|
+
if (Array.isArray(obj.material)) {
|
|
100
|
+
for (const mat of obj.material) {
|
|
101
|
+
mat.dispose();
|
|
102
|
+
if (mat.map) mat.map.dispose();
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
obj.material.dispose();
|
|
106
|
+
if (obj.material.map) {
|
|
107
|
+
obj.material.map.dispose();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.activeObjects = [];
|
|
113
|
+
this.renderer.clear();
|
|
114
|
+
this.renderer.setScissorTest(false);
|
|
115
|
+
}
|
|
116
|
+
save() {
|
|
117
|
+
this.stack.push(this.matrix.clone());
|
|
118
|
+
this.alphaStack.push(this.globalAlpha);
|
|
119
|
+
const box = new THREE.Vector4();
|
|
120
|
+
this.renderer.getScissor(box);
|
|
121
|
+
this.scissorStack.push({
|
|
122
|
+
enabled: this.renderer.getScissorTest(),
|
|
123
|
+
box
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
restore() {
|
|
127
|
+
if (this.stack.length > 0) this.matrix.copy(this.stack.pop());
|
|
128
|
+
if (this.alphaStack.length > 0) this.globalAlpha = this.alphaStack.pop();
|
|
129
|
+
if (this.scissorStack.length > 0) {
|
|
130
|
+
const state = this.scissorStack.pop();
|
|
131
|
+
this.renderer.setScissorTest(state.enabled);
|
|
132
|
+
this.renderer.setScissor(state.box);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
translate(x, y) {
|
|
136
|
+
const m = new THREE.Matrix4().makeTranslation(x, y, 0);
|
|
137
|
+
this.matrix.multiply(m);
|
|
138
|
+
}
|
|
139
|
+
scale(x, y) {
|
|
140
|
+
const m = new THREE.Matrix4().makeScale(x, y, 1);
|
|
141
|
+
this.matrix.multiply(m);
|
|
142
|
+
}
|
|
143
|
+
rotate(angle) {
|
|
144
|
+
const m = new THREE.Matrix4().makeRotationZ(angle);
|
|
145
|
+
this.matrix.multiply(m);
|
|
146
|
+
}
|
|
147
|
+
setGlobalAlpha(alpha) {
|
|
148
|
+
this.globalAlpha = alpha;
|
|
149
|
+
}
|
|
150
|
+
clip(x, y, width, height) {
|
|
151
|
+
const pos = new THREE.Vector3(x, y, 0).applyMatrix4(this.matrix);
|
|
152
|
+
const size = new THREE.Vector3(width, height, 0).applyMatrix4(this.matrix).sub(new THREE.Vector3(0, 0, 0).applyMatrix4(this.matrix));
|
|
153
|
+
const dpr = window.devicePixelRatio || 1;
|
|
154
|
+
const canvasHeight = this.renderer.domElement.height / dpr;
|
|
155
|
+
this.renderer.setScissor(
|
|
156
|
+
pos.x * dpr,
|
|
157
|
+
(canvasHeight - (pos.y + size.y)) * dpr,
|
|
158
|
+
size.x * dpr,
|
|
159
|
+
size.y * dpr
|
|
160
|
+
);
|
|
161
|
+
this.renderer.setScissorTest(true);
|
|
162
|
+
}
|
|
163
|
+
beginPath() {
|
|
164
|
+
this.currentPath = new ThreePath();
|
|
165
|
+
}
|
|
166
|
+
moveTo(x, y) {
|
|
167
|
+
this.currentPath?.moveTo(x, y);
|
|
168
|
+
}
|
|
169
|
+
lineTo(x, y) {
|
|
170
|
+
this.currentPath?.lineTo(x, y);
|
|
171
|
+
}
|
|
172
|
+
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
|
|
173
|
+
this.currentPath?.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
|
|
174
|
+
}
|
|
175
|
+
closePath() {
|
|
176
|
+
this.currentPath?.closePath();
|
|
177
|
+
}
|
|
178
|
+
arc(x, y, radius, startAngle, endAngle, counterclockwise) {
|
|
179
|
+
this.currentPath?.arc(x, y, radius, startAngle, endAngle, counterclockwise);
|
|
180
|
+
}
|
|
181
|
+
roundRect(x, y, width, height, radii) {
|
|
182
|
+
if (!this.currentPath) return;
|
|
183
|
+
if (width < 0) {
|
|
184
|
+
x += width;
|
|
185
|
+
width = -width;
|
|
186
|
+
}
|
|
187
|
+
if (height < 0) {
|
|
188
|
+
y += height;
|
|
189
|
+
height = -height;
|
|
190
|
+
}
|
|
191
|
+
let r_tl = 0, r_tr = 0, r_br = 0, r_bl = 0;
|
|
192
|
+
if (typeof radii === "number") {
|
|
193
|
+
r_tl = r_tr = r_br = r_bl = radii;
|
|
194
|
+
} else if (Array.isArray(radii)) {
|
|
195
|
+
if (radii.length === 1) {
|
|
196
|
+
r_tl = r_tr = r_br = r_bl = radii[0];
|
|
197
|
+
} else if (radii.length === 2) {
|
|
198
|
+
r_tl = r_br = radii[0];
|
|
199
|
+
r_tr = r_bl = radii[1];
|
|
200
|
+
} else if (radii.length === 3) {
|
|
201
|
+
r_tl = radii[0];
|
|
202
|
+
r_tr = r_bl = radii[1];
|
|
203
|
+
r_br = radii[2];
|
|
204
|
+
} else if (radii.length >= 4) {
|
|
205
|
+
r_tl = radii[0];
|
|
206
|
+
r_tr = radii[1];
|
|
207
|
+
r_br = radii[2];
|
|
208
|
+
r_bl = radii[3];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const tl_tr = r_tl + r_tr;
|
|
212
|
+
const bl_br = r_bl + r_br;
|
|
213
|
+
const tl_bl = r_tl + r_bl;
|
|
214
|
+
const tr_br = r_tr + r_br;
|
|
215
|
+
let factor = 1;
|
|
216
|
+
if (tl_tr > width) factor = Math.min(factor, width / tl_tr);
|
|
217
|
+
if (bl_br > width) factor = Math.min(factor, width / bl_br);
|
|
218
|
+
if (tl_bl > height) factor = Math.min(factor, height / tl_bl);
|
|
219
|
+
if (tr_br > height) factor = Math.min(factor, height / tr_br);
|
|
220
|
+
if (factor < 1) {
|
|
221
|
+
r_tl *= factor;
|
|
222
|
+
r_tr *= factor;
|
|
223
|
+
r_br *= factor;
|
|
224
|
+
r_bl *= factor;
|
|
225
|
+
}
|
|
226
|
+
this.currentPath.moveTo(x + r_tl, y);
|
|
227
|
+
this.currentPath.lineTo(x + width - r_tr, y);
|
|
228
|
+
this.currentPath.arc(x + width - r_tr, y + r_tr, r_tr, -Math.PI / 2, 0, false);
|
|
229
|
+
this.currentPath.lineTo(x + width, y + height - r_br);
|
|
230
|
+
this.currentPath.arc(x + width - r_br, y + height - r_br, r_br, 0, Math.PI / 2, false);
|
|
231
|
+
this.currentPath.lineTo(x + r_bl, y + height);
|
|
232
|
+
this.currentPath.arc(x + r_bl, y + height - r_bl, r_bl, Math.PI / 2, Math.PI, false);
|
|
233
|
+
this.currentPath.lineTo(x, y + r_tl);
|
|
234
|
+
this.currentPath.arc(x + r_tl, y + r_tl, r_tl, Math.PI, -Math.PI / 2, false);
|
|
235
|
+
}
|
|
236
|
+
drawImage(source, dx, dy, dw, dh) {
|
|
237
|
+
let texture;
|
|
238
|
+
if (source instanceof HTMLImageElement || source instanceof HTMLCanvasElement || typeof ImageBitmap !== "undefined" && source instanceof ImageBitmap) {
|
|
239
|
+
texture = new THREE.CanvasTexture(source);
|
|
240
|
+
} else {
|
|
241
|
+
texture = new THREE.Texture(source);
|
|
242
|
+
texture.needsUpdate = true;
|
|
243
|
+
}
|
|
244
|
+
texture.minFilter = THREE.LinearFilter;
|
|
245
|
+
const material = new THREE.MeshBasicMaterial({
|
|
246
|
+
map: texture,
|
|
247
|
+
transparent: true,
|
|
248
|
+
opacity: this.globalAlpha,
|
|
249
|
+
depthWrite: false
|
|
250
|
+
});
|
|
251
|
+
const geometry = new THREE.PlaneGeometry(dw, dh);
|
|
252
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
253
|
+
mesh.position.set(dx + dw / 2, dy + dh / 2, 0);
|
|
254
|
+
mesh.applyMatrix4(this.matrix);
|
|
255
|
+
this.scene.add(mesh);
|
|
256
|
+
this.activeObjects.push(mesh);
|
|
257
|
+
}
|
|
258
|
+
fill(colorOrGradient) {
|
|
259
|
+
if (!this.currentPath) return;
|
|
260
|
+
if (colorOrGradient && colorOrGradient.type === "linear") {
|
|
261
|
+
const grad = colorOrGradient;
|
|
262
|
+
const sortedStops = [...grad.colorStops].sort((a, b) => a.stop - b.stop);
|
|
263
|
+
if (sortedStops.length < 2) {
|
|
264
|
+
const fallbackColor = sortedStops.length === 1 ? sortedStops[0].color : "#ffffff";
|
|
265
|
+
this.fillSolidShape(fallbackColor);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const finalColors = [];
|
|
269
|
+
const finalStops = [];
|
|
270
|
+
if (sortedStops.length > 8) {
|
|
271
|
+
const lerpColor = (c1, c2, f) => {
|
|
272
|
+
const rgba1 = parseColorToRGBA(c1);
|
|
273
|
+
const rgba2 = parseColorToRGBA(c2);
|
|
274
|
+
return new THREE.Vector4(
|
|
275
|
+
rgba1[0] + (rgba2[0] - rgba1[0]) * f,
|
|
276
|
+
rgba1[1] + (rgba2[1] - rgba1[1]) * f,
|
|
277
|
+
rgba1[2] + (rgba2[2] - rgba1[2]) * f,
|
|
278
|
+
rgba1[3] + (rgba2[3] - rgba1[3]) * f
|
|
279
|
+
);
|
|
280
|
+
};
|
|
281
|
+
for (let k = 0; k < 8; k++) {
|
|
282
|
+
const t = k / 7;
|
|
283
|
+
if (t <= sortedStops[0].stop) {
|
|
284
|
+
const rgba = parseColorToRGBA(sortedStops[0].color);
|
|
285
|
+
finalColors.push(new THREE.Vector4(rgba[0], rgba[1], rgba[2], rgba[3]));
|
|
286
|
+
} else if (t >= sortedStops[sortedStops.length - 1].stop) {
|
|
287
|
+
const rgba = parseColorToRGBA(sortedStops[sortedStops.length - 1].color);
|
|
288
|
+
finalColors.push(new THREE.Vector4(rgba[0], rgba[1], rgba[2], rgba[3]));
|
|
289
|
+
} else {
|
|
290
|
+
let i = 0;
|
|
291
|
+
for (let idx = 0; idx < sortedStops.length - 1; idx++) {
|
|
292
|
+
if (t >= sortedStops[idx].stop && t <= sortedStops[idx + 1].stop) {
|
|
293
|
+
i = idx;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const gap = sortedStops[i + 1].stop - sortedStops[i].stop;
|
|
298
|
+
const f = gap > 1e-4 ? (t - sortedStops[i].stop) / gap : 0;
|
|
299
|
+
finalColors.push(lerpColor(sortedStops[i].color, sortedStops[i + 1].color, f));
|
|
300
|
+
}
|
|
301
|
+
finalStops.push(t);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
for (let i = 0; i < 8; i++) {
|
|
305
|
+
const stopIdx = Math.min(i, sortedStops.length - 1);
|
|
306
|
+
const stop = sortedStops[stopIdx];
|
|
307
|
+
const rgba = parseColorToRGBA(stop.color);
|
|
308
|
+
finalColors.push(new THREE.Vector4(rgba[0], rgba[1], rgba[2], rgba[3]));
|
|
309
|
+
finalStops.push(stop.stop);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const u_grad_start = new THREE.Vector3(grad.x0, grad.y0, 0).applyMatrix4(this.matrix);
|
|
313
|
+
const u_grad_end = new THREE.Vector3(grad.x1, grad.y1, 0).applyMatrix4(this.matrix);
|
|
314
|
+
const shapes = this.currentPath.toShapes();
|
|
315
|
+
for (const shape of shapes) {
|
|
316
|
+
const geometry = new THREE.ShapeGeometry(shape);
|
|
317
|
+
const material = new THREE.ShaderMaterial({
|
|
318
|
+
uniforms: {
|
|
319
|
+
u_grad_start: { value: new THREE.Vector2(u_grad_start.x, u_grad_start.y) },
|
|
320
|
+
u_grad_end: { value: new THREE.Vector2(u_grad_end.x, u_grad_end.y) },
|
|
321
|
+
u_grad_colors: { value: finalColors },
|
|
322
|
+
u_grad_stops: { value: finalStops },
|
|
323
|
+
u_global_alpha: { value: this.globalAlpha }
|
|
324
|
+
},
|
|
325
|
+
vertexShader: `
|
|
326
|
+
varying vec2 v_world_pos;
|
|
327
|
+
void main() {
|
|
328
|
+
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
329
|
+
v_world_pos = worldPos.xy;
|
|
330
|
+
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
|
331
|
+
}
|
|
332
|
+
`,
|
|
333
|
+
fragmentShader: `
|
|
334
|
+
varying vec2 v_world_pos;
|
|
335
|
+
uniform vec2 u_grad_start;
|
|
336
|
+
uniform vec2 u_grad_end;
|
|
337
|
+
uniform vec4 u_grad_colors[8];
|
|
338
|
+
uniform float u_grad_stops[8];
|
|
339
|
+
uniform float u_global_alpha;
|
|
340
|
+
|
|
341
|
+
void main() {
|
|
342
|
+
vec2 d = u_grad_end - u_grad_start;
|
|
343
|
+
float d_len_sq = dot(d, d);
|
|
344
|
+
float t = 0.0;
|
|
345
|
+
if (d_len_sq > 0.0001) {
|
|
346
|
+
t = clamp(dot(v_world_pos - u_grad_start, d) / d_len_sq, 0.0, 1.0);
|
|
347
|
+
}
|
|
348
|
+
vec4 finalColor = u_grad_colors[7];
|
|
349
|
+
if (t <= u_grad_stops[0]) {
|
|
350
|
+
finalColor = u_grad_colors[0];
|
|
351
|
+
} else {
|
|
352
|
+
for (int i = 0; i < 7; i++) {
|
|
353
|
+
if (t >= u_grad_stops[i] && t <= u_grad_stops[i+1]) {
|
|
354
|
+
float gap = u_grad_stops[i+1] - u_grad_stops[i];
|
|
355
|
+
float factor = gap > 0.0001 ? (t - u_grad_stops[i]) / gap : 0.0;
|
|
356
|
+
finalColor = mix(u_grad_colors[i], u_grad_colors[i+1], factor);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
gl_FragColor = vec4(finalColor.rgb, finalColor.a * u_global_alpha);
|
|
362
|
+
}
|
|
363
|
+
`,
|
|
364
|
+
transparent: true,
|
|
365
|
+
depthWrite: false
|
|
366
|
+
});
|
|
367
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
368
|
+
mesh.applyMatrix4(this.matrix);
|
|
369
|
+
this.scene.add(mesh);
|
|
370
|
+
this.activeObjects.push(mesh);
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
this.fillSolidShape(colorOrGradient);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
fillSolidShape(colorOrGradient) {
|
|
377
|
+
if (!this.currentPath) return;
|
|
378
|
+
const color = typeof colorOrGradient === "string" ? colorOrGradient : "#ffffff";
|
|
379
|
+
const shapes = this.currentPath.toShapes();
|
|
380
|
+
for (const shape of shapes) {
|
|
381
|
+
const geometry = new THREE.ShapeGeometry(shape);
|
|
382
|
+
const material = new THREE.MeshBasicMaterial({
|
|
383
|
+
color: new THREE.Color(color),
|
|
384
|
+
transparent: this.globalAlpha < 1,
|
|
385
|
+
opacity: this.globalAlpha,
|
|
386
|
+
depthWrite: false
|
|
387
|
+
});
|
|
388
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
389
|
+
mesh.applyMatrix4(this.matrix);
|
|
390
|
+
this.scene.add(mesh);
|
|
391
|
+
this.activeObjects.push(mesh);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
stroke(colorOrGradient, lineWidth = 1) {
|
|
395
|
+
if (!this.currentPath) return;
|
|
396
|
+
let color = "#ffffff";
|
|
397
|
+
if (typeof colorOrGradient === "string") {
|
|
398
|
+
color = colorOrGradient;
|
|
399
|
+
} else if (colorOrGradient && colorOrGradient.type === "linear") {
|
|
400
|
+
const grad = colorOrGradient;
|
|
401
|
+
if (grad.colorStops && grad.colorStops.length > 0) {
|
|
402
|
+
color = grad.colorStops[0].color;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const points = this.currentPath.getPoints();
|
|
406
|
+
if (points.length === 0) return;
|
|
407
|
+
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
408
|
+
const material = new THREE.LineBasicMaterial({
|
|
409
|
+
color: new THREE.Color(color),
|
|
410
|
+
transparent: this.globalAlpha < 1,
|
|
411
|
+
opacity: this.globalAlpha,
|
|
412
|
+
linewidth: lineWidth
|
|
413
|
+
});
|
|
414
|
+
const line = new THREE.Line(geometry, material);
|
|
415
|
+
line.applyMatrix4(this.matrix);
|
|
416
|
+
this.scene.add(line);
|
|
417
|
+
this.activeObjects.push(line);
|
|
418
|
+
}
|
|
419
|
+
fillText(text, x, y, font, color) {
|
|
420
|
+
if (typeof document === "undefined") return;
|
|
421
|
+
const canvas = document.createElement("canvas");
|
|
422
|
+
const ctx = canvas.getContext("2d");
|
|
423
|
+
ctx.font = font;
|
|
424
|
+
const width = Math.max(1, Math.ceil(ctx.measureText(text).width));
|
|
425
|
+
const fontSize = parseInt(font) || 16;
|
|
426
|
+
const height = Math.max(1, Math.ceil(fontSize * 1.5));
|
|
427
|
+
canvas.width = width;
|
|
428
|
+
canvas.height = height;
|
|
429
|
+
ctx.font = font;
|
|
430
|
+
let fillCol = "#ffffff";
|
|
431
|
+
if (typeof color === "string") {
|
|
432
|
+
fillCol = color;
|
|
433
|
+
} else if (color && color.type === "linear") {
|
|
434
|
+
const grad = color;
|
|
435
|
+
if (grad.colorStops && grad.colorStops.length > 0) {
|
|
436
|
+
fillCol = grad.colorStops[0].color;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
ctx.fillStyle = fillCol;
|
|
440
|
+
ctx.textBaseline = "alphabetic";
|
|
441
|
+
ctx.fillText(text, 0, fontSize);
|
|
442
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
443
|
+
texture.minFilter = THREE.LinearFilter;
|
|
444
|
+
const material = new THREE.MeshBasicMaterial({
|
|
445
|
+
map: texture,
|
|
446
|
+
transparent: true,
|
|
447
|
+
opacity: this.globalAlpha,
|
|
448
|
+
depthWrite: false
|
|
449
|
+
});
|
|
450
|
+
const geometry = new THREE.PlaneGeometry(width, height);
|
|
451
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
452
|
+
mesh.position.set(x + width / 2, y - height / 2 + fontSize, 0);
|
|
453
|
+
mesh.applyMatrix4(this.matrix);
|
|
454
|
+
this.scene.add(mesh);
|
|
455
|
+
this.activeObjects.push(mesh);
|
|
456
|
+
}
|
|
457
|
+
fillCircle(cx, cy, radius, color, alpha = 1) {
|
|
458
|
+
const geometry = new THREE.CircleGeometry(radius, 32);
|
|
459
|
+
const material = new THREE.MeshBasicMaterial({
|
|
460
|
+
color: new THREE.Color(color),
|
|
461
|
+
transparent: this.globalAlpha * alpha < 1,
|
|
462
|
+
opacity: this.globalAlpha * alpha,
|
|
463
|
+
depthWrite: false
|
|
464
|
+
});
|
|
465
|
+
const mesh = new THREE.Mesh(geometry, material);
|
|
466
|
+
mesh.position.set(cx, cy, 0);
|
|
467
|
+
mesh.applyMatrix4(this.matrix);
|
|
468
|
+
this.scene.add(mesh);
|
|
469
|
+
this.activeObjects.push(mesh);
|
|
470
|
+
}
|
|
471
|
+
flush() {
|
|
472
|
+
this.renderer.render(this.scene, this.camera);
|
|
473
|
+
}
|
|
474
|
+
createLinearGradient(x0, y0, x1, y1, colorStops) {
|
|
475
|
+
return new WebGLGradient(x0, y0, x1, y1, colorStops);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/ThreeAdapter.ts
|
|
480
|
+
import * as THREE2 from "three";
|
|
481
|
+
import {
|
|
482
|
+
Scene as VectoScene,
|
|
483
|
+
VectoJSEvent
|
|
484
|
+
} from "@vectojs/core";
|
|
485
|
+
var ThreeAdapter = class {
|
|
486
|
+
/** The Three.js CanvasTexture wrapping the offscreen Vecto canvas. */
|
|
487
|
+
texture;
|
|
488
|
+
/** The active VectoJS Scene instance. */
|
|
489
|
+
vectoScene;
|
|
490
|
+
/** The offscreen HTMLCanvasElement on which Vecto draws. */
|
|
491
|
+
canvas;
|
|
492
|
+
/** A pre-built THREE.Mesh with PlaneGeometry and this texture for immediate use. */
|
|
493
|
+
mesh;
|
|
494
|
+
/** Track hover states independently per pointerId for WebXR / Multi-Touch. */
|
|
495
|
+
activePointers = /* @__PURE__ */ new Map();
|
|
496
|
+
constructor(options) {
|
|
497
|
+
this.canvas = options.canvas || (typeof document !== "undefined" ? document.createElement("canvas") : { width: options.width, height: options.height });
|
|
498
|
+
this.canvas.width = options.width;
|
|
499
|
+
this.canvas.height = options.height;
|
|
500
|
+
const sceneOptions = {
|
|
501
|
+
...options.sceneOptions,
|
|
502
|
+
disableWindowResize: true
|
|
503
|
+
};
|
|
504
|
+
this.vectoScene = new VectoScene(this.canvas, sceneOptions);
|
|
505
|
+
this.vectoScene.resize(options.width, options.height);
|
|
506
|
+
this.texture = new THREE2.CanvasTexture(this.canvas);
|
|
507
|
+
this.texture.minFilter = THREE2.LinearFilter;
|
|
508
|
+
this.texture.magFilter = THREE2.LinearFilter;
|
|
509
|
+
const originalRender = this.vectoScene.render;
|
|
510
|
+
this.vectoScene.render = (renderer, dt, time) => {
|
|
511
|
+
originalRender.call(this.vectoScene, renderer, dt, time);
|
|
512
|
+
this.texture.needsUpdate = true;
|
|
513
|
+
};
|
|
514
|
+
const geometry = new THREE2.PlaneGeometry(1, 1);
|
|
515
|
+
const material = new THREE2.MeshBasicMaterial({
|
|
516
|
+
map: this.texture,
|
|
517
|
+
transparent: true,
|
|
518
|
+
depthWrite: false
|
|
519
|
+
});
|
|
520
|
+
this.mesh = new THREE2.Mesh(geometry, material);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Processes 3D Raycasting intersections and forwards pointer/scroll events.
|
|
524
|
+
* Call this from window/document event listeners passing the raycaster.
|
|
525
|
+
*
|
|
526
|
+
* @param raycaster Three.js Raycaster instance.
|
|
527
|
+
* @param type Pointer event type: 'pointerdown' | 'pointerup' | 'pointermove' | 'wheel' | 'click'.
|
|
528
|
+
* @param originalEvent Optional original DOM Event to forward scroll deltas or button states.
|
|
529
|
+
* @returns true if the ray intersected the VectoJS mesh; false otherwise.
|
|
530
|
+
*/
|
|
531
|
+
updateIntersection(raycaster, type, originalEvent) {
|
|
532
|
+
const intersects = raycaster.intersectObject(this.mesh);
|
|
533
|
+
const pointerId = originalEvent instanceof PointerEvent ? originalEvent.pointerId : 1;
|
|
534
|
+
let state = this.activePointers.get(pointerId);
|
|
535
|
+
if (!state) {
|
|
536
|
+
state = { isHovering: false, lastUv: new THREE2.Vector2(), lastTargetId: null };
|
|
537
|
+
this.activePointers.set(pointerId, state);
|
|
538
|
+
}
|
|
539
|
+
if (intersects.length > 0) {
|
|
540
|
+
const hit = intersects[0];
|
|
541
|
+
if (hit.uv) {
|
|
542
|
+
state.lastUv.copy(hit.uv);
|
|
543
|
+
state.isHovering = true;
|
|
544
|
+
this.dispatchAtUv(type, hit.uv, pointerId, originalEvent);
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (state.isHovering) {
|
|
549
|
+
state.isHovering = false;
|
|
550
|
+
this.dispatchAtUv("pointerleave", state.lastUv, pointerId, originalEvent);
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Dispatches pointer events mapped from UV coordinates [0, 1] to VectoJS entities.
|
|
556
|
+
*/
|
|
557
|
+
dispatchAtUv(type, uv, pointerId, originalEvent) {
|
|
558
|
+
const px = uv.x * this.canvas.width;
|
|
559
|
+
const py = (1 - uv.y) * this.canvas.height;
|
|
560
|
+
this.vectoScene.markDirty();
|
|
561
|
+
const hitEntity = this.vectoScene.findEntityAt(px, py);
|
|
562
|
+
const state = this.activePointers.get(pointerId);
|
|
563
|
+
if (state && type === "pointermove") {
|
|
564
|
+
const currentTargetId = hitEntity ? hitEntity.id : null;
|
|
565
|
+
if (currentTargetId !== state.lastTargetId) {
|
|
566
|
+
if (state.lastTargetId) {
|
|
567
|
+
const oldEntity = this.findEntityById(this.vectoScene.getRoot(), state.lastTargetId);
|
|
568
|
+
if (oldEntity) {
|
|
569
|
+
this.dispatchEventToTarget(oldEntity, "pointerleave", px, py, pointerId, originalEvent);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (hitEntity) {
|
|
573
|
+
this.dispatchEventToTarget(hitEntity, "hover", px, py, pointerId, originalEvent);
|
|
574
|
+
}
|
|
575
|
+
state.lastTargetId = currentTargetId;
|
|
576
|
+
}
|
|
577
|
+
} else if (state && type === "pointerleave") {
|
|
578
|
+
if (state.lastTargetId) {
|
|
579
|
+
const oldEntity = this.findEntityById(this.vectoScene.getRoot(), state.lastTargetId);
|
|
580
|
+
if (oldEntity) {
|
|
581
|
+
this.dispatchEventToTarget(oldEntity, "pointerleave", px, py, pointerId, originalEvent);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
state.lastTargetId = null;
|
|
585
|
+
}
|
|
586
|
+
if (hitEntity) {
|
|
587
|
+
this.dispatchEventToTarget(hitEntity, type, px, py, pointerId, originalEvent);
|
|
588
|
+
} else {
|
|
589
|
+
const fallbackEvent = this.createDOMEvent(type, px, py, pointerId, originalEvent);
|
|
590
|
+
this.canvas.dispatchEvent(fallbackEvent);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Routes events to the associated A11y DOM element, or Vecto's own event dispatch system.
|
|
595
|
+
*/
|
|
596
|
+
dispatchEventToTarget(entity, type, x, y, pointerId, originalEvent) {
|
|
597
|
+
const a11yEl = this.vectoScene.getA11yElement(entity.id);
|
|
598
|
+
if (a11yEl) {
|
|
599
|
+
const domEvent = this.createDOMEvent(type, x, y, pointerId, originalEvent);
|
|
600
|
+
a11yEl.dispatchEvent(domEvent);
|
|
601
|
+
if (type === "pointerdown" && (a11yEl instanceof HTMLInputElement || a11yEl instanceof HTMLTextAreaElement || a11yEl.getAttribute("tabindex") !== null)) {
|
|
602
|
+
a11yEl.focus();
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
const e = originalEvent instanceof MouseEvent ? originalEvent : void 0;
|
|
606
|
+
const vectoEvent = new VectoJSEvent(type, entity, e, type !== "pointerleave");
|
|
607
|
+
entity.dispatchEvent(vectoEvent);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
createDOMEvent(type, x, y, pointerId, originalEvent) {
|
|
611
|
+
if (type === "wheel") {
|
|
612
|
+
const wheelE = originalEvent instanceof WheelEvent ? originalEvent : void 0;
|
|
613
|
+
return new WheelEvent("wheel", {
|
|
614
|
+
clientX: x,
|
|
615
|
+
clientY: y,
|
|
616
|
+
deltaX: wheelE ? wheelE.deltaX : 0,
|
|
617
|
+
deltaY: wheelE ? wheelE.deltaY : 0,
|
|
618
|
+
deltaZ: wheelE ? wheelE.deltaZ : 0,
|
|
619
|
+
deltaMode: wheelE ? wheelE.deltaMode : 0,
|
|
620
|
+
bubbles: true,
|
|
621
|
+
cancelable: true
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
return new PointerEvent(type, {
|
|
625
|
+
clientX: x,
|
|
626
|
+
clientY: y,
|
|
627
|
+
button: originalEvent instanceof MouseEvent ? originalEvent.button : 0,
|
|
628
|
+
buttons: originalEvent instanceof MouseEvent ? originalEvent.buttons : 0,
|
|
629
|
+
pointerId,
|
|
630
|
+
bubbles: true,
|
|
631
|
+
cancelable: true
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
findEntityById(root, id) {
|
|
635
|
+
if (root.id === id) return root;
|
|
636
|
+
for (const child of root.children) {
|
|
637
|
+
const found = this.findEntityById(child, id);
|
|
638
|
+
if (found) return found;
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Resizes the offscreen canvas and VectoScene dimensions.
|
|
644
|
+
*/
|
|
645
|
+
resize(width, height) {
|
|
646
|
+
this.canvas.width = width;
|
|
647
|
+
this.canvas.height = height;
|
|
648
|
+
this.vectoScene.resize(width, height);
|
|
649
|
+
this.texture.needsUpdate = true;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Disposes of Three.js textures, geometries, and VectoJS scenes to prevent memory leaks.
|
|
653
|
+
*/
|
|
654
|
+
dispose() {
|
|
655
|
+
this.texture.dispose();
|
|
656
|
+
this.mesh.geometry.dispose();
|
|
657
|
+
if (Array.isArray(this.mesh.material)) {
|
|
658
|
+
for (const mat of this.mesh.material) mat.dispose();
|
|
659
|
+
} else {
|
|
660
|
+
this.mesh.material.dispose();
|
|
661
|
+
}
|
|
662
|
+
if (this.mesh.parent) {
|
|
663
|
+
this.mesh.parent.remove(this.mesh);
|
|
664
|
+
}
|
|
665
|
+
this.vectoScene.destroy();
|
|
666
|
+
this.activePointers.clear();
|
|
667
|
+
this.canvas.width = 0;
|
|
668
|
+
this.canvas.height = 0;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
export {
|
|
672
|
+
ThreeAdapter,
|
|
673
|
+
ThreeRenderer,
|
|
674
|
+
WebGLGradient
|
|
675
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vectojs/three",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "WebGL/Three.js rendering backend for VectoJS",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@vectojs/core": ">=0.1.0",
|
|
27
|
+
"three": "^0.160.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@vectojs/core": "workspace:*",
|
|
31
|
+
"@types/three": "^0.160.0",
|
|
32
|
+
"tsup": "^8.3.5",
|
|
33
|
+
"vitest": "^4.1.9"
|
|
34
|
+
}
|
|
35
|
+
}
|