PerfectVector
著者: Irene Kim

SVGのfeComposite:in、out、xorの結果を比較する

SVGフィルターの入力をin、out、xorで比較し、順序と部分的なアルファを検証。見た目の開口部が編集可能な形状ではない場合に、元のパスを保持します。

4 分で読める
目次

SVGのfeCompositeは、二つの描画入力を合成します。完全に不透明な入力では、inは重なりの中に最初の入力を残し、outは重なりの外に残します。xorは、共有する重なり以外の両入力を組み合わせます。これらは描画時の塗りを変える操作で、元のパスを編集可能な交差や差分に書き換えるものではありません。

重なりが開口部に見えても、納品物に実際の輪郭が必要なら、この違いが重要です。以下の例では、独自の二つの非対称な図形を比較し、入力を逆にして、部分的な透明度も確認します。フィルター結果と一緒に元のアートワークを保管してください。

演算子を選ぶ前に二つの入力に名前を付ける

MDNのfeCompositeリファレンスは、画像空間でピクセルごとに処理する操作と説明しています。inが最初の入力、in2が二つ目の入力を指定します。フィルターチェーンを変えたときに、気付かないうちにオペランドが変わらないよう、中間結果には明示的な名前を付けてください。

内側が不透明なら、次の三つの操作は比較しやすくなります。

操作最初の入力A、二つ目の入力B入力を逆にすると変わること
in重なり内のAの塗り代わりに重なりがBの塗りになる
outBの外にあるAの塗りAの外にあるBの塗りは異なる領域になる
xor不透明な共有部分以外の両入力同じ非重複領域が残る

同じoperator属性は、別のフィルタープリミティブにも異なる値で使われます。合成の文脈を明確にしてください。MDNのoperatorリファレンスは、モルフォロジー操作との違いを説明しています。

見える塗りを変える
重なった色付きアートワークと、重なりが塗られていない合成結果を比較する概念図
模式図:重なった入力から、塗られない領域を作れます。装飾的なシルエットと矢印は概念的なもので、以下の正確な演算子、アルファのサンプル、変わらないパスは、自作して実行したブラウザーテストに基づきます。

二つ目のアートワークの位置を明示する

最初のオペランドAはティール色の段付きブロック、Bは黄土色のゆがんだ凧形です。どちらも同じ0 0 240 160のviewBoxを使います。BはAのローカルなバウンディングボックスを超えるため、Aの範囲をフィルター領域と考えるのは不適切な出発点です。

フィルターはfilterUnits="userSpaceOnUse"を使い、領域をx=0、y=0、width=240、height=160に明示します。MDNのfilterUnitsリファレンスは、この座標の選択を説明しています。さらにprimitiveUnits="userSpaceOnUse"を設定し、二つ目の入力にも同じ枠全体の座標を与えます。

Bは自己完結したSVGのデータURLで、結果名operandBを指定したfeImageを通して渡します。画像入力としてフィルター内に入るもので、Bのパスをフィルター対象のアートワークの新しい直接の子要素にするものではありません。MDNのfeImageリファレンスは、画像入力としての動作を説明しています。

フィルターはsRGBを明示的に選びます。feCompositeリファレンスの説明どおり、このプリミティブの既定値はlinearRGBです。その既定値も演算子名も、パス編集の操作を提供しません。

独自の完全な比較を実行する

これをstarter.htmlとして保存し、たとえばpython3 -m http.server 8766 --bind 127.0.0.1でフォルダーをローカルHTTP配信します。http://127.0.0.1:8766/starter.htmlを開いてください。必要なら別の空きポートを使います。

