デザイン通りにコーディングできるようになってきてプログラムにもそこそこ強くなったけど、Awwwardsなどに掲載されているWebサイトを見ているとどうやって作られているかわからない…そんな経験はありませんか?
今回はモダンなブラウザで良く見るWebサイトを少し豊かにする知識と技術を簡単にまとめました。
これらの技術はデザインに沿ってコーディングをする以上のものになります。
実務では工数が掛かるためあまり行われず、知っていても使う機会は少ないと思いますが、それでも抑えておくと自身のWeb制作への理解が深まると思いますので、興味のある方はご一読下さい。
それではいきます。
目次
はじめに
制作にあたり、複数のプラグインやライブラリを使用しています。
Webサイトで使われるプラグイン(ライブラリ)の多くが海外製となっており、最近ではIEを非対応としたプラグインもあります。
特に最近の制作環境の主流であるnpmやyarnを使用する場合、意図してIE対応の必要なケースも多いので取扱いには気を付けて下さい。
今回もIEで不具合が起きていましたが、簡単に解消出来そうもなかったのでIE非対応で制作しています。
スマホは対応していますがグラフィックが汚いので、こちらも実務で使うためには要調整の必要があります。
今回のデモページはこちら。
様々な技術を1つのWebサイトにぎゅっと濃縮させました、重いです。
周期的に上からボールが降ってきて記事の画像に当たるとバウンドするサイトになります。
1プログレスバー / Progress bar
進捗バーとも言います、ローディングなどに良く用いられる表現です。
良いサイトを作っても、読み込みが完了するまでサイトが崩れた状態だと少し残念な気持ちになります。
また、回転するシンプルなローダーを置いたとしていつまで経っても終わらない…なんて事になったらブラウザバックもしたくなりますね。
プログレスバーはそんなローディングの状態を可視化することで、ユーザーにコンテンツが表示されるまでの残り時間を伝えやすくします。
しかし、一般的にフロント側のみでローディングの残り時間を判断するのは難しいです。
ファイルのダウンロードを想像するとイメージしやすいと思いますが、ユーザーの通信速度は必ずしも一定ではないため必ず何秒で終わるか分からないという理由です。
そのため、実装は「画像の読み込み状況から計算」したり「予め時間を決める・最大の時間を決める」事が多いです。
下記の例は10枚の画像を読み込み、全ての読み込みが完了したら処理を終了する簡単なコードです。
ログをみると3回ループして処理が終了しました。
[JS]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | (() => { const imageList = document.querySelectorAll('img'); if(imageList === null) return; const maxLen = [...imageList].length; let completeLen = 0; const roop = () => { console.log(completeLen); completeLen <= maxLen ? requestAnimationFrame(roop) : console.log('finish!'); } roop(); [...imageList].forEach(x => { const image = new Image(); image.src = x.getAttribute('src'); image.onload = () => completeLen++; }); })(); > 0 > 2 > 10 > finish! |
画像の枚数や容量によっては短い時間で処理が終了してしまうため、サイトによってはプログレスバーをほぼ見ることがなくなります。
そのため、Nuxt.jsなどの一部のフレームワークでは「プログレスバーを表示する時間の最小値を設定する」事でプログレスバーを実装をしているようです。
基本は最小より時間が掛かった場合はすぐに処理を終了させ、最小より早く読み込みが完了した場合は進捗が「100%」で止まる、といったものになります。
参考: https://ja.nuxtjs.org/docs/2.x/features/loading/
今回はシンプルにローディング時間を決めてしまいました。
ユーザーの50%は読み込みに3秒掛かると「長い」と感じてしまうそうですが、今回はゆったりと非表示になるまで約5秒に決めています。
また、円形のプログレスバーにしたかったのでSVGのcircleを使用してストロークアニメーションで実装しました。
[_progress.js]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | const _remove = ($svg) => { // 処理完了後に非表示 gsap.to($svg, { translateX: '-100%', ease: 'power4.inOut', duration: 1.4, }); gsap.to($svg.children[0], { translateX: '100%', ease: 'power4.inOut', duration: 1.4, onComplete: () => { gsap.set($svg, { display: 'none' }); } }); } export default function($svg, $circle, $num) { const l = $circle.getTotalLength(); gsap.set($svg.children[0], { opacity: 1 }); gsap.set($circle, { strokeDasharray: l, strokeDashoffset: l }); const tween = gsap.to($circle, { strokeDashoffset: 0, ease: 'power4.inOut', delay: 0.3, duration: 3, onUpdate: () => { const num = Math.round(tween.progress() * 100); $num.textContent = num; }, onComplete: () => _remove($svg) }); } |
[app.js]
1 2 3 4 5 6 7 | import Progress from './_progress' Progress( document.querySelector('.js-progress'), document.querySelector('.js-progress__circle'), document.querySelector('.js-progress__number') ); |
所感
読み込みの演出は「ユーザーを退屈させない」「読み込み中の崩れたコンテンツを見せない」など大切な役割を持っていていて魅力的ですが、見せたいのはその先にあるコンテンツなので、処理の軽い箇所はシンプルな「ローダー」、SPAの初回読み込みやサーバーとの通信に時間が掛かる箇所は「プログレスバー」などで使い分けすると良いのかなと思います。
2慣性スクロールとパララックス / Inertia scroll & Parallax
慣性スクロール
次にモダンな制作で見られる表現の一つに慣性スクロールがあります。
スクロールが滑らかになってオシャレに演出できる便利な表現です。
実装は自前で書いたりプラグインを使うケースが多いです。
自前で作る場合はスクロール領域をfixedにして浮かせ、ダミーの要素に高さを指定します。
スクロールした際に領域をゆったりとtransformなどで移動する、という方法が作りやすいかと思います。
Scroll Managerが上述の作りになっていますが、こういったアセットを使うと細かい調整も出来るのでおすすめです。
プラグインだと有名なものでluxy.jsやLocomotive Scrollがあります。
どちらも慣性スクロールを実装するだけなら充分に要件を満たしますが、Locomotive Scrollの方が多機能な印象です。
今回はLocomotive Scrollを使用しましたが、なんとたった数行で実装できてしまい驚きました。
後述するパララックスと組み合わせてLPなどで使用すると、一気にサイトの雰囲気が変わるのではないでしょうか。
[HTML]
1 2 3 | <div id="l-wrapper" data-scroll-container> /* 省略 */ </div> |
[JS]
1 2 3 4 | const scroll = new LocomotiveScroll({ el: document.querySelector('[data-scroll-container]'), smooth: true, }); |
注意点として、transformでの移動になる場合、追従するヘッダーなどはLocomotive Scrollを適用する外に移した方が不具合が少ないかと思います。
パララックス
パララックスは訳すと「視差効果」という意味になります。
スクロールの位置に応じて要素を緩急付けて動かすことで、奥行を感じさせる技法になります。
慣性スクロールと組み合わせるとより印象深くなるので、少しリッチな表現をしたい場合に活用すると良いです。
今回は最初の画像にパララックスを実装しています。
こちらもLocomotive Scrollを使用していますので、クラスを追加するだけで実装できてしまいます。
他にもこちらのプラグインでできることは多いので、興味のあるかたは試してみてはいかがでしょうか。
[HTML]
1 2 3 | <div class="about__img"> <span data-scroll data-scroll-speed="1"></span> </div> |
3マウスストーカー / Mouse stalker
カーソルに追従する要素の事を「マウスストーカー」と呼びます。
2000年頃のWebサイトでも多く見られたイメージですが、Javascriptにはまだできる事が少なくただサイトを重くする事が多かったため敬遠されていたようです。
モダンなサイトで再び見られ始めましたが、従来のような賑やかしではなくリンクのホバーエフェクトだったり、ユーザーをアシストする形で実装されている、ちょっとしたデザインで見かけることが多くなりました。
デフォルトのカーソルを非表示にして独自のカーソルでサイトを演出したり、マウスストーカーそのものをインタラクションデザインの一部として利用することも多いです。
マウスストーカーもdivなどの要素で作るものや、canvasやSVGで作るものまで幅広くあり多彩なデザインが存在します。
下記はデモページに使用したコードで、HTMLは空のspanのみを置いてcssでfixedにしています。
JSでspan要素をマウスの位置に合わせ、記事にカーソルを合わせるとクラスが付いて表示される、という実装です。
※今回慣性スクロールを使用している影響でe.pageYの値が正常に取得出来ていませんが、通常はwindow.pageYOffsetの値も使用する事が多いです。
[HTML]
1 | <span class="js-circle circle pc"></span> |
[JS]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | class El { constructor(el, modules) { this.el = el.parentNode.parentNode; this.modules = modules; this.el.addEventListener('mouseenter', () => this._mouseenter()); this.el.addEventListener('mouseleave', () => this._mouseleave()); } _mouseenter() { this.modules._enter(); } _mouseleave() { this.modules._leave(); } } export default class { // imageList → nodeList constructor(imageList) { this._page = { x: 0, y: 0 }; this.page = { x: 0, y: 0 }; this.circle = document.querySelector('.js-circle'); this.imageList = [...imageList].map(x => new El(x, this)); window.addEventListener('mousemove', (e) => this._mousemove(e)); requestAnimationFrame(this._render.bind(this)); } _mousemove(e) { // カーソルの位置を格納 this._page.x = e.pageX; this._page.y = e.pageY; } _enter() { this.circle.classList.add('is-active'); } _leave() { this.circle.classList.remove('is-active'); } _render() { // レンダリング this.page.x += (this._page.x - this.page.x) * 0.2; this.page.y += (this._page.y - this.page.y) * 0.2; this.circle.style.left = `${this.page.x}px`; this.circle.style.top = `${this.page.y}px`; requestAnimationFrame(this._render.bind(this)); } } |
所感
注意点として、レスポンシブデザインが主流の昨今ではスマホで見た時の仕様も考えなければいけないため、マウスストーカーがあるからと言って明らかにリンクかどうかわからないデザインを作ったり、マウスストーカーに重きを置いたデザインは避けるべきかと思いました。
4WebGL / WebGL
最後はWebGLの紹介です。
個人的にWeb制作の中で最も奥が深く難しいと思う技術で、これを抑えればWeb制作の表現力が飛躍的に身に付くのではないかと思います。
WebGLは「インタラクティブな3Dグラフィックスや2DグラフィックスをレンダリングするためのJavaScriptAPI」でcanvas要素を使用します。
引用元: WebGL: ウェブの 2D および 3D グラフィック – Web API | MDN
こちらは1から素のJSで書くことも出来ますが、ライブラリを使用すると面倒な記述を減らせます。
今回は2Dグラフィックスを扱いたかったので、2Dに強いPixiJSを使用。
(他にはp5.jsや3Dに強いThree.jsがありますが、強みが違うだけで基本的にどれでも同じことは可能です。)
今回実装するにあたり「Webページに物理演算を導入して上からボールを降らせたら面白いのでは…」という発想から着手しました。
そのため、まずはWebページに物理エンジンを導入したかったので、Matter.jsを使用して6秒ごとにボールを振らせます。
[JS]
1 2 3 4 5 6 7 8 9 10 11 12 13 | const _create = () => { const x = resolution.x * Math.random(); const circle = Bodies.circle(x, -100, Common.random(10, 25), { restitution: 1, friction: 0.1, render: { fillStyle: '#2a2a2a' } }); World.add(world, circle); } // 6秒ごとにボールを落とす setInterval(() => _create(), 6000); |
次に、ただボールを落とすだけではやはり物足りなかったので、PixiJSを使用して画像をメッシュに読み込み、各頂点にMatter.jsのsoftBodyの座標を当てる、といった実装をしています。
そうすると、softBodyにボールが当たり、頂点が更新され画像が同じように歪みます。
デバック用にカーソルで歪ませる例。
重力が下に向いていてこのままだと画像が落下するので、落下防止に左上と右上にアンカーを打ち込んで固定します。
[_pixi.js]
1 2 3 4 5 | this.imageList = [...imageList].map(el => { const mesh = new PIXI.SimplePlane(PIXI.Texture.from(el.src), 4, 3); this.app.stage.addChild(mesh); return mesh; }); |
[_matter.js]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | Events.on(engine, 'afterUpdate', () => { this.imageList.forEach((x, i) => { const target = this.pixiView.imageList[i]; this._render(x, target); }) }); /* 省略 */ _render(o, mesh) { if(mesh.vertexData.length < 1) return; const bodies = o.composites.bodies; const magnification = o.magnification; const property = o.property; bodies.forEach((vertex, i) => { i *= 2; const point = vertex.position; const x = point.x - (property.x + property.r); const y = point.y - (property.y + property.r); // PIXI.SimplePlaneの頂点座標を更新 mesh.vertexData[i] = x * magnification.x + property.x; mesh.vertexData[i + 1] = y * magnification.y + property.y; }); } |
以上でこちらの処理は完了です。
所感
今回はデモですので細かな調整は省きましたが、実際はsetIntervalで処理するとブラウザのタブ切り替えなどでスリープ中にも処理が蓄積されてしまいます。
また、「見えなくなったボールは消す」「リサイズした際の位置調整(今回はリロードで済ませました)」も必要になってくるかと思いますので、参考にする場合は気を付けて下さい。
参考
株式会社ココノヱ – coconoe inc.
https://9ye.jp/
昔からインスピレーションを頂いている岡山県にある制作会社のWebサイト。
愉快なキャラが落ちる跳ねるでインパクトがあって面白いです。
Rand
https://rand-d.com/
名古屋県にある、デザインやブランディングを行う会社のWebサイト。
マウスストーカーと慣性スクロールが柔らかい印象を与え、canvasを効果的に使用した波のような雰囲気が綺麗です。
エッグフォワード株式会社
https://eggforward.co.jp/about/
東京にあるコンサルティング会社のWebサイト。
ページ下部のスライダーにワンポイントに使用されたマウスストーカーはサイトのユーザビリティを損なわず、
また、背景はcanvasで描画されており微妙な動きがサイトの雰囲気を高めています。
最後に
今回の制作に使用したファイル一式をダウンロード出来ますので、ぜひ遊んでみて下さい。
作ってみたら想像より処理が重くなってしまった事や、リサイズ時の位置調整が大変で断念したことなど…
色々不備はありますが記事を読んでいる方の学習の参考になれば幸いです。
それでは。