@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tetromino piece definitions and TetrisPiece class
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { CONFIG, SHAPES, PIECE_TYPES } from "./config.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TetrisPiece - Represents a falling tetromino in 3D space
|
|
9
|
+
*
|
|
10
|
+
* Uses a voxel-based representation for true 3D rotation.
|
|
11
|
+
* Pieces can rotate around X, Y, and Z axes.
|
|
12
|
+
*/
|
|
13
|
+
export class TetrisPiece {
|
|
14
|
+
/**
|
|
15
|
+
* Create a new tetromino piece
|
|
16
|
+
* @param {string} type - Piece type (I, O, T, S, Z, L, J)
|
|
17
|
+
*/
|
|
18
|
+
constructor(type) {
|
|
19
|
+
this.type = type;
|
|
20
|
+
this.color = SHAPES[type].color;
|
|
21
|
+
|
|
22
|
+
// Convert 2D matrix to 3D voxels (relative positions)
|
|
23
|
+
this.voxels = this._matrixToVoxels(SHAPES[type].matrix);
|
|
24
|
+
|
|
25
|
+
// Position in grid coordinates
|
|
26
|
+
// Y = 0 is top of well, Y increases downward
|
|
27
|
+
this.x = 0;
|
|
28
|
+
this.y = 0;
|
|
29
|
+
this.z = 0;
|
|
30
|
+
|
|
31
|
+
// Center the piece horizontally in the well
|
|
32
|
+
this._centerPiece();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert 2D matrix to 3D voxel array
|
|
37
|
+
* @param {number[][]} matrix - 2D shape matrix
|
|
38
|
+
* @returns {Array<{x: number, y: number, z: number}>}
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
_matrixToVoxels(matrix) {
|
|
42
|
+
const voxels = [];
|
|
43
|
+
for (let z = 0; z < matrix.length; z++) {
|
|
44
|
+
for (let x = 0; x < matrix[z].length; x++) {
|
|
45
|
+
if (matrix[z][x]) {
|
|
46
|
+
voxels.push({ x, y: 0, z }); // y=0 since pieces start flat
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return voxels;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Center the piece at the top of the well
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
_centerPiece() {
|
|
58
|
+
const { width, depth } = CONFIG.grid;
|
|
59
|
+
const bounds = this.getBounds();
|
|
60
|
+
|
|
61
|
+
this.x = Math.floor((width - bounds.width) / 2);
|
|
62
|
+
this.z = Math.floor((depth - bounds.depth) / 2);
|
|
63
|
+
this.y = 0; // Start at top
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get bounding box dimensions of the piece
|
|
68
|
+
* @returns {{width: number, height: number, depth: number}}
|
|
69
|
+
*/
|
|
70
|
+
getBounds() {
|
|
71
|
+
if (this.voxels.length === 0) {
|
|
72
|
+
return { width: 0, height: 0, depth: 0 };
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
width: Math.max(...this.voxels.map((v) => v.x)) + 1,
|
|
76
|
+
height: Math.max(...this.voxels.map((v) => v.y)) + 1,
|
|
77
|
+
depth: Math.max(...this.voxels.map((v) => v.z)) + 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all world positions occupied by this piece
|
|
83
|
+
* @returns {Array<{x: number, y: number, z: number}>}
|
|
84
|
+
*/
|
|
85
|
+
getWorldPositions() {
|
|
86
|
+
return this.voxels.map((v) => ({
|
|
87
|
+
x: this.x + v.x,
|
|
88
|
+
y: this.y + v.y,
|
|
89
|
+
z: this.z + v.z,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Move the piece by the given offset
|
|
95
|
+
* @param {number} dx - X offset
|
|
96
|
+
* @param {number} dy - Y offset (positive = down)
|
|
97
|
+
* @param {number} dz - Z offset
|
|
98
|
+
*/
|
|
99
|
+
move(dx, dy, dz) {
|
|
100
|
+
this.x += dx;
|
|
101
|
+
this.y += dy;
|
|
102
|
+
this.z += dz;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Rotate the piece around the specified axis
|
|
107
|
+
* @param {string} axis - 'x', 'y', or 'z'
|
|
108
|
+
* @param {number} direction - 1 for clockwise, -1 for counter-clockwise
|
|
109
|
+
*/
|
|
110
|
+
rotate(axis = "y", direction = 1) {
|
|
111
|
+
// O piece doesn't rotate
|
|
112
|
+
if (this.type === "O") return;
|
|
113
|
+
|
|
114
|
+
this.voxels = this.voxels.map((v) => {
|
|
115
|
+
switch (axis) {
|
|
116
|
+
case "y": // Horizontal rotation (around Y axis)
|
|
117
|
+
return direction === 1
|
|
118
|
+
? { x: -v.z, y: v.y, z: v.x }
|
|
119
|
+
: { x: v.z, y: v.y, z: -v.x };
|
|
120
|
+
case "x": // Pitch rotation (around X axis)
|
|
121
|
+
return direction === 1
|
|
122
|
+
? { x: v.x, y: -v.z, z: v.y }
|
|
123
|
+
: { x: v.x, y: v.z, z: -v.y };
|
|
124
|
+
case "z": // Roll rotation (around Z axis)
|
|
125
|
+
return direction === 1
|
|
126
|
+
? { x: -v.y, y: v.x, z: v.z }
|
|
127
|
+
: { x: v.y, y: -v.x, z: v.z };
|
|
128
|
+
default:
|
|
129
|
+
return v;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Normalize to keep piece grounded (min coords = 0)
|
|
134
|
+
this._normalizeVoxels();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Normalize voxels so minimum x/y/z is 0
|
|
139
|
+
* This keeps the piece properly bounded after rotation
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_normalizeVoxels() {
|
|
143
|
+
if (this.voxels.length === 0) return;
|
|
144
|
+
|
|
145
|
+
const minX = Math.min(...this.voxels.map((v) => v.x));
|
|
146
|
+
const minY = Math.min(...this.voxels.map((v) => v.y));
|
|
147
|
+
const minZ = Math.min(...this.voxels.map((v) => v.z));
|
|
148
|
+
|
|
149
|
+
this.voxels = this.voxels.map((v) => ({
|
|
150
|
+
x: v.x - minX,
|
|
151
|
+
y: v.y - minY,
|
|
152
|
+
z: v.z - minZ,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Undo a rotation (for wall kick failure)
|
|
158
|
+
* @param {string} axis - 'x', 'y', or 'z'
|
|
159
|
+
* @param {number} direction - Original rotation direction to undo
|
|
160
|
+
*/
|
|
161
|
+
undoRotate(axis = "y", direction = 1) {
|
|
162
|
+
this.rotate(axis, -direction);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get the width of the piece (X dimension)
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
getWidth() {
|
|
170
|
+
return this.getBounds().width;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get the depth of the piece (Z dimension)
|
|
175
|
+
* @returns {number}
|
|
176
|
+
*/
|
|
177
|
+
getDepth() {
|
|
178
|
+
return this.getBounds().depth;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Clone this piece (for ghost piece calculation)
|
|
183
|
+
* @returns {TetrisPiece}
|
|
184
|
+
*/
|
|
185
|
+
clone() {
|
|
186
|
+
const cloned = new TetrisPiece(this.type);
|
|
187
|
+
cloned.voxels = this.voxels.map((v) => ({ ...v }));
|
|
188
|
+
cloned.x = this.x;
|
|
189
|
+
cloned.y = this.y;
|
|
190
|
+
cloned.z = this.z;
|
|
191
|
+
return cloned;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Bag randomizer for fair piece distribution
|
|
197
|
+
* Uses the "7-bag" system where each bag contains all 7 pieces
|
|
198
|
+
*/
|
|
199
|
+
class PieceBag {
|
|
200
|
+
constructor() {
|
|
201
|
+
this.bag = [];
|
|
202
|
+
this._refillBag();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Refill the bag with all piece types, shuffled
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
_refillBag() {
|
|
210
|
+
this.bag = [...PIECE_TYPES];
|
|
211
|
+
// Fisher-Yates shuffle
|
|
212
|
+
for (let i = this.bag.length - 1; i > 0; i--) {
|
|
213
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
214
|
+
[this.bag[i], this.bag[j]] = [this.bag[j], this.bag[i]];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the next piece type from the bag
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
next() {
|
|
223
|
+
if (this.bag.length === 0) {
|
|
224
|
+
this._refillBag();
|
|
225
|
+
}
|
|
226
|
+
return this.bag.pop();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Peek at the next piece without removing it
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
peek() {
|
|
234
|
+
if (this.bag.length === 0) {
|
|
235
|
+
this._refillBag();
|
|
236
|
+
}
|
|
237
|
+
return this.bag[this.bag.length - 1];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Global piece bag instance
|
|
242
|
+
let pieceBag = null;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get a random piece using the bag randomizer
|
|
246
|
+
* @returns {TetrisPiece}
|
|
247
|
+
*/
|
|
248
|
+
export function getRandomPiece() {
|
|
249
|
+
if (!pieceBag) {
|
|
250
|
+
pieceBag = new PieceBag();
|
|
251
|
+
}
|
|
252
|
+
return new TetrisPiece(pieceBag.next());
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Peek at the next piece type without consuming it
|
|
257
|
+
* @returns {string}
|
|
258
|
+
*/
|
|
259
|
+
export function peekNextPieceType() {
|
|
260
|
+
if (!pieceBag) {
|
|
261
|
+
pieceBag = new PieceBag();
|
|
262
|
+
}
|
|
263
|
+
return pieceBag.peek();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Reset the piece bag (for game restart)
|
|
268
|
+
*/
|
|
269
|
+
export function resetPieceBag() {
|
|
270
|
+
pieceBag = new PieceBag();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Create a specific piece type (for preview/testing)
|
|
275
|
+
* @param {string} type - Piece type
|
|
276
|
+
* @returns {TetrisPiece}
|
|
277
|
+
*/
|
|
278
|
+
export function createPiece(type) {
|
|
279
|
+
return new TetrisPiece(type);
|
|
280
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thomas Attractor 3D Visualization
|
|
3
|
+
*
|
|
4
|
+
* Thomas' Cyclically Symmetric Attractor (1999) discovered by René Thomas.
|
|
5
|
+
* Features elegant symmetry and smooth cyclical motion with a simple
|
|
6
|
+
* sinusoidal structure.
|
|
7
|
+
*
|
|
8
|
+
* Uses the Attractors module for pure math functions and WebGL for
|
|
9
|
+
* high-performance line rendering.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Game, Gesture, Screen, Attractors } from "/gcanvas.es.min.js";
|
|
13
|
+
import { Camera3D } from "/gcanvas.es.min.js";
|
|
14
|
+
import { WebGLLineRenderer } from "/gcanvas.es.min.js";
|
|
15
|
+
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
// CONFIGURATION
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const CONFIG = {
|
|
21
|
+
// Attractor settings (uses Attractors.thomas for equations)
|
|
22
|
+
attractor: {
|
|
23
|
+
dt: 0.08, // Thomas needs larger dt
|
|
24
|
+
scale: 60, // Scale factor for display
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Particle settings
|
|
28
|
+
particles: {
|
|
29
|
+
count: 300,
|
|
30
|
+
trailLength: 300,
|
|
31
|
+
spawnRange: 2, // Initial position range
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// Center offset - adjust to match attractor's visual barycenter
|
|
35
|
+
center: {
|
|
36
|
+
x: -0.2,
|
|
37
|
+
y: -0.2,
|
|
38
|
+
z: 0,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Camera settings
|
|
42
|
+
camera: {
|
|
43
|
+
perspective: 800,
|
|
44
|
+
rotationX: 0.3,
|
|
45
|
+
rotationY: 0.2,
|
|
46
|
+
inertia: true,
|
|
47
|
+
friction: 0.95,
|
|
48
|
+
clampX: false,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Visual settings - green/teal palette for Thomas
|
|
52
|
+
visual: {
|
|
53
|
+
minHue: 120, // Green (fast)
|
|
54
|
+
maxHue: 200, // Cyan-blue (slow)
|
|
55
|
+
maxSpeed: 2.5, // Thomas is slow-moving
|
|
56
|
+
saturation: 85,
|
|
57
|
+
lightness: 50,
|
|
58
|
+
maxAlpha: 0.8,
|
|
59
|
+
hueShiftSpeed: 8,
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// Glitch/blink effect
|
|
63
|
+
blink: {
|
|
64
|
+
chance: 0.012,
|
|
65
|
+
minDuration: 0.06,
|
|
66
|
+
maxDuration: 0.25,
|
|
67
|
+
intensityBoost: 1.4,
|
|
68
|
+
saturationBoost: 1.15,
|
|
69
|
+
alphaBoost: 1.2,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Zoom settings
|
|
73
|
+
zoom: {
|
|
74
|
+
min: 0.3,
|
|
75
|
+
max: 3.0,
|
|
76
|
+
speed: 0.5,
|
|
77
|
+
easing: 0.12,
|
|
78
|
+
baseScreenSize: 600,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// HELPER FUNCTIONS
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function hslToRgb(h, s, l) {
|
|
87
|
+
s /= 100;
|
|
88
|
+
l /= 100;
|
|
89
|
+
const k = (n) => (n + h / 30) % 12;
|
|
90
|
+
const a = s * Math.min(l, 1 - l);
|
|
91
|
+
const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
92
|
+
return {
|
|
93
|
+
r: Math.round(255 * f(0)),
|
|
94
|
+
g: Math.round(255 * f(8)),
|
|
95
|
+
b: Math.round(255 * f(4)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// ATTRACTOR PARTICLE
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
class AttractorParticle {
|
|
104
|
+
constructor(stepFn, spawnRange) {
|
|
105
|
+
this.stepFn = stepFn;
|
|
106
|
+
this.position = {
|
|
107
|
+
x: (Math.random() - 0.5) * spawnRange,
|
|
108
|
+
y: (Math.random() - 0.5) * spawnRange,
|
|
109
|
+
z: (Math.random() - 0.5) * spawnRange,
|
|
110
|
+
};
|
|
111
|
+
this.trail = [];
|
|
112
|
+
this.speed = 0;
|
|
113
|
+
this.blinkTime = 0;
|
|
114
|
+
this.blinkIntensity = 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateBlink(dt) {
|
|
118
|
+
const { chance, minDuration, maxDuration } = CONFIG.blink;
|
|
119
|
+
|
|
120
|
+
if (this.blinkTime > 0) {
|
|
121
|
+
this.blinkTime -= dt;
|
|
122
|
+
this.blinkIntensity = Math.max(
|
|
123
|
+
0,
|
|
124
|
+
this.blinkTime > 0
|
|
125
|
+
? Math.sin((this.blinkTime / ((minDuration + maxDuration) * 0.5)) * Math.PI)
|
|
126
|
+
: 0
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
if (Math.random() < chance) {
|
|
130
|
+
this.blinkTime = minDuration + Math.random() * (maxDuration - minDuration);
|
|
131
|
+
this.blinkIntensity = 1;
|
|
132
|
+
} else {
|
|
133
|
+
this.blinkIntensity = 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
update(dt, scale) {
|
|
139
|
+
const result = this.stepFn(this.position, dt);
|
|
140
|
+
this.position = result.position;
|
|
141
|
+
this.speed = result.speed;
|
|
142
|
+
|
|
143
|
+
// Add to trail (centered and scaled for display)
|
|
144
|
+
this.trail.unshift({
|
|
145
|
+
x: (this.position.x - CONFIG.center.x) * scale,
|
|
146
|
+
y: (this.position.y - CONFIG.center.y) * scale,
|
|
147
|
+
z: (this.position.z - CONFIG.center.z) * scale,
|
|
148
|
+
speed: this.speed,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (this.trail.length > CONFIG.particles.trailLength) {
|
|
152
|
+
this.trail.pop();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// DEMO CLASS
|
|
159
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
class ThomasDemo extends Game {
|
|
162
|
+
constructor(canvas) {
|
|
163
|
+
super(canvas);
|
|
164
|
+
this.backgroundColor = "#000";
|
|
165
|
+
this.enableFluidSize();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
init() {
|
|
169
|
+
super.init();
|
|
170
|
+
|
|
171
|
+
this.attractor = Attractors.thomas;
|
|
172
|
+
console.log(`Attractor: ${this.attractor.name}`);
|
|
173
|
+
console.log(`Equations:`, this.attractor.equations);
|
|
174
|
+
|
|
175
|
+
this.stepFn = this.attractor.createStepper();
|
|
176
|
+
|
|
177
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
178
|
+
const initialZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
179
|
+
this.zoom = initialZoom;
|
|
180
|
+
this.targetZoom = initialZoom;
|
|
181
|
+
this.defaultZoom = initialZoom;
|
|
182
|
+
|
|
183
|
+
this.camera = new Camera3D({
|
|
184
|
+
perspective: CONFIG.camera.perspective,
|
|
185
|
+
rotationX: CONFIG.camera.rotationX,
|
|
186
|
+
rotationY: CONFIG.camera.rotationY,
|
|
187
|
+
inertia: CONFIG.camera.inertia,
|
|
188
|
+
friction: CONFIG.camera.friction,
|
|
189
|
+
clampX: CONFIG.camera.clampX,
|
|
190
|
+
});
|
|
191
|
+
this.camera.enableMouseControl(this.canvas);
|
|
192
|
+
|
|
193
|
+
this.gesture = new Gesture(this.canvas, {
|
|
194
|
+
onZoom: (delta) => {
|
|
195
|
+
this.targetZoom *= 1 + delta * CONFIG.zoom.speed;
|
|
196
|
+
},
|
|
197
|
+
onPan: null,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this.canvas.addEventListener("dblclick", () => {
|
|
201
|
+
this.targetZoom = this.defaultZoom;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Log camera params and barycenter on mouse release
|
|
205
|
+
this.canvas.addEventListener("mouseup", () => {
|
|
206
|
+
console.log(`Camera: rotationX: ${this.camera.rotationX.toFixed(3)}, rotationY: ${this.camera.rotationY.toFixed(3)}`);
|
|
207
|
+
let sumX = 0, sumY = 0, sumZ = 0, count = 0;
|
|
208
|
+
for (const p of this.particles) {
|
|
209
|
+
sumX += p.position.x;
|
|
210
|
+
sumY += p.position.y;
|
|
211
|
+
sumZ += p.position.z;
|
|
212
|
+
count++;
|
|
213
|
+
}
|
|
214
|
+
console.log(`Barycenter: x: ${(sumX/count).toFixed(3)}, y: ${(sumY/count).toFixed(3)}, z: ${(sumZ/count).toFixed(3)}`);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
this.particles = [];
|
|
218
|
+
for (let i = 0; i < CONFIG.particles.count; i++) {
|
|
219
|
+
this.particles.push(
|
|
220
|
+
new AttractorParticle(this.stepFn, CONFIG.particles.spawnRange)
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const maxSegments = CONFIG.particles.count * CONFIG.particles.trailLength;
|
|
225
|
+
this.lineRenderer = new WebGLLineRenderer(maxSegments, {
|
|
226
|
+
width: this.width,
|
|
227
|
+
height: this.height,
|
|
228
|
+
blendMode: "additive",
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.segments = [];
|
|
232
|
+
|
|
233
|
+
if (!this.lineRenderer.isAvailable()) {
|
|
234
|
+
console.warn("WebGL not available, falling back to Canvas 2D");
|
|
235
|
+
this.useWebGL = false;
|
|
236
|
+
} else {
|
|
237
|
+
this.useWebGL = true;
|
|
238
|
+
console.log(`WebGL enabled, ${maxSegments} max segments`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.time = 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
onResize() {
|
|
245
|
+
if (this.lineRenderer?.isAvailable()) {
|
|
246
|
+
this.lineRenderer.resize(this.width, this.height);
|
|
247
|
+
}
|
|
248
|
+
const { min, max, baseScreenSize } = CONFIG.zoom;
|
|
249
|
+
this.defaultZoom = Math.min(max, Math.max(min, Screen.minDimension() / baseScreenSize));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
update(dt) {
|
|
253
|
+
super.update(dt);
|
|
254
|
+
this.camera.update(dt);
|
|
255
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoom.easing;
|
|
256
|
+
this.time += dt;
|
|
257
|
+
|
|
258
|
+
for (const particle of this.particles) {
|
|
259
|
+
particle.update(CONFIG.attractor.dt, CONFIG.attractor.scale);
|
|
260
|
+
particle.updateBlink(dt);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
collectSegments(cx, cy) {
|
|
265
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
266
|
+
CONFIG.visual;
|
|
267
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
268
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
269
|
+
|
|
270
|
+
this.segments.length = 0;
|
|
271
|
+
|
|
272
|
+
for (const particle of this.particles) {
|
|
273
|
+
if (particle.trail.length < 2) continue;
|
|
274
|
+
|
|
275
|
+
const blink = particle.blinkIntensity;
|
|
276
|
+
|
|
277
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
278
|
+
const curr = particle.trail[i];
|
|
279
|
+
const prev = particle.trail[i - 1];
|
|
280
|
+
|
|
281
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
282
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
283
|
+
|
|
284
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
285
|
+
|
|
286
|
+
const age = i / particle.trail.length;
|
|
287
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
288
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
289
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
290
|
+
|
|
291
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
292
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
293
|
+
const rgb = hslToRgb(hue, sat, lit);
|
|
294
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
295
|
+
|
|
296
|
+
this.segments.push({
|
|
297
|
+
x1: cx + p1.x * this.zoom,
|
|
298
|
+
y1: cy + p1.y * this.zoom,
|
|
299
|
+
x2: cx + p2.x * this.zoom,
|
|
300
|
+
y2: cy + p2.y * this.zoom,
|
|
301
|
+
r: rgb.r,
|
|
302
|
+
g: rgb.g,
|
|
303
|
+
b: rgb.b,
|
|
304
|
+
a: alpha,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return this.segments.length;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
renderCanvas2D(cx, cy) {
|
|
313
|
+
const { minHue, maxHue, maxSpeed, saturation, lightness, maxAlpha, hueShiftSpeed } =
|
|
314
|
+
CONFIG.visual;
|
|
315
|
+
const { intensityBoost, saturationBoost, alphaBoost } = CONFIG.blink;
|
|
316
|
+
const hueOffset = (this.time * hueShiftSpeed) % 360;
|
|
317
|
+
|
|
318
|
+
const ctx = this.ctx;
|
|
319
|
+
ctx.save();
|
|
320
|
+
ctx.globalCompositeOperation = "lighter";
|
|
321
|
+
ctx.lineCap = "round";
|
|
322
|
+
|
|
323
|
+
for (const particle of this.particles) {
|
|
324
|
+
if (particle.trail.length < 2) continue;
|
|
325
|
+
|
|
326
|
+
const blink = particle.blinkIntensity;
|
|
327
|
+
|
|
328
|
+
for (let i = 1; i < particle.trail.length; i++) {
|
|
329
|
+
const curr = particle.trail[i];
|
|
330
|
+
const prev = particle.trail[i - 1];
|
|
331
|
+
|
|
332
|
+
const p1 = this.camera.project(prev.x, prev.y, prev.z);
|
|
333
|
+
const p2 = this.camera.project(curr.x, curr.y, curr.z);
|
|
334
|
+
|
|
335
|
+
if (p1.scale <= 0 || p2.scale <= 0) continue;
|
|
336
|
+
|
|
337
|
+
const age = i / particle.trail.length;
|
|
338
|
+
const speedNorm = Math.min(curr.speed / maxSpeed, 1);
|
|
339
|
+
const baseHue = maxHue - speedNorm * (maxHue - minHue);
|
|
340
|
+
const hue = (baseHue + hueOffset) % 360;
|
|
341
|
+
|
|
342
|
+
const sat = Math.min(100, saturation * (1 + blink * (saturationBoost - 1)));
|
|
343
|
+
const lit = Math.min(100, lightness * (1 + blink * (intensityBoost - 1)));
|
|
344
|
+
const alpha = Math.min(1, (1 - age) * maxAlpha * (1 + blink * (alphaBoost - 1)));
|
|
345
|
+
|
|
346
|
+
ctx.strokeStyle = `hsla(${hue}, ${sat}%, ${lit}%, ${alpha})`;
|
|
347
|
+
ctx.lineWidth = 1;
|
|
348
|
+
|
|
349
|
+
ctx.beginPath();
|
|
350
|
+
ctx.moveTo(cx + p1.x * this.zoom, cy + p1.y * this.zoom);
|
|
351
|
+
ctx.lineTo(cx + p2.x * this.zoom, cy + p2.y * this.zoom);
|
|
352
|
+
ctx.stroke();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
ctx.restore();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
render() {
|
|
360
|
+
super.render();
|
|
361
|
+
if (!this.particles) return;
|
|
362
|
+
|
|
363
|
+
const cx = this.width / 2;
|
|
364
|
+
const cy = this.height / 2;
|
|
365
|
+
|
|
366
|
+
if (this.useWebGL && this.lineRenderer.isAvailable()) {
|
|
367
|
+
const segmentCount = this.collectSegments(cx, cy);
|
|
368
|
+
if (segmentCount > 0) {
|
|
369
|
+
this.lineRenderer.clear();
|
|
370
|
+
this.lineRenderer.updateLines(this.segments);
|
|
371
|
+
this.lineRenderer.render(segmentCount);
|
|
372
|
+
this.lineRenderer.compositeOnto(this.ctx, 0, 0);
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
this.renderCanvas2D(cx, cy);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
destroy() {
|
|
380
|
+
this.gesture?.destroy();
|
|
381
|
+
this.lineRenderer?.destroy();
|
|
382
|
+
super.destroy?.();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
387
|
+
// INITIALIZATION
|
|
388
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
window.addEventListener("load", () => {
|
|
391
|
+
const canvas = document.getElementById("game");
|
|
392
|
+
const demo = new ThomasDemo(canvas);
|
|
393
|
+
demo.start();
|
|
394
|
+
});
|