From a06ca38c94ea44ef80fa8d5dd5d032f3d087a1a7 Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 15:53:05 +0900 Subject: [PATCH 1/6] feat: add winertia support flag --- src/types.ts | 2 ++ src/windowTracker.ts | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 6208657..12b2f16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,8 @@ export type WindowTrackerOptions = { }; export type WindowTrackerObj = { + /** winertia が有効なデバイスかどうか */ + supportsWinertia: boolean; /** トラッカーを1フレーム進めて新しい状態を返す。dtが0以下の場合はnull */ update: (now: number, shakeAccelThreshold: number) => MotionStateObj | null; }; diff --git a/src/windowTracker.ts b/src/windowTracker.ts index f6b556a..6bcb03c 100644 --- a/src/windowTracker.ts +++ b/src/windowTracker.ts @@ -27,6 +27,12 @@ export const createWindowTracker = ({ const history: AccelerationObj[] = []; + const supportsWinertia = + typeof window !== "undefined" && + "screenX" in window && + "screenY" in window && + !window.matchMedia("(pointer: coarse)").matches; + const update = (now: number, shakeAccelThreshold: number): MotionStateObj | null => { // 前フレームからの経過時間(秒)。0以下なら同一/逆行タイムスタンプなのでスキップ const dt = (now - lastTime) / 1000; @@ -106,5 +112,5 @@ export const createWindowTracker = ({ return result; }; - return { update }; + return { supportsWinertia, update }; }; From 723c671b03afef105ac55465330278cf0c5acfde Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 15:55:30 +0900 Subject: [PATCH 2/6] feat: update readme --- README.ja.md | 7 ++++++- README.md | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.ja.md b/README.ja.md index 440dd1e..7bffeea 100644 --- a/README.ja.md +++ b/README.ja.md @@ -52,7 +52,12 @@ requestAnimationFrame(loop); createWindowTracker(options?: WindowTrackerOptions): WindowTrackerObj ``` -トラッカーのインスタンスを生成します。引数は省略可能で、各プロパティも省略可能です。戻り値は `update` メソッドを1つ持つオブジェクト(以下の `WindowTrackerObj` )です。 +トラッカーのインスタンスを生成します。引数は省略可能で、各プロパティも省略可能です。戻り値は以下のプロパティを持つ `WindowTrackerObj` です。 + +| プロパティ | 型 | 詳細 | +| ------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `supportsWinertia` | `boolean` | winertia が有効なデバイスかどうか。SSRでない・`screenX`/`screenY`が取得可能・タッチデバイスでない、の3条件をすべて満たす場合に`true`。 | +| `update` | `Function` | トラッカーを1フレーム進めます。詳細は以下を参照。 | | オプション | 型 | デフォルト | 詳細 | | -------------------- | -------- | ---------- | ----------------------------------------------------- | diff --git a/README.md b/README.md index 925e441..713920e 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,12 @@ requestAnimationFrame(loop); createWindowTracker(options?: WindowTrackerOptions): WindowTrackerObj ``` -Creates a tracker instance. The `options` argument is optional, and so is each of its properties. Returns an object exposing a single `update` method (`WindowTrackerObj`, see below). +Creates a tracker instance. The `options` argument is optional, and so is each of its properties. Returns a `WindowTrackerObj` with the following properties: + +| Property | Type | Description | +| ------------------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `supportsWinertia` | `boolean` | Whether the current environment supports winertia — requires a non-SSR context, `screenX`/`screenY` availability, and a non-touch (coarse pointer) device. | +| `update` | `Function` | Advances the tracker by one frame. See below. | | Option | Type | Default | Description | | -------------------- | -------- | ------- | ----------------------------------------------------------------------------------------- | From 09559f451afc56bbecce5b87d61f903a46ef5106 Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 16:06:58 +0900 Subject: [PATCH 3/6] fix: test --- src/windowTracker.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/windowTracker.test.ts b/src/windowTracker.test.ts index 9da6e50..0060106 100644 --- a/src/windowTracker.test.ts +++ b/src/windowTracker.test.ts @@ -11,8 +11,15 @@ const setScreenCoord = (x: number, y: number) => { beforeEach(() => { currentNow = 0; + vi.spyOn(performance, "now").mockImplementation(() => currentNow); + setScreenCoord(0, 0); + + Object.defineProperty(window, "matchMedia", { + value: vi.fn().mockReturnValue({ matches: false }), + configurable: true, + }); }); afterEach(() => { From 7b47c156c00dd0137ca323da7b497e0b51b587b1 Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 16:15:07 +0900 Subject: [PATCH 4/6] feat: if supportsWinertia is not enabled, update returns null --- src/windowTracker.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/windowTracker.ts b/src/windowTracker.ts index 6bcb03c..9f4e74f 100644 --- a/src/windowTracker.ts +++ b/src/windowTracker.ts @@ -27,13 +27,15 @@ export const createWindowTracker = ({ const history: AccelerationObj[] = []; - const supportsWinertia = + const supportsWinertia: boolean = typeof window !== "undefined" && "screenX" in window && "screenY" in window && !window.matchMedia("(pointer: coarse)").matches; const update = (now: number, shakeAccelThreshold: number): MotionStateObj | null => { + if (!supportsWinertia) return null; + // 前フレームからの経過時間(秒)。0以下なら同一/逆行タイムスタンプなのでスキップ const dt = (now - lastTime) / 1000; if (dt <= 0) return null; From b72367105176dded743747e05cc6df3e7f304a6a Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 16:17:17 +0900 Subject: [PATCH 5/6] feat: add test --- src/windowTracker.test.ts | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/windowTracker.test.ts b/src/windowTracker.test.ts index 0060106..9953854 100644 --- a/src/windowTracker.test.ts +++ b/src/windowTracker.test.ts @@ -119,24 +119,17 @@ describe("createWindowTracker", () => { expect(third?.shakeCount).toBe(2); }); - it("historyはhistoryLength件を超えない、かつ返り値は以後のupdateで変化しないコピーである", () => { - const tracker = createWindowTracker({ historyLength: 3 }); - - let lastState = tracker.update(0, 999999); // 経過時間0なのでnull想定 - for (let i = 1; i <= 5; i++) { - currentNow = i * 100; - setScreenCoord(i * 10, 0); - lastState = tracker.update(currentNow, 999999); - } + it("supportsWinertia が false の場合、update は常に null を返す", () => { + Object.defineProperty(window, "matchMedia", { + value: vi.fn().mockReturnValue({ matches: true }), // タッチデバイス扱い + configurable: true, + }); + const tracker = createWindowTracker(); - expect(lastState?.history).toHaveLength(3); - const snapshot = [...(lastState?.history ?? [])]; + expect(tracker.supportsWinertia).toBe(false); - // さらに update しても、以前取得した history は変化しない - currentNow = 600; - setScreenCoord(60, 0); - tracker.update(currentNow, 999999); - - expect(lastState?.history).toEqual(snapshot); + currentNow = 1000; + setScreenCoord(100, 0); + expect(tracker.update(currentNow, 999999)).toBeNull(); }); }); From 7d67f8ae97d5742dc6e5a8050f092fed0c713cea Mon Sep 17 00:00:00 2001 From: 273* Date: Sat, 11 Jul 2026 16:18:29 +0900 Subject: [PATCH 6/6] feat: update readme --- README.ja.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.ja.md b/README.ja.md index 7bffeea..e83642c 100644 --- a/README.ja.md +++ b/README.ja.md @@ -79,7 +79,7 @@ update(now: number, shakeAccelThreshold: number): MotionStateObj | null | `now` | `number` | 現在のタイムスタンプ。通常は`performance.now()`。前回呼び出しからの経過時間の計算に使う。 | | `shakeAccelThreshold` | `number` | シェイクと判定する加速度の大きさ (px/s²)。 | -前回の呼び出しからの経過時間が0以下の場合(同じタイムスタンプで`update`が2回呼ばれた場合など)は`null`を返します。それ以外は`MotionStateObj`を返します。 +`supportsWinertia` が `false` の場合、または前回の呼び出しからの経過時間が0以下の場合(同じタイムスタンプで`update`が2回呼ばれた場合など)は`null`を返します。それ以外は`MotionStateObj`を返します。 | フィールド | 型 | 説明 | | --------------- | ---------------------------------- | -------------------------------------------------------------------- | diff --git a/README.md b/README.md index 713920e..b2cdf41 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Advances the tracker by one frame and returns the current state. Call this every | `now` | `number` | Current timestamp, typically `performance.now()`. Used to compute elapsed time since the previous call. | | `shakeAccelThreshold` | `number` | Acceleration magnitude (px/s²) above which a shake is detected. | -Returns `null` if the elapsed time since the previous call is zero or negative (e.g. `update` called twice with the same timestamp). Otherwise returns a `MotionStateObj`: +Returns `null` if `supportsWinertia` is `false`, or if the elapsed time since the previous call is zero or negative (e.g. `update` called twice with the same timestamp). Otherwise returns a `MotionStateObj`: | Field | Type | Description | | --------------- | ---------------------------------- | ------------------------------------------------------------------------------ |