@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,388 @@
1
+ /**
2
+ * Hyperbolic 002 - Mobius Flow
3
+ *
4
+ * Inspired by hyperbolic space - a twisted ribbon (lemniscate) that
5
+ * breathes with noise. Click to disturb, watch it settle to equilibrium.
6
+ *
7
+ * Features:
8
+ * - Lemniscate ribbon with twist deformation
9
+ * - Click to add energy - more clicks = more chaos
10
+ * - Settles to subtle vibration equilibrium
11
+ * - Drag to rotate, auto-rotates when idle
12
+ */
13
+
14
+ import { gcanvas, Noise } from "/gcanvas.es.min.js";
15
+
16
+ // Configuration
17
+ const CONFIG = {
18
+ // Mobius geometry
19
+ radius: 350, // Increased from 300
20
+ width: 140, // Increased from 120
21
+ uSegments: 140, // Increased detail
22
+ vSegments: 24, // Increased detail
23
+
24
+ // Noise settings
25
+ noiseScale: 0.008,
26
+ noiseSpeed: 0.8,
27
+ noiseStrengthBase: 10,
28
+ noiseStrengthMax: 100,
29
+
30
+ // Projection
31
+ fov: 700,
32
+
33
+ // Colors
34
+ baseColor: { h: 190, s: 100, l: 60 }, // Cyan/Blue
35
+ bgColor: "#000000",
36
+ };
37
+
38
+ /**
39
+ * 3D Vector helper
40
+ */
41
+ class Vec3 {
42
+ constructor(x, y, z) {
43
+ this.x = x;
44
+ this.y = y;
45
+ this.z = z;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Main Study Class
51
+ */
52
+ class MobiusFlow {
53
+ constructor(game) {
54
+ this.game = game;
55
+ this.width = game.width;
56
+ this.height = game.height;
57
+
58
+ // Mesh data
59
+ this.vertices = [];
60
+ this.indices = [];
61
+
62
+ // Animation state
63
+ this.time = 0;
64
+ this.rotation = { x: 0, y: 0 };
65
+ this.targetRotation = { x: 0.4, y: 0.2 };
66
+ this.autoRotate = true;
67
+
68
+ // Interaction state - energy decays toward equilibrium
69
+ this.energy = 0;
70
+ this.noiseStrength = CONFIG.noiseStrengthBase;
71
+
72
+ this.initMesh();
73
+ }
74
+
75
+ initMesh() {
76
+ this.vertices = [];
77
+ this.indices = [];
78
+
79
+ // Generate Lemniscate (Infinity Symbol) Ribbon vertices
80
+ // Parametric equations for a 3D ribbon following a lemniscate curve
81
+
82
+ // Lemniscate of Bernoulli path:
83
+ // x = a * cos(t) / (1 + sin^2(t))
84
+ // y = a * sin(t) * cos(t) / (1 + sin^2(t))
85
+ // z = 0 (base curve is flat, but we'll twist the ribbon around it)
86
+
87
+ // Ribbon construction:
88
+ // P(t) = curve point
89
+ // T(t) = tangent vector
90
+ // N(t) = normal vector (perpendicular to tangent and "up")
91
+ // B(t) = binormal (cross product)
92
+
93
+ for (let i = 0; i < CONFIG.uSegments; i++) {
94
+ // u goes from 0 to 2PI
95
+ const u = (i / CONFIG.uSegments) * Math.PI * 2;
96
+
97
+ // Calculate curve point
98
+ const scale = CONFIG.radius;
99
+ const denom = 1 + Math.sin(u) * Math.sin(u);
100
+
101
+ const cx = scale * Math.cos(u) / denom;
102
+ const cy = scale * Math.sin(u) * Math.cos(u) / denom;
103
+ const cz = 0; // Base curve is flat
104
+
105
+ // Calculate tangent (derivative) approximately or analytically
106
+ // Let's use simple finite difference for tangent
107
+ const uNext = u + 0.01;
108
+ const denomNext = 1 + Math.sin(uNext) * Math.sin(uNext);
109
+ const cxNext = scale * Math.cos(uNext) / denomNext;
110
+ const cyNext = scale * Math.sin(uNext) * Math.cos(uNext) / denomNext;
111
+ const czNext = 0;
112
+
113
+ // Tangent vector
114
+ let tx = cxNext - cx;
115
+ let ty = cyNext - cy;
116
+ let tz = czNext - cz;
117
+
118
+ // Normalize tangent
119
+ const tLen = Math.sqrt(tx*tx + ty*ty + tz*tz);
120
+ tx /= tLen; ty /= tLen; tz /= tLen;
121
+
122
+ // Define a "Binormal" vector roughly perpendicular to tangent and Z-up
123
+ // Up vector (0, 0, 1)
124
+ // B = T x Up
125
+ let bx = ty * 1 - tz * 0;
126
+ let by = tz * 0 - tx * 1;
127
+ let bz = tx * 0 - ty * 0; // Will be non-zero if we had 3D path
128
+
129
+ // Since path is 2D (z=0), Binormal is just (-ty, tx, 0)
130
+
131
+ // Twist factor! Rotate the "Normal" around the Tangent as we go along
132
+ // Full twist: u goes 0->2PI, twist goes 0->2PI (or PI for mobius-like)
133
+ const twist = u; // Full 360 twist over the length
134
+
135
+ // Rotate the binormal vector around the tangent vector?
136
+ // Actually simpler: Just define the ribbon cross section vector
137
+ // Start with vector perpendicular to curve in XY plane (normal 2D)
138
+ // Then rotate it around the Tangent to get 3D ribbon
139
+
140
+ // Normal 2D vector (perpendicular to tangent in XY)
141
+ let nx = -ty;
142
+ let ny = tx;
143
+ let nz = 0;
144
+
145
+ // Apply twist rotation around the Tangent axis?
146
+ // Or just rotate around the curve itself?
147
+ // Let's rotate the normal vector [nx, ny, nz] around [tx, ty, tz] by 'twist'
148
+
149
+ // Rodrigues rotation formula for vector v around k by theta:
150
+ // v_rot = v cos(th) + (k x v) sin(th) + k(k.v)(1-cos(th))
151
+ // Here v = normal, k = tangent
152
+ // Since k.v = 0 (orthogonal), term 3 is 0.
153
+ // v_rot = n * cos(twist) + (t x n) * sin(twist)
154
+
155
+ // Cross product (t x n) is roughly the Z axis (0,0,1) direction
156
+ const cx_n = ty*nz - tz*ny;
157
+ const cy_n = tz*nx - tx*nz;
158
+ const cz_n = tx*ny - ty*nx;
159
+
160
+ const cosTwist = Math.cos(twist);
161
+ const sinTwist = Math.sin(twist);
162
+
163
+ const rx = nx * cosTwist + cx_n * sinTwist;
164
+ const ry = ny * cosTwist + cy_n * sinTwist;
165
+ const rz = nz * cosTwist + cz_n * sinTwist;
166
+
167
+ // Ribbon width vector
168
+ const wx = rx * CONFIG.width;
169
+ const wy = ry * CONFIG.width;
170
+ const wz = rz * CONFIG.width;
171
+
172
+ for (let j = 0; j < CONFIG.vSegments; j++) {
173
+ // v goes from -0.5 to 0.5
174
+ const v = (j / (CONFIG.vSegments - 1)) - 0.5;
175
+
176
+ const x = cx + wx * v;
177
+ const y = cy + wy * v;
178
+ const z = cz + wz * v;
179
+
180
+ this.vertices.push(new Vec3(x, y, z));
181
+
182
+ // Indices generation (Grid logic)
183
+ if (i < CONFIG.uSegments - 1) {
184
+ const current = i * CONFIG.vSegments + j;
185
+ const right = (i + 1) * CONFIG.vSegments + j;
186
+ const down = i * CONFIG.vSegments + (j + 1);
187
+
188
+ this.indices.push([current, right]);
189
+ if (j < CONFIG.vSegments - 1) {
190
+ this.indices.push([current, down]);
191
+ }
192
+ } else {
193
+ // Close loop
194
+ const current = i * CONFIG.vSegments + j;
195
+ const right = 0 * CONFIG.vSegments + j; // Connect back to start
196
+
197
+ this.indices.push([current, right]);
198
+ if (j < CONFIG.vSegments - 1) {
199
+ const down = i * CONFIG.vSegments + (j + 1);
200
+ this.indices.push([current, down]);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ update(dt) {
208
+ this.time += dt;
209
+
210
+ // Update dimensions
211
+ this.width = this.game.width;
212
+ this.height = this.game.height;
213
+
214
+ // Responsive scaling
215
+ const minDim = Math.min(this.width, this.height);
216
+ this.renderScale = minDim / 600; // Adjusted baseline for larger appearance
217
+
218
+ // Smooth rotation
219
+ this.rotation.x += (this.targetRotation.x - this.rotation.x) * 0.1;
220
+ this.rotation.y += (this.targetRotation.y - this.rotation.y) * 0.1;
221
+
222
+ // Auto rotate
223
+ if (this.autoRotate) {
224
+ this.targetRotation.y += dt * 0.3;
225
+ this.targetRotation.x += dt * 0.15;
226
+ }
227
+
228
+ // Decay energy smoothly toward 0
229
+ this.energy *= 0.95;
230
+ if (this.energy < 0.001) this.energy = 0;
231
+
232
+ // Interpolate noise strength: base (calm) + energy * (max - base)
233
+ const targetStrength = CONFIG.noiseStrengthBase + this.energy * (CONFIG.noiseStrengthMax - CONFIG.noiseStrengthBase);
234
+ this.noiseStrength += (targetStrength - this.noiseStrength) * 0.1;
235
+ }
236
+
237
+ render(ctx, width, height) {
238
+ const cx = width / 2;
239
+ const cy = height / 2;
240
+
241
+ // Clear
242
+ ctx.fillStyle = CONFIG.bgColor;
243
+ ctx.fillRect(0, 0, width, height);
244
+
245
+ const cosX = Math.cos(this.rotation.x);
246
+ const sinX = Math.sin(this.rotation.x);
247
+ const cosY = Math.cos(this.rotation.y);
248
+ const sinY = Math.sin(this.rotation.y);
249
+
250
+ // Transform and project
251
+ const projectedVertices = new Array(this.vertices.length);
252
+
253
+ for (let i = 0; i < this.vertices.length; i++) {
254
+ const v = this.vertices[i];
255
+
256
+ // 1. Noise Displacement
257
+ const nX = v.x * CONFIG.noiseScale;
258
+ const nY = v.y * CONFIG.noiseScale;
259
+ const nZ = v.z * CONFIG.noiseScale;
260
+ const t = this.time * CONFIG.noiseSpeed;
261
+
262
+ // Use 4D noise approximation (using 3D with moving slice)
263
+ const noiseVal = Noise.simplex3(nX + t, nY + t*0.5, nZ);
264
+ const displacement = noiseVal * this.noiseStrength;
265
+
266
+ // Deform
267
+ let dx = v.x + displacement * (v.x / CONFIG.radius);
268
+ let dy = v.y + displacement * (v.y / CONFIG.radius);
269
+ let dz = v.z + displacement * 0.5; // Less Z deformation
270
+
271
+ // 2. Rotate
272
+ let x1 = dx * cosY - dz * sinY;
273
+ let z1 = dz * cosY + dx * sinY;
274
+ let y1 = dy;
275
+
276
+ let y2 = y1 * cosX - z1 * sinX;
277
+ let z2 = z1 * cosX + y1 * sinX;
278
+ let x2 = x1;
279
+
280
+ // 3. Project
281
+ // Apply renderScale to coordinates BEFORE projection
282
+ // This scales the world-space object proportional to screen size
283
+ const s = this.renderScale || 1;
284
+ const sx = x2 * s;
285
+ const sy = y2 * s;
286
+ const sz = z2 * s;
287
+
288
+ const scale = CONFIG.fov / (CONFIG.fov + sz + 300);
289
+ const px = sx * scale + cx;
290
+ const py = sy * scale + cy;
291
+
292
+ projectedVertices[i] = { x: px, y: py, z: sz, scale: scale };
293
+ }
294
+
295
+ // Draw edges
296
+ // Scale line width with screen size too
297
+ const lineWidth = 1.5 * (this.renderScale || 1);
298
+ ctx.lineWidth = lineWidth;
299
+
300
+ for (let i = 0; i < this.indices.length; i++) {
301
+ const [idx1, idx2] = this.indices[i];
302
+ const p1 = projectedVertices[idx1];
303
+ const p2 = projectedVertices[idx2];
304
+
305
+ // Culling
306
+ if (p1.z < -CONFIG.fov + 10 || p2.z < -CONFIG.fov + 10) continue;
307
+
308
+ const avgZ = (p1.z + p2.z) / 2;
309
+
310
+ // Depth color mapping
311
+ // Map Z to opacity and lightness
312
+ const depthAlpha = Math.max(0.15, Math.min(1, (400 - avgZ) / 600));
313
+
314
+ // Color shift
315
+ // Base cyan, shift towards purple/blue at depth
316
+ const hue = (CONFIG.baseColor.h + avgZ * 0.1) % 360;
317
+
318
+ ctx.strokeStyle = `hsla(${hue}, ${CONFIG.baseColor.s}%, ${CONFIG.baseColor.l}%, ${depthAlpha})`;
319
+
320
+ ctx.beginPath();
321
+ ctx.moveTo(p1.x, p1.y);
322
+ ctx.lineTo(p2.x, p2.y);
323
+ ctx.stroke();
324
+ }
325
+ }
326
+ }
327
+
328
+ // Initialize
329
+ window.addEventListener("load", () => {
330
+ const canvas = document.getElementById("game");
331
+ if (!canvas) return;
332
+
333
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
334
+ const gameInstance = game.game;
335
+
336
+ const mobius = new MobiusFlow(gameInstance);
337
+
338
+ // Custom render loop
339
+ gameInstance.clear = function() {
340
+ mobius.render(this.ctx, this.width, this.height);
341
+ };
342
+
343
+ game.on("update", (dt) => {
344
+ mobius.update(dt);
345
+ });
346
+
347
+ // Interaction
348
+ let isDragging = false;
349
+ let lastX = 0;
350
+ let lastY = 0;
351
+
352
+ gameInstance.events.on("mousedown", (e) => {
353
+ isDragging = true;
354
+ lastX = e.x;
355
+ lastY = e.y;
356
+ });
357
+
358
+ gameInstance.events.on("mouseup", () => {
359
+ isDragging = false;
360
+ setTimeout(() => { mobius.autoRotate = true; }, 500);
361
+ });
362
+
363
+ gameInstance.events.on("mousemove", (e) => {
364
+ if (isDragging) {
365
+ const dx = (e.x - lastX) * 0.01;
366
+ const dy = (e.y - lastY) * 0.01;
367
+
368
+ mobius.targetRotation.y += dx;
369
+ mobius.targetRotation.x += dy;
370
+ mobius.autoRotate = false;
371
+
372
+ lastX = e.x;
373
+ lastY = e.y;
374
+ }
375
+ });
376
+
377
+ // Click to change color/noise
378
+ gameInstance.events.on("click", () => {
379
+ if(!isDragging) {
380
+ CONFIG.baseColor.h = Math.random() * 360;
381
+ // Add energy
382
+ mobius.energy = Math.min(mobius.energy + 0.4, 1.0);
383
+ }
384
+ });
385
+
386
+ game.start();
387
+ });
388
+
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Hyperbolic 003 - Hyperbolic Fabric
3
+ *
4
+ * Inspired by hyperbolic space - a ruffled disk that mimics crochet
5
+ * hyperbolic plane models. Click to disturb, watch it settle.
6
+ *
7
+ * Features:
8
+ * - Ruffled disk with exponential edge folding
9
+ * - Click to add energy - more clicks = more chaos
10
+ * - Settles to gentle wave equilibrium
11
+ * - Drag to rotate, auto-rotates when idle
12
+ */
13
+
14
+ import { gcanvas, Noise } from "/gcanvas.es.min.js";
15
+
16
+ // Configuration
17
+ const CONFIG = {
18
+ // Geometry
19
+ radius: 700, // Much larger
20
+ rings: 40,
21
+ slices: 80,
22
+
23
+ // Hyperbolic settings
24
+ waves: 8,
25
+ amplitudeBase: 80, // Calm equilibrium
26
+ amplitudeMax: 250, // Max when energized
27
+ exponent: 1.8,
28
+
29
+ // Noise
30
+ noiseScale: 0.005,
31
+ noiseSpeed: 0.4,
32
+ noiseStrengthBase: 10,
33
+ noiseStrengthMax: 60,
34
+
35
+ // Projection
36
+ fov: 800,
37
+
38
+ // Colors
39
+ baseColor: { h: 320, s: 100, l: 60 }, // Magenta/Pink
40
+ bgColor: "#000000",
41
+ };
42
+
43
+ /**
44
+ * 3D Vector helper
45
+ */
46
+ class Vec3 {
47
+ constructor(x, y, z) {
48
+ this.x = x;
49
+ this.y = y;
50
+ this.z = z;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Main Study Class
56
+ */
57
+ class HyperbolicFabric {
58
+ constructor(game) {
59
+ this.game = game;
60
+ this.width = game.width;
61
+ this.height = game.height;
62
+
63
+ this.vertices = [];
64
+ this.indices = [];
65
+
66
+ // Animation
67
+ this.time = 0;
68
+ this.rotation = { x: 0.4, y: 0 };
69
+ this.targetRotation = { x: 0.6, y: 0.2 };
70
+ this.autoRotate = true;
71
+
72
+ // Interaction state - energy decays toward equilibrium
73
+ this.energy = 0;
74
+ this.amplitude = CONFIG.amplitudeBase;
75
+ this.noiseStrength = CONFIG.noiseStrengthBase;
76
+
77
+ this.initMesh();
78
+ }
79
+
80
+ initMesh() {
81
+ this.vertices = [];
82
+ this.indices = [];
83
+
84
+ // Generate Polar Grid
85
+ for (let i = 0; i <= CONFIG.rings; i++) {
86
+ const rRatio = i / CONFIG.rings;
87
+ const r = rRatio * CONFIG.radius;
88
+
89
+ for (let j = 0; j < CONFIG.slices; j++) {
90
+ const thetaRatio = j / CONFIG.slices;
91
+ const theta = thetaRatio * Math.PI * 2;
92
+
93
+ // Base flat disk position
94
+ const x = r * Math.cos(theta);
95
+ const y = r * Math.sin(theta);
96
+ const z = 0;
97
+
98
+ this.vertices.push(new Vec3(x, y, z));
99
+
100
+ // Indices (Quads)
101
+ if (i < CONFIG.rings) {
102
+ const current = i * CONFIG.slices + j;
103
+ const nextRing = (i + 1) * CONFIG.slices + j;
104
+
105
+ // Connect to next point in ring (wrapping)
106
+ const nextSlice = i * CONFIG.slices + ((j + 1) % CONFIG.slices);
107
+ const nextRingNextSlice = (i + 1) * CONFIG.slices + ((j + 1) % CONFIG.slices);
108
+
109
+ // Radial line
110
+ this.indices.push([current, nextRing]);
111
+
112
+ // Angular line (ring)
113
+ this.indices.push([current, nextSlice]);
114
+
115
+ // Optional: Diagonal for triangulation look?
116
+ // this.indices.push([current, nextRingNextSlice]);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ update(dt) {
123
+ this.time += dt;
124
+
125
+ // Update dimensions
126
+ this.width = this.game.width;
127
+ this.height = this.game.height;
128
+
129
+ // Responsive scaling
130
+ const minDim = Math.min(this.width, this.height);
131
+ this.renderScale = minDim / 900;
132
+
133
+ // Smooth rotation
134
+ this.rotation.x += (this.targetRotation.x - this.rotation.x) * 0.1;
135
+ this.rotation.y += (this.targetRotation.y - this.rotation.y) * 0.1;
136
+
137
+ if (this.autoRotate) {
138
+ this.targetRotation.y += dt * 0.1;
139
+ this.targetRotation.x = this.targetRotation.x + Math.sin(this.time * 0.5) * 0.002;
140
+ }
141
+
142
+ // Decay energy smoothly toward 0
143
+ this.energy *= 0.95;
144
+ if (this.energy < 0.001) this.energy = 0;
145
+
146
+ // Interpolate amplitude and noise toward equilibrium
147
+ const targetAmp = CONFIG.amplitudeBase + this.energy * (CONFIG.amplitudeMax - CONFIG.amplitudeBase);
148
+ this.amplitude += (targetAmp - this.amplitude) * 0.1;
149
+
150
+ const targetNoise = CONFIG.noiseStrengthBase + this.energy * (CONFIG.noiseStrengthMax - CONFIG.noiseStrengthBase);
151
+ this.noiseStrength += (targetNoise - this.noiseStrength) * 0.1;
152
+ }
153
+
154
+ render(ctx, width, height) {
155
+ const cx = width / 2;
156
+ const cy = height / 2;
157
+
158
+ ctx.fillStyle = CONFIG.bgColor;
159
+ ctx.fillRect(0, 0, width, height);
160
+
161
+ const cosX = Math.cos(this.rotation.x);
162
+ const sinX = Math.sin(this.rotation.x);
163
+ const cosY = Math.cos(this.rotation.y);
164
+ const sinY = Math.sin(this.rotation.y);
165
+
166
+ const projectedVertices = new Array(this.vertices.length);
167
+ const scaleFactor = this.renderScale || 1;
168
+
169
+ // Transform Loop
170
+ for (let i = 0; i < this.vertices.length; i++) {
171
+ const v = this.vertices[i];
172
+
173
+ // Calculate derived polar coords
174
+ const r = Math.sqrt(v.x*v.x + v.y*v.y);
175
+ const theta = Math.atan2(v.y, v.x);
176
+ const rRatio = r / CONFIG.radius;
177
+
178
+ // 1. Hyperbolic Deformation (The "Crochet" Effect)
179
+ // Height increases exponentially with radius, oscillating by angle
180
+ const wavePhase = theta * CONFIG.waves;
181
+ // Add a radial movement to create flow (waves travel outwards)
182
+ const twistPhase = wavePhase + (rRatio * 10.0 - this.time * 3.0);
183
+
184
+ const zBase = Math.pow(rRatio, CONFIG.exponent) * this.amplitude * Math.sin(twistPhase);
185
+
186
+ // 2. Noise Detail
187
+ const nX = v.x * CONFIG.noiseScale;
188
+ const nY = v.y * CONFIG.noiseScale;
189
+ const t = this.time * CONFIG.noiseSpeed;
190
+ const noiseVal = Noise.simplex3(nX + t, nY + t, zBase * 0.01);
191
+
192
+ // Apply deformations
193
+ const dz = zBase + noiseVal * this.noiseStrength * rRatio; // Noise stronger at edges
194
+
195
+ // Position
196
+ let dx = v.x;
197
+ let dy = v.y;
198
+ let dz_final = dz;
199
+
200
+ // 3. Rotation
201
+ let x1 = dx * cosY - dz_final * sinY;
202
+ let z1 = dz_final * cosY + dx * sinY;
203
+ let y1 = dy;
204
+
205
+ let y2 = y1 * cosX - z1 * sinX;
206
+ let z2 = z1 * cosX + y1 * sinX;
207
+ let x2 = x1;
208
+
209
+ // 4. Projection
210
+ const sx = x2 * scaleFactor;
211
+ const sy = y2 * scaleFactor;
212
+ const sz = z2 * scaleFactor;
213
+
214
+ const scale = CONFIG.fov / (CONFIG.fov + sz + 400);
215
+ const px = sx * scale + cx;
216
+ const py = sy * scale + cy;
217
+
218
+ projectedVertices[i] = {
219
+ x: px,
220
+ y: py,
221
+ z: sz,
222
+ scale: scale,
223
+ rRatio: rRatio // Store for coloring
224
+ };
225
+ }
226
+
227
+ // Draw Edges
228
+ const lineWidth = 1.5 * scaleFactor;
229
+ ctx.lineWidth = lineWidth;
230
+
231
+ for (let i = 0; i < this.indices.length; i++) {
232
+ const [idx1, idx2] = this.indices[i];
233
+ const p1 = projectedVertices[idx1];
234
+ const p2 = projectedVertices[idx2];
235
+
236
+ // Culling
237
+ if (p1.z < -CONFIG.fov + 10 || p2.z < -CONFIG.fov + 10) continue;
238
+
239
+ const avgZ = (p1.z + p2.z) / 2;
240
+ const avgR = (p1.rRatio + p2.rRatio) / 2;
241
+
242
+ // Depth fading
243
+ const depthAlpha = Math.max(0.1, Math.min(1, (500 - avgZ) / 700));
244
+
245
+ // Color:
246
+ // Inner = Darker/Purple, Outer = Brighter/Pink
247
+ // Modulate Hue by radius
248
+ const hue = (CONFIG.baseColor.h + avgR * 40) % 360;
249
+ const light = 30 + avgR * 40; // Brighter at edges
250
+
251
+ ctx.strokeStyle = `hsla(${hue}, ${CONFIG.baseColor.s}%, ${light}%, ${depthAlpha})`;
252
+
253
+ ctx.beginPath();
254
+ ctx.moveTo(p1.x, p1.y);
255
+ ctx.lineTo(p2.x, p2.y);
256
+ ctx.stroke();
257
+ }
258
+ }
259
+ }
260
+
261
+ // Initialize
262
+ window.addEventListener("load", () => {
263
+ const canvas = document.getElementById("game");
264
+ if (!canvas) return;
265
+
266
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
267
+ const gameInstance = game.game;
268
+
269
+ const fabric = new HyperbolicFabric(gameInstance);
270
+
271
+ // Loop
272
+ gameInstance.clear = function() {
273
+ fabric.render(this.ctx, this.width, this.height);
274
+ };
275
+
276
+ game.on("update", (dt) => {
277
+ fabric.update(dt);
278
+ });
279
+
280
+ // Interaction
281
+ let isDragging = false;
282
+ let lastX = 0, lastY = 0;
283
+
284
+ gameInstance.events.on("mousedown", (e) => {
285
+ isDragging = true;
286
+ lastX = e.x;
287
+ lastY = e.y;
288
+ });
289
+
290
+ gameInstance.events.on("mouseup", () => {
291
+ isDragging = false;
292
+ setTimeout(() => { fabric.autoRotate = true; }, 500);
293
+ });
294
+
295
+ gameInstance.events.on("mousemove", (e) => {
296
+ if (isDragging) {
297
+ const dx = (e.x - lastX) * 0.01;
298
+ const dy = (e.y - lastY) * 0.01;
299
+
300
+ fabric.targetRotation.y += dx;
301
+ fabric.targetRotation.x += dy;
302
+ fabric.autoRotate = false;
303
+
304
+ lastX = e.x;
305
+ lastY = e.y;
306
+ }
307
+ });
308
+
309
+ gameInstance.events.on("click", () => {
310
+ if(!isDragging) {
311
+ // Add energy on click
312
+ fabric.energy = Math.min(fabric.energy + 0.4, 1.0);
313
+ CONFIG.baseColor.h = Math.random() * 360;
314
+ }
315
+ });
316
+
317
+ game.start();
318
+ });
319
+