【ウェブエンジニアのお遊び】JavaScriptでDOM操作をしてスキャニメーションを作ってみた!

【ウェブエンジニアのお遊び】JavaScriptでDOM操作をしてスキャニメーションを作ってみた!

Clock Icon2024.08.09

こんにちは、戸田です。
たまたまスキャニメーションを見る機会があってウェブで表現できそうと思ったのでやってみました!

スキャニメーションとは

スキャニメーション(Scanimation)(別名: スリットアニメーション)は目の錯覚を利用した昔からある遊びです。ギザギザのイラストの上にシマ模様が描かれた透明シートを重ねて横に動かすと、イラストが動いて見えます。

https://www.youtube.com/watch?v=8o2Z7eAP5nQ

もっと詳しく知りたい人はこちら ↓
https://scanimation-meijinkai.info/whatis/

今回作ったもの

画像の上で横スクロールをするとスリットが出てきます。

https://scanimation-dom.vercel.app/

CodePen は 0.5× にすると見やすいです。

仕組み

基本的な考え方

今回は 16 マスのグリッドを持つ 4 枚の画像をスキャニメーション化します。各マスには幅が 1/4 の色付き要素が配置されています。適切な間隔のスリットを持つシートで覆うと、特定の色のみが視認可能になります。以下は、スリットシートを被せる前の状態を示しています。
素材作成スペース (4).png

スリットを被せたらこうなります ↓

オレンジ
素材作成スペース (5).png 素材作成スペース.png 素材作成スペース (2).png 素材作成スペース (1).png

スリットを左右に動かすことで画像が動いているように見えます。

スリットの幅について

スリットの適切な幅とは以下の画像で説明します。
4/4 というのはグリッドの一つの幅を示しています。それに対して 3/4 の幅の帯と 1/4 の隙間を持ったスリットを使用することでうまく1つの要素のみを表示することができます。今回は 4 枚の表現ができるようにしましたが 10 枚ならスリットの帯の幅は 9/10, 隙間は 1/10 となります。
素材作成スペース (6).png

DOM の構成について

今回の DOM の構造です。

素材作成スペース (7).png

使用した要素

上が親要素で下に向けて入れ子になってます。

  1. .image-wrapper
  2. .image-row
  3. .image-block
  4. div

.image-row を採用した理由

利点 1

JavaScript で要素を作成する際、構造が理解しやすくなると考えました。
.image-wrapper の直下に .image-block を配置する選択肢もありましたが、行の概念を明確にするため .image-row を挟むことにしました。

利点 2

.image-row にのみ高さを指定をして、子要素の div は CSS で height: 100% を設定しました。その結果、各 div の高さを個別に指定する必要がなく、自動的に調整されます。

JavaScript での DOM 操作

まずは画像のデータ構造を考える

今回の画像は白黒のため 01 の 2 次元配列で管理することにしました。さらに 4 枚を表現する必要があるため以下の 3 次元配列のデータ構造にしました。

