はじめに
こんにちは、バックエンドエンジニアのTKです。弊社には「engineer」というslackチャンネルがありまして、LIGで働く国内外のエンジニアたちの悩みごとの相談から、気になった情報やイケてるサイト、サービスなどの会話が日夜飛び交っているのですが、先日そのチャンネルでCSSだけで作られたという素敵な作品が紹介されていました。
私自身(今ではバックエンドに転向したこともあり、その辺りからは離れてしまっていますが)、フロントエンドをメインにしていたころは時間を見つけてはチマチマと作っていたものです。懐かしさを感じながら当時のことを振り返ってみたのですが、センスも発想力も乏しい私の作品は、あまりイケてるとは言えないものだったなと苦笑いをしてしまいました。
そんな折、ふと、少し前にbox-shadowでピクセルアートを描くというのも流行っていたよなあ……と思い出しました。
あれって、ひとコマを1px × 1pxに設定すれば画像をそのまま再現できるのではないだろうか? これなら、センスや発想力がなくても、元となる画像さえあればCSSだけで好きなモノを描くことができます。
以前、1px × 1pxのdivで画像を再現するというスクリプトを書いたことがあったので、そのコードを応用すればわりと簡単に実装できそうです。
いや、わかってる、わかってるんです! CSSアートは人力でいろいろなプロパティを駆使しながら試行錯誤して描くからこそ『すごい!』となるんですよね。しかし、前述の通り私はセンスや発想力に乏しいため、力技で戦うしかないのです。(なにと戦っているんだ)
さっそく結果
せっかくなので好きな画像ファイルを変換できるように、アップロード機能(正確にはどこにもアップロードされるわけではありませんが)と生成した内容をコピーして使えるよう、画面上にCSSを出力する仕様を加えてみました。
これでどんなイラストや写真でも一つのdivで、しかもCSSだけで再現できるようになります!
あまり大きい画像を入れるとブラウザがクラッシュしてしまう可能性があるため、width + heightが1000pxをこえる場合はアラートのうえ終了するように設定してあります。ブラウザの限界を試してみたい方は、リミットを外して実行してみてください。
See the Pen
canvas2shadow by K (@lpcwww)
on CodePen.
少しだけ処理について解説
画像を読み込んでからの処理の流れは関数mainの内容を見るとわかるかと思いますが、大まかに下記のようになっています。
1. 画像のサイズを取得しておく
2. 画像のサイズが制限をこえている場合は処理を止める
3. 画像をcanvasへ描画し、その情報を取得する
4. 取得した情報を元にbox-shadowの値を形成する
5. CSSのテンプレートに、サイズとbox-shadowの値を挿入する
6. styleタグをDOMに挿入する
7. CSSの内容を表示する
この項目の中で、特に関数として切り出している部分について見てみましょう。
※アッパーケース(全部大文字)でかかれた部分は、関数外で定数として定義されています
画像をcanvasへ描画し、その情報を取得する
const getImagePixelData = (img, size) => {
CANVAS.width = size.w;
CANVAS.height = size.h;
const ctx = CANVAS.getContext('2d');
ctx.drawImage(img, 0, 0, size.w, size.h);
const imageData = ctx.getImageData(0, 0, size.w, size.h);
return imageData.data;
};
取得できる情報には1px × 1px単位で色情報が含まれているのですが、下記のように若干扱いづらい構造になっています。
[ R, G, B, A, R, G, B, A, ... ]
見やすくするとこんな感じです。
[
R, G, B, A,
R, G, B, A,
...
]
今回は単純な処理しかおこなわないのでそのまま使っていますが、一度扱いやすい構造に変換する処理を加えるのもいいでしょう。
取得した情報を元にbox-shadowの値を形成する
const c2s = (data, size) => {
let rgba = '';
let shadow = [];
let x = 1;
let y = 1;
for (let i = 0; i < data.length; i += 4) {
rgba = data[i] + ','
+ data[i + 1] + ','
+ data[i + 2] + ','
+ data[i + 3] / 255;
shadow.push( x + 'px ' + y + 'px ' + 'rgba('+ rgba + ')' );
if(x >= size.w) { x = 1; y++; } else { x++; }
}
return shadow.join(',');
};
取得したデータが4つ区切りで1ピクセルの情報(前項参照)となっているので、for文で4刻みでカウントアップしていきます。
また、変数rgbaに代入する際、一回目のループではi=0なので、配列dataの0番目、0+1=1番目、2番目、3番目を参照した値を使用します。二回目のループではi=4になっているので、4番目、4+1=5番目、6番目、7番目の値になります。
あらかじめ準備しておいたCSSのテンプレートに、サイズとbox-shadowの値を挿入する
const generateCSS = (shadow, size) => {
return `${ARTBOARD_CLASS_NAME} {
width: ${size.w}px;
height: ${size.h}px;
position:relative;
}
${ARTBOARD_CLASS_NAME}:after {
content: "";
width: 1px;
height: 1px;
position: absolute;
top: -1px;
left: -1px;
box-shadow: ${shadow.replace(/\)\,/g,'),\n\t')};
}`;
};
はじめはjavascriptのstyleプロパティを使う実装にしていたのですが、CSSだけでアートしている感を出すことと、最終的にCSSの内容を表示することなど考え、styleタグを挿入する実装へと変更しました。
CSSの内容を表示させたときの可読性(そんなレベルの量ではありませんが)を考え、box-shadowの値を改行しています。
おわりに
これは余談ですが、サンプルで入れているイラストは400px × 400pxのpngで、データサイズは元画像が82KB、base64で110KB、CSSファイルだと4.6MB(改行とインデントを削除して4.3MB)でした! まさに桁違い!
400 × 400でbox-shadowを160000も指定しているわけですね! そりゃあファイルサイズも大きくなります。しかも画像と違って伸縮ができません! 実案件ではまず使えませんね! 使い所がありません!
……これからも探究心を忘れずに知的好奇心を満たすためだけのコードを書き続けていきたいと思います! TKでした。
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。