@guinetik/gcanvas 1.0.5 → 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.
- package/dist/aizawa.html +27 -0
- package/dist/clifford.html +25 -0
- package/dist/cmb.html +24 -0
- package/dist/dadras.html +26 -0
- package/dist/dejong.html +25 -0
- package/dist/gcanvas.es.js +5130 -372
- 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/halvorsen.html +27 -0
- package/dist/index.html +96 -48
- package/dist/js/aizawa.js +425 -0
- package/dist/js/bezier.js +5 -5
- package/dist/js/clifford.js +236 -0
- package/dist/js/cmb.js +594 -0
- package/dist/js/dadras.js +405 -0
- package/dist/js/dejong.js +257 -0
- package/dist/js/halvorsen.js +405 -0
- package/dist/js/isometric.js +34 -46
- package/dist/js/lorenz.js +425 -0
- package/dist/js/painter.js +8 -8
- package/dist/js/rossler.js +480 -0
- package/dist/js/schrodinger.js +314 -18
- package/dist/js/thomas.js +394 -0
- package/dist/lorenz.html +27 -0
- package/dist/rossler.html +27 -0
- package/dist/scene-interactivity-test.html +220 -0
- package/dist/thomas.html +27 -0
- package/package.json +1 -1
- package/readme.md +30 -22
- package/src/game/objects/go.js +7 -0
- package/src/game/objects/index.js +2 -0
- package/src/game/objects/isometric-scene.js +53 -3
- package/src/game/objects/layoutscene.js +57 -0
- package/src/game/objects/mask.js +241 -0
- package/src/game/objects/scene.js +19 -0
- package/src/game/objects/wrapper.js +14 -2
- package/src/game/pipeline.js +17 -0
- package/src/game/ui/button.js +101 -16
- package/src/game/ui/theme.js +0 -6
- package/src/game/ui/togglebutton.js +25 -14
- package/src/game/ui/tooltip.js +12 -4
- package/src/index.js +3 -0
- package/src/io/gesture.js +409 -0
- package/src/io/index.js +4 -1
- package/src/io/keys.js +9 -1
- package/src/io/screen.js +476 -0
- package/src/math/attractors.js +664 -0
- package/src/math/heat.js +106 -0
- package/src/math/index.js +1 -0
- package/src/mixins/draggable.js +15 -19
- package/src/painter/painter.shapes.js +11 -5
- package/src/particle/particle-system.js +165 -1
- package/src/physics/index.js +26 -0
- package/src/physics/physics-updaters.js +333 -0
- package/src/physics/physics.js +375 -0
- package/src/shapes/image.js +5 -5
- package/src/shapes/index.js +2 -0
- package/src/shapes/parallelogram.js +147 -0
- package/src/shapes/righttriangle.js +115 -0
- package/src/shapes/svg.js +281 -100
- package/src/shapes/text.js +22 -6
- package/src/shapes/transformable.js +5 -0
- package/src/sound/effects.js +807 -0
- package/src/sound/index.js +13 -0
- package/src/webgl/index.js +7 -0
- package/src/webgl/shaders/clifford-point-shaders.js +131 -0
- package/src/webgl/shaders/dejong-point-shaders.js +131 -0
- package/src/webgl/shaders/point-sprite-shaders.js +152 -0
- package/src/webgl/webgl-clifford-renderer.js +477 -0
- package/src/webgl/webgl-dejong-renderer.js +472 -0
- package/src/webgl/webgl-line-renderer.js +391 -0
- package/src/webgl/webgl-particle-renderer.js +410 -0
- package/types/index.d.ts +30 -2
- package/types/io.d.ts +217 -0
- package/types/physics.d.ts +299 -0
- package/types/shapes.d.ts +8 -0
- package/types/webgl.d.ts +188 -109
package/dist/js/cmb.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cosmic Microwave Background Visualization
|
|
3
|
+
*
|
|
4
|
+
* A 3D visualization of the Cosmic Microwave Background (CMB) radiation,
|
|
5
|
+
* the oldest light in the universe. Uses WebGL particle rendering and
|
|
6
|
+
* Camera3D for 3D projection with physics-based thermal motion.
|
|
7
|
+
*
|
|
8
|
+
* The CMB shows tiny temperature fluctuations (anisotropies) that seeded
|
|
9
|
+
* the formation of galaxies. Temperature is ~2.725K with variations of ~0.00001K.
|
|
10
|
+
*
|
|
11
|
+
* Color mapping: Blue (cold) → White (average) → Red (hot)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
Game,
|
|
16
|
+
ParticleSystem,
|
|
17
|
+
Updaters,
|
|
18
|
+
PhysicsUpdaters,
|
|
19
|
+
Camera3D,
|
|
20
|
+
Noise,
|
|
21
|
+
Painter,
|
|
22
|
+
Gesture,
|
|
23
|
+
Tweenetik,
|
|
24
|
+
Easing,
|
|
25
|
+
Screen,
|
|
26
|
+
applyParticleHeatTransfer,
|
|
27
|
+
} from "/gcanvas.es.min.js";
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// CONFIGURATION
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get responsive configuration based on screen size
|
|
35
|
+
*/
|
|
36
|
+
function getResponsiveConfig() {
|
|
37
|
+
// Sphere radius based on smaller screen dimension (fills ~80% of screen)
|
|
38
|
+
const minDim = Screen.minDimension();
|
|
39
|
+
const sphereRadius = minDim * 0.4;
|
|
40
|
+
|
|
41
|
+
// Particle count scales with screen area (more particles = denser CMB)
|
|
42
|
+
// Mobile: ~1500, Tablet: ~2500, Desktop: ~4000
|
|
43
|
+
const numParticles = Screen.responsive(1500, 2500, 4000);
|
|
44
|
+
|
|
45
|
+
return { sphereRadius, numParticles };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CONFIG = {
|
|
49
|
+
// Particle count and distribution (set dynamically in init)
|
|
50
|
+
numParticles: 3000, // default, overridden by getResponsiveConfig()
|
|
51
|
+
sphereRadius: 400, // default, overridden by getResponsiveConfig()
|
|
52
|
+
|
|
53
|
+
// CMB temperature (Kelvin)
|
|
54
|
+
baseTemperature: 2.725,
|
|
55
|
+
temperatureVariation: 0.0001, // ±100 μK
|
|
56
|
+
|
|
57
|
+
// Visual settings
|
|
58
|
+
particleSize: 3,
|
|
59
|
+
particleSizeVariation: 2,
|
|
60
|
+
|
|
61
|
+
// Camera
|
|
62
|
+
perspective: 600,
|
|
63
|
+
autoRotateSpeed: 0.1,
|
|
64
|
+
cameraDistance: 0,
|
|
65
|
+
|
|
66
|
+
// Zoom (position scaling - fly into the CMB)
|
|
67
|
+
minZoom: 0.5, // zoomed out
|
|
68
|
+
maxZoom: 4.0, // zoomed in (inside the sphere)
|
|
69
|
+
zoomSmoothing: 0.12, // interpolation speed
|
|
70
|
+
|
|
71
|
+
// Big Bang animation
|
|
72
|
+
bigBang: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
initialZoom: 0.05, // start tiny
|
|
75
|
+
targetZoom: 1.0, // expand to full size
|
|
76
|
+
zoomDuration: 1.5, // seconds to expand (faster)
|
|
77
|
+
spawnDelay: 0.0001, // delay between particle spawns (ultra fast burst)
|
|
78
|
+
explosionForce: 800, // outward velocity (stronger)
|
|
79
|
+
flashDuration: 0.3, // white flash duration (quicker fade)
|
|
80
|
+
flashHoldTime: 0.05, // hold at full white (shorter)
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Physics - thermal motion
|
|
84
|
+
thermalMotion: 0.2,
|
|
85
|
+
thermalScale: 5,
|
|
86
|
+
sphereRestitution: 0.98,
|
|
87
|
+
|
|
88
|
+
// Heat transfer between particles (disabled for performance)
|
|
89
|
+
heatTransferEnabled: false,
|
|
90
|
+
heatTransferRate: 0.005,
|
|
91
|
+
heatTransferDistance: 30,
|
|
92
|
+
|
|
93
|
+
// Noise for temperature distribution
|
|
94
|
+
noiseScale: 0.015,
|
|
95
|
+
noiseOctaves: 3,
|
|
96
|
+
|
|
97
|
+
// Rendering
|
|
98
|
+
useWebGL: false,
|
|
99
|
+
blendMode: "screen",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
// HELPER FUNCTIONS
|
|
104
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generate a random point on a sphere surface
|
|
108
|
+
*/
|
|
109
|
+
function randomSpherePoint(radius) {
|
|
110
|
+
// Use spherical coordinates for uniform distribution
|
|
111
|
+
const theta = Math.random() * Math.PI * 2;
|
|
112
|
+
const phi = Math.acos(2 * Math.random() - 1);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
x: radius * Math.sin(phi) * Math.cos(theta),
|
|
116
|
+
y: radius * Math.sin(phi) * Math.sin(theta),
|
|
117
|
+
z: radius * Math.cos(phi),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate CMB temperature using 3D noise
|
|
123
|
+
* Returns normalized value from 0 to 1 (for heat.js compatibility)
|
|
124
|
+
*/
|
|
125
|
+
function getCMBTemperature(x, y, z) {
|
|
126
|
+
// Use multiple octaves of noise for realistic power spectrum
|
|
127
|
+
let temp = 0;
|
|
128
|
+
let amplitude = 1;
|
|
129
|
+
let frequency = CONFIG.noiseScale;
|
|
130
|
+
let maxValue = 0;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < CONFIG.noiseOctaves; i++) {
|
|
133
|
+
// 3D noise sampled at position
|
|
134
|
+
temp += Noise.perlin3(
|
|
135
|
+
x * frequency,
|
|
136
|
+
y * frequency,
|
|
137
|
+
z * frequency
|
|
138
|
+
) * amplitude;
|
|
139
|
+
|
|
140
|
+
maxValue += amplitude;
|
|
141
|
+
amplitude *= 0.5;
|
|
142
|
+
frequency *= 2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Normalize to 0 to 1
|
|
146
|
+
return (temp / maxValue + 1) * 0.5;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Map temperature (0-1) to color
|
|
151
|
+
* Planck CMB palette (based on actual satellite imagery):
|
|
152
|
+
* 0 (cold) = Deep Blue
|
|
153
|
+
* 0.5 = Light tan/cream (neutral)
|
|
154
|
+
* 1 (hot) = Deep Orange/Red-Orange
|
|
155
|
+
*/
|
|
156
|
+
function temperatureToColor(t) {
|
|
157
|
+
// Clamp
|
|
158
|
+
t = Math.max(0, Math.min(1, t));
|
|
159
|
+
|
|
160
|
+
let r, g, b;
|
|
161
|
+
|
|
162
|
+
if (t < 0.35) {
|
|
163
|
+
// Cold: Deep Blue to Light Blue
|
|
164
|
+
const s = t / 0.35; // 0 to 1
|
|
165
|
+
r = Math.floor(30 + 100 * s); // 30 → 130
|
|
166
|
+
g = Math.floor(80 + 120 * s); // 80 → 200
|
|
167
|
+
b = Math.floor(200 + 55 * s); // 200 → 255
|
|
168
|
+
} else if (t < 0.65) {
|
|
169
|
+
// Middle: Light Blue to Light Tan/Cream (neutral zone)
|
|
170
|
+
const s = (t - 0.35) / 0.3; // 0 to 1
|
|
171
|
+
r = Math.floor(130 + 100 * s); // 130 → 230
|
|
172
|
+
g = Math.floor(200 - 10 * s); // 200 → 190
|
|
173
|
+
b = Math.floor(255 - 120 * s); // 255 → 135
|
|
174
|
+
} else {
|
|
175
|
+
// Hot: Tan to Deep Orange
|
|
176
|
+
const s = (t - 0.65) / 0.35; // 0 to 1
|
|
177
|
+
r = Math.floor(230 + 25 * s); // 230 → 255
|
|
178
|
+
g = Math.floor(190 - 110 * s); // 190 → 80
|
|
179
|
+
b = Math.floor(135 - 135 * s); // 135 → 0
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { r, g, b, a: 0.95 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Update particle color based on temperature
|
|
187
|
+
*/
|
|
188
|
+
function updateParticleColor(p) {
|
|
189
|
+
const color = temperatureToColor(p.custom.temperature);
|
|
190
|
+
p.color.r = color.r;
|
|
191
|
+
p.color.g = color.g;
|
|
192
|
+
p.color.b = color.b;
|
|
193
|
+
p.color.a = color.a;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
197
|
+
// CUSTOM UPDATER - Color from temperature
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
const colorFromTemperature = (p, dt) => {
|
|
201
|
+
if (!p.alive || p.custom.temperature === undefined) return;
|
|
202
|
+
updateParticleColor(p);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// CMB DEMO CLASS
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
class CMBDemo extends Game {
|
|
210
|
+
constructor(canvas) {
|
|
211
|
+
super(canvas);
|
|
212
|
+
this.backgroundColor = "#000008";
|
|
213
|
+
this.enableFluidSize();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
init() {
|
|
217
|
+
super.init();
|
|
218
|
+
|
|
219
|
+
// Initialize Screen for responsive sizing
|
|
220
|
+
Screen.init(this);
|
|
221
|
+
|
|
222
|
+
// Apply responsive configuration
|
|
223
|
+
const responsive = getResponsiveConfig();
|
|
224
|
+
CONFIG.sphereRadius = responsive.sphereRadius;
|
|
225
|
+
CONFIG.numParticles = responsive.numParticles;
|
|
226
|
+
|
|
227
|
+
console.log(`Screen: ${Screen.width}x${Screen.height} | ${Screen.isMobile ? 'Mobile' : Screen.isTablet ? 'Tablet' : 'Desktop'}`);
|
|
228
|
+
console.log(`CMB Config: radius=${CONFIG.sphereRadius.toFixed(0)}, particles=${CONFIG.numParticles}`);
|
|
229
|
+
|
|
230
|
+
// Initialize noise
|
|
231
|
+
Noise.seed(42);
|
|
232
|
+
|
|
233
|
+
// Create 3D camera (unclamped for full rotation)
|
|
234
|
+
this.camera = new Camera3D({
|
|
235
|
+
perspective: CONFIG.perspective,
|
|
236
|
+
viewWidth: this.width,
|
|
237
|
+
viewHeight: this.height,
|
|
238
|
+
inertia: true,
|
|
239
|
+
friction: 0.95,
|
|
240
|
+
clampX: false,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Enable mouse/touch controls for rotation
|
|
244
|
+
this.camera.enableMouseControl(this.canvas);
|
|
245
|
+
|
|
246
|
+
// Big Bang state
|
|
247
|
+
const bb = CONFIG.bigBang;
|
|
248
|
+
this.bigBangActive = bb.enabled;
|
|
249
|
+
this.flashOpacity = bb.enabled ? 1.0 : 0;
|
|
250
|
+
this.spawnQueue = [];
|
|
251
|
+
this.spawnTimer = 0;
|
|
252
|
+
|
|
253
|
+
// Zoom state - start tiny for Big Bang
|
|
254
|
+
this.zoom = bb.enabled ? bb.initialZoom : 1.0;
|
|
255
|
+
this.targetZoom = bb.enabled ? bb.initialZoom : 1.0;
|
|
256
|
+
|
|
257
|
+
// Animate zoom expansion for Big Bang
|
|
258
|
+
if (bb.enabled) {
|
|
259
|
+
// Flash fade out after hold time
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
Tweenetik.to(this, { flashOpacity: 0 }, bb.flashDuration, Easing.easeOutQuad);
|
|
262
|
+
}, bb.flashHoldTime * 1000);
|
|
263
|
+
|
|
264
|
+
// Zoom expansion
|
|
265
|
+
Tweenetik.to(this, { targetZoom: bb.targetZoom }, bb.zoomDuration, Easing.easeOutCubic);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Gesture handler for zoom (wheel + pinch)
|
|
269
|
+
this.gesture = new Gesture(this.canvas, {
|
|
270
|
+
onZoom: (delta) => {
|
|
271
|
+
// delta > 0 = zoom in, delta < 0 = zoom out
|
|
272
|
+
const factor = delta > 0 ? 1.15 : 0.87;
|
|
273
|
+
this.targetZoom = Math.max(
|
|
274
|
+
CONFIG.minZoom,
|
|
275
|
+
Math.min(CONFIG.maxZoom, this.targetZoom * factor)
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Initial camera rotation for nice view
|
|
281
|
+
this.camera.rotationX = 0.2;
|
|
282
|
+
this.camera.rotationY = 0;
|
|
283
|
+
this.camera.z = CONFIG.cameraDistance;
|
|
284
|
+
|
|
285
|
+
// Zoom attract updater - particles attracted to their target positions scaled by zoom
|
|
286
|
+
const zoomAttract = (p, dt) => {
|
|
287
|
+
if (!p.alive) return;
|
|
288
|
+
const zoom = this.zoom;
|
|
289
|
+
const strength = 6 * dt;
|
|
290
|
+
const damping = 0.94;
|
|
291
|
+
|
|
292
|
+
// Attract to scaled target position
|
|
293
|
+
const dx = p.custom.targetX * zoom - p.x;
|
|
294
|
+
const dy = p.custom.targetY * zoom - p.y;
|
|
295
|
+
const dz = p.custom.targetZ * zoom - p.z;
|
|
296
|
+
|
|
297
|
+
p.vx = (p.vx + dx * strength) * damping;
|
|
298
|
+
p.vy = (p.vy + dy * strength) * damping;
|
|
299
|
+
p.vz = (p.vz + dz * strength) * damping;
|
|
300
|
+
|
|
301
|
+
p.x += p.vx * dt;
|
|
302
|
+
p.y += p.vy * dt;
|
|
303
|
+
p.z += p.vz * dt;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Create particle system
|
|
307
|
+
this.particles = new ParticleSystem(this, {
|
|
308
|
+
maxParticles: CONFIG.numParticles + 100,
|
|
309
|
+
camera: this.camera,
|
|
310
|
+
depthSort: true,
|
|
311
|
+
useWebGL: CONFIG.useWebGL,
|
|
312
|
+
blendMode: CONFIG.blendMode,
|
|
313
|
+
|
|
314
|
+
// Updaters for particle behavior
|
|
315
|
+
updaters: [
|
|
316
|
+
zoomAttract,
|
|
317
|
+
PhysicsUpdaters.thermal(() => CONFIG.thermalMotion, CONFIG.thermalScale),
|
|
318
|
+
colorFromTemperature,
|
|
319
|
+
],
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Add to pipeline
|
|
323
|
+
this.pipeline.add(this.particles);
|
|
324
|
+
|
|
325
|
+
// Auto-rotation
|
|
326
|
+
this.autoRotate = true;
|
|
327
|
+
this.time = 0;
|
|
328
|
+
|
|
329
|
+
// Create CMB particles - Big Bang spawns from center with delay
|
|
330
|
+
if (CONFIG.bigBang.enabled) {
|
|
331
|
+
this.prepareBigBangParticles();
|
|
332
|
+
} else {
|
|
333
|
+
this.createCMBParticles();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log(`CMB Demo initialized`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Prepare particles for Big Bang - queue them for staggered spawn
|
|
341
|
+
*/
|
|
342
|
+
prepareBigBangParticles() {
|
|
343
|
+
for (let i = 0; i < CONFIG.numParticles; i++) {
|
|
344
|
+
// Calculate target position on sphere
|
|
345
|
+
const pos = randomSpherePoint(CONFIG.sphereRadius);
|
|
346
|
+
const temp = getCMBTemperature(pos.x, pos.y, pos.z);
|
|
347
|
+
|
|
348
|
+
// Queue particle data for staggered spawning
|
|
349
|
+
this.spawnQueue.push({
|
|
350
|
+
targetX: pos.x,
|
|
351
|
+
targetY: pos.y,
|
|
352
|
+
targetZ: pos.z,
|
|
353
|
+
temperature: temp,
|
|
354
|
+
spawnTime: i * CONFIG.bigBang.spawnDelay,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
console.log(`Big Bang: ${this.spawnQueue.length} particles queued`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Spawn a single Big Bang particle from center
|
|
362
|
+
*/
|
|
363
|
+
spawnBigBangParticle(data) {
|
|
364
|
+
if (this.particles.particles.length >= this.particles.maxParticles) return;
|
|
365
|
+
|
|
366
|
+
const p = this.particles.acquire();
|
|
367
|
+
const bb = CONFIG.bigBang;
|
|
368
|
+
|
|
369
|
+
// Target position on sphere surface
|
|
370
|
+
p.custom.targetX = data.targetX;
|
|
371
|
+
p.custom.targetY = data.targetY;
|
|
372
|
+
p.custom.targetZ = data.targetZ;
|
|
373
|
+
p.custom.temperature = data.temperature;
|
|
374
|
+
|
|
375
|
+
// Start at center (the singularity)
|
|
376
|
+
p.x = 0;
|
|
377
|
+
p.y = 0;
|
|
378
|
+
p.z = 0;
|
|
379
|
+
|
|
380
|
+
// Explosion velocity - outward toward target
|
|
381
|
+
const dist = Math.sqrt(data.targetX ** 2 + data.targetY ** 2 + data.targetZ ** 2);
|
|
382
|
+
const nx = data.targetX / dist;
|
|
383
|
+
const ny = data.targetY / dist;
|
|
384
|
+
const nz = data.targetZ / dist;
|
|
385
|
+
|
|
386
|
+
// Random variation in explosion force
|
|
387
|
+
const force = bb.explosionForce * (0.8 + Math.random() * 0.4);
|
|
388
|
+
p.vx = nx * force;
|
|
389
|
+
p.vy = ny * force;
|
|
390
|
+
p.vz = nz * force;
|
|
391
|
+
|
|
392
|
+
// Color from temperature
|
|
393
|
+
updateParticleColor(p);
|
|
394
|
+
|
|
395
|
+
// Size
|
|
396
|
+
p.size = CONFIG.particleSize + data.temperature * CONFIG.particleSizeVariation;
|
|
397
|
+
p.shape = "circle";
|
|
398
|
+
|
|
399
|
+
// Lifecycle
|
|
400
|
+
p.lifetime = 999999;
|
|
401
|
+
p.age = 0;
|
|
402
|
+
p.alive = true;
|
|
403
|
+
|
|
404
|
+
this.particles.particles.push(p);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create CMB particles instantly (non-Big Bang mode)
|
|
409
|
+
*/
|
|
410
|
+
createCMBParticles() {
|
|
411
|
+
for (let i = 0; i < CONFIG.numParticles; i++) {
|
|
412
|
+
// Check if we've hit max
|
|
413
|
+
if (this.particles.particles.length >= this.particles.maxParticles) {
|
|
414
|
+
console.warn(`Hit max particles at ${i}`);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Get particle from pool
|
|
419
|
+
const p = this.particles.acquire();
|
|
420
|
+
|
|
421
|
+
// Position on sphere surface (store as target for zoom)
|
|
422
|
+
const pos = randomSpherePoint(CONFIG.sphereRadius);
|
|
423
|
+
p.custom.targetX = pos.x;
|
|
424
|
+
p.custom.targetY = pos.y;
|
|
425
|
+
p.custom.targetZ = pos.z;
|
|
426
|
+
|
|
427
|
+
// Initial position matches target
|
|
428
|
+
p.x = pos.x;
|
|
429
|
+
p.y = pos.y;
|
|
430
|
+
p.z = pos.z;
|
|
431
|
+
|
|
432
|
+
// Start with zero velocity (attract will handle motion)
|
|
433
|
+
p.vx = 0;
|
|
434
|
+
p.vy = 0;
|
|
435
|
+
p.vz = 0;
|
|
436
|
+
|
|
437
|
+
// CMB temperature at this position (0-1 range)
|
|
438
|
+
const temp = getCMBTemperature(pos.x, pos.y, pos.z);
|
|
439
|
+
p.custom.temperature = temp;
|
|
440
|
+
|
|
441
|
+
// Initial color based on temperature
|
|
442
|
+
updateParticleColor(p);
|
|
443
|
+
|
|
444
|
+
// Size varies slightly with temperature (hotter = slightly larger)
|
|
445
|
+
p.size = CONFIG.particleSize + temp * CONFIG.particleSizeVariation;
|
|
446
|
+
|
|
447
|
+
// Shape
|
|
448
|
+
p.shape = "circle";
|
|
449
|
+
|
|
450
|
+
// Long lifetime (effectively permanent)
|
|
451
|
+
p.lifetime = 999999;
|
|
452
|
+
p.age = 0;
|
|
453
|
+
p.alive = true;
|
|
454
|
+
|
|
455
|
+
// Add to active particles array
|
|
456
|
+
this.particles.particles.push(p);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
update(dt) {
|
|
461
|
+
super.update(dt);
|
|
462
|
+
|
|
463
|
+
this.time += dt;
|
|
464
|
+
|
|
465
|
+
// Update tweens (Big Bang animations)
|
|
466
|
+
Tweenetik.updateAll(dt);
|
|
467
|
+
|
|
468
|
+
// Big Bang particle spawning
|
|
469
|
+
if (this.spawnQueue.length > 0) {
|
|
470
|
+
this.spawnTimer += dt;
|
|
471
|
+
|
|
472
|
+
// Spawn all particles whose time has come
|
|
473
|
+
while (this.spawnQueue.length > 0 && this.spawnQueue[0].spawnTime <= this.spawnTimer) {
|
|
474
|
+
this.spawnBigBangParticle(this.spawnQueue.shift());
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Smooth zoom interpolation (fly into the CMB)
|
|
479
|
+
this.zoom += (this.targetZoom - this.zoom) * CONFIG.zoomSmoothing;
|
|
480
|
+
|
|
481
|
+
// Auto-rotate camera when not being dragged
|
|
482
|
+
if (this.autoRotate && !this.camera.isDragging()) {
|
|
483
|
+
this.camera.rotationY += CONFIG.autoRotateSpeed * dt;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Update camera dimensions
|
|
487
|
+
this.camera.viewWidth = this.width;
|
|
488
|
+
this.camera.viewHeight = this.height;
|
|
489
|
+
this.camera.update(dt);
|
|
490
|
+
|
|
491
|
+
// Apply heat transfer between nearby particles (from heat.js)
|
|
492
|
+
if (CONFIG.heatTransferEnabled) {
|
|
493
|
+
applyParticleHeatTransfer(this.particles.particles, {
|
|
494
|
+
maxDistance: CONFIG.heatTransferDistance,
|
|
495
|
+
rate: CONFIG.heatTransferRate,
|
|
496
|
+
falloff: 1,
|
|
497
|
+
temperatureKey: 'temperature',
|
|
498
|
+
filter: (p) => p.alive,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
render() {
|
|
504
|
+
super.render();
|
|
505
|
+
|
|
506
|
+
// Draw Big Bang flash overlay
|
|
507
|
+
if (this.flashOpacity > 0.01) {
|
|
508
|
+
this.ctx.fillStyle = `rgba(255, 255, 255, ${this.flashOpacity})`;
|
|
509
|
+
this.ctx.fillRect(0, 0, this.width, this.height);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Draw info overlay
|
|
513
|
+
this.drawOverlay();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
drawOverlay() {
|
|
517
|
+
const ctx = this.ctx;
|
|
518
|
+
|
|
519
|
+
// Temperature scale legend
|
|
520
|
+
const legendX = 20;
|
|
521
|
+
const legendY = this.height - 80;
|
|
522
|
+
const legendWidth = 150;
|
|
523
|
+
const legendHeight = 15;
|
|
524
|
+
|
|
525
|
+
// Draw gradient bar (matches temperatureToColor - Planck CMB palette)
|
|
526
|
+
const gradient = ctx.createLinearGradient(legendX, legendY, legendX + legendWidth, legendY);
|
|
527
|
+
gradient.addColorStop(0, 'rgb(30, 80, 200)'); // Deep blue
|
|
528
|
+
gradient.addColorStop(0.35, 'rgb(130, 200, 255)'); // Light blue
|
|
529
|
+
gradient.addColorStop(0.5, 'rgb(230, 190, 135)'); // Light tan/cream
|
|
530
|
+
gradient.addColorStop(1, 'rgb(255, 80, 0)'); // Deep orange
|
|
531
|
+
|
|
532
|
+
ctx.fillStyle = gradient;
|
|
533
|
+
ctx.fillRect(legendX, legendY, legendWidth, legendHeight);
|
|
534
|
+
|
|
535
|
+
// Border
|
|
536
|
+
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
|
|
537
|
+
ctx.lineWidth = 1;
|
|
538
|
+
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight);
|
|
539
|
+
|
|
540
|
+
// Labels
|
|
541
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
542
|
+
ctx.font = '10px monospace';
|
|
543
|
+
ctx.textAlign = 'center';
|
|
544
|
+
ctx.fillText('Cold', legendX + 20, legendY + legendHeight + 12);
|
|
545
|
+
ctx.fillText('Hot', legendX + legendWidth - 20, legendY + legendHeight + 12);
|
|
546
|
+
|
|
547
|
+
// Title
|
|
548
|
+
ctx.textAlign = 'left';
|
|
549
|
+
ctx.fillText('Temperature Anisotropy', legendX, legendY - 5);
|
|
550
|
+
|
|
551
|
+
// Stats
|
|
552
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
553
|
+
ctx.font = '11px monospace';
|
|
554
|
+
const statsY = this.height - 120;
|
|
555
|
+
ctx.fillText(`Particles: ${this.particles.particleCount}`, legendX, statsY);
|
|
556
|
+
ctx.fillText(`T₀ = ${CONFIG.baseTemperature} K`, legendX, statsY - 15);
|
|
557
|
+
ctx.fillText(`δT ≈ ±${(CONFIG.baseTemperature * CONFIG.temperatureVariation * 1e6).toFixed(0)} μK`, legendX, statsY - 30);
|
|
558
|
+
|
|
559
|
+
// Zoom level
|
|
560
|
+
ctx.fillText(`Zoom: ${this.zoom.toFixed(1)}x`, legendX, statsY - 45);
|
|
561
|
+
|
|
562
|
+
// Controls hint
|
|
563
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
|
564
|
+
ctx.font = '10px monospace';
|
|
565
|
+
ctx.fillText('Drag to rotate • Scroll to zoom', legendX, this.height - 15);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
stop() {
|
|
569
|
+
super.stop();
|
|
570
|
+
if (this.gesture) {
|
|
571
|
+
this.gesture.destroy();
|
|
572
|
+
}
|
|
573
|
+
if (this.particles) {
|
|
574
|
+
this.particles.destroy();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
onResize() {
|
|
579
|
+
if (this.camera) {
|
|
580
|
+
this.camera.viewWidth = this.width;
|
|
581
|
+
this.camera.viewHeight = this.height;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
587
|
+
// INITIALIZATION
|
|
588
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
window.addEventListener("load", () => {
|
|
591
|
+
const canvas = document.getElementById("game");
|
|
592
|
+
const demo = new CMBDemo(canvas);
|
|
593
|
+
demo.start();
|
|
594
|
+
});
|