前端嘛 Logo
前端嘛
元素周期表

元素周期表

2026-03-13
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Anime.js Periodic Table Auto Layout animation</title>

    <style>
      @font-face {
        font-family: "IoskeleyMono";
        src: url("https://assets.codepen.io/1137/IoskeleyMono-Regular.woff2")
          format("woff2");
        font-weight: 400;
        font-style: normal;
        font-display: swap;
      }

      @font-face {
        font-family: "IoskeleyMono";
        src: url("https://assets.codepen.io/1137/IoskeleyMono-Bold.woff2")
          format("woff2");
        font-weight: 700;
        font-style: normal;
        font-display: swap;
      }

      :root {
        --black-1: #252423;
        --black-2: #262422;
        --black-3: #262422;
        --black-4: #262422;
        --black-5: #272421;
        --black-6: #272421;
        --white-1: #f6f4f2;
        --white-2: #b8b6b3;
        --white-3: #9a9593;
        --white-4: #524f49;
        --white-5: #413d39;
        --white-6: #312e2b;
        --red-1: #ff4b4b;
        --red-2: #bf3e3e;
        --red-3: #9e3838;
        --red-4: #532a29;
        --red-5: #412726;
        --red-6: #322523;
        --corail-1: #ff8333;
        --corail-2: #bf672d;
        --corail-3: #9e582b;
        --corail-4: #533724;
        --corail-5: #412f23;
        --corail-6: #322922;
        --orange-1: #ffa828;
        --orange-2: #bf8026;
        --orange-3: #9e6d25;
        --orange-4: #523e23;
        --orange-5: #413422;
        --orange-6: #322b21;
        --yellow-1: #ffcc2a;
        --yellow-2: #bf9a27;
        --yellow-3: #9e8026;
        --yellow-4: #524723;
        --yellow-5: #413922;
        --yellow-6: #322d21;
        --citrus-1: #f9f640;
        --citrus-2: #bab836;
        --citrus-3: #9b9932;
        --citrus-4: #515027;
        --citrus-5: #403f24;
        --citrus-6: #323022;
        --lime-1: #b7ff54;
        --lime-2: #8bbe44;
        --lime-3: #759d3d;
        --lime-4: #42522b;
        --lime-5: #384027;
        --lime-6: #2d3123;
        --green-1: #6aff65;
        --green-2: #54be50;
        --green-3: #4a9e45;
        --green-4: #31522e;
        --green-5: #2c4029;
        --green-6: #273124;
        --emerald-1: #57f695;
        --emerald-2: #47b873;
        --emerald-3: #409962;
        --emerald-4: #2d5039;
        --emerald-5: #293f30;
        --emerald-6: #263128;
        --turquoise-1: #66ffbc;
        --turquoise-2: #52be8e;
        --turquoise-3: #479e77;
        --turquoise-4: #305242;
        --turquoise-5: #2b4035;
        --turquoise-6: #26312b;
        --cyan-1: #26f2d5;
        --cyan-2: #25b5a0;
        --cyan-3: #259686;
        --cyan-4: #244e48;
        --cyan-5: #233f39;
        --cyan-6: #23302d;
        --sega-1: #05dbe9;
        --sega-2: #0fa4ae;
        --sega-3: #138990;
        --sega-4: #1e4a4c;
        --sega-5: #203b3c;
        --sega-6: #212f2f;
        --sky-1: #33b3f1;
        --sky-2: #2e88b4;
        --sky-3: #2c7395;
        --sky-4: #26424e;
        --sky-5: #25363e;
        --sky-6: #242c2f;
        --indigo-1: #717aff;
        --indigo-2: #595fbe;
        --indigo-3: #4d519e;
        --indigo-4: #323351;
        --indigo-5: #2c2c3f;
        --indigo-6: #282630;
        --lavender-1: #a369ff;
        --lavender-2: #7d53be;
        --lavender-3: #6a489e;
        --lavender-4: #3e3051;
        --lavender-5: #342a3f;
        --lavender-6: #2b2530;
        --purple-1: #c06ddf;
        --purple-2: #9356a8;
        --purple-3: #7b4a8c;
        --purple-4: #45314b;
        --purple-5: #392b3c;
        --purple-6: #2f262d;
        --magenta-1: #e962bf;
        --magenta-2: #af4e90;
        --magenta-3: #93447a;
        --magenta-4: #4e2e43;
        --magenta-5: #3f2936;
        --magenta-6: #31252b;
        --pink-1: #ff86a7;
        --pink-2: #bf687f;
        --pink-3: #9f586b;
        --pink-4: #53363c;
        --pink-5: #412e32;
        --pink-6: #322729;
        --bg-1: #252423;
        --bg-2: #2a2928;
        --bg-3: #2f2e2d;
        --bg-4: #353433;
        --bg-5: #3a3938;
        --fg-1: #dddcda;
        --fg-2: #c6c3c1;
        --fg-3: #96918f;
        --fg-4: #65655e;
        --fg-5: #33332e;
        --input-border-radius: 0.25rem;
        --br: 1rem;
        --padding: 1rem;
        --border-width: 1px;
      }

      [data-color="fg"] {
        --color-1: var(--fg-1);
        --color-2: var(--fg-2);
        --color-3: var(--fg-3);
        --color-4: var(--fg-4);
        --color-5: var(--fg-5);
      }
      [data-color="bg"] {
        --color-1: var(--bg-1);
        --color-2: var(--bg-2);
        --color-3: var(--bg-3);
        --color-4: var(--bg-4);
        --color-5: var(--bg-5);
      }
      [data-color="0"] {
        --color-1: var(--red-1);
        --color-2: var(--red-2);
        --color-3: var(--red-3);
        --color-4: var(--red-4);
        --color-5: var(--red-5);
      }
      [data-color="1"] {
        --color-1: var(--corail-1);
        --color-2: var(--corail-2);
        --color-3: var(--corail-3);
        --color-4: var(--corail-4);
        --color-5: var(--corail-5);
      }
      [data-color="2"] {
        --color-1: var(--orange-1);
        --color-2: var(--orange-2);
        --color-3: var(--orange-3);
        --color-4: var(--orange-4);
        --color-5: var(--orange-5);
      }
      [data-color="3"] {
        --color-1: var(--yellow-1);
        --color-2: var(--yellow-2);
        --color-3: var(--yellow-3);
        --color-4: var(--yellow-4);
        --color-5: var(--yellow-5);
      }
      [data-color="4"] {
        --color-1: var(--citrus-1);
        --color-2: var(--citrus-2);
        --color-3: var(--citrus-3);
        --color-4: var(--citrus-4);
        --color-5: var(--citrus-5);
      }
      [data-color="5"] {
        --color-1: var(--lime-1);
        --color-2: var(--lime-2);
        --color-3: var(--lime-3);
        --color-4: var(--lime-4);
        --color-5: var(--lime-5);
      }
      [data-color="6"] {
        --color-1: var(--green-1);
        --color-2: var(--green-2);
        --color-3: var(--green-3);
        --color-4: var(--green-4);
        --color-5: var(--green-5);
      }
      [data-color="7"] {
        --color-1: var(--emerald-1);
        --color-2: var(--emerald-2);
        --color-3: var(--emerald-3);
        --color-4: var(--emerald-4);
        --color-5: var(--emerald-5);
      }
      [data-color="8"] {
        --color-1: var(--turquoise-1);
        --color-2: var(--turquoise-2);
        --color-3: var(--turquoise-3);
        --color-4: var(--turquoise-4);
        --color-5: var(--turquoise-5);
      }
      [data-color="9"] {
        --color-1: var(--cyan-1);
        --color-2: var(--cyan-2);
        --color-3: var(--cyan-3);
        --color-4: var(--cyan-4);
        --color-5: var(--cyan-5);
      }
      [data-color="10"] {
        --color-1: var(--sega-1);
        --color-2: var(--sega-2);
        --color-3: var(--sega-3);
        --color-4: var(--sega-4);
        --color-5: var(--sega-5);
      }
      [data-color="11"] {
        --color-1: var(--sky-1);
        --color-2: var(--sky-2);
        --color-3: var(--sky-3);
        --color-4: var(--sky-4);
        --color-5: var(--sky-5);
      }
      [data-color="12"] {
        --color-1: var(--indigo-1);
        --color-2: var(--indigo-2);
        --color-3: var(--indigo-3);
        --color-4: var(--indigo-4);
        --color-5: var(--indigo-5);
      }
      [data-color="13"] {
        --color-1: var(--lavender-1);
        --color-2: var(--lavender-2);
        --color-3: var(--lavender-3);
        --color-4: var(--lavender-4);
        --color-5: var(--lavender-5);
      }

      *,
      *:before,
      *:after {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
        border: 0;
        font: inherit;
        vertical-align: baseline;
      }

      html,
      body {
        background-color: var(--bg-1);
        color: var(--fg-1);
        font-family: "IoskeleyMono", monospace, sans-serif;
      }

      .br {
        border-radius: var(--br);
      }
      .br .br {
        border-radius: calc(var(--br) * 0.5);
      }
      .br .br .br {
        border-radius: calc(var(--br) * 0.25);
      }

      .black {
        color: var(--bg-1);
      }
      .white {
        color: var(--fg-1);
      }
      .red {
        color: var(--red-1);
      }
      .orange {
        color: var(--corail-1);
      }
      .yellow {
        color: var(--yellow-1);
      }
      .blue {
        color: var(--sky-1);
      }
      .green {
        color: var(--green-1);
      }

      .grid {
        --one-cell: 100px;
        --border-color: rgba(255, 255, 255, 0.75);
        flex-grow: 1;
        position: relative;
        width: 100%;
        height: 100%;
        padding: 0;
      }

      .grid:before {
        content: "";
        pointer-events: none;
        position: absolute;
        z-index: 0;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        background:
          linear-gradient(
            -90deg,
            rgba(255, 255, 255, 0.05) 1px,
            transparent 1px
          ),
          linear-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 1px),
          linear-gradient(
            -90deg,
            rgba(255, 255, 255, 0.04) 1px,
            transparent 1px
          ),
          linear-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px);
        background-size:
          calc(var(--one-cell) / 10) calc(var(--one-cell) / 10),
          calc(var(--one-cell) / 10) calc(var(--one-cell) / 10),
          var(--one-cell) var(--one-cell),
          var(--one-cell) var(--one-cell),
          var(--one-cell) var(--one-cell),
          var(--one-cell) var(--one-cell);
        background-position: 0 -1px;
        backface-visibility: hidden;
      }

      .grid:before {
        width: calc(75px + 100%);
        left: -75px;
      }

      @media (min-width: 500px) {
        .grid:before {
          width: 100%;
          left: 0;
        }
      }

      /* Logs */

      .log {
        --width: 22ch;
        --height: 20ch;
        position: fixed;
        z-index: 10;
        top: 0;
        overflow-y: scroll;
        width: var(--width);
        height: var(--height);
        padding-left: 1ch;
        padding-right: 1ch;
        padding-bottom: 1ch;
        font-family: ui-monospace, monospace;
        white-space: pre;
        background-color: rgba(0, 0, 0, 0.5);
      }

      @keyframes reveal {
        from {
          opacity: 1;
        }
        to {
          opacity: 0.4;
        }
      }

      .log span {
        animation: reveal 2s ease-in-out forwards;
      }

      /* Inputs */

      button,
      select {
        width: 100%;
        flex-shrink: 0.5;
        border: none;
        background-color: var(--white-1);
        border-radius: var(--input-border-radius);
        white-space: nowrap;
        margin: 0;
        height: 2rem;
      }

      button {
        padding: 0 0.75rem;
      }

      select:disabled,
      button:disabled {
        background-color: var(--grey);
      }

      select:not(:disabled):hover,
      button:not(:disabled):hover {
        cursor: pointer;
        background-color: var(--white-2);
      }

      legend {
        display: flex;
        gap: 0.5rem;
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        width: 100%;
        height: 2rem;
      }

      label {
        display: flex;
        gap: 0.5rem;
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        height: 2rem;
      }

      fieldset {
        display: flex;
        flex-wrap: nowrap;
        flex-direction: row;
        justify-content: center;
        align-items: center;
        gap: 1px;
      }

      fieldset.vertical {
        flex-direction: column;
        justify-content: flex-start;
        align-items: flex-start;
      }

      fieldset > * {
        border-radius: 0;
      }

      fieldset > *:first-child {
        border-radius: var(--input-border-radius) 0 0 var(--input-border-radius);
      }

      fieldset > *:last-child {
        border-radius: 0 var(--input-border-radius) var(--input-border-radius) 0;
      }

      body {
        --header-height: 4rem;
        display: flex;
        flex-direction: column;
        min-height: 100lvh;
        overflow: hidden;
        perspective: 1000px;
      }
      #scene {
        position: fixed;
        transform-style: preserve-3d;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        padding: calc(var(--header-height) + var(--padding)) var(--padding)
          var(--padding);
        inset: 0;
        padding: var(--header-height) var(--padding);
      }
      #scene-content {
        pointer-events: none;
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        transform-style: preserve-3d;
        backface-visibility: hidden;
      }
      #scene-content[data-layout="table"] {
        display: grid;
        grid-template-columns: repeat(18, var(--w));
        grid-auto-rows: var(--h);
        gap: 0.25rem;
        place-content: center;
      }
      .controls {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: row;
        flex-shrink: 1;
        position: fixed;
        z-index: 3;
        top: 0px;
        width: 100%;
        height: var(--header-height);
        padding: var(--padding);
      }
      .controls-group {
        display: flex;
        justify-content: space-between;
        align-items: center;
        flex-direction: row;
        height: var(--header-height);
        gap: calc(var(--padding) * 0.5);
      }
      .controls button {
        background-color: var(--bg-4);
        color: var(--fg-1);
        flex-shrink: 1;
      }
      .controls button:hover {
        background-color: var(--bg-5);
      }
      .controls button.is-active {
        background-color: var(--fg-1);
        color: var(--bg-3);
      }
      .credits {
        text-align: center;
        position: fixed;
        z-index: 1;
        bottom: 0px;
        width: 100%;
        height: var(--header-height);
        padding: var(--padding);
        font-size: 0.75rem;
      }
      .credits a {
        color: var(--red-1);
        text-decoration: none;
      }
      .element {
        overflow: hidden;
        pointer-events: auto;
        --w: 3.5rem;
        --h: 4rem;
        position: relative;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: space-between;
        border: 1px solid var(--color-3);
        background-color: var(--color-5);
        color: var(--color-1);
        width: var(--w);
        height: var(--h);
        border-radius: 0.25rem;
        padding: 0.25rem;
        transform-style: preserve-3d;
        z-index: 8;
        cursor: pointer;
      }
      button.element:focus-visible,
      button.element:hover {
        border: 1px solid var(--color-1);
        background-color: var(--color-4);
        color: var(--color-1);
        outline: none;
      }
      .element.is-expanded {
        --w: 13rem;
        --h: 13rem;
        z-index: 1;
        padding: 1rem;
        align-items: flex-start;
      }
      .element-description {
        display: none;
        transform-style: preserve-3d;
        flex-direction: column;
        gap: 0.25rem;
        width: 100%;
        font-size: 0.625rem;
        text-transform: none;
        color: var(--color-1);
        justify-self: flex-end;
        margin-top: 1.5rem;
        text-align: left;
      }
      .element.is-expanded .element-description {
        display: block;
      }
      .element-symbol {
        transform-style: preserve-3d;
        margin-top: -0.1rem;
        margin-bottom: 0.2rem;
        font-size: 1.5rem;
        font-weight: 700;
      }
      .element.is-expanded .element-symbol {
        margin-top: 0;
        margin-bottom: 0;
        font-size: 1rem;
        text-align: left;
      }
      .element-title {
        transform-style: preserve-3d;
        font-size: 0.5rem;
        font-weight: 700;
        text-align: center;
      }
      .element.is-expanded .element-title {
        font-size: 1.5rem;
        text-align: left;
        margin-top: 1rem;
      }
      .element-number {
        transform-style: preserve-3d;
        font-size: 0.5rem;
        text-align: right;
        align-self: flex-end;
      }
      .element.is-expanded .element-number {
        position: absolute;
        font-size: 1rem;
        top: 1rem;
        right: 1rem;
      }
      #scene-content[data-layout="sphere"] .element,
      #scene-content[data-layout="helix"] .element,
      #scene-content[data-layout="grid"] .element {
        position: absolute;
        left: 50%;
        top: 50%;
        margin-left: calc(var(--w) * -0.5);
        margin-top: calc(var(--h) * -0.5);
      }
      #scene-content[data-layout="table"] .element.is-expanded {
        left: -1.625rem;
        top: -1.25rem;
        margin-left: calc(var(--w) * -0.25);
        margin-top: calc(var(--h) * -0.25);
        margin-right: calc(var(--w) * -0.5);
        margin-bottom: calc(var(--h) * -0.5);
      }
    </style>
  </head>

  <body>
    <div class="controls">
      <div class="controls-group display br">
        <button class="is-active toggle" id="table">table</button>
        <button class="toggle" id="sphere">sphere</button>
        <button class="toggle" id="helix">helix</button>
        <button class="toggle" id="grid">grid</button>
      </div>
    </div>
    <div id="scene">
      <div id="scene-content" data-layout="table"></div>
    </div>

    <template id="element">
      <button class="element" data-color="0">
        <div class="element-number"></div>
        <div class="element-symbol"></div>
        <div class="element-title"></div>
        <div class="element-description"></div>
      </button>
    </template>
    <script type="module">
      import {
        createLayout,
        utils,
        stagger,
        spring,
        createTimer,
        createAnimatable,
      } from "https://esm.sh/animejs@4.3.0";

      // Elements data to populate the table

      const ELEMENT_FIELDS = {
        SYMBOL: 0,
        NAME: 1,
        COLOR: 2,
        COLUMN: 3,
        ROW: 4,
        ATOMIC_MASS: 5,
        DENSITY: 6,
        MELTING_POINT: 7,
        BOILING_POINT: 8,
      };
      const ELEMENT_STRIDE = 9;

      const elements = [
        // symbol, name, color, column, row, atomicMass, density, meltingPoint, boilingPoint
        "H",
        "Hydrogen",
        0,
        1,
        1,
        1.008,
        0.08988,
        -259.16,
        -252.88,
        "He",
        "Helium",
        1,
        18,
        1,
        4.0026022,
        0.1786,
        -272.2,
        -268.93,
        "Li",
        "Lithium",
        2,
        1,
        2,
        6.94,
        0.534,
        180.5,
        1329.85,
        "Be",
        "Beryllium",
        3,
        2,
        2,
        9.01218315,
        1.85,
        1286.85,
        2468.85,
        "B",
        "Boron",
        4,
        13,
        2,
        10.81,
        2.08,
        2075.85,
        3926.85,
        "C",
        "Carbon",
        0,
        14,
        2,
        12.011,
        1.821,
        null,
        null,
        "N",
        "Nitrogen",
        0,
        15,
        2,
        14.007,
        1.251,
        -210,
        -195.79,
        "O",
        "Oxygen",
        0,
        16,
        2,
        15.999,
        1.429,
        -218.79,
        -182.96,
        "F",
        "Fluorine",
        0,
        17,
        2,
        18.9984031636,
        1.696,
        -219.67,
        -188.12,
        "Ne",
        "Neon",
        1,
        18,
        2,
        20.17976,
        0.9002,
        -248.59,
        -246.05,
        "Na",
        "Sodium",
        2,
        1,
        3,
        22.989769282,
        0.968,
        97.79,
        882.94,
        "Mg",
        "Magnesium",
        3,
        2,
        3,
        24.305,
        1.738,
        649.85,
        1089.85,
        "Al",
        "Aluminium",
        5,
        13,
        3,
        26.98153857,
        2.7,
        660.32,
        2469.85,
        "Si",
        "Silicon",
        4,
        14,
        3,
        28.085,
        2.329,
        1413.85,
        3264.85,
        "P",
        "Phosphorus",
        0,
        15,
        3,
        30.9737619985,
        1.823,
        null,
        null,
        "S",
        "Sulfur",
        0,
        16,
        3,
        32.06,
        2.07,
        115.21,
        444.65,
        "Cl",
        "Chlorine",
        0,
        17,
        3,
        35.45,
        3.2,
        -101.55,
        -34.04,
        "Ar",
        "Argon",
        1,
        18,
        3,
        39.9481,
        1.784,
        -189.34,
        -185.85,
        "K",
        "Potassium",
        2,
        1,
        4,
        39.09831,
        0.862,
        63.55,
        758.85,
        "Ca",
        "Calcium",
        3,
        2,
        4,
        40.0784,
        1.55,
        841.85,
        1483.85,
        "Sc",
        "Scandium",
        6,
        3,
        4,
        44.9559085,
        2.985,
        1540.85,
        2835.85,
        "Ti",
        "Titanium",
        6,
        4,
        4,
        47.8671,
        4.506,
        1667.85,
        3286.85,
        "V",
        "Vanadium",
        6,
        5,
        4,
        50.94151,
        6,
        1909.85,
        3406.85,
        "Cr",
        "Chromium",
        6,
        6,
        4,
        51.99616,
        7.19,
        1906.85,
        2670.85,
        "Mn",
        "Manganese",
        6,
        7,
        4,
        54.9380443,
        7.21,
        1245.85,
        2060.85,
        "Fe",
        "Iron",
        6,
        8,
        4,
        55.8452,
        7.874,
        1537.85,
        2860.85,
        "Co",
        "Cobalt",
        6,
        9,
        4,
        58.9331944,
        8.9,
        1494.85,
        2926.85,
        "Ni",
        "Nickel",
        6,
        10,
        4,
        58.69344,
        8.908,
        1454.85,
        2729.85,
        "Cu",
        "Copper",
        6,
        11,
        4,
        63.5463,
        8.96,
        1084.62,
        2561.85,
        "Zn",
        "Zinc",
        5,
        12,
        4,
        65.382,
        7.14,
        419.53,
        906.85,
        "Ga",
        "Gallium",
        5,
        13,
        4,
        69.7231,
        5.91,
        29.76,
        2399.85,
        "Ge",
        "Germanium",
        4,
        14,
        4,
        72.6308,
        5.323,
        938.25,
        2832.85,
        "As",
        "Arsenic",
        4,
        15,
        4,
        74.9215956,
        5.727,
        null,
        null,
        "Se",
        "Selenium",
        0,
        16,
        4,
        78.9718,
        4.81,
        220.85,
        684.85,
        "Br",
        "Bromine",
        0,
        17,
        4,
        79.904,
        3.1028,
        -7.35,
        58.85,
        "Kr",
        "Krypton",
        1,
        18,
        4,
        83.7982,
        3.749,
        -157.37,
        -153.22,
        "Rb",
        "Rubidium",
        2,
        1,
        5,
        85.46783,
        1.532,
        39.3,
        687.85,
        "Sr",
        "Strontium",
        3,
        2,
        5,
        87.621,
        2.64,
        776.85,
        1376.85,
        "Y",
        "Yttrium",
        6,
        3,
        5,
        88.905842,
        4.472,
        1525.85,
        2929.85,
        "Zr",
        "Zirconium",
        6,
        4,
        5,
        91.2242,
        6.52,
        1854.85,
        4376.85,
        "Nb",
        "Niobium",
        6,
        5,
        5,
        92.906372,
        8.57,
        2476.85,
        4743.85,
        "Mo",
        "Molybdenum",
        6,
        6,
        5,
        95.951,
        10.28,
        2622.85,
        4638.85,
        "Tc",
        "Technetium",
        6,
        7,
        5,
        98,
        11,
        2156.85,
        4264.85,
        "Ru",
        "Ruthenium",
        6,
        8,
        5,
        101.072,
        12.45,
        2333.85,
        4149.85,
        "Rh",
        "Rhodium",
        6,
        9,
        5,
        102.905502,
        12.41,
        1963.85,
        3694.85,
        "Pd",
        "Palladium",
        6,
        10,
        5,
        106.421,
        12.023,
        1554.9,
        2962.85,
        "Ag",
        "Silver",
        6,
        11,
        5,
        107.86822,
        10.49,
        961.78,
        2161.85,
        "Cd",
        "Cadmium",
        5,
        12,
        5,
        112.4144,
        8.65,
        321.07,
        766.85,
        "In",
        "Indium",
        5,
        13,
        5,
        114.8181,
        7.31,
        156.6,
        2071.85,
        "Sn",
        "Tin",
        5,
        14,
        5,
        118.7107,
        7.365,
        231.93,
        2601.85,
        "Sb",
        "Antimony",
        4,
        15,
        5,
        121.7601,
        6.697,
        630.63,
        1634.85,
        "Te",
        "Tellurium",
        4,
        16,
        5,
        127.603,
        6.24,
        449.51,
        987.85,
        "I",
        "Iodine",
        0,
        17,
        5,
        126.904473,
        4.933,
        113.7,
        184.25,
        "Xe",
        "Xenon",
        1,
        18,
        5,
        131.2936,
        5.894,
        -111.75,
        -108.1,
        "Cs",
        "Cesium",
        2,
        1,
        6,
        132.905451966,
        1.93,
        28.55,
        670.85,
        "Ba",
        "Barium",
        3,
        2,
        6,
        137.3277,
        3.51,
        726.85,
        1844.85,
        "Hf",
        "Hafnium",
        6,
        4,
        6,
        178.492,
        13.31,
        2232.85,
        4602.85,
        "Ta",
        "Tantalum",
        6,
        5,
        6,
        180.947882,
        16.69,
        3016.85,
        5457.85,
        "W",
        "Tungsten",
        6,
        6,
        6,
        183.841,
        19.25,
        3421.85,
        5929.85,
        "Re",
        "Rhenium",
        6,
        7,
        6,
        186.2071,
        21.02,
        3185.85,
        5595.85,
        "Os",
        "Osmium",
        6,
        8,
        6,
        190.233,
        22.59,
        3032.85,
        5011.85,
        "Ir",
        "Iridium",
        6,
        9,
        6,
        192.2173,
        22.56,
        2445.85,
        4129.85,
        "Pt",
        "Platinum",
        6,
        10,
        6,
        195.0849,
        21.45,
        1768.25,
        3824.85,
        "Au",
        "Gold",
        6,
        11,
        6,
        196.9665695,
        19.3,
        1064.18,
        2969.85,
        "Hg",
        "Mercury",
        5,
        12,
        6,
        200.5923,
        13.534,
        -38.83,
        356.73,
        "Tl",
        "Thallium",
        5,
        13,
        6,
        204.38,
        11.85,
        303.85,
        1472.85,
        "Pb",
        "Lead",
        5,
        14,
        6,
        207.21,
        11.34,
        327.46,
        1748.85,
        "Bi",
        "Bismuth",
        5,
        15,
        6,
        208.980401,
        9.78,
        271.55,
        1563.85,
        "Po",
        "Polonium",
        4,
        16,
        6,
        209,
        9.196,
        253.85,
        961.85,
        "At",
        "Astatine",
        0,
        17,
        6,
        210,
        6.35,
        301.85,
        336.85,
        "Rn",
        "Radon",
        1,
        18,
        6,
        222,
        9.73,
        -71.15,
        -61.65,
        "Fr",
        "Francium",
        2,
        1,
        7,
        223,
        1.87,
        26.85,
        676.85,
        "Ra",
        "Radium",
        3,
        2,
        7,
        226,
        5.5,
        959.85,
        1736.85,
        "Rf",
        "Rutherfordium",
        6,
        4,
        7,
        267,
        23.2,
        2126.85,
        5526.85,
        "Db",
        "Dubnium",
        6,
        5,
        7,
        268,
        29.3,
        null,
        null,
        "Sg",
        "Seaborgium",
        6,
        6,
        7,
        269,
        35,
        null,
        null,
        "Bh",
        "Bohrium",
        6,
        7,
        7,
        270,
        37.1,
        null,
        null,
        "Hs",
        "Hassium",
        6,
        8,
        7,
        269,
        40.7,
        -147.15,
        null,
        "Mt",
        "Meitnerium",
        9,
        9,
        7,
        278,
        37.4,
        null,
        null,
        "Ds",
        "Darmstadtium",
        9,
        10,
        7,
        281,
        34.8,
        null,
        null,
        "Rg",
        "Roentgenium",
        9,
        11,
        7,
        282,
        28.7,
        null,
        null,
        "Cn",
        "Copernicium",
        9,
        12,
        7,
        285,
        14,
        null,
        3296.85,
        "Nh",
        "Nihonium",
        9,
        13,
        7,
        286,
        16,
        426.85,
        1156.85,
        "Fl",
        "Flerovium",
        9,
        14,
        7,
        289,
        14,
        66.85,
        146.85,
        "Mc",
        "Moscovium",
        9,
        15,
        7,
        289,
        13.5,
        396.85,
        1126.85,
        "Lv",
        "Livermorium",
        9,
        16,
        7,
        293,
        12.9,
        435.85,
        811.85,
        "Ts",
        "Tennessine",
        9,
        17,
        7,
        294,
        7.17,
        449.85,
        609.85,
        "Og",
        "Oganesson",
        9,
        18,
        7,
        294,
        4.95,
        null,
        76.85,
        "La",
        "Lanthanum",
        7,
        3,
        8,
        138.905477,
        6.162,
        919.85,
        3463.85,
        "Ce",
        "Cerium",
        7,
        4,
        8,
        140.1161,
        6.77,
        794.85,
        3442.85,
        "Pr",
        "Praseodymium",
        7,
        5,
        8,
        140.907662,
        6.77,
        934.85,
        3129.85,
        "Nd",
        "Neodymium",
        7,
        6,
        8,
        144.2423,
        7.01,
        1023.85,
        3073.85,
        "Pm",
        "Promethium",
        7,
        7,
        8,
        145,
        7.26,
        1041.85,
        2999.85,
        "Sm",
        "Samarium",
        7,
        8,
        8,
        150.362,
        7.52,
        1071.85,
        1899.85,
        "Eu",
        "Europium",
        7,
        9,
        8,
        151.9641,
        5.264,
        825.85,
        1528.85,
        "Gd",
        "Gadolinium",
        7,
        10,
        8,
        157.253,
        7.9,
        1311.85,
        2999.85,
        "Tb",
        "Terbium",
        7,
        11,
        8,
        158.925352,
        8.23,
        1355.85,
        3122.85,
        "Dy",
        "Dysprosium",
        7,
        12,
        8,
        162.5001,
        8.54,
        1406.85,
        2566.85,
        "Ho",
        "Holmium",
        7,
        13,
        8,
        164.930332,
        8.79,
        1460.85,
        2599.85,
        "Er",
        "Erbium",
        7,
        14,
        8,
        167.2593,
        9.066,
        1528.85,
        2867.85,
        "Tm",
        "Thulium",
        7,
        15,
        8,
        168.934222,
        9.32,
        1544.85,
        1949.85,
        "Yb",
        "Ytterbium",
        7,
        16,
        8,
        173.0451,
        6.9,
        823.85,
        1195.85,
        "Lu",
        "Lutetium",
        7,
        17,
        8,
        174.96681,
        9.841,
        1651.85,
        3401.85,
        "Ac",
        "Actinium",
        8,
        3,
        9,
        227,
        10,
        1226.85,
        3226.85,
        "Th",
        "Thorium",
        8,
        4,
        9,
        232.03774,
        11.724,
        1749.85,
        4787.85,
        "Pa",
        "Protactinium",
        8,
        5,
        9,
        231.035882,
        15.37,
        1567.85,
        4026.85,
        "U",
        "Uranium",
        8,
        6,
        9,
        238.028913,
        19.1,
        1132.15,
        4130.85,
        "Np",
        "Neptunium",
        8,
        7,
        9,
        237,
        20.45,
        638.85,
        4173.85,
        "Pu",
        "Plutonium",
        8,
        8,
        9,
        244,
        19.816,
        639.35,
        3231.85,
        "Am",
        "Americium",
        8,
        9,
        9,
        243,
        12,
        1175.85,
        2606.85,
        "Cm",
        "Curium",
        8,
        10,
        9,
        247,
        13.51,
        1339.85,
        3109.85,
        "Bk",
        "Berkelium",
        8,
        11,
        9,
        247,
        14.78,
        985.85,
        2626.85,
        "Cf",
        "Californium",
        8,
        12,
        9,
        251,
        15.1,
        899.85,
        1469.85,
        "Es",
        "Einsteinium",
        8,
        13,
        9,
        252,
        8.84,
        859.85,
        995.85,
        "Fm",
        "Fermium",
        8,
        14,
        9,
        257,
        null,
        1526.85,
        null,
        "Md",
        "Mendelevium",
        8,
        15,
        9,
        258,
        null,
        826.85,
        null,
        "No",
        "Nobelium",
        8,
        16,
        9,
        259,
        null,
        826.85,
        null,
        "Lr",
        "Lawrencium",
        8,
        17,
        9,
        266,
        null,
        1626.85,
        null,
      ];

      // Genetate the table html

      const [$sceneContent] = utils.$("#scene-content");
      const [$template] = utils.$("#element");

      for (let i = 0, l = elements.length / ELEMENT_STRIDE; i < l; i += 1) {
        const offset = i * ELEMENT_STRIDE;
        const $el = $template.content.cloneNode(true);
        const $element = $el.querySelector(".element");
        const $number = $element.querySelector(".element-number");
        const $symbol = $element.querySelector(".element-symbol");
        const $title = $element.querySelector(".element-title");
        const $description = $element.querySelector(".element-description");
        $number.textContent = i + 1;
        $symbol.textContent = elements[offset + ELEMENT_FIELDS.SYMBOL];
        $title.textContent = elements[offset + ELEMENT_FIELDS.NAME];
        $element.dataset.color = elements[offset + ELEMENT_FIELDS.COLOR];
        $element.style.gridColumn = elements[offset + ELEMENT_FIELDS.COLUMN];
        $element.style.gridRow = elements[offset + ELEMENT_FIELDS.ROW];
        $description.innerHTML = [
          `Atomic mass: ${elements[offset + ELEMENT_FIELDS.ATOMIC_MASS]} u`,
          `Density: ${elements[offset + ELEMENT_FIELDS.DENSITY]} g/cm3`,
          `Melting: ${elements[offset + ELEMENT_FIELDS.MELTING_POINT]} &deg;C`,
          `Boiling: ${elements[offset + ELEMENT_FIELDS.BOILING_POINT]} &deg;C`,
        ].join("<br>");
        $sceneContent.appendChild($el);
      }

      const cards = utils.$("#scene-content .element");

      // Create the cards layout and register the font-size as an extra property for animation
      const elementsLayout = createLayout($sceneContent, {
        properties: ["font-size"],
        duration: 2000,
        ease: "inOutExpo",
      });

      // Move the scene relative to the cursor position
      const sceneAnimatable = createAnimatable("#scene", {
        rotateX: 200,
        rotateY: 200,
      });
      const pointer = { x: 0, y: 0, rotateX: 15, rotateY: 20, rx: 0, ry: 0 };

      createTimer({
        onUpdate: () => {
          pointer.rx = utils.lerp(pointer.rx, pointer.rotateX, 0.01);
          pointer.ry = utils.lerp(pointer.ry, pointer.rotateY, 0.01);
          sceneAnimatable.rotateX(pointer.y * pointer.rx);
          sceneAnimatable.rotateY(pointer.x * pointer.ry);
        },
      });

      // The different layout tranform
      const transformLayout = {
        table: () => {
          // The table view use CSS grid and has no special tranforms except for a selected element
          pointer.rotateX = 15;
          pointer.rotateY = 20;
          cards.forEach(($el) => {
            $el.style.opacity = 1;
            $el.style.transform = $el.classList.contains("is-expanded")
              ? "translateZ(50px)"
              : "translateZ(10px)";
          });
        },
        sphere: () => {
          const radius = 300;
          pointer.rotateX = 40;
          pointer.rotateY = 360;
          cards.forEach(($el, i) => {
            const offsetZ = $el.classList.contains("is-expanded") ? 20 : 0;
            const phi = Math.acos(-1 + (2 * i) / cards.length);
            const theta = Math.sqrt(cards.length * Math.PI) * phi;
            const sinPhi = Math.sin(phi);
            const x = radius * sinPhi * Math.cos(theta);
            const y = radius * Math.cos(phi);
            const z = radius * sinPhi * Math.sin(theta);
            const yaw = Math.atan2(x, z);
            const pitch = -Math.atan2(y, Math.hypot(x, z));
            $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px) rotateY(${yaw}rad) rotateX(${pitch}rad) translateZ(${offsetZ}px)`;
          });
        },
        helix: () => {
          pointer.rotateX = 30;
          pointer.rotateY = 300;
          const radius = 400;
          const thetaStep = 0.16;
          const verticalSpacing = 3;
          const yOffset = (cards.length * verticalSpacing) / 2;
          cards.forEach(($el, i) => {
            const offsetZ = $el.classList.contains("is-expanded") ? 20 : 0;
            const theta = i * thetaStep + Math.PI;
            const y = -(i * verticalSpacing) + yOffset;
            const x = radius * Math.sin(theta);
            const z = radius * Math.cos(theta);
            const yaw = Math.atan2(x, z);
            const pitch = -Math.atan2(y, Math.hypot(x, z) * 2);
            $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px) rotateY(${yaw}rad) rotateX(${pitch}rad) translateZ(${offsetZ}px)`;
          });
        },
        grid: () => {
          pointer.rotateX = 10;
          pointer.rotateY = 60;
          const cols = 5;
          const rows = 5;
          const colGap = 150;
          const rowGap = 100;
          const depthGap = 150;
          const perLayer = cols * rows;
          const layers = Math.ceil(cards.length / perLayer);
          cards.forEach(($el, i) => {
            const offsetZ = $el.classList.contains("is-expanded") ? 20 : 0;
            const col = i % cols;
            const row = Math.floor(i / cols) % rows;
            const layer = Math.floor(i / perLayer);
            const x = (col - (cols - 1) / 2) * colGap;
            const y = ((rows - 1) / 2 - row) * rowGap;
            const z = offsetZ + (layer - (layers - 1) / 2) * depthGap;
            $el.style.transform = `translate3d(${x}px, ${y}px, ${z}px)`;
          });
        },
        random: () => {
          // The table view use CSS grid and has no special tranforms except for a selected element
          pointer.rotateX = 15;
          pointer.rotateY = 20;
          utils.set(cards, {
            x: () => utils.random(-500, 500),
            y: () => utils.random(-500, 500),
            z: () => utils.random(-500, 500),
          });
        },
      };

      document.addEventListener("pointermove", (event) => {
        const hw = window.innerWidth * 0.5;
        const hh = window.innerHeight * 0.5;
        pointer.x = utils.mapRange(event.clientX - hw, -hw, hw, 1, -1);
        pointer.y = utils.mapRange(event.clientY - hh, -hh, hh, -1, 1);
      });

      const toggles = utils.$(".controls button.toggle");

      document.addEventListener("click", (event) => {
        const $toggle = event.target.closest(".controls button.toggle");
        if ($toggle) {
          toggles.forEach((button) => button.classList.remove("is-active"));
          $toggle.classList.add("is-active");
          const layoutType = $toggle.id;
          elementsLayout.update(
            () => {
              cards.forEach(($el) => $el.classList.remove("is-expanded"));
              $sceneContent.dataset.layout = layoutType;
              transformLayout[layoutType]();
            },
            {
              delay: stagger([0, 750], { from: "random" }),
            },
          );
          return;
        }
        const $card = event.target.closest("#scene-content .element");
        const shouldExpand = $card && !$card.classList.contains("is-expanded");
        elementsLayout.update(
          () => {
            cards.forEach(($el) => $el.classList.remove("is-expanded"));
            if (shouldExpand) $card.classList.add("is-expanded");
            transformLayout[$sceneContent.dataset.layout]();
          },
          {
            ease: spring({ bounce: 0.2, duration: 350 }),
          },
        );
      });

      document.addEventListener("keydown", (event) => {
        if (event.key !== "Escape") return;
        const hasExpandedCard = cards.some(($el) =>
          $el.classList.contains("is-expanded"),
        );
        if (!hasExpandedCard) return;
        elementsLayout.update(
          () => {
            cards.forEach(($el) => $el.classList.remove("is-expanded"));
            transformLayout[$sceneContent.dataset.layout]();
          },
          {
            ease: spring({ bounce: 0.3, duration: 350 }),
          },
        );
      });

      // Intro animation

      transformLayout.random();
      utils.set(cards, { opacity: 0 });
      elementsLayout.update(() => transformLayout.table(), {
        delay: stagger([0, 750], { from: "random" }),
      });
    </script>
  </body>
</html>