JavaScriptでMac OSXのDockのように拡大縮小するアイコン

2019-03-25

題名のとおりなんですけど、マウスホバーでアイコンが拡大表示される、Mac OSXのDockのような動きをするメニューをJavaScriptで書きました。
例によってライブラリとかjQueryを使用していないので、掲載しているコードだけで動きます。
2016/6/13追記:スムーズさを出すためにフレームレートを変更したのと若干のコード変更を行いました。
2019/3/25追記:SVGアイコンをutf-8の生コードからbase64エンコードデータに変更しました。

このDockメニューはActionScript(Flash)でやってから何度も書いているのですが、HTML5やCSS3、SVGのおかげでだいぶ簡単に実現することができるようになりました。

デモ

下のSNS系のアイコンの上にマウスを乗っけてみてください。ちゃんとそれっぽく動くでしょう?!

Mac OSXが出てから15年以上にわたって変わらないGUIとして見た場合のDockは、やはり優れたGUIなんでしょうね。それをWebで実現したからといって使いどころは難しいですけど。。

DockのアイコンをSVGで描いていて、cssの::before擬似要素のcontent属性に設定しています。以前は、utf-8の生のSVGでcss内に記述しても問題ありませんでしたが、いつからか表示されなくなってましたので、修正してbase64エンコードしたSVGに変更しました。

クリックしても単なるダミーのURLを貼っているだけなので何も起きません。

これだけのことを実行するのに、JavaScriptの部分だけなら、Minify + gzipでたったの600バイト程度です。HTMLコードやCSS、8個のSVGアイコンも含めても3,500バイト程度ですよ。

小さいコードで書くことは、ダウンロード時間を短くするだけじゃなくて、今回のコードのように50/s程度のフレームレートで多数の計算やDOM変更を伴う場合でもスムーズに動く要因ともなります。

それでは、上記のデモを実現するコードを紹介します。

