# Game feel

Make jumps, hits and pickups feel good: squash and stretch, tweens, particles and trails from `@debuffjp/juice`, plus camera shake and hit-stop from the game itself. Press space or click in the example.

```ts playground
//+ import { Game } from "@debuffjp/core";
//+ import { Input } from "@debuffjp/input";
//+ import { Particles, squash, Tweens } from "@debuffjp/juice";
//+ const canvas = document.querySelector("#game")!;
//+ const game = new Game({ canvas, width: 320, height: 180, background: "#11140d" });
//+ const input = new Input(game, { bindings: { pop: ["Space", "Pointer"] } });
const tweens = new Tweens();
const particles = new Particles(() => game.random.next()); // seeded, so replays match
const player = { x: 160, y: 140, scaleX: 1, scaleY: 1, hop: 0 };

//+ game.add({ id: "play", update(dt) {
if (input.pressed("pop")) {
  squash(tweens, player, 0.35);
  tweens.to(player, { hop: 14 }, {
    duration: 0.12,
    onComplete: () => tweens.to(player, { hop: 0 }, { duration: 0.3, ease: "outBounce" }),
  });
  particles.emit({ x: player.x, y: player.y - 14, count: 16, colors: ["#cffb2d", "#38bdf8", "#f472b6"], shape: "circle", gravity: 300 });
  game.camera.shake(0.3);
  game.freeze(0.05);
}
tweens.update(dt);
particles.update(dt);
//+ }, render(draw) {
//+ draw.withCamera(game.camera, () => {
//+ draw.rect(6, player.y, 308, 30, "#1b2014");
//+ draw.push();
//+ draw.translate(player.x, player.y - player.hop);
//+ draw.scale(player.scaleX, player.scaleY);
//+ draw.rect(-14, -26, 28, 26, "#cffb2d");
//+ draw.rect(-8, -20, 5, 8, "#11140d");
//+ draw.rect(3, -20, 5, 8, "#11140d");
//+ draw.pop();
//+ particles.draw(draw);
//+ });
//+ draw.text("space or click", 8, 8, { color: "#9ca78a" });
//+ } }).go("play");
//+ game.start();
```

Create one `Tweens` and one `Particles` (or one per scene), and update both from your scene's `update`. Pass `Particles` the game's seeded random numbers, as above, so [replays](/docs/testing) and [scenarios](/docs/scenarios) produce the same sparks every run. Draw particles inside `draw.withCamera`, since they live in world space.

## Squash and stretch {#squash}

`squash(tweens, target, amount, duration)` sets the target's `scaleX` and `scaleY` to a squashed shape, then springs them back to 1. A positive `amount` squashes (wide and short, for landing); a negative one stretches (tall and thin, for a jump). The defaults are 0.3 and 0.25 seconds.

```ts
squash(tweens, player, -0.25); // take-off
squash(tweens, player, Math.min(0.35, impactSpeed / 1200)); // a harder landing squashes more
```

The target is any object with `scaleX` and `scaleY`. Apply them when you draw, with the origin at the feet so the character squashes into the floor instead of shrinking around its middle:

```ts
draw.push();
draw.translate(player.x, player.y); // y is the bottom of the sprite
draw.scale(player.scaleX, player.scaleY);
draw.sprite(frame, 0, 0, { origin: [0.5, 1] });
draw.pop();
```

`squash` replaces only the scale tweens on the target, so a move or fade that is already running keeps going. Before 0.8 it stopped every tween on the target.

## Tweens {#tweens}

`tweens.to(target, props, options)` moves any numeric properties of any object to new values over time.

```ts
tweens.to(coin, { y: coin.y - 20, alpha: 0 }, { duration: 0.4, ease: "outCubic" });

const t = tweens.to(door, { x: 64 }, { duration: 1, ease: "inOutQuad" });
await t.finished; // resolves when it completes, not when cancelled
```

| Option | Does |
| --- | --- |
| `duration` | Seconds. Required. |
| `ease` | A name below, or your own `(t) => number`. Default `outQuad`. |
| `delay` | Seconds to wait first. |
| `yoyo` | Play forward, then back. |
| `repeat` | Extra repetitions; -1 repeats forever. |
| `onComplete` | Called when it finishes. |