ページは、二つの元データと七つのフィルター付きケースを、明示した二つのサイズで作ります。各フィルターIDにはケースと幅を含め、同じドキュメント内の複数のインラインSVGが固有の参照を持つようにしています。各SVG保存リンクは、自己完結したフィルターと、エンコードされたB入力を保持します。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Original SVG composite artwork</title>
<style>
  body { margin: 24px; font: 16px system-ui; background: #f6f2e8; }
  main { display: grid; grid-template-columns: repeat(3, 260px); gap: 18px; }
  figure { margin: 0; }
  svg { display: block; background: white; border: 1px solid #bbb; }
  figcaption { margin: 6px 0; }
  pre { white-space: pre-wrap; font-size: 12px; }
</style>
<h1>Original operands and rendered composites</h1>
<main id="cases"></main>
<pre id="report"></pre>
<script>
  const A = 'M24 24 H132 V56 H192 V136 H24 Z';
  const B = 'M92 8 L216 84 L92 152 L60 84 Z';
  const svgNS = 'http://www.w3.org/2000/svg';
  const cases = [
    { id: 'a' }, { id: 'b' },
    { id: 'a-in-b', operator: 'in' },
    { id: 'a-out-b', operator: 'out' },
    { id: 'a-xor-b', operator: 'xor' },
    { id: 'b-in-a', operator: 'in', reverse: true },
    { id: 'b-out-a', operator: 'out', reverse: true },
    { id: 'b-xor-a', operator: 'xor', reverse: true },
    { id: 'a-out-half-b', operator: 'out', opacity: 0.5 },
  ];
  function documentSVG(test, width) {
    const height = width * 2 / 3;
    const id = test.id + '-' + width;
    const second = '<svg xmlns="' + svgNS + '" width="240" height="160" viewBox="0 0 240 160">'
      + '<path d="' + B + '" fill="#d4a53a" opacity="' + (test.opacity ?? 1) + '"/></svg>';
    const href = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(second);
    const first = test.reverse ? 'operandB' : 'SourceGraphic';
    const other = test.reverse ? 'SourceGraphic' : 'operandB';
    const defs = test.operator ? '<defs><filter id="' + id + '" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" x="0" y="0" width="240" height="160" color-interpolation-filters="sRGB">'
      + '<feImage href="' + href + '" x="0" y="0" width="240" height="160" preserveAspectRatio="none" result="operandB"/>'
      + '<feComposite in="' + first + '" in2="' + other + '" operator="' + test.operator + '"/></filter></defs>' : '';
    return '<svg xmlns="' + svgNS + '" width="' + width + '" height="' + height + '" viewBox="0 0 240 160">'
      + defs + '<path d="' + (test.id === 'b' ? B : A) + '" fill="' + (test.id === 'b' ? '#d4a53a' : '#176b5b')
      + '"' + (test.operator ? ' filter="url(#' + id + ')"' : '') + '/></svg>';
  }
  async function run() {
    const results = [];
    for (const width of [120, 240]) {
      for (const test of cases) {
        const source = documentSVG(test, width);
        const svg = new DOMParser().parseFromString(source, 'image/svg+xml').documentElement;
        const figure = document.createElement('figure');
        const caption = document.createElement('figcaption');
        caption.textContent = test.id + ', ' + width + ' × ' + width * 2 / 3;
        figure.append(document.importNode(svg, true), caption);
        document.getElementById('cases').append(figure);
        const path = figure.querySelector('path');
        const bbox = path.getBBox();
        const image = new Image();
        image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source);
        await image.decode();
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = width * 2 / 3;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0);
        const sample = (x, y) => [...ctx.getImageData(Math.floor(x * width / 240), Math.floor(y * width / 240), 1, 1).data];
        const hash = [...new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(path.getAttribute('d'))))]
          .map(v => v.toString(16).padStart(2, '0')).join('');
        const download = document.createElement('a');
        download.href = image.src;
        download.download = test.id + '-' + width + '.svg';
        download.textContent = 'Save SVG';
        figure.append(download);
        results.push({ id: test.id, width, height: canvas.height, d: path.getAttribute('d'), dSHA256: hash,
          bbox: { x: bbox.x, y: bbox.y, width: bbox.width, height: bbox.height },
          directPaths: svg.querySelectorAll('path').length, filterImages: svg.querySelectorAll('feImage').length,
          aOnly: sample(40, 40), bOnly: sample(200, 84), overlap: sample(120, 84), outside: sample(230, 150) });
      }
    }
    document.getElementById('report').textContent = JSON.stringify({ A, B, results }, null, 2);
  }
  run();
</script>
</html>

演算子の入力を逆にしても、コードは同じ直接のAパスを保持します。たとえばb-in-aは、feCompositeをin="operandB"とin2="SourceGraphic"に変えます。フィルター対象の元要素をBに置き換えるわけではありません。これにより、Aの元の形状を変えずに、オペランドの順序を見えるようにします。

ここでのピクセル確認は、生成した各SVGから描画したキャンバスを読み取ります。ダウンロードする納品物は、引き続きフィルター付きSVGです。新しいブーリアンパスでも、PerfectVectorの変換でもありません。

塗りと形状を別々に比較する

このページをそのまま一つのChrome環境で120 × 80と240 × 160で実行し、18個のSVGすべてを保存して、同じブラウザーで画像として開き直しました。表示サイズは明示的な入力であり、二つの物理デバイスに関する主張ではありません。

三つの内側サンプル位置は元の座標系を使います。Aだけが(40,40)、Bだけが(200,84)、重なりが(120,84)です。不透明なオペランドで、両サイズとも次のアルファ値が観察されました。

フィルター入力と演算子AのみのアルファBのみのアルファ重なりのアルファ
A in B00255
A out B25500
A xor B2552550
B in A00255
B out A02550
B xor A2552550

inの行は不透明な重なりを共有しますが、色は違います。Aが先ならティール色(23,107,91,255)、Bが先なら黄土色(212,165,58,255)です。外側のサンプル(230,150)は、すべて透明のままでした。これは選んだ内側のピクセルであり、境界のカバーや互換性の完全なテストではありません。