コード(HTML + CSS + JavaScript + アイコンSVG)

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="utf-8">
   <title>Dock</title>
   <style>
   #sns {
      position: relative;
      margin: 0 auto;
      display: flex;
   }

   /* for touch devices and javascript off user */
   #sns li {
      flex: 1 1 auto;
      list-style: none;
   }
   #sns a {
      display: block;
      text-align: center;
   }
   #sns a::before {
      display: block;
      margin: 0 auto;
   }

   .dock li {
      display: block;
      position: absolute;
      bottom: 0;
      transform-origin: 50% 100%;
   }
   .dock li.hide {
      opacity: 0;
   }
   .dock a {
      display: block;
      width: 30px;
      height: 30px;
      padding: 4px;
      margin: 0 1px;
      border-radius: 4px;
   }
   .dock span {
      display: none;
   }
   .dock li:hover span {
      position: absolute;
      display: block;
      margin: 0 0 3px;
      bottom: 100%;
      left: 50%;
      transform: translateX(-50%);
      white-space: nowrap;
      text-align: center;
      font-size: 10px;
      border-radius: 1em;
      padding: .1em 1em;
      color: #fff;
      background: #666;
   }
   .dock li:hover span::before {
      content:'';
      position:absolute;
      display:block;
      width:0;
      bottom:-3px;
      left:50%;
      margin-left: -4px;
      border-style:solid;
      border-color:#666 transparent;
      border-width: 4px 4px 0;
   }

   .facebook {
      background: #315096;
   }
   .facebook::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00xOCAzMmgtNlYxNkg4di01LjVsNC0uMDJWNy4yNEMxMiAyLjc0IDEzLjIgMCAxOC41IDBoNC40MnY1LjVoLTIuNzVDMTguMSA1LjUgMTggNi4zIDE4IDcuNzN2Mi43Nmg0Ljk1TDIyLjM4IDE2SDE4djE2eicvPjwvc3ZnPg==");
   }
   .twitter {
      background: #55acee;
   }
   .twitter::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00zMiA2LjA4Yy0xLjE4LjUyLTIuNDQuODctMy43NyAxLjAzIDEuMzUtLjggMi40LTIuMSAyLjktMy42Mi0xLjI4Ljc1LTIuNyAxLjMtNC4xOCAxLjYtMS4yLTEuMy0yLjktMi4wOC00LjgtMi4wOC0zLjYyIDAtNi41NiAyLjk0LTYuNTYgNi41NiAwIC41Mi4wNSAxLjAyLjE2IDEuNS01LjQ2LS4yNy0xMC4zLTIuOS0xMy41My02Ljg2LS41Ny45Ny0uOSAyLjEtLjkgMy4zIDAgMi4yOCAxLjE3IDQuMyAyLjkzIDUuNDctMS4wOC0uMDQtMi4xLS4zMy0yLjk3LS44My0uMDIuMDMtLjAyLjA2LS4wMi4xIDAgMy4xNyAyLjI3IDUuODIgNS4yNyA2LjQyLS41NS4xNS0xLjEzLjIzLTEuNzMuMjMtLjQyIDAtLjgzLS4wNS0xLjIzLS4xMi44MiAyLjYgMy4yNSA0LjUgNi4xMiA0LjU2LTIuMjUgMS43Ni01LjA4IDIuOC04LjE2IDIuOC0uNTMgMC0xLjA1LS4wMy0xLjU2LS4xQzIuOSAyNy45MyA2LjM1IDI5IDEwLjA2IDI5YzEyLjA4IDAgMTguNjgtMTAgMTguNjgtMTguNjggMC0uMjggMC0uNTYtLjAyLS44NSAxLjMtLjkyIDIuNC0yLjA4IDMuMjgtMy40eicvPjwvc3ZnPg==");
   }
   .hateb {
      background: #008fde;
   }
   .hateb::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzMiAzMic+PHBhdGggZmlsbD0nI2ZmZicgZD0nTTIzLjg3IDI0LjM1YzAgMS43NCAxLjQgMy4xNSAzLjE1IDMuMTVzMy4xNi0xLjQgMy4xNi0zLjE1YzAtMS43NC0xLjQyLTMuMTYtMy4xNi0zLjE2cy0zLjE1IDEuNC0zLjE1IDMuMTV6bS4yOC0yMC42Mmg1Ljc1djE1LjdoLTUuNzV6TTE1LjQgMTQuM3MzLjk4LS4yNiAzLjk4LTUuMDJjMC01LjUtNC45OC01LjU0LTcuODItNS41NEgxLjgydjIzLjc4aDkuNjVjNy44NiAwIDkuMi00LjMgOS4yLTcuMDVzLTEuMzQtNS4zNC01LjI2LTYuMTh6TTcuODggOC4yaDIuNjdjLjUgMCAyLjY3LjIgMi42NyAyLjI4IDAgMi40NC0xLjg4IDIuMzYtMy4xMyAyLjM2SDcuODdWOC4yMnpNMTEgMjIuNDZINy45di01LjIyaDMuMnMzLjIuMzggMy4yIDIuNi0xLjggMi42Mi0zLjI3IDIuNjJ6Jy8+PC9zdmc+");
   }
   .pocket {
      background: #ee4056;
   }
   .pocket::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00yNy41IDMuMTdINC4zM2MtMS41OCAwLTIuODYgMS4yNy0yLjg2IDIuODV2OC40NWMwIDcuOTggNi40NyAxNC40NSAxNC40NSAxNC40NSA3LjkgMCAxNC4zMi02LjM0IDE0LjQ2LTE0LjJ2LTguN2MwLTEuNTgtMS4yNy0yLjg1LTIuODUtMi44NXpNMjQgMTRsLTYuNDcgNi40N2MtLjQyLjQ0LTEuMDIuNjItMS42LjU1LS41Ni4wNy0xLjE1LS4xLTEuNi0uNTVMNy44NSAxNGMtLjc1LS43NS0uNzUtMS45OCAwLTIuNzRzMS45OC0uNzUgMi43NCAwbDUuMzMgNS4zNCA1LjM1LTUuMzRjLjc1LS43NSAxLjk4LS43NSAyLjczIDBzLjc2IDIgMCAyLjc0eicvPjwvc3ZnPg==");
   }
   .feedly {
      background: #6cc655;
   }
   .feedly::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00zMC41NiAxNi42N2wtMTMuMy0xMy4zYy0uNy0uNy0xLjgyLS43LTIuNTIgMGwtMTMuMyAxMy4zYy0uNy43LS43IDEuODIgMCAyLjUybDkuOTYgOS45NWg5LjJsOS45Ni05Ljk2Yy43LS43LjctMS44MyAwLTIuNTN6bS0yMi4xMyAyLjhsLTEuNDgtMS41Yy0uMS0uMS0uMS0uMjYgMC0uMzdsOC43OC04Ljc4Yy4xLS4xLjI3LS4xLjM3IDBsMS45OCAxLjk4Yy4xLjEuMS4yNyAwIC4zN2wtOC4zIDguM0g4LjQ0em05LjczIDUuNWwtMS40OCAxLjQ3aC0xLjM2bC0xLjQ4LTEuNDhjLS4xLS4xLS4xLS4yNyAwLS4zN2wxLjk3LTJjLjEyLS4xLjI4LS4xLjQgMGwxLjk2IDJjLjEuMS4xLjI2IDAgLjM2ek0xOC4yIDE4bC00Ljk0IDQuOTJIMTEuOWwtMS41LTEuNDhjLS4xLS4xLS4xLS4yNyAwLS4zN2w1LjQ0LTUuNDJjLjEtLjEuMjctLjEuMzcgMGwyIDEuOThjLjEuMS4xLjI3IDAgLjM3eicvPjwvc3ZnPg==");
   }
   .googlep {
      background: #dd4b39;
   }
   .googlep::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00xNy40NyAySDkuMUM1LjM0IDIgMS44IDQuODQgMS44IDguMTRjMCAzLjM2IDIuNTcgNi4wOCA2LjQgNi4wOC4yNiAwIC41IDAgLjc3LS4wMi0uMjUuNDctLjQzIDEtLjQzIDEuNTYgMCAuOTQuNSAxLjcgMS4xNCAyLjMtLjQ4IDAtLjk0LjAzLTEuNDUuMDNDMy41OCAxOC4xIDAgMjEuMDQgMCAyNC4xYzAgMy4wMiAzLjkyIDQuOTIgOC41NyA0LjkyIDUuMyAwIDguMjMtMyA4LjIzLTYuMDQgMC0yLjQyLS43LTMuODctMi45My01LjQ0LS43NS0uNTMtMi4yLTEuODQtMi4yLTIuNiAwLS45LjI2LTEuMzQgMS42LTIuNCAxLjQtMS4wOCAyLjM3LTIuNiAyLjM3LTQuMzcgMC0yLjEtLjk0LTQuMTctMi43LTQuODVoMi42NkwxNy40NyAyem0tMi45MiAyMC40OGMuMDYuMjguMS41Ny4xLjg3IDAgMi40NC0xLjU4IDQuMzUtNi4xIDQuMzUtMy4yIDAtNS41My0yLjA0LTUuNTMtNC40OCAwLTIuNCAyLjg4LTQuNCA2LjEtNC4zNS43NCAwIDEuNDQuMTMgMi4wOC4zMyAxLjc0IDEuMiAzIDEuOSAzLjM1IDMuMjh6bS01LjE1LTkuMWMtMi4xNi0uMDgtNC4yLTIuNDMtNC41OC01LjI2czEuMDctNSAzLjIzLTQuOTNjMi4xNi4wNSA0LjIgMi4zMiA0LjU4IDUuMTZzLTEuMDcgNS4wNy0zLjIzIDV6TTI2IDhWMmgtMnY2aC02djJoNnY2aDJ2LTZoNlY4eicvPjwvc3ZnPg==");
   }
   .line {
      background: #00c300;
   }
   .line::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00xNS45MyAxLjk1Yy03Ljk2IDAtMTQuNCA1LjI4LTE0LjQgMTEuOCAwIDUuODQgNS4yIDEwLjcgMTIuMDIgMTEuNjNoLjA4bC4yMy4wNGMuNzUuMSAxLjEuMjggMS4xIDEuMS0uMDIuOTItLjM4IDEuNi0uNiAyLjA0cy0uNjcgMi4yNSAxLjQgMS4xM2MxLjU4LS44OCA5LjM3LTQuNyAxMi44Ny0xMC4zNy45My0xLjQgMS41LTMgMS42Ny00LjY4di0uMTVsLjAzLS4yNXYtLjVjMC02LjUyLTYuNDQtMTEuOC0xNC40LTExLjh6TTkuOSAxNy4xNUg3LjEzYy0uNCAwLS43My0uMzMtLjczLS43M3YtNS41N2MwLS40LjMzLS43NC43NC0uNzRzLjczLjM0LjczLjc1djQuODNoMmMuNDIgMCAuNzUuMzMuNzUuNzNzLS4zMy43NS0uNzQuNzV6bTMuMTItLjczYzAgLjQtLjMzLjczLS43NC43M3MtLjczLS4zMy0uNzMtLjczdi01LjU3YzAtLjQuMzMtLjc0LjczLS43NHMuNzQuMzQuNzQuNzV2NS41NnptNi43LjVjMCAuMDItLjAyLjAzLS4wMy4wNGwtLjEuMDZjMCAuMDItLjAzLjAzLS4wNi4wNGwtLjA2LjAzYy0uMDIgMC0uMDUgMC0uMDguMDJoLS4wNWwtLjE1LjAzYy0uMDUgMC0uMSAwLS4xNC0uMDItLjAyIDAtLjA1IDAtLjA3LS4wMnMtLjA2IDAtLjA4IDBsLS4wNy0uMDVjLS4wMiAwLS4wNCAwLS4wNS0uMDItLjEtLjA3LS4xNy0uMTUtLjIzLS4yNEwxNS43MiAxM3YzLjRjMCAuNC0uMzMuNzMtLjczLjczcy0uNzQtLjMzLS43NC0uNzR2LTUuNTUtLjA0LS4xYy4wMi0uMDIuMDMtLjA1LjAzLS4wN3YtLjA2bC4wNi0uMXMwLS4wMi4wMi0uMDNjLjA1LS4wOC4xMi0uMTUuMi0uMi4wMiAwIC4wMy0uMDIuMDUtLjAzbC4wOC0uMDMuMDgtLjAyLjA3LS4wMi4xMy0uMDJIMTVjLjA1IDAgLjEuMDIuMTQuMDMuMDIgMCAuMDQgMCAuMDYuMDIuMDIgMCAuMDUgMCAuMDcuMDIuMDMgMCAuMDUuMDIuMDcuMDRsLjA2LjA0Yy4wNC4wMi4wOC4wNS4xLjEuMDUuMDMuMS4wOC4xMi4xM2wyLjg1IDMuNzd2LTMuNGMwLS40LjMzLS43My43My0uNzNzLjc0LjM0Ljc0Ljc1djUuNTZjMCAuMDYgMCAuMS0uMDIuMTZ2LjA1bC0uMDQuMWMwIC4wMi0uMDIuMDMtLjAzLjA1IDAgLjAzLS4wMi4wNS0uMDQuMDcgMCAuMDMtLjAzLjA2LS4wNS4wOGwtLjAzLjAzem00LjgyLTQuMDJjLjQgMCAuNzMuMzMuNzMuNzNzLS4zMy43My0uNzMuNzNoLTJ2MS4zMmgyYy40IDAgLjczLjMzLjczLjczcy0uMzMuNzUtLjczLjc1SDIxLjhjLS40MiAwLS43NC0uMzMtLjc0LS43NHYtNS41NWMwLS40LjMzLS43NC43My0uNzRoMi43NGMuNCAwIC43My4zNC43My43NXMtLjMzLjczLS43My43M2gtMnYxLjNoMnonLz48L3N2Zz4=");
   }
   .github {
      background: #333;
   }
   .github::before {
      content: url("data:image/svg+xml; base64, PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnICB2aWV3Qm94PScwIDAgMzIgMzInPjxwYXRoIGZpbGw9JyNmZmYnIGQ9J00xNiAwQzcuMTYgMCAwIDcuMTYgMCAxNnM3LjE2IDE2IDE2IDE2IDE2LTcuMTYgMTYtMTZTMjQuODQgMCAxNiAwem05LjUgMjUuNWMtMS4yMyAxLjI0LTIuNjcgMi4yLTQuMjcgMi44OC0uNC4xOC0uODIuMzMtMS4yNC40NnYtMi40YzAtMS4yNi0uNDQtMi4yLTEuMy0yLjc4LjU0LS4wNiAxLjAzLS4xMyAxLjUtLjIycy45Mi0uMjMgMS40Mi0uNC45Ni0uNCAxLjM2LS42NC44LS41NiAxLjE2LS45NS42OC0uODQuOTMtMS4zMy40NS0xLjEuNi0xLjc4LjItMS40Ni4yLTIuM2MwLTEuNi0uNS0yLjk4LTEuNTctNC4xMi40OC0xLjI1LjQzLTIuNi0uMTUtNC4wN2wtLjQtLjA1Yy0uMjYtLjAzLS43NS4wOC0xLjQ1LjM0cy0xLjUuNy0yLjM3IDEuMjhjLTEuMjQtLjM0LTIuNTMtLjUtMy44Ni0uNS0xLjM0IDAtMi42Mi4xNi0zLjg0LjUtLjU2LS4zNy0xLjA4LS42OC0xLjU3LS45M3MtLjktLjQyLTEuMi0uNS0uNTYtLjE1LS44Mi0uMTctLjQyLS4wMy0uNS0uMDJMOCA3Ljg1Yy0uNiAxLjQ4LS42NCAyLjg0LS4xNiA0LjA4LTEuMDYgMS4xNC0xLjU4IDIuNS0xLjU4IDQuMTMgMCAuODMuMDcgMS42LjIyIDIuM3MuMzQgMS4yNy42IDEuNzcuNTUuOTQuOTIgMS4zMy43Ni43IDEuMTYuOTUuODUuNDUgMS4zNi42My45OC4zIDEuNDMuNC45Ni4xNyAxLjUuMjNjLS44Ni41OC0xLjI4IDEuNS0xLjI4IDIuNzh2Mi40NGMtLjQ4LS4xNC0uOTQtLjMtMS40LS41LTEuNi0uNjctMy4wNC0xLjY0LTQuMjctMi44OHMtMi4yLTIuNjctMi44OC00LjI3Yy0uNy0xLjY2LTEuMDYtMy40Mi0xLjA2LTUuMjNzLjM2LTMuNTggMS4wNi01LjIzQzQuMyA5LjE3IDUuMjYgNy43MyA2LjUgNi41czIuNjctMi4yIDQuMjctMi44OGMxLjY2LS43IDMuNDItMS4wNiA1LjIzLTEuMDZzMy41OC4zNiA1LjIzIDEuMDZjMS42LjY3IDMuMDQgMS42NCA0LjI3IDIuODhzMi4yIDIuNjcgMi44OCA0LjI3Yy43IDEuNjUgMS4wNiAzLjQgMS4wNiA1LjIzcy0uMzYgMy41Ny0xLjA2IDUuMjNjLS42NyAxLjYtMS42NCAzLjA0LTIuODggNC4yN3onLz48L3N2Zz4=");
   }
   </style>
