🎉

react-native-skia-sprite-animator v0.2.0 をリリースしました

に公開

React Native/Expo で @shopify/react-native-skia を使ってスプライトアニメーションを扱うための小さなライブラリ、react-native-skia-sprite-animator の v0.2.0 を公開しました。

このライブラリは UI を含まない「再生(SpriteAnimator)」と「保存(spriteStorage)」の 2 機能に特化しており、アプリ側で自由に UI/エディタを組めるのが特徴です。今回の v0.2.0 は Godot の AnimatedSprite2D に近い操作性を意識しつつ、制御 API とイベント、反転・速度制御などを強化しました。

目次

  1. ライブラリの概要と思想
  2. v0.2.0 のハイライト
  3. 導入方法(インストール)
  4. SpriteAnimator 使い方(最小例 → 制御 → イベント)
  5. データ構造(SpriteData / JSON 例)
  6. ストレージ API(spriteStorage)
  7. よくある質問・Tips
  8. ロードマップと今後

ライブラリの概要と思想

react-native-skia-sprite-animator は、以下の 2 点に責務を絞っています。

  • 再生: Skia Canvas 上でスプライトシートから矩形を切り、フレーム/アニメーションを再生する SpriteAnimator コンポーネント。
  • 保存: expo-file-system へ画像と JSON を保存・読み込みする spriteStorage ユーティリティ。

「UI を含めない」ことで、アプリごとに異なるエディタ UI や操作体系(タップ/ドラッグ、ホットキー、ゲーム内ツールなど)を自由に実装できます。ライブラリはあくまで軽量な「基盤」として、再生とデータ入出力だけを担います。

v0.2.0 のハイライト

今回のアップデートは、再生制御と開発体験を中心に「実運用で使える最小セット」を整えました。主な追加・改善点は以下です(既存の基本機能に対して概ね非破壊的に追加しています)。

  • 複数アニメーションの切り替えに対応
    • animations でアニメーション名 → フレーム番号配列を定義、initialAnimation で起動時に選択可能
    • 実行時オーバーライド用の animations/animationsMeta props を用意(data.animations* とマージして利用)
  • Imperative Handle を公開し、コードから直接制御可能
    • play(name, { fromFrame, speedScale }), stop, pause, resume, setFrame, isPlaying, getCurrentAnimation
  • フレームイベントを追加
    • onFrameChange({ animationName, frameIndex, frameCursor })
    • onAnimationEnd(name)(非ループ時に一度だけ)
    • 既存の onEnd() も維持(非ループ時に一度だけ)
  • 速度・反転・スケール
    • fps / duration(フレーム毎ミリ秒)で速度制御、speedScale で倍率指定
    • flipX / flipY で水平方向・垂直方向の反転描画
    • spriteScale で描画スケール指定
  • 取り回しの向上
    • 画像ソースは require() アセットも SkImage も両対応
    • フレームが 1 枚のみの場合は自動的にタイマーを回さず静止表示
    • タイマーのクリーンアップや境界チェックの堅牢化

導入方法(インストール)

npm install react-native-skia-sprite-animator
# peer dependency
npx expo install react-native @shopify/react-native-skia expo-file-system

検証環境の例: Expo SDK 52 / React Native 0.82 / React 19

SpriteAnimator 使い方

最小例

import { SpriteAnimator, type SpriteData } from 'react-native-skia-sprite-animator';
import heroSheet from '../assets/hero.png';

const heroData: SpriteData = {
  frames: [
    { x: 0, y: 0, w: 64, h: 64 },
    { x: 64, y: 0, w: 64, h: 64 },
    { x: 128, y: 0, w: 64, h: 64 },
  ],
  animations: {
    idle: [0, 1, 2, 1],
    blink: [2],
  },
  meta: {
    displayName: 'Hero Sprite',
    origin: { x: 0.5, y: 1 },
  },
};

export function HeroPreview() {
  return (
    <SpriteAnimator
      image={heroSheet}
      data={heroData}
      initialAnimation="idle"
      animations={heroData.animations}
      autoplay
      fps={12}
      loop
      speedScale={1}
      flipX={false}
      flipY={false}
      spriteScale={1}
      style={{ width: 64, height: 64 }}
      onEnd={() => console.log('animation finished')}
    />
  );
}

ポイント

  • imagerequire() アセットも SkImage も渡せます。
  • frames{ x, y, w, h, duration? } の配列。duration が無いフレームは fps で再生。
  • animations{ name: number[] } でフレーム番号を列挙(例: idle: [0,1,2])。
  • animationsMeta でアニメーション単位の loop を上書き可能(例: { blink: { loop: false } })。
  • initialAnimation が無い場合は、最初のアニメーション or 生のフレーム順で再生します。

コードからの再生制御(Imperative Handle)

import { SpriteAnimator, type SpriteAnimatorHandle } from 'react-native-skia-sprite-animator';

const animatorRef = useRef<SpriteAnimatorHandle>(null);

