本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
アイコン

封筒のアイコンのふたが開き、中の手紙がのぞく

デモ

操作:カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

用途
アイコン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

封筒は、胴・ふたの表・ふたの裏・手紙の 4 つの形を重ねて描きます。リンクにカーソルを重ねると、ふたが上の辺を軸に手前へ返って開き、手紙が半分ほど上がってのぞきます。離れると同じ動きを逆にたどり、手紙が沈んでからふたが閉じます。

ふたは回さずに、三角の頂点の位置を計算して形(d)を書き換えます。上の辺を軸に返すと、頂点は軸から深さ 7(24 のグリッドで)の cos の分だけ上下し、sin の分だけ手前へ出ます。手前へ出た分だけ遠近をかけて中心から離すと、真横を向く途中で頂点が軸より少し上へふくらみ、平らな図形でも手前へ返ったように見えます。手前を向くほどふたを明るく塗って、陰影も付けます。

90° を越えると、ふたの裏は手紙の奥に回ります。1 つの形のまま動かすと、開いたふたが手紙の前に重なって手紙を隠してしまいます。手紙より手前に置いた表と、奥に置いた裏の 2 つの形を用意し、90° を境に描くほうを切り替えます。明るさは表と裏で同じ式から出すので、入れ替わる所で色が飛びません。

ふたは 0.4 秒の power2.inOut で開き、手紙はふたが 90° を越えるころ(0.22 秒後)から 0.45 秒の power3.out で 8 だけ上げます。離れるときは同じ動きを逆にたどるので、back.out のように行き過ぎる ease にすると、手紙が沈む前にいったん跳ね上がって見えます。閉じるほうは 2 倍の速さにしておくと、リンクの上を横切っただけのときにすぐ元に戻ります。

押すとページへ移るので <a> にし、アイコンは読み上げから外して、名前は「お問い合わせ」の文言に持たせます。キーボードで焦点が入ったときも開きます。動きを減らす設定では、ほぼ一瞬で切り替えます。線の太さは 24 のグリッドで 1.4(手紙の行は 1.05)にして、ほかの線のアイコンと並べても重く見えない細さにしています。このデモは 24px のアイコンを 9 倍に拡大して見せています。お問い合わせやニュースレターの登録への導線、フッターの連絡先のリンクに向いています。

コード

HTML
<!-- 押すとページへ移るので a。アイコンは飾りなので読み上げから外し、名前は文言が持つ -->
<a class="mail-link" href="/contact/">
  <svg class="mail-link__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
    <!-- 奥から: ふたの裏 → 手紙 → 胴 → ふたの表。ふたの形は script.js が描く -->
    <path class="mail-link__back" />
    <g class="mail-link__letter">
      <rect x="7" y="9.5" width="10" height="9.5" rx="0.5" />
      <path d="M9.5 11.9h5M9.5 13.7h3" />
    </g>
    <path class="mail-link__body" d="M4.5 9h15a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18.5v-8A1.5 1.5 0 0 1 4.5 9Z" />
    <path class="mail-link__front" d="M3 9L12 16L21 9Z" />
  </svg>
  <span>お問い合わせ</span>
</a>

CSS
.mail-link {
  --ink: #eeece6;     /* 線と文字の色 */
  --ground: #111111;  /* 封筒の塗り(地の色) */
  --paper: #262626;   /* 手紙の塗り。ふたが手前を向いたときの明るさにもなる */
  --size: 24px;       /* アイコンの大きさ */
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--ink);
  font-size: 15px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.04em;
  text-decoration: none;
}

/* 24 グリッドで線幅 1.4(24px で描けば 1.4px)。線端と角は丸。面は地の色で塗り、奥の線を隠す */
.mail-link__icon {
  flex: none;
  width: var(--size);
  height: var(--size);
  overflow: visible;
  fill: var(--ground);
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.mail-link__letter rect {
  fill: var(--paper);
}

.mail-link__letter path {
  fill: none;
  stroke-width: 1.05;
}

.mail-link:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

JavaScript
import { gsap } from "gsap";

const HINGE = 9; // ふたの軸(上の辺)の y(24 のグリッドで)
const DEPTH = 7; // ふたの深さ
const EYE = 36; // 透視の視点までの距離。小さいほど、返る途中で大きくふくらむ
const CENTER = 14.5; // 透視の中心の y(封筒の中央)
const RISE = 8; // 手紙が上がる量
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

document.querySelectorAll(".mail-link").forEach((link) => {
  const front = link.querySelector(".mail-link__front");
  const back = link.querySelector(".mail-link__back");
  const letter = link.querySelector(".mail-link__letter");
  const styles = getComputedStyle(link);
  // 色は CSS が正。GSAP は var(--name) のままでは混ぜられないので、値にして渡す
  const shade = gsap.utils.interpolate(styles.getPropertyValue("--ground").trim(), styles.getPropertyValue("--paper").trim());

  const state = { open: 0, lift: 0 };
  const draw = () => {
    // ふたは回さずに、上の辺を軸に返したときの頂点の位置を透視で求めて、三角を描き直す
    const angle = state.open * Math.PI;
    const z = DEPTH * Math.sin(angle);
    const tip = CENTER + (HINGE + DEPTH * Math.cos(angle) - CENTER) * (EYE / (EYE - z));
    // 90° までは表を手紙の手前に、越えたら裏を手紙の奥に描く(1 つの形のままだと、開いたふたが手紙を隠す)
    const [face, hidden] = state.open <= 0.5 ? [front, back] : [back, front];
    face.setAttribute("d", `M3 ${HINGE}L12 ${tip.toFixed(3)}L21 ${HINGE}Z`);
    hidden.setAttribute("d", "");
    face.style.fill = shade(Math.sin(angle)); // 手前を向くほど明るく塗る
    letter.setAttribute("transform", `translate(0 ${(-RISE * state.lift).toFixed(3)})`);
  };
  draw();

  // 手紙は行き過ぎさせない(閉じるときは逆にたどるので、行き過ぎる ease だと沈む前に跳ね上がる)
  const hover = gsap
    .timeline({ paused: true, onUpdate: draw })
    .to(state, { open: 1, duration: 0.4, ease: "power2.inOut" }, 0)
    .to(state, { lift: 1, duration: 0.45, ease: "power3.out" }, 0.22);

  // 閉じるのは 2 倍の速さ。動きを減らす設定では、ほぼ一瞬で切り替える
  const open = () => hover.timeScale(reduceMotion ? 20 : 1).play();
  const close = () => hover.timeScale(reduceMotion ? 20 : 2).reverse();
  link.addEventListener("mouseenter", open);
  link.addEventListener("mouseleave", close);
  // キーボードで焦点が入ったときも開く
  link.addEventListener("focus", open);
  link.addEventListener("blur", close);
});

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

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