Processes & Tweens
YAGE’s process system (from @yagejs/core) gives you timers, tweens, and
sequenced workflows that run inside the game loop. A process tied to an entity
is cancelled automatically when that entity is destroyed. Scene-scoped and
engine-global processes follow their scene or the engine instead.
Picking the Right Tool
Section titled “Picking the Right Tool”The process system has four overlapping primitives. They’re not redundant — each is the cleanest fit for a different shape of problem.
| Use | Reach for |
|---|---|
| Wait N seconds then run a callback | Process.delay() |
| Cooldown / restartable timer | pc.slot() |
| Animate a single property A → B | Tween.to() / .vec2() |
| Interpolate a number from→to with a custom setter | Tween.custom() |
| Arbitrary per-frame logic (no interpolation) | new Process({ update }) |
| Multi-step “do this, then this, then this” | Sequence |
| Run several animations together | Sequence.parallel() |
| Multi-point or non-monotonic animation curves | KeyframeAnimator |
| Fire discrete events at specific times | KeyframeAnimator + event |
A few rules of thumb:
Tweenis for two-point animations — start value, end value, easing. Reach for it first; it’s the simplest of the bunch.Sequenceis for orchestration, not animation. If your problem fits on a timeline (“flash, wait, fade, destroy”), it’s a sequence — even when the individual steps happen to be tweens.KeyframeAnimatoris for curves a tween can’t draw. A bobbing motion that goes up, back down, and past the start; an idle loop with three hand-tuned poses; a track that needs to fire a sound at exactly t=0.4s. If a single ease curve isn’t enough, you want keyframes.pc.slot()is for state, not animation. Cooldowns, invincibility windows, “time until next spawn” — anything you’ll check repeatedly withslot.completed.
When in doubt, start with a tween or a sequence. Promote to
KeyframeAnimator only when you find yourself layering multiple tweens to
fake one curve.
ProcessComponent
Section titled “ProcessComponent”Add a ProcessComponent to any entity to gain access to entity-level timing:
import { ProcessComponent } from "@yagejs/core";
entity.add(new ProcessComponent());const pc = entity.get(ProcessComponent);A slot is a reusable, restartable process handle. It is ideal for cooldowns, timers, and anything that needs to be checked or restarted repeatedly.
const cooldown = pc.slot({ duration: 0.3 });
function fire() { // spawn a projectile}
// In update():if (cooldown.completed) { cooldown.start(); // begin the cooldown fire();}restart() combines cancel + start in one call, which is useful when you want
to reset a timer that may still be running:
cooldown.restart();Both take one-off overrides that apply to that run only. The slot keeps the config it was created with for every later start:
const dash = pc.slot({ duration: 0.4 });
dash.start({ duration: 0.15 }); // this dash is short// ...after it completes:dash.start(); // back to 0.4start() on a slot that is still running does nothing, overrides included —
use restart(overrides) to end the current run and start a new one.
Slots can also run callbacks on completion:
import { Component } from "@yagejs/core";
class HealthComponent extends Component { vulnerable = true;}
const invincibility = pc.slot({ duration: 2, onComplete: () => entity.get(HealthComponent).vulnerable = true,});A slot with loop: true never completes, so its onComplete never runs. To
run something every few seconds, loop a sequence instead:
import { Sequence } from "@yagejs/core";
function spawnWave() { // spawn the next wave of enemies}
// Spawn a wave every 2 seconds, for as long as the entity livespc.run(new Sequence().wait(2).call(() => spawnWave()).loop().build());Cancelling a slot keeps it registered so it can start again. When its owner will never reuse it, remove the slot from the component:
pc.removeSlot(invincibility);removeSlot() cancels an active slot, unregisters it, and returns true.
It returns false when the slot belongs to another ProcessComponent or was
already removed.
One-Off Processes
Section titled “One-Off Processes”For fire-and-forget delays or single-use timers, use pc.run():
import { Process, ProcessComponent, Transform, type Vec2 } from "@yagejs/core";
const pc = entity.get(ProcessComponent);
function spawnExplosion(at: Vec2) { // spawn the explosion effect at `at`}
pc.run(Process.delay(0.5, () => { spawnExplosion(entity.get(Transform).position); entity.destroy();}));pc.run() hands back the Process it scheduled, and toPromise() on that
process resolves when the run ends — completion, cancel(), or a reset for a
re-run. That is how an async method waits for work it scheduled:
import { ProcessComponent, Scene, Tween } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
class GameOverScene extends Scene { readonly name = "game-over";}
const pc = entity.get(ProcessComponent);const overlay = entity.get(SpriteComponent);const scenes = engine.scenes;
async function fadeOutAndLeave() { await pc .run(Tween.custom((v) => (overlay.alpha = v), 1, 0, 0.4)) .toPromise(); await scenes.replace(new GameOverScene());}The promise carries no result, and a process that has already completed resolves one immediately. Read whatever the process wrote once it resolves.
Durations
Section titled “Durations”Every duration is in seconds and must be finite and greater than zero.
Process, Process.delay, Sequence.wait, Tween.to/custom/vec2, and
a slot’s duration all throw on 0, a negative number, NaN, or Infinity,
naming the value they were given. Tween endpoints must be finite too, so a NaN in a position
value fails at the call instead of being written into the property.
Tween.stagger is the one exception on zero: its stepSeconds must be finite
and zero or greater, and passing 0 starts every item at once.
A duration completes on the tick that reaches it. Elapsed time is accumulated by adding each frame’s delta, so a duration built from whole steps — 0.25 s of 1/60 s steps — sums a fraction short of its target; the window still closes on step 15 rather than running one step long:
import { Process, ProcessComponent } from "@yagejs/core";
const pc = entity.get(ProcessComponent);
function endAttackWindow() { // close the attack's hit window}
// On the fixed clock at 60 Hz: fires on step 15.pc.run(Process.delay(0.25, () => endAttackWindow()), { clock: "fixed" });elapsed still reports the time that actually accumulated, overshoot included,
and a looping process carries only the overshoot into its next pass.
Choosing a Clock
Section titled “Choosing a Clock”Every process and slot on a ProcessComponent advances on one of two clocks:
"frame"(the default) — rendered-frame time. Right for visuals: tweens, fades, shakes."fixed"— the engine’s fixed timestep, the same clock physics steps on. Right for gameplay timing that must match a fixed-step simulation: attack windows, cooldowns, timers that gate physics-driven movement.
Pick the clock when you schedule the work:
import { Process, ProcessComponent } from "@yagejs/core";
const pc = entity.get(ProcessComponent);
function endDash() { // return the entity to normal movement}
// A dash window that stays in step with physics:pc.run(Process.delay(0.25, () => endDash()), { clock: "fixed" });
// A cooldown on the fixed clock:const cooldown = pc.slot({ duration: 0.3, clock: "fixed" });Why it matters: frame time and fixed time drift apart whenever a frame is
dropped, a single frame runs two fixed steps, or the display refreshes faster
than the fixed rate. A gameplay window on the frame clock then opens and
closes at slightly different simulation times each run. Keeping gameplay
timers on "fixed" and visuals on "frame" — even on the same entity —
removes that drift without making animations choppy.
Both clocks share the same pause and time-scale behavior for entity processes:
scene pause stops them, and global, scene, and per-entity time scales apply to
both. Fixed-clock processes advance after the physics step and before
component fixedUpdate(dt) calls. The slot’s clock is set at creation;
start()/restart() overrides cannot change it.
Gameplay subsystems pick the fixed clock for you: the abilities addon
schedules its timelines and cooldowns on "fixed" by default. The "frame"
default here applies to what you schedule directly — tweens, fades, and other
presentation work.
A KeyframeAnimator animation carries the choice on its own definition, since
the animator schedules the track for you: clock: "fixed" on one animation def
puts that track on the fixed clock and leaves the animator’s other animations
where they are.
Scoped Queues
Section titled “Scoped Queues”An entity-scoped process queue carries the clock for everything it enqueues.
It routes through the entity’s ProcessComponent and cancels only what it put
there:
import { makeEntityScopedQueue, Process } from "@yagejs/core";
function endDash() { // return the entity to normal movement}
const gameplay = makeEntityScopedQueue(entity, { clock: "fixed" });gameplay.run(Process.delay(0.25, () => endDash()));
// Tear down everything this queue started:gameplay.cancelAll();The clock is read when the queue is created, so run(p) takes none. One
queue carries one clock. Frame-clock visuals on the same entity belong in a
second queue, and that queue has its own cancelAll().
The scene and global queues take the same option. A round timer, a wave
spawner, or a boss enrage at 60 seconds belongs to the scene rather than to
any one entity. A timer scheduled on an entity’s ProcessComponent ends when
that entity is destroyed.
A scene resolves the ProcessSystem through its engine context, which is
bound by the time onEnter() runs:
import { Process, ProcessSystemKey, Scene, makeSceneScopedQueue,} from "@yagejs/core";
function spawnWave(wave: number) { // spawn the enemies of this wave}
class Arena extends Scene { readonly name = "arena";
onEnter(): void { const processSystem = this.context.resolve(ProcessSystemKey); const waves = makeSceneScopedQueue(processSystem, this, { clock: "fixed" }); waves.run(Process.delay(30, () => spawnWave(2), ["waves"])); }}A scene queue pauses with its scene and follows the scene’s time scale on
both clocks. makeGlobalScopedQueue(processSystem, { clock: "fixed" }) runs
under the global time scale alone, with no scene pause gating.
waves.cancelAll() cancels what that queue enqueued.
processSystem.cancelForScene(scene, "waves") cancels every process carrying
the "waves" tag in that scene, on both clocks. The tag lives on the process,
so the snippet passes it as Process.delay’s third argument.
Tweens
Section titled “Tweens”Tweens smoothly interpolate a value over time. They run as processes, so they respect pause state and entity lifetime.
import { ProcessComponent, Tween, Transform, Vec2, easeOutQuad, easeInOutQuad,} from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const pc = entity.get(ProcessComponent);const sprite = entity.get(SpriteComponent);const targetPos = new Vec2(400, 300);
// Tween a component property with a setter (durations in seconds)pc.run(Tween.custom((v) => (sprite.alpha = v), 1, 0, 0.3, easeOutQuad));
// Tween a Vec2 (e.g., position) — pass a setter that applies the valueconst t = entity.get(Transform);pc.run(Tween.vec2( (v) => t.setPosition(v.x, v.y), t.position, targetPos, 0.6, easeInOutQuad,));
// Custom tween — interpolates from→to over duration; setter receives// the eased value each framepc.run( Tween.custom( (v) => t.setScale(1 + v, 1 + v), 0, 0.5, 0.5, easeOutQuad, ),);Built-In Easings
Section titled “Built-In Easings”An easing takes t in [0,1] and returns 0 at t = 0 and 1 at t = 1.
easeLinear holds a constant speed; the ten families below each come in an
ease-in form (slow start), an ease-out form (slow finish) and an ease-in-out
form (slow at both ends).
| Family | Ease in | Ease out | Ease in-out |
|---|---|---|---|
| sine | easeInSine | easeOutSine | easeInOutSine |
| quad | easeInQuad | easeOutQuad | easeInOutQuad |
| cubic | easeInCubic | easeOutCubic | easeInOutCubic |
| quart | easeInQuart | easeOutQuart | easeInOutQuart |
| quint | easeInQuint | easeOutQuint | easeInOutQuint |
| expo | easeInExpo | easeOutExpo | easeInOutExpo |
| circ | easeInCirc | easeOutCirc | easeInOutCirc |
| back | easeInBack | easeOutBack | easeInOutBack |
| elastic | easeInElastic | easeOutElastic | easeInOutElastic |
| bounce | easeInBounce | easeOutBounce | easeInOutBounce |
The back and elastic families leave [0,1] between the endpoints, which is
the point of them — back reaches about 0.1 past an endpoint, easeInElastic
and easeOutElastic about 0.37, easeInOutElastic about 0.12. That overshoot
carries the interpolated value with it, so pair them with a target that
tolerates the excursion. A scale pop is the classic fit:
import { ProcessComponent, Transform, Tween, easeOutBack } from "@yagejs/core";
const pc = entity.get(ProcessComponent);const t = entity.get(Transform);
pc.run(Tween.custom((v) => t.setScale(v, v), 0, 1, 0.35, easeOutBack));A camera zoom is not: camera.zoomTo(0.1, 1, easeOutElastic) drives zoom
briefly negative and flips the view for those frames. The bounce family stays
inside [0,1] but rises, falls back and rises again.
Import any easing from @yagejs/core.
Sequences
Section titled “Sequences”Chain multiple steps into a linear or branching workflow using the sequence builder:
import { ProcessComponent, Sequence, Tween, easeOutQuad } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const pc = entity.get(ProcessComponent);const sprite = entity.get(SpriteComponent);
pc.run( new Sequence() .call(() => sprite.alpha = 1) .wait(0.2) .then(Tween.custom((v) => (sprite.alpha = v), 1, 0, 0.4, easeOutQuad)) .call(() => entity.destroy()) .build(),);Parallel Steps
Section titled “Parallel Steps”Run multiple processes at the same time within a sequence:
import { ProcessComponent, Sequence, Transform, Tween, Vec2, easeInQuad, easeOutQuad,} from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const pc = entity.get(ProcessComponent);const sprite = entity.get(SpriteComponent);const offscreen = new Vec2(-100, 300);const t = entity.get(Transform);
pc.run( new Sequence() .parallel( Tween.custom((v) => (sprite.alpha = v), 1, 0, 0.3, easeOutQuad), Tween.vec2((v) => t.setPosition(v.x, v.y), t.position, offscreen, 0.3, easeInQuad), ) .call(() => entity.destroy()) .build(),);The sequence advances past a .parallel() step once all of its children
complete.
Sequence.build() compiles the steps into the wrapping Process and returns
it. Nothing ticks until a runner drives that process — pass it to
pc.run(...), or to a scene or global queue’s run(...).
Cancelling a sequence cancels whatever step it is running. A tween instance
you built, passed to .then() or .parallel(), and still hold is cancelled
with the sequence, so it reads completed === true rather than staying
unfinished.
A built sequence is reusable: pass one as a step of another sequence, or repeat it, and it starts from its first step each time.
Looping
Section titled “Looping”import { ProcessComponent, Sequence, Tween, easeInOutQuad } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const pc = entity.get(ProcessComponent);const sprite = entity.get(SpriteComponent);
function flash() { // toggle the sprite's tint}
// Loop foreverpc.run( new Sequence() .then(Tween.custom((v) => (sprite.alpha = v), 1, 0.5, 0.5, easeInOutQuad)) .then(Tween.custom((v) => (sprite.alpha = v), 0.5, 1, 0.5, easeInOutQuad)) .loop() .build(),);
// Repeat a fixed number of timespc.run( new Sequence() .call(() => flash()) .wait(0.1) .repeat(5) .build(),);TimerEntity
Section titled “TimerEntity”For scene-level timing that doesn’t belong to any game object, use
TimerEntity. It comes with a ProcessComponent pre-attached so you don’t
need to create a custom entity:
import { TimerEntity, Process, Scene } from "@yagejs/core";
class NextLevel extends Scene { readonly name = "next-level";}
const timer = scene.spawn(TimerEntity);timer.run(Process.delay(3, () => { engine.scenes.replace(new NextLevel());}));TimerEntity exposes .run(), .slot(), .removeSlot(), and .cancel()
directly. They forward to its internal ProcessComponent, so you don’t need
to fetch the component manually.
Cancelling by Tag
Section titled “Cancelling by Tag”Tag processes so you can cancel groups of them later:
import { ProcessComponent, Transform, Tween, easeOutQuad } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const pc = entity.get(ProcessComponent);const sprite = entity.get(SpriteComponent);const t = entity.get(Transform);
pc.run(Tween.custom((v) => (sprite.alpha = v), 1, 0, 0.3, easeOutQuad), { tags: ["vfx"] });pc.run(Tween.custom((v) => t.setScale(v, v), 1, 2, 0.3, easeOutQuad), { tags: ["vfx"] });
// Cancel all processes tagged "vfx"pc.cancel("vfx");A process can carry multiple tags — { tags: ["vfx", "ui"] } lets the
same process be cancelled by either group.
This is useful for cleaning up visual effects when an entity changes state (for example, cancelling hit-flash tweens when the entity dies).
Keyframe Animation
Section titled “Keyframe Animation”Tween is great for point-to-point animations — “fade from 1 to 0”, “slide
from A to B”. When you need multi-point animation — a bobbing motion that
goes up, back down, and past the start, or a four-frame easing curve that
isn’t expressible as a single easeInOut — reach for KeyframeAnimator.
KeyframeAnimator is a component that hosts multiple named keyframe tracks
and runs any number of them concurrently. Each track is a list of (time, value) control points, and the animator interpolates between them on every
tick of the track’s clock, pushing the result to a setter function you supply.
import { KeyframeAnimator, ProcessComponent, Transform, Vec2, easeInOutQuad,} from "@yagejs/core";
const entity = scene.spawn("lantern");entity.add(new Transform({ position: new Vec2(100, 200) }));entity.add(new ProcessComponent());
const anim = entity.add( new KeyframeAnimator({ bob: { keyframes: [ { time: 0, data: 0 }, { time: 0.5, data: 10 }, { time: 1, data: 0 }, ], setter: (v) => { const t = entity.get(Transform); t.setPosition(t.position.x, 200 + (v as number)); }, loop: true, easing: easeInOutQuad, }, }),);
anim.play("bob");This lantern rises 10 pixels, falls back, and loops forever — something
neither a single Tween nor an easing function could express on its own.
KeyframeAnimator requires ProcessComponent on the same entity because it
runs as a process under the hood. Each keyframe’s time is in seconds
along the track.
A track needs at least two keyframes whose times are finite and sorted
ascending, and a duration and speed that are finite and greater than
zero — anything else throws when the animation starts. The first keyframe does not have to sit at time 0; a track
that starts at 0.2 s holds that keyframe’s value through the lead-in.
Multiple Tracks
Section titled “Multiple Tracks”You can declare several named animations and play them independently:
import { KeyframeAnimator, Transform } from "@yagejs/core";
const baseY = entity.get(Transform).position.y;
const anim = entity.add( new KeyframeAnimator<"bob" | "pulse">({ bob: { keyframes: [ { time: 0, data: 0 }, { time: 0.5, data: 10 }, { time: 1, data: 0 }, ], setter: (v) => { const t = entity.get(Transform); t.setPosition(t.position.x, baseY + (v as number)); }, loop: true, }, pulse: { keyframes: [ { time: 0, data: 1.0 }, { time: 0.4, data: 1.2 }, { time: 0.8, data: 1.0 }, ], setter: (v) => entity.get(Transform).setScale(v as number, v as number), loop: true, speed: 1.5, }, }),);
anim.play("bob");anim.play("pulse");// ... later:anim.stop("pulse");The generic parameter KeyframeAnimator<"bob" | "pulse"> gives you
autocomplete and compile-time checking on the track names — typos like
anim.play("bbo") become type errors.
Each KeyframeAnimationDef accepts the same options you’d expect:
| Option | Purpose |
|---|---|
keyframes | The array of { time, data, easing?, event? } control points |
setter | Optional — function called with the interpolated value on every tick of the track’s clock. Omit for pure-timeline tracks that only fire event callbacks |
clock | Clock that advances playback — "frame" (the default) or "fixed" |
loop | Restart at time 0 when the track finishes (default false) |
speed | Multiplier on track time (default 1) |
duration | Override the auto-computed track length |
easing | Default easing between keyframes (each keyframe can override) |
onEnter / onExit | Lifecycle callbacks when a track starts or stops |
You can also fire discrete events from within a track using the event
property on a keyframe — useful for syncing sound or VFX to animation beats.
Pure-timeline tracks (no setter)
Section titled “Pure-timeline tracks (no setter)”When all you need is a sequence of time-aligned side-effects — cutscene
beats, gameplay-rhythm cues, audio one-shots — leave setter off:
import { KeyframeAnimator } from "@yagejs/core";import { AudioManagerKey } from "@yagejs/audio";
const audio = context.resolve(AudioManagerKey);
new KeyframeAnimator({ intro: { keyframes: [ { time: 0, data: 0, event: () => audio.play("step") }, { time: 0.25, data: 0, event: () => audio.play("step") }, { time: 0.5, data: 0, event: () => audio.play("door") }, ], // no setter — the values aren't read, only the events matter },});When a timeline’s events drive gameplay rather than presentation — spawning a hitbox, opening a parry window, applying damage — put that animation on the fixed clock:
import { KeyframeAnimator, Transform } from "@yagejs/core";
function spawnHitbox(attack: "jab" | "cross") { // spawn the hitbox for `attack`}
function endCombo() { // return to idle}
new KeyframeAnimator({ combo: { clock: "fixed", keyframes: [ { time: 0, data: 0, event: () => spawnHitbox("jab") }, { time: 0.18, data: 0, event: () => spawnHitbox("cross") }, { time: 0.4, data: 0, event: () => endCombo() }, ], }, flash: { // No clock — the default "frame" keeps the visual smooth. keyframes: [ { time: 0, data: 1 }, { time: 0.1, data: 1.3 }, { time: 0.2, data: 1 }, ], setter: (v) => entity.get(Transform).setScale(v as number, v as number), },});Why it matters: on the frame clock, the hitbox at time: 0.18 spawns at a
different point in the fixed-step sequence on every run, because rendered-frame
time and fixed time drift apart. When a slow frame hits the
maxFixedStepsPerFrame cap, the fixed loop leaves part of that frame’s elapsed
time unsimulated, while a frame-clock timeline still advances by the whole
frame delta. On "fixed" the beats stay in step with the world they drive.
Setter-driven visuals stay on "frame". A setter on the fixed clock is written
only on fixed steps, so a rendered frame that runs no fixed step shows the
previous value. The clock is chosen per animation, so both can live on one
animator as above.
Lower-Level Primitives
Section titled “Lower-Level Primitives”Under the hood, KeyframeAnimator is built on two primitives you can use
directly when you don’t need the named-track machinery:
import { ProcessComponent, Transform, createKeyframeTrack, easeOutQuad, interpolate,} from "@yagejs/core";
const pc = entity.get(ProcessComponent);
// A one-off keyframe track as a Processpc.run( createKeyframeTrack({ keyframes: [ { time: 0, data: 0 }, { time: 0.6, data: 100 }, ], setter: (v) => { const t = entity.get(Transform); t.setPosition(v as number, t.position.y); }, }),);
// Raw interpolation for bespoke driver codeconst blended = interpolate(0, 100, 0.5, easeOutQuad); // ≈ 75Interpolatable — the type parameter for keyframe data and the
interpolate primitive — resolves to number | Vec2Like. Both are supported
out of the box; if you need to animate other types (colour, quaternion),
compose multiple number tracks or write a custom setter.
Common Recipes
Section titled “Common Recipes”Patterns you’ll write over and over. Copy, adapt, and tag them so you can cancel groups when state changes.
Damage Flash
Section titled “Damage Flash”A short red-then-white pulse on hit. Tag it so a follow-up hit cancels the in-flight flash before starting a new one.
import { type Entity, ProcessComponent, Sequence } from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
const TINT_HIT = 0xff5050;const TINT_NORMAL = 0xffffff;
function damageFlash(entity: Entity) { const sprite = entity.get(SpriteComponent); const pc = entity.get(ProcessComponent); pc.cancel("flash"); pc.run( new Sequence() .call(() => (sprite.tint = TINT_HIT)) .wait(0.12) .call(() => (sprite.tint = TINT_NORMAL)) .build(), { tags: ["flash"] }, );}For a smooth fade rather than a hard cut, tween a separate flashAmount
value 0 → 1 → 0 and resolve the tint per-frame inside the setter (channel-
wise lerp on 0xRRGGBB).
Squash & Stretch on Land
Section titled “Squash & Stretch on Land”Single keyframe track — scale Y squashes down, then bounces back. The “past the resting state” overshoot makes it feel responsive.
import { KeyframeAnimator, Transform, easeOutQuad } from "@yagejs/core";
const anim = entity.add(new KeyframeAnimator<"land">({ land: { keyframes: [ { time: 0, data: 1.0 }, { time: 0.08, data: 0.7 }, { time: 0.24, data: 1.1 }, { time: 0.36, data: 1.0 }, ], setter: (v) => entity.get(Transform).setScale(1, v as number), easing: easeOutQuad, },}));
// On landing:anim.play("land");Camera Shake
Section titled “Camera Shake”Random offset for a fixed window, decaying to zero. Shake intensity drives both magnitude and the per-frame jitter.
import { type Entity, ProcessComponent, Transform, Tween, easeOutQuad,} from "@yagejs/core";
function shake(camera: Entity, magnitude = 8, duration = 0.25) { const t = camera.get(Transform); const base = t.position; camera.get(ProcessComponent).run( Tween.custom( (k) => { const m = magnitude * (1 - k); t.setPosition( base.x + (Math.random() * 2 - 1) * m, base.y + (Math.random() * 2 - 1) * m, ); }, 0, 1, duration, easeOutQuad, ), { tags: ["shake"] }, );}Cancel with pc.cancel("shake") if a stronger shake supersedes a weaker
one. Reset the camera to its base position after cancellation if your camera
controller doesn’t already.
Hit-Pause (Time Freeze)
Section titled “Hit-Pause (Time Freeze)”A few frames of frozen time on a heavy impact. The scene’s SceneTime
service freezes the entire scene — components, processes, tweens, particles,
and physics — and restores it on a real-time timer:
import { SceneTimeKey } from "@yagejs/core";
// From a Component:this.use(SceneTimeKey).freezeFor(0.08); // 80ms freeze frameThe duration counts real seconds, so the freeze cannot stall its own timer. Overlapping freezes (or a freeze during slow motion) compose safely — see Hitstop, Slow Motion, and Freeze Frames.
Enemy Telegraph
Section titled “Enemy Telegraph”The “wind-up” before a heavy attack. Sequence orchestrates the windup, hold, and execution.
import { type Entity, ProcessComponent, Sequence, Tween, easeInOutQuad,} from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
function telegraph(enemy: Entity, attack: () => void) { const sprite = enemy.get(SpriteComponent); const pc = enemy.get(ProcessComponent); pc.run( new Sequence() .call(() => (sprite.tint = 0xff5050)) .then(Tween.custom((v) => (sprite.alpha = v), 1, 0.5, 0.2, easeInOutQuad)) .wait(0.3) .call(() => { sprite.alpha = 1; sprite.tint = 0xffffff; }) .call(attack) .build(), );}UI Fade In/Out
Section titled “UI Fade In/Out”Two tweens — one for entry, one for exit. Tag so a fade-out cancels an in-flight fade-in cleanly.
import { type Entity, ProcessComponent, Sequence, Tween, easeInQuad, easeOutQuad,} from "@yagejs/core";import { SpriteComponent } from "@yagejs/renderer";
function fadeIn(entity: Entity, duration = 0.2) { const sprite = entity.get(SpriteComponent); sprite.alpha = 0; entity.get(ProcessComponent).cancel("fade"); entity.get(ProcessComponent).run( Tween.custom((v) => (sprite.alpha = v), 0, 1, duration, easeOutQuad), { tags: ["fade"] }, );}
function fadeOut(entity: Entity, duration = 0.2, onDone?: () => void) { const sprite = entity.get(SpriteComponent); entity.get(ProcessComponent).cancel("fade"); entity.get(ProcessComponent).run( new Sequence() .then(Tween.custom((v) => (sprite.alpha = v), 1, 0, duration, easeInQuad)) .call(() => onDone?.()) .build(), { tags: ["fade"] }, );}Looping Idle (Two-Frame Bob)
Section titled “Looping Idle (Two-Frame Bob)”Lightweight idle motion for an NPC or pickup. KeyframeAnimator with
loop: true is a one-liner.
import { KeyframeAnimator, Transform, easeInOutQuad } from "@yagejs/core";
const baseY = entity.get(Transform).position.y;
const anim = entity.add(new KeyframeAnimator<"idle">({ idle: { keyframes: [ { time: 0, data: 0 }, { time: 0.8, data: 4 }, { time: 1.6, data: 0 }, ], setter: (v) => { const t = entity.get(Transform); t.setPosition(t.position.x, baseY + (v as number)); }, loop: true, easing: easeInOutQuad, },}));anim.play("idle");Cancellation Hygiene
Section titled “Cancellation Hygiene”Two patterns to remember:
- Tag every effect tween so you can cancel groups by name when state
changes.
pc.cancel("vfx")on death;pc.cancel("flash")before starting a new flash. - Processes auto-cancel on entity destroy. You don’t need cleanup logic
for the common case —
entity.destroy()cancels everything bound to it, down to the tweens a sequence started. Same for slots.
If you find yourself writing a manual _running boolean alongside a
process, you probably want a pc.slot() instead — it tracks completion
and exposes restart() / cancel() / completed for free.
Coding agents: fetch https://yage.dev/llms.txt first and prefer the Markdown references it links over these HTML pages. This page's Markdown counterpart is /llms/core-concepts.md.