本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ローディング

ローディング中に二重らせんの点が回り続ける

デモ

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

2本の鎖の点を横に並べ、位置ごとの位相をずらしながら回すと、高さと奥行きが入れ替わってらせんに見えます。手前に来た点ほど大きく濃く、奥へ回った点ほど小さく薄く描きます。

1周するのにかかる時間は2.16秒で、横幅の中にねじれが1.5回入るようにしています。向かい合う2点を結ぶ横木は、点が真横を向くあたりでいちばん濃く見えるようにしています。

奥にある点を先に描き、横木、手前の点の順に重ね直しているので、どちらの鎖が手前に来ても正しく前後関係が見えます。並べ替えを省くと、奥の点が手前の点より上に描かれたままになる瞬間が出ます。

点の組の数やねじれの数、振れ幅は、どれもスクリプトの先頭で変えられます。動きを減らす設定では振れ幅をごく小さくして、回転そのものは止めずに残します。解析中・生成中・スキャン中のような、処理の見当がつかない待ち時間の画面に向いています。

コード

HTML
<div class="dna-loader" role="status">
  <svg class="dna-loader__svg" viewBox="0 0 320 120" aria-hidden="true"></svg>
  <span class="dna-loader__text">読み込み中</span>
</div>

CSS
.dna-loader {
  --ground: #111111;  /* 地の色 */
  --strand-a: #94d390; /* 鎖 A の点の色 */
  --strand-b: #f3efe1; /* 鎖 B の点の色。A と明るさを離して 2 本を見分ける */
  --rung: #eeece6;     /* 横木の線の色 */
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  background: var(--ground);
}

.dna-loader__svg {
  display: block;
  width: min(320px, 80vw);
  height: auto;
  overflow: visible;
}

/* 読み上げにだけ届く文言。画面には出さない */
.dna-loader__text {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SVG_NS = "http://www.w3.org/2000/svg";
const DURATION = 2.16; // 1 周(秒)
const PAIRS = 16; // 点の組の数
const TURNS = 1.5; // 横幅の中のねじれの数
const WIDTH = 320;
const HEIGHT = 120;
const MARGIN = 16;
// 動きを減らす設定では、らせんの振れ幅をごく小さくする
const AMP = reduceMotion ? 8 : 38;
const DOT = 6; // 手前の点の半径。いちばん奥では半分

function node(parent, tag, attrs) {
  const n = document.createElementNS(SVG_NS, tag);
  for (const [key, value] of Object.entries(attrs)) n.setAttribute(key, String(value));
  parent.append(n);
  return n;
}

// k は奥行き(0 = いちばん奥、1 = いちばん手前)。fade は両端を薄くする係数
function place(dot, y, k, fade) {
  dot.setAttribute("cy", y.toFixed(2));
  dot.setAttribute("r", (DOT * (0.5 + 0.5 * k)).toFixed(2));
  dot.setAttribute("fill-opacity", ((0.3 + 0.7 * k) * fade).toFixed(3));
}

function createLoader(root) {
  const svg = root.querySelector(".dna-loader__svg");
  svg.setAttribute("viewBox", `0 0 ${WIDTH} ${HEIGHT}`);
  const style = getComputedStyle(root);
  // 色は CSS の変数が正。SVG の属性には var() を渡せないので、値にして読む
  const strandA = style.getPropertyValue("--strand-a").trim();
  const strandB = style.getPropertyValue("--strand-b").trim();
  const rungColor = style.getPropertyValue("--rung").trim();

  const pairs = Array.from({ length: PAIRS }, (_, i) => {
    const x = MARGIN + (i * (WIDTH - 2 * MARGIN)) / (PAIRS - 1);
    const group = node(svg, "g", {});
    const rung = node(group, "line", { x1: x, x2: x, stroke: rungColor, "stroke-width": 1.5, "stroke-linecap": "round" });
    const a = node(group, "circle", { cx: x, fill: strandA });
    const b = node(group, "circle", { cx: x, fill: strandB });
    // 両端の 2 組ずつを薄くして、らせんが中央から伸びているように見せる
    const fade = Math.min(1, (Math.min(i, PAIRS - 1 - i) + 1) / 3);
    return { group, rung, a, b, fade };
  });

  const state = { phase: 0 };
  function draw() {
    const cy = HEIGHT / 2;
    const base = state.phase * Math.PI * 2;
    pairs.forEach(({ group, rung, a, b, fade }, i) => {
      const theta = base + (i * TURNS * Math.PI * 2) / (PAIRS - 1);
      const s = Math.sin(theta);
      const k = (Math.cos(theta) + 1) / 2; // 鎖 A の奥行き。鎖 B は反対側
      const ya = cy + AMP * s;
      const yb = cy - AMP * s;
      place(a, ya, k, fade);
      place(b, yb, 1 - k, fade);
      rung.setAttribute("y1", ya.toFixed(2));
      rung.setAttribute("y2", yb.toFixed(2));
      rung.setAttribute("stroke-opacity", ((0.12 + 0.18 * Math.abs(s)) * fade).toFixed(3));
      // 奥の点 → 横木 → 手前の点
      if (k >= 0.5) group.append(b, rung, a);
      else group.append(a, rung, b);
    });
  }

  const spin = gsap.to(state, { phase: 1, duration: DURATION, ease: "none", repeat: -1, onUpdate: draw });

  return {
    // 解析が終わったら呼ぶ。呼ぶまで回り続ける
    done() {
      gsap.to(root, {
        autoAlpha: 0,
        duration: reduceMotion ? 0 : 0.3,
        onComplete: () => {
          spin.kill();
          root.remove();
        },
      });
    },
  };
}

// 例: 解析や生成の完了を待って loader.done()
export const loader = createLoader(document.querySelector(".dna-loader"));

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。