Updated perf
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user