Chienomi

Mimir Yokohama トップページの解説

開発::web

まえがき

Mimir Yokohamaウェブサイトのトップページがリニューアルされた。

このリニューアルは、従来とは方向性が違い、ヴィジュアル的な要素が強い。 しかし、その実装は至って簡単なものだ。

この記事は初心者に適すると考えているが、内容的にはJavaScriptのテクニックなどを含み、JavaScriptが当然に読め、CSSと共にその特性を理解していることを前提としているので、どこまでを初心者と呼ぶかの問題があるものである。

ウェブサイトを制作する上で重要なことは、ウェブサイトの機能的目的を達成することである。

例えばChienomiは情報サイトであり、長文のテキストコンテンツであるから、目的のコンテンツを探しやすく、また読みやすいことが重要となる。 加えて、障害時のトラブルシューティングにも使われることから、制限された環境でも利用しやすい必要がある。

Mimir Yokohamaの場合は商用サイトであるから、プロモーションとしての効果が重要だ。 だが、現実にはMimir Yokohamaの場合、Chienomi同様にテキストコンテンツが読まれており、Chienomiよりも一般向けのコンテンツであるために、コンテンツ量は1/10程度であるにも関わらず、Chienomiに匹敵するアクセスとなっている。

その状況を鑑みて、Mimir Yokohamaはより初心者が読みやすい環境を作るように、リーディング向きのデザインに変更された。 しかしその結果、非常に地味なデザインになってしまい、商用サイトとしてのアピール力を損なってしまった。

このことを問題視し、トップページだけ別誂えで用意した。

そもそもの好みの話

もちろん、私は実際のウェブサイトとしてはアクセシビリティは最も優先されるべき事項だと思っているし、アクセスを阻害することは許されないと考えている。

ただ、そうしたグローバルなんちゃらをさいおいて、単純に好みだけを述べよというのであれば、スタイリッシュなウェブサイトが好みだ。 まだIEとNNが戦争をしていた頃も、JavaScriptアニメーションのサイトとか、Flashのサイトとか大好きだったし、私のウェブサイトもFlashで書かれていた黒歴史もある。

今まで私が見た中で一番好きなサイト……は、まぁ諸々の事情で自重するとして、スッとアニメーションする感じとか、デザイン的要素がしっかり入った「動的デザイン」みたいなの、すごく好きなのだ。 アニメーションともまた違う、そういうデザインはウェブでしか表現できない部分でもあるし。

もちろん、現実にはクソ重いとか、環境によってデザインが崩れるとか、色んな問題があって難しいのだが。

どうしても、現実に沿ってとにかく軽く、読みやすいサイトにしてしまっているが、それは本来の目的とはズレているし、好みからいってもせめてもうちょっとスタイリッシュ要素を入れたい。 でないと技術アピールにもならない。

モバイル用スライド

基本構造

ナビゲーションボタンのonclickイベントにリスナーが設定されており、これによって現要素を非表示にして、次要素を表示するというものだ。 簡単に言えば次のような感じである。

nextBtn.onclick = function() {
  imgs[index].style.display = "none"
  index++
  if (index >= imgs.length) {index = 0}
  imgs[index].style.display = "inline-block"
}

アニメーションなしならこれで良いのだが、アニメーションのためにはなにをどうアニメーションするのかということを与えなければならない。

Nextの場合、現画像は左へ消え、次画像は右から現われる。このためには

  • 現画像を左へ移動する
  • 現画像の透過度を0.0にする
  • 次画像は右に存在していなければならない
  • 次画像の透過度は0.0でなければならない
  • 次画像を右から移動する
  • 次画像の透過度を1.0にする

ということを満たさなければならない。

アニメーションのことを考えなければそう難しくないのだが、relativeな要素はその要素の前にある要素が消えてもrelativeな位置からは動かない。 だから、ファインダー内に常に画像を収めるためには、次画像が出現する前に現画像はdisaply: noneである必要がある。

実際のコードは次の通り

function show_next(cur) {
  if (visible_block) {
    visible_block.style.display = "none"
  }
  visible_block = undefined
  cur.style.display = "none"
  imgs[index].style.display = "inline-block"
  timer = setTimeout(function() {
    imgs[index].className = "visible"
    visible_block = imgs[index]
  }, 50)
}

