@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,275 @@
1
+ /**
2
+ * Study 001 - Grid Bounce
3
+ *
4
+ * Inspired by @okazz_ - Colored circles bouncing between grid points
5
+ * with motion trails.
6
+ *
7
+ * Features:
8
+ * - Grid of anchor dots using fluent API
9
+ * - Colored circles that tween between adjacent grid points
10
+ * - Motion trail effect via semi-transparent clear
11
+ * - Staggered movement timing
12
+ * - Fully responsive
13
+ */
14
+
15
+ import { gcanvas, Easing } from "/gcanvas.es.min.js";
16
+
17
+ // Configuration
18
+ const CONFIG = {
19
+ // Grid settings
20
+ gridSpacing: 40,
21
+ dotRadius: 2,
22
+ dotColor: "rgba(80, 80, 80, 0.6)",
23
+
24
+ // Circle settings
25
+ circleRadius: 8,
26
+ circleDensity: 0.35,
27
+
28
+ // Animation settings
29
+ moveDuration: 0.3,
30
+ moveInterval: { min: 0.5, max: 2.0 },
31
+
32
+ // Trail effect
33
+ trailAlpha: 0.12,
34
+
35
+ // Colors
36
+ colors: [
37
+ "#FF3B30", "#FF9500", "#FFCC00", "#34C759", "#00C7BE",
38
+ "#007AFF", "#5856D6", "#AF52DE", "#FF2D55", "#FFFFFF",
39
+ ],
40
+ };
41
+
42
+ /**
43
+ * Circle state for animation
44
+ */
45
+ class CircleState {
46
+ constructor(gridX, gridY, spacing) {
47
+ this.gridX = gridX;
48
+ this.gridY = gridY;
49
+ this.targetGridX = gridX;
50
+ this.targetGridY = gridY;
51
+ this.spacing = spacing;
52
+
53
+ this.x = gridX * spacing;
54
+ this.y = gridY * spacing;
55
+
56
+ this.isMoving = false;
57
+ this.moveProgress = 0;
58
+ this.startX = this.x;
59
+ this.startY = this.y;
60
+ this.endX = this.x;
61
+ this.endY = this.y;
62
+
63
+ this.nextMoveTime = Math.random() * CONFIG.moveInterval.max;
64
+ this.timeSinceLastMove = 0;
65
+ }
66
+
67
+ pickNewTarget(maxGridX, maxGridY) {
68
+ const directions = [
69
+ { dx: 0, dy: -1 },
70
+ { dx: 0, dy: 1 },
71
+ { dx: -1, dy: 0 },
72
+ { dx: 1, dy: 0 },
73
+ ];
74
+
75
+ const valid = directions.filter(d => {
76
+ const newX = this.gridX + d.dx;
77
+ const newY = this.gridY + d.dy;
78
+ return newX >= 0 && newX <= maxGridX && newY >= 0 && newY <= maxGridY;
79
+ });
80
+
81
+ if (valid.length === 0) return;
82
+
83
+ const dir = valid[Math.floor(Math.random() * valid.length)];
84
+ this.targetGridX = this.gridX + dir.dx;
85
+ this.targetGridY = this.gridY + dir.dy;
86
+
87
+ this.startX = this.x;
88
+ this.startY = this.y;
89
+ this.endX = this.targetGridX * this.spacing;
90
+ this.endY = this.targetGridY * this.spacing;
91
+ this.isMoving = true;
92
+ this.moveProgress = 0;
93
+ }
94
+
95
+ update(dt, maxGridX, maxGridY) {
96
+ if (this.isMoving) {
97
+ this.moveProgress += dt / CONFIG.moveDuration;
98
+
99
+ if (this.moveProgress >= 1) {
100
+ this.moveProgress = 1;
101
+ this.isMoving = false;
102
+ this.gridX = this.targetGridX;
103
+ this.gridY = this.targetGridY;
104
+ this.x = this.endX;
105
+ this.y = this.endY;
106
+ this.timeSinceLastMove = 0;
107
+ this.nextMoveTime = CONFIG.moveInterval.min +
108
+ Math.random() * (CONFIG.moveInterval.max - CONFIG.moveInterval.min);
109
+ } else {
110
+ const t = Easing.easeInOutCubic(this.moveProgress);
111
+ this.x = this.startX + (this.endX - this.startX) * t;
112
+ this.y = this.startY + (this.endY - this.startY) * t;
113
+ }
114
+ } else {
115
+ this.timeSinceLastMove += dt;
116
+ if (this.timeSinceLastMove >= this.nextMoveTime) {
117
+ this.pickNewTarget(maxGridX, maxGridY);
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // Initialize
124
+ window.addEventListener("load", () => {
125
+ const canvas = document.getElementById("game");
126
+ if (!canvas) return;
127
+
128
+ const game = gcanvas({ canvas, bg: "#000", fluid: true });
129
+ const scene = game.scene("main");
130
+ const gameInstance = game.game;
131
+
132
+ // Debounced resize handling
133
+ let resizeTimeout = null;
134
+ let needsRebuild = false;
135
+
136
+ const handleResize = () => {
137
+ clearTimeout(resizeTimeout);
138
+ resizeTimeout = setTimeout(() => {
139
+ // Clear canvas fully before rebuild
140
+ gameInstance.ctx.fillStyle = "#000";
141
+ gameInstance.ctx.fillRect(0, 0, gameInstance.width, gameInstance.height);
142
+ needsRebuild = true;
143
+ }, 100);
144
+ };
145
+
146
+ window.addEventListener("resize", handleResize);
147
+
148
+ // State
149
+ let gridCols = 0;
150
+ let gridRows = 0;
151
+ let offsetX = 0;
152
+ let offsetY = 0;
153
+ let circles = [];
154
+
155
+ // Override clear for trail effect + direct dot rendering
156
+ gameInstance.clear = function () {
157
+ const ctx = this.ctx;
158
+
159
+ // Trail effect
160
+ ctx.fillStyle = `rgba(0, 0, 0, ${CONFIG.trailAlpha})`;
161
+ ctx.fillRect(0, 0, gameInstance.width, gameInstance.height);
162
+
163
+ // Draw grid dots directly in a single batched path (no GameObject overhead)
164
+ ctx.fillStyle = CONFIG.dotColor;
165
+ ctx.beginPath();
166
+ const spacing = CONFIG.gridSpacing;
167
+ const r = CONFIG.dotRadius;
168
+ const twoPi = Math.PI * 2;
169
+
170
+ for (let x = 0; x <= gridCols; x++) {
171
+ for (let y = 0; y <= gridRows; y++) {
172
+ const px = x * spacing + offsetX;
173
+ const py = y * spacing + offsetY;
174
+
175
+ ctx.moveTo(px + r, py);
176
+ ctx.arc(px, py, r, 0, twoPi);
177
+ }
178
+ }
179
+ ctx.fill();
180
+ };
181
+
182
+ /**
183
+ * Setup grid based on current size
184
+ */
185
+ function setupGrid() {
186
+ const spacing = CONFIG.gridSpacing;
187
+ const padding = spacing;
188
+ const w = gameInstance.width;
189
+ const h = gameInstance.height;
190
+
191
+ gridCols = Math.floor((w - padding * 2) / spacing);
192
+ gridRows = Math.floor((h - padding * 2) / spacing);
193
+ offsetX = (w - gridCols * spacing) / 2;
194
+ offsetY = (h - gridRows * spacing) / 2;
195
+
196
+ // Clear existing circles
197
+ circles = [];
198
+
199
+ // Create bouncing circles
200
+ const occupied = new Set();
201
+ const totalPositions = (gridCols + 1) * (gridRows + 1);
202
+ const numCircles = Math.floor(totalPositions * CONFIG.circleDensity);
203
+
204
+ for (let i = 0; i < numCircles; i++) {
205
+ let gx, gy, key;
206
+ let attempts = 0;
207
+
208
+ do {
209
+ gx = Math.floor(Math.random() * (gridCols + 1));
210
+ gy = Math.floor(Math.random() * (gridRows + 1));
211
+ key = `${gx},${gy}`;
212
+ attempts++;
213
+ } while (occupied.has(key) && attempts < 100);
214
+
215
+ if (attempts >= 100) continue;
216
+ occupied.add(key);
217
+
218
+ const color = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
219
+ const name = `circle_${i}`;
220
+
221
+ const state = new CircleState(gx, gy, spacing);
222
+
223
+ scene.go({ x: state.x + offsetX, y: state.y + offsetY, name })
224
+ .circle({ radius: CONFIG.circleRadius, fill: color });
225
+
226
+ circles.push({ name, state, color });
227
+ }
228
+ }
229
+
230
+ // Initial setup
231
+ setupGrid();
232
+
233
+ // Update loop
234
+ game.on("update", (dt, ctx) => {
235
+ // Check for resize (debounced flag)
236
+ if (needsRebuild) {
237
+ needsRebuild = false;
238
+
239
+ // Clear scene completely and rebuild
240
+ scene.sceneInstance.clear();
241
+ // Clear refs
242
+ for (const c of circles) {
243
+ delete ctx.refs[c.name];
244
+ }
245
+
246
+ setupGrid();
247
+ }
248
+
249
+ // Update circle positions
250
+ for (const c of circles) {
251
+ c.state.update(dt, gridCols, gridRows);
252
+
253
+ const go = ctx.refs[c.name];
254
+ if (go) {
255
+ go.x = c.state.x + offsetX;
256
+ go.y = c.state.y + offsetY;
257
+ }
258
+ }
259
+ });
260
+
261
+ // Click to randomize colors
262
+ game.on("click", (ctx) => {
263
+ for (const c of circles) {
264
+ const newColor = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
265
+ c.color = newColor;
266
+
267
+ const go = ctx.refs[c.name];
268
+ if (go && go._fluentShape) {
269
+ go._fluentShape.color = newColor;
270
+ }
271
+ }
272
+ });
273
+
274
+ game.start();
275
+ });
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Study 002 - Hex Grid
3
+ *
4
+ * Inspired by @okazz_ - Triangular lattice with ring circles
5
+ *
6
+ * Features:
7
+ * - Triangular/hexagonal lattice (6 directions)
8
+ * - Ring circles with center dots
9
+ * - Visible grid lines
10
+ * - Light background
11
+ */
12
+
13
+ import { gcanvas, Easing } from "/gcanvas.es.min.js";
14
+
15
+ // Configuration
16
+ const CONFIG = {
17
+ // Grid settings
18
+ gridSpacing: 50,
19
+ dotRadius: 4,
20
+ dotColor: "#333",
21
+ lineColor: "rgba(0, 0, 0, 0.03)",
22
+ lineWidth: 1,
23
+
24
+ // Circle settings
25
+ circleRadius: 14,
26
+ circleDensity: 0.3,
27
+
28
+ // Animation settings
29
+ moveDuration: 0.25,
30
+ moveInterval: { min: 0.4, max: 1.5 },
31
+
32
+ // Trail effect
33
+ trailAlpha: 0.15,
34
+
35
+ // Background
36
+ bgColor: "#F5F5F0",
37
+
38
+ // Colors
39
+ colors: [
40
+ "#E63946", "#F4A261", "#E9C46A", "#2A9D8F", "#00B4D8",
41
+ "#0077B6", "#7209B7", "#F72585", "#4CC9F0", "#80ED99",
42
+ ],
43
+ };
44
+
45
+ // Triangular grid row height factor (sin 60°)
46
+ const ROW_HEIGHT = Math.sin(Math.PI / 3); // ~0.866
47
+
48
+ /**
49
+ * Get pixel position for a grid cell
50
+ */
51
+ function gridToPixel(col, row, spacing, offsetX, offsetY) {
52
+ const x = col * spacing + (row % 2) * (spacing / 2) + offsetX;
53
+ const y = row * spacing * ROW_HEIGHT + offsetY;
54
+ return { x, y };
55
+ }
56
+
57
+ /**
58
+ * Get valid neighbors for a triangular lattice cell
59
+ */
60
+ function getNeighbors(col, row, maxCol, maxRow) {
61
+ const neighbors = [];
62
+ const isOddRow = row % 2 === 1;
63
+
64
+ // Horizontal neighbors (always valid pattern)
65
+ const directions = [
66
+ { dc: -1, dr: 0 }, // left
67
+ { dc: 1, dr: 0 }, // right
68
+ ];
69
+
70
+ // Diagonal neighbors depend on row parity
71
+ if (isOddRow) {
72
+ directions.push(
73
+ { dc: 0, dr: -1 }, // up-left
74
+ { dc: 1, dr: -1 }, // up-right
75
+ { dc: 0, dr: 1 }, // down-left
76
+ { dc: 1, dr: 1 }, // down-right
77
+ );
78
+ } else {
79
+ directions.push(
80
+ { dc: -1, dr: -1 }, // up-left
81
+ { dc: 0, dr: -1 }, // up-right
82
+ { dc: -1, dr: 1 }, // down-left
83
+ { dc: 0, dr: 1 }, // down-right
84
+ );
85
+ }
86
+
87
+ for (const { dc, dr } of directions) {
88
+ const nc = col + dc;
89
+ const nr = row + dr;
90
+ if (nc >= 0 && nc <= maxCol && nr >= 0 && nr <= maxRow) {
91
+ neighbors.push({ col: nc, row: nr });
92
+ }
93
+ }
94
+
95
+ return neighbors;
96
+ }
97
+
98
+ /**
99
+ * Circle state for animation
100
+ */
101
+ class CircleState {
102
+ constructor(col, row, spacing) {
103
+ this.col = col;
104
+ this.row = row;
105
+ this.targetCol = col;
106
+ this.targetRow = row;
107
+ this.spacing = spacing;
108
+
109
+ this.x = 0;
110
+ this.y = 0;
111
+
112
+ this.isMoving = false;
113
+ this.moveProgress = 0;
114
+ this.startX = 0;
115
+ this.startY = 0;
116
+ this.endX = 0;
117
+ this.endY = 0;
118
+
119
+ this.nextMoveTime = Math.random() * CONFIG.moveInterval.max;
120
+ this.timeSinceLastMove = 0;
121
+ }
122
+
123
+ updatePosition(offsetX, offsetY) {
124
+ const pos = gridToPixel(this.col, this.row, this.spacing, offsetX, offsetY);
125
+ this.x = pos.x;
126
+ this.y = pos.y;
127
+ }
128
+
129
+ pickNewTarget(maxCol, maxRow, offsetX, offsetY) {
130
+ const neighbors = getNeighbors(this.col, this.row, maxCol, maxRow);
131
+ if (neighbors.length === 0) return;
132
+
133
+ const target = neighbors[Math.floor(Math.random() * neighbors.length)];
134
+ this.targetCol = target.col;
135
+ this.targetRow = target.row;
136
+
137
+ this.startX = this.x;
138
+ this.startY = this.y;
139
+
140
+ const endPos = gridToPixel(this.targetCol, this.targetRow, this.spacing, offsetX, offsetY);
141
+ this.endX = endPos.x;
142
+ this.endY = endPos.y;
143
+
144
+ this.isMoving = true;
145
+ this.moveProgress = 0;
146
+ }
147
+
148
+ update(dt, maxCol, maxRow, offsetX, offsetY) {
149
+ if (this.isMoving) {
150
+ this.moveProgress += dt / CONFIG.moveDuration;
151
+
152
+ if (this.moveProgress >= 1) {
153
+ this.moveProgress = 1;
154
+ this.isMoving = false;
155
+ this.col = this.targetCol;
156
+ this.row = this.targetRow;
157
+ this.x = this.endX;
158
+ this.y = this.endY;
159
+ this.timeSinceLastMove = 0;
160
+ this.nextMoveTime = CONFIG.moveInterval.min +
161
+ Math.random() * (CONFIG.moveInterval.max - CONFIG.moveInterval.min);
162
+ } else {
163
+ const t = Easing.easeInOutCubic(this.moveProgress);
164
+ this.x = this.startX + (this.endX - this.startX) * t;
165
+ this.y = this.startY + (this.endY - this.startY) * t;
166
+ }
167
+ } else {
168
+ this.timeSinceLastMove += dt;
169
+ if (this.timeSinceLastMove >= this.nextMoveTime) {
170
+ this.pickNewTarget(maxCol, maxRow, offsetX, offsetY);
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ // Initialize
177
+ window.addEventListener("load", () => {
178
+ const canvas = document.getElementById("game");
179
+ if (!canvas) return;
180
+
181
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
182
+ const scene = game.scene("main");
183
+ const gameInstance = game.game;
184
+
185
+ // Debounced resize handling
186
+ let resizeTimeout = null;
187
+ let needsRebuild = false;
188
+
189
+ const handleResize = () => {
190
+ clearTimeout(resizeTimeout);
191
+ resizeTimeout = setTimeout(() => {
192
+ // Clear canvas fully before rebuild
193
+ gameInstance.ctx.fillStyle = CONFIG.bgColor;
194
+ gameInstance.ctx.fillRect(0, 0, gameInstance.width, gameInstance.height);
195
+ needsRebuild = true;
196
+ }, 100);
197
+ };
198
+
199
+ window.addEventListener("resize", handleResize);
200
+
201
+ // State
202
+ let gridCols = 0;
203
+ let gridRows = 0;
204
+ let offsetX = 0;
205
+ let offsetY = 0;
206
+ let circles = [];
207
+
208
+ // Override clear for trail effect + grid lines (layer 1)
209
+ gameInstance.clear = function () {
210
+ const ctx = this.ctx;
211
+
212
+ // Trail effect with light background
213
+ ctx.fillStyle = `rgba(245, 245, 240, ${CONFIG.trailAlpha})`;
214
+ ctx.fillRect(0, 0, gameInstance.width, gameInstance.height);
215
+
216
+ const spacing = CONFIG.gridSpacing;
217
+
218
+ // Draw grid lines (bottom layer)
219
+ ctx.strokeStyle = CONFIG.lineColor;
220
+ ctx.lineWidth = CONFIG.lineWidth;
221
+ ctx.beginPath();
222
+
223
+ for (let row = 0; row <= gridRows; row++) {
224
+ for (let col = 0; col <= gridCols; col++) {
225
+ const pos = gridToPixel(col, row, spacing, offsetX, offsetY);
226
+ const neighbors = getNeighbors(col, row, gridCols, gridRows);
227
+
228
+ // Only draw lines to neighbors with higher index to avoid duplicates
229
+ for (const n of neighbors) {
230
+ if (n.row > row || (n.row === row && n.col > col)) {
231
+ const nPos = gridToPixel(n.col, n.row, spacing, offsetX, offsetY);
232
+ ctx.moveTo(pos.x, pos.y);
233
+ ctx.lineTo(nPos.x, nPos.y);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ ctx.stroke();
239
+ };
240
+
241
+ // Draw grid dots on TOP of everything (layer 3)
242
+ function drawGridDots(ctx) {
243
+ ctx.fillStyle = CONFIG.dotColor;
244
+ ctx.beginPath();
245
+ const spacing = CONFIG.gridSpacing;
246
+ const r = CONFIG.dotRadius;
247
+ const twoPi = Math.PI * 2;
248
+
249
+ for (let row = 0; row <= gridRows; row++) {
250
+ for (let col = 0; col <= gridCols; col++) {
251
+ const pos = gridToPixel(col, row, spacing, offsetX, offsetY);
252
+ ctx.moveTo(pos.x + r, pos.y);
253
+ ctx.arc(pos.x, pos.y, r, 0, twoPi);
254
+ }
255
+ }
256
+ ctx.fill();
257
+ }
258
+
259
+ // Override render to add dots on top after pipeline
260
+ const originalRender = gameInstance.render.bind(gameInstance);
261
+ gameInstance.render = function () {
262
+ originalRender();
263
+ drawGridDots(this.ctx);
264
+ };
265
+
266
+ /**
267
+ * Setup grid based on current size
268
+ */
269
+ function setupGrid() {
270
+ const spacing = CONFIG.gridSpacing;
271
+ const padding = spacing;
272
+ const w = gameInstance.width;
273
+ const h = gameInstance.height;
274
+
275
+ // Calculate grid dimensions
276
+ gridCols = Math.floor((w - padding * 2) / spacing);
277
+ gridRows = Math.floor((h - padding * 2) / (spacing * ROW_HEIGHT));
278
+
279
+ // Center the grid
280
+ const gridWidth = gridCols * spacing + spacing / 2; // Account for offset rows
281
+ const gridHeight = gridRows * spacing * ROW_HEIGHT;
282
+ offsetX = (w - gridWidth) / 2 + spacing / 4;
283
+ offsetY = (h - gridHeight) / 2;
284
+
285
+ // Clear existing circles
286
+ circles = [];
287
+
288
+ // Create bouncing circles
289
+ const occupied = new Set();
290
+ const totalPositions = (gridCols + 1) * (gridRows + 1);
291
+ const numCircles = Math.floor(totalPositions * CONFIG.circleDensity);
292
+
293
+ for (let i = 0; i < numCircles; i++) {
294
+ let col, row, key;
295
+ let attempts = 0;
296
+
297
+ do {
298
+ col = Math.floor(Math.random() * (gridCols + 1));
299
+ row = Math.floor(Math.random() * (gridRows + 1));
300
+ key = `${col},${row}`;
301
+ attempts++;
302
+ } while (occupied.has(key) && attempts < 100);
303
+
304
+ if (attempts >= 100) continue;
305
+ occupied.add(key);
306
+
307
+ const color = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
308
+ const name = `circle_${i}`;
309
+
310
+ const state = new CircleState(col, row, spacing);
311
+ state.updatePosition(offsetX, offsetY);
312
+
313
+ // Create filled circle (layer 2 - grid dots render on top)
314
+ scene.go({ x: state.x, y: state.y, name })
315
+ .circle({ radius: CONFIG.circleRadius, fill: color });
316
+
317
+ circles.push({ name, state, color });
318
+ }
319
+ }
320
+
321
+ // Initial setup
322
+ setupGrid();
323
+
324
+ // Update loop
325
+ game.on("update", (dt, ctx) => {
326
+ // Check for resize (debounced flag)
327
+ if (needsRebuild) {
328
+ needsRebuild = false;
329
+
330
+ // Clear scene completely and rebuild
331
+ scene.sceneInstance.clear();
332
+ // Clear refs
333
+ for (const c of circles) {
334
+ delete ctx.refs[c.name];
335
+ }
336
+
337
+ setupGrid();
338
+ }
339
+
340
+ // Update circle positions
341
+ for (const c of circles) {
342
+ c.state.update(dt, gridCols, gridRows, offsetX, offsetY);
343
+
344
+ const go = ctx.refs[c.name];
345
+ if (go) {
346
+ go.x = c.state.x;
347
+ go.y = c.state.y;
348
+ }
349
+ }
350
+ });
351
+
352
+ // Click to randomize colors
353
+ game.on("click", (ctx) => {
354
+ for (const c of circles) {
355
+ const newColor = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
356
+ c.color = newColor;
357
+
358
+ const go = ctx.refs[c.name];
359
+ if (go && go._fluentShape) {
360
+ go._fluentShape.color = newColor;
361
+ }
362
+ }
363
+ });
364
+
365
+ game.start();
366
+ });