const images = [
  // 紫
  [
    [1, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
  ],
  // オレンジ
  [
    [1, 1, 0, 0],
    [1, 0, 0, 0],
    [0, 0, 0, 0],
    [0, 0, 0, 0],
  ],
  // 緑
  [
    [1, 1, 1, 0],
    [1, 1, 0, 0],
    [1, 0, 0, 0],
    [0, 0, 0, 0],
  ],
  // 青
  [
    [1, 1, 1, 1],
    [1, 1, 1, 0],
    [1, 1, 0, 0],
    [1, 0, 0, 0],
  ],
];

デモで表示しているものは 50×50 で表現しています。
配列を作るのは 4×4 なら手作業でも簡単ですが、50×50 だと厳しいです。
なのでプログラムの力を使って計算で 50×50 の配列を作ることにしました。

と言っても計算するのはめんどくさいので全部 AI に作ってもらいました。
AI の進化が凄すぎて一発で必要なプログラムを組んでくれました!

AI への質問

自分

js で triangle[50][50]の三角形の 2 次元配列を作りたいです。 angle という定数で角度を決めて自由に設定できるようにしたいです。 以下の例は書きやすさを求めて少ない length で記述しています。

triangle[5][5] = [
  [0, 1, 0, 0, 0],
  [0, 1, 1, 0, 0],
  [0, 1, 1, 1, 0],
  [0, 1, 0, 0, 0],
  [0, 1, 0, 0, 0],
];

AI の回答

はい、理解しました。三角形の 2 次元配列を作成し、角度を自由に設定できるようにするのは興味深い課題です。以下のポイントを考慮しながら、最適な解決策を提案します。

パフォーマンス: 大きな配列を効率的に生成する必要があります。
柔軟性: 角度を自由に設定できるようにします。
保守性: コードは読みやすく、将来の変更に対応しやすいものにします。
再利用性: 関数をモジュール化して、他のプロジェクトでも使えるようにします。
以下に、これらの要件を満たす JavaScript コードを提案します:

function createTriangle(size, angle) {
  // 角度をラジアンに変換
  const radians = (angle * Math.PI) / 180;

  // タンジェントを計算(0度と90度の場合は特別に処理)
  const tan = angle === 0 ? 0 : angle === 90 ? Infinity : Math.tan(radians);

  // 2次元配列を初期化
  const triangle = Array(size)
    .fill()
    .map(() => Array(size).fill(0));

  for (let y = 0; y < size; y++) {
    const width = Math.min(Math.round(y / tan), size);
    for (let x = 0; x < width; x++) {
      triangle[y][x] = 1;
    }
  }

  return triangle;
}

// 使用例
const size = 50;
const angle = 45; // 度数法で指定
const triangle = createTriangle(size, angle);

// 結果の表示(一部のみ)
console.log(triangle.slice(0, 5).map((row) => row.slice(0, 5)));

このコードの特徴と利点:

  • パフォーマンス
    Array(size).fill().map()を使用して効率的に 2 次元配列を初期化しています。
    各行の幅を計算し、必要な部分のみを埋めることで、不要な計算を避けています。
  • 柔軟性
    angle パラメータを使用して、任意の角度で三角形を生成できます。
    0 度から 90 度までの全ての角度に対応しています。
  • 保守性
    関数名と変数名が明確で、コードの意図が理解しやすくなっています。
    コメントを適切に使用して、複雑な部分を説明しています。
  • 再利用性
    createTriangle 関数は独立しており、他のプロジェクトでも簡単に使用できます。
  • エッジケースの処理
    0 度と 90 度の場合を特別に処理し、エラーを防いでいます。
  • 精度
    Math.round()を使用して、小数点以下の値を適切に丸めています。
  • 注意点
    このコードは 50x50 のサイズで動作しますが、非常に大きな配列を生成する場合はメモリ使用量に注意が必要です。
    角度が非常に小さい場合、生成される三角形が細くなりすぎる可能性があります。
  • 改善の余地
    メモリ効率を上げるために、スパース配列や圧縮技術を使用することを検討できます。三角形の向きや位置を調整するオプションを追加できます。

このソリューションは、要件を満たしつつ、効率的で柔軟性のある実装を提供しています。必要に応じて、さらなる最適化や機能追加を行うことができます。

自分

今度は中心から指定した角度の扇形で表現したいです

AI の回答

承知しました。中心から指定した角度の扇形を 2 次元配列で表現するのは興味深い課題です。この要求に対して、以下のポイントを考慮しながら解決策を提案します:

  1. パフォーマンス: 大きな配列を効率的に生成する必要があります。
  2. 精度: 扇形の形状を可能な限り正確に表現します。
  3. 柔軟性: 角度を自由に設定でき、さらに扇形の向きも調整できるようにします。
  4. 保守性: コードは読みやすく、将来の変更に対応しやすいものにします。
  5. 再利用性: 関数をモジュール化して、他のプロジェクトでも使えるようにします。

以下に、これらの要件を満たす JavaScript コードを提案します:

function createSector(size, angle, startAngle = 0) {
  // 角度をラジアンに変換
  const startRad = (startAngle * Math.PI) / 180;
  const endRad = ((startAngle + angle) * Math.PI) / 180;

  // 2次元配列を初期化
  const sector = Array(size)
    .fill()
    .map(() => Array(size).fill(0));

  const center = Math.floor(size / 2);
  const radius = center;

  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      // 中心からの相対座標を計算
      const dx = x - center;
      const dy = center - y; // y軸は上下反転

      // 点と中心の距離を計算
      const distance = Math.sqrt(dx * dx + dy * dy);

      if (distance <= radius) {
        // 点の角度を計算(アークタンジェントを使用)
        let angle = Math.atan2(dy, dx);
        if (angle < 0) angle += 2 * Math.PI; // 角度を0~2πの範囲に調整

        // 点が扇形の範囲内にあるかチェック
        if (angle >= startRad && angle <= endRad) {
          sector[y][x] = 1;
        }
      }
    }
  }

  return sector;
}

