Spaces:
Running
Running
Download patch_3d.mjs from bep40/hangmu-docker: direct link, hf CLI and curl.
- Browser
- Download file 19.8 kB
-
https://huggingface.co/spaces/bep40/hangmu-docker/resolve/main/patch_3d.mjs
- Command line
-
hf download hf://spaces/bep40/hangmu-docker/patch_3d.mjs
-
curl -L -o patch_3d.mjs https://huggingface.co/spaces/bep40/hangmu-docker/resolve/main/patch_3d.mjs
19.8 kB
| /* patch_3d.mjs — Bổ sung vật thể/scene/nhân vật 3D còn thiếu so với hangmu.online. | |
| Chạy SAU patch_app.mjs (pattern trỏ vào source ĐÃ patch). | |
| - app.js: avatar mặc định áo đỏ MU #c8102e (ưu tiên kit nếu server gửi), | |
| thêm vai + giày, va chạm ghế đá hội trường (5×10). | |
| - home3d.js: thiên nhiên quanh cửa hang (cỏ/bụi/lá/đá instancedMesh), | |
| bảng chuyên cần attendance trên bia đá. | |
| - cave3d.js: cúp vàng trung tâm + banner MU trên vòm. | |
| Nếu pattern không khớp -> exit(1). */ | |
| import fs from 'node:fs' | |
| function patchFile(path, reps) { | |
| let s = fs.readFileSync(path, 'utf8') | |
| const failures = [] | |
| function toRe(str) { | |
| const esc = str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') | |
| return esc.replace(/\s+/g, '\\s+') | |
| } | |
| function rep(oldStr, newStr) { | |
| const exact = s.split(oldStr).length - 1 | |
| if (exact === 1) { s = s.replace(oldStr, newStr); return } | |
| const re = new RegExp(toRe(oldStr), 'g') | |
| const m = s.match(re) | |
| if (m && m.length === 1) { s = s.replace(re, newStr); return } | |
| failures.push(`pattern not unique/found (exact=${exact}, re=${m ? m.length : 0}): ${oldStr.slice(0, 70)}`) | |
| } | |
| for (const [o, n] of reps) rep(o, n) | |
| if (failures.length) { | |
| console.error(`PATCH 3D FAILED (${path}):`) | |
| failures.forEach((f) => console.error(' -', f)) | |
| process.exit(1) | |
| } | |
| fs.writeFileSync(path, s) | |
| console.log('patched ok (3d):', path) | |
| } | |
| /* ================= app.js ================= */ | |
| patchFile('frontend/app.js', [ | |
| /* Palette: thêm đỏ MU da291c */ | |
| [ "const COLORS = ['#c8102e', '#fdbd10', '#ffffff', '#4ade80', '#60a5fa', '#f472b6', '#a78bfa', '#fb923c']", | |
| "const COLORS = ['#c8102e', '#da291c', '#fdbd10', '#4ade80', '#60a5fa', '#f472b6', '#a78bfa', '#fb923c']" ], | |
| /* Avatar: mặc định áo đỏ MU nếu server không gửi kit */ | |
| [ `function PlayerAvatar({ id, p, isLocal }) { | |
| const color = COLORS[parseInt(id.slice(-1), 16) % COLORS.length]`, | |
| `function PlayerAvatar({ id, p, isLocal }) { | |
| const color = (p.kit != null && COLORS[p.kit % COLORS.length]) || '#c8102e'` ], | |
| /* Va chạm ghế đá hội trường (5 hàng × 10 ghế, y hệt hall.js gốc) */ | |
| [ "const KEYMAP = { ArrowUp: 'up', KeyW: 'up', ArrowDown: 'down', KeyS: 'down', ArrowLeft: 'left', KeyA: 'left', ArrowRight: 'right', KeyD: 'right' }", | |
| `const KEYMAP = { ArrowUp: 'up', KeyW: 'up', ArrowDown: 'down', KeyS: 'down', ArrowLeft: 'left', KeyA: 'left', ArrowRight: 'right', KeyD: 'right' } | |
| /* Va chạm ghế đá (hội trường trước màn hình lớn) — 5 hàng × 10 ghế, y hệt hall.js gốc */ | |
| const BENCH_COLLIDE = (() => { | |
| const out = [] | |
| for (let row = 0; row < 5; row++) { | |
| const z = -37 + row * 2.6 | |
| for (let col = 0; col < 10; col++) { | |
| const x = -10.8 + col * 2.4 | |
| out.push({ x, z, r: 1.05 }) | |
| } | |
| } | |
| return out | |
| })() | |
| function benchBlocked(nx, nz) { | |
| for (let i = 0; i < BENCH_COLLIDE.length; i++) { | |
| const b = BENCH_COLLIDE[i] | |
| const dx = nx - b.x, dz = nz - b.z | |
| if (dx * dx + dz * dz < b.r * b.r) return true | |
| } | |
| return false | |
| }` ], | |
| /* Chặn đi xuyên ghế */ | |
| [ ` const fireSafe = 8.2 | |
| const d0 = Math.hypot(me.pos.x, me.pos.z) | |
| if (d0 >= fireSafe && Math.hypot(nx, nz) < fireSafe) { | |
| const s = fireSafe / Math.max(Math.hypot(nx, nz), 1e-6) | |
| nx *= s; nz *= s | |
| } | |
| me.pos.x = nx; me.pos.z = nz`, | |
| ` const fireSafe = 8.2 | |
| const d0 = Math.hypot(me.pos.x, me.pos.z) | |
| if (d0 >= fireSafe && Math.hypot(nx, nz) < fireSafe) { | |
| const s = fireSafe / Math.max(Math.hypot(nx, nz), 1e-6) | |
| nx *= s; nz *= s | |
| } | |
| // va chạm ghế đá hội trường: không cho đi xuyên qua | |
| if (benchBlocked(nx, nz)) { | |
| if (!benchBlocked(nx, me.pos.z)) nz = me.pos.z | |
| else if (!benchBlocked(me.pos.x, nz)) nx = me.pos.x | |
| else { nx = me.pos.x; nz = me.pos.z } | |
| } | |
| me.pos.x = nx; me.pos.z = nz` ], | |
| /* Vai + giày cho avatar */ | |
| [ ` <group position={[0.22, 1.0 + yOff, 0]}> | |
| <mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}> | |
| <capsuleGeometry args={[0.18, 0.95, 4, 8]} /> | |
| <meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} /> | |
| </mesh> | |
| </group>`, | |
| ` <group position={[0.22, 1.0 + yOff, 0]}> | |
| <mesh castShadow rotation={[-Math.sin(bob.current * anim / 4) * 0.9, 0, 0]}> | |
| <capsuleGeometry args={[0.18, 0.95, 4, 8]} /> | |
| <meshStandardMaterial color={0x14141f} roughness={0.8} metalness={0.1} /> | |
| </mesh> | |
| </group> | |
| {/* vai — cầu vai rộng kiểu áo bóng đá */} | |
| <mesh position={[-0.62, 1.9 + yOff, 0]} rotation-z={0.2}> | |
| <sphereGeometry args={[0.16, 8, 8]} /> | |
| <meshStandardMaterial color={color} roughness={0.7} /> | |
| </mesh> | |
| <mesh position={[0.62, 1.9 + yOff, 0]} rotation-z={-0.2}> | |
| <sphereGeometry args={[0.16, 8, 8]} /> | |
| <meshStandardMaterial color={color} roughness={0.7} /> | |
| </mesh> | |
| {/* giày — đế dưới chân */} | |
| <mesh position={[-0.09, 0.22 + yOff, 0.16]} rotation-x={0.25}> | |
| <boxGeometry args={[0.18, 0.09, 0.3]} /> | |
| <meshStandardMaterial color={0x1a1a1a} roughness={0.5} metalness={0.15} /> | |
| </mesh> | |
| <mesh position={[0.09, 0.22 + yOff, 0.16]} rotation-x={0.25}> | |
| <boxGeometry args={[0.18, 0.09, 0.3]} /> | |
| <meshStandardMaterial color={0x1a1a1a} roughness={0.5} metalness={0.15} /> | |
| </mesh>` ], | |
| ]) | |
| /* ================= home3d.js ================= */ | |
| patchFile('frontend/home3d.js', [ | |
| /* Thiên nhiên quanh cửa hang */ | |
| [ `const FULLR = Math.PI * 2`, | |
| `const FULLR = Math.PI * 2 | |
| /* ========================================================================= | |
| THIÊN NHIÊN QUANH CỬA HANG — cỏ / bụi / lá / đá (instancedMesh, y hệt gốc) | |
| ========================================================================= */ | |
| function _instancedNature() { | |
| const rand = mulberry(31) | |
| const grass = [], bushes = [], leaves = [], stones = [] | |
| const push = (arr, geo, px, py, pz, sx, sy, sz, ry) => arr.push({ geo, px, py, pz, sx, sy, sz, ry }) | |
| const gGeo = new THREE.ConeGeometry(0.05, 0.42, 3) | |
| for (let i = 0; i < 220; i++) { | |
| const a = rand() * Math.PI * 2, r = 4 + rand() * 13 | |
| push(grass, gGeo, Math.sin(a) * r * 0.9, 0.14 + rand() * 0.2, Math.cos(a) * r * 0.8 + 1.5, 0.8 + rand() * 0.9, 0.8 + rand() * 0.9, 0.8 + rand() * 0.9, rand() * Math.PI) | |
| } | |
| const bGeo = new THREE.IcosahedronGeometry(0.5, 1) | |
| for (let i = 0; i < 26; i++) { | |
| const a = rand() * Math.PI * 2, r = 6 + rand() * 15 | |
| push(bushes, bGeo, Math.sin(a) * r * 0.9, 0.25 + rand() * 0.25, Math.cos(a) * r * 0.8 + 1.5, 0.5 + rand() * 0.7, 0.5 + rand() * 0.5, 0.5 + rand() * 0.7, rand() * Math.PI) | |
| } | |
| const lGeo = new THREE.PlaneGeometry(0.09, 0.05) | |
| for (let i = 0; i < 40; i++) { | |
| const a = rand() * Math.PI * 2, r = 3 + rand() * 16 | |
| push(leaves, lGeo, Math.sin(a) * r * 0.9, 0.6 + rand() * 1.8, Math.cos(a) * r * 0.8 + 1.5, 1, 1, 1, rand() * Math.PI) | |
| } | |
| const sGeo = new THREE.IcosahedronGeometry(0.28, 1) | |
| for (let i = 0; i < 14; i++) { | |
| const a = rand() * Math.PI * 2, r = 4.5 + rand() * 14 | |
| push(stones, sGeo, Math.sin(a) * r * 0.9, 0.12 + rand() * 0.18, Math.cos(a) * r * 0.8 + 1.5, 0.5 + rand() * 1.2, 0.45 + rand() * 0.9, 0.5 + rand() * 1.2, rand() * Math.PI) | |
| } | |
| return { grass, bushes, leaves, stones } | |
| } | |
| const _nature = _instancedNature() | |
| function NatureGround() { | |
| const gRef = useRef(null), bRef = useRef(null), lRef = useRef(null), sRef = useRef(null) | |
| const green = new THREE.Color('#6d7d38'), green2 = new THREE.Color('#56662d'), leaf = new THREE.Color('#7f9a4a') | |
| const stone = new THREE.Color('#9a8a6c'), stone2 = new THREE.Color('#77664c') | |
| useMemo(() => { | |
| const dummy = new THREE.Object3D() | |
| const fill = (ref, arr, cA, cB) => { | |
| if (!ref.current) return | |
| arr.forEach((it, i) => { | |
| dummy.position.set(it.px, it.py, it.pz); dummy.scale.set(it.sx, it.sy, it.sz); dummy.rotation.y = it.ry | |
| dummy.updateMatrix(); ref.current.setMatrixAt(i, dummy.matrix) | |
| ref.current.setColorAt(i, i % 2 ? cB : cA) | |
| }) | |
| ref.current.instanceMatrix.needsUpdate = true | |
| ref.current.instanceColor.needsUpdate = true | |
| } | |
| fill(gRef, _nature.grass, green, green2) | |
| fill(bRef, _nature.bushes, green, green2) | |
| fill(lRef, _nature.leaves, leaf, leaf) | |
| fill(sRef, _nature.stones, stone, stone2) | |
| }, []) | |
| return ( | |
| <> | |
| <instancedMesh ref={gRef} args={[undefined, undefined, _nature.grass.length]}> | |
| <coneGeometry args={[0.05, 0.42, 3]} /> | |
| <meshLambertMaterial vertexColors flatShading /> | |
| </instancedMesh> | |
| <instancedMesh ref={bRef} args={[undefined, undefined, _nature.bushes.length]}> | |
| <icosahedronGeometry args={[0.5, 1]} /> | |
| <meshLambertMaterial vertexColors flatShading /> | |
| </instancedMesh> | |
| <instancedMesh ref={lRef} args={[undefined, undefined, _nature.leaves.length]}> | |
| <planeGeometry args={[0.09, 0.05]} /> | |
| <meshLambertMaterial vertexColors flatShading side={THREE.DoubleSide} transparent opacity={0.92} /> | |
| </instancedMesh> | |
| <instancedMesh ref={sRef} args={[undefined, undefined, _nature.stones.length]}> | |
| <icosahedronGeometry args={[0.28, 1]} /> | |
| <meshLambertMaterial vertexColors flatShading /> | |
| </instancedMesh> | |
| </> | |
| ) | |
| }` ], | |
| /* Dây leo quanh vòm cửa + bảng chuyên cần đứng — vines/board như gốc */ | |
| [ `function GroundGeom() {`, | |
| `function Vines() { | |
| const strands = useMemo(() => { | |
| const out = [] | |
| const rand = mulberry(13) | |
| for (let i = 0; i < 26; i++) { | |
| const a = Math.PI + (i - 13) * 0.12 | |
| const r = 4.9 | |
| const x = Math.sin(a) * r * 1.15 + 1.2, z = Math.cos(a) * r - 1.5 | |
| const len = 1.6 + rand() * 2.2 | |
| out.push({ x, z, len, sway: rand() * Math.PI * 2, leaf: rand() * 5 }) | |
| } | |
| return out | |
| }, []) | |
| return ( | |
| <group> | |
| {strands.map((v, i) => ( | |
| <group key={i} position={[v.x, 3.4 - v.len / 2, v.z]}> | |
| <mesh> | |
| <cylinderGeometry args={[0.035, 0.05, v.len, 4]} /> | |
| <meshLambertMaterial color="#3d5a22" flatShading /> | |
| </mesh> | |
| <mesh position={[0, -v.len / 2 + 0.1, 0]} rotation-x={0.5}> | |
| <sphereGeometry args={[0.1, 5, 5]} /> | |
| <meshLambertMaterial color="#4c7a2e" flatShading /> | |
| </mesh> | |
| </group> | |
| ))} | |
| </group> | |
| ) | |
| } | |
| function AttendanceBoard() { | |
| const [rows, setRows] = React.useState(null) | |
| React.useEffect(() => { | |
| let alive = true | |
| fetch('/api/attendance').then(r => r.ok ? r.json() : Promise.reject()).then(d => { | |
| if (alive && d && d.rows) setRows(d.rows) | |
| }).catch(() => { | |
| if (!alive) return | |
| const r = [] | |
| for (let i = 0; i < 5; i++) { | |
| const row = [] | |
| for (let j = 0; j < 10; j++) row.push(Math.random() > 0.35 ? { name: '', number: 0 } : null) | |
| r.push(row) | |
| } | |
| setRows(r) | |
| }) | |
| return () => { alive = false } | |
| }, []) | |
| const tex = React.useMemo(() => { | |
| const c = document.createElement('canvas'); c.width = 640; c.height = 320 | |
| const g = c.getContext('2d') | |
| g.fillStyle = '#7a5f42'; g.fillRect(0, 0, 640, 320) | |
| g.strokeStyle = '#4a3522'; g.lineWidth = 4; g.strokeRect(4, 4, 632, 312) | |
| g.fillStyle = '#3a2a18'; g.font = '700 26px "Be Vietnam Pro", sans-serif'; g.textAlign = 'center' | |
| g.fillText('CHUYÊN CẦN THÁNG NÀY', 320, 36) | |
| for (let r = 0; r < 5; r++) for (let c = 0; c < 10; c++) { | |
| const x = 16 + c * 61, y = 50 + r * 50 | |
| g.fillStyle = '#5a442c'; g.fillRect(x, y, 56, 42) | |
| if (rows && rows[r] && rows[r][c]) { | |
| g.fillStyle = '#c8102e'; g.fillRect(x + 3, y + 3, 50, 36) | |
| g.fillStyle = '#fff'; g.font = '600 18px "Be Vietnam Pro", sans-serif' | |
| g.fillText('✓', x + 28, y + 27) | |
| } else { g.fillStyle = '#8a6f4e'; g.fillRect(x + 3, y + 3, 50, 36) } | |
| } | |
| const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace | |
| return t | |
| }, [rows]) | |
| if (!rows) return null | |
| return ( | |
| <mesh position={[0, 1.45, 0.02]} castShadow> | |
| <planeGeometry args={[2.9, 1.45]} /> | |
| <meshStandardMaterial map={tex} roughness={0.85} /> | |
| </mesh> | |
| ) | |
| } | |
| function GroundGeom() {` ], | |
| /* Wire AttendanceBoard vào PostAndPlaque (trụ bảng) */ | |
| [ ` <meshStandardMaterial map={tex} alphaTest={0.5} side={THREE.DoubleSide} roughness={0.85} /> | |
| </mesh> | |
| )}`, | |
| ` <meshStandardMaterial map={tex} alphaTest={0.5} side={THREE.DoubleSide} roughness={0.85} /> | |
| </mesh> | |
| )} | |
| <AttendanceBoard />` ], | |
| /* Wire NatureGround + Vines vào HomeScene */ | |
| [ ` <CaveExterior /> | |
| <group position={layout.stele} rotation-y={layout.steleRotY}>`, | |
| ` <CaveExterior /> | |
| <NatureGround /> | |
| <Vines /> | |
| <group position={layout.stele} rotation-y={layout.steleRotY}>` ], | |
| /* Bảng chuyên cần attendance trên bia đá */ | |
| [ `/* ---- Camera rig + layout (MA gốc) ---- */`, | |
| `/* ========================================================================= | |
| BẢNG CHUYÊN CẦN (attendance) — tái hiện Mn() hangmu.online: | |
| 5×10 ô trên bia đá, mỗi ô = người có mặt hôm nay | |
| ========================================================================= */ | |
| function SteleTop() { | |
| const [rows, setRows] = React.useState(null) | |
| React.useEffect(() => { | |
| let alive = true | |
| fetch('/api/attendance').then(r => r.ok ? r.json() : Promise.reject()).then(d => { | |
| if (alive && d && d.rows) setRows(d.rows) | |
| }).catch(() => { | |
| if (!alive) return | |
| const r = [] | |
| for (let i = 0; i < 5; i++) { | |
| const row = [] | |
| for (let j = 0; j < 10; j++) row.push(Math.random() > 0.35 ? { name: '', number: 0 } : null) | |
| r.push(row) | |
| } | |
| setRows(r) | |
| }) | |
| return () => { alive = false } | |
| }, []) | |
| const boardTex = React.useMemo(() => { | |
| const c = document.createElement('canvas') | |
| c.width = 640; c.height = 320 | |
| const g = c.getContext('2d') | |
| g.fillStyle = '#8a6f4e'; g.fillRect(0, 0, 640, 320) | |
| g.fillStyle = '#7a5f42'; g.fillRect(8, 8, 624, 304) | |
| g.strokeStyle = '#4a3522'; g.lineWidth = 4; g.strokeRect(8, 8, 624, 304) | |
| g.fillStyle = '#3a2a18'; g.font = '700 26px "Be Vietnam Pro", sans-serif' | |
| g.textAlign = 'center' | |
| g.fillText('CHUYÊN CẦN THÁNG NÀY', 320, 38) | |
| if (rows) { | |
| for (let r = 0; r < 5; r++) { | |
| for (let c = 0; c < 10; c++) { | |
| const x = 18 + c * 61, y = 52 + r * 50 | |
| g.fillStyle = '#5a442c'; g.fillRect(x, y, 56, 42) | |
| if (rows[r] && rows[r][c]) { | |
| g.fillStyle = '#c8102e'; g.fillRect(x + 3, y + 3, 50, 36) | |
| g.fillStyle = '#fff'; g.font = '600 18px "Be Vietnam Pro", sans-serif' | |
| g.fillText('✓', x + 28, y + 26) | |
| } else { | |
| g.fillStyle = '#8a6f4e'; g.fillRect(x + 3, y + 3, 50, 36) | |
| } | |
| } | |
| } | |
| } | |
| const t = new THREE.CanvasTexture(c) | |
| t.colorSpace = THREE.SRGBColorSpace | |
| return t | |
| }, [rows]) | |
| if (!rows) return null | |
| return ( | |
| <mesh position={[0, 2.4, 0.16]} castShadow> | |
| <planeGeometry args={[2.6, 1.3]} /> | |
| <meshStandardMaterial map={boardTex} roughness={0.85} /> | |
| </mesh> | |
| ) | |
| } | |
| /* ---- Camera rig + layout (MA gốc) ---- */` ], | |
| /* Wire SteleTop vào StelePlate */ | |
| [ ` <meshStandardMaterial attach="material-1" color="#d6ccba" roughness={0.9} /> | |
| </mesh> | |
| </group> | |
| </group> | |
| ) | |
| }`, | |
| ` <meshStandardMaterial attach="material-1" color="#d6ccba" roughness={0.9} /> | |
| </mesh> | |
| </group> | |
| <SteleTop /> | |
| </group> | |
| ) | |
| }` ], | |
| ]) | |
| /* ================= cave3d.js ================= */ | |
| patchFile('frontend/cave3d.js', [ | |
| /* Cúp vàng + banner MU trên vòm */ | |
| [ `export function CaveScene() {`, | |
| `/* ========================================================================= | |
| CÚP VÀNG TRUNG TÂM (trophy) — trước vòng huyền thoại, kỷ vật MU | |
| ========================================================================= */ | |
| function Trophy() { | |
| return ( | |
| <group position={[0, 0, 0]} rotation-y={0.5}> | |
| <mesh position={[0, 0.12, 0]} castShadow> | |
| <cylinderGeometry args={[0.85, 1.0, 0.24, 8]} /> | |
| <meshStandardMaterial color="#5a4a2c" metalness={0.6} roughness={0.4} flatShading /> | |
| </mesh> | |
| <mesh position={[0, 0.34, 0]} castShadow> | |
| <cylinderGeometry args={[0.55, 0.7, 0.2, 8]} /> | |
| <meshStandardMaterial color="#8a6a2c" metalness={0.7} roughness={0.35} flatShading /> | |
| </mesh> | |
| <mesh position={[0, 0.85, 0]} castShadow> | |
| <cylinderGeometry args={[0.42, 0.5, 0.85, 10]} /> | |
| <meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading /> | |
| </mesh> | |
| <mesh position={[0, 1.32, 0]} castShadow> | |
| <torusGeometry args={[0.5, 0.09, 8, 18]} /> | |
| <meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading /> | |
| </mesh> | |
| <mesh position={[-0.42, 0.85, 0]} rotation-z={0.3}> | |
| <torusGeometry args={[0.26, 0.07, 8, 14, Math.PI]} /> | |
| <meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading /> | |
| </mesh> | |
| <mesh position={[0.42, 0.85, 0]} rotation-z={-0.3}> | |
| <torusGeometry args={[0.26, 0.07, 8, 14, Math.PI]} /> | |
| <meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading /> | |
| </mesh> | |
| <mesh position={[0, 1.62, 0]} castShadow> | |
| <sphereGeometry args={[0.2, 14, 14]} /> | |
| <meshStandardMaterial color="#c9a24e" metalness={0.85} roughness={0.25} flatShading /> | |
| </mesh> | |
| <pointLight position={[0, 2.6, 0]} color="#ffd76a" intensity={18} distance={9} decay={1.6} /> | |
| </group> | |
| ) | |
| } | |
| /* ========================================================================= | |
| BANNER MU TRÊN VÒM — không khí fan quanh hang | |
| ========================================================================= */ | |
| function makeBannerTex(text) { | |
| const c = document.createElement('canvas') | |
| c.width = 512; c.height = 128 | |
| const g = c.getContext('2d') | |
| g.fillStyle = '#c8102e'; g.fillRect(0, 0, 512, 128) | |
| g.fillStyle = '#8f0f22'; g.fillRect(6, 6, 500, 116) | |
| g.strokeStyle = '#ffd76a'; g.lineWidth = 4; g.strokeRect(6, 6, 500, 116) | |
| g.fillStyle = '#ffffff'; g.font = '900 52px "Be Vietnam Pro", sans-serif' | |
| g.textAlign = 'center'; g.textBaseline = 'middle' | |
| g.fillText(text, 256, 66) | |
| const t = new THREE.CanvasTexture(c) | |
| t.colorSpace = THREE.SRGBColorSpace | |
| return t | |
| } | |
| function CaveBanners() { | |
| const texA = useMemo(() => makeBannerTex('GLORY GLORY'), []) | |
| const texB = useMemo(() => makeBannerTex('MAN UNITED'), []) | |
| const texC = useMemo(() => makeBannerTex('MU FAN'), []) | |
| const conf = [ | |
| { tex: texA, pos: [-14, 12.5, -24], rot: [0, 0.5, 0] }, | |
| { tex: texB, pos: [16, 12.8, -30], rot: [0, -0.45, 0] }, | |
| { tex: texC, pos: [0, 13.2, -8], rot: [0, 0, 0] }, | |
| ] | |
| return ( | |
| <group> | |
| {conf.map((c, i) => ( | |
| <group key={i} position={c.pos} rotation={c.rot}> | |
| <mesh> | |
| <planeGeometry args={[6, 1.5]} /> | |
| <meshBasicMaterial map={c.tex} transparent toneMapped={false} fog={false} /> | |
| </mesh> | |
| </group> | |
| ))} | |
| </group> | |
| ) | |
| } | |
| export function CaveScene() {` ], | |
| /* Wire vào CaveScene (pattern sau patch_app: <JerseyDisplay /> có sẵn) */ | |
| [ ` <JerseyDisplay /> | |
| <Entrance />`, | |
| ` <Trophy /> | |
| <CaveBanners /> | |
| <JerseyDisplay /> | |
| <Entrance />` ], | |
| ]) |