@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.
Files changed (193) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/animations.html +31 -0
  3. package/dist/basic.html +38 -0
  4. package/dist/baskara.html +31 -0
  5. package/dist/bezier.html +35 -0
  6. package/dist/beziersignature.html +29 -0
  7. package/dist/blackhole.html +28 -0
  8. package/dist/blob.html +35 -0
  9. package/dist/coordinates.html +698 -0
  10. package/dist/cube3d.html +23 -0
  11. package/dist/demos.css +303 -0
  12. package/dist/dino.html +42 -0
  13. package/dist/easing.html +28 -0
  14. package/dist/events.html +195 -0
  15. package/dist/fluent.html +647 -0
  16. package/dist/fluid-simple.html +22 -0
  17. package/dist/fluid.html +37 -0
  18. package/dist/fractals.html +36 -0
  19. package/dist/gameobjects.html +626 -0
  20. package/dist/gcanvas.es.js +517 -0
  21. package/dist/gcanvas.es.min.js +1 -1
  22. package/dist/gcanvas.umd.js +1 -1
  23. package/dist/gcanvas.umd.min.js +1 -1
  24. package/dist/genart.html +26 -0
  25. package/dist/gendream.html +26 -0
  26. package/dist/group.html +36 -0
  27. package/dist/home.html +587 -0
  28. package/dist/hyperbolic001.html +23 -0
  29. package/dist/hyperbolic002.html +23 -0
  30. package/dist/hyperbolic003.html +23 -0
  31. package/dist/hyperbolic004.html +23 -0
  32. package/dist/hyperbolic005.html +22 -0
  33. package/dist/index.html +398 -0
  34. package/dist/isometric.html +34 -0
  35. package/dist/js/animations.js +452 -0
  36. package/dist/js/basic.js +204 -0
  37. package/dist/js/baskara.js +751 -0
  38. package/dist/js/bezier.js +692 -0
  39. package/dist/js/beziersignature.js +241 -0
  40. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  41. package/dist/js/blackhole/blackhole.obj.js +318 -0
  42. package/dist/js/blackhole/index.js +409 -0
  43. package/dist/js/blackhole/particle.js +56 -0
  44. package/dist/js/blackhole/starfield.obj.js +218 -0
  45. package/dist/js/blob.js +2276 -0
  46. package/dist/js/coordinates.js +840 -0
  47. package/dist/js/cube3d.js +789 -0
  48. package/dist/js/dino.js +1420 -0
  49. package/dist/js/easing.js +477 -0
  50. package/dist/js/fluent.js +183 -0
  51. package/dist/js/fluid-simple.js +253 -0
  52. package/dist/js/fluid.js +527 -0
  53. package/dist/js/fractals.js +932 -0
  54. package/dist/js/fractalworker.js +93 -0
  55. package/dist/js/gameobjects.js +176 -0
  56. package/dist/js/genart.js +268 -0
  57. package/dist/js/gendream.js +209 -0
  58. package/dist/js/group.js +140 -0
  59. package/dist/js/hyperbolic001.js +310 -0
  60. package/dist/js/hyperbolic002.js +388 -0
  61. package/dist/js/hyperbolic003.js +319 -0
  62. package/dist/js/hyperbolic004.js +345 -0
  63. package/dist/js/hyperbolic005.js +340 -0
  64. package/dist/js/info-toggle.js +25 -0
  65. package/dist/js/isometric.js +863 -0
  66. package/dist/js/kerr.js +1547 -0
  67. package/dist/js/lavalamp.js +590 -0
  68. package/dist/js/layout.js +354 -0
  69. package/dist/js/mondrian.js +285 -0
  70. package/dist/js/opacity.js +275 -0
  71. package/dist/js/painter.js +484 -0
  72. package/dist/js/particles-showcase.js +514 -0
  73. package/dist/js/particles.js +299 -0
  74. package/dist/js/patterns.js +397 -0
  75. package/dist/js/penrose/artifact.js +69 -0
  76. package/dist/js/penrose/blackhole.js +121 -0
  77. package/dist/js/penrose/constants.js +73 -0
  78. package/dist/js/penrose/game.js +943 -0
  79. package/dist/js/penrose/lore.js +278 -0
  80. package/dist/js/penrose/penrosescene.js +892 -0
  81. package/dist/js/penrose/ship.js +216 -0
  82. package/dist/js/penrose/sounds.js +211 -0
  83. package/dist/js/penrose/voidparticle.js +55 -0
  84. package/dist/js/penrose/voidscene.js +258 -0
  85. package/dist/js/penrose/voidship.js +144 -0
  86. package/dist/js/penrose/wormhole.js +46 -0
  87. package/dist/js/pipeline.js +555 -0
  88. package/dist/js/plane3d.js +256 -0
  89. package/dist/js/platformer.js +1579 -0
  90. package/dist/js/scene.js +304 -0
  91. package/dist/js/scenes.js +320 -0
  92. package/dist/js/schrodinger.js +410 -0
  93. package/dist/js/schwarzschild.js +1015 -0
  94. package/dist/js/shapes.js +628 -0
  95. package/dist/js/space/alien.js +171 -0
  96. package/dist/js/space/boom.js +98 -0
  97. package/dist/js/space/boss.js +353 -0
  98. package/dist/js/space/buff.js +73 -0
  99. package/dist/js/space/bullet.js +102 -0
  100. package/dist/js/space/constants.js +85 -0
  101. package/dist/js/space/game.js +1884 -0
  102. package/dist/js/space/hud.js +112 -0
  103. package/dist/js/space/laserbeam.js +179 -0
  104. package/dist/js/space/lightning.js +277 -0
  105. package/dist/js/space/minion.js +192 -0
  106. package/dist/js/space/missile.js +212 -0
  107. package/dist/js/space/player.js +430 -0
  108. package/dist/js/space/powerup.js +90 -0
  109. package/dist/js/space/starfield.js +58 -0
  110. package/dist/js/space/starpower.js +90 -0
  111. package/dist/js/spacetime.js +559 -0
  112. package/dist/js/sphere3d.js +229 -0
  113. package/dist/js/sprite.js +473 -0
  114. package/dist/js/starfaux/config.js +118 -0
  115. package/dist/js/starfaux/enemy.js +353 -0
  116. package/dist/js/starfaux/hud.js +78 -0
  117. package/dist/js/starfaux/index.js +482 -0
  118. package/dist/js/starfaux/laser.js +182 -0
  119. package/dist/js/starfaux/player.js +468 -0
  120. package/dist/js/starfaux/terrain.js +560 -0
  121. package/dist/js/study001.js +275 -0
  122. package/dist/js/study002.js +366 -0
  123. package/dist/js/study003.js +331 -0
  124. package/dist/js/study004.js +389 -0
  125. package/dist/js/study005.js +209 -0
  126. package/dist/js/study006.js +194 -0
  127. package/dist/js/study007.js +192 -0
  128. package/dist/js/study008.js +413 -0
  129. package/dist/js/svgtween.js +204 -0
  130. package/dist/js/tde/accretiondisk.js +471 -0
  131. package/dist/js/tde/blackhole.js +219 -0
  132. package/dist/js/tde/blackholescene.js +209 -0
  133. package/dist/js/tde/config.js +59 -0
  134. package/dist/js/tde/index.js +820 -0
  135. package/dist/js/tde/jets.js +290 -0
  136. package/dist/js/tde/lensedstarfield.js +154 -0
  137. package/dist/js/tde/tdestar.js +297 -0
  138. package/dist/js/tde/tidalstream.js +372 -0
  139. package/dist/js/tde_old/blackhole.obj.js +354 -0
  140. package/dist/js/tde_old/debris.obj.js +791 -0
  141. package/dist/js/tde_old/flare.obj.js +239 -0
  142. package/dist/js/tde_old/index.js +448 -0
  143. package/dist/js/tde_old/star.obj.js +812 -0
  144. package/dist/js/tetris/config.js +157 -0
  145. package/dist/js/tetris/grid.js +286 -0
  146. package/dist/js/tetris/index.js +1195 -0
  147. package/dist/js/tetris/renderer.js +634 -0
  148. package/dist/js/tetris/tetrominos.js +280 -0
  149. package/dist/js/tiles.js +312 -0
  150. package/dist/js/tweendemo.js +79 -0
  151. package/dist/js/visibility.js +102 -0
  152. package/dist/kerr.html +28 -0
  153. package/dist/lavalamp.html +27 -0
  154. package/dist/layouts.html +37 -0
  155. package/dist/logo.svg +4 -0
  156. package/dist/loop.html +84 -0
  157. package/dist/mondrian.html +32 -0
  158. package/dist/og_image.png +0 -0
  159. package/dist/opacity.html +36 -0
  160. package/dist/painter.html +39 -0
  161. package/dist/particles-showcase.html +28 -0
  162. package/dist/particles.html +24 -0
  163. package/dist/patterns.html +33 -0
  164. package/dist/penrose-game.html +31 -0
  165. package/dist/pipeline.html +737 -0
  166. package/dist/plane3d.html +24 -0
  167. package/dist/platformer.html +43 -0
  168. package/dist/scene.html +33 -0
  169. package/dist/scenes.html +96 -0
  170. package/dist/schrodinger.html +27 -0
  171. package/dist/schwarzschild.html +27 -0
  172. package/dist/shapes.html +16 -0
  173. package/dist/space.html +85 -0
  174. package/dist/spacetime.html +27 -0
  175. package/dist/sphere3d.html +24 -0
  176. package/dist/sprite.html +18 -0
  177. package/dist/starfaux.html +22 -0
  178. package/dist/study001.html +23 -0
  179. package/dist/study002.html +23 -0
  180. package/dist/study003.html +23 -0
  181. package/dist/study004.html +23 -0
  182. package/dist/study005.html +22 -0
  183. package/dist/study006.html +24 -0
  184. package/dist/study007.html +24 -0
  185. package/dist/study008.html +22 -0
  186. package/dist/svgtween.html +29 -0
  187. package/dist/tde.html +28 -0
  188. package/dist/tetris3d.html +25 -0
  189. package/dist/tiles.html +28 -0
  190. package/dist/transforms.html +400 -0
  191. package/dist/tween.html +45 -0
  192. package/dist/visibility.html +33 -0
  193. 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
+ }
@@ -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
+ }