本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
ヒーロー

ヒーローの見出しの立体の文字がゆっくり傾き、表面を光が流れる

デモ

仕様

用途
ヒーロー
動作
DOM+WebGL
種類
シーン
使うもの
three/addons/utils/BufferGeometryUtils.js
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

立体の見出しは、字の輪郭を SVG の path として HTML に置き、three.js の SVGLoader で図形に読み込んで、ExtrudeGeometry で厚みを付けて作ります。穴のある字(D・e・g など)は、path の fill-rule を evenodd にしておくと穴として抜けます。three.js には書体のファイルが付いていないので、デザインツールで文字をアウトライン化した SVG の path に差し替えれば、手持ちの書体の見出し(和文も欧文も)がそのまま立体になります。WebGL が使えない環境では、同じ path が平らな字として見えます。

表面を流れる光は、映り込み用に作った暗い部屋(細い縦長の照明 2 本と天井の面光源)を PMREM でぼかした環境マップです。平らな面を θ 傾けると映り込みは 2θ 動くので、左右に約 17° 傾けるだけで、光の筋が字の端から端まで流れます。傾きと上下の浮きは、10 秒で 1 周する正弦波の組み合わせです。

素材は MeshPhysicalMaterial で、下地の映り込みを切り(specularIntensity を 0)、クリアコートだけに照明を映します。こうすると光が面全体に広がらず、細い筋になります。PMREM のぼかしは 0.004 まで小さくします。0.02 ほどでも細い照明の山が削られ、照明を明るくしても筋が白く立ちません。

押し出した字の法線は、側面と縁の丸みだけを toCreasedNormals でなめらかにつなぎ、表と裏の平らな面は元の法線に戻します。全部をなめらかにすると、表の面の大きな三角形に縁の傾きが混ざり、映り込みがまだらにゆがみます。縁の丸みは字の内側に取っているので、字が太らず、詰めた字間でも隣の字と重なりません。

字・光・影の色は CSS の変数で変えられ、canvas は透明にして CSS のグラデーションの地に重ねています。影は左上の主光源から奥の壁に落とした、ぼかした影です。見出しの文は見えない h1 にも入れ、読み上げと検索に届けます。動きを減らす設定では傾けた 1 枚だけを描き、画面の外では描きません。ブランドサイトや 3D 制作のスタジオのファーストビューに向いています。

コード

