@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,549 @@
|
|
|
1
|
+
import { Scene } from "..";
|
|
2
|
+
import { Painter } from "../../painter/painter";
|
|
3
|
+
import {
|
|
4
|
+
horizontalLayout,
|
|
5
|
+
verticalLayout,
|
|
6
|
+
tileLayout,
|
|
7
|
+
applyLayout,
|
|
8
|
+
gridLayout,
|
|
9
|
+
} from "../../util/layout";
|
|
10
|
+
|
|
11
|
+
// LayoutScene base class
|
|
12
|
+
export class LayoutScene extends Scene {
|
|
13
|
+
constructor(game, options = {}) {
|
|
14
|
+
super(game, options);
|
|
15
|
+
this.spacing = options.spacing ?? 10;
|
|
16
|
+
this.padding = options.padding ?? 0;
|
|
17
|
+
this.autoSize = options.autoSize ?? true;
|
|
18
|
+
this.align = options.align ?? "start";
|
|
19
|
+
this.debug = options.debug ?? false;
|
|
20
|
+
this._layoutDirty = true; // Initially dirty
|
|
21
|
+
|
|
22
|
+
// Scroll configuration
|
|
23
|
+
this.scrollable = options.scrollable ?? false;
|
|
24
|
+
this.scrollFriction = options.scrollFriction ?? 0.92;
|
|
25
|
+
this.scrollBounce = options.scrollBounce ?? 0.3;
|
|
26
|
+
this.scrollThreshold = options.scrollThreshold ?? 0.5;
|
|
27
|
+
this._viewportWidth = options.viewportWidth ?? null;
|
|
28
|
+
this._viewportHeight = options.viewportHeight ?? null;
|
|
29
|
+
|
|
30
|
+
// Scroll state
|
|
31
|
+
this._scrollOffset = { x: 0, y: 0 };
|
|
32
|
+
this._scrollVelocity = { x: 0, y: 0 };
|
|
33
|
+
this._scrollDragging = false;
|
|
34
|
+
this._scrollDragStart = { x: 0, y: 0 };
|
|
35
|
+
this._scrollDragStartOffset = { x: 0, y: 0 };
|
|
36
|
+
this._lastDragPosition = { x: 0, y: 0 };
|
|
37
|
+
this._lastDragTime = 0;
|
|
38
|
+
|
|
39
|
+
// Setup scroll interaction if enabled
|
|
40
|
+
if (this.scrollable) {
|
|
41
|
+
this._setupScrollInteraction();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Track if we need to initialize scroll position on first layout
|
|
45
|
+
this._scrollInitialized = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Template method to be overridden by subclasses
|
|
49
|
+
calculateLayout() {
|
|
50
|
+
// Subclasses override this to return their layout result
|
|
51
|
+
throw new Error("Subclasses must implement calculateLayout()");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
update(dt) {
|
|
55
|
+
// Check if layout needs update
|
|
56
|
+
if (this._boundsDirty || this._layoutDirty) {
|
|
57
|
+
// Store previous dimensions to detect changes
|
|
58
|
+
const prevWidth = this.width;
|
|
59
|
+
const prevHeight = this.height;
|
|
60
|
+
|
|
61
|
+
// 1. Calculate the layout - delegated to subclasses
|
|
62
|
+
const layoutResult = this.calculateLayout();
|
|
63
|
+
|
|
64
|
+
// 2. Update dimensions if autoSize is enabled
|
|
65
|
+
if (this.autoSize && layoutResult) {
|
|
66
|
+
// Store full content size for scroll bounds calculation
|
|
67
|
+
this._contentWidth = layoutResult.width;
|
|
68
|
+
this._contentHeight = layoutResult.height;
|
|
69
|
+
|
|
70
|
+
const axis = this.getScrollAxis();
|
|
71
|
+
const viewportW = this._viewportWidth;
|
|
72
|
+
const viewportH = this._viewportHeight;
|
|
73
|
+
|
|
74
|
+
// Calculate visible size - cap to viewport when scrollable and content exceeds viewport
|
|
75
|
+
let visibleWidth = layoutResult.width;
|
|
76
|
+
let visibleHeight = layoutResult.height;
|
|
77
|
+
|
|
78
|
+
if (this.scrollable && viewportW !== null && axis.horizontal) {
|
|
79
|
+
visibleWidth = Math.min(layoutResult.width, viewportW);
|
|
80
|
+
}
|
|
81
|
+
if (this.scrollable && viewportH !== null && axis.vertical) {
|
|
82
|
+
visibleHeight = Math.min(layoutResult.height, viewportH);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Break potential infinite loops by only changing when actually different
|
|
86
|
+
if (Math.abs(this.width - visibleWidth) > 0.1) {
|
|
87
|
+
this.width = visibleWidth;
|
|
88
|
+
}
|
|
89
|
+
if (Math.abs(this.height - visibleHeight) > 0.1) {
|
|
90
|
+
this.height = visibleHeight;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Apply positions to children - but only if we have positions
|
|
95
|
+
if (layoutResult && layoutResult.positions) {
|
|
96
|
+
this.applyPositionsToChildren(layoutResult.positions);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Initialize scroll position to show start of content (with padding)
|
|
100
|
+
if (this.scrollable && !this._scrollInitialized && this._needsScrolling()) {
|
|
101
|
+
const bounds = this._getScrollBounds();
|
|
102
|
+
const axis = this.getScrollAxis();
|
|
103
|
+
// Start at max scroll to show beginning of content
|
|
104
|
+
if (axis.horizontal) this._scrollOffset.x = bounds.maxX;
|
|
105
|
+
if (axis.vertical) this._scrollOffset.y = bounds.maxY;
|
|
106
|
+
this._scrollInitialized = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 5. Clear dirty flags
|
|
110
|
+
this._boundsDirty = false;
|
|
111
|
+
this._layoutDirty = false;
|
|
112
|
+
|
|
113
|
+
// If dimensions changed and this isn't coming from a bounds update,
|
|
114
|
+
// we need to prevent markBoundsDirty from being called recursively
|
|
115
|
+
if (
|
|
116
|
+
(prevWidth !== this.width || prevHeight !== this.height) &&
|
|
117
|
+
!this._updatingBoundsFromLayout
|
|
118
|
+
) {
|
|
119
|
+
this._updatingBoundsFromLayout = true;
|
|
120
|
+
// Call parent's markBoundsDirty, but NOT our override
|
|
121
|
+
Scene.prototype.markBoundsDirty.call(this);
|
|
122
|
+
this._updatingBoundsFromLayout = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle scroll momentum when not dragging
|
|
127
|
+
if (this.scrollable && !this._scrollDragging) {
|
|
128
|
+
this._updateScrollMomentum(dt);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Call parent update
|
|
132
|
+
super.update(dt);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
markBoundsDirty() {
|
|
136
|
+
if (this._updatingBoundsFromLayout) {
|
|
137
|
+
// Just set the flag without propagating
|
|
138
|
+
this._boundsDirty = true;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Call parent implementation
|
|
143
|
+
super.markBoundsDirty();
|
|
144
|
+
|
|
145
|
+
// Set layout dirty flag
|
|
146
|
+
this._layoutDirty = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Shared method to apply positions using utility function
|
|
150
|
+
applyPositionsToChildren(positions) {
|
|
151
|
+
// Each subclass will override just the positioning options
|
|
152
|
+
applyLayout(this.children, positions, this.getLayoutOffset());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Subclasses override this to return their specific offset needs
|
|
156
|
+
getLayoutOffset() {
|
|
157
|
+
return { offsetX: 0, offsetY: 0 };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Override to mark layout dirty when children change
|
|
161
|
+
add(go) {
|
|
162
|
+
const result = super.add(go);
|
|
163
|
+
this._layoutDirty = true;
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
remove(go) {
|
|
168
|
+
const result = super.remove(go);
|
|
169
|
+
this._layoutDirty = true;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Scroll axis - subclasses override for different directions
|
|
174
|
+
getScrollAxis() {
|
|
175
|
+
return { horizontal: false, vertical: true }; // Default: vertical scroll
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Override bounds for scrollable layouts to use viewport for hit testing
|
|
179
|
+
calculateBounds() {
|
|
180
|
+
if (this.scrollable && (this._viewportWidth || this._viewportHeight)) {
|
|
181
|
+
const w = this._viewportWidth ?? this.width;
|
|
182
|
+
const h = this._viewportHeight ?? this.height;
|
|
183
|
+
return {
|
|
184
|
+
x: -w / 2,
|
|
185
|
+
y: -h / 2,
|
|
186
|
+
width: w,
|
|
187
|
+
height: h,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return super.calculateBounds();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get scroll bounds based on content size vs viewport
|
|
194
|
+
_getScrollBounds() {
|
|
195
|
+
// Use content size (full layout size before capping to viewport)
|
|
196
|
+
const contentW = this._contentWidth ?? this.width;
|
|
197
|
+
const contentH = this._contentHeight ?? this.height;
|
|
198
|
+
const viewportW = this._viewportWidth ?? contentW;
|
|
199
|
+
const viewportH = this._viewportHeight ?? contentH;
|
|
200
|
+
|
|
201
|
+
// Content is centered, so scroll range is symmetric around 0
|
|
202
|
+
// This allows scrolling to see both the start (with padding) and end of content
|
|
203
|
+
const scrollRangeX = Math.max(0, contentW - viewportW);
|
|
204
|
+
const scrollRangeY = Math.max(0, contentH - viewportH);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
minX: -scrollRangeX / 2,
|
|
208
|
+
maxX: scrollRangeX / 2,
|
|
209
|
+
minY: -scrollRangeY / 2,
|
|
210
|
+
maxY: scrollRangeY / 2,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clamp scroll position to bounds with elastic bounce-back
|
|
215
|
+
_clampScrollBounds() {
|
|
216
|
+
const bounds = this._getScrollBounds();
|
|
217
|
+
const axis = this.getScrollAxis();
|
|
218
|
+
|
|
219
|
+
if (axis.horizontal) {
|
|
220
|
+
if (this._scrollOffset.x < bounds.minX) {
|
|
221
|
+
this._scrollOffset.x +=
|
|
222
|
+
(bounds.minX - this._scrollOffset.x) * this.scrollBounce;
|
|
223
|
+
this._scrollVelocity.x = 0;
|
|
224
|
+
} else if (this._scrollOffset.x > bounds.maxX) {
|
|
225
|
+
this._scrollOffset.x +=
|
|
226
|
+
(bounds.maxX - this._scrollOffset.x) * this.scrollBounce;
|
|
227
|
+
this._scrollVelocity.x = 0;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (axis.vertical) {
|
|
232
|
+
if (this._scrollOffset.y < bounds.minY) {
|
|
233
|
+
this._scrollOffset.y +=
|
|
234
|
+
(bounds.minY - this._scrollOffset.y) * this.scrollBounce;
|
|
235
|
+
this._scrollVelocity.y = 0;
|
|
236
|
+
} else if (this._scrollOffset.y > bounds.maxY) {
|
|
237
|
+
this._scrollOffset.y +=
|
|
238
|
+
(bounds.maxY - this._scrollOffset.y) * this.scrollBounce;
|
|
239
|
+
this._scrollVelocity.y = 0;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Update scroll momentum physics
|
|
245
|
+
_updateScrollMomentum(dt) {
|
|
246
|
+
const axis = this.getScrollAxis();
|
|
247
|
+
|
|
248
|
+
if (axis.horizontal) {
|
|
249
|
+
this._scrollVelocity.x *= this.scrollFriction;
|
|
250
|
+
if (Math.abs(this._scrollVelocity.x) < this.scrollThreshold) {
|
|
251
|
+
this._scrollVelocity.x = 0;
|
|
252
|
+
}
|
|
253
|
+
this._scrollOffset.x += this._scrollVelocity.x * dt * 60;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (axis.vertical) {
|
|
257
|
+
this._scrollVelocity.y *= this.scrollFriction;
|
|
258
|
+
if (Math.abs(this._scrollVelocity.y) < this.scrollThreshold) {
|
|
259
|
+
this._scrollVelocity.y = 0;
|
|
260
|
+
}
|
|
261
|
+
this._scrollOffset.y += this._scrollVelocity.y * dt * 60;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this._clampScrollBounds();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Setup scroll interaction handlers
|
|
268
|
+
_setupScrollInteraction() {
|
|
269
|
+
this.interactive = true;
|
|
270
|
+
|
|
271
|
+
this._scrollInputDownHandler = (e) => {
|
|
272
|
+
// Manually check if the click is within our viewport bounds
|
|
273
|
+
// since Pipeline dispatches to children first, not the Scene itself
|
|
274
|
+
if (this._isPointInViewport(e.x, e.y)) {
|
|
275
|
+
this._onScrollDragStart(e);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
this._scrollInputMoveHandler = (e) => this._onScrollDragMove(e);
|
|
279
|
+
this._scrollInputUpHandler = (e) => this._onScrollDragEnd(e);
|
|
280
|
+
|
|
281
|
+
// Listen on game events instead of this.on() because Pipeline
|
|
282
|
+
// doesn't dispatch inputdown to Scene objects themselves
|
|
283
|
+
this.game.events.on("inputdown", this._scrollInputDownHandler);
|
|
284
|
+
this.game.events.on("inputmove", this._scrollInputMoveHandler);
|
|
285
|
+
this.game.events.on("inputup", this._scrollInputUpHandler);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Check if a point is within the layout's viewport bounds
|
|
289
|
+
_isPointInViewport(screenX, screenY) {
|
|
290
|
+
// Transform screen coordinates to layout's local space
|
|
291
|
+
let localX = screenX - this.x;
|
|
292
|
+
let localY = screenY - this.y;
|
|
293
|
+
|
|
294
|
+
// Account for parent transforms if we have a parent
|
|
295
|
+
let current = this.parent;
|
|
296
|
+
while (current) {
|
|
297
|
+
localX -= current.x || 0;
|
|
298
|
+
localY -= current.y || 0;
|
|
299
|
+
current = current.parent;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check against viewport bounds (centered at origin)
|
|
303
|
+
const viewportW = this._viewportWidth ?? this.width;
|
|
304
|
+
const viewportH = this._viewportHeight ?? this.height;
|
|
305
|
+
const halfW = viewportW / 2;
|
|
306
|
+
const halfH = viewportH / 2;
|
|
307
|
+
|
|
308
|
+
return localX >= -halfW && localX <= halfW && localY >= -halfH && localY <= halfH;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
_onScrollDragStart(e) {
|
|
312
|
+
this._scrollDragging = true;
|
|
313
|
+
this._scrollDragStart = { x: e.x, y: e.y };
|
|
314
|
+
this._scrollDragStartOffset = { ...this._scrollOffset };
|
|
315
|
+
this._lastDragPosition = { x: e.x, y: e.y };
|
|
316
|
+
this._lastDragTime = performance.now();
|
|
317
|
+
this._scrollVelocity = { x: 0, y: 0 };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_onScrollDragMove(e) {
|
|
321
|
+
if (!this._scrollDragging) return;
|
|
322
|
+
|
|
323
|
+
const axis = this.getScrollAxis();
|
|
324
|
+
const now = performance.now();
|
|
325
|
+
const dt = Math.max(1, now - this._lastDragTime) / 1000;
|
|
326
|
+
|
|
327
|
+
if (axis.horizontal) {
|
|
328
|
+
const deltaX = e.x - this._scrollDragStart.x;
|
|
329
|
+
this._scrollOffset.x = this._scrollDragStartOffset.x + deltaX;
|
|
330
|
+
this._scrollVelocity.x = (e.x - this._lastDragPosition.x) / (dt * 60);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (axis.vertical) {
|
|
334
|
+
const deltaY = e.y - this._scrollDragStart.y;
|
|
335
|
+
this._scrollOffset.y = this._scrollDragStartOffset.y + deltaY;
|
|
336
|
+
this._scrollVelocity.y = (e.y - this._lastDragPosition.y) / (dt * 60);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this._lastDragPosition = { x: e.x, y: e.y };
|
|
340
|
+
this._lastDragTime = now;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
_onScrollDragEnd() {
|
|
344
|
+
this._scrollDragging = false;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check if content exceeds viewport (scrolling needed)
|
|
348
|
+
_needsScrolling() {
|
|
349
|
+
if (!this.scrollable) return false;
|
|
350
|
+
|
|
351
|
+
// Use content size (full layout size before capping to viewport)
|
|
352
|
+
const contentW = this._contentWidth ?? this.width;
|
|
353
|
+
const contentH = this._contentHeight ?? this.height;
|
|
354
|
+
const viewportW = this._viewportWidth ?? contentW;
|
|
355
|
+
const viewportH = this._viewportHeight ?? contentH;
|
|
356
|
+
const axis = this.getScrollAxis();
|
|
357
|
+
|
|
358
|
+
// Only enable scrolling if content exceeds viewport in the scroll direction
|
|
359
|
+
if (axis.horizontal && contentW > viewportW) return true;
|
|
360
|
+
if (axis.vertical && contentH > viewportH) return true;
|
|
361
|
+
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Draw with clipping for scrollable layouts
|
|
366
|
+
draw() {
|
|
367
|
+
if (this._needsScrolling()) {
|
|
368
|
+
this._drawScrollable();
|
|
369
|
+
} else {
|
|
370
|
+
super.draw();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_drawScrollable() {
|
|
375
|
+
// Apply transforms (rotation, scale, etc.) from Transformable
|
|
376
|
+
this.applyTransforms();
|
|
377
|
+
|
|
378
|
+
// Draw debug bounds BEFORE clipping (shows actual content size)
|
|
379
|
+
this.drawDebug();
|
|
380
|
+
|
|
381
|
+
// Get viewport dimensions with padding inset
|
|
382
|
+
const padding = this.padding ?? 0;
|
|
383
|
+
const viewportW = (this._viewportWidth ?? this.width) - padding * 2;
|
|
384
|
+
const viewportH = (this._viewportHeight ?? this.height) - padding * 2;
|
|
385
|
+
|
|
386
|
+
// Save state, then clip to viewport and apply scroll offset
|
|
387
|
+
Painter.save();
|
|
388
|
+
|
|
389
|
+
// Clip to viewport with padding inset (centered at origin)
|
|
390
|
+
Painter.ctx.beginPath();
|
|
391
|
+
Painter.ctx.rect(-viewportW / 2, -viewportH / 2, viewportW, viewportH);
|
|
392
|
+
Painter.ctx.clip();
|
|
393
|
+
// Clear the path after clipping to prevent shapes from filling the clip rect
|
|
394
|
+
Painter.ctx.beginPath();
|
|
395
|
+
|
|
396
|
+
// Apply scroll offset
|
|
397
|
+
Painter.ctx.translate(this._scrollOffset.x, this._scrollOffset.y);
|
|
398
|
+
|
|
399
|
+
// Render children within clipped/scrolled region
|
|
400
|
+
this._collection
|
|
401
|
+
.getSortedChildren()
|
|
402
|
+
.filter((obj) => obj.visible)
|
|
403
|
+
.forEach((obj) => {
|
|
404
|
+
Painter.save();
|
|
405
|
+
obj.render();
|
|
406
|
+
Painter.restore();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
Painter.restore();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Programmatic scroll API
|
|
413
|
+
scrollTo(x, y) {
|
|
414
|
+
const axis = this.getScrollAxis();
|
|
415
|
+
if (axis.horizontal) this._scrollOffset.x = x;
|
|
416
|
+
if (axis.vertical) this._scrollOffset.y = y;
|
|
417
|
+
this._scrollVelocity = { x: 0, y: 0 };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
scrollBy(deltaX, deltaY) {
|
|
421
|
+
const axis = this.getScrollAxis();
|
|
422
|
+
if (axis.horizontal) this._scrollOffset.x += deltaX;
|
|
423
|
+
if (axis.vertical) this._scrollOffset.y += deltaY;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
getScrollPosition() {
|
|
427
|
+
return { ...this._scrollOffset };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
resetScroll() {
|
|
431
|
+
this._scrollOffset = { x: 0, y: 0 };
|
|
432
|
+
this._scrollVelocity = { x: 0, y: 0 };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// HorizontalLayout with clean implementation
|
|
437
|
+
export class HorizontalLayout extends LayoutScene {
|
|
438
|
+
// Override scroll axis for horizontal scrolling
|
|
439
|
+
getScrollAxis() {
|
|
440
|
+
return { horizontal: true, vertical: false };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Override only the layout-specific methods
|
|
444
|
+
calculateLayout() {
|
|
445
|
+
return horizontalLayout(this.children, {
|
|
446
|
+
spacing: this.spacing,
|
|
447
|
+
padding: this.padding,
|
|
448
|
+
align: this.align,
|
|
449
|
+
centerItems: true,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
getLayoutOffset() {
|
|
454
|
+
// Use content width (full size) not visible width (capped to viewport)
|
|
455
|
+
const w = this._contentWidth ?? this.width;
|
|
456
|
+
return {
|
|
457
|
+
offsetX: -w / 2,
|
|
458
|
+
offsetY: 0,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// VerticalLayout with clean implementation
|
|
464
|
+
export class VerticalLayout extends LayoutScene {
|
|
465
|
+
// Override only the layout-specific methods
|
|
466
|
+
calculateLayout() {
|
|
467
|
+
return verticalLayout(this.children, {
|
|
468
|
+
spacing: this.spacing,
|
|
469
|
+
padding: this.padding,
|
|
470
|
+
align: this.align,
|
|
471
|
+
centerItems: true,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
getLayoutOffset() {
|
|
476
|
+
// Use content height (full size) not visible height (capped to viewport)
|
|
477
|
+
const h = this._contentHeight ?? this.height;
|
|
478
|
+
return {
|
|
479
|
+
offsetX: 0,
|
|
480
|
+
offsetY: -h / 2,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// TileLayout with clean implementation
|
|
486
|
+
export class TileLayout extends LayoutScene {
|
|
487
|
+
constructor(game, options = {}) {
|
|
488
|
+
super(game, options);
|
|
489
|
+
this.columns = options.columns ?? 4;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
calculateLayout() {
|
|
493
|
+
if (!this.children.length) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return tileLayout(this.children, {
|
|
498
|
+
columns: this.columns,
|
|
499
|
+
spacing: this.spacing,
|
|
500
|
+
padding: this.padding,
|
|
501
|
+
centerItems: true,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
getLayoutOffset() {
|
|
506
|
+
// Use content size (full size) not visible size (capped to viewport)
|
|
507
|
+
const w = this._contentWidth ?? this.width;
|
|
508
|
+
const h = this._contentHeight ?? this.height;
|
|
509
|
+
return {
|
|
510
|
+
offsetX: -w / 2,
|
|
511
|
+
offsetY: -h / 2,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export class GridLayout extends LayoutScene {
|
|
517
|
+
constructor(game, options = {}) {
|
|
518
|
+
super(game, options);
|
|
519
|
+
this.columns = options.columns ?? 4;
|
|
520
|
+
this.debug = options.debug ?? false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
calculateLayout() {
|
|
524
|
+
//console.log("calculateLayout", this.columns, this.children.length);
|
|
525
|
+
if (!this.children.length) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
//console.log("calculateLayout", this.width, this.height, this.autoSize);
|
|
529
|
+
return gridLayout(this.children, {
|
|
530
|
+
columns : this.columns,
|
|
531
|
+
spacing : this.spacing,
|
|
532
|
+
padding : this.padding,
|
|
533
|
+
centerItems: this.centerItems,
|
|
534
|
+
/* only pass these two when autoSize is *off* */
|
|
535
|
+
width : this.autoSize ? undefined : this.width,
|
|
536
|
+
height: this.autoSize ? undefined : this.height,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
getLayoutOffset() {
|
|
541
|
+
// Use content size (full size) not visible size (capped to viewport)
|
|
542
|
+
const w = this._contentWidth ?? this.width;
|
|
543
|
+
const h = this._contentHeight ?? this.height;
|
|
544
|
+
return {
|
|
545
|
+
offsetX: -w / 2,
|
|
546
|
+
offsetY: -h / 2,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|