@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,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
|
+
|