📺

加工なし CSS のみで画像にグリッチと RGB ずらし効果をかける

12 min read

はじめに

以下のツイートが500いいねを超えましたので解説記事を書きます。

https://twitter.com/ixkaito/status/1444946193826471937

デモ&ソースコード

CodePen に完成品のデモとソースコードを置いてあります。

ずっと観ていると気分が悪くなる恐れがあるのでご注意ください。

グリッチとは

元々は「一過性の障害」という意味ですが、映像の世界ではしばしば「映像の乱れ」という意味で用いられます。ホラーやサイバーなどでよく見られる演出の一つ。

glitch の画像検索結果
https://www.google.com/search?q=glitch&tbm=isch

RGB ずらしとは

色収差、RGB シフト、RGB split (RGB 分割) など、さまざまな言い方がありますが、要するに本来光の三原色である Red、Green、Blue がズレることなく重なって映像を映し出すところ、それがズレた状態で表示されることです。

グリッチエフェクトと一緒に用いたり、場合によっては色ズレも含めてグリッチと言ったりしますので、今回も合わせて実装することでよりグリッチ感が出ると思います。

実装

コンテナ要素

html
<div class="glitch" style="background-image: url(...);">
</div>

要素のインラインスタイルでグリッチ効果をかけたい画像を background-image として指定します。

後述、画像を RGB に分割するときに何度も画像 URL を記述しないで済むために background-image を使います。CSS で背景画像を指定してもよいが、HTML 側で画像を簡単に変更できるようあえてインラインスタイルを使っています。

scss
.glitch {
  background: #000 no-repeat center;
  background-size: 0; // `.glitch` 自体には背景画像を表示しないようにする
  height: 100vh; // width や height は自由
  overflow: hidden;
  position: relative;
}

CSS はこんな感じ。ポイントは background-size: 0;.glitch 要素自体には画像を表示させません。

生の CSS でももちろん大丈夫ですが、アニメーションさせるときにランダムに画像を動かす記述を楽にするため Sass 前提で進みます。

画像を RBG に分割

html
 <div class="glitch" style="background-image: url(...);">
+  <div class="channel r"></div>
 </div>

試しに Red チャンネルのみを表示させてみます。

scss
 .glitch {
   ...
+  .channel {
+    background: inherit; // コンテナ要素の背景を継承
+    background-size: cover;
+    bottom: 0;
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+
+  // あとの実装のため上記 `.channel` と分けて記述
+  .channel {
+    &::before {
+      bottom: 0;
+      content: "";
+      display: block;
+      left: 0;
+      mix-blend-mode: multiply; // 画像に R/G/B を乗算で重ねるとそれぞれのチャンネルを作ることができる
+      position: absolute;
+      right: 0;
+      top: 0;
+    }
+  }
+
+  .r {
+    &::before {
+      background: #f00;
+    }
+  }
}

これで Red チャンネルが表示されるはず。

R チャンネル

html
 <div class="glitch" style="background-image: url(...);">
   <div class="channel r"></div>
+  <div class="channel g"></div>
+  <div class="channel b"></div>
 </div>

同様に Green、Blue チャンネルを追加。

scss
 .glitch {
   ...

   .channel {
     ...
   }
   
   .channel {
+    mix-blend-mode: screen; // 各チャンネルをスクリーンで重ねると元の画像に合成することができる
  
     &::before {
       ...
     }
   }
   
   .r {
     ...
   }
  
+  .g {
+    &::before {
+      background: #0f0;
+    }
+  }
+  
+  .b {
+    &::before {
+      background: #00f;
+    }
+  }

RGB チャンネルをそれぞれ作ってスクリーンで重ねると元の画像が表示されます。

RGB チャンネルで合成された画像

この状態でチャンネルを translate() で適当に動かしてみると、RGB ずらしの効果を得られます。

scss
   .r {
+    transform: translate(4px, 2px);
     ...
   }

   .g {
+    transform: translate(-3px, -1px);
     ...
   }

RGB をずらした画像

RGB ずらしのアニメーション

scss
@function rand($min, $max) {
  @return random() * ($max - $min) + $min;
}

最大値と最小値を簡単に設定できるランダム関数を作っておきます。

scss
$animation-duration: 3s; // 何秒おきにアニメーションをループするか
$glitch-duration: 20%; // グリッチ (RGB ずらし) 自体の長さ、上記秒数に対するパーセンテージ
$glitch-frequency: 10; // 動きの頻度
$glitch-interval: $glitch-duration / $glitch-frequency;

@mixin rgb-shift($name) {
  @keyframes rgb-shift-#{$name} {
    @for $i from 0 to $glitch-frequency {
      #{$i * $glitch-interval} {
        transform: translate(
          #{rand(-2, 2) * 1%}, // 左右にどれぐらい動くか (横幅の±2%)
          #{rand(-0.5, 0.5) * 1%} // 上下にどれぐらい動くか (縦幅の±0.5%)
        );
      }
    }

    #{$glitch-duration}, 100% {
      transform: none;
    }
  }

  animation: rgb-shift-#{$name} $animation-duration steps(1, jump-end) infinite alternate both;
}

