@guinetik/gcanvas 1.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/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Euclidian } from "./euclidian.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Geometry2d
|
|
5
|
+
* ----------
|
|
6
|
+
*
|
|
7
|
+
* A foundational class representing any object with spatial boundaries.
|
|
8
|
+
* Builds upon `Euclidian` by adding:
|
|
9
|
+
*
|
|
10
|
+
* - **Bounding logic** with memoization
|
|
11
|
+
* - **Constraint enforcement** (`minX`, `maxX`, etc.)
|
|
12
|
+
* - **Property change tracking** for dirty bounding recalculations
|
|
13
|
+
*
|
|
14
|
+
* This class is *not* concerned with transforms, rendering, or visibility.
|
|
15
|
+
* Instead, it's the core layer where **position + size = spatial identity**.
|
|
16
|
+
*
|
|
17
|
+
* ### Core Responsibilities
|
|
18
|
+
*
|
|
19
|
+
* 1. **Bounds Calculation**: Caches and computes bounding boxes using the Template Method pattern:
|
|
20
|
+
* - `getBounds()` → returns cached bounds
|
|
21
|
+
* - `calculateBounds()` → override point for subclasses
|
|
22
|
+
* 2. **Constraints**: Applies optional min/max limits on x/y positions
|
|
23
|
+
* 3. **Change Tracking**: Automatically marks bounds as dirty when spatial props are modified
|
|
24
|
+
* 4. **Tick Awareness**: Supports `update(dt)` to re-evaluate bounds when needed
|
|
25
|
+
*
|
|
26
|
+
* ### Coordinate System
|
|
27
|
+
*
|
|
28
|
+
* - The `x` and `y` properties refer to the **center** of the object.
|
|
29
|
+
* - Use `getLocalPosition()` for top-left alignment in layout systems.
|
|
30
|
+
*
|
|
31
|
+
* ### Subclassing Guidelines
|
|
32
|
+
*
|
|
33
|
+
* - Override `calculateBounds()` if the object has non-rectangular or transformed geometry.
|
|
34
|
+
* - Call `markBoundsDirty()` in custom logic if bounds-affecting state changes.
|
|
35
|
+
*
|
|
36
|
+
* @abstract
|
|
37
|
+
* @extends Euclidian
|
|
38
|
+
*/
|
|
39
|
+
export class Geometry2d extends Euclidian {
|
|
40
|
+
/**
|
|
41
|
+
* @param {Object} [options={}]
|
|
42
|
+
* @param {number} [options.minX] - Minimum X constraint (optional)
|
|
43
|
+
* @param {number} [options.maxX] - Maximum X constraint (optional)
|
|
44
|
+
* @param {number} [options.minY] - Minimum Y constraint (optional)
|
|
45
|
+
* @param {number} [options.maxY] - Maximum Y constraint (optional)
|
|
46
|
+
* @param {boolean} [options.crisp=true] - Whether to round to whole pixels
|
|
47
|
+
*/
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
super(options);
|
|
50
|
+
this._minX = options.minX;
|
|
51
|
+
this._maxX = options.maxX;
|
|
52
|
+
this._minY = options.minY;
|
|
53
|
+
this._maxY = options.maxY;
|
|
54
|
+
this._boundsDirty = true;
|
|
55
|
+
this._cachedBounds = null;
|
|
56
|
+
this.crisp = options.crisp ?? true;
|
|
57
|
+
this.logger.log("Geometry2d", this.x, this.y, this.width, this.height);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update() {
|
|
61
|
+
this.trace("Geometry2d.update");
|
|
62
|
+
this.applyConstraints();
|
|
63
|
+
this.getBounds(); // Trigger lazy recompute if dirty
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets the minimum allowed X value.
|
|
68
|
+
* @type {number|undefined}
|
|
69
|
+
*/
|
|
70
|
+
get minX() {
|
|
71
|
+
return this._minX;
|
|
72
|
+
}
|
|
73
|
+
set minX(v) {
|
|
74
|
+
this._minX = v;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets the maximum allowed X value.
|
|
79
|
+
* @type {number|undefined}
|
|
80
|
+
*/
|
|
81
|
+
get maxX() {
|
|
82
|
+
return this._maxX;
|
|
83
|
+
}
|
|
84
|
+
set maxX(v) {
|
|
85
|
+
this._maxX = v;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Gets the minimum allowed Y value.
|
|
90
|
+
* @type {number|undefined}
|
|
91
|
+
*/
|
|
92
|
+
get minY() {
|
|
93
|
+
return this._minY;
|
|
94
|
+
}
|
|
95
|
+
set minY(v) {
|
|
96
|
+
this._minY = v;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gets the maximum allowed Y value.
|
|
101
|
+
* @type {number|undefined}
|
|
102
|
+
*/
|
|
103
|
+
get maxY() {
|
|
104
|
+
return this._maxY;
|
|
105
|
+
}
|
|
106
|
+
set maxY(v) {
|
|
107
|
+
this._maxY = v;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether the bounding box is dirty and needs recalculation.
|
|
112
|
+
* @type {boolean}
|
|
113
|
+
* @readonly
|
|
114
|
+
*/
|
|
115
|
+
get boundsDirty() {
|
|
116
|
+
return this._boundsDirty;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Applies positional constraints and optionally rounds to whole pixels.
|
|
121
|
+
*/
|
|
122
|
+
applyConstraints() {
|
|
123
|
+
if (this._minX !== undefined) this.x = Math.max(this.x, this._minX);
|
|
124
|
+
if (this._maxX !== undefined) this.x = Math.min(this.x, this._maxX);
|
|
125
|
+
if (this._minY !== undefined) this.y = Math.max(this.y, this._minY);
|
|
126
|
+
if (this._maxY !== undefined) this.y = Math.min(this.y, this._maxY);
|
|
127
|
+
|
|
128
|
+
if (this.crisp) {
|
|
129
|
+
this.x = Math.round(this.x);
|
|
130
|
+
this.y = Math.round(this.y);
|
|
131
|
+
this.width = Math.round(this.width);
|
|
132
|
+
this.height = Math.round(this.height);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns the object's bounding box.
|
|
138
|
+
* Uses memoization to avoid unnecessary recomputation.
|
|
139
|
+
*
|
|
140
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
141
|
+
*/
|
|
142
|
+
getBounds() {
|
|
143
|
+
if (this._boundsDirty || !this._cachedBounds) {
|
|
144
|
+
//this.trace("Geometry2d.getBounds", this.name || this.constructor.name, this._boundsDirty, this._cachedBounds);
|
|
145
|
+
this._cachedBounds = this.calculateBounds();
|
|
146
|
+
this._boundsDirty = false;
|
|
147
|
+
}
|
|
148
|
+
return this._cachedBounds;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Called by `getBounds()` when bounds are dirty.
|
|
153
|
+
* Can be overridden to support more complex bounds (e.g. transformed shapes).
|
|
154
|
+
*
|
|
155
|
+
* @protected
|
|
156
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
157
|
+
*/
|
|
158
|
+
calculateBounds() {
|
|
159
|
+
return {
|
|
160
|
+
width: this.width,
|
|
161
|
+
height: this.height,
|
|
162
|
+
x: this.x,
|
|
163
|
+
y: this.y,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Returns the object's top-left corner, taking into account group containment.
|
|
169
|
+
* Useful for layouting or aligning objects to pixel grids.
|
|
170
|
+
*
|
|
171
|
+
* @returns {{x: number, y: number}}
|
|
172
|
+
*/
|
|
173
|
+
getLocalPosition() {
|
|
174
|
+
// Get the parent group's position if it exists
|
|
175
|
+
let parentX = 0;
|
|
176
|
+
let parentY = 0;
|
|
177
|
+
|
|
178
|
+
// If this object is part of a group, adjust for the group's position
|
|
179
|
+
if (this.parent) {
|
|
180
|
+
parentX = this.parent.x;
|
|
181
|
+
parentY = this.parent.y;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
x: (this.x - parentX) - this.width / 2,
|
|
186
|
+
y: (this.y - parentY) - this.height / 2,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Marks bounds as dirty.
|
|
192
|
+
* Called automatically by internal setters, but exposed for custom logic.
|
|
193
|
+
*
|
|
194
|
+
* @protected
|
|
195
|
+
*/
|
|
196
|
+
markBoundsDirty() {
|
|
197
|
+
this._boundsDirty = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
validateProp(v, prop) {
|
|
201
|
+
super.validateProp(v, prop);
|
|
202
|
+
const originalProp = this[prop];
|
|
203
|
+
if(v !== originalProp) {
|
|
204
|
+
//console.log("Geometry2d.marking bounds dirty", this.name || this.constructor.name, prop, v, "originalProp", originalProp);
|
|
205
|
+
this.markBoundsDirty();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setTopLeft(x, y) {
|
|
210
|
+
this.x = x + this.width / 2;
|
|
211
|
+
this.y = y + this.height / 2;
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
setCenter(x, y) {
|
|
216
|
+
this.x = x;
|
|
217
|
+
this.y = y;
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { Painter } from "../painter/painter";
|
|
2
|
+
import { Transformable } from "./transformable";
|
|
3
|
+
import { ZOrderedCollection } from "../util";
|
|
4
|
+
/**
|
|
5
|
+
* Group - A powerful container for composing and manipulating multiple transformable objects
|
|
6
|
+
*
|
|
7
|
+
* ### Core Capabilities
|
|
8
|
+
*
|
|
9
|
+
* - Aggregate multiple transformable objects into a single unit
|
|
10
|
+
* - Efficient bounding box calculation with memoization
|
|
11
|
+
* - Performant object management
|
|
12
|
+
* - Property inheritance from group to children
|
|
13
|
+
*
|
|
14
|
+
* ### Rendering Behavior
|
|
15
|
+
*
|
|
16
|
+
* - Applies group's transformations to all children
|
|
17
|
+
* - Renders children in order of addition
|
|
18
|
+
* - Supports both Shape and GameObject hierarchies
|
|
19
|
+
*
|
|
20
|
+
* @extends Transformable
|
|
21
|
+
*/
|
|
22
|
+
export class Group extends Transformable {
|
|
23
|
+
/**
|
|
24
|
+
* Creates a new Group instance
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} [options={}] - Additional rendering options
|
|
27
|
+
* @param {boolean} [options.inheritOpacity=true] - Whether opacity should cascade to children
|
|
28
|
+
* @param {boolean} [options.inheritVisible=true] - Whether visibility should cascade to children
|
|
29
|
+
* @param {boolean} [options.inheritScale=false] - Whether scale should cascade to children
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
// Call parent constructor with all options
|
|
33
|
+
super(options);
|
|
34
|
+
|
|
35
|
+
// Create the z-ordered collection
|
|
36
|
+
this._collection = new ZOrderedCollection({
|
|
37
|
+
sortByZIndex: options.sortByZIndex || true
|
|
38
|
+
});
|
|
39
|
+
this._collection._owner = this; // Give collection a reference to its owner
|
|
40
|
+
|
|
41
|
+
// Initialize state tracking
|
|
42
|
+
this._childrenVersion = 0;
|
|
43
|
+
this._cachedBounds = null;
|
|
44
|
+
|
|
45
|
+
options.width = Math.max(0, options.width || 0);
|
|
46
|
+
options.height = Math.max(0, options.height || 0);
|
|
47
|
+
|
|
48
|
+
// Track if dimensions were explicitly set in constructor
|
|
49
|
+
this.userDefinedWidth = options.width;
|
|
50
|
+
this.userDefinedHeight = options.height;
|
|
51
|
+
|
|
52
|
+
// Only consider dimensions as user-defined if they were explicitly provided in options
|
|
53
|
+
this.userDefinedDimensions = options.width !== undefined && options.height !== undefined &&
|
|
54
|
+
(options.width > 0 || options.height > 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Add object to group with type checking
|
|
60
|
+
* @param {Transformable} object - Object to add
|
|
61
|
+
* @returns {Transformable} The added object
|
|
62
|
+
*/
|
|
63
|
+
add(object) {
|
|
64
|
+
if (object == null || object == undefined) {
|
|
65
|
+
throw new Error("Object is null or undefined");
|
|
66
|
+
}
|
|
67
|
+
if (!(object instanceof Transformable)) {
|
|
68
|
+
throw new TypeError("Group can only add Transformable instances");
|
|
69
|
+
}
|
|
70
|
+
object.parent = this;
|
|
71
|
+
this._collection.add(object);
|
|
72
|
+
this._childrenVersion++;
|
|
73
|
+
this.markBoundsDirty();
|
|
74
|
+
this.invalidateCache();
|
|
75
|
+
return object;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Remove object from group
|
|
80
|
+
* @param {Transformable} object - Object to remove
|
|
81
|
+
* @returns {boolean} Whether object was removed
|
|
82
|
+
*/
|
|
83
|
+
remove(object) {
|
|
84
|
+
const result = this._collection.remove(object);
|
|
85
|
+
if (result) {
|
|
86
|
+
object.parent = null;
|
|
87
|
+
this._childrenVersion++;
|
|
88
|
+
this.markBoundsDirty();
|
|
89
|
+
this.invalidateCache();
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clear all objects from group
|
|
96
|
+
*/
|
|
97
|
+
clear() {
|
|
98
|
+
this._collection.clear();
|
|
99
|
+
this._childrenVersion++;
|
|
100
|
+
this.markBoundsDirty();
|
|
101
|
+
this.invalidateCache();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Z-ordering methods
|
|
105
|
+
bringToFront(object) {
|
|
106
|
+
return this._collection.bringToFront(object);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
sendToBack(object) {
|
|
110
|
+
return this._collection.sendToBack(object);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
bringForward(object) {
|
|
114
|
+
return this._collection.bringForward(object);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sendBackward(object) {
|
|
118
|
+
return this._collection.sendBackward(object);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render group and all children.
|
|
123
|
+
* Transformations are already applied by super.draw().
|
|
124
|
+
*/
|
|
125
|
+
draw() {
|
|
126
|
+
super.draw();
|
|
127
|
+
this.logger.log("Group.draw children:", this.children.length);
|
|
128
|
+
this._renderChildren();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render children normally (non-cached path)
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_renderChildren() {
|
|
136
|
+
const sortedChildren = this._collection.getSortedChildren();
|
|
137
|
+
for (let i = 0; i < sortedChildren.length; i++) {
|
|
138
|
+
const child = sortedChildren[i];
|
|
139
|
+
if (child.visible) {
|
|
140
|
+
Painter.save();
|
|
141
|
+
child.render();
|
|
142
|
+
Painter.restore();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Update all children with active update methods
|
|
150
|
+
* @param {number} dt - Delta time in seconds
|
|
151
|
+
*/
|
|
152
|
+
update(dt) {
|
|
153
|
+
this.logger.groupCollapsed("Group.update");
|
|
154
|
+
const sortedChildren = this._collection.getSortedChildren();
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < sortedChildren.length; i++) {
|
|
157
|
+
const child = sortedChildren[i];
|
|
158
|
+
if (child.active && typeof child.update === 'function') {
|
|
159
|
+
child.update(dt);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
super.update(dt);
|
|
163
|
+
this.logger.groupEnd();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get group's children array
|
|
168
|
+
* @returns {Array} Children array
|
|
169
|
+
*/
|
|
170
|
+
get children() {
|
|
171
|
+
// Check if the collection exists and if it doesn't, return an empty array
|
|
172
|
+
return this._collection?.children || [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Override width getter
|
|
177
|
+
* @returns {number} Width
|
|
178
|
+
*/
|
|
179
|
+
get width() {
|
|
180
|
+
if (this.userDefinedDimensions) {
|
|
181
|
+
return this._width;
|
|
182
|
+
}
|
|
183
|
+
return this.getBounds().width;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Override width setter
|
|
188
|
+
* @param {number} v - New width
|
|
189
|
+
*/
|
|
190
|
+
set width(v) {
|
|
191
|
+
const max = Math.max(0, v);
|
|
192
|
+
this._width = max;
|
|
193
|
+
this.userDefinedWidth = max;
|
|
194
|
+
this.userDefinedDimensions = (this.userDefinedWidth > 0 || this.userDefinedHeight > 0) &&
|
|
195
|
+
this.userDefinedWidth !== undefined && this.userDefinedHeight !== undefined;
|
|
196
|
+
this.markBoundsDirty();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Override height getter
|
|
201
|
+
* @returns {number} Height
|
|
202
|
+
*/
|
|
203
|
+
get height() {
|
|
204
|
+
if (this.userDefinedDimensions) {
|
|
205
|
+
return this._height;
|
|
206
|
+
}
|
|
207
|
+
return this.getBounds().height;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Override height setter
|
|
212
|
+
* @param {number} v - New height
|
|
213
|
+
*/
|
|
214
|
+
set height(v) {
|
|
215
|
+
const max = Math.max(0, v);
|
|
216
|
+
this._height = max;
|
|
217
|
+
this.userDefinedHeight = max;
|
|
218
|
+
this.userDefinedDimensions = (this.userDefinedWidth > 0 || this.userDefinedHeight > 0) &&
|
|
219
|
+
this.userDefinedWidth !== undefined && this.userDefinedHeight !== undefined;
|
|
220
|
+
this.markBoundsDirty();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Override calculateBounds to compute from children
|
|
226
|
+
* @returns {Object} Bounds object
|
|
227
|
+
*/
|
|
228
|
+
calculateBounds() {
|
|
229
|
+
// If explicitly sized, use those dimensions
|
|
230
|
+
if (this.userDefinedDimensions) {
|
|
231
|
+
return {
|
|
232
|
+
x: this.x,
|
|
233
|
+
y: this.y,
|
|
234
|
+
width: this._width,
|
|
235
|
+
height: this._height
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// No children = empty bounds
|
|
240
|
+
if (!this.children?.length) {
|
|
241
|
+
return {
|
|
242
|
+
x: this.x,
|
|
243
|
+
y: this.y,
|
|
244
|
+
width: 0,
|
|
245
|
+
height: 0
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let minX = Infinity;
|
|
250
|
+
let minY = Infinity;
|
|
251
|
+
let maxX = -Infinity;
|
|
252
|
+
let maxY = -Infinity;
|
|
253
|
+
|
|
254
|
+
// Calculate bounds from all children
|
|
255
|
+
for (const child of this.children) {
|
|
256
|
+
// Get the child's position and dimensions
|
|
257
|
+
const childX = child.x;
|
|
258
|
+
const childY = child.y;
|
|
259
|
+
const childWidth = child.width;
|
|
260
|
+
const childHeight = child.height;
|
|
261
|
+
|
|
262
|
+
// Calculate the child's bounding box edges
|
|
263
|
+
const childLeft = childX - childWidth / 2;
|
|
264
|
+
const childRight = childX + childWidth / 2;
|
|
265
|
+
const childTop = childY - childHeight / 2;
|
|
266
|
+
const childBottom = childY + childHeight / 2;
|
|
267
|
+
|
|
268
|
+
// Update min/max coordinates
|
|
269
|
+
minX = Math.min(minX, childLeft);
|
|
270
|
+
maxX = Math.max(maxX, childRight);
|
|
271
|
+
minY = Math.min(minY, childTop);
|
|
272
|
+
maxY = Math.max(maxY, childBottom);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Calculate dimensions
|
|
276
|
+
const width = maxX - minX;
|
|
277
|
+
const height = maxY - minY;
|
|
278
|
+
|
|
279
|
+
// Return bounds centered on group position
|
|
280
|
+
return {
|
|
281
|
+
x: this.x,
|
|
282
|
+
y: this.y,
|
|
283
|
+
width: width,
|
|
284
|
+
height: height
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns debug bounds in local space (centered at origin).
|
|
290
|
+
* Used for debug drawing after transforms have been applied.
|
|
291
|
+
* @returns {{x: number, y: number, width: number, height: number}}
|
|
292
|
+
*/
|
|
293
|
+
getDebugBounds() {
|
|
294
|
+
const bounds = this.calculateBounds();
|
|
295
|
+
|
|
296
|
+
// Return bounds centered at local origin (0, 0)
|
|
297
|
+
// This works because debug is drawn after translation to group's position
|
|
298
|
+
return {
|
|
299
|
+
width: bounds.width,
|
|
300
|
+
height: bounds.height,
|
|
301
|
+
x: -bounds.width / 2,
|
|
302
|
+
y: -bounds.height / 2,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ============================================================
|
|
307
|
+
// Group-wide Transform Operations
|
|
308
|
+
// ============================================================
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Applies a transform callback to each child in the group.
|
|
312
|
+
* Useful for batch operations on all children.
|
|
313
|
+
*
|
|
314
|
+
* @param {function(import('./transform.js').Transform, import('./transformable.js').Transformable, number): void} callback
|
|
315
|
+
* Callback receiving (transform, child, index)
|
|
316
|
+
* @returns {Group} this for chaining
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* // Scale all children by 0.5
|
|
320
|
+
* group.forEachTransform((t) => t.scale(0.5));
|
|
321
|
+
*
|
|
322
|
+
* // Rotate each child differently
|
|
323
|
+
* group.forEachTransform((t, child, i) => t.rotation(i * 15));
|
|
324
|
+
*/
|
|
325
|
+
forEachTransform(callback) {
|
|
326
|
+
this.children.forEach((child, index) => {
|
|
327
|
+
if (child.transform) {
|
|
328
|
+
callback(child.transform, child, index);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Translates all children by the given offset.
|
|
336
|
+
* This moves children relative to their current positions,
|
|
337
|
+
* not the group itself.
|
|
338
|
+
*
|
|
339
|
+
* @param {number} dx - Delta X
|
|
340
|
+
* @param {number} dy - Delta Y
|
|
341
|
+
* @returns {Group} this for chaining
|
|
342
|
+
*/
|
|
343
|
+
translateChildren(dx, dy) {
|
|
344
|
+
return this.forEachTransform((t) => t.translateBy(dx, dy));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Scales all children by the given factor.
|
|
349
|
+
*
|
|
350
|
+
* @param {number} factor - Scale factor (1 = no change)
|
|
351
|
+
* @returns {Group} this for chaining
|
|
352
|
+
*/
|
|
353
|
+
scaleChildren(factor) {
|
|
354
|
+
return this.forEachTransform((t) => t.scaleBy(factor));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Rotates all children by the given amount.
|
|
359
|
+
*
|
|
360
|
+
* @param {number} degrees - Rotation amount in degrees
|
|
361
|
+
* @returns {Group} this for chaining
|
|
362
|
+
*/
|
|
363
|
+
rotateChildren(degrees) {
|
|
364
|
+
return this.forEachTransform((t) => t.rotateBy(degrees));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Resets transforms on all children to default values.
|
|
369
|
+
*
|
|
370
|
+
* @returns {Group} this for chaining
|
|
371
|
+
*/
|
|
372
|
+
resetChildTransforms() {
|
|
373
|
+
return this.forEachTransform((t) => t.reset());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
|
|
4
|
+
export class Heart extends Shape {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
super(options);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
draw() {
|
|
10
|
+
super.draw();
|
|
11
|
+
const w = this.width;
|
|
12
|
+
const h = this.height;
|
|
13
|
+
const topCurveHeight = h * 0.3;
|
|
14
|
+
const lines = Painter.lines;
|
|
15
|
+
lines.beginPath();
|
|
16
|
+
lines.moveTo(0, topCurveHeight);
|
|
17
|
+
// Left arc
|
|
18
|
+
lines.bezierCurveTo(0, 0, -w / 2, 0, -w / 2, topCurveHeight);
|
|
19
|
+
// Bottom point
|
|
20
|
+
lines.bezierCurveTo(-w / 2, h * 0.8, 0, h, 0, h);
|
|
21
|
+
// Right arc
|
|
22
|
+
lines.bezierCurveTo(0, h, w / 2, h * 0.8, w / 2, topCurveHeight);
|
|
23
|
+
lines.bezierCurveTo(w / 2, 0, 0, 0, 0, topCurveHeight);
|
|
24
|
+
lines.closePath();
|
|
25
|
+
if (this.color) {
|
|
26
|
+
Painter.colors.fill(this.color);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (this.stroke) {
|
|
30
|
+
Painter.colors.stroke(this.stroke, this.lineWidth);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getBounds() {
|
|
35
|
+
return {
|
|
36
|
+
x: this.x,
|
|
37
|
+
y: this.y + this.height / 2,
|
|
38
|
+
width: this.width,
|
|
39
|
+
height: this.height,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Shape } from "./shape.js";
|
|
2
|
+
import { Painter } from "../painter/painter.js";
|
|
3
|
+
export class Hexagon extends Shape {
|
|
4
|
+
constructor(radius, options = {}) {
|
|
5
|
+
super(options);
|
|
6
|
+
this.radius = radius;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
draw() {
|
|
10
|
+
super.draw();
|
|
11
|
+
const points = Array.from({ length: 6 }, (_, i) => {
|
|
12
|
+
const angle = (Math.PI / 3) * i;
|
|
13
|
+
return {
|
|
14
|
+
x: Math.cos(angle) * this.radius,
|
|
15
|
+
y: Math.sin(angle) * this.radius,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
Painter.shapes.polygon(
|
|
20
|
+
points,
|
|
21
|
+
this.color,
|
|
22
|
+
this.stroke,
|
|
23
|
+
this.lineWidth
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|