@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
package/src/math/heat.js
CHANGED
|
@@ -200,3 +200,109 @@ export function heatTransferFalloff(temp1, temp2, distance, maxDistance, rate, f
|
|
|
200
200
|
|
|
201
201
|
return heatDiff * rate * proximity;
|
|
202
202
|
}
|
|
203
|
+
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
// PARTICLE SYSTEM HEAT TRANSFER
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Apply heat transfer between nearby particles in a particle system.
|
|
210
|
+
* Uses Newton's law of cooling with distance falloff.
|
|
211
|
+
* Follows the same pattern as Collision.applyCircleSeparation.
|
|
212
|
+
*
|
|
213
|
+
* @param {Array<Object>} particles - Array of particles with { x, y, custom }
|
|
214
|
+
* @param {Object} [options={}] - Configuration options
|
|
215
|
+
* @param {number} [options.maxDistance=50] - Maximum distance for heat transfer
|
|
216
|
+
* @param {number} [options.rate=0.01] - Heat transfer rate coefficient
|
|
217
|
+
* @param {number} [options.falloff=1] - Distance falloff (1=linear, 2=quadratic)
|
|
218
|
+
* @param {string} [options.temperatureKey='temperature'] - Key in particle.custom
|
|
219
|
+
* @param {Function} [options.filter=null] - Filter function (particle) => boolean
|
|
220
|
+
* @param {boolean} [options.useSizeAsRadius=true] - Use particle.size for distance calc
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* // Basic usage - all particles exchange heat
|
|
224
|
+
* applyParticleHeatTransfer(particles, {
|
|
225
|
+
* maxDistance: 40,
|
|
226
|
+
* rate: 0.02,
|
|
227
|
+
* });
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* // With filter - exclude sorted particles (Maxwell's Demon)
|
|
231
|
+
* applyParticleHeatTransfer(particles, {
|
|
232
|
+
* maxDistance: 36,
|
|
233
|
+
* rate: 0.015,
|
|
234
|
+
* filter: (p) => !p.custom.sorted,
|
|
235
|
+
* });
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* // With quadratic falloff for more localized transfer
|
|
239
|
+
* applyParticleHeatTransfer(particles, {
|
|
240
|
+
* maxDistance: 60,
|
|
241
|
+
* rate: 0.025,
|
|
242
|
+
* falloff: 2,
|
|
243
|
+
* });
|
|
244
|
+
*/
|
|
245
|
+
export function applyParticleHeatTransfer(particles, options = {}) {
|
|
246
|
+
const {
|
|
247
|
+
maxDistance = 50,
|
|
248
|
+
rate = 0.01,
|
|
249
|
+
falloff = 1,
|
|
250
|
+
temperatureKey = 'temperature',
|
|
251
|
+
filter = null,
|
|
252
|
+
useSizeAsRadius = true,
|
|
253
|
+
} = options;
|
|
254
|
+
|
|
255
|
+
const n = particles.length;
|
|
256
|
+
const maxDist2 = maxDistance * maxDistance;
|
|
257
|
+
|
|
258
|
+
for (let i = 0; i < n; i++) {
|
|
259
|
+
const pi = particles[i];
|
|
260
|
+
|
|
261
|
+
// Skip filtered particles
|
|
262
|
+
if (filter && !filter(pi)) continue;
|
|
263
|
+
|
|
264
|
+
// Ensure temperature exists
|
|
265
|
+
if (pi.custom[temperatureKey] === undefined) {
|
|
266
|
+
pi.custom[temperatureKey] = 0.5;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (let j = i + 1; j < n; j++) {
|
|
270
|
+
const pj = particles[j];
|
|
271
|
+
|
|
272
|
+
// Skip filtered particles
|
|
273
|
+
if (filter && !filter(pj)) continue;
|
|
274
|
+
|
|
275
|
+
// Ensure temperature exists
|
|
276
|
+
if (pj.custom[temperatureKey] === undefined) {
|
|
277
|
+
pj.custom[temperatureKey] = 0.5;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Distance check (squared for performance)
|
|
281
|
+
const dx = pi.x - pj.x;
|
|
282
|
+
const dy = pi.y - pj.y;
|
|
283
|
+
const dist2 = dx * dx + dy * dy;
|
|
284
|
+
|
|
285
|
+
if (dist2 >= maxDist2 || dist2 < 0.0001) continue;
|
|
286
|
+
|
|
287
|
+
const dist = Math.sqrt(dist2);
|
|
288
|
+
|
|
289
|
+
// Calculate heat transfer with optional distance falloff
|
|
290
|
+
const delta = heatTransferFalloff(
|
|
291
|
+
pi.custom[temperatureKey],
|
|
292
|
+
pj.custom[temperatureKey],
|
|
293
|
+
dist,
|
|
294
|
+
maxDistance,
|
|
295
|
+
rate,
|
|
296
|
+
falloff
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Apply symmetric transfer (energy conservation)
|
|
300
|
+
pi.custom[temperatureKey] += delta;
|
|
301
|
+
pj.custom[temperatureKey] -= delta;
|
|
302
|
+
|
|
303
|
+
// Clamp to valid range
|
|
304
|
+
pi.custom[temperatureKey] = Math.max(0, Math.min(1, pi.custom[temperatureKey]));
|
|
305
|
+
pj.custom[temperatureKey] = Math.max(0, Math.min(1, pj.custom[temperatureKey]));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/math/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export {Noise} from "./noise.js";
|
|
|
6
6
|
export {Tensor} from "./tensor.js";
|
|
7
7
|
export {generatePenroseTilingPixels} from "./penrose.js";
|
|
8
8
|
export { BooleanAlgebra } from "./boolean.js";
|
|
9
|
+
export { Attractors, AttractorType, AttractorDimension, getAttractorNames, getAttractor, get3DAttractors, get2DAttractors } from "./attractors.js";
|
|
9
10
|
|
|
10
11
|
// Physics modules
|
|
11
12
|
export * from "./gr.js";
|
package/src/mixins/draggable.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export function applyDraggable(go, options = {}) {
|
|
2
2
|
const game = go.game;
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
// Clear any existing state to avoid duplicates
|
|
5
5
|
go.dragging = false;
|
|
6
6
|
go.dragOffset = { x: 0, y: 0 };
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
// Clean up any existing event handlers to prevent duplicates
|
|
9
9
|
if (go._dragInputMoveHandler) {
|
|
10
10
|
game.events.off("inputmove", go._dragInputMoveHandler);
|
|
@@ -12,26 +12,22 @@ export function applyDraggable(go, options = {}) {
|
|
|
12
12
|
if (go._dragInputUpHandler) {
|
|
13
13
|
game.events.off("inputup", go._dragInputUpHandler);
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
// Make sure the object is interactive
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} else {
|
|
20
|
-
go.interactive = true;
|
|
21
|
-
}
|
|
22
|
-
|
|
17
|
+
go.interactive = true;
|
|
18
|
+
|
|
23
19
|
// Define the input handlers and store them on the object to allow cleanup
|
|
24
20
|
go._dragInputDownHandler = (e) => {
|
|
25
|
-
// console.log("Drag input down", go.constructor.name);
|
|
21
|
+
// console.log("Drag input down", go.constructor.name);
|
|
26
22
|
go.dragging = true;
|
|
27
|
-
|
|
23
|
+
|
|
28
24
|
// Calculate offset from mouse position to object center
|
|
29
25
|
go.dragOffset.x = go.x - e.x;
|
|
30
26
|
go.dragOffset.y = go.y - e.y;
|
|
31
|
-
|
|
27
|
+
|
|
32
28
|
if (options.onDragStart) options.onDragStart();
|
|
33
29
|
};
|
|
34
|
-
|
|
30
|
+
|
|
35
31
|
go._dragInputMoveHandler = (e) => {
|
|
36
32
|
//console.log("Drag input move", go.constructor.name, "dragging:", go.dragging);
|
|
37
33
|
if (go.dragging) {
|
|
@@ -41,27 +37,27 @@ export function applyDraggable(go, options = {}) {
|
|
|
41
37
|
go.y = e.y + go.dragOffset.y;
|
|
42
38
|
}
|
|
43
39
|
};
|
|
44
|
-
|
|
40
|
+
|
|
45
41
|
go._dragInputUpHandler = (e) => {
|
|
46
42
|
//console.log("Drag input up", go.constructor.name, "dragging:", go.dragging);
|
|
47
43
|
if (!go.dragging) return;
|
|
48
|
-
|
|
44
|
+
|
|
49
45
|
go.dragging = false;
|
|
50
46
|
if (options.onDragEnd) options.onDragEnd();
|
|
51
47
|
};
|
|
52
|
-
|
|
48
|
+
|
|
53
49
|
// Bind the event handlers
|
|
54
50
|
go.on("inputdown", go._dragInputDownHandler);
|
|
55
51
|
game.events.on("inputmove", go._dragInputMoveHandler);
|
|
56
52
|
game.events.on("inputup", go._dragInputUpHandler);
|
|
57
|
-
|
|
53
|
+
|
|
58
54
|
// Return a cleanup function
|
|
59
55
|
return () => {
|
|
60
56
|
// Remove event listeners
|
|
61
57
|
go.off("inputdown", go._dragInputDownHandler);
|
|
62
58
|
game.events.off("inputmove", go._dragInputMoveHandler);
|
|
63
59
|
game.events.off("inputup", go._dragInputUpHandler);
|
|
64
|
-
|
|
60
|
+
|
|
65
61
|
// Clean up properties
|
|
66
62
|
delete go._dragInputDownHandler;
|
|
67
63
|
delete go._dragInputMoveHandler;
|
|
@@ -69,4 +65,4 @@ export function applyDraggable(go, options = {}) {
|
|
|
69
65
|
delete go.dragging;
|
|
70
66
|
delete go.dragOffset;
|
|
71
67
|
};
|
|
72
|
-
}
|
|
68
|
+
}
|
|
@@ -254,23 +254,29 @@ export class PainterShapes {
|
|
|
254
254
|
* @param {number} [lineWidth] - Line width
|
|
255
255
|
* @returns {void}
|
|
256
256
|
*/
|
|
257
|
-
static polygon(points, fillColor, strokeColor, lineWidth) {
|
|
257
|
+
static polygon(points, fillColor, strokeColor, lineWidth, lineJoin) {
|
|
258
258
|
if (points.length < 2) return;
|
|
259
259
|
|
|
260
|
-
Painter.
|
|
261
|
-
|
|
260
|
+
const ctx = Painter.ctx;
|
|
261
|
+
|
|
262
|
+
// Build the path
|
|
263
|
+
ctx.beginPath();
|
|
264
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
262
265
|
|
|
263
266
|
for (let i = 1; i < points.length; i++) {
|
|
264
|
-
|
|
267
|
+
ctx.lineTo(points[i].x, points[i].y);
|
|
265
268
|
}
|
|
266
269
|
|
|
267
|
-
|
|
270
|
+
ctx.closePath();
|
|
268
271
|
|
|
272
|
+
// Fill using Painter.colors (sets fillStyle and calls fill)
|
|
269
273
|
if (fillColor) {
|
|
270
274
|
Painter.colors.fill(fillColor);
|
|
271
275
|
}
|
|
272
276
|
|
|
277
|
+
// Stroke using Painter.colors
|
|
273
278
|
if (strokeColor) {
|
|
279
|
+
if (lineJoin) ctx.lineJoin = lineJoin;
|
|
274
280
|
Painter.colors.stroke(strokeColor, lineWidth);
|
|
275
281
|
}
|
|
276
282
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Optional Camera3D integration with depth sorting
|
|
8
8
|
* - Multiple emitter support
|
|
9
9
|
* - Blend mode control
|
|
10
|
+
* - Optional WebGL GPU-accelerated rendering
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* const particles = new ParticleSystem(this, {
|
|
@@ -14,6 +15,7 @@
|
|
|
14
15
|
* depthSort: true,
|
|
15
16
|
* maxParticles: 3000,
|
|
16
17
|
* blendMode: "screen",
|
|
18
|
+
* useWebGL: true, // Enable GPU rendering
|
|
17
19
|
* updaters: [Updaters.velocity, Updaters.lifetime, Updaters.gravity(150)],
|
|
18
20
|
* });
|
|
19
21
|
* particles.addEmitter("fountain", new ParticleEmitter({ rate: 50 }));
|
|
@@ -23,6 +25,7 @@ import { GameObject } from "../game/objects/go.js";
|
|
|
23
25
|
import { Painter } from "../painter/painter.js";
|
|
24
26
|
import { Particle } from "./particle.js";
|
|
25
27
|
import { Updaters } from "./updaters.js";
|
|
28
|
+
import { WebGLParticleRenderer } from "../webgl/webgl-particle-renderer.js";
|
|
26
29
|
|
|
27
30
|
export class ParticleSystem extends GameObject {
|
|
28
31
|
/**
|
|
@@ -34,6 +37,13 @@ export class ParticleSystem extends GameObject {
|
|
|
34
37
|
* @param {string} [options.blendMode="source-over"] - Canvas blend mode
|
|
35
38
|
* @param {Function[]} [options.updaters] - Array of updater functions
|
|
36
39
|
* @param {boolean} [options.worldSpace=false] - Position particles in world space
|
|
40
|
+
* @param {boolean} [options.useWebGL=false] - Use GPU-accelerated rendering
|
|
41
|
+
* @param {WebGLParticleRenderer} [options.webglRenderer] - External WebGL renderer
|
|
42
|
+
* @param {string} [options.webglShape='circle'] - WebGL particle shape
|
|
43
|
+
* @param {string} [options.webglBlendMode='alpha'] - WebGL blend mode ('alpha' or 'additive')
|
|
44
|
+
* @param {boolean} [options.depthShading=false] - Shade particles by depth (closer=brighter)
|
|
45
|
+
* @param {number} [options.depthShadingMin=0.3] - Minimum brightness for far particles
|
|
46
|
+
* @param {number} [options.depthShadingMax=1.0] - Maximum brightness for near particles
|
|
37
47
|
*/
|
|
38
48
|
constructor(game, options = {}) {
|
|
39
49
|
super(game, options);
|
|
@@ -60,6 +70,37 @@ export class ParticleSystem extends GameObject {
|
|
|
60
70
|
this.blendMode = options.blendMode ?? "source-over";
|
|
61
71
|
this.worldSpace = options.worldSpace ?? false;
|
|
62
72
|
|
|
73
|
+
// WebGL rendering (optional GPU acceleration)
|
|
74
|
+
this.webglRenderer = null;
|
|
75
|
+
if (options.webglRenderer) {
|
|
76
|
+
// Use provided renderer
|
|
77
|
+
this.webglRenderer = options.webglRenderer;
|
|
78
|
+
} else if (options.useWebGL) {
|
|
79
|
+
// Auto-create WebGL renderer
|
|
80
|
+
this.webglRenderer = new WebGLParticleRenderer(this.maxParticles, {
|
|
81
|
+
width: game.width,
|
|
82
|
+
height: game.height,
|
|
83
|
+
shape: options.webglShape ?? 'circle',
|
|
84
|
+
blendMode: options.webglBlendMode ?? 'alpha',
|
|
85
|
+
});
|
|
86
|
+
if (!this.webglRenderer.isAvailable()) {
|
|
87
|
+
console.warn('WebGL not available, falling back to Canvas 2D');
|
|
88
|
+
this.webglRenderer = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Depth shading (closer = brighter, further = darker)
|
|
93
|
+
this.depthShading = options.depthShading ?? false;
|
|
94
|
+
this.depthShadingMin = options.depthShadingMin ?? 0.3;
|
|
95
|
+
this.depthShadingMax = options.depthShadingMax ?? 1.0;
|
|
96
|
+
|
|
97
|
+
// WebGL offset for particles in local/centered coordinate systems (e.g. inside Scene3D)
|
|
98
|
+
// When particles use centered coords (-w/2 to w/2), set this to { x: w/2, y: h/2 }
|
|
99
|
+
this.webglOffset = options.webglOffset ?? null;
|
|
100
|
+
|
|
101
|
+
// Reusable array for WebGL render list (avoid allocation per frame)
|
|
102
|
+
this._webglRenderList = [];
|
|
103
|
+
|
|
63
104
|
// Stats
|
|
64
105
|
this._particleCount = 0;
|
|
65
106
|
}
|
|
@@ -184,13 +225,115 @@ export class ParticleSystem extends GameObject {
|
|
|
184
225
|
|
|
185
226
|
if (this.particles.length === 0) return;
|
|
186
227
|
|
|
187
|
-
|
|
228
|
+
// Use WebGL if available
|
|
229
|
+
if (this.webglRenderer) {
|
|
230
|
+
this.renderWebGL();
|
|
231
|
+
} else if (this.camera && this.depthSort) {
|
|
188
232
|
this.renderWithDepthSort();
|
|
189
233
|
} else {
|
|
190
234
|
this.renderSimple();
|
|
191
235
|
}
|
|
192
236
|
}
|
|
193
237
|
|
|
238
|
+
/**
|
|
239
|
+
* GPU-accelerated rendering using WebGL point sprites.
|
|
240
|
+
* Projects particles through camera (if available), depth sorts,
|
|
241
|
+
* and renders via WebGLParticleRenderer.
|
|
242
|
+
*/
|
|
243
|
+
renderWebGL() {
|
|
244
|
+
const renderer = this.webglRenderer;
|
|
245
|
+
const renderList = this._webglRenderList;
|
|
246
|
+
|
|
247
|
+
// Resize WebGL canvas if needed
|
|
248
|
+
if (renderer.width !== this.game.width || renderer.height !== this.game.height) {
|
|
249
|
+
renderer.resize(this.game.width, this.game.height);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Clear render list
|
|
253
|
+
renderList.length = 0;
|
|
254
|
+
|
|
255
|
+
// Build render list with projections
|
|
256
|
+
const centerX = this.game.width / 2;
|
|
257
|
+
const centerY = this.game.height / 2;
|
|
258
|
+
|
|
259
|
+
if (this.camera && this.depthSort) {
|
|
260
|
+
// 3D mode: project through camera
|
|
261
|
+
for (const p of this.particles) {
|
|
262
|
+
const proj = this.camera.project(p.x, p.y, p.z);
|
|
263
|
+
|
|
264
|
+
// Cull particles behind camera
|
|
265
|
+
if (proj.z < -this.camera.perspective + 10) continue;
|
|
266
|
+
|
|
267
|
+
renderList.push({
|
|
268
|
+
x: centerX + proj.x,
|
|
269
|
+
y: centerY + proj.y,
|
|
270
|
+
z: proj.z,
|
|
271
|
+
size: p.size * proj.scale,
|
|
272
|
+
color: p.color,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Sort back to front (painter's algorithm)
|
|
277
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
278
|
+
|
|
279
|
+
// Apply depth shading if enabled
|
|
280
|
+
if (this.depthShading && renderList.length > 1) {
|
|
281
|
+
// Find z range
|
|
282
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
283
|
+
for (const item of renderList) {
|
|
284
|
+
if (item.z < minZ) minZ = item.z;
|
|
285
|
+
if (item.z > maxZ) maxZ = item.z;
|
|
286
|
+
}
|
|
287
|
+
const zRange = maxZ - minZ;
|
|
288
|
+
|
|
289
|
+
if (zRange > 0) {
|
|
290
|
+
const brightnessRange = this.depthShadingMax - this.depthShadingMin;
|
|
291
|
+
|
|
292
|
+
for (const item of renderList) {
|
|
293
|
+
// Normalize z: 0 = far (maxZ), 1 = near (minZ)
|
|
294
|
+
const t = (maxZ - item.z) / zRange;
|
|
295
|
+
const brightness = this.depthShadingMin + t * brightnessRange;
|
|
296
|
+
|
|
297
|
+
// Apply brightness to color (create new color object to avoid mutating particle)
|
|
298
|
+
item.color = {
|
|
299
|
+
r: item.color.r * brightness,
|
|
300
|
+
g: item.color.g * brightness,
|
|
301
|
+
b: item.color.b * brightness,
|
|
302
|
+
a: item.color.a,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// 2D mode: direct screen coords (with optional offset for centered coordinate systems)
|
|
309
|
+
const offsetX = this.webglOffset?.x ?? 0;
|
|
310
|
+
const offsetY = this.webglOffset?.y ?? 0;
|
|
311
|
+
for (const p of this.particles) {
|
|
312
|
+
renderList.push({
|
|
313
|
+
x: p.x + offsetX,
|
|
314
|
+
y: p.y + offsetY,
|
|
315
|
+
size: p.size,
|
|
316
|
+
color: p.color,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Upload to GPU and render
|
|
322
|
+
renderer.clear();
|
|
323
|
+
const count = renderer.updateParticles(renderList);
|
|
324
|
+
renderer.render(count);
|
|
325
|
+
|
|
326
|
+
// Composite onto main canvas
|
|
327
|
+
// If using webglOffset, composite at negative offset to counteract Scene3D translation
|
|
328
|
+
const compositeX = this.webglOffset ? -this.webglOffset.x : 0;
|
|
329
|
+
const compositeY = this.webglOffset ? -this.webglOffset.y : 0;
|
|
330
|
+
Painter.useCtx((ctx) => {
|
|
331
|
+
ctx.globalCompositeOperation = this.blendMode;
|
|
332
|
+
renderer.compositeOnto(ctx, compositeX, compositeY);
|
|
333
|
+
ctx.globalCompositeOperation = "source-over";
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
194
337
|
/**
|
|
195
338
|
* Simple 2D rendering (no depth sorting).
|
|
196
339
|
*/
|
|
@@ -304,6 +447,27 @@ export class ParticleSystem extends GameObject {
|
|
|
304
447
|
this._particleCount = 0;
|
|
305
448
|
}
|
|
306
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Destroy the particle system and free resources.
|
|
452
|
+
*/
|
|
453
|
+
destroy() {
|
|
454
|
+
this.clear();
|
|
455
|
+
if (this.webglRenderer) {
|
|
456
|
+
this.webglRenderer.destroy();
|
|
457
|
+
this.webglRenderer = null;
|
|
458
|
+
}
|
|
459
|
+
this.pool = [];
|
|
460
|
+
this.emitters.clear();
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Check if WebGL rendering is active.
|
|
465
|
+
* @type {boolean}
|
|
466
|
+
*/
|
|
467
|
+
get isWebGL() {
|
|
468
|
+
return this.webglRenderer !== null && this.webglRenderer.isAvailable();
|
|
469
|
+
}
|
|
470
|
+
|
|
307
471
|
/**
|
|
308
472
|
* Get current particle count.
|
|
309
473
|
* @type {number}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module physics
|
|
3
|
+
* @description Physics module for particle simulations.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - Physics: Stateless physics calculations (collision, forces, kinematics)
|
|
7
|
+
* - PhysicsUpdaters: Composable updaters for ParticleSystem
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { Physics, PhysicsUpdaters } from '@guinetik/gcanvas';
|
|
11
|
+
*
|
|
12
|
+
* // Use Physics directly for custom calculations
|
|
13
|
+
* const collision = Physics.checkCollision(p1, p2);
|
|
14
|
+
*
|
|
15
|
+
* // Use PhysicsUpdaters with ParticleSystem
|
|
16
|
+
* const system = new ParticleSystem(game, {
|
|
17
|
+
* updaters: [
|
|
18
|
+
* Updaters.velocity,
|
|
19
|
+
* PhysicsUpdaters.particleCollisions(0.9),
|
|
20
|
+
* PhysicsUpdaters.bounds3D(bounds),
|
|
21
|
+
* ]
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export { Physics } from './physics.js';
|
|
26
|
+
export { PhysicsUpdaters } from './physics-updaters.js';
|