// true motive_nxt means "show next one", otherwise it means "show previous one".
function ban_slide_btn(motive_nxt) {
  if (timer) { timer.clearTimeout }
  var cur = imgs[index]
  if (!cur.style.display) {
    cur.style.display = "inline-block"
  }
  if (motive_nxt) {
    cur.className = "hidden_left"
    index++
  } else {
    cur.className = "hidden_right"
    index--
  }
  if (index >= imgs.length) {
    index = 0
  } else if (index < 0) {
    index = imgs.length - 1
  }
  if (motive_nxt) {
    imgs[index].className = "hidden_right"
  } else {
    imgs[index].className = "hidden_left"
  }
  timer = setTimeout(function() {show_next(cur)}, 140)
}

boxの位置と透過度はCSS classで管理されている。 これについては単にclassを切り替えれば良い。

lefttopといったプロパティが設定されていたとしても、ボックスの配置はボックスが存在する場合にのみ行われる。 つまり、position: relative;left: 50px;display: none;のボックスは、情報として右に50pxズレた要素であるということを持ってはいるが、この時点で右に50pxズレて配置されているわけではない。 あくまで、displayされたときに配置されるべき位置から右に50pxズレるのである。

この動作においてはボックスの左右方向はどっち向きにも操作できるため、事前に左右どちらかに配置することはできない。stateとしては

  1. 表示
  2. 非表示左
  3. 非表示右

の3つがあるのだが、displayの問題を別にすれば、nextなら

  1. 現画像を非表示左へ
  2. 次画像を非表示右へ
  3. 次画像を表示へ

という3ステップが必要になる。

ではこれにdisplayを含めればいいかというとそうはいかない。 displayはアニメーションしないし、そもそもdisplay: noneになった瞬間に要素は配置から消えてしまう。

アニメーションしない場合は気にならないのだが、アニメーションする場合はdisplay: noneにするタイミングをアニメーションに合わせて遅延させなければならない。

awaitを使えばもっと単純になりそうだな、と思ったのだが、結局stateが一様ではなくなるため、ボタンを連打されるとおかしなことになる。だから、同期的に状態を保証する必要があった。

また、animationendイベントをつかまえる方法もあるが、これも連打されたときに困るので採用しなかった。

実は、ボタン連打時に連打操作を無効にするという挙動を採用するならば、awaitanimationendを使ったモダンな設計にすることができるのだが、「連打aheadできたほうが快適だよね」という判断でこういう形になった。 (自身のイベントIDをクローズして現行イベントIDが更新されていれば中止する、という方法をとればanimationend+await方式で連打もできるが、それがわかりやすいとは言い難い。)

実際にやっていることとしては、まず現要素を非表示クラスに変更し、現要素を保存した上でindexを更新する(オーバーフローのリセットも行う)。 そして次要素を左右適切な非表示クラスに変更する(元のクラスと同一である可能性もある)。 そしてsetTimeoutでアニメーション時間がすぎるのを待つ。アニメーションが遅延してしまった場合については、打ち切りで要素が消えてしまうのは仕方ないと考える。

アニメーション時間を待ったら、現要素は配置から消去する。 これで次要素を配置してもOKな状態になった。しかし、即座に次要素を表示クラスにして表示させると、配置にかかる時間のためにアニメーションの途中で現われる感じになる。 そのため、表示クラスへの変更は、要素を配置してからさらに遅延させる必要がある。そこで配置したあとさらにsetTimeoutを呼んで遅延させてから、表示クラスへと変更する。

これらはtimerというタイマー変数に格納する。 タイマー変数がある状態というのは未実行の更新処理が残っているということである。 なので、表示更新が完了していないのにさらにクリックコールバックが呼ばれた場合はclearTimeoutでキャンセルする。

だが、単にそれだけでは表示された要素が残ってしまう可能性がある。 表示更新において重要なのは、「ひとつの要素が表示状態にある」ことではなく、「全ての要素が配置されていない状態で配置する」ことである。 そのため、実際に配置したときにvisible_blockという変数に配置した要素を格納しておき、配置を行う(elem.style.display = "inline-block"を行う)関数内でそれを行うより先にvisible_block.style.display = "none"とすることで配置されているブロックがないことを保証する。

これがうまくいくことを理解するためには、JavaScriptは原則としてシングルスレッドで動作する、ということを理解しておく必要がある。その上で、コールバックが起動されたときの状態について考えれば良い。

flexじゃなくてtable

flexのほうが向いてそうなものだが、スライドはtableになっている。

