概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Glue Cross-Browser Responsive Irregular Images with Sticky Tape
- 公開日: 2017/07/10
- 著者: Ana Tudor
- ブログサイト: css-tricks.com
注: 英語記事執筆の時点からブラウザのバージョンが進んでいるため、本記事内の細かなCodePen埋め込み表示は元記事のCodePen表示と若干異なっている可能性があります。また、ブラウザによってもCodePen表示が若干異なる可能性があります。
CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)
この間、Vasilis van Gemert氏のサイト「Atlas of Makers」のことがふと頭をよぎりました。とても面白く風変わりなこのサイトに惹かれて調べてみたところ、ここはまさに学ぶ価値のあるサイトであると確信できました。このサイトには数年におよび記事やよもやま話がたくさん詰まっていますが、サイト制作に使われている機能が実にクールなのです。このサイトの技法にはCSS Grid、カスタムプロパティ、blendモードや、SVGまで動員されていますが、それにもかかわらずなぜかそれほど知られていません。
このサイトでは四角でない不定形の画像の作成にSVGを使っており、まるでページに蛍光テープやスコッチテープで貼り付けたかのように表示されています(スコッチテープエフェクト)。本記事では、ブラウザだけで作業できるシンプルな手法でこの技法を再現する方法について解説します。それでは始めましょう。
訳注: 以下、原文のscotch tapeは単に「テープ」と表記します。
1. 画像のポリゴン頂点座標の取得
まずは題材選びから。こちらのsnow leopard(ユキヒョウ)画像を例として使うことにします。
続いて、このsnow leopardの輪郭に合わせてざっくりポリゴンで囲みます。今回はClippy(作: Bennett Feely)というサービスを使いました。CSSのclip-path
は現時点ではクロスブラウザではないため、ここでは使いません(使いたい方はMicrosoft Edgeのフォーラムで投票してください: ログイン不要です)。Clippyはとても素晴らしいサービスで、うんざりするほどのボタンやらオプションやらを装備した画像レタッチソフトウェアを使わずに、ポリゴンの頂点のデータだけをきわめて高速に取り出せます。
次は画像のカスタムURLとカスタム寸法を設定します。Clippyではviewportのサイズに沿って寸法を制限できますが、ここでは実際の画像のサイズはまったく重要ではありません(特に、最終出力は%
値だけで指定されるため)。この画像にはアスペクト比2:3
だけを指定することにします。
Clippyでカスタム寸法とURLを指定する場所は上のスクリーンショットに印をつけてあります。カスタム寸法の縦横比は2:3
になるようにする必要があります。幅は540 = 2*270
、高さは810 = 3*270
とします。
作業を説明しやすくするため、Show outside clip-path
オプションをオンにしておきます。
続いて、カスタムポリゴンをクリッピングパスで使えるように、すべての頂点を選択し、パスをきちんと閉じてから頂点の位置を適当に調整します。
ネコの輪郭をごく大まかに外側からなぞるようにカスタムポリゴンの点を選択すると、CSSのclip-path
コードが生成されます。
2. ブラウザのdeveloper consoleでの作業
このポイント(%
値)のリストを選択し、ブラウザのdeveloper consoleを開いて以下のJavaScriptコードを貼り付け、文字列部分にポイントリストを貼り付けます。
// JS(見やすさのため適当に改行を挿入しています)
let coords = '69% 89%, 84% 89%, 91% 54%,
79% 22%, 56% 14%, 45% 16%,
28% 0, 8% 0, 8% 10%,
33% 33%, 33% 70%,
47% 100%, 73% 100%';
次のコードをコンソールに貼り付けて、%
記号をすべて取り除き、文字列を分割します。
// JS
coords = coords.replace(/%/g, '').split(', ').map(c => c.split(' '));
続いて画像の寸法を指定します。
// JS
let dims = [736, 1103];
これで、画像の寸法の座標を拡大縮小できるようになりました。得られた値は次のJavaScriptコードで四捨五入します。画像がかなり大きいので、ネコの輪郭をなぞるポリゴン座標の小数点部分は不要です。
// JS
coords = coords.map(c => c.map((c, i) => Math.round(.01*dims[i]*c)));
後は以下のコードを実行すればおしまいです。
// JS
`[${coords.map(c => `[${c.join(', ')}]`).join(', ')}]`;
得られた値をブラウザのdeveloper consoleからコピーしてフォームに貼り付けます。
3. SVG画像の生成
今度はPugでSVG画像を生成しましょう。ここでは2.で取得した頂点座標の配列をそのまま使っています。
// pug
- var coords = [[508, 982], [618, 982], [670, 596], [581, 243], [412, 154], [331, 176], [206, 0], [59, 0], [59, 110], [243, 364], [243, 772], [346, 1103], [537, 1103]];
- var w = 736, h = 1103;
svg(viewBox=[0, 0, w, h].join(' '))
clipPath#cp
polygon(points=coords.join(' '))
image(xlink:href='snow_derpard.jpg'
width=w height=h
clip-path='url(#cp)')
上のコードを実行すると、次のように不規則な形の画像を得られます。
See the Pen irregular shaped image by Ana Tudor (@thebabydino) on CodePen.
4. 不規則な画像にテープエフェクトを追加する
いよいよページの画像にテープを貼り付けます。テープを生成するために、先ほどと同じ座標の配列を使います。テープの長さを座標から読み出すコードを繰り返すために、まずループを書きます。
// pug
-// (上と同じなので省略)
- var n = coords.length;
svg(viewBox=[0, 0, w, h].join(' '))
-// (上と同じなので省略)
- for(var i = 0; i < n; i++) {
- }
続いてこのループ内で、頂点から次の頂点にテープを貼るかどうかを乱数で決めます。
// pug
- for(var i = 0; i < n; i++) {
- if(Math.random() > .5) {
path(d=`M${coords[i]} ${coords[(i + 1)%n]}`)
- }
- }
stroke
がnone
になっているので、これだけではテープは表示されません(Codepen)。
次のようにhsl()
にランダムなhue値を与えてstroke
を太くすると、テープが表示されるようになります。
// scss
stroke: hsl(random(360), 90%, 60%);
stroke-width: 5%;
mix-blend-mode: multiply
さらにmix-blend-mode: multiply
を指定することで、よりテープらしく見えるようにしています。
See the Pen irregular shaped image with tape #1 by Ana Tudor (@thebabydino) on CodePen.
5. クロスブラウザ化
だいぶよくなってきましたが、実はまだ問題がいくつか残されています。
最初にして最大の問題は、クロスブラウザになっていないということです。mix-blend-mode
はまだEdgeで動きません(まだの方はぜひ投票をお願いします!)。Edgeでほぼ同等の効果を得るために、Edgeの場合のみstroke
を半透明にすることにします。
当初私が思いついた手法は、RGBコンポーネントでサポートされない非整数をcalc()
値で整数に変換するというものでしたが、この方法は現時点ではEdgeでしかサポートされていません。今使っているのは、残念ながらrgb()
値ではなくhsl()
値です。今回は幸いScssを使っているので、これでRGBコンポーネントを抽出できます。
// scss
$c: hsl(random(360), 90%, 60%);
stroke: $c;
stroke: rgba(calc(#{red($c)} - .5), green($c), blue($c), .5)
最下部の行はEdgeには適用されますが、calc()
の動作によってChromeとFirefoxでは無効になります(下図左がChrome、右がFirefox)。
ただし、今後他のブラウザがEdgeと同じ動作になればこの手は使えなくなります。
今後も使える方法としては、@supports
がよいでしょう(codepen)。
path {
$c: hsl(random(360), 90%, 60%);
stroke: rgba($c, .5);
@supports (mix-blend-mode: multiply) { stroke: $c }
}
6. テープの端を重ね貼り表示する
次の問題は、テープの端をもう少し伸ばしてテープらしく表示する方法です。幸い、この問題はstroke-line-cap
をsquare
に設定するだけで簡単に修正できます。これを指定すれば、簡単にテープの両端をテープ幅の半分だけ長くできます。
See the Pen irregular shaped image with tape #3 by Ana Tudor (@thebabydino) on CodePen.
7. テープのはみ出しが切り落とされないようにする
最後の問題は、テープがSVG画像の端で切り落とされてしまうことです。SVGのoverflow
プロパティをvisible
に設定したとしても、SVGの内容が切り落とされてしまったり、画像の次に来る要素で隠されてしまったりする可能性が残ります。
これについては、image
の周りのviewBox
スペースを増やせばどうにかできそうです。テープで隠れないようにするのに十分なスペース増加分を、ここではp
と呼ぶことにします。
-// 同上
- var w1 = w + 2*p, h1 = h + 2*p;
svg(viewBox=[-p, -p, w1, h1].join(' '))
-// 同上
このp
の値をどうやって決めるかが問題です。
理論編
値を決めるときには、今使っているstroke-width
の値が%
になっている点に注意しておく必要があります。SVGでは、stroke-width
などの%
値はSVG領域の対角線の長さから算出されます。今回の場合、SVG領域は幅w
、高さh
の四角形(rectangle)です。この四角形に対角線を引くと、ご存知ピタゴラスの定理によって以下の黄色い三角形の斜辺を計算できることがわかります。
したがって斜辺の長さは以下のPugコードで得られます。
// pug
- var d = Math.sqrt(w*w + h*h);
ここからは、stroke-width
を対角線の長さの5%で計算することにします。これは対角線d
に.05
をかけたものと同等です。
// pug
- var f = .05, sw = f*d;
ここで得た値は、%
値(5%
)からユーザー設定の単位(.05*d
)に変換されていることにご注目ください。こうしておくと、viewBox
の寸法を増やしたときに対角線の長さも増え、対角線の5%
の長さも得られるので便利です。
stroke
を描画すると、あらゆるpath
について幅の半分はパスの線の外側に、半分はパスの線の内側にそれぞれはみ出ます。今やりたいのは、viewBox
のスペースをstroke-width
の半分の長さより大きく取ることです。前述のstroke-linecap
によって、パスの両端からstroke-width
の半分だけはみ出す点にも注意しておく必要があります。
実践編
ここからは実践編として、クリッピング用ポリゴンの頂点の1つが元画像のちょうど端にかかっている状況を考えてみましょう。こうした点がいくつあっても方法は同じですが、話を簡単にするために、ここでは1つの点についてだけ考えることにします。
以下のようにポリゴンの端点Eが元画像の最上部の境界にかかっているとします。さらに、切り出したSVGでも同様であるとします。
今知りたいのは、作成したstroke
とstroke-linecap
にsquare
が設定されている場合に、画像最上部の境界からどのぐらいはみ出すかです。はみ出す長さは境界との角度によって変わるので、はみ出し部分が切り落とされてしまわないよう、境界のさらに上に確保する最大の余白を知りたいのです。
この点をより深く理解するために、次のインタラクティブなデモを用意しました。デモを開き、スライダを左右に動かしてはみ出した部分を回転させると、stroke
(およびsquare-linecap
)の隅が境界の外側でどのぐらいはみ出すかを実感できます。
See the Pen rotation at the top edge by Ana Tudor (@thebabydino) on CodePen.
stroke
とstroke-linecap
の外側の隅が描く軌道をたどるとわかるように、画像の境界の外側に必要な余白は、境界上の端点Eと、stroke
およびstroke-linecap
によってできた隅(図ではAまたはB、位置は角度によって異なる)との間の線分が境界と垂直になるときに最大になり、必要な余白の長さはこの線分AE(またはBE)の長さに等しくなります。
テープの端はstroke-width
の半分の長さだけ端点Eからはみ出します。このはみ出しは、余弦PBと通常の方向EPで同じ長さになります。したがって、三角形EBPは2辺の長さがそれぞれstroke-width
の半分に等しい直角二等辺三角形となります。線分EBの長さは、この直角二等辺三角形の斜辺の長さになります。
ふたたびピタゴラスの定理に登場いただくと、Pugコードは次のようになります。
// pug
- var hw = .5*sw;
- var p = Math.sqrt(hw*hw + hw*hw) = hw*Math.sqrt(2);
これまでの結果をすべて合わせると、次のPugコードになります。
// pug
/* 座標と初期サイズは同上 */
- var f = .05, d = Math.sqrt(w*w + h*h);
- var sw = f*d, hw = .5*sw;
- var p = +(hw*Math.sqrt(2)).toFixed(2);
- var w1 = w + 2*p, h1 = h + 2*p;
svg(viewBox=[-p, -p, w1, h1].join(' ')
style=`--sw: ${+sw.toFixed(2)}px`)
/* 同上 */
後はCSS側で、stroke-width
の値を与えてテープのはみ出しを調整するだけです。
// scss
stroke-width: var(--sw);
この--sw
は、stroke-width
を設定してcalc(var(--sw)*1px)
を算出するのに使うため、単位のない値は使えない点にご注意ください。理論的にはこれでも動くはずですが、現実にはFirefoxとEdgeでstroke-*
のcalc()
値がまだサポートされていません。
CodePen上の最終的な表示は、次のようになります。
See the Pen irregular shaped image with tape #4 by Ana Tudor (@thebabydino) on CodePen.