@guinetik/gcanvas 1.0.5 → 2.0.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/aizawa.html +27 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/gcanvas.es.js +5130 -372
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/halvorsen.html +27 -0
- package/dist/index.html +96 -48
- package/dist/js/aizawa.js +425 -0
- package/dist/js/bezier.js +5 -5
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/isometric.js +34 -46
- package/dist/js/lorenz.js +425 -0
- package/dist/js/painter.js +8 -8
- package/dist/js/rossler.js +480 -0
- package/dist/js/schrodinger.js +314 -18
- package/dist/js/thomas.js +394 -0
- package/dist/lorenz.html +27 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/thomas.html +27 -0
- package/package.json +1 -1
- package/readme.md +30 -22
- package/src/game/objects/go.js +7 -0
- package/src/game/objects/index.js +2 -0
- package/src/game/objects/isometric-scene.js +53 -3
- package/src/game/objects/layoutscene.js +57 -0
- package/src/game/objects/mask.js +241 -0
- package/src/game/objects/scene.js +19 -0
- package/src/game/objects/wrapper.js +14 -2
- package/src/game/pipeline.js +17 -0
- package/src/game/ui/button.js +101 -16
- package/src/game/ui/theme.js +0 -6
- package/src/game/ui/togglebutton.js +25 -14
- package/src/game/ui/tooltip.js +12 -4
- package/src/index.js +3 -0
- package/src/io/gesture.js +409 -0
- package/src/io/index.js +4 -1
- package/src/io/keys.js +9 -1
- package/src/io/screen.js +476 -0
- package/src/math/attractors.js +664 -0
- package/src/math/heat.js +106 -0
- package/src/math/index.js +1 -0
- package/src/mixins/draggable.js +15 -19
- package/src/painter/painter.shapes.js +11 -5
- package/src/particle/particle-system.js +165 -1
- package/src/physics/index.js +26 -0
- package/src/physics/physics-updaters.js +333 -0
- package/src/physics/physics.js +375 -0
- package/src/shapes/image.js +5 -5
- package/src/shapes/index.js +2 -0
- package/src/shapes/parallelogram.js +147 -0
- package/src/shapes/righttriangle.js +115 -0
- package/src/shapes/svg.js +281 -100
- package/src/shapes/text.js +22 -6
- package/src/shapes/transformable.js +5 -0
- package/src/sound/effects.js +807 -0
- package/src/sound/index.js +13 -0
- package/src/webgl/index.js +7 -0
- package/src/webgl/shaders/clifford-point-shaders.js +131 -0
- package/src/webgl/shaders/dejong-point-shaders.js +131 -0
- package/src/webgl/shaders/point-sprite-shaders.js +152 -0
- package/src/webgl/webgl-clifford-renderer.js +477 -0
- package/src/webgl/webgl-dejong-renderer.js +472 -0
- package/src/webgl/webgl-line-renderer.js +391 -0
- package/src/webgl/webgl-particle-renderer.js +410 -0
- package/types/index.d.ts +30 -2
- package/types/io.d.ts +217 -0
- package/types/physics.d.ts +299 -0
- package/types/shapes.d.ts +8 -0
- package/types/webgl.d.ts +188 -109
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebGLParticleRenderer - GPU-accelerated particle rendering via point sprites
|
|
3
|
+
*
|
|
4
|
+
* Renders particles using WebGL GL_POINTS with gl_PointSize for GPU acceleration.
|
|
5
|
+
* Works alongside the existing ParticleSystem which handles state management,
|
|
6
|
+
* pooling, and updaters on the CPU.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Pre-allocated buffers for zero-allocation rendering
|
|
10
|
+
* - Multiple shape presets (circle, glow, square)
|
|
11
|
+
* - Additive or alpha blending modes
|
|
12
|
+
* - Automatic fallback detection
|
|
13
|
+
* - Compositing onto Canvas 2D
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const renderer = new WebGLParticleRenderer(10000);
|
|
17
|
+
* renderer.updateParticles(projectedParticles);
|
|
18
|
+
* renderer.render(particleCount);
|
|
19
|
+
* renderer.compositeOnto(ctx, 0, 0);
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
POINT_SPRITE_VERTEX,
|
|
24
|
+
POINT_SPRITE_CIRCLE_FRAGMENT,
|
|
25
|
+
POINT_SPRITE_GLOW_FRAGMENT,
|
|
26
|
+
POINT_SPRITE_SQUARE_FRAGMENT,
|
|
27
|
+
POINT_SPRITE_SOFT_SQUARE_FRAGMENT,
|
|
28
|
+
} from './shaders/point-sprite-shaders.js';
|
|
29
|
+
|
|
30
|
+
export class WebGLParticleRenderer {
|
|
31
|
+
/**
|
|
32
|
+
* Create a WebGL particle renderer
|
|
33
|
+
* @param {number} maxParticles - Maximum particle capacity (pre-allocated)
|
|
34
|
+
* @param {Object} options - Configuration options
|
|
35
|
+
* @param {number} options.width - Initial canvas width
|
|
36
|
+
* @param {number} options.height - Initial canvas height
|
|
37
|
+
* @param {string} options.shape - Particle shape: 'circle', 'glow', 'square', 'softSquare'
|
|
38
|
+
* @param {string} options.blendMode - Blend mode: 'additive' or 'alpha'
|
|
39
|
+
*/
|
|
40
|
+
constructor(maxParticles = 10000, options = {}) {
|
|
41
|
+
this.maxParticles = maxParticles;
|
|
42
|
+
this.width = options.width || 800;
|
|
43
|
+
this.height = options.height || 600;
|
|
44
|
+
this.shape = options.shape || 'circle';
|
|
45
|
+
this.blendMode = options.blendMode || 'alpha';
|
|
46
|
+
|
|
47
|
+
// Create offscreen canvas
|
|
48
|
+
this.canvas = document.createElement('canvas');
|
|
49
|
+
this.canvas.width = this.width;
|
|
50
|
+
this.canvas.height = this.height;
|
|
51
|
+
|
|
52
|
+
// Get WebGL context
|
|
53
|
+
this.gl = this.canvas.getContext('webgl', {
|
|
54
|
+
alpha: true,
|
|
55
|
+
premultipliedAlpha: true,
|
|
56
|
+
antialias: false, // Points don't need AA
|
|
57
|
+
preserveDrawingBuffer: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!this.gl) {
|
|
61
|
+
console.warn('WebGL not available for particle rendering');
|
|
62
|
+
this.available = false;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.available = true;
|
|
67
|
+
|
|
68
|
+
// Pre-allocate typed arrays (reused each frame)
|
|
69
|
+
this._positions = new Float32Array(maxParticles * 2);
|
|
70
|
+
this._sizes = new Float32Array(maxParticles);
|
|
71
|
+
this._colors = new Float32Array(maxParticles * 4);
|
|
72
|
+
|
|
73
|
+
// Setup WebGL
|
|
74
|
+
this._initGL();
|
|
75
|
+
this._createBuffers();
|
|
76
|
+
this._compileShaders();
|
|
77
|
+
this._setupBlending();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if WebGL is available
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
isAvailable() {
|
|
85
|
+
return this.available;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize WebGL state
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
_initGL() {
|
|
93
|
+
const gl = this.gl;
|
|
94
|
+
gl.viewport(0, 0, this.width, this.height);
|
|
95
|
+
gl.enable(gl.BLEND);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Setup blending mode
|
|
100
|
+
* @private
|
|
101
|
+
*/
|
|
102
|
+
_setupBlending() {
|
|
103
|
+
const gl = this.gl;
|
|
104
|
+
|
|
105
|
+
if (this.blendMode === 'additive') {
|
|
106
|
+
// Additive blending (screen/lighter mode)
|
|
107
|
+
// With premultiplied alpha: src + dest
|
|
108
|
+
gl.blendFunc(gl.ONE, gl.ONE);
|
|
109
|
+
} else {
|
|
110
|
+
// Standard alpha blending with premultiplied alpha
|
|
111
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set blend mode
|
|
117
|
+
* @param {string} mode - 'additive' or 'alpha'
|
|
118
|
+
*/
|
|
119
|
+
setBlendMode(mode) {
|
|
120
|
+
this.blendMode = mode;
|
|
121
|
+
if (this.available) {
|
|
122
|
+
this._setupBlending();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create GPU buffers for particle data
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
_createBuffers() {
|
|
131
|
+
const gl = this.gl;
|
|
132
|
+
|
|
133
|
+
// Position buffer (vec2 per particle)
|
|
134
|
+
this.positionBuffer = gl.createBuffer();
|
|
135
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
136
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._positions, gl.DYNAMIC_DRAW);
|
|
137
|
+
|
|
138
|
+
// Size buffer (float per particle)
|
|
139
|
+
this.sizeBuffer = gl.createBuffer();
|
|
140
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
141
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._sizes, gl.DYNAMIC_DRAW);
|
|
142
|
+
|
|
143
|
+
// Color buffer (vec4 per particle)
|
|
144
|
+
this.colorBuffer = gl.createBuffer();
|
|
145
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
|
|
146
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._colors, gl.DYNAMIC_DRAW);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compile point sprite shaders
|
|
151
|
+
* @private
|
|
152
|
+
*/
|
|
153
|
+
_compileShaders() {
|
|
154
|
+
const gl = this.gl;
|
|
155
|
+
|
|
156
|
+
// Select fragment shader based on shape
|
|
157
|
+
let fragmentSource;
|
|
158
|
+
switch (this.shape) {
|
|
159
|
+
case 'glow':
|
|
160
|
+
fragmentSource = POINT_SPRITE_GLOW_FRAGMENT;
|
|
161
|
+
break;
|
|
162
|
+
case 'square':
|
|
163
|
+
fragmentSource = POINT_SPRITE_SQUARE_FRAGMENT;
|
|
164
|
+
break;
|
|
165
|
+
case 'softSquare':
|
|
166
|
+
fragmentSource = POINT_SPRITE_SOFT_SQUARE_FRAGMENT;
|
|
167
|
+
break;
|
|
168
|
+
case 'circle':
|
|
169
|
+
default:
|
|
170
|
+
fragmentSource = POINT_SPRITE_CIRCLE_FRAGMENT;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Compile vertex shader
|
|
175
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
176
|
+
gl.shaderSource(vertexShader, POINT_SPRITE_VERTEX);
|
|
177
|
+
gl.compileShader(vertexShader);
|
|
178
|
+
|
|
179
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
180
|
+
console.error('Vertex shader error:', gl.getShaderInfoLog(vertexShader));
|
|
181
|
+
this.available = false;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Compile fragment shader
|
|
186
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
187
|
+
gl.shaderSource(fragmentShader, fragmentSource);
|
|
188
|
+
gl.compileShader(fragmentShader);
|
|
189
|
+
|
|
190
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
191
|
+
console.error('Fragment shader error:', gl.getShaderInfoLog(fragmentShader));
|
|
192
|
+
this.available = false;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Link program
|
|
197
|
+
this.program = gl.createProgram();
|
|
198
|
+
gl.attachShader(this.program, vertexShader);
|
|
199
|
+
gl.attachShader(this.program, fragmentShader);
|
|
200
|
+
gl.linkProgram(this.program);
|
|
201
|
+
|
|
202
|
+
if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
|
|
203
|
+
console.error('Program link error:', gl.getProgramInfoLog(this.program));
|
|
204
|
+
this.available = false;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
gl.useProgram(this.program);
|
|
209
|
+
|
|
210
|
+
// Get attribute locations
|
|
211
|
+
this.aPosition = gl.getAttribLocation(this.program, 'aPosition');
|
|
212
|
+
this.aSize = gl.getAttribLocation(this.program, 'aSize');
|
|
213
|
+
this.aColor = gl.getAttribLocation(this.program, 'aColor');
|
|
214
|
+
|
|
215
|
+
// Get uniform locations
|
|
216
|
+
this.uResolution = gl.getUniformLocation(this.program, 'uResolution');
|
|
217
|
+
|
|
218
|
+
// Set initial resolution
|
|
219
|
+
gl.uniform2f(this.uResolution, this.width, this.height);
|
|
220
|
+
|
|
221
|
+
// Clean up shader objects
|
|
222
|
+
gl.deleteShader(vertexShader);
|
|
223
|
+
gl.deleteShader(fragmentShader);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Change particle shape (recompiles fragment shader)
|
|
228
|
+
* @param {string} shape - 'circle', 'glow', 'square', or 'softSquare'
|
|
229
|
+
*/
|
|
230
|
+
setShape(shape) {
|
|
231
|
+
if (shape === this.shape) return;
|
|
232
|
+
this.shape = shape;
|
|
233
|
+
|
|
234
|
+
if (this.available) {
|
|
235
|
+
// Delete old program
|
|
236
|
+
if (this.program) {
|
|
237
|
+
this.gl.deleteProgram(this.program);
|
|
238
|
+
}
|
|
239
|
+
// Recompile with new fragment shader
|
|
240
|
+
this._compileShaders();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resize the renderer
|
|
246
|
+
* @param {number} width - New width
|
|
247
|
+
* @param {number} height - New height
|
|
248
|
+
*/
|
|
249
|
+
resize(width, height) {
|
|
250
|
+
this.width = width;
|
|
251
|
+
this.height = height;
|
|
252
|
+
this.canvas.width = width;
|
|
253
|
+
this.canvas.height = height;
|
|
254
|
+
|
|
255
|
+
if (this.available) {
|
|
256
|
+
const gl = this.gl;
|
|
257
|
+
gl.viewport(0, 0, width, height);
|
|
258
|
+
gl.useProgram(this.program);
|
|
259
|
+
gl.uniform2f(this.uResolution, width, height);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Update particle data in GPU buffers
|
|
265
|
+
*
|
|
266
|
+
* @param {Array} particles - Array of projected particles with screen coords
|
|
267
|
+
* Each particle should have: { x, y, size, color: {r, g, b, a} }
|
|
268
|
+
* Colors should be 0-255 for RGB, 0-1 for alpha
|
|
269
|
+
* @returns {number} Number of particles updated
|
|
270
|
+
*/
|
|
271
|
+
updateParticles(particles) {
|
|
272
|
+
if (!this.available) return 0;
|
|
273
|
+
|
|
274
|
+
const count = Math.min(particles.length, this.maxParticles);
|
|
275
|
+
const gl = this.gl;
|
|
276
|
+
|
|
277
|
+
// Fill typed arrays
|
|
278
|
+
for (let i = 0; i < count; i++) {
|
|
279
|
+
const p = particles[i];
|
|
280
|
+
const i2 = i * 2;
|
|
281
|
+
const i4 = i * 4;
|
|
282
|
+
|
|
283
|
+
// Position (screen coords)
|
|
284
|
+
this._positions[i2] = p.x;
|
|
285
|
+
this._positions[i2 + 1] = p.y;
|
|
286
|
+
|
|
287
|
+
// Size
|
|
288
|
+
this._sizes[i] = p.size;
|
|
289
|
+
|
|
290
|
+
// Color (normalize RGB to 0-1)
|
|
291
|
+
const color = p.color;
|
|
292
|
+
// For premultiplied alpha, multiply RGB by alpha
|
|
293
|
+
const a = color.a !== undefined ? color.a : 1;
|
|
294
|
+
const r = (color.r / 255) * a;
|
|
295
|
+
const g = (color.g / 255) * a;
|
|
296
|
+
const b = (color.b / 255) * a;
|
|
297
|
+
|
|
298
|
+
this._colors[i4] = r;
|
|
299
|
+
this._colors[i4 + 1] = g;
|
|
300
|
+
this._colors[i4 + 2] = b;
|
|
301
|
+
this._colors[i4 + 3] = a;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Upload to GPU via bufferSubData (faster than bufferData for partial updates)
|
|
305
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
306
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._positions.subarray(0, count * 2));
|
|
307
|
+
|
|
308
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
309
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._sizes.subarray(0, count));
|
|
310
|
+
|
|
311
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
|
|
312
|
+
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this._colors.subarray(0, count * 4));
|
|
313
|
+
|
|
314
|
+
return count;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Clear the canvas
|
|
319
|
+
* @param {number} r - Red (0-1)
|
|
320
|
+
* @param {number} g - Green (0-1)
|
|
321
|
+
* @param {number} b - Blue (0-1)
|
|
322
|
+
* @param {number} a - Alpha (0-1)
|
|
323
|
+
*/
|
|
324
|
+
clear(r = 0, g = 0, b = 0, a = 0) {
|
|
325
|
+
if (!this.available) return;
|
|
326
|
+
const gl = this.gl;
|
|
327
|
+
gl.clearColor(r, g, b, a);
|
|
328
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Render particles to the WebGL canvas
|
|
333
|
+
* @param {number} count - Number of particles to render
|
|
334
|
+
*/
|
|
335
|
+
render(count) {
|
|
336
|
+
if (!this.available || count === 0) return;
|
|
337
|
+
|
|
338
|
+
const gl = this.gl;
|
|
339
|
+
|
|
340
|
+
gl.useProgram(this.program);
|
|
341
|
+
|
|
342
|
+
// Bind position attribute
|
|
343
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
344
|
+
gl.enableVertexAttribArray(this.aPosition);
|
|
345
|
+
gl.vertexAttribPointer(this.aPosition, 2, gl.FLOAT, false, 0, 0);
|
|
346
|
+
|
|
347
|
+
// Bind size attribute
|
|
348
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.sizeBuffer);
|
|
349
|
+
gl.enableVertexAttribArray(this.aSize);
|
|
350
|
+
gl.vertexAttribPointer(this.aSize, 1, gl.FLOAT, false, 0, 0);
|
|
351
|
+
|
|
352
|
+
// Bind color attribute
|
|
353
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
|
|
354
|
+
gl.enableVertexAttribArray(this.aColor);
|
|
355
|
+
gl.vertexAttribPointer(this.aColor, 4, gl.FLOAT, false, 0, 0);
|
|
356
|
+
|
|
357
|
+
// Draw all particles in a single call
|
|
358
|
+
gl.drawArrays(gl.POINTS, 0, count);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Composite the WebGL canvas onto a 2D canvas context
|
|
363
|
+
* @param {CanvasRenderingContext2D} ctx - Target 2D context
|
|
364
|
+
* @param {number} x - X position
|
|
365
|
+
* @param {number} y - Y position
|
|
366
|
+
* @param {number} [width] - Optional width
|
|
367
|
+
* @param {number} [height] - Optional height
|
|
368
|
+
*/
|
|
369
|
+
compositeOnto(ctx, x = 0, y = 0, width, height) {
|
|
370
|
+
if (!this.available) return;
|
|
371
|
+
ctx.drawImage(
|
|
372
|
+
this.canvas,
|
|
373
|
+
x, y,
|
|
374
|
+
width ?? this.canvas.width,
|
|
375
|
+
height ?? this.canvas.height
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get the WebGL canvas element
|
|
381
|
+
* @returns {HTMLCanvasElement}
|
|
382
|
+
*/
|
|
383
|
+
getCanvas() {
|
|
384
|
+
return this.canvas;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Destroy the renderer and free resources
|
|
389
|
+
*/
|
|
390
|
+
destroy() {
|
|
391
|
+
if (!this.available) return;
|
|
392
|
+
|
|
393
|
+
const gl = this.gl;
|
|
394
|
+
|
|
395
|
+
// Delete program
|
|
396
|
+
if (this.program) {
|
|
397
|
+
gl.deleteProgram(this.program);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Delete buffers
|
|
401
|
+
gl.deleteBuffer(this.positionBuffer);
|
|
402
|
+
gl.deleteBuffer(this.sizeBuffer);
|
|
403
|
+
gl.deleteBuffer(this.colorBuffer);
|
|
404
|
+
|
|
405
|
+
// Clear typed arrays
|
|
406
|
+
this._positions = null;
|
|
407
|
+
this._sizes = null;
|
|
408
|
+
this._colors = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -437,12 +437,40 @@ export {
|
|
|
437
437
|
Updaters
|
|
438
438
|
} from './particle';
|
|
439
439
|
|
|
440
|
+
// ==========================================================================
|
|
441
|
+
// Physics Module
|
|
442
|
+
// ==========================================================================
|
|
443
|
+
|
|
444
|
+
export {
|
|
445
|
+
Physics,
|
|
446
|
+
PhysicsUpdaters,
|
|
447
|
+
CollisionResult,
|
|
448
|
+
ForceResult,
|
|
449
|
+
VelocityResult,
|
|
450
|
+
ElasticCollisionResult,
|
|
451
|
+
Bounds3D,
|
|
452
|
+
Sphere as PhysicsSphere,
|
|
453
|
+
Position3D,
|
|
454
|
+
PhysicsParticle
|
|
455
|
+
} from './physics';
|
|
456
|
+
|
|
440
457
|
// ==========================================================================
|
|
441
458
|
// WebGL Module (Optional)
|
|
442
459
|
// ==========================================================================
|
|
443
460
|
|
|
444
461
|
export {
|
|
445
462
|
WebGLRenderer,
|
|
446
|
-
|
|
447
|
-
|
|
463
|
+
WebGLParticleRenderer,
|
|
464
|
+
WebGLLineRenderer,
|
|
465
|
+
WebGLDeJongRenderer,
|
|
466
|
+
WebGLCliffordRenderer,
|
|
467
|
+
DEJONG_MAX_ITERATIONS,
|
|
468
|
+
DEJONG_POINT_VERTEX,
|
|
469
|
+
DEJONG_POINT_FRAGMENTS,
|
|
470
|
+
CLIFFORD_MAX_ITERATIONS,
|
|
471
|
+
CLIFFORD_POINT_VERTEX,
|
|
472
|
+
CLIFFORD_POINT_FRAGMENTS,
|
|
473
|
+
SPHERE_SHADERS,
|
|
474
|
+
WebGLBlendMode,
|
|
475
|
+
PointSpriteShape
|
|
448
476
|
} from './webgl';
|
package/types/io.d.ts
CHANGED
|
@@ -186,3 +186,220 @@ export class Input {
|
|
|
186
186
|
*/
|
|
187
187
|
static init(game: Game): void;
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
// ==========================================================================
|
|
191
|
+
// Screen Detection
|
|
192
|
+
// ==========================================================================
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Screen/device detection and responsive utilities.
|
|
196
|
+
* Static class that tracks screen size, device type, and orientation.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* // Get responsive values
|
|
200
|
+
* const scaleFactor = Screen.responsive(1.5, 2, 3);
|
|
201
|
+
*
|
|
202
|
+
* // Check device type
|
|
203
|
+
* if (Screen.isMobile) {
|
|
204
|
+
* // Mobile-specific logic
|
|
205
|
+
* }
|
|
206
|
+
*
|
|
207
|
+
* // Listen for changes
|
|
208
|
+
* game.events.on('devicechange', (e) => {
|
|
209
|
+
* console.log('Device type changed:', e.isMobile);
|
|
210
|
+
* });
|
|
211
|
+
*/
|
|
212
|
+
export class Screen {
|
|
213
|
+
/** Mobile breakpoint in pixels (default: 768) */
|
|
214
|
+
static MOBILE_BREAKPOINT: number;
|
|
215
|
+
/** Tablet breakpoint in pixels (default: 1024) */
|
|
216
|
+
static TABLET_BREAKPOINT: number;
|
|
217
|
+
|
|
218
|
+
/** Current screen/window width */
|
|
219
|
+
static width: number;
|
|
220
|
+
/** Current screen/window height */
|
|
221
|
+
static height: number;
|
|
222
|
+
/** Device pixel ratio for high-DPI displays */
|
|
223
|
+
static pixelRatio: number;
|
|
224
|
+
|
|
225
|
+
/** Whether device is mobile (width <= MOBILE_BREAKPOINT) */
|
|
226
|
+
static isMobile: boolean;
|
|
227
|
+
/** Whether device is tablet (MOBILE_BREAKPOINT < width <= TABLET_BREAKPOINT) */
|
|
228
|
+
static isTablet: boolean;
|
|
229
|
+
/** Whether device is desktop (width > TABLET_BREAKPOINT) */
|
|
230
|
+
static isDesktop: boolean;
|
|
231
|
+
/** Whether device has touch capability */
|
|
232
|
+
static hasTouch: boolean;
|
|
233
|
+
|
|
234
|
+
/** Current orientation: 'portrait' or 'landscape' */
|
|
235
|
+
static orientation: 'portrait' | 'landscape';
|
|
236
|
+
/** Whether screen is in portrait mode */
|
|
237
|
+
static isPortrait: boolean;
|
|
238
|
+
/** Whether screen is in landscape mode */
|
|
239
|
+
static isLandscape: boolean;
|
|
240
|
+
|
|
241
|
+
/** Whether wake lock is currently enabled (requested by user) */
|
|
242
|
+
static wakeLockEnabled: boolean;
|
|
243
|
+
/** Whether the Wake Lock API is supported in this browser */
|
|
244
|
+
static wakeLockSupported: boolean;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Initialize screen detection for a game instance.
|
|
248
|
+
* @param game - Game instance
|
|
249
|
+
*/
|
|
250
|
+
static init(game: Game): void;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get a responsive value based on device type.
|
|
254
|
+
* @param mobile - Value for mobile devices
|
|
255
|
+
* @param tablet - Value for tablet devices (defaults to mobile)
|
|
256
|
+
* @param desktop - Value for desktop devices (defaults to tablet)
|
|
257
|
+
* @returns The appropriate value for current device
|
|
258
|
+
*/
|
|
259
|
+
static responsive<T>(mobile: T, tablet?: T, desktop?: T): T;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get a value scaled by pixel ratio for high-DPI displays.
|
|
263
|
+
* @param value - Base value to scale
|
|
264
|
+
* @returns Value multiplied by device pixel ratio
|
|
265
|
+
*/
|
|
266
|
+
static scaled(value: number): number;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Check if device is likely touch-primary (mobile/tablet with touch).
|
|
270
|
+
* @returns True if device is touch-primary
|
|
271
|
+
*/
|
|
272
|
+
static isTouchPrimary(): boolean;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get the smaller dimension of the screen.
|
|
276
|
+
* @returns The smaller of width or height
|
|
277
|
+
*/
|
|
278
|
+
static minDimension(): number;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get the larger dimension of the screen.
|
|
282
|
+
* @returns The larger of width or height
|
|
283
|
+
*/
|
|
284
|
+
static maxDimension(): number;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the aspect ratio (width / height).
|
|
288
|
+
* @returns The aspect ratio
|
|
289
|
+
*/
|
|
290
|
+
static aspectRatio(): number;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if screen matches a CSS media query.
|
|
294
|
+
* @param query - CSS media query string
|
|
295
|
+
* @returns True if query matches
|
|
296
|
+
*/
|
|
297
|
+
static matches(query: string): boolean;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if user prefers reduced motion.
|
|
301
|
+
* @returns True if user prefers reduced motion
|
|
302
|
+
*/
|
|
303
|
+
static prefersReducedMotion(): boolean;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check if user prefers dark color scheme.
|
|
307
|
+
* @returns True if user prefers dark mode
|
|
308
|
+
*/
|
|
309
|
+
static prefersDarkMode(): boolean;
|
|
310
|
+
|
|
311
|
+
// Wake Lock API
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Request a wake lock to prevent the screen from sleeping.
|
|
315
|
+
* Useful for games/simulations that should keep the display on.
|
|
316
|
+
* The lock is automatically re-acquired when the page becomes visible.
|
|
317
|
+
* @returns True if wake lock was successfully acquired
|
|
318
|
+
*/
|
|
319
|
+
static requestWakeLock(): Promise<boolean>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Release the wake lock, allowing the screen to sleep normally.
|
|
323
|
+
* Call this when your game/simulation stops or pauses.
|
|
324
|
+
*/
|
|
325
|
+
static releaseWakeLock(): Promise<void>;
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if wake lock is currently active.
|
|
329
|
+
* @returns True if wake lock is held
|
|
330
|
+
*/
|
|
331
|
+
static isWakeLockActive(): boolean;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ==========================================================================
|
|
335
|
+
// Gesture Recognition
|
|
336
|
+
// ==========================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Gesture options for configuring zoom, pan, and tap behavior.
|
|
340
|
+
*/
|
|
341
|
+
export interface GestureOptions {
|
|
342
|
+
/** Callback for zoom: (delta, centerX, centerY) => void. delta > 0 = zoom in */
|
|
343
|
+
onZoom?: (delta: number, centerX: number, centerY: number) => void;
|
|
344
|
+
/** Callback for pan: (dx, dy) => void */
|
|
345
|
+
onPan?: (dx: number, dy: number) => void;
|
|
346
|
+
/** Callback for tap/click: (x, y) => void */
|
|
347
|
+
onTap?: (x: number, y: number) => void;
|
|
348
|
+
/** Callback when drag starts: (x, y) => void */
|
|
349
|
+
onDragStart?: (x: number, y: number) => void;
|
|
350
|
+
/** Callback when drag ends: () => void */
|
|
351
|
+
onDragEnd?: () => void;
|
|
352
|
+
/** Zoom sensitivity for mouse wheel (default: 0.1) */
|
|
353
|
+
wheelZoomFactor?: number;
|
|
354
|
+
/** Zoom sensitivity for pinch gesture (default: 1) */
|
|
355
|
+
pinchZoomFactor?: number;
|
|
356
|
+
/** Scale factor for pan deltas (default: 1) */
|
|
357
|
+
panScale?: number;
|
|
358
|
+
/** Max movement in pixels to still count as tap (default: 10) */
|
|
359
|
+
tapThreshold?: number;
|
|
360
|
+
/** Max duration in ms for tap (default: 300) */
|
|
361
|
+
tapTimeout?: number;
|
|
362
|
+
/** Prevent default browser behavior (default: true) */
|
|
363
|
+
preventDefault?: boolean;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* High-level gesture recognition for zoom, pan, and tap.
|
|
368
|
+
* Works seamlessly across mouse and touch input.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* const gesture = new Gesture(canvas, {
|
|
372
|
+
* onZoom: (delta, cx, cy) => {
|
|
373
|
+
* this.zoom *= delta > 0 ? 1.1 : 0.9;
|
|
374
|
+
* },
|
|
375
|
+
* onPan: (dx, dy) => {
|
|
376
|
+
* this.offsetX += dx;
|
|
377
|
+
* this.offsetY += dy;
|
|
378
|
+
* },
|
|
379
|
+
* onTap: (x, y) => {
|
|
380
|
+
* this.handleClick(x, y);
|
|
381
|
+
* }
|
|
382
|
+
* });
|
|
383
|
+
*
|
|
384
|
+
* // Cleanup
|
|
385
|
+
* gesture.destroy();
|
|
386
|
+
*/
|
|
387
|
+
export class Gesture {
|
|
388
|
+
/** The canvas element gestures are attached to */
|
|
389
|
+
canvas: HTMLCanvasElement;
|
|
390
|
+
/** Whether a drag is currently in progress */
|
|
391
|
+
readonly isDragging: boolean;
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Create a new Gesture handler.
|
|
395
|
+
* @param canvas - Canvas element to attach gestures to
|
|
396
|
+
* @param options - Configuration options
|
|
397
|
+
*/
|
|
398
|
+
constructor(canvas: HTMLCanvasElement, options?: GestureOptions);
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Remove all event listeners and clean up.
|
|
402
|
+
* Call this when the gesture handler is no longer needed.
|
|
403
|
+
*/
|
|
404
|
+
destroy(): void;
|
|
405
|
+
}
|