#SlideTopBan {
  display: table;
  margin: auto;
  max-width: 90%;
}

#SlideTopBan div {
  display: table-cell;
  vertical-align: middle;
}

やってみるとわかるのだが、flexにすると幅をフルに使おうとするのでセンタリングが効かない。 さらに、flexコンテンツのvertical-alignも効かない。

それに、テーブル全体の最大幅が決まっていると、縮小されたときに「縮小する余地のあるセルから縮める」という挙動になる。これはflexの挙動とおよそ等しい。 そして、ボタンのほうは「コンテンツが1文字しかない」ので縮小する余地がなく、常に画像が縮小される。

だから、flexよりもtableのほうが望んだ挙動になる。

ナビゲーションはテキスト

ナビゲーションボタンは純粋にを使った文字になっている。 これはちゃんと意味がある。

第一に軽い。

そして、1つの文字になっているため、table-cellであるときに圧縮されたり、改行されたりといった問題を生じない。

もちろん、操作しやすいようボックス全体のクリックイベントを設定する。

CSSアニメーション

#TopMainBanner img.visible {
  opacity: 1.0;
  left: 0px;
}

の状態から

#TopMainBanner img.hidden_left {
  opacity: 0.0;
  left: -100px;
}

の状態へとクラスを変更したとして、アニメーションがなければ一瞬で切り替わる。 同要素には

#TopMainBanner img {
  transition: all 120ms 0s ease;
}

と設定されており、これによって状態の変遷がアニメーションになる。

transitionといえばIE8まで搭載されていたIE固有のページアニメーションを思い浮かべるおっさんもいるかもしれないが、これがモダンなtransitionである。IEの(metaタグに書く)transitionはなかったことにされた。

transitionborderbackgroundなどと同じく一括指定プロパティである。 このような形での指定はサンプルでよく出てくるので使われることが多いだろう。 transition-timing-functionとしてはeaseは使いやすい。linearよりもむしろ自然で、多くの場合適するだろう。

PC用画像

初期版は「超レトロ」

初期版ではアニメーションがなく、画像がならんだバナーに見出しとボックスが並ぶ形式だった。 これはもう、ものすごくレトロだ。レトロに見えないようにしてるけれど、ぶっちゃけフレームセットが流行るよりも前の形式に近い。

<HTML>
  <BODY>
    <CENTER>
      <IMG src="1.jpeg">
      <IMG src="2.jpeg">
      <IMG src="3.jpeg">
      <IMG src="4.jpeg">
      <IMG src="5.jpeg">
      <BR>
      <TABLE>
        <TR><TD>見出し</TD></TR>
        <TR><TD><TEXTAREA>内容</TEXTAREA></TD></TR>
      </TABLE>
    </CENTER>
  </BODY>
</HTML>

涙で前が見えないというやつである。

ちなみに、center要素はNetscape Navigatorが実装した独自タグで、HTML3で登場。HTML4では非推奨となってtransitionalに残された。 HTML4ではスタイルシートの利用が推奨されていたが、現実としてはHTML4 transitionalで書くほうが一般的であった。 だから、せいぜい2001年くらいまでの書き方である。

だが、基本的に見栄えとしてはそれと大差ないので、さすがにこれはこれで地味すぎる、と思い、アニメーションを追加した。

CSSアニメーション

各要素はならんだ状態で配置されるべきであるため、スライドのときのように配置しないようにはしない。 ただし、visibility: hidden;している。このため、要素は配置はされているが、存在はしていない状態になる。 これは、「ズレた状態で停止してしまうことがないように」という配慮だが、実際あまりいらないかもしれない。

アニメーションはopacity, left, topの変化によるものであり、最初はそれぞれズレて透明な状態で配置されている。 この違いは親要素のクラスによって該当するセレクタが変化するようにしており、単純に親要素のクラスを変更すれば状態が変化する。

このままだと画面幅による縮小についても同じようにゆっくりとしたアニメーションが適用されてしまうため、個別に指定している。

#WideTopBan img { 
  position: relative;
  transition-property: top, left, opacity, height;
  transition-duration: 1240ms, 1240ms, 1080ms, 240ms;
  transition-timing-function: ease-out, ease-out, ease-out, ease;
}

画像サイズはheightによってコントロールされている。そのため、height変化は240msと他と比べて早いアニメーションにしている。

