Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions src/components/CalibrationPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ function formatValue(v: number): string {
<div class="cal-row">
<span class="cal-label">Backlight</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="0" max="5" step="0.1"
<input type="range" class="cal-slider" min="0" max="15" step="0.1"
:value="store.config.lights.backlightIntensity"
@input="onLightSlider('backlightIntensity', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.backlightIntensity) }}</span>
Expand All @@ -166,12 +166,48 @@ function formatValue(v: number): string {
<div class="cal-row">
<span class="cal-label">Spotlight</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="0" max="2" step="0.1"
<input type="range" class="cal-slider" min="0" max="20" step="0.1"
:value="store.config.lights.spotlightIntensity"
@input="onLightSlider('spotlightIntensity', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.spotlightIntensity) }}</span>
</div>
</div>
<div class="cal-row">
<span class="cal-label">Spot X</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="-1" max="1" step="0.05"
:value="store.config.lights.spotlightX"
@input="onLightSlider('spotlightX', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.spotlightX) }}</span>
</div>
</div>
<div class="cal-row">
<span class="cal-label">Spot Y</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="-1" max="1" step="0.05"
:value="store.config.lights.spotlightY"
@input="onLightSlider('spotlightY', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.spotlightY) }}</span>
</div>
</div>
<div class="cal-row">
<span class="cal-label">Spot angle</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="10" max="90" step="1"
:value="store.config.lights.spotlightAngle"
@input="onLightSlider('spotlightAngle', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.spotlightAngle) }}</span>
</div>
</div>
<div class="cal-row">
<span class="cal-label">Spot soft</span>
<div class="cal-slider-wrap">
<input type="range" class="cal-slider" min="0" max="1" step="0.05"
:value="store.config.lights.spotlightPenumbra"
@input="onLightSlider('spotlightPenumbra', ($event.target as HTMLInputElement).value)" />
<span class="cal-value">{{ formatValue(store.config.lights.spotlightPenumbra) }}</span>
</div>
</div>
</div>

<!-- Card -->
Expand Down
63 changes: 54 additions & 9 deletions src/composables/useThreeScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function useThreeScene(containerRef: Ref<HTMLElement | null>) {
let camera: PerspectiveCamera | null = null
let animationId: number | null = null
let lastTime = performance.now() * 0.001
let dimStartTime = 0
let wasDimmed = false

// Card state
const cardMeshes = shallowRef<Mesh[]>([])
Expand Down Expand Up @@ -406,13 +408,8 @@ export function useThreeScene(containerRef: Ref<HTMLElement | null>) {
// Update off-axis camera
updateOffAxisCamera(store.eyePos.x, store.eyePos.y, store.eyePos.z)

// Update head-tracked spotlight position
// Spotlight (fixed position — intensity updated below)
const spotlight = scene.getObjectByName('headSpotlight') as SpotLight | undefined
if (spotlight) {
spotlight.position.set(store.eyePos.x * 0.8, store.eyePos.y * 0.8, store.eyePos.z * 0.6)
spotlight.target.position.set(0, 0, -store.dimensions.boxD * 0.5)
spotlight.target.updateMatrixWorld()
}

// Update tilt springs (gyroscope or mouse)
const tilt = gyroscope.isActive.value ? gyroscope : mouseTilt
Expand Down Expand Up @@ -533,10 +530,58 @@ export function useThreeScene(containerRef: Ref<HTMLElement | null>) {
back.intensity += (targetB - back.intensity) * dimRate
}

// Update spotlight intensity from config
// Track dim transition for candle stagger timing
if (store.isDimmed && !wasDimmed) dimStartTime = time
wasDimmed = store.isDimmed

// Animate candle lights + flames (staggered on, together off, with flicker)
for (let i = 0; i < 5; i++) {
const candle = scene.getObjectByName(`candle${i}`) as PointLight | undefined
const flame = scene.getObjectByName(`candleFlame${i}`) as Mesh | undefined
if (!candle) continue
if (store.isDimmed) {
const elapsed = time - dimStartTime
const delay = i * 0.3
const baseTarget = elapsed > delay ? 0.8 : 0
const flicker = baseTarget * (0.9 + 0.1 * Math.sin(time * (3 + i * 0.7)))
candle.intensity += (flicker - candle.intensity) * dimRate
// Flame visibility + wobble
if (flame) {
const targetOpacity = baseTarget > 0 ? 0.9 : 0
const mat = flame.material as { opacity: number }
mat.opacity += (targetOpacity - mat.opacity) * dimRate
const scaleFlicker = 0.85 + 0.15 * Math.sin(time * (4 + i * 1.1))
flame.scale.set(scaleFlicker, 0.9 + 0.2 * Math.sin(time * (3.5 + i * 0.9)), scaleFlicker)
}
} else {
candle.intensity += (0 - candle.intensity) * dimRate
if (flame) {
const mat = flame.material as { opacity: number }
mat.opacity += (0 - mat.opacity) * dimRate
}
}
}

// Update spotlight from config
if (spotlight) {
spotlight.intensity +=
(store.config.lights.spotlightIntensity - spotlight.intensity) * dimRate
const targetSpot = store.isDimmed ? 0 : store.config.lights.spotlightIntensity
spotlight.intensity += (targetSpot - spotlight.intensity) * dimRate
const { screenW, screenH, boxD } = store.dimensions
const spotX = store.config.lights.spotlightX
const spotY = store.config.lights.spotlightY
spotlight.position.set(
(screenW / 2) * spotX,
(screenH / 2) * spotY,
boxD * 0.1,
)
spotlight.target.position.set(
-(screenW / 2) * spotX * 0.4,
-(screenH / 2) * spotY * 0.2,
-boxD * 0.6,
)
spotlight.target.updateMatrixWorld()
spotlight.angle = (store.config.lights.spotlightAngle * Math.PI) / 180
spotlight.penumbra = store.config.lights.spotlightPenumbra
}

renderer.render(scene, camera)
Expand Down
12 changes: 8 additions & 4 deletions src/data/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ export const DEFAULT_CONFIG: AppConfig = {
webcamOffsetX: 0,
webcamOffsetY: 0,
lights: {
ambientIntensity: 0.4,
directionalIntensity: 2.4,
backlightIntensity: 1.6,
spotlightIntensity: 1.4,
ambientIntensity: 0.1,
directionalIntensity: 1.3,
backlightIntensity: 10.0,
spotlightIntensity: 1.3,
spotlightX: 0.4,
spotlightY: 1.0,
spotlightAngle: 48,
spotlightPenumbra: 1.0,
},
shaders: {
illustrationRare: {
Expand Down
66 changes: 59 additions & 7 deletions src/three/buildBox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {
AmbientLight,
BufferGeometry,
ConeGeometry,
CylinderGeometry,
DirectionalLight,
MeshBasicMaterial,
Line,
LineDashedMaterial,
Mesh,
Expand Down Expand Up @@ -313,22 +316,71 @@ export function buildBoxShell(
backLight.name = 'solidBack'
backLight.position.set(0, 0, -boxD * 0.7)
scene.add(backLight)

// Candle objects + lights along the bottom of the back wall
const candleSpacing = [-0.6, -0.3, 0, 0.3, 0.6]
const candleH = hh * 0.08
const candleR = hh * 0.012
const candleMat = new MeshStandardMaterial({
color: 0xf5f0e0,
emissive: 0x222018,
emissiveIntensity: 0.2,
roughness: 0.8,
})
for (let i = 0; i < candleSpacing.length; i++) {
const cx = hw * candleSpacing[i]!
const cy = -hh + candleH / 2
const cz = -boxD * 0.85

// Candle body
const body = new Mesh(new CylinderGeometry(candleR, candleR, candleH, 8), candleMat)
body.position.set(cx, cy, cz)
body.receiveShadow = true
scene.add(body)

// Flame (cone, transparent — faded in/out with candle light)
const flameH = candleH * 0.5
const flameR = candleR * 0.6
const flame = new Mesh(
new ConeGeometry(flameR, flameH, 6),
new MeshBasicMaterial({
color: 0xffcc44,
transparent: true,
opacity: 0,
depthWrite: false,
}),
)
flame.name = `candleFlame${i}`
flame.position.set(cx, -hh + candleH + flameH / 2, cz)
scene.add(flame)

// Point light (off by default, animated in dim mode)
const light = new PointLight(0xffaa44, 0, boxD * 0.6, 0)
light.name = `candle${i}`
light.position.set(cx, -hh + candleH + flameH * 0.4, cz)
scene.add(light)
}
} else {
scene.add(new AmbientLight(0xffffff, 0.05))
}

// Head-tracked spotlight for dynamic shadows
// Fixed spotlight casting shadows onto the walls
const spotX = lights?.spotlightX ?? 0.8
const spotY = lights?.spotlightY ?? 0.9
const spotAngle = lights?.spotlightAngle ?? 48
const spotPenumbra = lights?.spotlightPenumbra ?? 1.0
const spotI = lights?.spotlightIntensity ?? 0.6
const spotlight = new SpotLight(0xffffff, spotI, boxD * 3, Math.PI / 5, 0.4, 1.5)
const spotlight = new SpotLight(0xffffff, spotI, 0, (spotAngle * Math.PI) / 180, spotPenumbra, 0)
spotlight.name = 'headSpotlight'
spotlight.castShadow = true
spotlight.shadow.mapSize.width = 1024
spotlight.shadow.mapSize.height = 1024
spotlight.shadow.mapSize.width = 4096
spotlight.shadow.mapSize.height = 4096
spotlight.shadow.radius = 10
spotlight.shadow.camera.near = 5
spotlight.shadow.camera.far = boxD * 3
spotlight.shadow.camera.far = dims.eyeDefaultZ + boxD
spotlight.shadow.bias = -0.001
spotlight.position.set(0, 0, dims.eyeDefaultZ * 0.5)
spotlight.target.position.set(0, 0, -boxD / 2)
spotlight.position.set(hw * spotX, hh * spotY, boxD * 0.1)
spotlight.target.position.set(-hw * spotX * 0.4, -hh * spotY * 0.2, -boxD * 0.6)
scene.add(spotlight)
scene.add(spotlight.target)
}
6 changes: 5 additions & 1 deletion src/three/buildCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
DoubleSide,
Mesh,
MeshBasicMaterial,
MeshDepthMaterial,
PlaneGeometry,
RGBADepthPacking,
RGBAFormat,
ShaderMaterial,
UnsignedByteType,
Expand Down Expand Up @@ -152,7 +154,7 @@ export function buildCardMesh(
uniforms.uBirthdayDank2Tex = { value: birthdayTextures?.dank2 ?? blackPixel }
}

return new Mesh(
const mesh = new Mesh(
cardGeo,
new ShaderMaterial({
uniforms,
Expand All @@ -162,6 +164,8 @@ export function buildCardMesh(
transparent: true,
}),
)
mesh.customDepthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking })
return mesh
}

export function buildActivationMaterial(
Expand Down
4 changes: 4 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export interface LightConfig {
directionalIntensity: number
backlightIntensity: number
spotlightIntensity: number
spotlightX: number
spotlightY: number
spotlightAngle: number
spotlightPenumbra: number
}

export interface SceneConfig {
Expand Down