自作スプライトエディタで作ったJSONをCanvasで再生する最小サンプル

コンピュータ

サンプルコードの概要

何をするコードか

  • 8×8スプライト × 4フレームのアニメーションを表示する最小サンプル

  • スプライトデータは JSONをコードに直接貼り付け

  • HTML5 Canvas + JavaScriptのみで動作

  • ゲームループ + ダブルバッファを使用


構成

  • 表示用 Canvas(screen)

  • 描画用 Canvas(buffer / オフスクリーン)

  • requestAnimationFrame によるゲームループ

  • フレーム切り替えは時間差分で制御


描画の流れ

  1. 裏バッファ(buffer)を毎フレーム白で塗りつぶす

  2. 現在のフレーム(8×8)を拡大描画

  3. 完成した画面を 表Canvasへ一括転送

[ buffer ] ← 毎フレーム再構築
↓ drawImage
[ screen ] ← 表示のみ

スプライトデータ

  • インデックスカラー方式

  • 0 番は透明色

  • 各フレームは 8×8 = 64要素の配列

  • JSONはエディタ出力をそのまま使用可能

ソースコード

ファイル名:sprite_editor.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>8x8x4 Sprite Editor</title>

<style>
  body {
    font-family: monospace;
    display: flex;
    gap: 16px;
    padding: 12px;
  }

  canvas {
    border: 1px solid #333;
    image-rendering: pixelated;
    cursor: crosshair;
  }

  .grid {
    display: grid;
    grid-template-columns: repeat(2, auto);
    gap: 6px;
  }

  .palette {
    margin-top: 8px;
  }

  .palette div {
    width: 24px;
    height: 24px;
    display: inline-block;
    border: 1px solid #000;
    cursor: pointer;
    box-sizing: border-box;
  }

  .palette div.active {
    outline: 2px solid red;
  }

  textarea {
    width: 480px;
    height: 520px;
    font-family: monospace;
    font-size: 12px;
  }

  h4 {
    margin: 8px 0 4px;
  }
</style>
</head>
<body>

<!-- ===== LEFT : EDITOR ===== -->
<div>
  <div class="grid" id="editGrid"></div>

  <h4>Palette</h4>
  <div class="palette" id="palette"></div>

  <p>Current Color Index: <b><span id="curColor">1</span></b></p>
</div>

<!-- ===== RIGHT : JSON OUTPUT ===== -->
<textarea id="jsonOutput" spellcheck="false"></textarea>

<script>
/* ===== Constants ===== */
const TILE_SIZE = 8;
const FRAMES = 4;
const SCALE = 24;

/* ===== Sprite Data ===== */
const frames = Array.from({ length: FRAMES }, () =>
  new Uint8Array(TILE_SIZE * TILE_SIZE)
);

let currentColor = 1;

/* ===== Palette ===== */
const paletteColors = [
  "rgba(0,0,0,0)", // 0 transparent
  "#000000", "#ffffff", "#ff0000", "#00ff00", "#0000ff",
  "#ffff00", "#ff00ff", "#00ffff", "#888888", "#444444",
  "#ff8800", "#88ff00", "#0088ff", "#ff0088", "#00ff88"
];

/* ===== Canvas Setup ===== */
const grid = document.getElementById("editGrid");
const canvases = [];

for (let i = 0; i < FRAMES; i++) {
  const c = document.createElement("canvas");
  c.width = TILE_SIZE * SCALE;
  c.height = TILE_SIZE * SCALE;
  c.dataset.index = i;
  c.addEventListener("mousedown", onDraw);
  grid.appendChild(c);
  canvases.push(c);
}

/* ===== Drawing ===== */
function redraw() {
  canvases.forEach((c, fi) => {
    const ctx = c.getContext("2d");
    ctx.clearRect(0, 0, c.width, c.height);

    for (let y = 0; y < TILE_SIZE; y++) {
      for (let x = 0; x < TILE_SIZE; x++) {
        const idx = frames[fi][y * TILE_SIZE + x];
        if (idx !== 0) {
          ctx.fillStyle = paletteColors[idx];
          ctx.fillRect(x * SCALE, y * SCALE, SCALE, SCALE);
        }
        ctx.strokeStyle = "#555";
        ctx.strokeRect(x * SCALE, y * SCALE, SCALE, SCALE);
      }
    }
  });

  updateJSON();
}

/* ===== Mouse Draw ===== */
function onDraw(e) {
  const rect = e.target.getBoundingClientRect();
  const x = Math.floor((e.clientX - rect.left) / SCALE);
  const y = Math.floor((e.clientY - rect.top) / SCALE);
  const fi = e.target.dataset.index;

  if (x >= 0 && y >= 0 && x < TILE_SIZE && y < TILE_SIZE) {
    frames[fi][y * TILE_SIZE + x] = currentColor;
    redraw();
  }
}

/* ===== Palette UI ===== */
const paletteDiv = document.getElementById("palette");

paletteColors.forEach((col, idx) => {
  const d = document.createElement("div");
  d.style.background = col;
  if (idx === currentColor) d.classList.add("active");

  d.onclick = () => {
    currentColor = idx;
    document.getElementById("curColor").textContent = idx;
    document.querySelectorAll(".palette div")
      .forEach(p => p.classList.remove("active"));
    d.classList.add("active");
  };

  paletteDiv.appendChild(d);
});