transitionの値はリストであり、リストの長さが足りないものについては循環する。 opacityは位置よりも少し短くアニメーションを終えるようになっている。つまり、フェードインのほうがスライドインよりも少し早い。

画像の読み込みタイミングとアニメーション

最初は無策でスクリプトが読み込み次第いきなり親要素のクラスを変えていたのだが、本番だと画像読み込みが間に合わず、途中から現れたり、途中で位置が変わったりする問題が起きた。

そこで、「順番的に最後の画像が読まれてればだいたい大丈夫だろう」と考え、最後のimg要素のonload属性に設定したのだが、やはり手抜きはやめようと思ってちゃんと直した。

現在の仕様では全てのimg要素がonload="imgLoaded()"という属性を持っている。 このimgLoadedは少し工夫されている。

var imgLoaded
(function() {
  var nLoaded = 0
  imgLoaded = function() {
    nLoaded++
    if (nLoaded == imgs.length) { document.getElementById("WideTopBan").className = "wideban_visible" }
  }
})()

全ての画像が読まれたときに親要素のクラス変更を実施している。 imgLoadedが呼ばれた時点でカウントアップし、これが要素数と等しくなれば全ての画像が読まれたことを意味する。

それは別に難しい話ではないと思うのだが、これをするためには関数の外側のスコープでカウンター変数が定義されていなければならない。だから、

var nLoaded = 0
  function imgLoaded() {
  nLoaded++
  if (nLoaded == imgs.length) { document.getElementById("WideTopBan").className =  "wideban_visible" }
}

となるのだが、こうするとnLoadedもグローバルなスコープになってしまう。

別にここまでのコードは、このページが別誂えで、このページからのみ、このスクリプトのみを読む、という前提になっているので、グローバルエクスポートしたって構わないのだが、nLoadedを匿名関数スコープにして、imgLoadedをクロージャとして使う、という方法を採用した。 今回の場合、実用的意味はほとんどない。

ブロックの高さを確定

各画像は338x600pxである。 なので、「338 x 5 + α」なサイズを満たさない場合、heightを縮小することで画像全体を縮小するというレスポンシブ方式をとっている。 スクロールバーやUI要素により、スクリーンサイズに対して実際のビューポートサイズが小さいということは普通にあるので、少し余裕をもたせている。

だが、単にこれだけだと、画像を描画するまではボックスの高さを確定できないため、後続要素が画像読み込みによって下に下がる、という問題が発生する。これは結構ストレスフルだ。 また、アニメーションの具合によって、縮小が間に合わず、画像が折り返されてしまうこともある。

なので画像だけでなく、ボックス全体の高さも合わせて固定する。

@media screen and (min-width: 1260px) and (max-width: 1424px) {
  #WideTopBan, #WideTopBan img {
    height: 420px;
  }
}

ページそのもの

ページそのものはeRubyで書かれており、従来は手動でマージしていた最新記事のインフォメーションなども自動で取り込む形になった。 (従来も自動取り込みができなかったわけではない)

更新スクリプトにeRubyトップページの生成が含まれているため、トップページの更新について意識する必要は特にない。

画像自体

画像はそもそもできるだけ軽くしたいためになるべく入れないようにしており、デザインのために結構な数の画像を入れることになってしまうことに抵抗を感じていた。

この画像は読み込みが遅くなるとエクスペリエンスを悪くしてしまうので、極力軽くするように、画像はJPEGにした上でさらに最適化を施したものにしている。 このため、トップページはfaviconを含む全リソースを読んでも150kBほどに抑えられている。

実はDilloでも大丈夫

メニューが表示されない方はこちら

という項目でフォローする形でアクセシビリティを確保しており、トップページは利用できないケースも想定しているのだが、実はDilloでも割と問題なく表示されたりする。

Dilloでの表示

もちろん、画像が折り返されるとか、両方のUIが出てしまうといった問題はあるが、Dilloが何を反映するかということから問題が起きないようになっており、案外問題ない。

Altも書いてあるのでw3mでも実用上問題なかったりするし

w3mでの表示

音声ブラウザでも多分問題ない。

サービスページの画像の裏話

トップページと同じ形式の画像が、サービス案内のページにも入ったが、実はこれは事故である。

というのも、このデザインを構想した段階で試しにアートワークを作り始めたのだが、なぜか間違ってトップページのメニューではなくサービスメニューのバナーを作ってしまったのだ。

間違って作ってしまったバナー

出来てから気づいたので、もったいないから再利用したわけである。