@guinetik/gcanvas 1.0.4 → 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/CNAME +1 -0
- package/dist/aizawa.html +27 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +14368 -9093
- 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/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/halvorsen.html +27 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +446 -0
- package/dist/isometric.html +34 -0
- package/dist/js/aizawa.js +425 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +851 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/lorenz.js +425 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/rossler.js +480 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +706 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/thomas.js +394 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/lorenz.html +27 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/thomas.html +27 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -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
|
@@ -431,6 +431,63 @@ export class LayoutScene extends Scene {
|
|
|
431
431
|
this._scrollOffset = { x: 0, y: 0 };
|
|
432
432
|
this._scrollVelocity = { x: 0, y: 0 };
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Returns scroll offset for hit testing coordinate transformation.
|
|
437
|
+
* @returns {{x: number, y: number}} Scroll offset to apply
|
|
438
|
+
*/
|
|
439
|
+
getHitTestOffset() {
|
|
440
|
+
if (!this.scrollable) {
|
|
441
|
+
return { x: 0, y: 0 };
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
x: this._scrollOffset?.x || 0,
|
|
445
|
+
y: this._scrollOffset?.y || 0,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Checks if a child is within the visible viewport and should be hittable.
|
|
451
|
+
* @param {GameObject} child - The child to check
|
|
452
|
+
* @returns {boolean} True if child is within viewport
|
|
453
|
+
*/
|
|
454
|
+
isChildHittable(child) {
|
|
455
|
+
// If not scrollable or doesn't need scrolling, all children are hittable
|
|
456
|
+
if (!this.scrollable || !this._needsScrolling()) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const axis = this.getScrollAxis();
|
|
461
|
+
const vpW = this._viewportWidth ?? this.width;
|
|
462
|
+
const vpH = this._viewportHeight ?? this.height;
|
|
463
|
+
const scrollX = this._scrollOffset?.x || 0;
|
|
464
|
+
const scrollY = this._scrollOffset?.y || 0;
|
|
465
|
+
|
|
466
|
+
// Child position with scroll applied (relative to viewport center)
|
|
467
|
+
const childScrolledX = child.x + scrollX;
|
|
468
|
+
const childScrolledY = child.y + scrollY;
|
|
469
|
+
|
|
470
|
+
// Get child dimensions (use half for centered bounds check)
|
|
471
|
+
const childHalfW = (child.width || 0) / 2;
|
|
472
|
+
const childHalfH = (child.height || 0) / 2;
|
|
473
|
+
|
|
474
|
+
// Check if child overlaps with viewport
|
|
475
|
+
if (axis.horizontal) {
|
|
476
|
+
// Child right edge must be past viewport left, child left edge must be before viewport right
|
|
477
|
+
if (childScrolledX + childHalfW < -vpW / 2 || childScrolledX - childHalfW > vpW / 2) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (axis.vertical) {
|
|
483
|
+
// Child bottom edge must be past viewport top, child top edge must be before viewport bottom
|
|
484
|
+
if (childScrolledY + childHalfH < -vpH / 2 || childScrolledY - childHalfH > vpH / 2) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
434
491
|
}
|
|
435
492
|
|
|
436
493
|
// HorizontalLayout with clean implementation
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mask - Clipping mask for scenes and game objects
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple shapes with animatable properties:
|
|
5
|
+
* - circle: radius
|
|
6
|
+
* - rectangle: width, height, cornerRadius
|
|
7
|
+
* - ellipse: radiusX, radiusY
|
|
8
|
+
* - path: custom path function
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Create a circular mask that grows
|
|
12
|
+
* const mask = new Mask({
|
|
13
|
+
* shape: 'circle',
|
|
14
|
+
* x: game.width / 2,
|
|
15
|
+
* y: game.height / 2,
|
|
16
|
+
* radius: 0,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Animate the radius
|
|
20
|
+
* mask.radius = 100;
|
|
21
|
+
*
|
|
22
|
+
* // Apply to context
|
|
23
|
+
* mask.apply(ctx);
|
|
24
|
+
* // ... render masked content ...
|
|
25
|
+
* mask.remove(ctx);
|
|
26
|
+
*/
|
|
27
|
+
export class Mask {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
// Shape type
|
|
30
|
+
this.shape = options.shape || 'circle';
|
|
31
|
+
|
|
32
|
+
// Position (center point)
|
|
33
|
+
this.x = options.x ?? 0;
|
|
34
|
+
this.y = options.y ?? 0;
|
|
35
|
+
|
|
36
|
+
// Scale (multiplies dimensions)
|
|
37
|
+
this.scaleX = options.scaleX ?? 1;
|
|
38
|
+
this.scaleY = options.scaleY ?? 1;
|
|
39
|
+
|
|
40
|
+
// Circle properties
|
|
41
|
+
this.radius = options.radius ?? 100;
|
|
42
|
+
|
|
43
|
+
// Rectangle properties
|
|
44
|
+
this.width = options.width ?? 200;
|
|
45
|
+
this.height = options.height ?? 200;
|
|
46
|
+
this.cornerRadius = options.cornerRadius ?? 0;
|
|
47
|
+
|
|
48
|
+
// Ellipse properties
|
|
49
|
+
this.radiusX = options.radiusX ?? 100;
|
|
50
|
+
this.radiusY = options.radiusY ?? 100;
|
|
51
|
+
|
|
52
|
+
// Custom path function: (ctx, mask) => void
|
|
53
|
+
this.pathFn = options.pathFn ?? null;
|
|
54
|
+
|
|
55
|
+
// Invert the mask (show outside, hide inside)
|
|
56
|
+
this.invert = options.invert ?? false;
|
|
57
|
+
|
|
58
|
+
// Track if currently applied (for safety)
|
|
59
|
+
this._applied = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Apply the mask to a canvas context
|
|
64
|
+
* @param {CanvasRenderingContext2D} ctx - The canvas context
|
|
65
|
+
*/
|
|
66
|
+
apply(ctx) {
|
|
67
|
+
if (this._applied) {
|
|
68
|
+
console.warn('Mask already applied. Call remove() first.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ctx.save();
|
|
73
|
+
ctx.beginPath();
|
|
74
|
+
|
|
75
|
+
if (this.invert) {
|
|
76
|
+
// For inverted mask, draw full canvas then cut out the shape
|
|
77
|
+
// This requires knowing the canvas size, so we use a large rect
|
|
78
|
+
ctx.rect(-10000, -10000, 20000, 20000);
|
|
79
|
+
this._drawPath(ctx);
|
|
80
|
+
// evenodd fill rule creates the "hole"
|
|
81
|
+
ctx.clip('evenodd');
|
|
82
|
+
} else {
|
|
83
|
+
this._drawPath(ctx);
|
|
84
|
+
ctx.clip();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._applied = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remove the mask (restore context)
|
|
92
|
+
* @param {CanvasRenderingContext2D} ctx - The canvas context
|
|
93
|
+
*/
|
|
94
|
+
remove(ctx) {
|
|
95
|
+
if (!this._applied) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
ctx.restore();
|
|
99
|
+
this._applied = false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Draw the mask path (internal)
|
|
104
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
105
|
+
*/
|
|
106
|
+
_drawPath(ctx) {
|
|
107
|
+
const sx = this.scaleX;
|
|
108
|
+
const sy = this.scaleY;
|
|
109
|
+
|
|
110
|
+
switch (this.shape) {
|
|
111
|
+
case 'circle':
|
|
112
|
+
// Use average scale for circle
|
|
113
|
+
const scale = (sx + sy) / 2;
|
|
114
|
+
ctx.arc(this.x, this.y, this.radius * scale, 0, Math.PI * 2);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'rectangle':
|
|
118
|
+
const w = this.width * sx;
|
|
119
|
+
const h = this.height * sy;
|
|
120
|
+
const cr = this.cornerRadius * Math.min(sx, sy);
|
|
121
|
+
|
|
122
|
+
if (cr > 0) {
|
|
123
|
+
// Rounded rectangle
|
|
124
|
+
this._roundedRect(ctx, this.x - w/2, this.y - h/2, w, h, cr);
|
|
125
|
+
} else {
|
|
126
|
+
// Simple rectangle
|
|
127
|
+
ctx.rect(this.x - w/2, this.y - h/2, w, h);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case 'ellipse':
|
|
132
|
+
ctx.ellipse(this.x, this.y, this.radiusX * sx, this.radiusY * sy, 0, 0, Math.PI * 2);
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case 'path':
|
|
136
|
+
if (this.pathFn) {
|
|
137
|
+
this.pathFn(ctx, this);
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
console.warn(`Unknown mask shape: ${this.shape}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Draw a rounded rectangle path
|
|
148
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
149
|
+
* @param {number} x
|
|
150
|
+
* @param {number} y
|
|
151
|
+
* @param {number} w
|
|
152
|
+
* @param {number} h
|
|
153
|
+
* @param {number} r - Corner radius
|
|
154
|
+
*/
|
|
155
|
+
_roundedRect(ctx, x, y, w, h, r) {
|
|
156
|
+
// Clamp corner radius to half of smallest dimension
|
|
157
|
+
r = Math.min(r, w / 2, h / 2);
|
|
158
|
+
|
|
159
|
+
ctx.moveTo(x + r, y);
|
|
160
|
+
ctx.lineTo(x + w - r, y);
|
|
161
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
162
|
+
ctx.lineTo(x + w, y + h - r);
|
|
163
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
164
|
+
ctx.lineTo(x + r, y + h);
|
|
165
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
166
|
+
ctx.lineTo(x, y + r);
|
|
167
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
168
|
+
ctx.closePath();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Set position
|
|
173
|
+
* @param {number} x
|
|
174
|
+
* @param {number} y
|
|
175
|
+
* @returns {Mask} this for chaining
|
|
176
|
+
*/
|
|
177
|
+
setPosition(x, y) {
|
|
178
|
+
this.x = x;
|
|
179
|
+
this.y = y;
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Set uniform scale
|
|
185
|
+
* @param {number} scale
|
|
186
|
+
* @returns {Mask} this for chaining
|
|
187
|
+
*/
|
|
188
|
+
setScale(scale) {
|
|
189
|
+
this.scaleX = scale;
|
|
190
|
+
this.scaleY = scale;
|
|
191
|
+
return this;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set non-uniform scale
|
|
196
|
+
* @param {number} sx
|
|
197
|
+
* @param {number} sy
|
|
198
|
+
* @returns {Mask} this for chaining
|
|
199
|
+
*/
|
|
200
|
+
setScaleXY(sx, sy) {
|
|
201
|
+
this.scaleX = sx;
|
|
202
|
+
this.scaleY = sy;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a circle mask
|
|
208
|
+
* @param {number} x - Center X
|
|
209
|
+
* @param {number} y - Center Y
|
|
210
|
+
* @param {number} radius
|
|
211
|
+
* @returns {Mask}
|
|
212
|
+
*/
|
|
213
|
+
static circle(x, y, radius) {
|
|
214
|
+
return new Mask({ shape: 'circle', x, y, radius });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create a rectangle mask
|
|
219
|
+
* @param {number} x - Center X
|
|
220
|
+
* @param {number} y - Center Y
|
|
221
|
+
* @param {number} width
|
|
222
|
+
* @param {number} height
|
|
223
|
+
* @param {number} cornerRadius - Optional rounded corners
|
|
224
|
+
* @returns {Mask}
|
|
225
|
+
*/
|
|
226
|
+
static rectangle(x, y, width, height, cornerRadius = 0) {
|
|
227
|
+
return new Mask({ shape: 'rectangle', x, y, width, height, cornerRadius });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create an ellipse mask
|
|
232
|
+
* @param {number} x - Center X
|
|
233
|
+
* @param {number} y - Center Y
|
|
234
|
+
* @param {number} radiusX
|
|
235
|
+
* @param {number} radiusY
|
|
236
|
+
* @returns {Mask}
|
|
237
|
+
*/
|
|
238
|
+
static ellipse(x, y, radiusX, radiusY) {
|
|
239
|
+
return new Mask({ shape: 'ellipse', x, y, radiusX, radiusY });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -186,4 +186,23 @@ export class Scene extends GameObject {
|
|
|
186
186
|
get children() {
|
|
187
187
|
return this._collection.children;
|
|
188
188
|
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns additional offset to apply during hit testing.
|
|
192
|
+
* Override in subclasses (e.g., LayoutScene) to account for scroll offset.
|
|
193
|
+
* @returns {{x: number, y: number}} Additional offset for hit test coordinate transform
|
|
194
|
+
*/
|
|
195
|
+
getHitTestOffset() {
|
|
196
|
+
return { x: 0, y: 0 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Checks if a child should be hittable (receive input events).
|
|
201
|
+
* Override in subclasses (e.g., LayoutScene) to implement viewport culling.
|
|
202
|
+
* @param {GameObject} child - The child to check
|
|
203
|
+
* @returns {boolean} True if child should be hittable
|
|
204
|
+
*/
|
|
205
|
+
isChildHittable(child) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
189
208
|
}
|
|
@@ -68,7 +68,13 @@ export class GameObjectShapeWrapper extends GameObject {
|
|
|
68
68
|
* @param {Object} options - Configuration options
|
|
69
69
|
*/
|
|
70
70
|
constructor(game, shape, options = {}) {
|
|
71
|
-
|
|
71
|
+
// IMPORTANT: Strip 'anchor' from options passed to GameObject
|
|
72
|
+
// In shape context, 'anchor' means "image rendering anchor" (center, top-left, etc.)
|
|
73
|
+
// In GameObject, 'anchor' triggers applyAnchor mixin for auto-positioning
|
|
74
|
+
// These are different concepts - don't let shape anchor trigger positioning
|
|
75
|
+
const { anchor: _shapeAnchor, ...goOptions } = options;
|
|
76
|
+
|
|
77
|
+
super(game, goOptions);
|
|
72
78
|
|
|
73
79
|
// Validate shape
|
|
74
80
|
if (!shape || shape == null || shape == undefined) {
|
|
@@ -224,9 +230,15 @@ export class GameObjectShapeWrapper extends GameObject {
|
|
|
224
230
|
|
|
225
231
|
/**
|
|
226
232
|
* Draw method to render the shape
|
|
233
|
+
*
|
|
234
|
+
* IMPORTANT: Call shape.draw() NOT shape.render()!
|
|
235
|
+
* The wrapper's render() has already translated to (this.x, this.y).
|
|
236
|
+
* Calling shape.render() would call Painter.translateTo(shape.x, shape.y)
|
|
237
|
+
* which OVERWRITES (not adds to) the current translation.
|
|
238
|
+
* By calling shape.draw() directly, we render at the wrapper's position.
|
|
227
239
|
*/
|
|
228
240
|
draw() {
|
|
229
241
|
super.draw();
|
|
230
|
-
this.shape.
|
|
242
|
+
this.shape.draw();
|
|
231
243
|
}
|
|
232
244
|
}
|
package/src/game/pipeline.js
CHANGED
|
@@ -78,6 +78,17 @@ export class Pipeline extends Loggable {
|
|
|
78
78
|
if (scene.children && scene.children.length > 0) {
|
|
79
79
|
for (let i = scene.children.length - 1; i >= 0; i--) {
|
|
80
80
|
const child = scene.children[i];
|
|
81
|
+
|
|
82
|
+
// Check if child is hittable (e.g., within viewport for scrollable layouts)
|
|
83
|
+
if (scene.isChildHittable && !scene.isChildHittable(child)) {
|
|
84
|
+
// Force mouseout if child was hovered but is now outside viewport
|
|
85
|
+
if (child._hovered) {
|
|
86
|
+
child._hovered = false;
|
|
87
|
+
child.events.emit("mouseout", e);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
if (child instanceof Scene) {
|
|
82
93
|
this._hoverScene(child, e); // recurse into nested scenes
|
|
83
94
|
} else {
|
|
@@ -152,6 +163,12 @@ export class Pipeline extends Loggable {
|
|
|
152
163
|
// First check children (they render on top, so should get priority)
|
|
153
164
|
for (let i = scene.children.length - 1; i >= 0; i--) {
|
|
154
165
|
const child = scene.children[i];
|
|
166
|
+
|
|
167
|
+
// Check if child is hittable (e.g., within viewport for scrollable layouts)
|
|
168
|
+
if (scene.isChildHittable && !scene.isChildHittable(child)) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
155
172
|
if (child instanceof Scene) {
|
|
156
173
|
// Recurse deeper if child is also a Scene
|
|
157
174
|
const hit = this._dispatchToScene(child, type, e);
|
package/src/game/ui/button.js
CHANGED
|
@@ -110,8 +110,9 @@ export class Button extends GameObject {
|
|
|
110
110
|
// Basic position and sizing
|
|
111
111
|
this.x = x;
|
|
112
112
|
this.y = y;
|
|
113
|
-
|
|
114
|
-
this.
|
|
113
|
+
// Ensure minimum touch target size (44x44px recommended for mobile)
|
|
114
|
+
this.width = Math.max(width, 44);
|
|
115
|
+
this.height = Math.max(height, 44);
|
|
115
116
|
this.padding = padding;
|
|
116
117
|
this.textAlign = textAlign;
|
|
117
118
|
this.textBaseline = textBaseline;
|
|
@@ -200,6 +201,10 @@ export class Button extends GameObject {
|
|
|
200
201
|
/**
|
|
201
202
|
* Update label position based on alignment and baseline settings
|
|
202
203
|
* @private
|
|
204
|
+
*
|
|
205
|
+
* Note: TextShape now centers its bounding box at (x, y) regardless of textAlign.
|
|
206
|
+
* For non-center alignments, we adjust the position by half the text dimensions
|
|
207
|
+
* so that left-aligned text STARTS at the left edge, not centers there.
|
|
203
208
|
*/
|
|
204
209
|
alignText() {
|
|
205
210
|
if (!this.label) return;
|
|
@@ -207,13 +212,22 @@ export class Button extends GameObject {
|
|
|
207
212
|
const halfWidth = this.width / 2;
|
|
208
213
|
const halfHeight = this.height / 2;
|
|
209
214
|
|
|
210
|
-
//
|
|
215
|
+
// Get text dimensions (available after TextShape._calculateBounds)
|
|
216
|
+
const textHalfWidth = (this.label._width || 0) / 2;
|
|
217
|
+
const textHalfHeight = (this.label._height || 0) / 2;
|
|
218
|
+
|
|
219
|
+
// Horizontal alignment - position where text CENTER should be
|
|
220
|
+
// TextShape centers its bounding box at (x, y), so we adjust accordingly
|
|
211
221
|
switch (this.textAlign) {
|
|
212
222
|
case "left":
|
|
213
|
-
|
|
223
|
+
// Text should START at left edge + padding
|
|
224
|
+
// So text CENTER should be at left edge + padding + half text width
|
|
225
|
+
this.label.x = -halfWidth + this.padding + textHalfWidth;
|
|
214
226
|
break;
|
|
215
227
|
case "right":
|
|
216
|
-
|
|
228
|
+
// Text should END at right edge - padding
|
|
229
|
+
// So text CENTER should be at right edge - padding - half text width
|
|
230
|
+
this.label.x = halfWidth - this.padding - textHalfWidth;
|
|
217
231
|
break;
|
|
218
232
|
case "center":
|
|
219
233
|
default:
|
|
@@ -221,13 +235,15 @@ export class Button extends GameObject {
|
|
|
221
235
|
break;
|
|
222
236
|
}
|
|
223
237
|
|
|
224
|
-
// Vertical alignment
|
|
238
|
+
// Vertical alignment - position where text CENTER should be
|
|
225
239
|
switch (this.textBaseline) {
|
|
226
240
|
case "top":
|
|
227
|
-
|
|
241
|
+
// Text should START at top edge + padding
|
|
242
|
+
this.label.y = -halfHeight + this.padding + textHalfHeight;
|
|
228
243
|
break;
|
|
229
244
|
case "bottom":
|
|
230
|
-
|
|
245
|
+
// Text should END at bottom edge - padding
|
|
246
|
+
this.label.y = halfHeight - this.padding - textHalfHeight;
|
|
231
247
|
break;
|
|
232
248
|
case "middle":
|
|
233
249
|
default:
|
|
@@ -257,16 +273,85 @@ export class Button extends GameObject {
|
|
|
257
273
|
this.onPressed = onPressed;
|
|
258
274
|
this.onRelease = onRelease;
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
this.
|
|
262
|
-
this.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
276
|
+
// Track pointer state for proper hover handling
|
|
277
|
+
this._pointerOver = false;
|
|
278
|
+
this._isTouch = false;
|
|
279
|
+
|
|
280
|
+
// Mouse hover events (desktop only)
|
|
281
|
+
this.on("mouseover", (e) => {
|
|
282
|
+
this._pointerOver = true;
|
|
283
|
+
if (!this._isTouch) {
|
|
284
|
+
this.setState("hover");
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.on("mouseout", (e) => {
|
|
289
|
+
this._pointerOver = false;
|
|
290
|
+
if (!this._isTouch) {
|
|
291
|
+
this.setState("default");
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Touch/pointer down - prevent default for mobile-friendly behavior
|
|
296
|
+
this.on("inputdown", (e) => {
|
|
297
|
+
// Detect touch input
|
|
298
|
+
if (e.touches || (e.nativeEvent && e.nativeEvent.type === 'touchstart')) {
|
|
299
|
+
this._isTouch = true;
|
|
300
|
+
// Prevent default touch behaviors (scrolling, zooming)
|
|
301
|
+
if (e.nativeEvent) {
|
|
302
|
+
e.nativeEvent.preventDefault();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
this._pointerOver = true;
|
|
306
|
+
this.setState("pressed");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Touch/pointer up
|
|
310
|
+
this.on("inputup", (e) => {
|
|
311
|
+
const wasPressed = this.state === "pressed";
|
|
312
|
+
|
|
313
|
+
// Prevent default touch behaviors
|
|
314
|
+
if (e.touches || (e.nativeEvent && e.nativeEvent.type === 'touchend')) {
|
|
315
|
+
if (e.nativeEvent) {
|
|
316
|
+
e.nativeEvent.preventDefault();
|
|
317
|
+
}
|
|
318
|
+
this._isTouch = true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Verify pointer is still over button using hit test
|
|
322
|
+
const stillOver = this._hitTest && this._hitTest(e.x, e.y);
|
|
323
|
+
|
|
324
|
+
// Fire onClick if user was in "pressed" state and pointer is still over
|
|
325
|
+
if (wasPressed && stillOver && typeof onClick === "function") {
|
|
266
326
|
onClick();
|
|
267
327
|
}
|
|
268
|
-
|
|
269
|
-
|
|
328
|
+
|
|
329
|
+
// Check if pointer is still over the button
|
|
330
|
+
// On touch devices, don't set hover state (no hover on touch)
|
|
331
|
+
if (stillOver && !this._isTouch) {
|
|
332
|
+
this.setState("hover");
|
|
333
|
+
} else {
|
|
334
|
+
this.setState("default");
|
|
335
|
+
// Reset touch flag after a delay to allow mouse hover to work
|
|
336
|
+
if (this._isTouch) {
|
|
337
|
+
setTimeout(() => {
|
|
338
|
+
this._isTouch = false;
|
|
339
|
+
}, 300);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Touch move - update pointer position
|
|
345
|
+
this.on("inputmove", (e) => {
|
|
346
|
+
// Check if pointer is still over button
|
|
347
|
+
if (this._hitTest && this._hitTest(e.x, e.y)) {
|
|
348
|
+
this._pointerOver = true;
|
|
349
|
+
} else {
|
|
350
|
+
this._pointerOver = false;
|
|
351
|
+
if (this.state === "hover" && !this._isTouch) {
|
|
352
|
+
this.setState("default");
|
|
353
|
+
}
|
|
354
|
+
}
|
|
270
355
|
});
|
|
271
356
|
}
|
|
272
357
|
|
package/src/game/ui/theme.js
CHANGED
|
@@ -43,7 +43,13 @@ export class ToggleButton extends Button {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Update our visual style for toggled vs. not
|
|
46
|
+
// Store current state before refresh
|
|
47
|
+
const currentState = this.state;
|
|
46
48
|
this.refreshToggleVisual();
|
|
49
|
+
// Re-apply current state after refresh to ensure proper colors
|
|
50
|
+
if (currentState) {
|
|
51
|
+
this.setState(currentState);
|
|
52
|
+
}
|
|
47
53
|
},
|
|
48
54
|
});
|
|
49
55
|
// Terminal × Vercel theme for toggled state
|
|
@@ -59,8 +65,13 @@ export class ToggleButton extends Button {
|
|
|
59
65
|
|
|
60
66
|
toggle(v) {
|
|
61
67
|
// Toggle the button state and refresh visuals
|
|
68
|
+
const currentState = this.state;
|
|
62
69
|
this.toggled = v;
|
|
63
70
|
this.refreshToggleVisual();
|
|
71
|
+
// Re-apply current state after refresh to ensure proper colors
|
|
72
|
+
if (currentState) {
|
|
73
|
+
this.setState(currentState);
|
|
74
|
+
}
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
/**
|
|
@@ -68,32 +79,32 @@ export class ToggleButton extends Button {
|
|
|
68
79
|
*/
|
|
69
80
|
refreshToggleVisual() {
|
|
70
81
|
if (this.toggled) {
|
|
71
|
-
//
|
|
72
|
-
this.bg.
|
|
73
|
-
this.bg.
|
|
82
|
+
// Active/toggled styling - use correct property names
|
|
83
|
+
this.bg.color = this.colorActiveBg;
|
|
84
|
+
this.bg.stroke = this.colorActiveStroke;
|
|
74
85
|
this.label.color = this.colorActiveText;
|
|
75
86
|
} else {
|
|
76
|
-
// Revert to normal styling
|
|
77
|
-
this.bg.
|
|
78
|
-
this.bg.
|
|
87
|
+
// Revert to normal styling - use correct property names
|
|
88
|
+
this.bg.color = this.colors.default.bg;
|
|
89
|
+
this.bg.stroke = this.colors.default.stroke;
|
|
79
90
|
this.label.color = this.colors.default.text;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* so we don't lose the "toggled" color. This is optional.
|
|
95
|
+
* Override setState to properly handle toggled state.
|
|
96
|
+
* When toggled, always use active colors. When not toggled, use normal button behavior.
|
|
87
97
|
*/
|
|
88
98
|
setState(state) {
|
|
99
|
+
// Always call parent first to handle cursor and callbacks
|
|
89
100
|
super.setState(state);
|
|
90
|
-
|
|
91
|
-
// If
|
|
92
|
-
// after the parent sets hover/pressed colors.
|
|
101
|
+
|
|
102
|
+
// If toggled, override colors with active colors (ignore hover/pressed colors)
|
|
93
103
|
if (this.toggled) {
|
|
94
|
-
this.bg.
|
|
95
|
-
this.bg.
|
|
104
|
+
this.bg.color = this.colorActiveBg;
|
|
105
|
+
this.bg.stroke = this.colorActiveStroke;
|
|
96
106
|
this.label.color = this.colorActiveText;
|
|
97
107
|
}
|
|
108
|
+
// If not toggled, parent's setState already set the correct colors
|
|
98
109
|
}
|
|
99
110
|
}
|
package/src/game/ui/tooltip.js
CHANGED
|
@@ -237,12 +237,20 @@ export class Tooltip extends GameObject {
|
|
|
237
237
|
this.bg.height = textHeight + this.padding * 2;
|
|
238
238
|
|
|
239
239
|
// Position each line inside bg
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
// TextShape now centers its bounding box at (x, y), so for left/top aligned text
|
|
241
|
+
// we need to position the CENTER where we want it, not the top-left corner.
|
|
242
|
+
// Text should START at the left edge + padding, so CENTER is at left edge + padding + textWidth/2
|
|
243
|
+
const bgLeft = -this.bg.width / 2;
|
|
242
244
|
|
|
243
245
|
for (let i = 0; i < this.lineShapes.length; i++) {
|
|
244
|
-
this.lineShapes[i]
|
|
245
|
-
|
|
246
|
+
const shape = this.lineShapes[i];
|
|
247
|
+
const textHalfWidth = (shape._width || 0) / 2;
|
|
248
|
+
const textHalfHeight = (shape._height || lineHeight) / 2;
|
|
249
|
+
|
|
250
|
+
// Text starts at left edge + padding, so center is at left edge + padding + halfWidth
|
|
251
|
+
shape.x = bgLeft + this.padding + textHalfWidth;
|
|
252
|
+
// Text starts at top edge + padding + i*lineHeight, so center is offset by halfHeight
|
|
253
|
+
shape.y = -this.bg.height / 2 + this.padding + i * lineHeight + textHalfHeight;
|
|
246
254
|
}
|
|
247
255
|
}
|
|
248
256
|
|