@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,345 @@
1
+ /**
2
+ * Hyperbolic 004 - Dini's Vortex
3
+ *
4
+ * Inspired by hyperbolic space - Dini's Surface, a twisted pseudosphere
5
+ * of constant negative curvature. Click to disturb, watch it settle.
6
+ *
7
+ * Features:
8
+ * - Dini's Surface (hyperbolic vortex)
9
+ * - Click to add energy - more clicks = more chaos
10
+ * - Settles to gentle pulsing 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
+ scale: 180, // Much larger
20
+ uTurns: 4,
21
+ uSegments: 180,
22
+ vSegments: 40,
23
+ b: 0.15,
24
+
25
+ // Noise
26
+ noiseScale: 0.008,
27
+ noiseSpeed: 0.5,
28
+ noiseStrengthBase: 8,
29
+ noiseStrengthMax: 70,
30
+
31
+ // Projection
32
+ fov: 700,
33
+
34
+ // Colors
35
+ baseColor: { h: 45, s: 100, l: 60 }, // Gold/Orange/Yellow
36
+ bgColor: "#000000", // Pure black
37
+ };
38
+
39
+ /**
40
+ * 3D Vector helper
41
+ */
42
+ class Vec3 {
43
+ constructor(x, y, z) {
44
+ this.x = x;
45
+ this.y = y;
46
+ this.z = z;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Main Study Class
52
+ */
53
+ class DiniVortex {
54
+ constructor(game) {
55
+ this.game = game;
56
+ this.width = game.width;
57
+ this.height = game.height;
58
+
59
+ this.vertices = [];
60
+ this.indices = [];
61
+
62
+ // Animation
63
+ this.time = 0;
64
+ this.rotation = { x: 0.2, y: 0 };
65
+ this.targetRotation = { x: 0.2, y: 0.5 };
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
+ // Dini's Surface Equations:
80
+ // x = a * cos(u) * sin(v)
81
+ // y = a * sin(u) * sin(v)
82
+ // z = a * (cos(v) + ln(tan(v/2))) + b*u
83
+
84
+ // Constraints:
85
+ // u: 0 to 4PI (or more for spiraling)
86
+ // v: 0.1 to PI (profile curve)
87
+
88
+ const a = CONFIG.scale;
89
+ const b = CONFIG.b * CONFIG.scale; // Scale the pitch too
90
+
91
+ const uMax = CONFIG.uTurns * Math.PI * 2;
92
+
93
+ for (let i = 0; i < CONFIG.uSegments; i++) {
94
+ const uRatio = i / (CONFIG.uSegments - 1);
95
+ const u = uRatio * uMax;
96
+
97
+ for (let j = 0; j < CONFIG.vSegments; j++) {
98
+ const vRatio = j / (CONFIG.vSegments - 1);
99
+ // v must avoid 0 (singularity for ln(tan(0)))
100
+ // Range from small epsilon to just under PI/2 (asymptote) or up to 2 (curl back)
101
+ // Standard Dini profile usually 0.1 to 2.0
102
+ const v = 0.05 + vRatio * 1.8;
103
+
104
+ const sinV = Math.sin(v);
105
+ const cosV = Math.cos(v);
106
+ const tanHalfV = Math.tan(v / 2);
107
+
108
+ const x = a * Math.cos(u) * sinV;
109
+ const y = a * Math.sin(u) * sinV;
110
+
111
+ // Handle singularity gracefully
112
+ let term2 = 0;
113
+ if (tanHalfV > 0) {
114
+ term2 = Math.log(tanHalfV);
115
+ }
116
+
117
+ const z = a * (cosV + term2) + b * u;
118
+
119
+ // Center the spiral vertically
120
+ // Approximation: z ranges roughly from a*2 to -infinity?
121
+ // Let's shift it down by half the spiral height
122
+ const zShift = z - (b * uMax * 0.5);
123
+
124
+ this.vertices.push(new Vec3(x, y, zShift));
125
+
126
+ // Indices (Grid)
127
+ if (i < CONFIG.uSegments - 1 && j < CONFIG.vSegments - 1) {
128
+ const current = i * CONFIG.vSegments + j;
129
+ const right = (i + 1) * CONFIG.vSegments + j;
130
+ const down = i * CONFIG.vSegments + (j + 1);
131
+ const diag = (i + 1) * CONFIG.vSegments + (j + 1);
132
+
133
+ // Wireframe style: grid lines
134
+ this.indices.push([current, right]);
135
+ this.indices.push([current, down]);
136
+
137
+ // Optional: Add diagonals for triangulation density
138
+ // this.indices.push([current, diag]);
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ update(dt) {
145
+ this.time += dt;
146
+
147
+ // Update dimensions
148
+ this.width = this.game.width;
149
+ this.height = this.game.height;
150
+
151
+ // Responsive scaling
152
+ const minDim = Math.min(this.width, this.height);
153
+ this.renderScale = minDim / 600; // Adjusted baseline for larger appearance
154
+
155
+ // Smooth rotation
156
+ this.rotation.x += (this.targetRotation.x - this.rotation.x) * 0.1;
157
+ this.rotation.y += (this.targetRotation.y - this.rotation.y) * 0.1;
158
+
159
+ if (this.autoRotate) {
160
+ this.targetRotation.y += dt * 0.2;
161
+ this.targetRotation.x = 0.2 + Math.sin(this.time * 0.3) * 0.1;
162
+ }
163
+
164
+ // Decay energy smoothly toward 0
165
+ this.energy *= 0.95;
166
+ if (this.energy < 0.001) this.energy = 0;
167
+
168
+ // Interpolate noise strength toward equilibrium
169
+ const targetStrength = CONFIG.noiseStrengthBase + this.energy * (CONFIG.noiseStrengthMax - CONFIG.noiseStrengthBase);
170
+ this.noiseStrength += (targetStrength - this.noiseStrength) * 0.1;
171
+ }
172
+
173
+ render(ctx, width, height) {
174
+ const cx = width / 2;
175
+ const cy = height / 2;
176
+
177
+ ctx.fillStyle = CONFIG.bgColor;
178
+ ctx.fillRect(0, 0, width, height);
179
+
180
+ const cosX = Math.cos(this.rotation.x);
181
+ const sinX = Math.sin(this.rotation.x);
182
+ const cosY = Math.cos(this.rotation.y);
183
+ const sinY = Math.sin(this.rotation.y);
184
+
185
+ const projectedVertices = new Array(this.vertices.length);
186
+ const scaleFactor = this.renderScale || 1;
187
+
188
+ // Transform Loop
189
+ for (let i = 0; i < this.vertices.length; i++) {
190
+ const v = this.vertices[i];
191
+
192
+ // Calculate derived polar coords for effect
193
+ const r = Math.sqrt(v.x*v.x + v.y*v.y);
194
+ const angle = Math.atan2(v.y, v.x);
195
+
196
+ // 1. Noise Deformation
197
+ // Flow along the spiral (u direction, roughly Z and Angle)
198
+ const t = this.time * CONFIG.noiseSpeed;
199
+
200
+ // We want the noise to flow "up" the spiral
201
+ const flow = v.z * 0.01 - t;
202
+
203
+ const nX = v.x * CONFIG.noiseScale;
204
+ const nY = v.y * CONFIG.noiseScale;
205
+ const nZ = v.z * CONFIG.noiseScale;
206
+
207
+ const noiseVal = Noise.simplex3(nX + Math.cos(t), nY + Math.sin(t), nZ + flow);
208
+
209
+ // Pulse effect
210
+ const pulse = 1.0 + Math.sin(t * 2 + v.z * 0.02) * 0.1;
211
+
212
+ // Apply deformations
213
+ // Expand/contract radially
214
+ const dr = noiseVal * this.noiseStrength;
215
+
216
+ let dx = v.x + (v.x / (r+0.1)) * dr;
217
+ let dy = v.y + (v.y / (r+0.1)) * dr;
218
+ let dz = v.z * pulse;
219
+
220
+ // 2. Rotation
221
+ let x1 = dx * cosY - dz * sinY;
222
+ let z1 = dz * cosY + dx * sinY;
223
+ let y1 = dy;
224
+
225
+ let y2 = y1 * cosX - z1 * sinX;
226
+ let z2 = z1 * cosX + y1 * sinX;
227
+ let x2 = x1;
228
+
229
+ // 3. Projection
230
+ const s = this.renderScale || 1;
231
+ const sx = x2 * s;
232
+ const sy = y2 * s;
233
+ const sz = z2 * s;
234
+
235
+ const scale = CONFIG.fov / (CONFIG.fov + sz + 400);
236
+ const px = sx * scale + cx;
237
+ const py = sy * scale + cy;
238
+
239
+ projectedVertices[i] = {
240
+ x: px,
241
+ y: py,
242
+ z: sz,
243
+ scale: scale,
244
+ origZ: v.z // Store original Z for gradient coloring
245
+ };
246
+ }
247
+
248
+ // Draw Edges
249
+ const lineWidth = 1.5 * (this.renderScale || 1);
250
+ ctx.lineWidth = lineWidth;
251
+
252
+ // Batch stroke? No, color changes per segment
253
+ for (let i = 0; i < this.indices.length; i++) {
254
+ const [idx1, idx2] = this.indices[i];
255
+ const p1 = projectedVertices[idx1];
256
+ const p2 = projectedVertices[idx2];
257
+
258
+ // Culling
259
+ if (p1.z < -CONFIG.fov + 10 || p2.z < -CONFIG.fov + 10) continue;
260
+
261
+ const avgZ = (p1.z + p2.z) / 2;
262
+ const avgOrigZ = (p1.origZ + p2.origZ) / 2;
263
+
264
+ // Depth fading
265
+ const depthAlpha = Math.max(0.1, Math.min(1, (600 - avgZ) / 800));
266
+
267
+ // Color Gradient along the spiral height
268
+ // Map origZ (-height/2 to height/2) to Hue
269
+ // Height is roughly scale * b * uMax = 60 * 0.15 * 8PI ~= 226
270
+ const heightRange = 300;
271
+ const normalizedH = (avgOrigZ + heightRange/2) / heightRange;
272
+
273
+ // Gold to Red to Purple
274
+ const hue = (CONFIG.baseColor.h + normalizedH * 60) % 360;
275
+ const light = 40 + normalizedH * 40; // Brighter at top
276
+
277
+ ctx.strokeStyle = `hsla(${hue}, ${CONFIG.baseColor.s}%, ${light}%, ${depthAlpha})`;
278
+
279
+ ctx.beginPath();
280
+ ctx.moveTo(p1.x, p1.y);
281
+ ctx.lineTo(p2.x, p2.y);
282
+ ctx.stroke();
283
+ }
284
+ }
285
+ }
286
+
287
+ // Initialize
288
+ window.addEventListener("load", () => {
289
+ const canvas = document.getElementById("game");
290
+ if (!canvas) return;
291
+
292
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
293
+ const gameInstance = game.game;
294
+
295
+ const vortex = new DiniVortex(gameInstance);
296
+
297
+ // Loop
298
+ gameInstance.clear = function() {
299
+ vortex.render(this.ctx, this.width, this.height);
300
+ };
301
+
302
+ game.on("update", (dt) => {
303
+ vortex.update(dt);
304
+ });
305
+
306
+ // Interaction
307
+ let isDragging = false;
308
+ let lastX = 0, lastY = 0;
309
+
310
+ gameInstance.events.on("mousedown", (e) => {
311
+ isDragging = true;
312
+ lastX = e.x;
313
+ lastY = e.y;
314
+ });
315
+
316
+ gameInstance.events.on("mouseup", () => {
317
+ isDragging = false;
318
+ setTimeout(() => { vortex.autoRotate = true; }, 500);
319
+ });
320
+
321
+ gameInstance.events.on("mousemove", (e) => {
322
+ if (isDragging) {
323
+ const dx = (e.x - lastX) * 0.01;
324
+ const dy = (e.y - lastY) * 0.01;
325
+
326
+ vortex.targetRotation.y += dx;
327
+ vortex.targetRotation.x += dy;
328
+ vortex.autoRotate = false;
329
+
330
+ lastX = e.x;
331
+ lastY = e.y;
332
+ }
333
+ });
334
+
335
+ gameInstance.events.on("click", () => {
336
+ if(!isDragging) {
337
+ // Add energy on click
338
+ vortex.energy = Math.min(vortex.energy + 0.4, 1.0);
339
+ CONFIG.baseColor.h = Math.random() * 360;
340
+ }
341
+ });
342
+
343
+ game.start();
344
+ });
345
+
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Hyperbolic 005 - Fractal Terrain
3
+ *
4
+ * Inspired by hyperbolic space - a Julia set rendered as 3D terrain.
5
+ * The iteration counts become elevation, creating fractal mountains.
6
+ * Click to disturb, watch it settle to equilibrium.
7
+ *
8
+ * Features:
9
+ * - Julia set as heightmap
10
+ * - Animated c parameter morphs the fractal
11
+ * - Click to add energy - more clicks = more chaos
12
+ * - Settles to gentle breathing equilibrium
13
+ * - Drag to rotate
14
+ */
15
+
16
+ import { gcanvas, Noise, Fractals } from "/gcanvas.es.min.js";
17
+
18
+ const CONFIG = {
19
+ // Mesh resolution (lower = faster, higher = more detail)
20
+ gridSize: 100,
21
+
22
+ // Julia parameters
23
+ maxIterations: 30,
24
+ cReal: -0.7,
25
+ cImag: 0.27015,
26
+ cAnimSpeed: 0.15,
27
+ cAnimRadius: 0.1,
28
+
29
+ // Height scaling
30
+ heightScale: 200,
31
+ heightScaleBase: 120,
32
+ heightScaleMax: 350,
33
+
34
+ // Noise
35
+ noiseScale: 0.02,
36
+ noiseSpeed: 0.5,
37
+ noiseStrengthBase: 5,
38
+ noiseStrengthMax: 40,
39
+
40
+ // View
41
+ fov: 600,
42
+ baseRotationX: 0.8,
43
+ baseRotationY: 0,
44
+
45
+ // Colors
46
+ baseHue: 280,
47
+ bgColor: "#000",
48
+ };
49
+
50
+ class Vec3 {
51
+ constructor(x, y, z) {
52
+ this.x = x;
53
+ this.y = y;
54
+ this.z = z;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Generate fractal heightmap using library's Fractals class
60
+ */
61
+ function generateFractalHeightmap(size, cReal, cImag, maxIter) {
62
+ // Use the library's Julia set generator
63
+ const rawData = Fractals.julia(
64
+ size,
65
+ size,
66
+ maxIter,
67
+ cReal,
68
+ cImag,
69
+ 1.0, // zoom
70
+ 0, // offsetX
71
+ 0 // offsetY
72
+ );
73
+
74
+ // The library returns iteration counts:
75
+ // - 0 = interior points (never escaped)
76
+ // - 1 to maxIter-1 = escaped after that many iterations
77
+
78
+ const data = new Float32Array(size * size);
79
+
80
+ for (let i = 0; i < rawData.length; i++) {
81
+ const iter = rawData[i];
82
+
83
+ if (iter === 0) {
84
+ // Interior points - these are the "mountains" (highest)
85
+ data[i] = 1.0;
86
+ } else {
87
+ // Escaped points - lower iterations = further from set = valleys
88
+ // Higher iterations = closer to boundary = foothills
89
+ // Use inverse + power curve for dramatic terrain
90
+ const t = iter / maxIter;
91
+ // Invert: high iterations = high terrain (near boundary)
92
+ // Apply power curve for more dramatic relief
93
+ const height = Math.pow(1 - t, 0.5); // sqrt gives more midrange detail
94
+ data[i] = height * 0.9; // Scale to leave room for interior peaks
95
+ }
96
+ }
97
+
98
+ return data;
99
+ }
100
+
101
+ class FractalTerrain {
102
+ constructor(game) {
103
+ this.game = game;
104
+ this.time = 0;
105
+
106
+ this.vertices = [];
107
+ this.indices = [];
108
+ this.heightData = null;
109
+
110
+ this.rotation = { x: CONFIG.baseRotationX, y: 0 };
111
+ this.targetRotation = { x: CONFIG.baseRotationX, y: 0.3 };
112
+ this.autoRotate = true;
113
+
114
+ // Energy system
115
+ this.energy = 0;
116
+ this.noiseStrength = CONFIG.noiseStrengthBase;
117
+ this.heightScale = CONFIG.heightScaleBase;
118
+
119
+ // Animated c parameter
120
+ this.cPhase = 0;
121
+
122
+ this.initMesh();
123
+ }
124
+
125
+ initMesh() {
126
+ const size = CONFIG.gridSize;
127
+ const spacing = 900 / size;
128
+
129
+ this.vertices = [];
130
+ this.indices = [];
131
+
132
+ // Create grid vertices
133
+ for (let y = 0; y < size; y++) {
134
+ for (let x = 0; x < size; x++) {
135
+ const px = (x - size / 2) * spacing;
136
+ const py = (y - size / 2) * spacing;
137
+ this.vertices.push(new Vec3(px, py, 0));
138
+
139
+ // Create edges
140
+ if (x < size - 1) {
141
+ const current = y * size + x;
142
+ const right = y * size + x + 1;
143
+ this.indices.push([current, right]);
144
+ }
145
+ if (y < size - 1) {
146
+ const current = y * size + x;
147
+ const down = (y + 1) * size + x;
148
+ this.indices.push([current, down]);
149
+ }
150
+ }
151
+ }
152
+
153
+ this.regenerateFractal();
154
+ }
155
+
156
+ regenerateFractal() {
157
+ // Animate c parameter in a circle
158
+ const cReal = CONFIG.cReal + Math.cos(this.cPhase) * CONFIG.cAnimRadius;
159
+ const cImag = CONFIG.cImag + Math.sin(this.cPhase) * CONFIG.cAnimRadius;
160
+
161
+ this.heightData = generateFractalHeightmap(
162
+ CONFIG.gridSize,
163
+ cReal,
164
+ cImag,
165
+ CONFIG.maxIterations
166
+ );
167
+ }
168
+
169
+ update(dt) {
170
+ this.time += dt;
171
+
172
+ // Animate c parameter slowly
173
+ this.cPhase += dt * CONFIG.cAnimSpeed;
174
+ this.regenerateFractal();
175
+
176
+ // Responsive scaling
177
+ const minDim = Math.min(this.game.width, this.game.height);
178
+ this.renderScale = minDim / 800;
179
+
180
+ // Smooth rotation
181
+ this.rotation.x += (this.targetRotation.x - this.rotation.x) * 0.08;
182
+ this.rotation.y += (this.targetRotation.y - this.rotation.y) * 0.08;
183
+
184
+ if (this.autoRotate) {
185
+ this.targetRotation.y += dt * 0.15;
186
+ }
187
+
188
+ // Decay energy
189
+ this.energy *= 0.95;
190
+ if (this.energy < 0.001) this.energy = 0;
191
+
192
+ // Interpolate noise and height scale
193
+ const targetNoise = CONFIG.noiseStrengthBase +
194
+ this.energy * (CONFIG.noiseStrengthMax - CONFIG.noiseStrengthBase);
195
+ this.noiseStrength += (targetNoise - this.noiseStrength) * 0.1;
196
+
197
+ const targetHeight = CONFIG.heightScaleBase +
198
+ this.energy * (CONFIG.heightScaleMax - CONFIG.heightScaleBase);
199
+ this.heightScale += (targetHeight - this.heightScale) * 0.1;
200
+ }
201
+
202
+ render(ctx, width, height) {
203
+ const cx = width / 2;
204
+ const cy = height / 2;
205
+ const size = CONFIG.gridSize;
206
+
207
+ ctx.fillStyle = CONFIG.bgColor;
208
+ ctx.fillRect(0, 0, width, height);
209
+
210
+ const cosX = Math.cos(this.rotation.x);
211
+ const sinX = Math.sin(this.rotation.x);
212
+ const cosY = Math.cos(this.rotation.y);
213
+ const sinY = Math.sin(this.rotation.y);
214
+
215
+ const projected = new Array(this.vertices.length);
216
+ const scale = this.renderScale || 1;
217
+ const t = this.time * CONFIG.noiseSpeed;
218
+
219
+ // Transform vertices
220
+ for (let i = 0; i < this.vertices.length; i++) {
221
+ const v = this.vertices[i];
222
+ const h = this.heightData[i];
223
+
224
+ // Base height from fractal
225
+ let z = h * this.heightScale;
226
+
227
+ // Add noise disturbance
228
+ const noiseVal = Noise.simplex3(
229
+ v.x * CONFIG.noiseScale + t,
230
+ v.y * CONFIG.noiseScale,
231
+ t * 0.5
232
+ );
233
+ z += noiseVal * this.noiseStrength;
234
+
235
+ // Rotate Y
236
+ let x1 = v.x * cosY - z * sinY;
237
+ let z1 = z * cosY + v.x * sinY;
238
+ let y1 = v.y;
239
+
240
+ // Rotate X
241
+ let y2 = y1 * cosX - z1 * sinX;
242
+ let z2 = z1 * cosX + y1 * sinX;
243
+ let x2 = x1;
244
+
245
+ // Scale and project
246
+ const sx = x2 * scale;
247
+ const sy = y2 * scale;
248
+ const sz = z2 * scale;
249
+
250
+ const projScale = CONFIG.fov / (CONFIG.fov + sz + 300);
251
+ const px = sx * projScale + cx;
252
+ const py = sy * projScale + cy;
253
+
254
+ projected[i] = { x: px, y: py, z: sz, h, scale: projScale };
255
+ }
256
+
257
+ // Draw edges
258
+ ctx.lineWidth = 1.5 * scale;
259
+
260
+ for (const [i1, i2] of this.indices) {
261
+ const p1 = projected[i1];
262
+ const p2 = projected[i2];
263
+
264
+ if (p1.z < -CONFIG.fov + 50 || p2.z < -CONFIG.fov + 50) continue;
265
+
266
+ const avgZ = (p1.z + p2.z) / 2;
267
+ const avgH = (p1.h + p2.h) / 2;
268
+
269
+ // Depth alpha
270
+ const depthAlpha = Math.max(0.1, Math.min(1, (400 - avgZ) / 600));
271
+
272
+ // Color based on height
273
+ const hue = (CONFIG.baseHue + avgH * 120 + this.time * 20) % 360;
274
+ const light = 30 + avgH * 50;
275
+
276
+ ctx.strokeStyle = `hsla(${hue}, 90%, ${light}%, ${depthAlpha})`;
277
+ ctx.beginPath();
278
+ ctx.moveTo(p1.x, p1.y);
279
+ ctx.lineTo(p2.x, p2.y);
280
+ ctx.stroke();
281
+ }
282
+ }
283
+ }
284
+
285
+ // Initialize
286
+ window.addEventListener("load", () => {
287
+ const canvas = document.getElementById("game");
288
+ if (!canvas) return;
289
+
290
+ const game = gcanvas({ canvas, bg: CONFIG.bgColor, fluid: true });
291
+ const gameInstance = game.game;
292
+
293
+ const terrain = new FractalTerrain(gameInstance);
294
+
295
+ gameInstance.clear = function () {
296
+ terrain.render(this.ctx, this.width, this.height);
297
+ };
298
+
299
+ game.on("update", (dt) => {
300
+ terrain.update(dt);
301
+ });
302
+
303
+ // Interaction
304
+ let isDragging = false;
305
+ let lastX = 0, lastY = 0;
306
+
307
+ gameInstance.events.on("mousedown", (e) => {
308
+ isDragging = true;
309
+ lastX = e.x;
310
+ lastY = e.y;
311
+ });
312
+
313
+ gameInstance.events.on("mouseup", () => {
314
+ isDragging = false;
315
+ setTimeout(() => { terrain.autoRotate = true; }, 500);
316
+ });
317
+
318
+ gameInstance.events.on("mousemove", (e) => {
319
+ if (isDragging) {
320
+ const dx = (e.x - lastX) * 0.01;
321
+ const dy = (e.y - lastY) * 0.01;
322
+
323
+ terrain.targetRotation.y += dx;
324
+ terrain.targetRotation.x += dy;
325
+ terrain.autoRotate = false;
326
+
327
+ lastX = e.x;
328
+ lastY = e.y;
329
+ }
330
+ });
331
+
332
+ gameInstance.events.on("click", () => {
333
+ if (!isDragging) {
334
+ terrain.energy = Math.min(terrain.energy + 0.4, 1.0);
335
+ CONFIG.baseHue = Math.random() * 360;
336
+ }
337
+ });
338
+
339
+ game.start();
340
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Auto-injects a toggle button for #info overlay on mobile devices.
3
+ * Include this script in demo pages that have an #info element.
4
+ */
5
+ (function() {
6
+ document.addEventListener("DOMContentLoaded", function() {
7
+ const info = document.getElementById("info");
8
+ if (!info) return;
9
+
10
+ // Create toggle button
11
+ const toggle = document.createElement("button");
12
+ toggle.id = "info-toggle";
13
+ toggle.textContent = "?";
14
+ toggle.setAttribute("aria-label", "Toggle info panel");
15
+
16
+ // Insert button before info element
17
+ info.parentNode.insertBefore(toggle, info);
18
+
19
+ // Toggle functionality
20
+ toggle.addEventListener("click", function() {
21
+ info.classList.toggle("open");
22
+ toggle.textContent = info.classList.contains("open") ? "×" : "?";
23
+ });
24
+ });
25
+ })();