Eases: `linear`, `inQuad`, `outQuad`, `inOutQuad`, `inCubic`, `outCubic`, `inOutCubic`, `outBack` (overshoots, then settles), `outElastic` (wobbles), `outBounce` (bounces to a stop).

`tweens.kill(target)` stops every tween on an object, and `tweens.kill(target, ["x"])` only those on `x`. `tweens.has(target, "scaleX")` tells you whether one is running, which is how the templates let an idle wobble play only while nothing else is squashing the character.

## Particles {#particles}

`particles.emit(options)` releases a burst at a point. Every option except `x` and `y` has a default:

| Option | Default | Does |
| --- | --- | --- |
| `count` | 10 | Particles in the burst. |
| `speed` | `[40, 120]` | Speed range in px/s. |
| `angle`, `spread` | 0, full circle | Direction in radians, and the arc around it. `angle: -Math.PI / 2, spread: 0.8` sprays upwards. |
| `life` | `[0.3, 0.6]` | Lifetime range in seconds. Particles fade over the second half of their life. |
| `size`, `sizeEnd` | `[2, 4]`, 0 | Size range in px, and the fraction of it left at the end of life. |
| `colors` | `["#fff"]` | Each particle picks one. |
| `gravity` | 0 | Downward acceleration in px/s². |
| `drag` | 1 | Velocity kept per second; 0.1 slows them hard, 1 not at all. |
| `shape` | `"rect"` | Or `"circle"`. |
| `additive` | `false` | Additive blending, for glows and sparks. |

One `Particles` holds up to 2,000 particles at once (`new Particles(random, max)` changes that) and reuses them, so bursts don't allocate. For tens of thousands of particles or bullets, draw them yourself with `draw.sprites` ([Drawing](/docs/drawing#why-it-s-fast)).

## Trails {#trails}

A `Trail` draws a fading line behind something fast, such as a dash, a projectile or a sword swipe.

```ts
import { Trail } from "@debuffjp/juice";

const trail = new Trail(12, "#cffb2d", 3); // points kept, colour, width
trail.push(bullet.x, bullet.y); // every update
trail.draw(draw); // inside draw.withCamera
```

Call `trail.clear()` when the object respawns, or the trail draws a line across the screen.

## Camera shake {#shake}

`game.camera.shake(amount)` adds trauma from 0 to 1. The shake grows with the square of the trauma, so small hits stay small and big ones stack up, and it fades by itself.

```ts
game.camera.shake(0.3); // a hit
game.camera.shake(0.6); // an explosion
```

At trauma 1 the camera moves up to `game.camera.shakeAmplitude` pixels (8 by default), and loses `game.camera.shakeDecay` trauma a second (1.5 by default). Shake only shows on what you draw inside `draw.withCamera`, so the HUD stays still.

## Hit-stop and slow motion {#hit-stop}

`game.freeze(seconds)` pauses the simulation for a moment while drawing carries on. A few hundredths of a second on a hit makes it land harder.

```ts
game.freeze(0.06); // a hit
game.freeze(0.12); // the player dies
```

Freezing again while frozen keeps the longer of the two, so many hits in one frame don't add up to a long pause. `game.frozen` is `true` during one. `game.timeScale = 0.5` runs the simulation at half speed until you set it back to 1.

## Reduced motion {#reduced-motion}

Some players turn on reduced motion in their system settings. `game.reducedMotion` follows that setting live, and camera shake is turned down to a quarter while it's on, so it's still felt. Tone down your own effects too:

```ts
if (!game.reducedMotion) squash(tweens, player, 0.3);
```

Flashes, screen-wide particles and fast parallax are the usual candidates. `new Game({ reducedMotion: true })` forces it on (or `false` off), and a scenario can set `"reducedMotion": true` to capture how the game looks with it ([Scenarios](/docs/scenarios)).

## Tuning {#tuning}

How much to squash, shake and freeze is a matter of taste. Put the amounts in [params](/docs/params) and tune them with the game running:

```ts
const feel = game.params.define("feel", {
  shake: { type: "number", default: 0.3, min: 0, max: 1 },
  hitStop: { type: "number", default: 0.06, min: 0, max: 0.3, units: "s" },
});

game.camera.shake(feel.values.shake);
game.freeze(feel.values.hitStop);
```

[API reference](/api/@debuffjp/juice/)