こんにちは、「ふ」です。
先日、読者様からこんなリクエストをいただきました(ありがとうございます)。
「背景画像を固定したまま、ページ全体をスクロールさせるには?」
具体的には⬇︎のようなものです。
スクロールする<body>の一部分に窓を開け、そこだけ背景画像を見せる、というものです。
スクロールさせると<body>自体は流れていきますが、背景画像は固定されたままの状態です。
早速作ってみましたので、その手順を紹介していきます。
皆さんも「こんなものを実装してみたい」というものがありましたら、twitterやお問合せフォームからリクエストしてみてください。
<body>要素に窓を開けるには、SVGのマスク機能を利用します。
SVGにおいてマスクとなる要素は、マスクされる側の要素に対して自身の不透明度を移植します。ここでいう「不透明度」とは、rgbを掛け合わして算出される値で、CSSのopacityやalpha値とは別のものです。
⬆︎は白正方形の真ん中に小さい黒正方形を配置しています。この要素をマスクとして使用した場合、白の部分は不透明度1、黒の部分は不透明度0がマスクされる側の要素に移植されます。
たとえば⬆︎のベージュ色の長方形。先程のマスク要素を適用すると。白の部分は不透明度1でそのままの状態、黒の部分は不透明度0、つまり透過されます。
ではもし、マスクされる長方形がhtmlの<body>領域いっぱいに広がっていたとしたら。
マスクをかけると、黒の部分だけが透過され、見た目は「<body>に窓が空いている」状態になります。
この「bodyいっぱいの長方形に掛かったSVGのマスク」に対して、前面に本文などのコンテンツ、背面に固定された(スクロールしない)画像を配置して「3層構造」とすれば、より「窓が空いている感」を盛り上げることになります。
それでは先ほどの「3層構造」となる要素を実際に配置していきましょう。
・窓の外の景色となる画像(スクロールさせない)
・窓を開ける壁←<body>いっぱいのSVG領域
・本文などのコンテンツ
〜の3つです。景色→壁→コンテンツの順に、背面から重ねていきます。
▪️fu.svg
はじめに、「窓の外の景色」とする画像⬆︎から。
<body> <img id = "「「5kesiki」」" src = "fu.svg"> </body>
body { position:relative; } 「「5#kesiki」」 { position:fixed; top:0; z-index:-2; }
景色画像のidを「kesiki」とし、最背面に固定します。
ここはスクロールさせないので、「position:fixed」にします。fixedを有効にさせるために親要素である<body>を「position:relative」に指定。
3層構造の再背面にするため、z-indexを「-2」としました。
現在の状態⬆︎です。
2層目の「壁」である、<body>いっぱいのSVG領域を置きましょう。
<body> <img id = "kesiki" src = "fu.svg"> <svg id = "「「5kabe」」" width = "100%" height = "100%"> <rect width = "100%" height = "100%" 「「1fill = "beige"」」/> </svg> </body>
<body>直下にSVG領域を設けました。widthとheightを「100%」に指定しておくことで、body自体の高さや幅が変化してもそれに合わせてSVG領域が広がってくれます。
その中にこれまた領域いっぱいのrect(長方形)を配置。実際にはこの長方形がコンテンツの背景部分となります。fill(塗りつぶしの色)属性で、好みの背景色を付けておきましょう。
#kabe { position:absolute; top:0; z-index:-1; 「「3/*safari対策*/」」 transform:translateZ(-1px); }
壁SVGもコンテンツの背後に回り込ませるため、「position:absolute」として絶対配置にしています。
3層構造の2層目となるので、z-indexを「-1」にします。
ここで厄介なのが、safariにおいて「z-index:-1」の指定だけでは背後に回り込ませることができません。
そのためtranslateを使い、壁SVGをz方向に1pxぶん後退させました。
景色を配置したときの「z-index:-2」は正常に動いたのに。
う〜む、今後もしばらくsafariくんには手を焼くのでしょうね💧
前面に壁SVGをおいたので、景色が完全にかくれてしまっています。
さらにこの前面に、コンテンツを配置しましょう。
<body> <img id = "kesiki" src = "fu.svg"> <svg 「「3〜 略 〜」」 > 「「3〜 略 〜」」 </svg> </body>
.content { width:80%; height:10em; border:solid 6px gray; }
文章でもなんでも、テキトーにコンテンツを配置してください。ただし、スクロールが観察できるように画面高さを大幅に超える内容にしましょう。
「ふ」はめんどくさいので<div>領域を3つ配置して、スクロール観察に十分な高さをCSSで指定しておきました。
これで「3層構造」の配置は完了です。
それでは「窓」を開けるためのマスクを実装していきます。
<svg id = "kabe" width = "100%" height = "100%"> <rect id = "「「5sikaku」」" width = "100%" height = "100%" fill = "beige"/> <mask id = "「「1mask」」"> <rect width = "100%" height = "100%" fill = "#fff"/> <rect x = "25%" y = "40%" width = "25%" height = "25%" fill = "#000"/> </mask> </svg>
すでに配置してある、<body>領域いっぱいのベージュ長方形。ここにマスクを掛けるため、呼び出し用のid「sikaku」を指定しました。
そしてmask要素を追加します。領域いっぱいの白長方形の前面に、1部黒長方形が重なったもの。
左が作成したマスクのイメージ。
右の「#sikaku」に対し、マスクを施します。
#sikaku { mask:url(#mask); }
<body>に窓を開けることができました。
スクロールしても、外の景色画像は固定されています。
<body>に窓を開けることはできました。しかしこれだけでは、「意図した位置に窓を配置」することはできていません。
ページ内の窓にしたい位置に<div>要素を置き、位置とサイズを取得。
取得した値をもとに、マスクの黒長方形の位置とサイズを一致させるようにしましょう。
「「5#madoarea」」 { width:60%; height:10em; margin:0 auto; border:solid 5px crimson; }
1つ目の「.contents」の後ろに<div>を配置、idを「madoarea」としました。
幅と高さを指定し、わかりやすいよう紅色のborderを付けています。
JavaScriptのgetBounidingClientRectメソッドを使って、#madoareaの位置と高さを取得しましょう。その値をマスクの黒長方形の座標値に移植します。
<mask id = "mask"> <rect width = "100%" height = "100%" fill = "#fff"/> <rect id = "「「5mado」」" x = "25%" y = "40%" width = "25%" height = "25%" fill = "#000"/> </mask>
JavaScriptの前に、SVGのコードを下処理。
<mask>要素内の黒長方形を呼び出せるよう、id名を「mado」としておきます。
「「3// ページが読み込まれてから関数呼び出し」」「「1 ....@1@」」 window.onload = function () { 「「3// madoareaとmado、及びbodyを取得」」「「1 ....@2@」」 const madoarea = document.getElementById("madoarea"); const mado = document.getElementById("mado"); const body = documet.querySelector("body"); 「「3// madoareaの位置とサイズ」」「「1 ....@3@」」 let areasize = madoarea.getBoundingClientRect(); 「「3// 幅と高さを#madoに移植」」「「1 ....@4@」」 mado.setAttribute("width",`${areasize.width}`); mado.setAttribute("height",`${areasize.height}`); 「「3// 親要素との差分を考慮し、x座標を取得」」「「1 ....@5@」」 mado.setAttribute("x",`${mado_size.x-body.getBoundingClientRect().x}`); 「「3// スクロール位置を加算したy座標を移植」」「「1 ....@6@」」 mado.setAttribute("y",`${window.scrollY+mado_size.y}`); }
@1@ レイアウトが崩れないよう、ページが読み込まれてから処理を開始するようにしています。
@2@ 位置とサイズを合わせたい「#madoarea」「#mado」を取得。 そして<body>要素も取得しておきましょう。のちほど使用します。
@3@ getBoundingClientRect( )メソッドは、要素の幅と高さ、画面に対しての位置を取得することができます。「#madoarea」の情報を取得しておきます。
@4@ 取得した情報を、黒長方形(#mado)に移植していきます。
width/heightについてはそのまま適用しても問題ありません。setAttributeを使って書き込みましょう。
@5@ PC版でのデザインでは、⬆︎のようにbodyの最大幅を固定しているものも多々見受けられます。
SVG要素のx座標は自身の領域の左端を0としてスタートしますが、boundingClientRectのxは画面の左側が基準となります。
そのためPCでの表示状態では<body>の外側の大きさだけ、双方のx値に差分がでてしまいます。<body>左端からの正確なx値を求めるには、外側の部分を引いてやる必要があります。
@6@ getBoundingClientRectのtopは、「画面上部からの距離」を取得します。現在のスクロール量を考慮した値ではないので、window.scrollYでスクロール量を取得し、topに足し合わせる必要があります。
結果⬆︎です。「#madoarea」に黒長方形を一致させることができました。
窓枠のborderは消すなり装飾するなり、お好みのスタイルにしてください。
これで思い通りの場所に「窓」を設置することができますね ♪
お疲れ様でした!
最後までお読みくださり、ありがとうございます。
今回は読者様のリクエストに基づき、記事を作成させていただきました。とても楽しかったです。
製作中は「窓」の仕組みそのものよりも、ページへの正確な配置作業に苦戦しました。ChromeとSafatiで検証しながら、何とか完成した次第です。
しかしながらwebページには様々なレイアウトが存在します。皆さんのwebサイトに今回の「窓」をうまく開けるには、ここで述べた以外にも調整すべき点が出てくるかもしれません。
もし記事についての疑問点や他にも「こういうのを作りたい」などあれば、「ふ」のtwitterなどからリクエストいただければ、できる範囲とはなりますがお答えしたいと思います。
今後も面白いUIを作ったら、紹介させていだだきますね。
ではまた〜 ♫
ベクターグラフィック、web、ガジェットなど。役立つ情報や観ていてたのしいページを書いていきたいと思います。