Web開発
スリーストローク問題
The Three-Stroke Problem (penpot.app)
要約
物理学における「三体問題」は、3つの天体が互いに軌道を描く際の予測不可能性とカオスな性質を説明する有名な問題です。この記事では、この概念をPenpotのデザインシステムにおける「スリーストローク問題」としてアナロジーで説明しています。これは、3種類のストローク(内側、中央、外側)が複雑に絡み合うと、デザインシステムがカオスになるという問題です。
全文翻訳
3つの物体が互いに軌道を描くとき、結果として生じる力学系は、ほとんどの初期条件に対してカオスになります。
これは、3つの天体の不安定性とカオスな性質を説明するために使用される、よく知られた物理学の問題の記述です。三体問題は、ニュートン力学によれば、これらの3つの物体が互いに相互作用する方法は予測不可能であり、不確実性の急速な増大を引き起こすことを示しています。
異なるストロークタイプが同時に適用され、天体のようなオブジェクトを形成する四角形と円。
これは、私が「スリーストローク問題」と名付けたものに焦点を当て、PenpotがWebGLレンダリングに移行する際に直面した課題のいくつかにとって、素晴らしいアナロジーであると私は考えています。元のステートメントを言い換えれば、この問題は次のように述べることができます。
3種類のストロークが互いに絡み合うとき、結果として生じるデザインシステムは、ほとんどの初期条件に対してカオスになります。
基本から始めましょう。ストロークとは何でしょうか?
ストローク、またはボーダーとも呼ばれるものは、レイヤーの周りを囲む線です。デザインソフトウェアでは、ストロークのプロパティを更新して、そのスタイル、サイズ、色をカスタマイズできます。([1])
この定義は正確で的確です。それは形状を囲み、境界を定める線です。この線を特徴づけるのは、それが実線である必要はないということです。さまざまなスタイル(破線、点線)を持つことができ、定義にはサイズや色などの他のプロパティも含まれています。
Penpotで異なるストロークスタイルを使用した形状の例。([6])
Penpotでの異なるシンプルなストロークの組み合わせ
フォーマット間のストローク
CSSでは、同等のものはborderプロパティになります。その主なプロパティは次のように使用できます: border: <width> <style> <color>。border-radiusプロパティは、コーナーの丸み半径を定義し、形状に適用されます。各ボーダープロパティには、独立したCSSプロパティもあります: border-width、border-style、border-color。([3])
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="./style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Border Styles</title>
</head>
<body>
<div class="box solid"></div>
<div class="box dotted"></div>
<div class="box dashed"></div>
</body>
</html>
.box {
width: 200px;
height: 200px;
padding: 16px;
margin: 12px 0;
font-family: sans-serif;
background: #c7ff4fFF;
}
.solid {
border: 10px solid #f76e28FF;
}
.dotted {
border: 10px dotted #f76e28FF;
border-radius: 50px;
}
.dashed {
border: 10px dashed #f76e28FF;
border-radius: 100%;
}
SVGでは、Penpotの元のレンダラーが使用するフォーマット(WebGLレンダラーで置き換えています)では、これらのスタイルの同等物は次のようになります。([2])
<svg width="240" height="700" viewBox="0 0 240 700" xmlns="http://www.w3.org/2000/svg">
<!-- 1) Solid -->
<rect x="20" y="20" width="200" height="200" fill="#c7ff4f" stroke="#f76e28" stroke-width="10" />
<!-- 2) Dotted + border-radius: 50px -->
<rect x="20" y="244" width="200" height="200" rx="50" ry="50" fill="#c7ff4f" stroke="#f76e28" stroke-width="10" stroke-linecap="round" stroke-dasharray="1 18" />
<!-- 3) Dashed circle -->
<circle cx="120" cy="568" r="95" fill="#c7ff4f" stroke="#f76e28" stroke-width="10" stroke-dasharray="20 12" />
</svg>
3つのストロークの登場
この定義に、もう1つ関連するプロパティを追加する必要があります。それは、形状を境界付ける位置に対するストロークのタイプです。そして、ここでスリーストローク問題に遭遇します。内側、中央、外側のストロークです。([7])
内側ストローク: 形状の完全に内側に配置されます。その幅は形状の端から内側に向かって実行されます。
中央ストローク: ストロークの半分が形状の内側に、もう半分が外側に配置されます。定義された幅の半分が形状の内側に、もう半分が外側に配置されます。
外側ストローク: 形状の完全に外側に配置されます。その幅は形状の端から外側に向かって実行されます。
Penpotで、ソリッドフィルカラーと組み合わされた異なるストロークスタイルの組み合わせ
ここでパーティーが始まります。なぜなら、私たちを一つにするパスが分岐し始めるからです。まず、CSSでは、デフォルトのボーダーは内側タイプです。このため、外側または中央のボーダーが必要な場合は、outlineおよびoutline-offsetプロパティを使用して、ボーダーを外側または内側に「押し出す」(負の値を使用する)必要があります。outlineはborderの同等物ですが、外側タイプのストロークです。
「中央」ボーダーの場合、使用した幅の半分だけボーダーを内側に移動する必要があります。
.outer {
outline: 10px solid #f76e28FF;
}
.center {
outline: 10px solid #f76e28FF;
outline-offset: -5px;
}
一方、CSSでは中央ストロークを取得するために工夫が必要ですが、SVGでは中央ストロークがデフォルトです。内側および外側のストロークを取得するには、クリップとマスクを適用する必要があります。
<svg width="520" height="170" viewBox="0 0 520 170" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="clipRect">
<rect x="20" y="20" width="120" height="120" rx="20"/>
</clipPath>
<mask id="outerOnly">
<rect width="100%" height="100%" fill="white"/>
<!-- remove anything on/inside original shape -->
<rect x="380" y="20" width="120" height="120" rx="20" fill="black"/>
</mask>
</defs>
<!-- CENTER (default) -->
<rect x="200" y="20" width="120" height="120" rx="20" fill="#c7ff4f" stroke="#f76e28" stroke-width="12"/>
<!-- INNER (clipped centered stroke) -->
<g clip-path="url(#clipRect)">
<rect x="20" y="20" width="120" height="120" rx="20" fill="#c7ff4f" stroke="#f76e28" stroke-width="12"/>
</g>
<!-- OUTER (masked centered stroke) -->
<rect x="380" y="20" width="120" height="120" rx="20" fill="#c7ff4f"/>
<rect x="380" y="20" width="120" height="120" rx="20" fill="none" stroke="#f76e28" stroke-width="12" mask="url(#outerOnly)"/>
</svg>
WebGLレンダラーでの実装方法
では、WebGLレンダラーではどうしているのでしょうか?この場合、Penpotではよく知られたSkia 2Dレンダリングライブラリを使用することにしました。高レベルでは、SkiaはSkPaintクラス([4])を提供しており、形状のペイント方法を定義できます。PaintのタイプはFill、Stroke、またはStrokeFillです。Penpotで行うことに関しては、さまざまなPaintとそのストロークメソッドの組み合わせを使用することになります。([5])Skiaでの実装方法は次のとおりです。
Penpotで内側、外側、中央の異なる種類のストロークを、異なるフィルタイプと組み合わせて使用します。
内側ストローク
// Inner stroke: Skia strokes are always centered on the geometry,
// so we inset the rect by width/2. The stroke then spans from the
// shape edge inward -> fully inside the shape.
SkPaint stroke;
stroke.setAntiAlias(true);
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(strokeWidth);
stroke.setColor(0xFFF76E28);
const SkRect innerRect = rect.makeInset(strokeWidth / 2, strokeWidth / 2);
canvas->drawRect(innerRect, stroke);
Skiaでの内側ストローク
中央ストローク
// Center stroke: this is Skia's native behavior. The stroke
// straddles the boundary, half inside, half outside. We draw
// directly on the shape rect, no offset needed.
SkPaint stroke;
stroke.setAntiAlias(true);
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(strokeWidth);
stroke.setColor(0xFF14CECA);
canvas->drawRect(rect, stroke);
Skiaでの中央ストローク
外側ストローク
// Outer stroke: outset the rect by width/2 so the centered stroke
// spans from the shape edge outward -> fully outside the shape.
SkPaint stroke;
stroke.setAntiAlias(true);
stroke.setStyle(SkPaint::kStroke_Style);
stroke.setStrokeWidth(strokeWidth);
stroke.setColor(0xFFFC8CFF);
const SkRect outerRect = rect.makeOutset(strokeWidth / 2, strokeWidth / 2);
canvas->drawRect(outerRect, stroke);
Skiaでの外側ストローク
パス: すべてが四角形ではない
これまでのところ、シンプル、あるいは少なくとも直感的であるように思えます。しかし、デザインの世界では、すべてが四角形ではありません。私たちはMinecraftの世界には住んでいません。そのため、パスのようなより複雑な形状を取り込み始めると、Paintを追加して各形状を2回描画するだけでは不十分になります。内側および外側のストロークには、他のSkia関数に頼る必要があります。デフォルトでは、Skiaのストロークは中央揃え(SVGと同様)であるため、その場合は特別なことは何もする必要がありません。
この三角形パスを仮定します。
const SkPath path = SkPathB