Tech Racho エンジニアの「?」を「!」に。
  • 開発

CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

注: 英語記事執筆の時点からブラウザのバージョンが進んでいるため、本記事内の細かなCodePen埋め込み表示は元記事のCodePen表示と若干異なっている可能性があります。また、ブラウザによってもCodePen表示が若干異なる可能性があります。

CSS/JS: 画像を不規則な形に切り取ってテープで貼り付けるテクニック(翻訳)

この間、Vasilis van Gemert氏のサイト「Atlas of Makers」のことがふと頭をよぎりました。とても面白く風変わりなこのサイトに惹かれて調べてみたところ、ここはまさに学ぶ価値のあるサイトであると確信できました。このサイトには数年におよび記事やよもやま話がたくさん詰まっていますが、サイト制作に使われている機能が実にクールなのです。このサイトの技法にはCSS Grid、カスタムプロパティ、blendモードや、SVGまで動員されていますが、それにもかかわらずなぜかそれほど知られていません。

このサイトでは四角でない不定形の画像の作成にSVGを使っており、まるでページに蛍光テープやスコッチテープで貼り付けたかのように表示されています(スコッチテープエフェクト)。本記事では、ブラウザだけで作業できるシンプルな手法でこの技法を再現する方法について解説します。それでは始めましょう。

訳注: 以下、原文のscotch tapeは単に「テープ」と表記します。

1. 画像のポリゴン頂点座標の取得

まずは題材選びから。こちらのsnow leopard(ユキヒョウ)画像を例として使うことにします。

ふわふわのsnow leopardが好奇の目でこちらを見ている画像です

続いて、このsnow leopardの輪郭に合わせてざっくりポリゴンで囲みます。今回はClippy(作: Bennett Feely)というサービスを使いました。CSSのclip-pathは現時点ではクロスブラウザではないため、ここでは使いません(使いたい方はMicrosoft Edgeのフォーラムで投票してください: ログイン不要です)。Clippyはとても素晴らしいサービスで、うんざりするほどのボタンやらオプションやらを装備した画像レタッチソフトウェアを使わずに、ポリゴンの頂点のデータだけをきわめて高速に取り出せます。

次は画像のカスタムURLとカスタム寸法を設定します。Clippyではviewportのサイズに沿って寸法を制限できますが、ここでは実際の画像のサイズはまったく重要ではありません(特に、最終出力は%値だけで指定されるため)。この画像にはアスペクト比2:3だけを指定することにします。

ClippyでサイズとURLを指定する

Clippyでカスタム寸法とURLを指定する場所は上のスクリーンショットに印をつけてあります。カスタム寸法の縦横比は2:3になるようにする必要があります。幅は540 = 2*270、高さは810 = 3*270とします。

作業を説明しやすくするため、Show outside clip-pathオプションをオンにしておきます。

アニメーションGIF: 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からコピーしてフォームに貼り付けます。

ブラウザのdeveloper toolsでの作業

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]}`)
  - }
- }

strokenoneになっているので、これだけではテープは表示されません(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)。

2つ目のstrokeの指定が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-capsquareに設定するだけで簡単に修正できます。これを指定すれば、簡単にテープの両端をテープ幅の半分だけ長くできます。

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)です。この四角形に対角線を引くと、ご存知ピタゴラスの定理によって以下の黄色い三角形の斜辺を計算できることがわかります。

SVGの四角形に斜辺を引くと、直角の隣にある2辺がSVG viewBoxの幅と高さになる

したがって斜辺の長さは以下の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の半分だけはみ出す点にも注意しておく必要があります。


`stroke`はパスの線の両側面に半分ずつはみ出し、`square-linecap`によって`stroke-width`の半分だけ端点からはみ出す。

実践編

ここからは実践編として、クリッピング用ポリゴンの頂点の1つが元画像のちょうど端にかかっている状況を考えてみましょう。こうした点がいくつあっても方法は同じですが、話を簡単にするために、ここでは1つの点についてだけ考えることにします。

以下のようにポリゴンの端点Eが元画像の最上部の境界にかかっているとします。さらに、切り出したSVGでも同様であるとします。

元画像の最上部の境界にある点を示す画像。

今知りたいのは、作成したstrokestroke-linecapsquareが設定されている場合に、画像最上部の境界からどのぐらいはみ出すかです。はみ出す長さは境界との角度によって変わるので、はみ出し部分が切り落とされてしまわないよう、境界のさらに上に確保する最大の余白を知りたいのです。

この点をより深く理解するために、次のインタラクティブなデモを用意しました。デモを開き、スライダを左右に動かしてはみ出した部分を回転させると、stroke(およびsquare-linecap)の隅が境界の外側でどのぐらいはみ出すかを実感できます。

See the Pen rotation at the top edge by Ana Tudor (@thebabydino) on CodePen.

strokestroke-linecapの外側の隅が描く軌道をたどるとわかるように、画像の境界の外側に必要な余白は、境界上の端点Eと、strokeおよびstroke-linecapによってできた隅(図ではAまたはB、位置は角度によって異なる)との間の線分が境界と垂直になるときに最大になり、必要な余白の長さはこの線分AE(またはBE)の長さに等しくなります。

テープの端はstroke-widthの半分の長さだけ端点Eからはみ出します。このはみ出しは、余弦PBと通常の方向EPで同じ長さになります。したがって、三角形EBPは2辺の長さがそれぞれstroke-widthの半分に等しい直角二等辺三角形となります。線分EBの長さは、この直角二等辺三角形の斜辺の長さになります。

パスの端点Eと、strokeおよびstroke-linecapではみ出した隅(AまたはB)との間の線分の長さは、stroke-widthの半分の長さを2辺とする直角二等辺三角形の斜辺と等しくなることを示す

ふたたびピタゴラスの定理に登場いただくと、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.

関連記事

新版: やばい臭いのするCSSコード(翻訳)

Webデザイナーがこっそり教える4つのテクニック

CSSでの句読点ぶら下げ: hanging-punctuationプロパティ


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。