RGB のそれぞれのチャンネル用に @keyframes の作成と animation の設定を同時に行うミックスインを作っておきます。

@use 'sass:math';

@function rand($min, $max) {
  @return math.random() * ($max - $min) + $min;
}

$glitch-interval: math.div($glitch-duration, $glitch-frequency);

最新の Sass では上記のように math 関数を使うよう推奨されていますが、CodePen で一部対応していないものがあるので、全編とおして少しレガシーな書き方をします。

ポイントとして、animation-timing-functionsteps(n, <jumpterm>) 関数を使うと、滑らかな動きではなく、カクカクした動きになるのでよりグリッチっぽくなります。詳しくは animation-timing-function | MDN

animation-direction を alternate にする違い

もう一つは animation-directionalternate にしているところ。これがなくても $animation-duration $glitch-duration $glitch-frequency を調整すれば大丈夫なんですが、上図のように同じ長さのグリッチと静止状態でも、alternate にすると繰り返されるアニメーションの長さは半分で済みます。それによって書き出される CSS コードも半分ぐらいで済みます。

scss
   .r {
-    transform: translate(4px, 2px);
+    @include rgb-shift(r);
     ...
   }
   
   .g {
-    transform: translate(-3px, -1px);
+    @include rgb-shift(g);
     ...
   }
   
   .b {
+    @include rgb-shift(b);
     ...
   }

各チャンネルでミックスインをインクルードすれば、RGB ずらしのアニメーションが再生されます。

RGB ずらしのアニメーション

グリッチ

グリッチをどのように実装するかというと、同じ画像を重ねて一部を切り取ってずらす。

scss
 .glitch {
   ...
+  &::before,
+  &::after,
   .channel {
     background: inherit;
     background-size: cover;
     bottom: 0;
     left: 0;
     position: absolute;
     right: 0;
     top: 0;
   }

+  &::before {
+    content: "";
+  }
+
+  &::after {
+    content: "";
+  }
   ...
 }

疑似要素を使って同じ画像を重ねます (div などでも可) 。

scss
 .glitch {
   ...

   &::before {
+    clip-path: polygon(
+      0 20%,
+      100% 20%,
+      100% 30%,
+      0 30%
+    );
+    transform: translate(5%, 0.5%);
     content: "";
   }
 
   &::after {
+    clip-path: polygon(
+      0 60%,
+      100% 60%,
+      100% 65%,
+      0 65%
+    );
+    transform: translate(-5%, -0.5%);
     content: "";
   }
   ...
 }

試しに静止状態のグリッチを表示してみます。画像の切り抜きは clip-path() を使います。詳細は clip-path | MDN

静止グリッチのテスト

before のほうは、RGB レイヤーより下にあり、さらに RGB レイヤーは mix-blend-mode: sreen; になっているため、その部分だけが明るくなり、けっこうグリッチっぽいと思います。

after のほうは少しわかりにくいんですが、真ん中より少し下に画像の一部だけが少しズレています。

2つだけですが、激しく動かしたりサイズを変えたりして、RGB ずらしと組み合わせるとかなりグリッチっぽくなります。もちろん div などを使えばもっと増やすことも可能です。

グリッチアニメーション

scss
@mixin glitch($name) {
  @keyframes glitch-#{$name} {
    @for $i from 0 to $glitch-frequency {
      $left: 0%;
      $right: 100%;
      $top: rand(0, 90) * 1%; // 切り取る位置 (縦軸0%〜90%内でランダムに動かす)
      $bottom: $top + rand(1, 10) * 1%; // 切り取るサイズ (画像縦幅1%〜10%)

      #{$i * $glitch-interval} {
        clip-path: polygon(
          $left $top,
          $right $top,
          $right $bottom,
          $left $bottom
        );
        transform: translate(
          #{rand(-8, 8) * 1%}, // 左右にどれぐらい動くか (横幅の±8%)
          #{rand(-0.5, 0.5) * 1%} // 上下にどれぐらい動くか (縦幅の±0.5%)
        );
      }
    }

    #{$glitch-duration}, 100% {
      clip-path: none;
      transform: none;
    }
  }

  animation: glitch-#{$name} $animation-duration linear infinite alternate both;
}