HTML
<section class="depth-hero">
  <!-- 立体の見出しは canvas に描く飾り。読み上げと検索には h1 の文を使う -->
  <canvas class="depth-hero__canvas" aria-hidden="true"></canvas>
  <p class="depth-hero__eyebrow">3D Studio</p>
  <h1 class="depth-hero__title">
    <span class="depth-hero__label">Design in depth.</span>
    <!-- 立体にする字の輪郭。自分の見出しの SVG の path に差し替える(穴は fill-rule="evenodd" で抜く)。WebGL が使えないときは、この平らな字が見える -->
    <svg class="depth-hero__outline" viewBox="0 0 1071.53 477.75" aria-hidden="true"><path fill-rule="evenodd" d="M597.85 .36l4.48-.36l6 .91l9 4.07l8.3 8.38l4.11 10l-.17 12l-3.86 9l-4.38 5.19l-4 3.17l-10 4.11l-6 .61l-7-.73l-6-2.11l-5-2.96l-6.38-6.28l-2.87-5l-1.88-7l-.3-6l.98-6l4.21-9l3.64-4l5.6-4.29l4-2.04zM127.03 21.36l67.3-.22l16 .88l19 2.99l11 3.1l14 6.04l8 5.01l11.41 10.2l9.91 14l5.06 12l3.01 14l.96 21l-1.97 17l-4.13 13l-6.83 13l-5.13 7l-5.29 5.6l-12 9.7l-9 5.14l-15 5.91l-12 2.96l-17 2.14l-77.49-.45zM366.13 60.36l12.2-.22l13 1.86l9 2.91l13 7.09l9.43 8.36l6.93 9l6 12l2.96 10l1.97 17l-.21 9l-1.08 4.49l-83 0l-.91 .51l1.45 4l4.46 4.47l3 1.91l6 2.07l13-.15l7-2.97l6-4.91l2 .17l38.04 19.41l.27 1l-5.31 6.97l-5 5.19l-7 5.3l-11 5.82l-13 4.17l-13 1.79l-15 .05l-16-3l-15-6.04l-9-5.94l-11.32-11.31l-4.06-6l-4.02-8l-3.74-12l-1.34-15l1.19-16l3.99-13l6-11l9.59-11l12.71-9.2l15-6.19zM501.91 60.36l7.42-.35l12 .99l14 3.07l13 4.84l13.45 7.45l.05 1l-3.17 4l-22.33 26.52l-2-.11l-12-5.86l-11-1.91l-6 .9l-2 1.46l-.67 2l.18 2l3.49 2.4l19 4.36l8 2.83l6 3.02l7 4.93l5.35 5.46l5.05 9l1.82 8l.1 9l-1.2 8l-3.67 9l-6.02 8l-7.43 6.26l-10 5.17l-9 2.88l-14 2l-13 .01l-15-2.1l-17-4.91l-14-6.94l-5.55-4.37l26.55-31.4l2 .03l6 4.68l6 3.09l9 2.9l10 .1l3.46-2.4l.04-3l-2.5-2.5l-29-9.13l-11-6.97l-5.19-5.4l-3.23-5l-2.97-7l-1.06-6l.12-11l1.94-8l4.01-8l5.84-7l7.54-6.14l10-5.12l10-3.12zM691.89 62.36l17.44-.43l9 1.99l11 5.12l5 4.04l3 3.54l.67-1.26l.09-10l1.24-.48l51.61 .48l.04 113l-.24 11l-1.09 8l-2 8l-3.31 8l-7.58 12l-5.86 6l-6.57 5.16l-9 5.12l-8 3.12l-12 2.91l-16 1.86l-10 .16l-15-1.03l-16-2.97l-11-3.13l-16-6.96l-7.33-5.24l24.33-36.57l10 5.79l10 4.26l14 2.99l9-.02l9-2.06l6-2.98l5.12-4.41l4.32-8l.56-10.16l-7 5.44l-6 3.17l-5 1.98l-9 1.93l-15 .04l-7-1.1l-9-2.9l-12-6.99l-8-7.87l-6.44-9.54l-3.84-9l-3.12-13l-.84-18l1.98-14l4.86-14l7.05-12l5.2-6l6.15-5.36l12-6.88zM893.1 62.36l9.23-.73l9 .38l9 2.19l4 1.77l5 3.07l7 6.57l2.32 2.75l4.89 9l2.2 7l1.99 11l.16 86l-56.23 0l0-66l-.84-9l-1.02-3l-2.47-3.36l-4-2.1l-3-.16l-4 .46l-4.46 3.16l-2.87 5l-1.02 5l-.16 70l-55.49 .5l-.57-1.5l.07-125l53.5-.48l1.22 .48l.1 11l.68 .98l5-5.29l4-2.99l10-5.06zM572.94 65.36l56.07 0l.06 125l-.74 1.5l-55.47-.5zM183.33 70.72l-.61 .64l0 70l.61 .65l18-.22l10-2.12l5-1.91l8-5.53l4.29-4.87l2.95-6l1.99-9l.27-10l-1.04-7l-2.14-6l-3.01-5l-5.31-5.35l-11-6.01l-9-2.06zM369.33 97.12l-5 1.75l-4 2.93l-4.36 5.56l-1.12 3l.48 .66l36 .02l.5-.68l-2.02-6l-5.48-5.33l-6-2.04zM711.33 107.98l-7 3.06l-5.39 6.32l-2.12 6l.33 11l1.89 5l2.1 3l5.19 4.21l8 2.26l6-.18l4-1.02l4-1.92l2.63-2.35l4.56-6l2.23-9l-1.18-7l-1.84-4l-3.26-4l-6.14-4.22l-4-1.23zM25.64 228.36l3.69-.4l7 1l9 4.23l7.36 7.17l2.92 5l1.86 6l.02 11l-1.8 6l-2.92 5l-7.44 7.13l-6 3.15l-4 1.17l-6 .62l-7-.67l-10-4.17l-3.99-3.23l-4.29-5l-2.16-4l-1.89-6l.01-11l1.93-6l2.95-5l7.44-7.27l6-3.12zM372.91 238.36l1.42-.53l53 0l1.57 .53l.08 181l-.65 .5l-49 0l-.57-.5l-.43-11.48l-5 4.84l-6 4.07l-13 4.98l-14 1.67l-15-1.71l-6-1.97l-8-4.12l-10.28-8.28l-8.98-12l-6.17-14l-2.91-12l-.96-10l.11-9l1.93-13l3.91-12l2.93-6l5.07-8l5.35-6.15l6-5.21l8-5.06l7-2.9l10-2.13l14-.02l7 1.27l10 3.82l9 6.91l.56-.53zM853.33 238.36l1-.53l54 0l1.45 .53l0 66l.55 1.28l5-5.56l11-7.11l10-2.95l8-.39l8 .3l6 1.11l8 3.04l6 3.95l7.29 7.33l6 11l2.16 7l1.76 11l-.21 85.47l-56.17-.47l-.36-74l-1.2-4l-2.27-3.07l-4-2.34l-3-.2l-4 .46l-4.5 3.15l-2.99 5l-.98 4l-.13 71l-56.2 0zM761.86 265.36l1.47-.67l53 .22l.37 27.45l27.11 1l-.45 40l-26.03 .09l-.63 .91l.12 36l1.33 5l2.3 3l3.88 2.35l7 1.05l9-1.5l1.33 1.1l.01 37l-1.34 1.23l-14 3.06l-13 .75l-19-1.62l-7-1.97l-7-3.09l-6-4.54l-6.3-7.82l-2.93-6l-2.21-9l-.89-11l0-44l-.67-.91l-16-.06l-.92-1.03l.23-39l17.22-1zM507.13 288.36l12.2-.22l13 1.86l9 2.91l13 7.09l9.43 8.36l6.93 9l6 12l2.96 10l1.97 17l-.21 9l-1.08 4.49l-83 0l-.91 .51l1.45 4l4.46 4.47l3 1.91l6 2.07l13-.15l7-2.97l6-4.91l2 .17l38.04 19.41l.27 1l-5.31 6.97l-5 5.19l-7 5.3l-11 5.82l-13 4.17l-13 1.79l-15 .05l-16-3l-15-6.04l-9-5.94l-11.32-11.31l-4.06-6l-4.02-8l-3.74-12l-1.34-15l1.19-16l3.99-13l6-11l9.59-11l12.71-9.2l15-6.19zM158.34 290.36l8.99-.71l9 .33l9 2.08l4 1.85l5 3.01l7.21 6.44l3 4l4.22 8l2.79 9l1.87 18l-.2 77l-56.38 0l-.11-71l-.94-5l-1.97-4l-4.49-3.21l-4-.42l-3 .17l-4 2.15l-3.32 4.31l-1.11 3l-.82 8l-.12 66l-55.63 .48l-.39-126.48l54.39-.41l1 12.77l4-4.45l7-5.27l8-3.94zM675.53 290.36l8.8-.73l9 .4l5 .91l9 3.16l11 6.83l6 5.65l6.26 7.78l7.06 14l4.06 16l.82 12l-.87 12l-4.88 18l-6.2 12l-7.25 9.01l-6 5.38l-6 4.02l-12 4.96l-14 1.66l-10-.7l-13-4.28l-5-2.7l-6-4.59l-.22 65.24l-.78 1.33l-54 .06l-1-1.39l.12-183l51.88-.48l.92 .48l.14 10l.94 .81l7-6.05l5-2.96l7-2.94zM.82 293.36l55.99 0l-.48 126.49l-55.56-.49zM510.33 325.12l-5 1.75l-4 2.93l-4.36 5.56l-1.12 3l.48 .66l36 .02l.5-.68l-2.02-6l-5.48-5.33l-6-2.04zM349.33 335.98l-7 3.06l-5.39 6.32l-2.12 6l.33 11l1.89 5l2.1 3l5.19 4.21l8 2.26l6-.18l4-1.02l4-1.92l2.63-2.35l4.56-6l2.23-9l-1.18-7l-1.84-4l-3.26-4l-6.14-4.22l-4-1.23zM664.33 336.21l-3 .81l-3.88 2.34l-5.61 6l-2.92 8l.07 6l1.01 4l4.33 6.88l6 4.58l8 2.04l9-1.04l4-1.96l3-2.53l4.45-6.97l1.05-4l.1-5l-1.09-7l-4.21-7l-6.3-4.45l-4-1.05zM1033.61 361.36l11.72-.12l6 1.74l6 3.27l8.4 8.11l2.93 5l2.08 6l.79 7l-.76 7l-4.17 10l-4.01 5l-5.26 4.33l-6 3.08l-7 1.79l-7 .21l-6-.97l-4-1.13l-6-3.11l-5-3.95l-4.34-5.25l-4.13-10l-.79-6l.81-8l2.05-6l2.97-5l8.58-8l5.85-3.25z"/></svg>
  </h1>
  <div class="depth-hero__foot">
    <p class="depth-hero__lead">ここに説明文が入ります。</p>
    <a class="depth-hero__scroll" href="#studio">スクロール</a>
  </div>
