@guinetik/gcanvas 1.0.4 → 2.0.0

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 (261) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/aizawa.html +27 -0
  3. package/dist/animations.html +31 -0
  4. package/dist/basic.html +38 -0
  5. package/dist/baskara.html +31 -0
  6. package/dist/bezier.html +35 -0
  7. package/dist/beziersignature.html +29 -0
  8. package/dist/blackhole.html +28 -0
  9. package/dist/blob.html +35 -0
  10. package/dist/clifford.html +25 -0
  11. package/dist/cmb.html +24 -0
  12. package/dist/coordinates.html +698 -0
  13. package/dist/cube3d.html +23 -0
  14. package/dist/dadras.html +26 -0
  15. package/dist/dejong.html +25 -0
  16. package/dist/demos.css +303 -0
  17. package/dist/dino.html +42 -0
  18. package/dist/easing.html +28 -0
  19. package/dist/events.html +195 -0
  20. package/dist/fluent.html +647 -0
  21. package/dist/fluid-simple.html +22 -0
  22. package/dist/fluid.html +37 -0
  23. package/dist/fractals.html +36 -0
  24. package/dist/gameobjects.html +626 -0
  25. package/dist/gcanvas.es.js +14368 -9093
  26. package/dist/gcanvas.es.min.js +1 -1
  27. package/dist/gcanvas.umd.js +1 -1
  28. package/dist/gcanvas.umd.min.js +1 -1
  29. package/dist/genart.html +26 -0
  30. package/dist/gendream.html +26 -0
  31. package/dist/group.html +36 -0
  32. package/dist/halvorsen.html +27 -0
  33. package/dist/home.html +587 -0
  34. package/dist/hyperbolic001.html +23 -0
  35. package/dist/hyperbolic002.html +23 -0
  36. package/dist/hyperbolic003.html +23 -0
  37. package/dist/hyperbolic004.html +23 -0
  38. package/dist/hyperbolic005.html +22 -0
  39. package/dist/index.html +446 -0
  40. package/dist/isometric.html +34 -0
  41. package/dist/js/aizawa.js +425 -0
  42. package/dist/js/animations.js +452 -0
  43. package/dist/js/basic.js +204 -0
  44. package/dist/js/baskara.js +751 -0
  45. package/dist/js/bezier.js +692 -0
  46. package/dist/js/beziersignature.js +241 -0
  47. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  48. package/dist/js/blackhole/blackhole.obj.js +318 -0
  49. package/dist/js/blackhole/index.js +409 -0
  50. package/dist/js/blackhole/particle.js +56 -0
  51. package/dist/js/blackhole/starfield.obj.js +218 -0
  52. package/dist/js/blob.js +2276 -0
  53. package/dist/js/clifford.js +236 -0
  54. package/dist/js/cmb.js +594 -0
  55. package/dist/js/coordinates.js +840 -0
  56. package/dist/js/cube3d.js +789 -0
  57. package/dist/js/dadras.js +405 -0
  58. package/dist/js/dejong.js +257 -0
  59. package/dist/js/dino.js +1420 -0
  60. package/dist/js/easing.js +477 -0
  61. package/dist/js/fluent.js +183 -0
  62. package/dist/js/fluid-simple.js +253 -0
  63. package/dist/js/fluid.js +527 -0
  64. package/dist/js/fractals.js +932 -0
  65. package/dist/js/fractalworker.js +93 -0
  66. package/dist/js/gameobjects.js +176 -0
  67. package/dist/js/genart.js +268 -0
  68. package/dist/js/gendream.js +209 -0
  69. package/dist/js/group.js +140 -0
  70. package/dist/js/halvorsen.js +405 -0
  71. package/dist/js/hyperbolic001.js +310 -0
  72. package/dist/js/hyperbolic002.js +388 -0
  73. package/dist/js/hyperbolic003.js +319 -0
  74. package/dist/js/hyperbolic004.js +345 -0
  75. package/dist/js/hyperbolic005.js +340 -0
  76. package/dist/js/info-toggle.js +25 -0
  77. package/dist/js/isometric.js +851 -0
  78. package/dist/js/kerr.js +1547 -0
  79. package/dist/js/lavalamp.js +590 -0
  80. package/dist/js/layout.js +354 -0
  81. package/dist/js/lorenz.js +425 -0
  82. package/dist/js/mondrian.js +285 -0
  83. package/dist/js/opacity.js +275 -0
  84. package/dist/js/painter.js +484 -0
  85. package/dist/js/particles-showcase.js +514 -0
  86. package/dist/js/particles.js +299 -0
  87. package/dist/js/patterns.js +397 -0
  88. package/dist/js/penrose/artifact.js +69 -0
  89. package/dist/js/penrose/blackhole.js +121 -0
  90. package/dist/js/penrose/constants.js +73 -0
  91. package/dist/js/penrose/game.js +943 -0
  92. package/dist/js/penrose/lore.js +278 -0
  93. package/dist/js/penrose/penrosescene.js +892 -0
  94. package/dist/js/penrose/ship.js +216 -0
  95. package/dist/js/penrose/sounds.js +211 -0
  96. package/dist/js/penrose/voidparticle.js +55 -0
  97. package/dist/js/penrose/voidscene.js +258 -0
  98. package/dist/js/penrose/voidship.js +144 -0
  99. package/dist/js/penrose/wormhole.js +46 -0
  100. package/dist/js/pipeline.js +555 -0
  101. package/dist/js/plane3d.js +256 -0
  102. package/dist/js/platformer.js +1579 -0
  103. package/dist/js/rossler.js +480 -0
  104. package/dist/js/scene.js +304 -0
  105. package/dist/js/scenes.js +320 -0
  106. package/dist/js/schrodinger.js +706 -0
  107. package/dist/js/schwarzschild.js +1015 -0
  108. package/dist/js/shapes.js +628 -0
  109. package/dist/js/space/alien.js +171 -0
  110. package/dist/js/space/boom.js +98 -0
  111. package/dist/js/space/boss.js +353 -0
  112. package/dist/js/space/buff.js +73 -0
  113. package/dist/js/space/bullet.js +102 -0
  114. package/dist/js/space/constants.js +85 -0
  115. package/dist/js/space/game.js +1884 -0
  116. package/dist/js/space/hud.js +112 -0
  117. package/dist/js/space/laserbeam.js +179 -0
  118. package/dist/js/space/lightning.js +277 -0
  119. package/dist/js/space/minion.js +192 -0
  120. package/dist/js/space/missile.js +212 -0
  121. package/dist/js/space/player.js +430 -0
  122. package/dist/js/space/powerup.js +90 -0
  123. package/dist/js/space/starfield.js +58 -0
  124. package/dist/js/space/starpower.js +90 -0
  125. package/dist/js/spacetime.js +559 -0
  126. package/dist/js/sphere3d.js +229 -0
  127. package/dist/js/sprite.js +473 -0
  128. package/dist/js/starfaux/config.js +118 -0
  129. package/dist/js/starfaux/enemy.js +353 -0
  130. package/dist/js/starfaux/hud.js +78 -0
  131. package/dist/js/starfaux/index.js +482 -0
  132. package/dist/js/starfaux/laser.js +182 -0
  133. package/dist/js/starfaux/player.js +468 -0
  134. package/dist/js/starfaux/terrain.js +560 -0
  135. package/dist/js/study001.js +275 -0
  136. package/dist/js/study002.js +366 -0
  137. package/dist/js/study003.js +331 -0
  138. package/dist/js/study004.js +389 -0
  139. package/dist/js/study005.js +209 -0
  140. package/dist/js/study006.js +194 -0
  141. package/dist/js/study007.js +192 -0
  142. package/dist/js/study008.js +413 -0
  143. package/dist/js/svgtween.js +204 -0
  144. package/dist/js/tde/accretiondisk.js +471 -0
  145. package/dist/js/tde/blackhole.js +219 -0
  146. package/dist/js/tde/blackholescene.js +209 -0
  147. package/dist/js/tde/config.js +59 -0
  148. package/dist/js/tde/index.js +820 -0
  149. package/dist/js/tde/jets.js +290 -0
  150. package/dist/js/tde/lensedstarfield.js +154 -0
  151. package/dist/js/tde/tdestar.js +297 -0
  152. package/dist/js/tde/tidalstream.js +372 -0
  153. package/dist/js/tde_old/blackhole.obj.js +354 -0
  154. package/dist/js/tde_old/debris.obj.js +791 -0
  155. package/dist/js/tde_old/flare.obj.js +239 -0
  156. package/dist/js/tde_old/index.js +448 -0
  157. package/dist/js/tde_old/star.obj.js +812 -0
  158. package/dist/js/tetris/config.js +157 -0
  159. package/dist/js/tetris/grid.js +286 -0
  160. package/dist/js/tetris/index.js +1195 -0
  161. package/dist/js/tetris/renderer.js +634 -0
  162. package/dist/js/tetris/tetrominos.js +280 -0
  163. package/dist/js/thomas.js +394 -0
  164. package/dist/js/tiles.js +312 -0
  165. package/dist/js/tweendemo.js +79 -0
  166. package/dist/js/visibility.js +102 -0
  167. package/dist/kerr.html +28 -0
  168. package/dist/lavalamp.html +27 -0
  169. package/dist/layouts.html +37 -0
  170. package/dist/logo.svg +4 -0
  171. package/dist/loop.html +84 -0
  172. package/dist/lorenz.html +27 -0
  173. package/dist/mondrian.html +32 -0
  174. package/dist/og_image.png +0 -0
  175. package/dist/opacity.html +36 -0
  176. package/dist/painter.html +39 -0
  177. package/dist/particles-showcase.html +28 -0
  178. package/dist/particles.html +24 -0
  179. package/dist/patterns.html +33 -0
  180. package/dist/penrose-game.html +31 -0
  181. package/dist/pipeline.html +737 -0
  182. package/dist/plane3d.html +24 -0
  183. package/dist/platformer.html +43 -0
  184. package/dist/rossler.html +27 -0
  185. package/dist/scene-interactivity-test.html +220 -0
  186. package/dist/scene.html +33 -0
  187. package/dist/scenes.html +96 -0
  188. package/dist/schrodinger.html +27 -0
  189. package/dist/schwarzschild.html +27 -0
  190. package/dist/shapes.html +16 -0
  191. package/dist/space.html +85 -0
  192. package/dist/spacetime.html +27 -0
  193. package/dist/sphere3d.html +24 -0
  194. package/dist/sprite.html +18 -0
  195. package/dist/starfaux.html +22 -0
  196. package/dist/study001.html +23 -0
  197. package/dist/study002.html +23 -0
  198. package/dist/study003.html +23 -0
  199. package/dist/study004.html +23 -0
  200. package/dist/study005.html +22 -0
  201. package/dist/study006.html +24 -0
  202. package/dist/study007.html +24 -0
  203. package/dist/study008.html +22 -0
  204. package/dist/svgtween.html +29 -0
  205. package/dist/tde.html +28 -0
  206. package/dist/tetris3d.html +25 -0
  207. package/dist/thomas.html +27 -0
  208. package/dist/tiles.html +28 -0
  209. package/dist/transforms.html +400 -0
  210. package/dist/tween.html +45 -0
  211. package/dist/visibility.html +33 -0
  212. package/package.json +1 -1
  213. package/readme.md +30 -22
  214. package/src/game/objects/go.js +7 -0
  215. package/src/game/objects/index.js +2 -0
  216. package/src/game/objects/isometric-scene.js +53 -3
  217. package/src/game/objects/layoutscene.js +57 -0
  218. package/src/game/objects/mask.js +241 -0
  219. package/src/game/objects/scene.js +19 -0
  220. package/src/game/objects/wrapper.js +14 -2
  221. package/src/game/pipeline.js +17 -0
  222. package/src/game/ui/button.js +101 -16
  223. package/src/game/ui/theme.js +0 -6
  224. package/src/game/ui/togglebutton.js +25 -14
  225. package/src/game/ui/tooltip.js +12 -4
  226. package/src/index.js +3 -0
  227. package/src/io/gesture.js +409 -0
  228. package/src/io/index.js +4 -1
  229. package/src/io/keys.js +9 -1
  230. package/src/io/screen.js +476 -0
  231. package/src/math/attractors.js +664 -0
  232. package/src/math/heat.js +106 -0
  233. package/src/math/index.js +1 -0
  234. package/src/mixins/draggable.js +15 -19
  235. package/src/painter/painter.shapes.js +11 -5
  236. package/src/particle/particle-system.js +165 -1
  237. package/src/physics/index.js +26 -0
  238. package/src/physics/physics-updaters.js +333 -0
  239. package/src/physics/physics.js +375 -0
  240. package/src/shapes/image.js +5 -5
  241. package/src/shapes/index.js +2 -0
  242. package/src/shapes/parallelogram.js +147 -0
  243. package/src/shapes/righttriangle.js +115 -0
  244. package/src/shapes/svg.js +281 -100
  245. package/src/shapes/text.js +22 -6
  246. package/src/shapes/transformable.js +5 -0
  247. package/src/sound/effects.js +807 -0
  248. package/src/sound/index.js +13 -0
  249. package/src/webgl/index.js +7 -0
  250. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  251. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  252. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  253. package/src/webgl/webgl-clifford-renderer.js +477 -0
  254. package/src/webgl/webgl-dejong-renderer.js +472 -0
  255. package/src/webgl/webgl-line-renderer.js +391 -0
  256. package/src/webgl/webgl-particle-renderer.js +410 -0
  257. package/types/index.d.ts +30 -2
  258. package/types/io.d.ts +217 -0
  259. package/types/physics.d.ts +299 -0
  260. package/types/shapes.d.ts +8 -0
  261. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Study 005 - Hex Flow