/* ===== JSON Formatting ===== */
function formatPixels(pixels, indent = "        ") {
  const lines = [];
  for (let i = 0; i < pixels.length; i += 8) {
    lines.push(indent + pixels.slice(i, i + 8).join(", "));
  }
  return "[\n" + lines.join(",\n") + "\n      ]";
}

function updateJSON() {
  const out = [];

  out.push("{");
  out.push('  "version": 1,');
  out.push('  "tileSize": 8,');
  out.push('  "frameLayout": "2x2",');
  out.push('  "transparentIndex": 0,');
  out.push('  "palette": ' + JSON.stringify(paletteColors, null, 2) + ",");
  out.push('  "frames": [');

  frames.forEach((frame, i) => {
    out.push("    {");
    out.push(`      "id": ${i},`);
    out.push(`      "pixels": ${formatPixels(frame)}`);
    out.push(i === frames.length - 1 ? "    }" : "    },");
  });

  out.push("  ]");
  out.push("}");

  document.getElementById("jsonOutput").value = out.join("\n");
}

/* ===== Init ===== */
redraw();
</script>

</body>
</html>

ファイル名:sprite_anim_sample.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Sprite Animation Sample</title>
<style>
  canvas {
    border: 1px solid #333;
    image-rendering: pixelated;
  }
</style>
</head>
<body>

<canvas id="screen" width="320" height="240"></canvas>

<script>
/* ===== Paste sprite JSON here ===== */
const sprite = {
  "tileSize": 8,
  "transparentIndex": 0,
  "palette": [
    "rgba(0,0,0,0)",
    "#000000",
    "#ffffff",
    "#ff0000",
    "#00ff00",
    "#0000ff",
    "#ffff00",
    "#ff00ff",
    "#00ffff",
    "#888888"
  ],
  "frames": [
    {
      "id": 0,
      "pixels": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0
      ]
    },
    {
      "id": 1,
      "pixels": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0,
        0, 0, 1, 0, 0, 1, 0, 0,
        0, 0, 1, 0, 0, 1, 0, 0,
        0, 0, 0, 1, 1, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0
      ]
    },
    {
      "id": 2,
      "pixels": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 1, 1, 1, 1, 1, 0,
        0, 0, 1, 0, 0, 0, 1, 0,
        0, 0, 1, 0, 0, 0, 1, 0,
        0, 0, 1, 0, 0, 0, 1, 0,
        0, 0, 1, 1, 1, 1, 1, 0,
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0
      ]
    },
    {
      "id": 3,
      "pixels": [
        0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 1, 1, 1, 1, 1, 0,
        0, 1, 0, 0, 0, 0, 0, 1,
        0, 1, 0, 0, 0, 0, 0, 1,
        0, 1, 0, 0, 0, 0, 0, 1,
        0, 1, 0, 0, 0, 0, 0, 1,
        0, 1, 0, 0, 0, 0, 0, 1,
        0, 0, 1, 1, 1, 1, 1, 0
      ]
    }
  ]
};
/* ===== End of sprite JSON ===== */

/* ===== Constants ===== */
const SCREEN_W = 320;
const SCREEN_H = 240;
const SCALE = 4;   // 8x8 → 32x32
const FPS = 4;

/* ===== Canvas (double buffer) ===== */
const screen = document.getElementById("screen");
const screenCtx = screen.getContext("2d");

const buffer = document.createElement("canvas");
buffer.width = SCREEN_W;
buffer.height = SCREEN_H;
const bufCtx = buffer.getContext("2d");

/* ===== Prepare runtime data ===== */
const palette = sprite.palette;
const frames = sprite.frames.map(f => new Uint8Array(f.pixels));

let frameIndex = 0;
let lastTime = 0;

/* ===== Draw 8x8 sprite ===== */
function draw8x8(ctx, pixels, x, y, scale) {
  for (let py = 0; py < 8; py++) {
    for (let px = 0; px < 8; px++) {
      const idx = pixels[py * 8 + px];
      if (idx !== sprite.transparentIndex) {
        ctx.fillStyle = palette[idx];
        ctx.fillRect(
          x + px * scale,
          y + py * scale,
          scale,
          scale
        );
      }
    }
  }
}

/* ===== Game Loop (method ②) ===== */
function loop(time) {
  requestAnimationFrame(loop);

  if (time - lastTime > 1000 / FPS) {
    frameIndex = (frameIndex + 1) % frames.length;
    lastTime = time;
  }

  // --- build full frame on back buffer ---
  bufCtx.fillStyle = "#FFFFFF";                 // background
  bufCtx.fillRect(0, 0, SCREEN_W, SCREEN_H); // clear + bg

  draw8x8(bufCtx, frames[frameIndex], 144, 104, SCALE);

  // --- present ---
  screenCtx.drawImage(buffer, 0, 0);
}

requestAnimationFrame(loop);
</script>

</body>
</html>

実行例

スプライトエディタのスクリーンショット
https://kareteruhito.github.io/my-demo-app/game_tools/sprite_editor.html

スプライトエディタで作ったJSONを使ったアニメーションサンプル
https://kareteruhito.github.io/my-demo-app/game_tools/sprite_anim_sample.html

コメント