</section>

CSS
.depth-hero {
  /* 立体と光の色。script.js がこの 6 つを読んでシーンへ渡す */
  --headline: #688297; /* 立体の字 */
  --key: #fff3e4; /* 左上からの主光源 */
  --sky: #fff8ee; /* 上からの環境の光 */
  --ground: #bdb2a2; /* 下からの照り返し */
  --room: #23252f; /* つやに映る部屋の暗がり。明るくすると、字の面全体が淡くなる */
  --shadow: #4a5b66; /* 奥の壁に落ちる影 */
  --bg: #f2f1ed;
  --ink: #262626;
  --mute: #6b6b6b;
  --gutter: clamp(24px, 7vw, 96px);
  position: relative;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto; /* 肩書き・見出し・下の行 */
  box-sizing: border-box;
  min-height: min(90vh, 720px);
  padding: 96px var(--gutter) 48px;
  overflow: hidden;
  color: var(--ink);
  /* 地は CSS で描く。見出しの後ろだけ、照明が壁に当たったように白く明るませる */
  background: radial-gradient(ellipse at 50% 44%, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0) 60%), var(--bg);
}

.depth-hero__canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

.depth-hero > :not(canvas) { position: relative; } /* canvas より前に出す */

.depth-hero__eyebrow {
  margin: 0;
  color: var(--mute);
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.14em;
  text-align: center;
  text-transform: uppercase;
}