// 使用例
const size = 50;
const angle = 90; // 扇形の角度(度数法)
const startAngle = 0; // 開始角度(度数法)
const sector = createSector(size, angle, startAngle);

// 結果の表示(一部のみ)
console.log(
  sector
    .slice(0, 10)
    .map((row) => row.slice(0, 10).join(" "))
    .join("\n")
);

このコードの特徴と利点:

  1. パフォーマンス:

    • 効率的な 2 次元配列の初期化を行っています。
    • 必要な部分のみを計算することで、不要な処理を避けています。
  2. 精度:

    • Math.atan2を使用して角度を正確に計算し、扇形の形状を精密に表現しています。
  3. 柔軟性:

    • angleパラメータで扇形の角度を、startAngleパラメータで開始角度を指定できます。
    • これにより、任意の角度と向きの扇形を生成できます。
  4. 保守性:

    • 関数名と変数名が明確で、コードの意図が理解しやすくなっています。
    • コメントを適切に使用して、複雑な部分を説明しています。
  5. 再利用性:

    • createSector関数は独立しており、他のプロジェクトでも簡単に使用できます。
  6. エッジケースの処理:

    • 角度の正規化を行い、負の角度や 360 度以上の角度にも対応しています。

注意点:

  • このコードは 50x50 のサイズで動作しますが、非常に大きな配列を生成する場合はメモリ使用量に注意が必要です。
  • 小さなサイズの場合、扇形の形状が粗くなる可能性があります。

改善の余地:

  • アンチエイリアシングを実装して、扇形の輪郭をよりスムーズにすることができます。
  • メモリ効率を上げるために、スパース配列や圧縮技術を使用することを検討できます。
  • パフォーマンスを向上させるために、対称性を利用して計算量を減らすことができます。

このソリューションは、要件を満たしつつ、効率的で柔軟性のある実装を提供しています。必要に応じて、さらなる最適化や機能追加を行うことができます。

DOM の操作

最近 React での DOM 操作しかやらないためバニラ JS で DOM を扱いに戸惑いました。
でも考え方はシンプルです。

  1. 親要素から順番に要素を作る
  2. 要素に必要なクラスやスタイルを当てる
  3. 親要素に対して appendChild メソッドで子要素を追加する

これを続けていくことで作れます。

width や height は ↑ の「基本的な考え方」で説明した方法を参考に計算します。
スリットも同じ考え方で作成することができます。

さらに気になる方はソースコードを参考にしてみてください。
https://github.com/ShuntaToda/scanimation-dom

まとめ

今回は DOM でスキャニメーションを作成してみました。
スキャニメーションの仕組み、データ構造、DOM 操作(JS の知識)の全てが揃ってやっと完成するものなので初心者・中級者の方はやってみると新たな気づきがあるかもしれません。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.