| |
| |
| |
| |
| |
| |
| |
| |
| |
| import * as THREE from 'three'; |
| import { TRACK_WIDTH, CURB_W } from './config.js'; |
| import { frame } from './track.js'; |
|
|
| |
|
|
| let _shared = null; |
| function getShared() { |
| if (_shared) return _shared; |
|
|
| |
| const qCanvas = document.createElement('canvas'); |
| qCanvas.width = qCanvas.height = 128; |
| const qctx = qCanvas.getContext('2d'); |
| const grad = qctx.createLinearGradient(0, 0, 0, 128); |
| grad.addColorStop(0, '#ffc85e'); |
| grad.addColorStop(1, '#f09120'); |
| qctx.fillStyle = grad; |
| qctx.fillRect(0, 0, 128, 128); |
| qctx.strokeStyle = 'rgba(60,30,5,0.9)'; |
| qctx.lineWidth = 8; |
| qctx.strokeRect(4, 4, 120, 120); |
| |
| qctx.fillStyle = 'rgba(60,30,5,0.8)'; |
| for (const [rx, ry] of [[16, 16], [112, 16], [16, 112], [112, 112]]) { |
| qctx.beginPath(); qctx.arc(rx, ry, 5, 0, Math.PI * 2); qctx.fill(); |
| } |
| qctx.fillStyle = '#fff7e8'; |
| qctx.strokeStyle = 'rgba(60,30,5,0.85)'; |
| qctx.lineWidth = 6; |
| qctx.font = '900 84px Arial'; |
| qctx.textAlign = 'center'; |
| qctx.textBaseline = 'middle'; |
| qctx.strokeText('?', 64, 70); |
| qctx.fillText('?', 64, 70); |
| const decalTex = new THREE.CanvasTexture(qCanvas); |
| decalTex.anisotropy = 4; |
|
|
| |
| const gCanvas = document.createElement('canvas'); |
| gCanvas.width = gCanvas.height = 128; |
| const gctx = gCanvas.getContext('2d'); |
| const rg = gctx.createRadialGradient(64, 64, 4, 64, 64, 64); |
| rg.addColorStop(0, 'rgba(255,190,90,0.55)'); |
| rg.addColorStop(0.4, 'rgba(255,150,50,0.22)'); |
| rg.addColorStop(1, 'rgba(255,120,30,0)'); |
| gctx.fillStyle = rg; |
| gctx.fillRect(0, 0, 128, 128); |
| const glowTex = new THREE.CanvasTexture(gCanvas); |
|
|
| _shared = { |
| decalTex, |
| glowTex, |
| boxGeo: new THREE.BoxGeometry(1.3, 1.3, 1.3), |
| lidGeo: new THREE.BoxGeometry(1.36, 0.18, 1.36), |
| decalGeo: new THREE.PlaneGeometry(1.05, 1.05), |
| glowGeo: new THREE.PlaneGeometry(3.6, 3.6), |
| boxMat: new THREE.MeshPhongMaterial({ |
| color: 0xffa832, emissive: 0x7a4a00, emissiveIntensity: 0.55, |
| shininess: 90, specular: 0xffeecc, |
| }), |
| lidMat: new THREE.MeshPhongMaterial({ |
| color: 0x8a5a18, emissive: 0x3a2404, emissiveIntensity: 0.4, |
| shininess: 60, |
| }), |
| glowMat: new THREE.MeshBasicMaterial({ |
| map: glowTex, transparent: true, depthWrite: false, |
| blending: THREE.AdditiveBlending, side: THREE.DoubleSide, |
| }), |
| }; |
| return _shared; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function createItemBoxes(scene) { |
| const S = getShared(); |
| const boxes = []; |
| const COUNT = 10; |
|
|
| for (let i = 0; i < COUNT; i++) { |
| const t = (i + 0.5) / COUNT; |
| const { point, side } = frame(t); |
| const sideSign = i % 2 === 0 ? 1 : -1; |
| const lateral = sideSign * (TRACK_WIDTH / 2 + CURB_W + 2.2); |
|
|
| const box = new THREE.Group(); |
|
|
| const body = new THREE.Mesh(S.boxGeo, S.boxMat); |
| body.castShadow = true; |
| box.add(body); |
|
|
| |
| for (const y of [0.62, -0.62]) { |
| const lid = new THREE.Mesh(S.lidGeo, S.lidMat); |
| lid.position.y = y; |
| box.add(lid); |
| } |
|
|
| |
| const decalMat = new THREE.MeshBasicMaterial({ map: S.decalTex, transparent: true }); |
| for (const [pos, rotY] of [ |
| [[0, 0, 0.66], 0], [[0, 0, -0.66], Math.PI], |
| [[0.66, 0, 0], Math.PI / 2], [[-0.66, 0, 0], -Math.PI / 2], |
| ]) { |
| const q = new THREE.Mesh(S.decalGeo, decalMat); |
| q.position.set(...pos); |
| q.rotation.y = rotY; |
| box.add(q); |
| } |
|
|
| |
| const glow = new THREE.Mesh(S.glowGeo, S.glowMat); |
| glow.renderOrder = 5; |
| box.add(glow); |
| box.userData.glow = glow; |
|
|
| box.position.set(point.x + side.x * lateral, 1.7, point.z + side.z * lateral); |
| box.userData.baseY = 1.7; |
| box.userData.phase = i * 1.37; |
| box.userData.spinRate = 0.6 + (i % 3) * 0.25; |
| scene.add(box); |
| boxes.push(box); |
| } |
| return boxes; |
| } |
|
|
| |
| |
| |
| |
| export function updateItemBoxes(boxes, elapsed, camera) { |
| if (!boxes.length) return; |
| |
| const S = getShared(); |
| S.boxMat.emissiveIntensity = 0.45 + 0.25 * Math.sin(elapsed * 2.2); |
| for (const box of boxes) { |
| const ph = box.userData.phase; |
| box.position.y = box.userData.baseY + Math.sin(elapsed * 1.6 + ph) * 0.25; |
| box.rotation.y = elapsed * box.userData.spinRate + ph; |
| |
| if (camera && box.userData.glow) box.userData.glow.lookAt(camera.position); |
| } |
| } |
|
|