/* 立体の字と影は、この行の中央に描かれる。狭い画面でも影がリード文にかからない高さを取る */
.depth-hero__title {
  display: grid;
  place-items: center;
  min-height: min(70vw, 320px);
  margin: 0;
}

/* 見出しの文は読み上げと検索のためのもの。見た目は立体の字が受け持つ */
.depth-hero__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* WebGL が使えないときの平らな字。立体が描けたら隠す(場所は取ったまま) */
.depth-hero__outline { width: min(100%, 640px); height: auto; fill: var(--headline); }
.depth-hero.is-3d .depth-hero__outline { visibility: hidden; }

.depth-hero__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px 32px;
}

.depth-hero__lead {
  margin: 0;
  font-size: 18px;
  line-height: 1.8;
  letter-spacing: 0.04em;
  word-break: auto-phrase; /* 和文を文節の切れ目で折り返す(対応していないブラウザでは、ふつうに折り返す) */
}

.depth-hero__scroll {
  display: inline-flex;
  align-items: center;
  gap: 16px;
  height: 32px;
  color: var(--mute);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-decoration: none;
}

.depth-hero__scroll::after { content: ""; width: 56px; height: 2px; background: currentColor; }
.depth-hero__scroll:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

JavaScript
import * as THREE from "three";
import { SVGLoader } from "three/addons/loaders/SVGLoader.js";
import { toCreasedNormals } from "three/addons/utils/BufferGeometryUtils.js";

