79 lines
4.5 KiB
TypeScript
79 lines
4.5 KiB
TypeScript
import type { FC } from 'react'
|
|||
|
|
import * as THREE from 'three'
|
||
|
|
import { SpinningModel } from './model3d'
|
||
|
|
|
||
|
|
// The Barricelli Luggage: Nils Aall Barricelli's patented suitcase-commode. Two wooden
|
||
|
|
// beams that taper upward carry three suitcases slung between them as drawers, so the
|
||
|
|
// whole thing reads as a rudimentary chest of drawers built from luggage.
|
||
|
|
|
||
|
|
const WOOD = new THREE.MeshStandardMaterial({ color: 0x6b4a2f, roughness: 0.85, metalness: 0.04, flatShading: true })
|
||
|
|
const BRASS = new THREE.MeshStandardMaterial({ color: 0xc9a24a, roughness: 0.45, metalness: 0.6, flatShading: true })
|
||
|
|
const STRAP = new THREE.MeshStandardMaterial({ color: 0x2f2118, roughness: 0.8, flatShading: true })
|
||
|
|
const LEATHERS = [0x8a5a34, 0x6f4632, 0x9a6b3f].map(color => new THREE.MeshStandardMaterial({ color, roughness: 0.8, metalness: 0.05, flatShading: true }))
|
||
|
|
|
||
|
|
// One suitcase drawer: leather body, a lid seam, two wrap-around straps, brass latches,
|
||
|
|
// corner caps, and a front pull (it is a drawer now, so the handle faces the viewer).
|
||
|
|
function buildSuitcase(width: number, height: number, depth: number, leather: THREE.Material) {
|
||
|
|
const suitcase = new THREE.Group()
|
||
|
|
const body = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth), leather); suitcase.add(body)
|
||
|
|
const seam = new THREE.Mesh(new THREE.BoxGeometry(width + 0.02, 0.04, depth + 0.02), STRAP); suitcase.add(seam)
|
||
|
|
for (const sign of [-1, 1]) {
|
||
|
|
const strap = new THREE.Mesh(new THREE.BoxGeometry(0.07, height + 0.02, depth + 0.04), STRAP)
|
||
|
|
strap.position.x = sign * width * 0.3; suitcase.add(strap)
|
||
|
|
const latch = new THREE.Mesh(new THREE.BoxGeometry(0.1, 0.07, 0.03), BRASS)
|
||
|
|
latch.position.set(sign * width * 0.3, 0.02, depth / 2 + 0.01); suitcase.add(latch)
|
||
|
|
}
|
||
|
|
for (const sx of [-1, 1]) for (const sy of [-1, 1]) { // brass corner caps on the front face
|
||
|
|
const cap = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.07, 0.06), BRASS)
|
||
|
|
cap.position.set(sx * (width / 2 - 0.035), sy * (height / 2 - 0.035), depth / 2)
|
||
|
|
suitcase.add(cap)
|
||
|
|
}
|
||
|
|
const pull = new THREE.Mesh(new THREE.BoxGeometry(0.26, 0.05, 0.05), BRASS)
|
||
|
|
pull.position.set(0, -height * 0.16, depth / 2 + 0.05); suitcase.add(pull)
|
||
|
|
for (const sign of [-1, 1]) { const stem = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.03, 0.05), BRASS); stem.position.set(sign * 0.1, -height * 0.16, depth / 2 + 0.02); suitcase.add(stem) }
|
||
|
|
return suitcase
|
||
|
|
}
|
||
|
|
|
||
|
|
function buildBarricelliLuggage() {
|
||
|
|
const frame = new THREE.Group()
|
||
|
|
const postHeight = 2.1
|
||
|
|
const suitcase = { w: 1.05, h: 0.44, d: 0.62 }
|
||
|
|
const levels = [0.62, 0, -0.62] // three stacked drawers
|
||
|
|
|
||
|
|
for (const sign of [-1, 1]) { // two beams, each tapering upward, leaning slightly inward
|
||
|
|
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.085, 0.12, postHeight, 4), WOOD)
|
||
|
|
post.rotation.y = Math.PI / 4 // square the prism to face front
|
||
|
|
post.rotation.z = -sign * 0.05 // splay the feet, converge the tops
|
||
|
|
post.position.x = sign * 0.66
|
||
|
|
frame.add(post)
|
||
|
|
const foot = new THREE.Mesh(new THREE.BoxGeometry(0.24, 0.09, 0.32), WOOD)
|
||
|
|
foot.position.set(sign * 0.7, -postHeight / 2 - 0.02, 0); frame.add(foot)
|
||
|
|
}
|
||
|
|
// Top cap and a low stretcher tie the beams into a stand.
|
||
|
|
const cap = new THREE.Mesh(new THREE.BoxGeometry(1.44, 0.11, 0.5), WOOD); cap.position.y = postHeight / 2 + 0.02; frame.add(cap)
|
||
|
|
const stretcher = new THREE.Mesh(new THREE.BoxGeometry(1.4, 0.08, 0.18), WOOD); stretcher.position.y = -postHeight / 2 + 0.14; frame.add(stretcher)
|
||
|
|
|
||
|
|
levels.forEach((y, i) => {
|
||
|
|
const drawer = buildSuitcase(suitcase.w, suitcase.h, suitcase.d, LEATHERS[i % LEATHERS.length])
|
||
|
|
drawer.position.y = y; frame.add(drawer)
|
||
|
|
for (const sign of [-1, 1]) { // small wooden brackets mounting each case to the beams
|
||
|
|
const bracket = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.08, 0.3), WOOD)
|
||
|
|
bracket.position.set(sign * (suitcase.w / 2 + 0.05), y, 0); frame.add(bracket)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
frame.traverse(obj => { if (obj instanceof THREE.Mesh) obj.castShadow = true })
|
||
|
|
return frame
|
||
|
|
}
|
||
|
|
|
||
|
|
export const BarricelliLuggageMerit: FC<{ label: string; onComplete: () => void }> = ({ label, onComplete }) => (
|
||
|
|
<div className="cutscene-card merit-card">
|
||
|
|
<div className="title-card-inner">
|
||
|
|
<small className="merit-eyebrow">◆ MERIT AWARDED ◆</small>
|
||
|
|
<SpinningModel build={buildBarricelliLuggage} className="merit-stage" cameraPos={[0.9, 0.35, 4.9]} />
|
||
|
|
<h1>{label}</h1>
|
||
|
|
<button className="cutscene-begin" onClick={event => { event.stopPropagation(); onComplete() }}>Accept ▸</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|