@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,331 @@
1
+ /**
2
+ * Study 003 - Circuit
3
+ *
4
+ * Generative circuit patterns with traversing dots.
5
+ *
6
+ * Features:
7
+ * - Random maze-like circuit connections
8
+ * - Ring nodes at grid intersections
9
+ * - White dots that traverse the circuit paths
10
+ * - Click to regenerate
11
+ */
12
+
13
+ import { gcanvas, Easing } from "/gcanvas.es.min.js";
14
+
15
+ // Configuration
16
+ const CONFIG = {
17
+ // Grid settings
18
+ gridSpacing: 40,
19
+ nodeRadius: 8,
20
+ nodeLineWidth: 2.5,
21
+ nodeColor: "rgba(255, 255, 255, 0.5)",
22
+ nodeFill: "#000",
23
+
24
+ // Path settings
25
+ pathWidth: 1.5,
26
+ pathColor: "rgba(255, 255, 255, 0.4)",
27
+ connectionProbability: 0.5,
28
+
29
+ // Traveler settings
30
+ travelerRadius: 4,
31
+ travelerColor: "#fff",
32
+ travelerCount: 0.25, // Percentage of connected nodes with travelers
33
+ travelerSpeedMin: 30, // Pixels per second
34
+ travelerSpeedMax: 120,
35
+
36
+ // Background
37
+ bgColor: "#000",
38
+ };
39
+
40
+ /**
41
+ * Traveler - moves along circuit paths
42
+ */
43
+ class Traveler {
44
+ constructor(startNode, graph, spacing) {
45
+ this.graph = graph;
46
+ this.spacing = spacing;
47
+ this.currentNode = startNode;
48
+ this.targetNode = null;
49
+ this.x = startNode.x;
50
+ this.y = startNode.y;
51
+ this.progress = 0;
52
+ this.isMoving = false;
53
+ this.startX = this.x;
54
+ this.startY = this.y;
55
+ this.endX = this.x;
56
+ this.endY = this.y;
57
+ this.waitTime = Math.random() * 0.5;
58
+ // Random speed for this traveler
59
+ this.speed = CONFIG.travelerSpeedMin +
60
+ Math.random() * (CONFIG.travelerSpeedMax - CONFIG.travelerSpeedMin);
61
+ }
62
+
63
+ pickNextTarget() {
64
+ const neighbors = this.graph.getConnections(this.currentNode.col, this.currentNode.row);
65
+ if (neighbors.length === 0) return false;
66
+
67
+ // Pick random connected neighbor
68
+ const next = neighbors[Math.floor(Math.random() * neighbors.length)];
69
+ this.targetNode = this.graph.getNode(next.col, next.row);
70
+
71
+ if (!this.targetNode) return false;
72
+
73
+ this.startX = this.x;
74
+ this.startY = this.y;
75
+ this.endX = this.targetNode.x;
76
+ this.endY = this.targetNode.y;
77
+ this.progress = 0;
78
+ this.isMoving = true;
79
+ return true;
80
+ }
81
+
82
+ update(dt) {
83
+ if (this.isMoving) {
84
+ const distance = Math.sqrt(
85
+ Math.pow(this.endX - this.startX, 2) + Math.pow(this.endY - this.startY, 2)
86
+ );
87
+ const duration = distance / this.speed;
88
+ this.progress += dt / duration;
89
+
90
+ if (this.progress >= 1) {
91
+ this.progress = 1;
92
+ this.isMoving = false;
93
+ this.x = this.endX;
94
+ this.y = this.endY;
95
+ this.currentNode = this.targetNode;
96
+ this.waitTime = 0.1 + Math.random() * 0.3;
97
+ } else {
98
+ const t = Easing.easeInOutQuad(this.progress);
99
+ this.x = this.startX + (this.endX - this.startX) * t;
100
+ this.y = this.startY + (this.endY - this.startY) * t;
101
+ }
102
+ } else {
103
+ this.waitTime -= dt;
104
+ if (this.waitTime <= 0) {
105
+ this.pickNextTarget();
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Circuit Graph - manages nodes and connections
113
+ */
114
+ class CircuitGraph {
115
+ constructor() {
116
+ this.nodes = new Map();
117
+ this.connections = new Map();
118
+ }
119
+
120
+ clear() {
121
+ this.nodes.clear();
122
+ this.connections.clear();
123
+ }
124
+
125
+ addNode(col, row, x, y) {
126
+ const key = `${col},${row}`;
127
+ this.nodes.set(key, { col, row, x, y });
128
+ }
129
+
130
+ getNode(col, row) {
131
+ return this.nodes.get(`${col},${row}`);
132
+ }
133
+
134
+ addConnection(col1, row1, col2, row2) {
135
+ const key1 = `${col1},${row1}`;
136
+ const key2 = `${col2},${row2}`;
137
+
138
+ if (!this.connections.has(key1)) {
139
+ this.connections.set(key1, []);
140
+ }
141
+ if (!this.connections.has(key2)) {
142
+ this.connections.set(key2, []);
143
+ }
144
+
145
+ // Add bidirectional connection
146
+ this.connections.get(key1).push({ col: col2, row: row2 });
147
+ this.connections.get(key2).push({ col: col1, row: row1 });
148
+ }
149
+
150
+ getConnections(col, row) {
151
+ return this.connections.get(`${col},${row}`) || [];
152
+ }
153
+
154
+ hasConnection(col1, row1, col2, row2) {
155
+ const conns = this.getConnections(col1, row1);
156
+ return conns.some(c => c.col === col2 && c.row === row2);
157
+ }
158
+ }
159
+
160
+ // Initialize
161
+ window.addEventListener("load", () => {
162
+ const canvas = document.getElementById("game");
163
+ if (!canvas) return;
164
+
165
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
166
+ const scene = game.scene("main");
167
+ const gameInstance = game.game;
168
+
169
+ // State
170
+ let gridCols = 0;
171
+ let gridRows = 0;
172
+ let offsetX = 0;
173
+ let offsetY = 0;
174
+ let graph = new CircuitGraph();
175
+ let travelers = [];
176
+
177
+ // Debounced resize
178
+ let resizeTimeout = null;
179
+ let needsRebuild = false;
180
+
181
+ const handleResize = () => {
182
+ clearTimeout(resizeTimeout);
183
+ resizeTimeout = setTimeout(() => {
184
+ gameInstance.ctx.fillStyle = CONFIG.bgColor;
185
+ gameInstance.ctx.fillRect(0, 0, gameInstance.width, gameInstance.height);
186
+ needsRebuild = true;
187
+ }, 100);
188
+ };
189
+
190
+ window.addEventListener("resize", handleResize);
191
+
192
+ /**
193
+ * Generate random circuit
194
+ */
195
+ function generateCircuit() {
196
+ const spacing = CONFIG.gridSpacing;
197
+ const padding = spacing * 1.5;
198
+ const w = gameInstance.width;
199
+ const h = gameInstance.height;
200
+
201
+ gridCols = Math.floor((w - padding * 2) / spacing);
202
+ gridRows = Math.floor((h - padding * 2) / spacing);
203
+ offsetX = (w - gridCols * spacing) / 2;
204
+ offsetY = (h - gridRows * spacing) / 2;
205
+
206
+ // Clear previous
207
+ graph.clear();
208
+ travelers = [];
209
+
210
+ // Create nodes
211
+ for (let row = 0; row <= gridRows; row++) {
212
+ for (let col = 0; col <= gridCols; col++) {
213
+ const x = col * spacing + offsetX;
214
+ const y = row * spacing + offsetY;
215
+ graph.addNode(col, row, x, y);
216
+ }
217
+ }
218
+
219
+ // Create random connections (only right and down to avoid duplicates)
220
+ for (let row = 0; row <= gridRows; row++) {
221
+ for (let col = 0; col <= gridCols; col++) {
222
+ // Connect right
223
+ if (col < gridCols && Math.random() < CONFIG.connectionProbability) {
224
+ graph.addConnection(col, row, col + 1, row);
225
+ }
226
+ // Connect down
227
+ if (row < gridRows && Math.random() < CONFIG.connectionProbability) {
228
+ graph.addConnection(col, row, col, row + 1);
229
+ }
230
+ }
231
+ }
232
+
233
+ // Create travelers on random connected nodes
234
+ const nodeList = Array.from(graph.nodes.values());
235
+ const connectedNodes = nodeList.filter(n => graph.getConnections(n.col, n.row).length > 0);
236
+ const numTravelers = Math.floor(connectedNodes.length * CONFIG.travelerCount);
237
+
238
+ for (let i = 0; i < numTravelers && connectedNodes.length > 0; i++) {
239
+ const idx = Math.floor(Math.random() * connectedNodes.length);
240
+ const node = connectedNodes.splice(idx, 1)[0];
241
+ travelers.push(new Traveler(node, graph, spacing));
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Draw the circuit (nodes and paths)
247
+ */
248
+ function drawCircuit(ctx) {
249
+ const spacing = CONFIG.gridSpacing;
250
+
251
+ // Draw connections first (under nodes)
252
+ ctx.strokeStyle = CONFIG.pathColor;
253
+ ctx.lineWidth = CONFIG.pathWidth;
254
+ ctx.lineCap = "round";
255
+
256
+ const drawnConnections = new Set();
257
+
258
+ for (const [key, node] of graph.nodes) {
259
+ const connections = graph.getConnections(node.col, node.row);
260
+
261
+ for (const conn of connections) {
262
+ // Avoid drawing same connection twice
263
+ const connKey = [key, `${conn.col},${conn.row}`].sort().join("-");
264
+ if (drawnConnections.has(connKey)) continue;
265
+ drawnConnections.add(connKey);
266
+
267
+ const targetNode = graph.getNode(conn.col, conn.row);
268
+ if (!targetNode) continue;
269
+
270
+ ctx.beginPath();
271
+ ctx.moveTo(node.x, node.y);
272
+ ctx.lineTo(targetNode.x, targetNode.y);
273
+ ctx.stroke();
274
+ }
275
+ }
276
+
277
+ // Draw nodes (only where connections exist)
278
+ ctx.fillStyle = CONFIG.nodeFill;
279
+ ctx.strokeStyle = CONFIG.nodeColor;
280
+ ctx.lineWidth = CONFIG.nodeLineWidth;
281
+
282
+ for (const [key, node] of graph.nodes) {
283
+ // Only draw nodes that have at least one connection
284
+ const connections = graph.getConnections(node.col, node.row);
285
+ if (connections.length === 0) continue;
286
+
287
+ ctx.beginPath();
288
+ ctx.arc(node.x, node.y, CONFIG.nodeRadius, 0, Math.PI * 2);
289
+ ctx.fill();
290
+ ctx.stroke();
291
+ }
292
+
293
+ // Draw travelers (on top of everything)
294
+ ctx.fillStyle = CONFIG.travelerColor;
295
+ for (const traveler of travelers) {
296
+ ctx.beginPath();
297
+ ctx.arc(traveler.x, traveler.y, CONFIG.travelerRadius, 0, Math.PI * 2);
298
+ ctx.fill();
299
+ }
300
+ }
301
+
302
+ // Generate initial circuit
303
+ generateCircuit();
304
+
305
+ // Custom render - no trail effect, just redraw
306
+ gameInstance.clear = function () {
307
+ this.ctx.fillStyle = CONFIG.bgColor;
308
+ this.ctx.fillRect(0, 0, this.width, this.height);
309
+ drawCircuit(this.ctx);
310
+ };
311
+
312
+ // Update loop
313
+ game.on("update", (dt, ctx) => {
314
+ if (needsRebuild) {
315
+ needsRebuild = false;
316
+ generateCircuit();
317
+ }
318
+
319
+ // Update travelers
320
+ for (const traveler of travelers) {
321
+ traveler.update(dt);
322
+ }
323
+ });
324
+
325
+ // Click to regenerate
326
+ game.on("click", () => {
327
+ generateCircuit();
328
+ });
329
+
330
+ game.start();
331
+ });
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Study 004 - Hex Bloom
3
+ *
4
+ * Radial hexagon explosion with Tweenetik animations.
5
+ * Inspired by @okazz_
6
+ *
7
+ * Features:
8
+ * - Hexagons bloom radially from center
9
+ * - Color transition: black → colorful → white
10
+ * - Tweenetik-powered staggered animations
11
+ * - Click to trigger new bloom
12
+ */
13
+
14
+ import { gcanvas, Tweenetik, Easing } from "/gcanvas.es.min.js";
15
+
16
+ // Configuration
17
+ const CONFIG = {
18
+ // Grid settings
19
+ hexRadius: 25,
20
+ hexSpacing: 2,
21
+
22
+ // Animation settings
23
+ bloomDuration: 0.8,
24
+ staggerDelay: 0.03, // Delay per ring
25
+ scaleFrom: 0,
26
+ scaleTo: 1,
27
+
28
+ // Colors
29
+ colors: [
30
+ "#FF3B30", "#FF9500", "#FFCC00", "#34C759", "#00C7BE",
31
+ "#007AFF", "#5856D6", "#AF52DE", "#FF2D55", "#FF6B6B",
32
+ "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD",
33
+ ],
34
+
35
+ // Background
36
+ bgColor: "#000",
37
+ bloomColor: "#fff",
38
+ };
39
+
40
+ // Hexagon math
41
+ const HEX_ANGLE = Math.PI / 3;
42
+ const HEX_HEIGHT_RATIO = Math.sqrt(3);
43
+
44
+ /**
45
+ * Calculate hexagon grid position (axial coordinates to pixel)
46
+ */
47
+ function hexToPixel(q, r, size, centerX, centerY) {
48
+ const x = size * (3 / 2 * q);
49
+ const y = size * (HEX_HEIGHT_RATIO / 2 * q + HEX_HEIGHT_RATIO * r);
50
+ return { x: centerX + x, y: centerY + y };
51
+ }
52
+
53
+ /**
54
+ * Draw a hexagon
55
+ */
56
+ function drawHexagon(ctx, x, y, radius, fillColor, strokeColor, lineWidth) {
57
+ ctx.beginPath();
58
+ for (let i = 0; i < 6; i++) {
59
+ const angle = HEX_ANGLE * i - Math.PI / 6;
60
+ const hx = x + radius * Math.cos(angle);
61
+ const hy = y + radius * Math.sin(angle);
62
+ if (i === 0) {
63
+ ctx.moveTo(hx, hy);
64
+ } else {
65
+ ctx.lineTo(hx, hy);
66
+ }
67
+ }
68
+ ctx.closePath();
69
+
70
+ if (fillColor) {
71
+ ctx.fillStyle = fillColor;
72
+ ctx.fill();
73
+ }
74
+ if (strokeColor && lineWidth > 0) {
75
+ ctx.strokeStyle = strokeColor;
76
+ ctx.lineWidth = lineWidth;
77
+ ctx.stroke();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Draw a star/asterisk shape
83
+ */
84
+ function drawStar(ctx, x, y, radius, points, color, lineWidth) {
85
+ ctx.strokeStyle = color;
86
+ ctx.lineWidth = lineWidth;
87
+ ctx.lineCap = "round";
88
+
89
+ for (let i = 0; i < points; i++) {
90
+ const angle = (Math.PI * 2 / points) * i - Math.PI / 2;
91
+ ctx.beginPath();
92
+ ctx.moveTo(x, y);
93
+ ctx.lineTo(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius);
94
+ ctx.stroke();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Hex cell state for animation
100
+ */
101
+ class HexCell {
102
+ constructor(q, r, x, y, distFromCenter) {
103
+ this.q = q;
104
+ this.r = r;
105
+ this.x = x;
106
+ this.y = y;
107
+ this.distFromCenter = distFromCenter;
108
+
109
+ // Animation state
110
+ this.scale = 0;
111
+ this.opacity = 0;
112
+ this.colorPhase = 0; // 0 = colored, 1 = white
113
+ this.rotation = 0;
114
+
115
+ // Visual properties
116
+ this.color = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
117
+ this.isStar = Math.random() < 0.15; // 15% chance to be a star
118
+ this.starPoints = 4 + Math.floor(Math.random() * 4); // 4-7 points
119
+ }
120
+
121
+ draw(ctx, baseRadius) {
122
+ if (this.opacity <= 0 || this.scale <= 0) return;
123
+
124
+ const radius = baseRadius * this.scale;
125
+
126
+ ctx.save();
127
+ ctx.globalAlpha = this.opacity;
128
+ ctx.translate(this.x, this.y);
129
+ ctx.rotate(this.rotation);
130
+
131
+ // Interpolate color from colorful to white based on colorPhase
132
+ const r = Math.floor(this.hexToRgb(this.color).r + (255 - this.hexToRgb(this.color).r) * this.colorPhase);
133
+ const g = Math.floor(this.hexToRgb(this.color).g + (255 - this.hexToRgb(this.color).g) * this.colorPhase);
134
+ const b = Math.floor(this.hexToRgb(this.color).b + (255 - this.hexToRgb(this.color).b) * this.colorPhase);
135
+ const currentColor = `rgb(${r},${g},${b})`;
136
+
137
+ if (this.isStar) {
138
+ drawStar(ctx, 0, 0, radius, this.starPoints, currentColor, 2);
139
+ } else {
140
+ // Draw hexagon with stroke and optional fill
141
+ const fillAlpha = this.colorPhase;
142
+ const fillColor = fillAlpha > 0.1 ? `rgba(${r},${g},${b},${fillAlpha})` : null;
143
+ const strokeColor = currentColor;
144
+ drawHexagon(ctx, 0, 0, radius, fillColor, strokeColor, 2);
145
+ }
146
+
147
+ ctx.restore();
148
+ }
149
+
150
+ hexToRgb(hex) {
151
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
152
+ return result ? {
153
+ r: parseInt(result[1], 16),
154
+ g: parseInt(result[2], 16),
155
+ b: parseInt(result[3], 16)
156
+ } : { r: 255, g: 255, b: 255 };
157
+ }
158
+ }
159
+
160
+ // Initialize
161
+ window.addEventListener("load", () => {
162
+ const canvas = document.getElementById("game");
163
+ if (!canvas) return;
164
+
165
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
166
+ const scene = game.scene("main");
167
+ const gameInstance = game.game;
168
+
169
+ // State
170
+ let hexCells = [];
171
+ let bgWhiteness = 0; // 0 = black, 1 = white
172
+ let isAnimating = false;
173
+ let phase = "bloom"; // "bloom" or "collapse"
174
+
175
+ // Debounced resize
176
+ let resizeTimeout = null;
177
+ let needsRebuild = false;
178
+
179
+ const handleResize = () => {
180
+ clearTimeout(resizeTimeout);
181
+ resizeTimeout = setTimeout(() => {
182
+ needsRebuild = true;
183
+ }, 100);
184
+ };
185
+
186
+ window.addEventListener("resize", handleResize);
187
+
188
+ /**
189
+ * Generate hexagon grid
190
+ */
191
+ function generateGrid() {
192
+ const w = gameInstance.width;
193
+ const h = gameInstance.height;
194
+ const centerX = w / 2;
195
+ const centerY = h / 2;
196
+ const size = CONFIG.hexRadius + CONFIG.hexSpacing;
197
+
198
+ hexCells = [];
199
+
200
+ // Calculate how many rings we need to cover the screen
201
+ const maxDist = Math.sqrt(w * w + h * h) / 2;
202
+ const rings = Math.ceil(maxDist / (size * HEX_HEIGHT_RATIO)) + 2;
203
+
204
+ // Generate hexagonal grid using axial coordinates
205
+ for (let q = -rings; q <= rings; q++) {
206
+ for (let r = -rings; r <= rings; r++) {
207
+ const pos = hexToPixel(q, r, size, centerX, centerY);
208
+
209
+ // Skip if too far from visible area
210
+ if (pos.x < -size * 2 || pos.x > w + size * 2 ||
211
+ pos.y < -size * 2 || pos.y > h + size * 2) {
212
+ continue;
213
+ }
214
+
215
+ const distFromCenter = Math.sqrt(
216
+ Math.pow(pos.x - centerX, 2) + Math.pow(pos.y - centerY, 2)
217
+ );
218
+
219
+ hexCells.push(new HexCell(q, r, pos.x, pos.y, distFromCenter));
220
+ }
221
+ }
222
+
223
+ // Sort by distance from center for staggered animation
224
+ hexCells.sort((a, b) => a.distFromCenter - b.distFromCenter);
225
+ }
226
+
227
+ /**
228
+ * Trigger bloom animation (center → edges, black → white)
229
+ */
230
+ function triggerBloom() {
231
+ if (isAnimating) return;
232
+ isAnimating = true;
233
+ phase = "bloom";
234
+
235
+ // Reset all cells for bloom
236
+ for (const cell of hexCells) {
237
+ cell.scale = 0;
238
+ cell.opacity = 0;
239
+ cell.colorPhase = 0;
240
+ cell.rotation = (Math.random() - 0.5) * 0.5;
241
+ cell.color = CONFIG.colors[Math.floor(Math.random() * CONFIG.colors.length)];
242
+ }
243
+
244
+ bgWhiteness = 0;
245
+
246
+ // Find max distance for normalization
247
+ const maxDist = hexCells.length > 0 ?
248
+ hexCells[hexCells.length - 1].distFromCenter : 1;
249
+
250
+ // Animate each cell with Tweenetik (inner to outer)
251
+ let lastDelay = 0;
252
+ for (const cell of hexCells) {
253
+ const normalizedDist = cell.distFromCenter / maxDist;
254
+ const delay = normalizedDist * CONFIG.staggerDelay * hexCells.length * 0.3;
255
+ lastDelay = Math.max(lastDelay, delay);
256
+
257
+ // Scale up
258
+ Tweenetik.to(cell, { scale: 1, opacity: 1 }, CONFIG.bloomDuration * 0.6, Easing.easeOutBack, {
259
+ delay: delay,
260
+ });
261
+
262
+ // Color transition (colorful → white)
263
+ Tweenetik.to(cell, { colorPhase: 1 }, CONFIG.bloomDuration * 0.8, Easing.easeInOutQuad, {
264
+ delay: delay + CONFIG.bloomDuration * 0.3,
265
+ });
266
+
267
+ // Slight rotation
268
+ Tweenetik.to(cell, { rotation: 0 }, CONFIG.bloomDuration, Easing.easeOutQuad, {
269
+ delay: delay,
270
+ });
271
+ }
272
+
273
+ // Wait for all hexagons to finish, then trigger collapse
274
+ const totalBloomTime = lastDelay + CONFIG.bloomDuration * 1.1;
275
+ const randomPause = 100 + Math.random() * 100;
276
+ setTimeout(() => {
277
+ isAnimating = false;
278
+ setTimeout(() => triggerCollapse(), randomPause);
279
+ }, totalBloomTime * 1000);
280
+ }
281
+
282
+ /**
283
+ * Trigger collapse animation (edges → center, white → black)
284
+ */
285
+ function triggerCollapse() {
286
+ if (isAnimating) return;
287
+ isAnimating = true;
288
+ phase = "collapse";
289
+
290
+ // Find max distance for normalization
291
+ const maxDist = hexCells.length > 0 ?
292
+ hexCells[hexCells.length - 1].distFromCenter : 1;
293
+
294
+ // Animate each cell (outer to inner - reverse order)
295
+ let lastDelay = 0;
296
+ for (const cell of hexCells) {
297
+ const normalizedDist = cell.distFromCenter / maxDist;
298
+ // Reverse: outer cells animate first
299
+ const delay = (1 - normalizedDist) * CONFIG.staggerDelay * hexCells.length * 0.3;
300
+ lastDelay = Math.max(lastDelay, delay);
301
+
302
+ // Color transition back (white → colorful)
303
+ Tweenetik.to(cell, { colorPhase: 0 }, CONFIG.bloomDuration * 0.5, Easing.easeOutQuad, {
304
+ delay: delay,
305
+ });
306
+
307
+ // Scale down and fade out
308
+ Tweenetik.to(cell, { scale: 0, opacity: 0 }, CONFIG.bloomDuration * 0.6, Easing.easeInBack, {
309
+ delay: delay + CONFIG.bloomDuration * 0.3,
310
+ });
311
+
312
+ // Rotation
313
+ Tweenetik.to(cell, { rotation: (Math.random() - 0.5) * 0.5 }, CONFIG.bloomDuration, Easing.easeInQuad, {
314
+ delay: delay,
315
+ });
316
+ }
317
+
318
+ // Wait for all hexagons to finish collapsing, then trigger bloom
319
+ const totalCollapseTime = lastDelay + CONFIG.bloomDuration * 1.1;
320
+ const randomPause = 100 + Math.random() * 400;
321
+ setTimeout(() => {
322
+ isAnimating = false;
323
+ setTimeout(() => triggerBloom(), randomPause);
324
+ }, totalCollapseTime * 1000);
325
+ }
326
+
327
+ /**
328
+ * Reset to black (for resize)
329
+ */
330
+ function resetToBlack() {
331
+ Tweenetik.killAll();
332
+
333
+ for (const cell of hexCells) {
334
+ cell.scale = 0;
335
+ cell.opacity = 0;
336
+ cell.colorPhase = 0;
337
+ }
338
+
339
+ bgWhiteness = 0;
340
+ isAnimating = false;
341
+ phase = "bloom";
342
+ }
343
+
344
+ // Generate initial grid
345
+ generateGrid();
346
+
347
+ // Custom render
348
+ gameInstance.clear = function () {
349
+ // Background color interpolated between black and white
350
+ const bg = Math.floor(bgWhiteness * 255);
351
+ this.ctx.fillStyle = `rgb(${bg},${bg},${bg})`;
352
+ this.ctx.fillRect(0, 0, this.width, this.height);
353
+
354
+ // Draw all hex cells
355
+ for (const cell of hexCells) {
356
+ cell.draw(this.ctx, CONFIG.hexRadius);
357
+ }
358
+ };
359
+
360
+ // Update loop
361
+ game.on("update", (dt) => {
362
+ if (needsRebuild) {
363
+ needsRebuild = false;
364
+ resetToBlack();
365
+ generateGrid();
366
+ // Auto-trigger bloom after rebuild
367
+ setTimeout(() => triggerBloom(), 100);
368
+ }
369
+
370
+ // Update Tweenetik
371
+ Tweenetik.updateAll(dt);
372
+ });
373
+
374
+ // Click to bloom
375
+ game.on("click", () => {
376
+ if (bgWhiteness > 0.9) {
377
+ // If already white, reset and bloom again
378
+ resetToBlack();
379
+ setTimeout(() => triggerBloom(), 50);
380
+ } else {
381
+ triggerBloom();
382
+ }
383
+ });
384
+
385
+ // Auto-start bloom
386
+ setTimeout(() => triggerBloom(), 500);
387
+
388
+ game.start();
389
+ });