@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,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
|
+
});
|