フーノページ

webページに窓を開けて背景画像を表示する〜JavaScript/SVG。








リクエストいただきました。


こんにちは、「ふ」です。
先日、読者様からこんなリクエストをいただきました(ありがとうございます)。

「背景画像を固定したまま、ページ全体をスクロールさせるには?」

具体的には⬇︎のようなものです。




スクロールする<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対策。

ここで厄介なのが、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、ガジェットなど。役立つ情報や観ていてたのしいページを書いていきたいと思います。