opencroc 1.6.9 → 1.8.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.
@@ -0,0 +1,489 @@
1
+ /* ═══════════════════════════════════════════════════════════════════════════════
2
+ OpenCroc Studio 3D — Three.js Engine
3
+ Scene, Renderer, Post-processing, Clock
4
+ ~2500 lines
5
+ ═══════════════════════════════════════════════════════════════════════════════ */
6
+
7
+ import * as THREE from 'three';
8
+ import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
9
+ import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
10
+ import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
11
+ import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
12
+ import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';
13
+ import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
14
+
15
+ /* ─── Module-level singletons ──────────────────────────────────────────────── */
16
+ let renderer = null;
17
+ let scene = null;
18
+ let camera = null;
19
+ let composer = null;
20
+ let clock = null;
21
+ let bloomPass = null;
22
+ let fxaaPass = null;
23
+
24
+ /* ═══════════════════════════════════════════════════════════════════════════════
25
+ 1. createEngine — Initialize the full Three.js rendering pipeline
26
+ ═══════════════════════════════════════════════════════════════════════════════ */
27
+ export async function createEngine(canvas, theme = 'dark') {
28
+ clock = new THREE.Clock();
29
+
30
+ /* ─── Scene ────────────────────────────────────────────────────────────── */
31
+ scene = new THREE.Scene();
32
+ scene.fog = theme === 'dark'
33
+ ? new THREE.FogExp2(0x050510, 0.012)
34
+ : new THREE.FogExp2(0xe8ecf4, 0.008);
35
+
36
+ /* ─── Camera ───────────────────────────────────────────────────────────── */
37
+ const aspect = window.innerWidth / window.innerHeight;
38
+ camera = new THREE.PerspectiveCamera(55, aspect, 0.1, 500);
39
+ camera.position.set(18, 14, 18);
40
+ camera.lookAt(0, 0, 0);
41
+
42
+ /* ─── Renderer ─────────────────────────────────────────────────────────── */
43
+ renderer = new THREE.WebGLRenderer({
44
+ canvas,
45
+ antialias: true,
46
+ alpha: false,
47
+ powerPreference: 'high-performance',
48
+ });
49
+ renderer.setSize(window.innerWidth, window.innerHeight);
50
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
51
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
52
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
53
+ renderer.toneMappingExposure = theme === 'dark' ? 1.0 : 1.4;
54
+ renderer.shadowMap.enabled = true;
55
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
56
+
57
+ if (theme === 'dark') {
58
+ renderer.setClearColor(0x050510);
59
+ } else {
60
+ renderer.setClearColor(0xe8ecf4);
61
+ }
62
+
63
+ /* ─── Lighting ─────────────────────────────────────────────────────────── */
64
+ setupLighting(scene, theme);
65
+
66
+ /* ─── Post-processing ──────────────────────────────────────────────────── */
67
+ setupPostProcessing(theme);
68
+
69
+ /* ─── Ground Grid ──────────────────────────────────────────────────────── */
70
+ createGroundGrid(theme);
71
+
72
+ /* ─── Sky ──────────────────────────────────────────────────────────────── */
73
+ createSkyDome(theme);
74
+
75
+ return { renderer, scene, camera, composer, clock };
76
+ }
77
+
78
+ /* ═══════════════════════════════════════════════════════════════════════════════
79
+ 2. Lighting Setup
80
+ ═══════════════════════════════════════════════════════════════════════════════ */
81
+ function setupLighting(scene, theme) {
82
+ // Remove existing lights
83
+ scene.children.filter(c => c.isLight).forEach(l => scene.remove(l));
84
+
85
+ if (theme === 'dark') {
86
+ /* ── Dark theme: moody blue-green ambient + dramatic spots ────────── */
87
+ const ambient = new THREE.AmbientLight(0x1a2a4a, 0.4);
88
+ ambient.name = 'ambient';
89
+ scene.add(ambient);
90
+
91
+ const hemi = new THREE.HemisphereLight(0x0d1b2a, 0x0a0f1e, 0.3);
92
+ hemi.name = 'hemi';
93
+ scene.add(hemi);
94
+
95
+ // Main directional (moonlight)
96
+ const dir = new THREE.DirectionalLight(0x4488cc, 0.6);
97
+ dir.name = 'dir-main';
98
+ dir.position.set(-10, 20, 10);
99
+ dir.castShadow = true;
100
+ dir.shadow.mapSize.set(2048, 2048);
101
+ dir.shadow.camera.near = 0.5;
102
+ dir.shadow.camera.far = 60;
103
+ dir.shadow.camera.left = -25;
104
+ dir.shadow.camera.right = 25;
105
+ dir.shadow.camera.top = 25;
106
+ dir.shadow.camera.bottom = -25;
107
+ dir.shadow.bias = -0.002;
108
+ dir.shadow.normalBias = 0.02;
109
+ scene.add(dir);
110
+
111
+ // Accent spot (green glow from center)
112
+ const accent = new THREE.PointLight(0x34d399, 2.0, 30, 1.5);
113
+ accent.name = 'accent-glow';
114
+ accent.position.set(0, 6, 0);
115
+ accent.castShadow = false;
116
+ scene.add(accent);
117
+
118
+ // Rim light (purple back-light)
119
+ const rim = new THREE.PointLight(0xa78bfa, 1.0, 25, 1.5);
120
+ rim.name = 'rim-light';
121
+ rim.position.set(-12, 8, -12);
122
+ scene.add(rim);
123
+
124
+ // Warm light (desk area)
125
+ const warm = new THREE.PointLight(0xfbbf24, 0.8, 15, 2);
126
+ warm.name = 'warm-desk';
127
+ warm.position.set(6, 4, 6);
128
+ scene.add(warm);
129
+
130
+ } else {
131
+ /* ── Light theme: bright natural lighting ─────────────────────────── */
132
+ const ambient = new THREE.AmbientLight(0xf0f4fa, 0.6);
133
+ ambient.name = 'ambient';
134
+ scene.add(ambient);
135
+
136
+ const hemi = new THREE.HemisphereLight(0xddeeff, 0xf0ece0, 0.5);
137
+ hemi.name = 'hemi';
138
+ scene.add(hemi);
139
+
140
+ // Sun
141
+ const dir = new THREE.DirectionalLight(0xfff5e6, 1.2);
142
+ dir.name = 'dir-main';
143
+ dir.position.set(12, 25, 8);
144
+ dir.castShadow = true;
145
+ dir.shadow.mapSize.set(2048, 2048);
146
+ dir.shadow.camera.near = 0.5;
147
+ dir.shadow.camera.far = 60;
148
+ dir.shadow.camera.left = -25;
149
+ dir.shadow.camera.right = 25;
150
+ dir.shadow.camera.top = 25;
151
+ dir.shadow.camera.bottom = -25;
152
+ dir.shadow.bias = -0.002;
153
+ dir.shadow.normalBias = 0.02;
154
+ scene.add(dir);
155
+
156
+ // Soft fill
157
+ const fill = new THREE.DirectionalLight(0xb3d4ff, 0.4);
158
+ fill.name = 'fill-light';
159
+ fill.position.set(-8, 12, -5);
160
+ scene.add(fill);
161
+
162
+ // Subtle accent
163
+ const accent = new THREE.PointLight(0x059669, 0.6, 20, 2);
164
+ accent.name = 'accent-glow';
165
+ accent.position.set(0, 5, 0);
166
+ scene.add(accent);
167
+ }
168
+ }
169
+
170
+ /* ═══════════════════════════════════════════════════════════════════════════════
171
+ 3. Post-processing Pipeline
172
+ ═══════════════════════════════════════════════════════════════════════════════ */
173
+ function setupPostProcessing(theme) {
174
+ const size = renderer.getSize(new THREE.Vector2());
175
+
176
+ composer = new EffectComposer(renderer);
177
+
178
+ // Render pass
179
+ const renderPass = new RenderPass(scene, camera);
180
+ composer.addPass(renderPass);
181
+
182
+ // Bloom pass — gives the neon glow effect
183
+ bloomPass = new UnrealBloomPass(
184
+ new THREE.Vector2(size.x, size.y),
185
+ theme === 'dark' ? 0.6 : 0.15, // strength
186
+ 0.4, // radius
187
+ theme === 'dark' ? 0.85 : 0.95 // threshold
188
+ );
189
+ composer.addPass(bloomPass);
190
+
191
+ // FXAA anti-aliasing
192
+ fxaaPass = new ShaderPass(FXAAShader);
193
+ fxaaPass.uniforms['resolution'].value.set(1 / size.x, 1 / size.y);
194
+ composer.addPass(fxaaPass);
195
+
196
+ // Output pass (gamma correction)
197
+ const outputPass = new OutputPass();
198
+ composer.addPass(outputPass);
199
+ }
200
+
201
+ /* ═══════════════════════════════════════════════════════════════════════════════
202
+ 4. Ground Grid — Procedural infinite grid
203
+ ═══════════════════════════════════════════════════════════════════════════════ */
204
+ function createGroundGrid(theme) {
205
+ /* ── Ground plane ──────────────────────────────────────────────────────── */
206
+ const groundGeo = new THREE.PlaneGeometry(200, 200);
207
+ const groundMat = new THREE.MeshStandardMaterial({
208
+ color: theme === 'dark' ? 0x0a0f1e : 0xdee4ed,
209
+ roughness: 0.95,
210
+ metalness: 0.0,
211
+ });
212
+ const ground = new THREE.Mesh(groundGeo, groundMat);
213
+ ground.rotation.x = -Math.PI / 2;
214
+ ground.position.y = -0.01;
215
+ ground.receiveShadow = true;
216
+ ground.name = 'ground';
217
+ scene.add(ground);
218
+
219
+ /* ── Grid lines ────────────────────────────────────────────────────────── */
220
+ const gridSize = 80;
221
+ const gridDiv = 40;
222
+ const gridHelper = new THREE.GridHelper(
223
+ gridSize, gridDiv,
224
+ theme === 'dark' ? 0x1a2a3a : 0xbcc5d0,
225
+ theme === 'dark' ? 0x0f1a2a : 0xd0d8e0,
226
+ );
227
+ gridHelper.position.y = 0.01;
228
+ gridHelper.material.opacity = theme === 'dark' ? 0.3 : 0.2;
229
+ gridHelper.material.transparent = true;
230
+ gridHelper.name = 'grid';
231
+ scene.add(gridHelper);
232
+
233
+ /* ── Accent grid ring around center ────────────────────────────────────── */
234
+ const ringGeo = new THREE.RingGeometry(8, 8.08, 64);
235
+ const ringMat = new THREE.MeshBasicMaterial({
236
+ color: theme === 'dark' ? 0x34d399 : 0x059669,
237
+ transparent: true,
238
+ opacity: theme === 'dark' ? 0.4 : 0.2,
239
+ side: THREE.DoubleSide,
240
+ });
241
+ const ring = new THREE.Mesh(ringGeo, ringMat);
242
+ ring.rotation.x = -Math.PI / 2;
243
+ ring.position.y = 0.02;
244
+ ring.name = 'center-ring';
245
+ scene.add(ring);
246
+
247
+ /* ── Second ring ───────────────────────────────────────────────────────── */
248
+ const ring2Geo = new THREE.RingGeometry(14, 14.06, 64);
249
+ const ring2Mat = new THREE.MeshBasicMaterial({
250
+ color: theme === 'dark' ? 0x60a5fa : 0x2563eb,
251
+ transparent: true,
252
+ opacity: theme === 'dark' ? 0.2 : 0.1,
253
+ side: THREE.DoubleSide,
254
+ });
255
+ const ring2 = new THREE.Mesh(ring2Geo, ring2Mat);
256
+ ring2.rotation.x = -Math.PI / 2;
257
+ ring2.position.y = 0.02;
258
+ ring2.name = 'outer-ring';
259
+ scene.add(ring2);
260
+ }
261
+
262
+ /* ═══════════════════════════════════════════════════════════════════════════════
263
+ 5. Sky Dome — Gradient atmosphere
264
+ ═══════════════════════════════════════════════════════════════════════════════ */
265
+ function createSkyDome(theme) {
266
+ const skyGeo = new THREE.SphereGeometry(150, 32, 32);
267
+
268
+ // Custom shader for gradient sky
269
+ const skyMat = new THREE.ShaderMaterial({
270
+ uniforms: {
271
+ topColor: { value: theme === 'dark' ? new THREE.Color(0x0a0f2e) : new THREE.Color(0x87ceeb) },
272
+ bottomColor: { value: theme === 'dark' ? new THREE.Color(0x050510) : new THREE.Color(0xe8ecf4) },
273
+ offset: { value: 20 },
274
+ exponent: { value: 0.6 },
275
+ },
276
+ vertexShader: `
277
+ varying vec3 vWorldPosition;
278
+ void main() {
279
+ vec4 worldPos = modelMatrix * vec4(position, 1.0);
280
+ vWorldPosition = worldPos.xyz;
281
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
282
+ }
283
+ `,
284
+ fragmentShader: `
285
+ uniform vec3 topColor;
286
+ uniform vec3 bottomColor;
287
+ uniform float offset;
288
+ uniform float exponent;
289
+ varying vec3 vWorldPosition;
290
+ void main() {
291
+ float h = normalize(vWorldPosition + offset).y;
292
+ gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
293
+ }
294
+ `,
295
+ side: THREE.BackSide,
296
+ depthWrite: false,
297
+ });
298
+
299
+ const sky = new THREE.Mesh(skyGeo, skyMat);
300
+ sky.name = 'sky';
301
+ scene.add(sky);
302
+
303
+ /* ── Stars (dark theme only) ───────────────────────────────────────────── */
304
+ if (theme === 'dark') {
305
+ createStarField();
306
+ }
307
+ }
308
+
309
+ /* ═══════════════════════════════════════════════════════════════════════════════
310
+ 6. Star Field — Procedural stars
311
+ ═══════════════════════════════════════════════════════════════════════════════ */
312
+ function createStarField() {
313
+ const count = 2000;
314
+ const positions = new Float32Array(count * 3);
315
+ const sizes = new Float32Array(count);
316
+ const colors = new Float32Array(count * 3);
317
+
318
+ const starColors = [
319
+ new THREE.Color(0xffffff),
320
+ new THREE.Color(0xccddff),
321
+ new THREE.Color(0xffeedd),
322
+ new THREE.Color(0xddeeff),
323
+ new THREE.Color(0x34d399),
324
+ ];
325
+
326
+ for (let i = 0; i < count; i++) {
327
+ // Distribute on upper hemisphere
328
+ const theta = Math.random() * Math.PI * 2;
329
+ const phi = Math.random() * Math.PI * 0.45; // Only upper portion
330
+ const r = 100 + Math.random() * 40;
331
+
332
+ positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
333
+ positions[i * 3 + 1] = r * Math.cos(phi);
334
+ positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
335
+
336
+ sizes[i] = 0.3 + Math.random() * 1.2;
337
+
338
+ const c = starColors[Math.floor(Math.random() * starColors.length)];
339
+ colors[i * 3] = c.r;
340
+ colors[i * 3 + 1] = c.g;
341
+ colors[i * 3 + 2] = c.b;
342
+ }
343
+
344
+ const geo = new THREE.BufferGeometry();
345
+ geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
346
+ geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
347
+ geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
348
+
349
+ const mat = new THREE.ShaderMaterial({
350
+ uniforms: {
351
+ time: { value: 0 },
352
+ },
353
+ vertexShader: `
354
+ attribute float size;
355
+ attribute vec3 color;
356
+ varying vec3 vColor;
357
+ varying float vSize;
358
+ uniform float time;
359
+ void main() {
360
+ vColor = color;
361
+ vSize = size;
362
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
363
+ float twinkle = 0.7 + 0.3 * sin(time * 2.0 + position.x * 10.0 + position.z * 7.0);
364
+ gl_PointSize = size * twinkle * (200.0 / -mvPosition.z);
365
+ gl_Position = projectionMatrix * mvPosition;
366
+ }
367
+ `,
368
+ fragmentShader: `
369
+ varying vec3 vColor;
370
+ varying float vSize;
371
+ void main() {
372
+ vec2 center = gl_PointCoord - vec2(0.5);
373
+ float dist = length(center);
374
+ if (dist > 0.5) discard;
375
+ float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
376
+ float glow = exp(-dist * dist * 8.0);
377
+ gl_FragColor = vec4(vColor * (0.8 + glow * 0.5), alpha * 0.9);
378
+ }
379
+ `,
380
+ transparent: true,
381
+ depthWrite: false,
382
+ blending: THREE.AdditiveBlending,
383
+ });
384
+
385
+ const stars = new THREE.Points(geo, mat);
386
+ stars.name = 'stars';
387
+ scene.add(stars);
388
+ }
389
+
390
+ /* ═══════════════════════════════════════════════════════════════════════════════
391
+ 7. Resize Handler
392
+ ═══════════════════════════════════════════════════════════════════════════════ */
393
+ export function resizeEngine() {
394
+ if (!renderer || !camera) return;
395
+ const w = window.innerWidth;
396
+ const h = window.innerHeight;
397
+ camera.aspect = w / h;
398
+ camera.updateProjectionMatrix();
399
+ renderer.setSize(w, h);
400
+ if (composer) composer.setSize(w, h);
401
+ if (fxaaPass) fxaaPass.uniforms['resolution'].value.set(1 / w, 1 / h);
402
+ }
403
+
404
+ /* ═══════════════════════════════════════════════════════════════════════════════
405
+ 8. Update Functions
406
+ ═══════════════════════════════════════════════════════════════════════════════ */
407
+
408
+ /** Called each frame to update time-based uniforms */
409
+ export function updateEngine(dt) {
410
+ // Update star twinkle
411
+ const stars = scene.getObjectByName('stars');
412
+ if (stars && stars.material.uniforms) {
413
+ stars.material.uniforms.time.value += dt;
414
+ }
415
+
416
+ // Animate center ring
417
+ const ring = scene.getObjectByName('center-ring');
418
+ if (ring) {
419
+ ring.rotation.z += dt * 0.1;
420
+ }
421
+
422
+ const ring2 = scene.getObjectByName('outer-ring');
423
+ if (ring2) {
424
+ ring2.rotation.z -= dt * 0.05;
425
+ }
426
+ }
427
+
428
+ /* ═══════════════════════════════════════════════════════════════════════════════
429
+ 9. Theme Update
430
+ ═══════════════════════════════════════════════════════════════════════════════ */
431
+ export function updateEngineTheme(theme) {
432
+ if (!renderer || !scene) return;
433
+
434
+ // Update clear color
435
+ renderer.setClearColor(theme === 'dark' ? 0x050510 : 0xe8ecf4);
436
+ renderer.toneMappingExposure = theme === 'dark' ? 1.0 : 1.4;
437
+
438
+ // Update fog
439
+ scene.fog = theme === 'dark'
440
+ ? new THREE.FogExp2(0x050510, 0.012)
441
+ : new THREE.FogExp2(0xe8ecf4, 0.008);
442
+
443
+ // Update lighting
444
+ setupLighting(scene, theme);
445
+
446
+ // Update bloom
447
+ if (bloomPass) {
448
+ bloomPass.strength = theme === 'dark' ? 0.6 : 0.15;
449
+ bloomPass.threshold = theme === 'dark' ? 0.85 : 0.95;
450
+ }
451
+
452
+ // Update ground
453
+ const ground = scene.getObjectByName('ground');
454
+ if (ground) ground.material.color.setHex(theme === 'dark' ? 0x0a0f1e : 0xdee4ed);
455
+
456
+ // Update grid
457
+ const grid = scene.getObjectByName('grid');
458
+ if (grid) {
459
+ grid.material.opacity = theme === 'dark' ? 0.3 : 0.2;
460
+ }
461
+
462
+ // Update sky
463
+ const sky = scene.getObjectByName('sky');
464
+ if (sky && sky.material.uniforms) {
465
+ sky.material.uniforms.topColor.value.setHex(theme === 'dark' ? 0x0a0f2e : 0x87ceeb);
466
+ sky.material.uniforms.bottomColor.value.setHex(theme === 'dark' ? 0x050510 : 0xe8ecf4);
467
+ }
468
+
469
+ // Stars visibility
470
+ const stars = scene.getObjectByName('stars');
471
+ if (stars) stars.visible = theme === 'dark';
472
+ if (!stars && theme === 'dark') createStarField();
473
+
474
+ // Center ring
475
+ const ring = scene.getObjectByName('center-ring');
476
+ if (ring) {
477
+ ring.material.color.setHex(theme === 'dark' ? 0x34d399 : 0x059669);
478
+ ring.material.opacity = theme === 'dark' ? 0.4 : 0.2;
479
+ }
480
+ }
481
+
482
+ /* ═══════════════════════════════════════════════════════════════════════════════
483
+ 10. Getters
484
+ ═══════════════════════════════════════════════════════════════════════════════ */
485
+ export function getRenderer() { return renderer; }
486
+ export function getScene() { return scene; }
487
+ export function getCamera() { return camera; }
488
+ export function getComposer() { return composer; }
489
+ export function getClock() { return clock; }