@guinetik/gcanvas 1.0.4 → 1.0.5
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/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/coordinates.html +698 -0
- package/dist/cube3d.html +23 -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 +517 -0
- 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/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 +398 -0
- package/dist/isometric.html +34 -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/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -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/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 +863 -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/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/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -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/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/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/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/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
|
@@ -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
|
+
}
|
package/dist/js/tiles.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Game,
|
|
3
|
+
Scene,
|
|
4
|
+
TileLayout,
|
|
5
|
+
HorizontalLayout,
|
|
6
|
+
Rectangle,
|
|
7
|
+
ShapeGOFactory,
|
|
8
|
+
Button,
|
|
9
|
+
FPSCounter,
|
|
10
|
+
Painter,
|
|
11
|
+
Tween,
|
|
12
|
+
Easing,
|
|
13
|
+
Position,
|
|
14
|
+
} from "/gcanvas.es.min.js";
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
const CONFIG = {
|
|
18
|
+
tileSize: 50,
|
|
19
|
+
tileSpacing: 12,
|
|
20
|
+
tilePadding: 20,
|
|
21
|
+
initialTiles: 50,
|
|
22
|
+
maxColumns: 10,
|
|
23
|
+
uiHeight: 100, // Space reserved for UI at bottom
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class TileDemo extends Scene {
|
|
27
|
+
constructor(game, options = {}) {
|
|
28
|
+
super(game, options);
|
|
29
|
+
this.elapsedTime = this.lastChangeTime = 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
isMobile() {
|
|
33
|
+
return this.game.width < 600;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getResponsiveColumns() {
|
|
37
|
+
const availableWidth = this.game.width - CONFIG.tilePadding * 2;
|
|
38
|
+
const tileWithSpacing = CONFIG.tileSize + CONFIG.tileSpacing;
|
|
39
|
+
const columns = Math.floor(availableWidth / tileWithSpacing);
|
|
40
|
+
return Math.max(2, Math.min(CONFIG.maxColumns, columns));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getViewportDimensions() {
|
|
44
|
+
const margin = this.isMobile() ? 20 : 40;
|
|
45
|
+
return {
|
|
46
|
+
width: this.game.width - margin * 2,
|
|
47
|
+
height: this.game.height - CONFIG.uiHeight - margin,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
init() {
|
|
52
|
+
const game = this.game;
|
|
53
|
+
const viewport = this.getViewportDimensions();
|
|
54
|
+
const columns = this.getResponsiveColumns();
|
|
55
|
+
|
|
56
|
+
// 1) Create a scrollable tile grid anchored at center
|
|
57
|
+
this.grid = new TileLayout(game, {
|
|
58
|
+
anchor: Position.CENTER,
|
|
59
|
+
anchorOffsetY: -CONFIG.uiHeight / 2 + 20,
|
|
60
|
+
columns: columns,
|
|
61
|
+
spacing: CONFIG.tileSpacing,
|
|
62
|
+
padding: CONFIG.tilePadding,
|
|
63
|
+
autoSize: true,
|
|
64
|
+
debug: true,
|
|
65
|
+
// Enable scrolling
|
|
66
|
+
scrollable: true,
|
|
67
|
+
viewportWidth: viewport.width,
|
|
68
|
+
viewportHeight: viewport.height,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Add initial tiles
|
|
72
|
+
for (let i = 0; i < CONFIG.initialTiles; i++) {
|
|
73
|
+
this.addTile();
|
|
74
|
+
}
|
|
75
|
+
this.add(this.grid);
|
|
76
|
+
|
|
77
|
+
// 2) Create a HorizontalLayout at bottom-center for UI buttons
|
|
78
|
+
this.createUI();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createUI() {
|
|
82
|
+
const game = this.game;
|
|
83
|
+
const isMobile = this.isMobile();
|
|
84
|
+
const buttonWidth = isMobile ? 70 : 90;
|
|
85
|
+
|
|
86
|
+
this.bottomUI = new HorizontalLayout(game, {
|
|
87
|
+
anchor: Position.BOTTOM_CENTER,
|
|
88
|
+
anchorOffsetY: -15,
|
|
89
|
+
spacing: isMobile ? 5 : 10,
|
|
90
|
+
padding: 10,
|
|
91
|
+
debug: false,
|
|
92
|
+
align: "center",
|
|
93
|
+
});
|
|
94
|
+
this.add(this.bottomUI);
|
|
95
|
+
|
|
96
|
+
// "Add Tile" button
|
|
97
|
+
this.bottomUI.add(new Button(game, {
|
|
98
|
+
text: isMobile ? "+" : "Add",
|
|
99
|
+
width: isMobile ? 40 : buttonWidth,
|
|
100
|
+
onClick: () => this.addTile(),
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
// "Remove Tile" button
|
|
104
|
+
this.bottomUI.add(new Button(game, {
|
|
105
|
+
text: isMobile ? "-" : "Remove",
|
|
106
|
+
width: isMobile ? 40 : buttonWidth,
|
|
107
|
+
onClick: () => this.removeTile(),
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// "Add Column" button
|
|
111
|
+
this.bottomUI.add(new Button(game, {
|
|
112
|
+
text: isMobile ? "+Col" : "+ Column",
|
|
113
|
+
width: isMobile ? 50 : buttonWidth,
|
|
114
|
+
onClick: () => {
|
|
115
|
+
const maxColumns = this.getResponsiveColumns();
|
|
116
|
+
if (this.grid.columns < maxColumns) {
|
|
117
|
+
this.grid.columns++;
|
|
118
|
+
this.grid.markBoundsDirty();
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// "Remove Column" button
|
|
124
|
+
this.bottomUI.add(new Button(game, {
|
|
125
|
+
text: isMobile ? "-Col" : "- Column",
|
|
126
|
+
width: isMobile ? 50 : buttonWidth,
|
|
127
|
+
onClick: () => {
|
|
128
|
+
this.grid.columns = Math.max(1, this.grid.columns - 1);
|
|
129
|
+
this.grid.markBoundsDirty();
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
// Add 10 tiles button (for quickly testing scroll)
|
|
134
|
+
this.bottomUI.add(new Button(game, {
|
|
135
|
+
text: isMobile ? "+10" : "Add 10",
|
|
136
|
+
width: isMobile ? 45 : buttonWidth,
|
|
137
|
+
onClick: () => {
|
|
138
|
+
for (let i = 0; i < 10; i++) this.addTile();
|
|
139
|
+
},
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
addTile() {
|
|
144
|
+
const rect = new Rectangle({
|
|
145
|
+
width: CONFIG.tileSize,
|
|
146
|
+
height: CONFIG.tileSize,
|
|
147
|
+
color: Painter.colors.randomColorHSL(),
|
|
148
|
+
strokeColor: "white",
|
|
149
|
+
});
|
|
150
|
+
const tileGO = ShapeGOFactory.create(this.game, rect);
|
|
151
|
+
this.grid.add(tileGO);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
removeTile() {
|
|
155
|
+
if (this.grid.children.length > 0) {
|
|
156
|
+
const last = this.grid.children[this.grid.children.length - 1];
|
|
157
|
+
this.grid.remove(last);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
onResize() {
|
|
162
|
+
if (!this.grid) return;
|
|
163
|
+
|
|
164
|
+
// Update columns based on new width
|
|
165
|
+
const newColumns = this.getResponsiveColumns();
|
|
166
|
+
if (this.grid.columns !== newColumns) {
|
|
167
|
+
this.grid.columns = newColumns;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update viewport for scrolling
|
|
171
|
+
const viewport = this.getViewportDimensions();
|
|
172
|
+
this.grid._viewportWidth = viewport.width;
|
|
173
|
+
this.grid._viewportHeight = viewport.height;
|
|
174
|
+
|
|
175
|
+
this.grid.markBoundsDirty();
|
|
176
|
+
if (this.bottomUI) {
|
|
177
|
+
this.bottomUI.markBoundsDirty();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
update(dt) {
|
|
182
|
+
super.update(dt);
|
|
183
|
+
this.elapsedTime += dt;
|
|
184
|
+
|
|
185
|
+
// Only process tiles every N seconds to reduce frequency
|
|
186
|
+
if (this.elapsedTime - this.lastChangeTime > 0.1) {
|
|
187
|
+
this.lastChangeTime = this.elapsedTime;
|
|
188
|
+
|
|
189
|
+
// Random number of tiles to change (between 1 and 10% of total tiles)
|
|
190
|
+
const tilesToChange = Math.max(
|
|
191
|
+
1,
|
|
192
|
+
Math.floor(this.grid.children.length * (0.1 + Math.random() * 0.1))
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Filter out tiles that are already tweening
|
|
196
|
+
const availableTiles = this.grid.children.filter(
|
|
197
|
+
(tile) => !tile.isTweening
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Shuffle and pick random tiles
|
|
201
|
+
const shuffled = [...availableTiles].sort(() => 0.5 - Math.random());
|
|
202
|
+
const selectedTiles = shuffled.slice(
|
|
203
|
+
0,
|
|
204
|
+
Math.min(tilesToChange, availableTiles.length)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Start tweening on selected tiles
|
|
208
|
+
for (const tile of selectedTiles) {
|
|
209
|
+
tile.isTweening = true;
|
|
210
|
+
|
|
211
|
+
// Store RGB values directly instead of CSS string to avoid parsing issues
|
|
212
|
+
const rgb = tile.shape.color ?
|
|
213
|
+
Painter.colors.parseColorString(tile.shape.color) :
|
|
214
|
+
[255, 255, 255]; // Default to white if color is undefined
|
|
215
|
+
|
|
216
|
+
tile.startRGB = rgb;
|
|
217
|
+
|
|
218
|
+
// Generate vibrant random color in RGB directly
|
|
219
|
+
const hue = Math.floor(Math.random() * 360);
|
|
220
|
+
const saturation = 80 + Math.floor(Math.random() * 20); // 80-100%
|
|
221
|
+
const lightness = 50 + Math.floor(Math.random() * 30); // 50-80%
|
|
222
|
+
const targetRGB = Painter.colors.hslToRgb(hue, saturation, lightness);
|
|
223
|
+
|
|
224
|
+
tile.targetRGB = targetRGB;
|
|
225
|
+
tile.tweenElapsed = 0;
|
|
226
|
+
tile.tweenDuration = 0.5 + Math.random() * 0.5; // Random duration between 0.5-1.0 seconds
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Update all tweening tiles
|
|
231
|
+
for (const tile of this.grid.children) {
|
|
232
|
+
if (tile.isTweening) {
|
|
233
|
+
try {
|
|
234
|
+
// Update elapsed time
|
|
235
|
+
tile.tweenElapsed += dt;
|
|
236
|
+
|
|
237
|
+
// Calculate progress with proper easing
|
|
238
|
+
const progress = Math.min(tile.tweenElapsed / tile.tweenDuration, 1.0);
|
|
239
|
+
const easedProgress = Easing.easeInOutQuad(progress);
|
|
240
|
+
|
|
241
|
+
// Use the stored RGB values directly
|
|
242
|
+
if (tile.startRGB && tile.targetRGB) {
|
|
243
|
+
const newRGB = Tween.tweenColor(
|
|
244
|
+
tile.startRGB,
|
|
245
|
+
tile.targetRGB,
|
|
246
|
+
easedProgress
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Convert interpolated RGB values to CSS color string
|
|
250
|
+
tile.shape.color = Painter.colors.rgbArrayToCSS(newRGB);
|
|
251
|
+
|
|
252
|
+
// Check if tween is complete
|
|
253
|
+
if (progress >= 1) {
|
|
254
|
+
tile.isTweening = false;
|
|
255
|
+
tile.shape.color = Painter.colors.rgbArrayToCSS(tile.targetRGB);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// Color missing, reset the tweening state
|
|
259
|
+
console.warn("Missing color values for tweening");
|
|
260
|
+
tile.isTweening = false;
|
|
261
|
+
|
|
262
|
+
// Set a fallback color
|
|
263
|
+
const fallbackRGB = [
|
|
264
|
+
Math.floor(Math.random() * 255),
|
|
265
|
+
Math.floor(Math.random() * 255),
|
|
266
|
+
Math.floor(Math.random() * 255)
|
|
267
|
+
];
|
|
268
|
+
tile.shape.color = Painter.colors.rgbArrayToCSS(fallbackRGB);
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
// Handle any errors in the color transition
|
|
272
|
+
console.warn("Error during color tweening:", error);
|
|
273
|
+
tile.isTweening = false;
|
|
274
|
+
|
|
275
|
+
// Set a fallback color
|
|
276
|
+
const fallbackRGB = [
|
|
277
|
+
Math.floor(Math.random() * 255),
|
|
278
|
+
Math.floor(Math.random() * 255),
|
|
279
|
+
Math.floor(Math.random() * 255)
|
|
280
|
+
];
|
|
281
|
+
tile.shape.color = Painter.colors.rgbArrayToCSS(fallbackRGB);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//
|
|
289
|
+
export class MyGame extends Game {
|
|
290
|
+
constructor(canvas) {
|
|
291
|
+
super(canvas);
|
|
292
|
+
this.backgroundColor = "black";
|
|
293
|
+
this.enableFluidSize();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
init() {
|
|
297
|
+
super.init();
|
|
298
|
+
this.tileDemo = new TileDemo(this);
|
|
299
|
+
this.pipeline.add(this.tileDemo);
|
|
300
|
+
this.pipeline.add(
|
|
301
|
+
new FPSCounter(this, {
|
|
302
|
+
anchor: "bottom-right",
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
onResize() {
|
|
308
|
+
if (this.tileDemo) {
|
|
309
|
+
this.tileDemo.onResize();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Game,
|
|
3
|
+
Rectangle,
|
|
4
|
+
Group,
|
|
5
|
+
Scene,
|
|
6
|
+
ShapeGOFactory,
|
|
7
|
+
TextShape,
|
|
8
|
+
FPSCounter,
|
|
9
|
+
Tween,
|
|
10
|
+
Tweenetik,
|
|
11
|
+
Easing,
|
|
12
|
+
} from "/gcanvas.es.min.js";
|
|
13
|
+
export class TweenDemo extends Scene {
|
|
14
|
+
constructor(game, options = {}) {
|
|
15
|
+
super(game, options);
|
|
16
|
+
const bg = new Rectangle({ width: 100, height: 100, color: "#0f0" });
|
|
17
|
+
const frame = new Rectangle({ width: 80, height: 80, color: "#000" });
|
|
18
|
+
this.label = new TextShape("1%", {
|
|
19
|
+
font: "18px monospace",
|
|
20
|
+
color: "#F0F",
|
|
21
|
+
align: "center",
|
|
22
|
+
baseline: "middle",
|
|
23
|
+
});
|
|
24
|
+
const group = new Group();
|
|
25
|
+
group.add(bg);
|
|
26
|
+
group.add(frame);
|
|
27
|
+
group.add(this.label);
|
|
28
|
+
group.scaleX = group.scaleY = 2; // add individual scale to this group to test if the outer group scales it too
|
|
29
|
+
//
|
|
30
|
+
const wrapper = new Group();
|
|
31
|
+
wrapper.add(group);
|
|
32
|
+
//
|
|
33
|
+
this.box = ShapeGOFactory.create(game, wrapper);
|
|
34
|
+
this.box.anchor = "center";
|
|
35
|
+
this.add(this.box);
|
|
36
|
+
this.growing = false;
|
|
37
|
+
this.tween();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tween() {
|
|
41
|
+
this.growing = !this.growing;
|
|
42
|
+
const scale = this.growing ? 2 : 1;
|
|
43
|
+
const easing = this.growing ? Easing.easeOutElastic : Easing.easeInOutBack;
|
|
44
|
+
// Tweenetik is a simple tweening library that works with any object
|
|
45
|
+
Tweenetik.to(
|
|
46
|
+
this.box, // an object or sprite
|
|
47
|
+
{ scaleX: scale, scaleY: scale }, // the properties & end-values
|
|
48
|
+
2.0, // duration in seconds
|
|
49
|
+
easing, // easing function
|
|
50
|
+
{
|
|
51
|
+
delay: 0.3,
|
|
52
|
+
onComplete: () => this.tween(), // restart the tween
|
|
53
|
+
onUpdate: () => this.updateText(),
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updateText() {
|
|
59
|
+
this.label.text = `${Math.round(this.box.scaleX * 100)}%`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//
|
|
63
|
+
export class MyGame extends Game {
|
|
64
|
+
constructor(canvas) {
|
|
65
|
+
super(canvas);
|
|
66
|
+
this.backgroundColor = "black";
|
|
67
|
+
this.enableFluidSize();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
init() {
|
|
71
|
+
super.init();
|
|
72
|
+
this.pipeline.add(new TweenDemo(this, {debug: true, anchor: "center"}));
|
|
73
|
+
this.pipeline.add(
|
|
74
|
+
new FPSCounter(this, {
|
|
75
|
+
anchor: "bottom-right",
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|