3
+ *
4
+ * A hexagonal grid that breathes, warps, and flows with
5
+ * wave interference patterns. Hypnotic honeycomb energy.
6
+ *
7
+ * Features:
8
+ * - Hexagonal tiling that fills the screen
9
+ * - Multi-wave interference displacement
10
+ * - Color cycling based on position + time
11
+ * - Rotation and scale pulsing per cell
12
+ * - Click to shift color palette
13
+ */
14
+
15
+ import { gcanvas } from "/gcanvas.es.min.js";
16
+
17
+ const CONFIG = {
18
+ // Hex grid
19
+ hexSize: 35,
20
+
21
+ // Wave settings
22
+ waveSpeed: 2.0,
23
+ waveFreq: 0.015,
24
+ waveAmp: 12,
25
+
26
+ // Rotation
27
+ rotationSpeed: 0.8,
28
+ rotationAmp: 0.4,
29
+
30
+ // Scale pulsing
31
+ scaleAmp: 0.3,
32
+
33
+ // Color
34
+ hueSpeed: 30,
35
+ hueSpread: 120,
36
+ saturation: 85,
37
+ lightness: 55,
38
+
39
+ // Style
40
+ strokeWidth: 2,
41
+ fillAlpha: 0.7,
42
+
43
+ bgColor: "#0a0a12",
44
+ };
45
+
46
+ const HEX_ANGLE = Math.PI / 3;
47
+ const SQRT3 = Math.sqrt(3);
48
+
49
+ /**
50
+ * Draw a single hexagon with rotation
51
+ */
52
+ function drawHex(ctx, x, y, size, rotation, fillColor, strokeColor) {
53
+ if (size < 2) return;
54
+
55
+ ctx.save();
56
+ ctx.translate(x, y);
57
+ ctx.rotate(rotation);
58
+
59
+ ctx.beginPath();
60
+ for (let i = 0; i < 6; i++) {
61
+ const angle = HEX_ANGLE * i - Math.PI / 6;
62
+ const hx = size * Math.cos(angle);
63
+ const hy = size * Math.sin(angle);
64
+ if (i === 0) {
65
+ ctx.moveTo(hx, hy);
66
+ } else {
67
+ ctx.lineTo(hx, hy);
68
+ }
69
+ }
70
+ ctx.closePath();
71
+
72
+ ctx.fillStyle = fillColor;
73
+ ctx.fill();
74
+
75
+ ctx.strokeStyle = strokeColor;
76
+ ctx.lineWidth = CONFIG.strokeWidth;
77
+ ctx.stroke();
78
+
79
+ ctx.restore();
80
+ }
81
+
82
+ /**
83
+ * Convert axial hex coords to pixel
84
+ */
85
+ function hexToPixel(q, r, size) {
86
+ const x = size * (3/2 * q);
87
+ const y = size * (SQRT3/2 * q + SQRT3 * r);
88
+ return { x, y };
89
+ }
90
+
91
+ // Initialize
92
+ window.addEventListener("load", () => {
93
+ const canvas = document.getElementById("game");
94
+ if (!canvas) return;
95
+
96
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
97
+ const gameInstance = game.game;
98
+
99
+ let hexGrid = [];
100
+ let time = Math.random() * 100;
101
+ let hueOffset = 0;
102
+
103
+ function buildGrid(w, h) {
104
+ hexGrid = [];
105
+ const size = CONFIG.hexSize;
106
+
107
+ // Use diagonal to ensure full coverage during any transform
108
+ const diagonal = Math.sqrt(w * w + h * h);
109
+ const cols = Math.ceil(diagonal / (size * 1.5)) + 4;
110
+ const rows = Math.ceil(diagonal / (size * SQRT3)) + 4;
111
+
112
+ const startQ = -Math.floor(cols / 2);
113
+ const startR = -Math.floor(rows / 2);
114
+
115
+ for (let q = startQ; q < startQ + cols; q++) {
116
+ for (let r = startR; r < startR + rows; r++) {
117
+ const pos = hexToPixel(q, r, size);
118
+ hexGrid.push({
119
+ q, r,
120
+ baseX: pos.x,
121
+ baseY: pos.y,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ buildGrid(gameInstance.width, gameInstance.height);
128
+
129
+ window.addEventListener("resize", () => {
130
+ buildGrid(gameInstance.width, gameInstance.height);
131
+ });
132
+
133
+ gameInstance.clear = function () {
134
+ const ctx = this.ctx;
135
+ const w = this.width;
136
+ const h = this.height;
137
+ const cx = w / 2;
138
+ const cy = h / 2;
139
+
140
+ // Clear
141
+ ctx.fillStyle = CONFIG.bgColor;
142
+ ctx.fillRect(0, 0, w, h);
143
+
144
+ const t = time;
145
+
146
+ for (const hex of hexGrid) {
147
+ // World position
148
+ const wx = hex.baseX + cx;
149
+ const wy = hex.baseY + cy;
150
+
151
+ // Distance from center for radial effects
152
+ const dx = hex.baseX;
153
+ const dy = hex.baseY;
154
+ const dist = Math.sqrt(dx * dx + dy * dy);
155
+ const angle = Math.atan2(dy, dx);
156
+
157
+ // Multi-wave displacement
158
+ const wave1 = Math.sin(dist * CONFIG.waveFreq - t * CONFIG.waveSpeed);
159
+ const wave2 = Math.sin(dist * CONFIG.waveFreq * 0.7 + t * CONFIG.waveSpeed * 0.5 + angle * 3);
160
+ const wave3 = Math.cos(angle * 6 - t * 1.5) * Math.sin(dist * 0.02);
161
+
162
+ const combinedWave = (wave1 + wave2 * 0.6 + wave3 * 0.4) / 2;
163
+
164
+ // Displacement
165
+ const displaceX = Math.cos(angle) * combinedWave * CONFIG.waveAmp;
166
+ const displaceY = Math.sin(angle) * combinedWave * CONFIG.waveAmp;
167
+
168
+ const finalX = wx + displaceX;
169
+ const finalY = wy + displaceY;
170
+
171
+ // Scale pulsing
172
+ const scalePulse = 1 + combinedWave * CONFIG.scaleAmp;
173
+ const size = CONFIG.hexSize * scalePulse;
174
+
175
+ // Rotation
176
+ const rotation = combinedWave * CONFIG.rotationAmp + t * 0.1;
177
+
178
+ // Color based on position + time
179
+ const hue = (hueOffset + dist * 0.3 + angle * 20 + t * CONFIG.hueSpeed) % 360;
180
+ const sat = CONFIG.saturation + combinedWave * 15;
181
+ const light = CONFIG.lightness + combinedWave * 20;
182
+
183
+ const fillColor = `hsla(${hue}, ${sat}%, ${light}%, ${CONFIG.fillAlpha})`;
184
+ const strokeColor = `hsla(${(hue + 30) % 360}, ${sat}%, ${Math.min(light + 20, 80)}%, 0.9)`;
185
+
186
+ drawHex(ctx, finalX, finalY, size * 0.9, rotation, fillColor, strokeColor);
187
+ }
188
+
189
+ // Draw center glow
190
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, 150);
191
+ gradient.addColorStop(0, `hsla(${(hueOffset + t * 50) % 360}, 100%, 70%, 0.15)`);
192
+ gradient.addColorStop(1, "transparent");
193
+ ctx.fillStyle = gradient;
194
+ ctx.beginPath();
195
+ ctx.arc(cx, cy, 150, 0, Math.PI * 2);
196
+ ctx.fill();
197
+ };
198
+
199
+ game.on("update", (dt) => {
200
+ time += dt;
201
+ });
202
+
203
+ // Click to shift palette
204
+ gameInstance.events.on("click", () => {
205
+ hueOffset = (hueOffset + 60) % 360;
206
+ });
207
+
208
+ game.start();
209
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Study 006 - Psychedelic Drop
3
+ *
4
+ * A grid of particles that reacts to a "drop" in the center, creating
5
+ * psychedelic ripples and interference patterns.
6
+ *
7
+ * Features:
8
+ * - Grid-based particle system
9
+ * - Wave propagation math (sine waves based on distance)
10
+ * - Dynamic color cycling (HSL)
11
+ * - Mouse interaction to move the "drop" source
12
+ */
13
+
14
+ import { gcanvas } from "/gcanvas.es.min.js";
15
+
16
+ // Configuration
17
+ const CONFIG = {
18
+ // Grid settings
19
+ spacing: 20,
20
+ baseSize: 4,
21
+
22
+ // Wave settings
23
+ waveSpeed: 3.0,
24
+ waveFreq: 0.05,
25
+ amplitude: 15,
26
+
27
+ // Color settings
28
+ colorSpeed: 50,
29
+ baseHue: 0,
30
+ hueRange: 180,
31
+ saturation: 80,
32
+ lightness: 60,
33
+
34
+ // Interaction
35
+ dropDecay: 0.95,
36
+
37
+ // Background
38
+ bgColor: "#050505",
39
+ };
40
+
41
+ /**
42
+ * A single particle in the grid
43
+ */
44
+ class DropParticle {
45
+ constructor(x, y, col, row) {
46
+ this.x = x;
47
+ this.y = y;
48
+ this.baseX = x;
49
+ this.baseY = y;
50
+ this.col = col;
51
+ this.row = row;
52
+ this.size = CONFIG.baseSize;
53
+ this.hue = 0;
54
+ }
55
+
56
+ update(dt, time, dropX, dropY) {
57
+ // Calculate distance from the "drop" point
58
+ const dx = this.baseX - dropX;
59
+ const dy = this.baseY - dropY;
60
+ const dist = Math.sqrt(dx * dx + dy * dy);
61
+
62
+ // Calculate wave effect
63
+ // sin(distance * frequency - time * speed)
64
+ const angle = dist * CONFIG.waveFreq - time * CONFIG.waveSpeed;
65
+ const wave = Math.sin(angle);
66
+
67
+ // Secondary interference wave
68
+ const wave2 = Math.cos(angle * 0.5 + time);
69
+
70
+ // Displacement
71
+ const displacement = wave * CONFIG.amplitude;
72
+ const displacement2 = wave2 * (CONFIG.amplitude * 0.5);
73
+
74
+ // this.x = this.baseX + (dx / dist) * displacement; // Radial displacement
75
+ // this.y = this.baseY + (dy / dist) * displacement;
76
+
77
+ // Vertical/Z-like displacement simulated by size and color
78
+ this.size = CONFIG.baseSize + (wave + 1) * 3 + (wave2 + 1);
79
+
80
+ // Color calculation
81
+ // Base hue + wave offset + time cycling
82
+ this.hue = (CONFIG.baseHue + dist * 0.5 + time * CONFIG.colorSpeed) % 360;
83
+
84
+ // Opacity based on wave peaks
85
+ this.alpha = 0.3 + (wave + 1) * 0.35;
86
+ }
87
+
88
+ render(ctx) {
89
+ ctx.fillStyle = `hsla(${this.hue}, ${CONFIG.saturation}%, ${CONFIG.lightness}%, ${this.alpha})`;
90
+
91
+ // Draw circle
92
+ ctx.beginPath();
93
+ ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
94
+ ctx.fill();
95
+
96
+ // Optional: Draw a small connection line to neighbors if close enough (too expensive for many particles)
97
+ }
98
+ }
99
+
100
+ // Initialize
101
+ window.addEventListener("load", () => {
102
+ const canvas = document.getElementById("game");
103
+ if (!canvas) return;
104
+
105
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
106
+ const gameInstance = game.game;
107
+
108
+ let particles = [];
109
+ let dropX = 0;
110
+ let dropY = 0;
111
+ let time = 0;
112
+
113
+ function createGrid(w, h) {
114
+ particles = [];
115
+ const cols = Math.ceil(w / CONFIG.spacing);
116
+ const rows = Math.ceil(h / CONFIG.spacing);
117
+
118
+ // Center the grid
119
+ const startX = (w - (cols - 1) * CONFIG.spacing) / 2;
120
+ const startY = (h - (rows - 1) * CONFIG.spacing) / 2;
121
+
122
+ for (let i = 0; i < cols; i++) {
123
+ for (let j = 0; j < rows; j++) {
124
+ const x = startX + i * CONFIG.spacing;
125
+ const y = startY + j * CONFIG.spacing;
126
+ particles.push(new DropParticle(x, y, i, j));
127
+ }
128
+ }
129
+ }
130
+
131
+ // Initial setup
132
+ dropX = gameInstance.width / 2;
133
+ dropY = gameInstance.height / 2;
134
+ createGrid(gameInstance.width, gameInstance.height);
135
+
136
+ // Handle resize
137
+ let resizeTimeout = null;
138
+ window.addEventListener("resize", () => {
139
+ clearTimeout(resizeTimeout);
140
+ resizeTimeout = setTimeout(() => {
141
+ dropX = gameInstance.width / 2;
142
+ dropY = gameInstance.height / 2;
143
+ createGrid(gameInstance.width, gameInstance.height);
144
+ }, 100);
145
+ });
146
+
147
+ // Custom render loop
148
+ gameInstance.clear = function() {
149
+ // Standard clear
150
+ this.ctx.fillStyle = CONFIG.bgColor;
151
+ this.ctx.fillRect(0, 0, this.width, this.height);
152
+
153
+ // Render all particles
154
+ for (const p of particles) {
155
+ p.render(this.ctx);
156
+ }
157
+ };
158
+
159
+ game.on("update", (dt) => {
160
+ time += dt;
161
+
162
+ // Update all particles
163
+ for (const p of particles) {
164
+ p.update(dt, time, dropX, dropY);
165
+ }
166
+ });
167
+
168
+ // Mouse interaction
169
+ let isDragging = false;
170
+
171
+ gameInstance.events.on("mousedown", () => {
172
+ isDragging = true;
173
+ });
174
+
175
+ gameInstance.events.on("mouseup", () => {
176
+ isDragging = false;
177
+ });
178
+
179
+ gameInstance.events.on("mousemove", (e) => {
180
+ if (!isDragging) return;
181
+ dropX = e.x;
182
+ dropY = e.y;
183
+ });
184
+
185
+ // Click to change palette
186
+ gameInstance.events.on("click", () => {
187
+ CONFIG.baseHue = Math.random() * 360;
188
+ CONFIG.waveFreq = 0.02 + Math.random() * 0.08;
189
+ CONFIG.waveSpeed = 1.0 + Math.random() * 4.0;
190
+ });
191
+
192
+ game.start();
193
+ });
194
+
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Study 009 - Interference
3
+ *
4
+ * Fast, colorful wave interference with sharp contrasts.
5
+ * No white wash - pure RGB separation with dark gaps.
6
+ *
7
+ * Features:
8
+ * - Sparse grid for performance + visual clarity
9
+ * - High-speed wave propagation
10
+ * - Sharp color bands, not blurry white
11
+ * - Click to randomize
12
+ */
13
+
14
+ import { gcanvas } from "/gcanvas.es.min.js";
15
+
16
+ const CONFIG = {
17
+ spacing: 25,
18
+ baseSize: 3,
19
+
20
+ // Wave settings - FAST
21
+ numEmitters: 4,
22
+ emitterSpeed: 1.5,
23
+ waveFreq: 0.04,
24
+ waveSpeed: 8.0,
25
+
26
+ // Chromatic
27
+ rgbSpatialOffset: 6,
28
+
29
+ // Rotation
30
+ globalRotationSpeed: 1.2,
31
+
32
+ bgColor: "#000",
33
+ };
34
+
35
+ class Emitter {
36
+ constructor(index, total) {
37
+ this.baseAngle = (index / total) * Math.PI * 2;
38
+ this.orbitRadius = 0.3;
39
+ this.freq = 0.03 + Math.random() * 0.03;
40
+ this.phase = Math.random() * Math.PI * 2;
41
+ this.speed = 6 + Math.random() * 4;
42
+ }
43
+
44
+ getPosition(time, cx, cy, radius) {
45
+ const angle = this.baseAngle + time * CONFIG.emitterSpeed;
46
+ return {
47
+ x: cx + Math.cos(angle) * this.orbitRadius * radius,
48
+ y: cy + Math.sin(angle) * this.orbitRadius * radius,
49
+ };
50
+ }
51
+
52
+ wave(x, y, time, ex, ey) {
53
+ const dist = Math.hypot(x - ex, y - ey);
54
+ return Math.sin(dist * this.freq - time * this.speed + this.phase);
55
+ }
56
+ }
57
+
58
+ window.addEventListener("load", () => {
59
+ const canvas = document.getElementById("game");
60
+ if (!canvas) return;
61
+
62
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
63
+ const gi = game.game;
64
+
65
+ let grid = [];
66
+ let emitters = [];
67
+ let time = Math.random() * 100;
68
+
69
+ function init() {
70
+ emitters = [];
71
+ for (let i = 0; i < CONFIG.numEmitters; i++) {
72
+ emitters.push(new Emitter(i, CONFIG.numEmitters));
73
+ }
74
+ }
75
+
76
+ function buildGrid(w, h) {
77
+ grid = [];
78
+ // Use the diagonal as the grid size so rotation never shows black corners
79
+ const diagonal = Math.sqrt(w * w + h * h);
80
+ const cols = Math.ceil(diagonal / CONFIG.spacing) + 2;
81
+ const rows = Math.ceil(diagonal / CONFIG.spacing) + 2;
82
+
83
+ // Center the oversized grid
84
+ const ox = (w - (cols - 1) * CONFIG.spacing) / 2;
85
+ const oy = (h - (rows - 1) * CONFIG.spacing) / 2;
86
+
87
+ for (let i = 0; i < cols; i++) {
88
+ for (let j = 0; j < rows; j++) {
89
+ grid.push({ x: ox + i * CONFIG.spacing, y: oy + j * CONFIG.spacing });
90
+ }
91
+ }
92
+ }
93
+
94
+ init();
95
+ buildGrid(gi.width, gi.height);
96
+
97
+ window.addEventListener("resize", () => {
98
+ buildGrid(gi.width, gi.height);
99
+ });
100
+
101
+ gi.clear = function () {
102
+ const ctx = this.ctx;
103
+ const w = this.width;
104
+ const h = this.height;
105
+ const cx = w / 2;
106
+ const cy = h / 2;
107
+ const maxR = Math.max(w, h) * 0.5;
108
+
109
+ // Hard clear - no trails, sharp image
110
+ ctx.fillStyle = CONFIG.bgColor;
111
+ ctx.fillRect(0, 0, w, h);
112
+
113
+ const epos = emitters.map(e => e.getPosition(time, cx, cy, maxR));
114
+
115
+ const cosG = Math.cos(time * CONFIG.globalRotationSpeed);
116
+ const sinG = Math.sin(time * CONFIG.globalRotationSpeed);
117
+
118
+ for (const p of grid) {
119
+ // Rotate around center
120
+ const dx = p.x - cx;
121
+ const dy = p.y - cy;
122
+ const rx = cx + dx * cosG - dy * sinG;
123
+ const ry = cy + dx * sinG + dy * cosG;
124
+
125
+ // Get wave from each emitter with phase offsets for RGB
126
+ let wR = 0, wG = 0, wB = 0;
127
+ for (let i = 0; i < emitters.length; i++) {
128
+ const e = emitters[i];
129
+ const ep = epos[i];
130
+ wR += e.wave(rx, ry, time, ep.x, ep.y);
131
+ wG += e.wave(rx, ry, time + 0.3, ep.x, ep.y);
132
+ wB += e.wave(rx, ry, time + 0.6, ep.x, ep.y);
133
+ }
134
+ wR /= emitters.length;
135
+ wG /= emitters.length;
136
+ wB /= emitters.length;
137
+
138
+ // Only draw if wave is positive (creates dark gaps)
139
+ const threshold = 0.1;
140
+
141
+ // Size pulses with wave
142
+ const avgWave = (wR + wG + wB) / 3;
143
+ const size = CONFIG.baseSize + Math.max(0, avgWave) * 5;
144
+
145
+ // Offset direction for RGB split
146
+ const angle = Math.atan2(ry - cy, rx - cx);
147
+ const offX = Math.cos(angle + Math.PI/2) * CONFIG.rgbSpatialOffset;
148
+ const offY = Math.sin(angle + Math.PI/2) * CONFIG.rgbSpatialOffset;
149
+
150
+ // Draw RGB circles only when wave > threshold
151
+ if (wR > threshold) {
152
+ const intensity = Math.floor(wR * 255);
153
+ ctx.fillStyle = `rgb(${intensity}, 0, 0)`;
154
+ ctx.beginPath();
155
+ ctx.arc(rx - offX, ry - offY, size, 0, Math.PI * 2);
156
+ ctx.fill();
157
+ }
158
+
159
+ if (wG > threshold) {
160
+ const intensity = Math.floor(wG * 255);
161
+ ctx.fillStyle = `rgb(0, ${intensity}, 0)`;
162
+ ctx.beginPath();
163
+ ctx.arc(rx, ry, size, 0, Math.PI * 2);
164
+ ctx.fill();
165
+ }
166
+
167
+ if (wB > threshold) {
168
+ const intensity = Math.floor(wB * 255);
169
+ ctx.fillStyle = `rgb(0, 0, ${intensity})`;
170
+ ctx.beginPath();
171
+ ctx.arc(rx + offX, ry + offY, size, 0, Math.PI * 2);
172
+ ctx.fill();
173
+ }
174
+ }
175
+ };
176
+
177
+ game.on("update", (dt) => {
178
+ time += dt;
179
+ });
180
+
181
+ gi.events.on("click", () => {
182
+ for (const e of emitters) {
183
+ e.freq = 0.02 + Math.random() * 0.05;
184
+ e.speed = 5 + Math.random() * 6;
185
+ e.phase = Math.random() * Math.PI * 2;
186
+ e.orbitRadius = 0.2 + Math.random() * 0.3;
187
+ }
188
+ CONFIG.globalRotationSpeed = 0.8 + Math.random() * 1.0;
189
+ });
190
+
191
+ game.start();
192
+ });