RGB ずらし同様、ミックスインを作っておきます。

scss
 .glitch {
   ...

   &::before {
+    @include glitch(before);
-    clip-path: polygon(
-      0 20%,
-      100% 20%,
-      100% 30%,
-      0 30%
-    );
-    transform: translate(5%, 0.5%);
     content: "";
   }
 
   &::after {
+    @include glitch(after);
-    clip-path: polygon(
-      0 60%,
-      100% 60%,
-      100% 65%,
-      0 65%
-    );
-    transform: translate(-5%, -0.5%);
     content: "";
   }
   ...
 }

あとはこれを疑似要素にインクルードすれば完成です。

おわりに

デモのように無限ループでアニメーションさせると閲覧者を気分悪くさせる可能性があるので、用法用量を考えたほうがいいとは思います。ただ、CSS でグリッチ・RGB ずらし効果をかけられるということは、簡単にマウスホバーの効果として利用できるということなので、演出の一つとしてわりとありかもしれません。

宣伝

腕に自信があるフロントエンドエンジニア、デザイナー、ディレクター、もしご興味ございましたら覗いてみてください。

https://kiteretz.com/careers/

ソースコード全体

html
<div class="glitch" style="background-image: url(https://images.unsplash.com/photo-1493514789931-586cb221d7a7?ixlib=rb-1.2.1&ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&auto=format&fit=crop&w=1920&q=80);">
  <div class="channel r"></div>
  <div class="channel g"></div>
  <div class="channel b"></div>
</div>
scss
@function rand($min, $max) {
  @return random() * ($max - $min) + $min;
}

$animation-duration: 3s;
$glitch-duration: 20%;
$glitch-frequency: 10;
$glitch-interval: $glitch-duration / $glitch-frequency;

@mixin rgb-shift($name) {
  @keyframes rgb-shift-#{$name} {
    @for $i from 0 to $glitch-frequency {
      #{$i * $glitch-interval} {
        transform: translate(
          #{rand(-2, 2) * 1%},
          #{rand(-0.5, 0.5) * 1%}
        );
      }
    }

    #{$glitch-duration}, 100% {
      transform: none;
    }
  }

  animation: rgb-shift-#{$name} $animation-duration steps(1, jump-end) infinite alternate both;
}

@mixin glitch($name) {
  @keyframes glitch-#{$name} {
    @for $i from 0 to $glitch-frequency {
      $left: 0%;
      $right: 100%;
      $top: rand(0, 90) * 1%;
      $bottom: $top + rand(1, 10) * 1%;

      #{$i * $glitch-interval} {
        clip-path: polygon(
          $left $top,
          $right $top,
          $right $bottom,
          $left $bottom
        );
        transform: translate(
          #{rand(-8, 8) * 1%},
          #{rand(-0.5, 0.5) * 1%}
        );
      }
    }

    #{$glitch-duration}, 100% {
      clip-path: none;
      transform: none;
    }
  }

  animation: glitch-#{$name} $animation-duration linear infinite alternate both;
}

body {
  margin: 0;
}

.glitch {
  background: #000 no-repeat center;
  background-size: 0;
  height: 100vh;
  position: relative;
  overflow: hidden;

  &::before,
  &::after,
  .channel {
    background: inherit;
    background-size: cover;
    bottom: 0;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
  }

  &::before {
    @include glitch(before);
    content: "";
  }

  &::after {
    @include glitch(after);
    content: "";
  }

  .channel {
    mix-blend-mode: screen;

    &::before {
      bottom: 0;
      content: "";
      display: block;
      mix-blend-mode: multiply;
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
    }
  }

  .r {
    @include rgb-shift(r);

    &::before {
      background: #f00;
    }
  }

  .g {
    @include rgb-shift(g);

    &::before {
      background: #0f0;
    }
  }

  .b {
    @include rgb-shift(b);

    &::before {
      background: #00f;
    }
  }
}

この記事に贈られたバッジ

Discussion

ログインするとコメントできます