const DURATION = 10; // 傾きがひと回りして、同じ姿勢に戻るまで(秒)
const WIDTH = 6.7; // 見出しの幅(three の単位。16:9 の画面で、画面の幅の半分強)。和文は字が 1em いっぱいに高いので、5.2 ほどに狭める
const DEPTH = 0.42, BEVEL = 0.05; // 押し出しの厚みと、縁の丸み
const CREASE = 0.6; // 面どうしの角度がこれより小さい境目は、なめらかにつなぐ(ラジアン)。字の角は立てたまま
const TILT_Y = 0.3, TILT_X = 0.07, FLOAT = 0.06; // 左右と前後の傾き(ラジアン)、上下の浮き。映り込みは傾きの 2 倍動く
const REST = 6.7; // 動きを減らす設定で描く 1 枚の時刻(秒)。光の筋が右寄りの字にかかる所
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

// 映り込み用のスタジオ。暗い部屋の手前に細い縦長の照明を左右に 1 本ずつ、天井に面光源を置き、PMREM でぼかして環境の光にする(画面には描かない)
function studio(renderer, room) {
  const env = new THREE.Scene();
  for (const [geometry, color, x, y, z] of [
    [new THREE.SphereGeometry(20, 32, 16), new THREE.Color(room), 0, 0, 0], // 部屋の壁
    // 細くて明るい照明にすると、字の面全体は淡くならずに、映り込みの筋だけが白く立つ
    [new THREE.PlaneGeometry(0.35, 18), new THREE.Color(60, 60, 60), 4.6, 0, 12],
    [new THREE.PlaneGeometry(0.35, 18), new THREE.Color(48, 48, 50), -4.6, 0, 12],
    [new THREE.PlaneGeometry(26, 5), new THREE.Color(1.4, 1.35, 1.3), 0, 12, 3], // 天井
  ]) {
    const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide }));
    mesh.position.set(x, y, z); mesh.lookAt(0, 0, 0); env.add(mesh);
  }
  // ぼかし(sigma)は小さく。大きいと細い照明の山が削られ、明るくしても筋が白く立たない
  const pmrem = new THREE.PMREMGenerator(renderer), target = pmrem.fromScene(env, 0.004);
  env.traverse((node) => { node.geometry?.dispose(); node.material?.dispose(); }); pmrem.dispose(); // 映し終えたら部屋は要らない
  return target;
}

// SVG の字の輪郭を押し出して立体にする。markup の path を自分の見出しの SVG の path に差し替えれば、その字(和文も)が立体になる
function extrude(svg) {
  const s = WIDTH / svg.viewBox.baseVal.width; // SVG の単位 → three の単位
  const shapes = new SVGLoader().parse(svg.outerHTML).paths.flatMap((path) => SVGLoader.createShapes(path)); // 穴は fill-rule で見分ける
  const geometry = new THREE.ExtrudeGeometry(shapes, {
    depth: DEPTH / s, steps: 1, curveSegments: 1, bevelEnabled: true, bevelThickness: BEVEL / s, bevelSegments: 4,
    bevelSize: (BEVEL * 0.8) / s, bevelOffset: (-BEVEL * 0.8) / s, // 縁の丸みを字の内側に取る(字が太らず、詰めた字間でも隣と重ならない)
  });
  geometry.center().scale(s, -s, -s); // SVG の y は下向き。y と z を一緒に裏返すと、面の向きを保ったまま字が正立する
  // 側面と縁の丸みはなめらかにつなぎ、平らな表と裏の面(グループ 0)は元の法線に戻す(戻さないと映り込みがまだらにゆがむ)
  const flat = geometry.attributes.normal.array.slice();
  toCreasedNormals(geometry, CREASE);
  const normals = geometry.attributes.normal.array;
  for (const { materialIndex, start, count } of geometry.groups) {
    if (materialIndex === 0) normals.set(flat.subarray(start * 3, (start + count) * 3), start * 3);
  }
  return geometry;
}

