Skip to content

Tweens: document what exists, and stop hand-rolling it - #1711

Merged
obiot merged 1 commit into
masterfrom
feat/tween-docs-and-conversions
Oct 4, 2026
Merged

obiot merged 1 commit into
masterfrom
feat/tween-docs-and-conversions

Conversation

@obiot

@obiot obiot commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

The engine has eleven easing families. The skills showed exactly one of
them, Quadratic.Out, twice. The one place that offered "the full list"
pointed at {@link Easing} — a page typedoc does not generate — so it went
nowhere, and nothing named Back, which is the difference between a fade and
an impact.

This started because I hand-rolled an easing curve by hand while the engine had
the named one sitting there, then found the same thing in two other examples.

Engine

  • Tween.Easing carries the list itself: all eleven families, what each is
    for, and the trap that Back and Elastic leave the range between your
    endpoints, so an opacity or a colour channel has to be clamped. Plus the note
    that overshoot follows the direction of travel, which is not obvious: a value
    tweened DOWN overshoots BELOW its target.
  • EasingFunction and InterpolationFunction exported as types. They were
    referenced by the documentation but not included in it — typedoc warned on
    both. Warnings go 166 → 164 and both type pages now exist.

easing.ts itself is untouched: Back has always been there. The gap was that
nothing pointed at it.

Skill

melonjs-scenes-and-state already had a good ## Tweens section. What it
lacked was the half that prevents the mistake rather than describing the
cure:

  • "You are hand-rolling a tween if you write this" — the longhand shape
    beside the tween, and the tells: a *Ms/*Age field only update() touches;
    a division by a *_MS constant; k * k or ** 1.4 inline, which is an
    easing spelled by hand; an if (x <= 0) arm that only cleans up at the end,
    which is onComplete; a second timer counting the same duration, which is
    delay.
  • "When NOT to reach for one" — a damp toward a moving target, an endless
    oscillation, a cooldown gate and a per-instance effect all look similar and
    are none of them tweens. The rule: fixed duration, known endpoint,
    one-shot, few instances.
  • "Pick the easing, not just the duration" — Back and Bounce by name.

Its triggers now match what someone writing one would say (fade out,
flash, pulse, punch, countdown in update) rather than the word "Tween",
which is precisely what they would never search for. melonjs-ui-and-text had
no ## Related skills block at all; it has one now, and
melonjs-renderables / melonjs-effects-and-shaders gained the pointer.

Also: where a Text gradient actually lives

Same file, same class of problem, and the one that started the whole thread.

Text.fillStyle accepts a Gradient; it never holds one. The
constructor routes a gradient to fillGradient and leaves fillStyle as the
pooled Color it always is, which is what keeps fillStyle.alpha gating the
fill and keeps the colour owned by the pool. So changing a ramp after
construction means assigning the field it landed in:

label.fillGradient = ramp;   // ✓
label.fillStyle = ramp;      // ✗ TS2740: Gradient is not a Color

I hit that TS2740 and misread it as a typing gap, nearly "fixing" it by
widening fillStyle to Color | Gradient — which would have broken the
alpha > 0 fill gate, the copy() path and the pooling. The engine is right;
the documentation was missing. No typing changed here, only the skill.

The melonjs-ui-and-text skill also gains a table of what fillStyle means in
each class, since the name is shared and the behaviour is not: a real Color
on Text against Renderable#tint on BitmapText, where white is the
absence of a tint rather than white glyphs, gradients are impossible, there
is no stroke, and size is a ratio rather than pixels.

Every claim in that section was checked against text.js and bitmaptext.js
rather than written from memory.

Conversions

Two, both one-shot singletons with a curve written by hand:

  • afterBurner's death wash. The effect lives on the camera rather than on a
    renderable, so nothing ticks it — which had forced a whole HUD#update(dt)
    and a call site in the game loop purely to drive the fade. Both are now
    gone.
    This is the case where a tween is not merely tidier: it removes
    machinery that existed only to work around not having one.
  • jungleRabbit's multiplier punch, which was Quadratic.Out written out as
    1 + MULT_PUNCH * (1 - k) * (1 - k).

Deliberately not converted: per-enemy hit flashes, score popups and
contrail nodes. Dozens are live at once and a float subtract beats a tween
each — which is the "when not to" rule earning its place.

Also in afterBurner

GAME OVER was a fixed 42px. It is now sized to 82% of the viewport, measured
from a throwaway bake rather than scaled with a transform, because Text
rasterizes at its font size and a scaled-up bake would be blurry at rest.
It takes a dark outline at 3% of the size, since red text over the red death
wash had almost no contrast at the one moment you are meant to read it. And it
drops with the same Back.Out stamp used elsewhere — with no shake, since
the death already fires shake(22, 900, force).

Verification

Engine suite 7534 passed | 10 skipped, npm run doc 0 errors, biome clean
across 731 files. The examples package sits at its 564 pre-existing type
errors, unchanged (counted with ANSI codes stripped — grep "error TS" silently
matches nothing against coloured tsc output, which had been hiding the real
figure).

All three touched examples were loaded in a browser and driven through start,
input and restart with zero page or console errors.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t

The engine has eleven easing families and the skills showed one of them,
Quadratic.Out, twice. The one place offering "the full list" pointed at
`{@link Easing}`, which typedoc has no page for, so it went nowhere. Nothing
named Back, which is the difference between a fade and an impact.

Tween.Easing now carries the list itself: every family, what each is for, and
the trap that Back and Elastic leave the range between the endpoints so an
opacity or a colour channel has to be clamped. EasingFunction and
InterpolationFunction are exported as types; they were referenced by the docs
but not included in them, which typedoc warned about twice.

The skill gains the half that prevents the mistake rather than describing the
cure: the longhand shape to recognise (a *Ms field only update() touches, a
division by a *_MS constant, `k * k` inline, an `if (x <= 0)` cleanup arm that
is onComplete), and when NOT to reach for a tween, since a damp toward a moving
target, an oscillation, a cooldown gate and a per-instance effect all look
similar and are not. Its triggers now match what someone WRITING one would say,
"fade out", "flash", "pulse", rather than the word Tween, which is what they
would never search for. melonjs-ui-and-text had no Related skills block at all.

Two conversions, both one-shot singletons with a curve spelled by hand:

- afterBurner's death wash. It lives on the camera rather than on a renderable
  so nothing ticked it, which had forced a whole HUD#update(dt) and a call site
  in the game loop purely to drive the fade. Both are gone.
- jungleRabbit's multiplier punch, which was Quadratic.Out written out as
  `1 + MULT_PUNCH * (1 - k) * (1 - k)`.

Deliberately not converted: per-enemy hit flashes, score popups and contrail
nodes, where dozens are live and a float subtract beats a tween each.

Also in afterBurner, GAME OVER is sized to the viewport instead of a fixed
42px, measured from a throwaway bake because Text rasterizes at its font size
and a scaled-up bake is blurry at rest. It takes a dark outline, since red text
over the red death wash had almost no contrast, and the same Back.Out drop
Earth Defender uses. No shake: the death already fires one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
Copilot AI balanced review requested due to automatic review settings October 4, 2026 00:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit ffe5321 into master Oct 4, 2026
6 checks passed
@obiot
obiot deleted the feat/tween-docs-and-conversions branch October 4, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants