# 手触り

ジャンプや攻撃、アイテム取得の手触りを良くします。`@debuffjp/juice` のスクワッシュ＆ストレッチ、トゥイーン、パーティクル、軌跡と、ゲーム本体のカメラの揺れとヒットストップを使います。例ではスペースキーを押すかクリックしてください。

```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()); // シード付きなのでリプレイでも同じになる
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();
```

`Tweens` と `Particles` を 1 つずつ（またはシーンごとに 1 つずつ）作り、どちらもシーンの `update` から更新します。上のように `Particles` にはゲームのシード付き乱数を渡しましょう。[リプレイ](/ja/docs/testing)や[シナリオ](/ja/docs/scenarios)で、毎回同じ火花が出ます。パーティクルはワールド座標にあるので、`draw.withCamera` の中で描きます。

## スクワッシュ＆ストレッチ {#squash}

`squash(tweens, target, amount, duration)` は対象の `scaleX` と `scaleY` をつぶれた形にしてから、ばねのように 1 へ戻します。`amount` が正ならつぶれ（横に広く低く、着地向け）、負なら伸びます（縦に細長く、ジャンプ向け）。デフォルトは 0.3 と 0.25 秒です。

```ts
squash(tweens, player, -0.25); // 踏み切り
squash(tweens, player, Math.min(0.35, impactSpeed / 1200)); // 強く着地するほど大きくつぶれる
```

対象は `scaleX` と `scaleY` を持つオブジェクトなら何でもかまいません。描くときに適用し、原点を足元に置きましょう。こうするとキャラクターは中心に向かって縮まず、床に向かってつぶれます。

```ts
draw.push();
draw.translate(player.x, player.y); // y はスプライトの下端
draw.scale(player.scaleX, player.scaleY);
draw.sprite(frame, 0, 0, { origin: [0.5, 1] });
draw.pop();
```

`squash` が置き換えるのは対象のスケールのトゥイーンだけなので、すでに動いている移動やフェードは続きます。0.8 より前は、対象のトゥイーンをすべて止めていました。

## トゥイーン {#tweens}

`tweens.to(target, props, options)` は、任意のオブジェクトの数値プロパティを時間をかけて新しい値へ動かします。

```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; // 完了したときに解決する（キャンセルでは解決しない）
```

| オプション | 内容 |
| --- | --- |
| `duration` | 秒数。必須です。 |
| `ease` | 下のイージング名か、自作の `(t) => number`。デフォルトは `outQuad` です。 |
| `delay` | 始まるまでに待つ秒数。 |
| `yoyo` | 進んでから逆再生で戻ります。 |
| `repeat` | 追加の繰り返し回数。-1 で無限に繰り返します。 |
| `onComplete` | 終わったときに呼ばれます。 |

イージング：`linear`、`inQuad`、`outQuad`、`inOutQuad`、`inCubic`、`outCubic`、`inOutCubic`、`outBack`（行き過ぎてから戻る）、`outElastic`（揺れる）、`outBounce`（弾んで止まる）。

`tweens.kill(target)` はオブジェクトのトゥイーンをすべて止め、`tweens.kill(target, ["x"])` は `x` のトゥイーンだけを止めます。`tweens.has(target, "scaleX")` で動いているかがわかります。テンプレートはこれを使い、ほかに何もつぶしていないときだけ待機中の揺れを再生しています。

## パーティクル {#particles}

`particles.emit(options)` は、ある地点からパーティクルを一度に放ちます。`x` と `y` 以外のオプションにはデフォルトがあります。

| オプション | デフォルト | 内容 |
| --- | --- | --- |
| `count` | 10 | 一度に出す数。 |
| `speed` | `[40, 120]` | 速さの範囲（px/s）。 |
| `angle`、`spread` | 0、全方向 | 向き（ラジアン）と、その周りの広がり。`angle: -Math.PI / 2, spread: 0.8` で上向きに飛び散ります。 |
| `life` | `[0.3, 0.6]` | 寿命の範囲（秒）。寿命の後半で薄れて消えます。 |
| `size`、`sizeEnd` | `[2, 4]`、0 | 大きさの範囲（px）と、寿命の終わりに残る割合。 |
| `colors` | `["#fff"]` | パーティクルごとに 1 色を選びます。 |
| `gravity` | 0 | 下向きの加速度（px/s²）。 |
| `drag` | 1 | 1 秒あたりに残る速度。0.1 で強く減速し、1 では減速しません。 |
| `shape` | `"rect"` | `"circle"` も使えます。 |
| `additive` | `false` | 加算合成。光や火花に使います。 |

`Particles` 1 つで同時に 2,000 個まで持てます（`new Particles(random, max)` で変えられます）。パーティクルは再利用されるので、放つたびにメモリを確保しません。何万ものパーティクルや弾は、`draw.sprites` で自分で描きましょう（[描画](/ja/docs/drawing#why-it-s-fast)）。

## 軌跡 {#trails}

`Trail` は、ダッシュや弾、剣の振りのように速く動くものの後ろに、薄れていく線を描きます。

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

const trail = new Trail(12, "#cffb2d", 3); // 残す点の数、色、太さ
trail.push(bullet.x, bullet.y); // 毎回の update で
trail.draw(draw); // draw.withCamera の中で
```

オブジェクトが復活するときは `trail.clear()` を呼びましょう。呼ばないと、画面を横切る線が描かれます。

## カメラの揺れ {#shake}

`game.camera.shake(amount)` は 0 から 1 の「トラウマ」を加えます。揺れの強さはトラウマの 2 乗に比例するので、小さな攻撃は小さいまま、大きな攻撃は重なって強くなり、揺れは自然に収まります。

```ts
game.camera.shake(0.3); // 攻撃が当たった
game.camera.shake(0.6); // 爆発
```

トラウマ 1 のとき、カメラは最大で `game.camera.shakeAmplitude` ピクセル（デフォルトは 8）動きます。トラウマは 1 秒に `game.camera.shakeDecay`（デフォルトは 1.5）ずつ減ります。揺れるのは `draw.withCamera` の中で描いたものだけなので、HUD は動きません。

## ヒットストップとスローモーション {#hit-stop}

`game.freeze(seconds)` は、描画を続けたままシミュレーションを一瞬止めます。攻撃が当たったときに数百分の 1 秒止めると、手応えが強くなります。

```ts
game.freeze(0.06); // 攻撃が当たった
game.freeze(0.12); // プレイヤーがやられた
```

止まっている間にもう一度呼ぶと、長いほうが残ります。同じフレームに何度当たっても、止まる時間は合計されません。止まっている間は `game.frozen` が `true` です。`game.timeScale = 0.5` にすると、1 に戻すまでシミュレーションが半分の速さで進みます。

## 動きを減らす設定 {#reduced-motion}

システム設定で動きを減らすようにしているプレイヤーもいます。`game.reducedMotion` はその設定にその場で従い、オンの間はカメラの揺れが 4 分の 1 に抑えられます。それでも揺れは感じられます。自分で加えたエフェクトも控えめにしましょう。

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

よく対象になるのは、フラッシュ、画面全体のパーティクル、速い視差スクロールです。`new Game({ reducedMotion: true })` で強制的にオン（`false` でオフ）にできます。シナリオで `"reducedMotion": true` を設定すれば、その状態の見た目をキャプチャできます（[シナリオ](/ja/docs/scenarios)）。

## 調整 {#tuning}

どれだけつぶし、揺らし、止めるかは好みの問題です。量は[パラメータ](/ja/docs/params)にして、ゲームを動かしながら調整しましょう。

```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 リファレンス](/api/@debuffjp/juice/)