Updated perf

This commit is contained in:
2026-09-09 18:01:25 -07:00
parent ad41f0e140
commit 2cfa0ca690
11 changed files with 485 additions and 46 deletions

View File

@@ -89,52 +89,83 @@ export function createFoilWrapper() {
return 0.011 * Math.sin(u * Math.PI * 96) + 0.005 * Math.sin(u * 173)
}
const sheetUV = sheets[0].mesh.geometry.getAttribute('uv')
const sheetSamples = Array.from({ length: sheetUV.count }, (_, i) => {
const u = sheetUV.getX(i)
const y = bottom + sheetUV.getY(i) * (top - bottom)
const x = (u * 2 - 1) * halfWidth
const across = u === 0 || u === 1 ? 0 : Math.pow(Math.sin(u * Math.PI), 0.24)
const lower = smooth(y, bottom + 0.14, bottom + 0.47)
const neck = 1 - smooth(y, 1.65, seam)
const envelope = across * lower
const diagonal = Math.sin(x * 13 + y * 7) * Math.sin(y * 3.1 - x * 2)
const folds = (0.009 * diagonal + 0.003 * Math.sin(x * 28 - y * 5))
* envelope * (0.35 + 0.65 * Math.pow(Math.abs(x / halfWidth), 1.5))
const crimp = 0.013 * (1 + 0.7 * Math.sin(x * 36))
* (1 - lower) * smooth(y, bottom, bottom + 0.1) * across
return { x, y, across, neck, envelope, folds, crimp,
opening: smooth(y, 0.6, seam), lip: smooth(y, 1, seam),
edgeY: y + tornEdge(u) * smooth(y, 1.85, seam) }
})
const stripSamples = Array.from({ length: stripUV.count }, (_, i) => {
const u = stripUV.getX(i)
const v = (bottom + stripUV.getY(i) * (top - bottom) - seam) / (top - seam)
return { u,
y: seam + v * (top - seam) + tornEdge(u) * (1 - v),
z: midDepth + 0.012 * Math.sin((u * 2 - 1) * halfWidth * 36) * Math.sin(v * Math.PI) }
})
for (const geometry of [...sheets.map(({ mesh }) => mesh.geometry), stripGeometry]) {
for (const name of ['position', 'normal']) {
const attribute = geometry.getAttribute(name)
const buffer = attribute instanceof THREE.InterleavedBufferAttribute ? attribute.data : attribute
buffer.setUsage(THREE.DynamicDrawUsage)
}
}
let lastMouth: number | undefined
let lastTear: number | undefined
let lastDetach: number | undefined
let lastStripExit: number | undefined
const lastTouch = new THREE.Vector3()
function deform(tear: number, detach: number, mouth: number, touch: THREE.Vector3, stripExit = 4.8) {
for (const { mesh, side } of sheets) {
if (mouth === lastMouth && (side === -1 || touch.equals(lastTouch))) continue
const geometry = mesh.geometry
const positions = geometry.getAttribute('position')
const uv = geometry.getAttribute('uv')
for (let i = 0; i < positions.count; i++) {
const u = uv.getX(i)
const y = bottom + uv.getY(i) * (top - bottom)
const x = (u * 2 - 1) * halfWidth
const across = u === 0 || u === 1 ? 0 : Math.pow(Math.sin(u * Math.PI), 0.24)
const lower = smooth(y, bottom + 0.14, bottom + 0.47)
const neck = 1 - smooth(y, 1.65, seam)
const opening = mouth * smooth(y, 0.6, seam)
const envelope = across * lower
const diagonal = Math.sin(x * 13 + y * 7) * Math.sin(y * 3.1 - x * 2)
const folds = (0.009 * diagonal + 0.003 * Math.sin(x * 28 - y * 5))
* envelope * (0.35 + 0.65 * Math.pow(Math.abs(x / halfWidth), 1.5))
const crimp = 0.013 * (1 + 0.7 * Math.sin(x * 36))
* (1 - lower) * smooth(y, bottom, bottom + 0.1) * across
const sample = sheetSamples[i]
const { x, y, across, neck, envelope, folds, crimp } = sample
const opening = mouth * sample.opening
const dent = side === 1 ? -0.045 * touch.z
* Math.exp(-((x - touch.x) ** 2 + (y - touch.y) ** 2) * 5) * envelope * neck : 0
const depth = 0.37 * neck + 0.64 * opening
const lip = mouth * (side === 1 ? -0.28 : 0.08) * across * smooth(y, 1, seam)
positions.setXYZ(i, x, y + tornEdge(u) * smooth(y, 1.85, seam) + lip,
const lip = mouth * (side === 1 ? -0.28 : 0.08) * across * sample.lip
positions.setXYZ(i, x, sample.edgeY + lip,
midDepth + side * (depth * envelope + folds * neck + crimp) + dent)
}
positions.needsUpdate = true
geometry.computeVertexNormals()
geometry.computeBoundingSphere()
}
lastMouth = mouth
lastTouch.copy(touch)
if (tear === lastTear && detach === lastDetach && (detach === 0 || stripExit === lastStripExit)) return
lastTear = tear
lastDetach = detach
lastStripExit = stripExit
const positions = stripGeometry.getAttribute('position')
const curvature = 0.55 + detach * 0.5
for (let i = 0; i < positions.count; i++) {
const u = stripUV.getX(i)
const v = (bottom + stripUV.getY(i) * (top - bottom) - seam) / (top - seam)
const { u, y, z } = stripSamples[i]
const length = Math.max(0, tear - u) * halfWidth * 2
const curvature = 0.55 + detach * 0.5
const angle = length * curvature
const anchor = (Math.max(tear, u) * 2 - 1) * halfWidth
const release = smooth(length, 0, 0.3)
// The released end rolls up around the moving tear front; the attached end stays sealed.
positions.setXYZ(i,
anchor - Math.sin(angle) / curvature + detach * stripExit,
seam + v * (top - seam) + tornEdge(u) * (1 - v)
+ 0.36 * (1 - Math.cos(angle)) / curvature + release * 0.035 + detach * 0.4,
midDepth + 0.012 * Math.sin((u * 2 - 1) * halfWidth * 36) * Math.sin(v * Math.PI)
- 0.85 * (1 - Math.cos(angle)) / curvature - detach * 0.65)
y + 0.36 * (1 - Math.cos(angle)) / curvature + release * 0.035 + detach * 0.4,
z - 0.85 * (1 - Math.cos(angle)) / curvature - detach * 0.65)
}
positions.needsUpdate = true
stripGeometry.computeVertexNormals()

View File

@@ -565,6 +565,7 @@ let lastPerformanceUpdate = performance.now()
let lastFrameTime: number | undefined
let pack: PackOpening | undefined
let packLoading = false
let packUIDirty = false
let packError: string | undefined
function updateMaterial() {
@@ -684,21 +685,28 @@ function updateStatus() {
}
function updatePackUI() {
packUIDirty = false
if (mode !== 'Pack') return
const view = pack?.view
packControlsElement.dataset.state = pack?.state ?? (packError ? 'error' : 'loading')
packControlsElement.dataset.paused = String(pack?.paused ?? false)
packTearProgress.hidden = !!pack && pack.state !== 'sealed' && pack.state !== 'opening'
packTearProgress.value = pack?.tearProgress ?? 0
packPrimaryButton.textContent = view?.action ?? (packError ? 'Retry loading' : 'Loading…')
packPrimaryButton.disabled = packLoading || pack?.state === 'complete'
packRestartButton.disabled = !pack
packSkipButton.disabled = !pack || pack.state === 'complete'
packSkipButton.title = 'Finish this motion or move to the next resting state'
const state = pack?.state ?? (packError ? 'error' : 'loading')
const paused = String(pack?.paused ?? false)
if (packControlsElement.dataset.state !== state) packControlsElement.dataset.state = state
if (packControlsElement.dataset.paused !== paused) packControlsElement.dataset.paused = paused
setIfChanged(packTearProgress, 'hidden', !!pack && pack.state !== 'sealed' && pack.state !== 'opening')
setIfChanged(packTearProgress, 'value', pack?.tearProgress ?? 0)
setIfChanged(packPrimaryButton, 'textContent', view?.action ?? (packError ? 'Retry loading' : 'Loading…'))
setIfChanged(packPrimaryButton, 'disabled', packLoading || pack?.state === 'complete')
setIfChanged(packRestartButton, 'disabled', !pack)
setIfChanged(packSkipButton, 'disabled', !pack || pack.state === 'complete')
setIfChanged(packSkipButton, 'title', 'Finish this motion or move to the next resting state')
const status = view?.status ?? packError ?? 'Preparing the prototype pack…'
if (packStatusElement.textContent !== status) packStatusElement.textContent = status
statusElement.textContent = 'Foil tear proof · 3 authored cards · no random rewards'
hintElement.textContent = view?.hint ?? 'Local reference artwork · approved runtime card materials'
setIfChanged(packStatusElement, 'textContent', status)
setIfChanged(statusElement, 'textContent', 'Foil tear proof · 3 authored cards · no random rewards')
setIfChanged(hintElement, 'textContent', view?.hint ?? 'Local reference artwork · approved runtime card materials')
}
function setIfChanged<T, K extends keyof T>(target: T, key: K, value: T[K]) {
if (target[key] !== value) target[key] = value
}
async function preparePack() {
@@ -739,7 +747,7 @@ async function preparePack() {
normalMap,
environment: activeEnvironment,
lightPosition,
onChange: updatePackUI,
onChange: () => { packUIDirty = true },
})
scene.add(pack.root)
pack.syncLighting(frontMaterial)
@@ -1462,6 +1470,7 @@ function animate(frameTime: number) {
}
if (mode === 'Pack') pack?.tick(frameTime)
if (packUIDirty) updatePackUI()
renderer.render(scene, mode === 'Pack' && pack ? pack.camera : camera)
}

View File

@@ -112,6 +112,8 @@ export class PackOpening {
private readonly touch = new THREE.Vector3()
private touchTarget = 0
private lastTick: number | undefined
private wrapperDirty = false
private wrapperStripExit = 4.8
private readonly pointers = new Map<number, THREE.Vector2>()
private tap: { id: number; x: number; y: number; at: number } | undefined
private pinchDistance: number | undefined
@@ -234,7 +236,7 @@ export class PackOpening {
this.updateCamera()
this.wrapper.root.visible = true
setPose(this.wrapper.root, pose(0, 0, 0))
this.wrapper.deform(0, 0, 0, this.touch)
this.invalidateWrapper()
this.cards.forEach((card, index) => {
card.object.visible = true
card.front.visible = false
@@ -244,17 +246,15 @@ export class PackOpening {
this.setState('sealed')
}
private shapeWrapper() {
const stripExit = (this.camera.position.z + 4)
private invalidateWrapper() {
this.wrapperDirty = true
this.wrapperStripExit = (this.camera.position.z + 4)
* Math.tan(THREE.MathUtils.degToRad(this.camera.fov / 2)) * this.camera.aspect + 2
this.wrapper.deform(this.tearProgress,
THREE.MathUtils.smoothstep(this.openingProgress, 0.38, 0.52),
THREE.MathUtils.smoothstep(this.openingProgress, 0.5, 0.62), this.touch, stripExit)
}
private setOpeningProgress(value: number) {
this.openingProgress = value
this.shapeWrapper()
this.invalidateWrapper()
const smooth = THREE.MathUtils.smoothstep
const settling = smooth(value, 0.87, 1)
const framing = smooth(value, 0.52, 0.76) * (1 - settling)
@@ -397,13 +397,23 @@ export class PackOpening {
}
tick(now: number) {
this.updateMotion(now)
if (this.active && this.wrapperDirty && this.wrapper.root.visible) {
this.wrapper.deform(this.tearProgress,
THREE.MathUtils.smoothstep(this.openingProgress, 0.38, 0.52),
THREE.MathUtils.smoothstep(this.openingProgress, 0.5, 0.62), this.touch, this.wrapperStripExit)
this.wrapperDirty = false
}
}
private updateMotion(now: number) {
const delta = Math.min(0.05, Math.max(0, (now - (this.lastTick ?? now)) / 1000))
this.lastTick = now
if (!this.active) return
if (Math.abs(this.touch.z - this.touchTarget) > 0.001) {
this.touch.z = this.reducedMotion.matches ? this.touchTarget
: THREE.MathUtils.lerp(this.touch.z, this.touchTarget, 1 - Math.exp(-delta * 18))
this.shapeWrapper()
this.invalidateWrapper()
}
const motion = this.motion
if (!motion || this.paused) return
@@ -423,7 +433,7 @@ export class PackOpening {
private pause(now: number) {
const interrupted = this.motion !== undefined
this.tick(now)
this.updateMotion(now)
if (this.motion && !this.paused) {
this.motion.elapsed += now - this.motion.startedAt
this.paused = true
@@ -493,7 +503,7 @@ export class PackOpening {
if (local) {
this.touch.x = THREE.MathUtils.clamp(local.x, -1.68, 1.68)
this.touch.y = THREE.MathUtils.clamp(local.y, -2.24, 1.7)
this.shapeWrapper()
this.invalidateWrapper()
}
}
}