Hi George,
Thanks again for your suggestion to use a custom curve function and update the innerRadius
in an onTick
. I’ve implemented that approach in my component, like this:
// This is a component file. You can use this file to define a custom component for your project.
// This component will appear as a custom component in the editor.
import * as ecs from '@8thwall/ecs' // This is how you access the ecs library.
// Utils Function
function easeOutElastic(x: number): number {
const c4 = (2 * Math.PI) / 3
return x === 0
? 0
: x === 1
? 1
: 2 ** (-10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1
}
// Component
ecs.registerComponent({
name: 'Tap To Place Portal',
schema: {
portalContent: ecs.eid,
portalHiderRing: ecs.eid,
portalRim: ecs.eid,
portalShadow: ecs.eid,
uiTapToPlacePrompt: ecs.eid,
uiResetButton: ecs.eid,
uiOverlay: ecs.eid,
},
// schemaDefaults: {
// },
data: {
startElapsed: ecs.f64, // record when portalPlaced begins
},
// add: (world, component) => {
// },
// tick: (world, component) => {
// },
// remove: (world, component) => {
// },
stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
const {
portalContent,
portalHiderRing, portalRim,
portalShadow, uiTapToPlacePrompt,
uiResetButton, uiOverlay,
} = schemaAttribute.get(eid)
// DEFAULT: hide everything, then after 2s move to awaitTap
ecs.defineState('Default')
.initial()
.onEnter(() => {
console.log('Entered Default State')
// hide portal content
ecs.Hidden.set(world, portalContent)
// collapse portal visuals
ecs.Scale.set(world, portalRim, {
x: 0.001,
y: 0.001,
z: 0.001,
})
ecs.Scale.set(world, portalShadow, {
x: 0.001,
y: 0.001,
z: 0.001,
})
ecs.RingGeometry.set(world, portalHiderRing, {
innerRadius: 0.01,
outerRadius: 20,
})
// hide UI
ecs.Hidden.set(world, uiResetButton)
ecs.Hidden.set(world, uiTapToPlacePrompt)
})
.wait(1000, 'awaitTap')
// AWAIT TAP: show the “tap to place” prompt
ecs.defineState('awaitTap')
.onEnter(() => {
console.log('Entered awaitTap State')
ecs.Hidden.remove(world, uiTapToPlacePrompt)
})
// when screen is tapped, animate portal and go to portalPlaced
.onEvent(
ecs.input.UI_CLICK,
'portalPlaced',
{target: uiOverlay}
)
// PORTAL PLACED: keep portal open and show reset button
ecs.defineState('portalPlaced')
.onEnter(() => {
// Recenter the world origin at the camera’s current pose
const {XR8} = window as any
if (XR8 && XR8.XrController) {
XR8.XrController.recenter()
}
const data = dataAttribute.cursor(eid)
data.startElapsed = world.time.elapsed
// show portal content
ecs.Hidden.remove(world, portalContent)
// run your placement animations
ecs.ScaleAnimation.set(world, portalRim, {
toX: 4.3,
toY: 4.3,
toZ: 4.3,
autoFrom: true,
duration: 1500,
loop: false,
reverse: false,
easeIn: false,
easeOut: true,
easingFunction: 'Elastic',
})
ecs.ScaleAnimation.set(world, portalShadow, {
toX: 15.5,
toY: 2,
toZ: 11,
autoFrom: true,
duration: 1500,
loop: false,
reverse: false,
easeIn: false,
easeOut: true,
easingFunction: 'Elastic',
})
// hide prompt
ecs.Hidden.set(world, uiTapToPlacePrompt)
// reveal reset button
ecs.Hidden.remove(world, uiResetButton)
})
// onTick to animate innerRadius over the same ms as the other annimations
.onTick(() => {
const data = dataAttribute.cursor(eid)
const elapsed = world.time.elapsed - data.startElapsed
const duration = 1500
// compute progress 0→1
const progress = Math.min(elapsed / duration, 1)
if (progress < 1) {
// apply elastic easing
const t = easeOutElastic(progress)
const radius = 0.01 + (3.5 - 0.01) * t
ecs.RingGeometry.set(world, portalHiderRing, {
innerRadius: radius,
outerRadius: 20,
})
} else {
// final snap once
ecs.RingGeometry.set(world, portalHiderRing, {
innerRadius: 3.5,
outerRadius: 20,
})
}
})
// when the reset‐button entity receives a UI_CLICK, go back to Default
.onEvent(
ecs.input.UI_CLICK,
'Default',
{target: uiResetButton}
)
},
})
Even with this setup, where the onTick
uses the same duration and the easeOutElastic
function as the ScaleAnimation
s, the animation of the innerRadius
still seems a bit out of sync with the scaling of the rim and shadow.
Do you have any ideas why this might be happening, or if there’s anything in how I’ve implemented the onTick
that could be causing this subtle desync? Is there a better way to ensure manual property updates stay perfectly synchronized with the built-in animation components in the ECS?
Thanks again for your help and guidance on this!
Best,