return (
  <>
    <SpriteAnimator ref={animatorRef} data={heroData} image={heroSheet} autoplay={false} />
    <Button title="Play Idle" onPress={() => animatorRef.current?.play('idle')} />
    <Button title="Blink Once" onPress={() => animatorRef.current?.play('blink', { speedScale: 1.5 })} />
    <Button title="Pause" onPress={() => animatorRef.current?.pause()} />
    <Button title="Resume" onPress={() => animatorRef.current?.resume()} />
  </>
);

公開メソッド

  • play(name?: string, opts?: { fromFrame?: number; speedScale?: number })
  • stop() / pause() / resume()
  • setFrame(frameIndex: number)
  • isPlaying() / getCurrentAnimation()

フレームイベント

import type { SpriteAnimatorFrameChangeEvent } from 'react-native-skia-sprite-animator';

const onFrameChange = (e: SpriteAnimatorFrameChangeEvent) => {
  console.log(e.animationName, e.frameIndex, e.frameCursor);
};

<SpriteAnimator onFrameChange={onFrameChange} onAnimationEnd={(name) => console.log('end', name)} />;

データ構造(SpriteData / JSON 例)

SpriteData は JSON 化しやすい構造です。spriteStorage にそのまま渡して保存できます。

const data: SpriteData = {
  frames: [
    { x: 0, y: 0, w: 64, h: 64, duration: 120 },
    { x: 64, y: 0, w: 64, h: 64 },
  ],
  animations: {
    walk: [0, 1],
    blink: [1],
  },
  animationsMeta: {
    walk: { loop: true },
    blink: { loop: false },
  },
  meta: {
    displayName: 'Hero Walk',
    imageUri: 'file:///sprites/images/img_hero.png',
    origin: { x: 0.5, y: 1 },
    version: 2,
  },
};

フィールド要約

  • frames: スプライトシート上の矩形配列。duration はミリ秒指定(無ければ fps)。
  • animations: アニメーション名 → フレーム番号(配列)。
  • animationsMeta: アニメーション単位のメタ(loop など)。
  • meta: 任意メタデータ(displayName, origin, version, imageUri 等)。

ストレージ API(spriteStorage)

expo-file-system 上に /sprites/images/sprites/meta、レジストリ registry.json を作成し、画像と JSON を保存・読み込みします。

import {
  saveSprite,
  loadSprite,
  listSprites,
  deleteSprite,
  configureSpriteStorage,
  getSpriteStoragePaths,
  clearSpriteStorage,
  type SpriteSavePayload,
} from 'react-native-skia-sprite-animator';

const payload: SpriteSavePayload = {
  frames,
  meta: {
    displayName: 'Hero Walk',
    version: 1,
  },
  animations: {
    walk: [0, 1, 2],
  },
  animationsMeta: {
    blink: { loop: false },
  },
};

const saved = await saveSprite({ imageTempUri: tempImageUri, sprite: payload });
const items = await listSprites();
const full = await loadSprite(saved.id);
await deleteSprite(saved.id);

// 保存先パスの参照・変更
configureSpriteStorage({ rootDir: 'file:///custom-root/' });
const paths = getSpriteStoragePaths(); // { root, images, meta, registry }

// まるごと削除(テストや初期化用)
await clearSpriteStorage();

ディレクトリ構成(デフォルト)

  • .../sprites/images/ — 保存された PNG 等の画像
  • .../sprites/meta/ — スプライト JSON({ id, frames, animations*, meta }
  • .../sprites/registry.json — 一覧用のサマリ(id, displayName, imageUri, createdAt

エラー挙動

  • imageTempUri が空、frames が空のときは例外を投げます。
  • 書き込み可能なディレクトリが無い場合も例外(documentDirectory / cacheDirectory)。

よくある質問・Tips

  • Q. 画像ソースは require()SkImage のどちらを使えば良い?
    • A. アプリ資産なら require() が手軽です。エディタやクロップ処理を通してメモリ上にある画像を直接描くときは SkImage を渡してください(内部で自動判別)。
  • Q. 1 フレームだけの表示はできますか?
    • A. 可能です。フレームが 1 枚のときは再生タイマーを回さず静止表示します。
  • Q. ループ有無や速度をアニメーションごとに変えたい
    • A. animationsMeta{ [name]: { loop } } と、play(name, { speedScale }) で対応できます。
  • Q. UI は付属しますか?
    • A. いいえ。UI やエディタは各アプリ側の責務です。ライブラリは「再生」と「保存」に特化しています。

ロードマップと今後

  • v0.2.x: 細かなバグ修正と型整備、サンプル拡充
  • v0.3: 編集用のロジック(useSpriteEditor)やユーティリティ(矩形操作、スナップ等)
  • v0.4: JSON テンプレート API(出力フォーマットを差し替え可能)

ご意見・課題があれば Issue/Pull Request 歓迎です。軽量な基盤として、現場で必要な最小限を丁寧に積み上げていきます。


最後までお読みいただきありがとうございました。react-native-skia-sprite-animator が、React Native/Expo での 2D スプライト制作・運用を少しでも手軽にする助けになれば幸いです。🎮

Discussion