</head>

<body>
   <p id="dummy" style="height:300px;">DUMMY for space</p>
   <ul id="sns">
      <li>
         <a class="pocket" href="#">pocket</a>
      </li>
      <li>
         <a class="facebook" href="#">facebook</a>
      </li>
      <li>
         <a class="twitter" href="#">twitter</a>
      </li>
      <li>
         <a class="github" href="#">github</a>
      </li>
      <li>
         <a class="googlep" href="#">google+</a>
      </li>
      <li>
         <a class="line" href="#">line</a>
      </li>
      <li>
         <a class="hateb" href="#">hateb</a>
      </li>
      <li>
         <a class="feedly" href="#">feedly</a>
      </li>
   </ul>
   <script>

   // マウスホバーした時の拡大率
   var zoom = 4;

   (function(zoom) {
      'use strict'

      //
      if('ontouchstart' in window) return;
      var anime;
      var mouseX;

      var dock = document.getElementById('sns');
      dock.classList.add('dock');
      var icons = dock.children;
      var defaultWidth = icons[0].offsetWidth;
      var bound = defaultWidth * 3.14;
      dock.style.width = defaultWidth * (icons.length -1) +'px';
      dock.style.height = defaultWidth +'px';
      [].forEach.call(icons, function(icon, i) {
         var span = document.createElement('span');
         var anchor = icon.getElementsByTagName('a')[0];
         var text = anchor.textContent;
         span.textContent = text;
         icon.appendChild(span);
         anchor.textContent = '';

         // icon.style.transition = ...でもOK
         icon.setAttribute('style', 'transition:.3s;left:' + i * defaultWidth + 'px');
      });

      // 50/sのフレームレートで実行される、各アイコンの位置や大きさを決めている関数
      function scaling(x) {
         for(var i = icons.length; i--;) {
            var icon = icons[i];
            var distance = (i * defaultWidth + defaultWidth / 2) - x;
            if(-bound < distance && distance < bound) {
               var rad = distance / defaultWidth * .5;
               var currentScale = icon.getBoundingClientRect().width / icon.offsetWidth;
               var scale = currentScale + (1 + (zoom - 1) * Math.cos(rad) - currentScale)/5;
               var currentLeft = icon.offsetLeft;
               var left = currentLeft + (i * defaultWidth + 2 * (zoom - 1) * defaultWidth * Math.sin(rad) -currentLeft)/5;
               // icon.style.left .., icon.style.transform = ..じゃないのは、速度を
               // 優先したため
               icon.setAttribute('style', 'left:' + left + 'px;transform:scale(' + scale + ')');
            } else {
               var left;
               if(-bound < distance) {
                  left = i * defaultWidth + 2.05 * (zoom - 1) * defaultWidth;
               } else {
                  left = i * defaultWidth - 2.05 * (zoom - 1) * defaultWidth;
               }
               var currentLeft = icon.offsetLeft;
               var currentScale = icon.getBoundingClientRect().width / icon.offsetWidth;
               icon.setAttribute('style', 'left:' + (currentLeft + (left - currentLeft)/5) + 'px;transform:scale('+(currentScale + (1-currentScale)/5)+')');
            }
         }
      }

      dock.addEventListener('mousemove', function(e) {
         mouseX = e.clientX - dock.getBoundingClientRect().left;
      });

      // dockの上にマウスが入ったときに各アイコンの拡大処理をスタート
      dock.addEventListener('mouseenter', function(e) {
         // フレームレート 50/s で各アイコンを拡大/縮小する
         anime = setInterval(function() {
            scaling(mouseX);
         }, 20);

         // dock自体も拡大しないと、iconの隙間にマウスが入ったときに
         // 不意に縮小する
         dock.style.height = defaultWidth * zoom + 'px';
         dock.style.marginTop = -(zoom - 1) * defaultWidth + 'px';
      });

      // マウスがdockから離れたとき
      dock.addEventListener('mouseleave', function(e) {
         clearInterval(anime);
         dock.style.height = defaultWidth + 'px';
         dock.style.marginTop = 0;

         // 通常の状態に戻る縮小処理はJavaScriptじゃなくてcssのtransitionでやってる
         // jsでやると、全ての処理が終了するまでanime処理を終わらせられなくて、その間に
         // またmouseenterになったときの処理が面倒
         for(var i = icons.length; i--;) {
            // tansform:scaleは書かなくても、setAttributeでやっていると
            // transform属性自体削除されているのでscale(1)扱いになる
            icons[i].setAttribute('style', 'transition: .3s;left:' + i * defaultWidth + 'px');
         }
      });
   }(zoom));
   </script>
</body>
</html>

このコードをコピーしてHTMLファイルを作れば、簡単に同じものが再現できますので、ぜひやってみてください。

JavaScriptSVGCSS3HTML5