【コピペOK】ホラー演出をCSSとjsで作る【clientX】

troubled-woman

ホラー演出を CSS と JavaScript で作りたい。

この声にお答えします。

対象読者

  • ホラー演出を実装したい人
  • CSS・JavaScript による表現の幅を広げたい人
  • 変わった演出が好きな人

実装したい人はもちろんですが、見て楽しい記事だと思うので、興味本位の読者も歓迎です。

コードはコピペしてもらっても構いません。

それではやっていきましょう。

マウスカーソルに合わせて移動するライトアップ

マウスカーソルが当たっている部分だけ明るくなります。

See the Pen spot-light by jun (@cpvhoorw-the-sasster) on CodePen.

コードの概要

document.addEventListener('DOMContentLoaded',... ではDOM構築後に中の処理が実行されるようにしています。
kaibunsho.addEventListener('mouseleave',... では怪文書のエリアからマウスカーソルが出た時に、スポットライトがなくなり、元の暗い背景色が全体にかかる処理を書いています。
コード:kaibunsho.addEventListener('mouseleave'...

// 怪文書のエリアからマウスカーソルが抜けた時にスポットライトを消す
kaibunsho.addEventListener('mouseleave', function (e) {
    kaibunsho.setAttribute(
        "style",
        `background-color:rgba(0,0,0,0.9); `
    );
})
kaibunsho.addEventListener('mousemove'... では怪文書のエリアでマウスカーソルを動かした時に、スポットライトがマウスカーソルに合わせて移動する処理を書いています。
コード:kaibunsho.addEventListener('mousemove'...

// 怪文書エリアでマウスカーソルを動かすとスポットライトが動く
kaibunsho.addEventListener('mousemove', function (e) {
    const rect = this.getBoundingClientRect();
    // 要素の左上を原点(0,0)とした時のマウスカーソルの位置を設定
    const x = e.clientX - rect.left;// 要素の左端からのマウスカーソルのX座標
    const y = e.clientY - rect.top;// 要素の上端からのマウスカーソルのY座標
    // マウスカーソルの位置にスポットライトをあてるCSSスタイルの設定
    kaibunsho.setAttribute(
        "style",
        `background-image:
        radial-gradient(circle at ${x}px ${y}px, 
            #fff  0px, #fff 30px, transparent 100px)`
      );
});
        

コードの詳しい解説

マウスカーソルの座標の取得

以下で怪文書の要素(<div class=”kaibunsho”>)の左上を原点(0,0)とした時のマウスカーソルの座標を変数 x と変数 y に代入しています。
コード:マウスカーソルの座標の取得
javascript

const rect = this.getBoundingClientRect();
// 要素の左上を原点(0,0)とした時のマウスカーソルの位置を設定
const x = e.clientX - rect.left;// 要素の左端からのマウスカーソルのX座標
const y = e.clientY - rect.top;// 要素の上端からのマウスカーソルのY座標
          
コードの解説:マウスカーソルの座標の取得

clientX/Y でビューポート(ブラウザの表示エリア)の左上を原点(0,0)とした時のマウスカーソルの座標を返しています。

getBoundingClientRect()メソッドではビューポートの左上を原点(0,0)とした時の要素の位置を返しています。 つまり式を解読すると以下になります。

const x = e.clientX - rect.left;
→(要素の左端を座標 0 とした時のマウスカーソルの水平方向の座標) = (ビューボートでのマウスカーソルの水平方向の位置) – (ビューポートでの要素の左端の座標)

const y = e.clientY - rect.top;
→(要素の上端を座標 0 とした時のマウスカーソルの垂直方向の位置) = (マウスカーソルのビューボート内で垂直方向の位置) – (ビューポートでの要素の上端の座標)

スポットライトの実装

スポットライトをグラデーションで実装しています。
上記の「マウスカーソルの座標の取得」で代入した x, ybackground-image で使用しています。
これがスポットライトの実装になります。
コード:スポットライトの実装
javascript

kaibunsho.setAttribute(
    "style",
    `background-image:
        radial-gradient(circle at ${x}px ${y}px, 
            #fff  0px, #fff 30px, transparent 100px)`
);
      
コードの解説:スポットライトの実装

コードを解読すると以下になります。

怪文書の要素(<div class="kaibunsho">)の背景色をグラデーションで設定
グラデーションは円形
円の中心は(x,y)
0px(円の中心)から 30px にかけては色#fff から #fff のグラデーション(同じ色なので実質は #fff 一色)
30px から 100px にかけては #fff から transparent のグラデーション
100px 移以降は transparent

transparent が設定されているところでは background-color: rgba(0, 0, 0, 0.9); が適用される)。

補足

イベントについて

mouleave
要素からマウスカーソルが出た時にイベントが発行します
mousemove
要素内でマウスカーソルが移動するとイベントが発行します

座標位置を表すプロパティとメソッドについて

clientX/Y
ビューポート(ブラウザの表示エリア)の左上の座標が原点(0,0)
screenX/Y
デバイスのディスプレイの左上の座標が原点(0,0)
もしウィンドウを移動していた場合は screenX/YpageX/Y がずれることになる。
pageX/Y
ページの左上の座標が原点(0,0)
スクロールしていた場合は、原点がウィンドウ外にいく。その結果、pageX/YclientX/Y の値がずれる
offsetX/Y
要素の左上の座標が(0,0)。
getBoundingClientRect()
要素の寸法とビューポートでの位置を返す
要素の位置を表すプロパティ
left
top
right
bottom
要素の寸法を表すプロパティ
width
height 要素の width(もしくはheight) + padding + border-width の値と同じ

画面を埋め尽くすテキスト

画面にテキストが時間差で現れます。
テキストが現れる位置と大きく表示される一文字はランダムで選出します。
また、何個テキストを表示するのかは設定できます。

See the Pen text-emerge by jun (@cpvhoorw-the-sasster) on CodePen.

コードの概要

ページがロードされると
window.onload = startAnime で、startAnime 関数が実行されます。
コード:startAnime
JavaScript

function startAnime() {
    // 追加するテキストを定義
    let text1 = ここにいるよ";
    let text2 = "ここにもいるよ";
    let text3 = "どこに行くの";
    let text4 = "終わらせない";
    let textArray = new Array(text1, text2, text3, text4)
    // テキストを設定した要素を追加する回数
    let textCount = 5;
    //テキストを挿入する要素の幅と高さを取得。あとでテキストの配置場所を指定する際に参照する。
    let kabe = document.querySelector(".kabe");
    let kabeHeight = kabe.offsetHeight;
    let kabeWidth = kabe.offsetWidth;
    //親要素にクラス名 play が付いてなければ処理をおこなう
    if (!kabe.classList.contains("play")) {
        emergeText();
    }
      
コードの解説:startAnime

追加されるテキストと追加するテキスト数を設定。

またテキストが表示される領域の<div class="kabe"> の幅と高さを取得しておく。
これは後にテキストの配置場所を指定する際に参照する。

続けて、emergeText 関数が実行されます
コード:emergeText
javascript

// テキストを含む新しく要素を作成しテキストを配置する処理
function emergeText() {
    --textCount;
    kabe.classList.add("play");
    // 新しくテキストを追加するために要素を作成
    let newElement = document.createElement("span")
    newElement.classList.add("appearText", "text")
    // textArray からランダムにテキストを選択後、テキスト内の一文字にspanタグをかける
    let originalText = textArray[Math.floor(Math.random() * textArray.length)];
    let rndIndex = Math.floor(Math.random() * originalText.length);
    let newText = originalText.replace(originalText[rndIndex], '<span class="big-font">' + originalText[rndIndex] + '</span>');
    newElement.innerHTML = newText;
    // 新規作成したテキストの配置場所をランダムに指定
    let rndPosY = Math.floor(Math.random() * kabeHeight);
    let rndPosX = Math.floor(Math.random() * kabeWidth);
    newElement.style.top = rndPosY + "px";
    newElement.style.left = rndPosX + "px";
    // 作成した要素を追加する
    kabe.appendChild(newElement);
    // 設定したテキスト数に達するまでテキスト作成の処理を実行する
    if (textCount == 0) {
        kabe.classList.remove("play");//なくなった場合は親要素のplayクラスを削除
    } else {
        setTimeout(function () { emergeText(); }, 500);	//0.5秒間隔でアニメーションをスタート
    }
}
  
コードの解説:emergeText

テキストを追加するために新しく span 要素を作成し、クラス名、テキスト、配置位置を設定していく。

設定後に <div class="kabe">kabe.appendChild(newElement) で要素を挿入。

上記の span 要素作成し挿入する処理を textCount 回数分繰り返し行う。
setTimeout() メソッドで 0.5 秒間間隔で関数 emergeText を実行している。

コードの詳しい解説

テキストの文面の設定

テキストの文面を以下で設定しています。
コード:テキストの文面の設定
JavaScript

function startAnime() {
// 追加するテキストを定義
let text1 = "ここにいるよ";
let text2 = "ここにもいるよ";
let text3 = "どこに行くの";
let text4 = "終わらせない";
let textArray = new Array(text1, text2, text3, text4)
⋮
function emergeText() {
    ⋮
    // 新しくテキストを追加するために要素を作成
    let newElement = document.createElement("span")
    newElement.classList.add("appearText", "text")
    // textArray からランダムにテキストを選択後、テキスト内の一文字にspanタグをかける
    let originalText = textArray[Math.floor(Math.random() * textArray.length)];
    let rndIndex = Math.floor(Math.random() * originalText.length);
    let newText = originalText.replace(originalText[rndIndex], '<span class="big-font">' + originalText[rndIndex] + '</span>');
    newElement.innerHTML = newText;
          
CSS

          .big-font {
            font-size: 2rem;
          }
        
コードの解説:テキストの文面の設定

startAnime 関数の最初の方で四個テキストを設定して、配列 textArray に格納しています。

抜粋した emergeText 関数のコードで以下を行なっています。
新しい span 要素の作成
新しい span 要素のクラスを appearTexttext に指定
設定した四個のテキストからランダムに一個のテキストを選出し、テキストの中の一文字をランダムに選出し replaceメソッドで、<span class=”big-font”>[一文字のテキスト]</span>に置換

CSSプロパティの設定でクラス big-font が指定されている要素のフォントサイズを大きくしています。

innerHTML で作成した要素のテキストとして設定しています。

テキストの配置場所の指定

テキストの配置場所を以下のように指定しています。
コード:テキストの配置場所の指定
JavaScript

function startAnime() {
    ⋮
    //テキストを挿入する要素の幅と高さを取得。あとでテキストの配置場所を指定する際に参照する。
    let kabe = document.querySelector(".kabe");
    let kabeHeight = kabe.offsetHeight;
    let kabeWidth = kabe.offsetWidth;
    ⋮
    function emergeText() {
        ⋮
        // 新規作成したテキストの配置場所をランダムに指定
        let rndPosY = Math.floor(Math.random() * kabeHeight);
        let rndPosX = Math.floor(Math.random() * kabeWidth);
        newElement.style.top = rndPosY + "px";
        newElement.style.left = rndPosX + "px";
            
CSS

.kabe {
    width: 300px;
    height: 300px;
    ⋮
    position: relative;
    ⋮
.text {
    ⋮
    position: absolute;
            
コードの解説:テキストの配置場所の指定

変数 kabeHeightkabeWidth<div class=”kabe”> の高さ、幅を代入しています。

emergeText 関数の中で rndPosYrndPosX<div class="kabe"> の高さ、幅を上限とした整数を代入し、
新しく作成した要素 newElement のCSSプロパティ topleft に 数値が rndPosrndPosYpx を指定しています。

テキストを含む span 要素に position:absolute の親要素(<div class="kabe">)に position:relative を指定しているので、
span 要素が <div class="kabe"> 要素の左上を起点に、topleft で指定された位置に配置されます。

テキスト追加を複数回行う処理と回数の指定

テキストの追加する回数を設定し、複数回テキストを追加するように設定しています。
コード:テキスト追加を複数回行う処理と回数の指定

function startAnime() {
    ⋮
    // テキストを設定した要素を追加する回数
    let textCount = 5;
    ⋮
    // テキストを含む新しく要素を作成しテキストを配置する処理
    function emergeText() {
        --textCount;
        ⋮
        // 作成した要素を追加する
        kabe.appendChild(newElement);
        // 設定したテキスト数に達するまでテキスト作成の処理を実行する
        if (textCount == 0) {
            kabe.classList.remove("play");//なくなった場合は親要素のplayクラスを削除
        } else {
            setTimeout(function () { emergeText(); }, 500);	//0.5秒間隔でアニメーションをスタート
        }
    }
  }
      
コードの解説:テキスト追加を複数回行う処理と回数の指定

変数 textCount にテキストを設定した要素を追加する回数を代入してます。

emergeText 関数を実行するたびに textCount をカウントダウンし、0 になるまで emergeText を呼び出しています。

emergeText 関数を呼び出す時は、setTimeout を使用して、0.5秒の間隔を置いています。
これでテキストが一つずつ時間を置いて現れるようになります。

テキストが徐々に現れるアニメーション

テキストを含む span 要素は追加直後は透明です。
そこから透明度を徐々に下げることで、文字が徐々に現れるようなエフェクトをかけています。
コード:テキストが徐々に現れるアニメーション
CSS

.text {
    ⋮
    /* 要素にアニメーション text_anime_on を適用 */
    animation-name: text_anime_on;
    /* アニメーション完了まで5s */
    animation-duration: 5s;
    /* アニメーションの変化が最初ゆっくりで終了まで加速する */
    animation-timing-function: ease-in;
    /* keyframesで最後に設定された値を保持する */
    animation-fill-mode: forwards;
}
⋮
@keyframes text_anime_on {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}
      
コードの解説:テキストが徐々に現れるアニメーション
@keyframes でアニメーションの動きを指定しています。 5秒間かけて文字が透明(opacity: 0;)の状態から透明度を下げて表示されるように指定しています。

補足

Math.random() メソッドの使い方について

Math.random()0~0.999...をランダムに返します。

Math.floor() は小数点を切り捨てます。
例えば Math.floor(1.2568) では、1となります。

なので以下の式では var を最大値とした、整数がランダムで返されます。
Math.floor(Math.random() * var);

今回の実装では以下の式で使っています。

追加するテキストをランダムに選出

let originalText = textArray[Math.floor(Math.random() * textArray.length)];

選出したテキストの中の一文字をランダムに選出。(この一文字だけサイズを大きくする)

let rndIndex = Math.floor(Math.random() * originalText.length);

テキストの配置位置をランダムに選出。


let rndPosY = Math.floor(Math.random() * kabeHeight);
let rndPosX = Math.floor(Math.random() * kabeWidth);
        

まとめ

ホラーエフェクトは実装していて楽しいです。
楽しい理由は「ユーザーを怖がらせる」という明確な目的があるからだと思います。
コーディングすること自体が目的でないコーディングを感じてもらって、少しでも楽しんでもらえたら嬉しいです。

コメント