document.querySelectorAll(".depth-hero").forEach((hero) => {
  const css = (name) => getComputedStyle(hero).getPropertyValue(name).trim();
  const canvas = hero.querySelector(".depth-hero__canvas"), title = hero.querySelector(".depth-hero__title");
  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); // 地は CSS。canvas は透明にして重ねる
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.toneMapping = THREE.NeutralToneMapping; renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.VSMShadowMap; // ぼかした影(壁だけが受け、字は落とすだけ)
  const scene = new THREE.Scene(), envTarget = studio(renderer, css("--room")); scene.environment = envTarget.texture;
  const camera = new THREE.PerspectiveCamera(30, 16 / 9, 0.1, 100); // 縦横比は resize で入れる
  camera.position.set(0, 0.15, 13); camera.lookAt(0, 0, 0);
  const key = new THREE.DirectionalLight(css("--key"), 2); // 左上からの主光源。奥の壁に影を落とす
  key.position.set(-2.6, 3.6, 10); key.castShadow = true; key.shadow.mapSize.set(512, 512);
  Object.assign(key.shadow.camera, { left: -7, right: 7, top: 4.5, bottom: -4.5, near: 1, far: 30 });
  key.shadow.radius = 12; key.shadow.blurSamples = 16;
  // 下地の層は映り込みを持たせず(specularIntensity: 0)、クリアコートだけに照明を映すと、光が広くぼけずに細い筋で流れる
  const text = new THREE.Mesh(extrude(hero.querySelector(".depth-hero__outline")),
    new THREE.MeshPhysicalMaterial({ color: css("--headline"), roughness: 0.4, metalness: 0, specularIntensity: 0, clearcoat: 1, clearcoatRoughness: 0.03 }));
  text.castShadow = true;
  // 影だけを描く壁。影を映す範囲(shadow.camera)より小さくする。はみ出すと、縦長や横長の画面で範囲の縁が細い線に見える
  const wall = new THREE.Mesh(new THREE.PlaneGeometry(12, 8), new THREE.ShadowMaterial({ color: css("--shadow"), opacity: 0.2 }));
  wall.position.set(0.5, -0.75, -2.1); wall.receiveShadow = true;
  scene.add(key, new THREE.HemisphereLight(css("--sky"), css("--ground"), 0.6), text, wall);
  hero.classList.add("is-3d"); // 立体が描けたら、代わりの平らな字を隠す

  const draw = (t) => {
    const a = ((t % DURATION) / DURATION) * Math.PI * 2;
    // 左右の傾きで映り込みの筋が横へ流れ、前後の傾きで縁のつやが上下に動く(周波数を整数にして、DURATION でつなぐ)
    text.rotation.set(TILT_X * Math.sin(a * 2 + 0.6), TILT_Y * Math.sin(a), 0);
    text.position.y = -0.12 + FLOAT * Math.sin(a * 2);
    renderer.render(scene, camera);
  };
  const sizes = new ResizeObserver(() => {
    const { clientWidth: w, clientHeight: h } = canvas;
    camera.aspect = w / h;
    camera.zoom = Math.min(1, camera.aspect / 1.1); // 縦長の画面では引いて、見出しを幅に収める
    // 立体は canvas の中央ではなく、見出しの行(肩書きとリード文のあいだ)の中央に描く。リード文が何行に折り返しても、影がかからない
    const box = canvas.getBoundingClientRect(), slot = title.getBoundingClientRect(), up = box.top + h / 2 - slot.top - slot.height / 2;
    camera.setViewOffset(w, h, 0, up, w, h); // 投影行列もここで更新される
    renderer.setSize(w, h, false); // 大きさは CSS が決めるので、style は書き換えない
    draw(reduceMotion ? REST : performance.now() / 1000); // 動きを減らす設定では、傾けたところを 1 枚だけ描いて止める
  });
  const view = new IntersectionObserver(([e]) => renderer.setAnimationLoop(e.isIntersecting && !reduceMotion ? (ms) => draw(ms / 1000) : null)); // 画面の外では描かない
  sizes.observe(canvas); sizes.observe(title); view.observe(canvas);
  hero.destroy = () => { // PJAX で幕ごと捨てるときに呼ぶ。放っておくと WebGL の文脈が積み上がる
    renderer.setAnimationLoop(null); sizes.disconnect(); view.disconnect();
    [text.geometry, text.material, wall.geometry, wall.material, envTarget, renderer].forEach((item) => item.dispose());
    renderer.forceContextLoss(); hero.classList.remove("is-3d");
  };
});

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

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