すべてのフィルター付きファイルには、Aの正確な元のd文字列と、それに一致するSHA-256が残りました。ローカルな形状のgetBBox()は、x=24、y=24、width=168、height=112のままです。この枠は元のパスを表し、見えるフィルターピクセルのバウンディングボックスではありません。エンコードしたBパスも変わりませんでした。したがって、二つの自作輪郭が保たれたままでも、フィルター結果の見た目は変わります。

保存した元データは、直接のパス一つを持ち、フィルター画像はありません。各フィルター付きファイルは、直接のAパス一つと、Bの別の一パスSVGを持つ一つのfeImageを含みます。通常のラスターのimage要素は追加されません。描画時の画像空間の操作とベクターの元オペランドは共存でき、元要素の一覧だけでは、結果が編集可能な合成形状になったことにはなりません。

部分的な不透明度は開口部の意味を変える

最後のケースでは、Aを不透明のままにし、Bを半分不透明にしてから、A out Bを適用します。重なりは空になりませんでした。両サイズで観察したRGBAは(22,106,90,127)で、描画と読み取りの処理による丸めを含む、部分的に透明なティール色です。

outでは、出力アルファは最初のアルファに、1から二つ目のアルファを引いた値を掛けたものです。inでは両者の積です。Filter Effectsの合成定義は、乗算済みの色とアルファでこれらの操作を定義しています。不透明なBはAの重なった塗りを消せますが、部分的に透明なBは一部を残します。

実際の背景上で結果を確認してください。白の上の薄い領域は、完全に透明な穴ではなく、半透明の塗りかもしれません。アンチエイリアスされた縁のピクセルも目視確認が必要です。内側のサンプルは、すべての境界ピクセルのアルファがゼロであることを保証しません。不透明度の再割り当てという別の作業には、SVGのアルファしきい値ガイドを参照してください。

フィルター効果を渡すか、本当の輪郭を作るか

利用先のレンダラーに見た目の合成を再現させたい場合は、このフィルターを残します。元のAとBのファイルを保管し、フィルター付きSVGを実際の利用先で開き直して確認してください。このブラウザーでの再読み込み成功は、別のレンダラーや製造機械を保証しません。

選択可能な合成輪郭や、カット用に仕上がった開口部が納品に必要なら、予定の手順で本当のパス編集またはブーリアン操作を実行し、確認します。ここで変わった見た目の塗りは、その形状を作りません。clipPathとmaskの比較ガイドは、関連する可視性の違いを説明します。SVGのグループ解除ガイドは、表示されるアートワークが期待した編集可能なオブジェクトではない場合に役立ちます。

クリップアートがピクセルでしか存在せず、編集可能な元の図形が必要な場合は、PerfectVectorでクリップアートSVGを準備し、輪郭と開口部を確認してマスターを残してください。目的のフィルターやパス操作は別に適用し、検証します。ベクター化は、このフィルターの見た目の重なりを、検証済みの製造用輪郭に変えるものではありません。

よくある質問

feComposite outは重なったパス形状を削除しますか? 削除しません。二つ目の入力のアルファを使い、描画された塗りを変えます。このフィルター付きファイルでは、重なりの塗りが透明になった場所でも、元のAのパスデータとローカルな形状範囲は変わりませんでした。

inとin2を入れ替えると結果が変わるのはなぜですか? 最初と二つ目の入力を示すためです。Inは二つ目の不透明度の内側に最初の塗りを残し、outはその外側に最初の塗りを残します。逆にしたinのケースは、不透明な重なりを共有しても異なる色を示し、逆にしたoutのケースは異なる領域を残します。

xorは常に重なりを完全に空にしますか? 完全に不透明な内側の重なりは、このxorのケースでは空でした。部分的なアルファとアンチエイリアスされた縁は、それぞれ確認が必要です。不透明な図形の集合図は、すべての半透明ピクセルを説明しません。

フィルターで作った開口部を、そのまま編集可能なカットパスとして渡せますか? 見た目のフィルター結果は、新しく作ったブーリアン形状ではありません。元の図形を残し、予定のパス処理で必要な輪郭を作成、確認し、受け取り側のアプリケーションと製造要件を別に検証してください。

出典

  1. MDN:feComposite — 画像空間での入力の役割、演算子の説明、既定のフィルター色空間。
  2. MDN:operator — 合成演算子の値と、別のモルフォロジーの文脈。
  3. MDN:filterUnits — フィルター領域の座標系。
  4. MDN:feImage — フィルターパイプラインへの画像入力。
  5. W3C:Filter Effectsの合成 — 乗算済みの色とアルファの合成定義。

自分のラスタークリップアートから始めますか。編集可能なSVGマスターを作成し、輪郭と開口部を確認して保管してください。そのうえで、選んだ見た目の合成や、別に作成したパスの結果を利用先で確かめます。

ブログのその他の記事

PerfectVector

編集しやすい
よりきれいなSVGから始めましょう

クレジットカード不要