From d59011a44b52cb1440a3dcef700c27856926fffa Mon Sep 17 00:00:00 2001 From: xiaoiver Date: Fri, 24 Jan 2025 22:06:42 +0800 Subject: [PATCH] feat: implement fill-rule for path with libtess #29 --- README.md | 3 + README.zh_CN.md | 3 + __tests__/ssr/path.spec.ts | 20 ++++++ .../ssr/snapshots/path-fill-rule-evenodd.png | Bin 0 -> 4256 bytes .../ssr/snapshots/path-fill-rule-evenodd.svg | 35 ++++++++++ __tests__/unit/serialize.spec.ts | 1 + __tests__/utils.ts | 6 +- packages/core/src/drawcalls/Mesh.ts | 2 +- packages/core/src/shapes/Path.ts | 9 ++- packages/core/src/utils/serialize.ts | 3 +- packages/core/src/utils/tessy.ts | 27 +++++--- packages/site/docs/.vitepress/config/en.js | 4 ++ packages/site/docs/.vitepress/config/zh.js | 4 ++ packages/site/docs/components/FillRule.vue | 65 ++++++++++++++++++ packages/site/docs/components/Harfbuzz.vue | 28 +++++--- packages/site/docs/components/Opentype.vue | 17 ++++- packages/site/docs/example/fill-rule.md | 26 +++++++ packages/site/docs/guide/lesson-013.md | 24 +++++++ packages/site/docs/guide/lesson-016.md | 42 +++++++++-- .../site/docs/public/fill-rule-evenodd.png | Bin 0 -> 35085 bytes packages/site/docs/reference/environment.md | 9 +++ packages/site/docs/zh/example/fill-rule.md | 26 +++++++ packages/site/docs/zh/guide/lesson-013.md | 24 +++++++ packages/site/docs/zh/guide/lesson-016.md | 43 ++++++++++-- .../site/docs/zh/reference/environment.md | 13 +++- 25 files changed, 399 insertions(+), 35 deletions(-) create mode 100644 __tests__/ssr/snapshots/path-fill-rule-evenodd.png create mode 100644 __tests__/ssr/snapshots/path-fill-rule-evenodd.svg create mode 100644 packages/site/docs/components/FillRule.vue create mode 100644 packages/site/docs/example/fill-rule.md create mode 100644 packages/site/docs/public/fill-rule-evenodd.png create mode 100644 packages/site/docs/zh/example/fill-rule.md diff --git a/README.md b/README.md index 3b56651..5428ea7 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,9 @@ pnpm run dev - Experimenting with SDF - Trying to draw fills using some triangulating methods and strokes using polylines + - Support earcut and libtess.js two triangulation schemes + - Handle holes in the path correctly + - Support `fillRule` property - Draw some hand-drawn shapes Lesson 13 - path diff --git a/README.zh_CN.md b/README.zh_CN.md index 0fcfd0a..8699cf3 100644 --- a/README.zh_CN.md +++ b/README.zh_CN.md @@ -160,6 +160,9 @@ pnpm run dev - 尝试使用 SDF 绘制 - 通过三角化绘制填充部分,使用折线绘制描边部分 + - 支持 earcut 和 libtess.js 两种三角化方案 + - 正确处理路径中的孔洞 + - 支持 `fillRule` 属性 - 实现一些手绘风格图形 Lesson 13 - path diff --git a/__tests__/ssr/path.spec.ts b/__tests__/ssr/path.spec.ts index a6c2340..895f70e 100644 --- a/__tests__/ssr/path.spec.ts +++ b/__tests__/ssr/path.spec.ts @@ -100,4 +100,24 @@ describe('Path', () => { 'path-holes-libtess', ); }); + + it('should render fill-rule correctly.', async () => { + const path = new Path({ + d: 'M50 0 L21 90 L98 35 L2 35 L79 90 Z', + fill: '#F67676', + fillRule: 'evenodd', + tessellationMethod: TesselationMethod.LIBTESS, + }); + canvas.appendChild(path); + canvas.render(); + + expect($canvas.getContext('webgl1')).toMatchWebGLSnapshot( + dir, + 'path-fill-rule-evenodd', + ); + expect(exporter.toSVG({ grid: true })).toMatchSVGSnapshot( + dir, + 'path-fill-rule-evenodd', + ); + }); }); diff --git a/__tests__/ssr/snapshots/path-fill-rule-evenodd.png b/__tests__/ssr/snapshots/path-fill-rule-evenodd.png new file mode 100644 index 0000000000000000000000000000000000000000..905c65d8b26a9d55dde2f6d3d86a6970706c4545 GIT binary patch literal 4256 zcmeHLX;4#F6vnnA6@3Xp5jo1;wBlL5qsG5W*6s0(FDN zK`3P^BEvY~2r4#6A&^K~p^Ypmm0%c%X&O?+kc9$afTZWX54cVKs;dkkd>z817jlOC>nOp8I^foYM2h>98{?a_m42|CX zdE2>3MdhZwh2^o&L=M@J78mv9K|V#deD@(gw_5|kUKe3;c>knxVApgQw%(|4C`~H0 zkIp>ecg1-R!3wJNCo%)Z`1v2a7oGvn%MrK?CV2TZGKGh>jb*%6FKChG;Neth{gg`p zRM((O$gyC6@Al9#Yyx24%Jw68Z@Hh`j8^H@ajq*9Hjd&%uKR4wa{0?xg4SW@KFmE* ze>`7TTGM%$cN_WpOytE)itNh%_(lnP-(kMY%0`&I>l(E?q*pBDJK3rwJY7?tr(ydp zZi+@3?>Ovoy?))$i9?B9@s2#Q0)8>t(~@J>%ooEw$~yZz_X|(iM+f;-bZDwO4-fBe zANfv9!!Gb~4-3NQ@YI|QfC+x7mixpYs?`1(E8GT~q>}a?mQ{pfr#(DGfX^E=s@ zsY@)T7jw%xG+E9UmoT-v7d91VIXAq-U}peTa0f!^1O{`Du9+{W=#IJ1VUPs(`#e_x zlIRaa=yJNJ4Vce8xfB=x_SG5^t!!fcR0D#t9=K;qV zgV86sR6&876UmniE|n;rL{>W4GMIcB#30S@HgoylfEVzS;j2u?^3C)pR@l1$C~+<5 zQXtCEQIueJiCDKFP)U0R?e|z=i_wYH>1@4s+EGWymR#^Hn!stF9L19|O#`?&V40mE z!)XYm4v4mYpi-Lx=jhv5oRuiL8Y<@|{57~{j1U6|DSnt?&h4Q*J+RAF+QSNUb)Y?f z^+)hst1(8tGXA+2;*A|-3&0TSO5)4{uwgY{jub0;b#)+?2a>qwUhN}IDvT_H_1)?^ zbLIqJ^)4KPN=SjeA&t?{t@eWx1w{Z(>!nKV3W5Rwi6XFM%fmSX{<-(Y^MN&1MUXcv zP8k@d5k$SfpV7`+I{O63;Z`^{bms{rq;mKc~5mCq*vqZ1#4e3cPIByn|pE!@_M zIi>)!HhNO(p45OKS(S2B{T@}zDRq|*CT>9bipd4zZlCAR02jL6HB?R}xbanahlUpn zq(RRhmb}}#Jb^QP2=-(8%th&eYn-F=7qn3qTQ?%6j_FmL@Lu1jPZ{O9T0YKiV?%S!2u zJy?k|4H|UssZ~R;ptLq$Qj5y%7$f#E zL@}E?me9^0n>If(4w4eb#A^&o^m3JmJIy90`;kc z#&u?WEU~A>pCjuQSLclDc=1)@2s^V-hc=l(?=wLe54oZ@S9^ljX<`cMf{FEfs(xle+#$;Tt;cfLLLjRF44!G`|1|N85`;V1tELKOy5 literal 0 HcmV?d00001 diff --git a/__tests__/ssr/snapshots/path-fill-rule-evenodd.svg b/__tests__/ssr/snapshots/path-fill-rule-evenodd.svg new file mode 100644 index 0000000..d0b90b4 --- /dev/null +++ b/__tests__/ssr/snapshots/path-fill-rule-evenodd.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/__tests__/unit/serialize.spec.ts b/__tests__/unit/serialize.spec.ts index 24c84c8..046f11c 100644 --- a/__tests__/unit/serialize.spec.ts +++ b/__tests__/unit/serialize.spec.ts @@ -234,6 +234,7 @@ describe('Serialize', () => { cullable: true, fill: 'none', fillOpacity: 1, + fillRule: 'nonzero', innerShadowBlurRadius: 0, innerShadowColor: 'black', innerShadowOffsetX: 0, diff --git a/__tests__/utils.ts b/__tests__/utils.ts index b859f69..8e10790 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -2,6 +2,7 @@ import _gl from 'gl'; import { createCanvas } from 'canvas'; import { JSDOM } from 'jsdom'; import { XMLSerializer } from '@xmldom/xmldom'; +import GraphemeSplitter from 'grapheme-splitter'; import { Adapter } from '../packages/core/src/environment'; export function sleep(n: number) { @@ -84,7 +85,10 @@ export const NodeJSAdapter: Adapter = { getXMLSerializer: () => new XMLSerializer(), getDOMParser: () => null, setCursor: () => {}, - splitGraphemes: (s: string) => [...s], + splitGraphemes: (s: string) => { + const splitter = new GraphemeSplitter(); + return splitter.splitGraphemes(s); + }, requestAnimationFrame: (callback: FrameRequestCallback) => { const currTime = new Date().getTime(); const timeToCall = Math.max(0, 16 - (currTime - lastTime)); diff --git a/packages/core/src/drawcalls/Mesh.ts b/packages/core/src/drawcalls/Mesh.ts index aa6caf4..dc039c4 100644 --- a/packages/core/src/drawcalls/Mesh.ts +++ b/packages/core/src/drawcalls/Mesh.ts @@ -139,7 +139,7 @@ export class Mesh extends Drawcall { this.points = points; // const err = deviation(vertices, holes, dimensions, indices); } else if (tessellationMethod === TesselationMethod.LIBTESS) { - const newPoints = triangulate(rawPoints); + const newPoints = triangulate(rawPoints, (instance as Path).fillRule); this.indexBufferData = new Uint32Array( new Array(newPoints.length / 2).fill(undefined).map((_, i) => i), ); diff --git a/packages/core/src/shapes/Path.ts b/packages/core/src/shapes/Path.ts index 172b859..14f6f3f 100644 --- a/packages/core/src/shapes/Path.ts +++ b/packages/core/src/shapes/Path.ts @@ -16,6 +16,12 @@ export interface PathAttributes extends ShapeAttributes { */ d: string; + /** + * The fill rule to use for rendering the path. + * @see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule + */ + fillRule?: CanvasFillRule; + /** * The tesselation method to use for rendering the path. */ @@ -59,10 +65,11 @@ export function PathWrapper(Base: TBase) { constructor(attributes: Partial = {}) { super(attributes); - const { d, tessellationMethod } = attributes; + const { d, tessellationMethod, fillRule } = attributes; this.d = d; this.tessellationMethod = tessellationMethod ?? TesselationMethod.EARCUT; + this.fillRule = fillRule ?? 'nonzero'; } get d() { diff --git a/packages/core/src/utils/serialize.ts b/packages/core/src/utils/serialize.ts index a00c520..8376eb0 100644 --- a/packages/core/src/utils/serialize.ts +++ b/packages/core/src/utils/serialize.ts @@ -117,7 +117,7 @@ const rectAttributes = [ 'dropShadowBlurRadius', ] as const; const polylineAttributes = ['points'] as const; -const pathAttributes = ['d'] as const; +const pathAttributes = ['d', 'fillRule'] as const; const textAttributes = [ 'x', 'y', @@ -161,6 +161,7 @@ const defaultValues = { dropShadowOffsetX: 0, dropShadowOffsetY: 0, dropShadowBlurRadius: 0, + fillRule: 'nonzero', }; type CommonAttributeName = ( diff --git a/packages/core/src/utils/tessy.ts b/packages/core/src/utils/tessy.ts index 84a7bbf..e9f9b0c 100644 --- a/packages/core/src/utils/tessy.ts +++ b/packages/core/src/utils/tessy.ts @@ -1,16 +1,9 @@ /** * @see https://github.com/brendankenny/libtess.js/blob/gh-pages/examples/osm/triangulate.js + * @see https://github.com/ShukantPal/pixi-essentials/blob/049c67d0126ca771e026a04702a63fee1ce25d16/packages/svg/src/utils/buildPath.ts#L12 */ import libtess from 'libtess'; -const tessy = new libtess.GluTesselator(); -// tessy.gluTessProperty(libtess.gluEnum.GLU_TESS_WINDING_RULE, libtess.windingRule.GLU_TESS_WINDING_POSITIVE); -tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); -tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); -tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); -tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); -tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); - // function called for each vertex of tesselator output function vertexCallback(data, polyVertArray) { // console.log(data[0], data[1]); @@ -36,7 +29,23 @@ function edgeCallback(flag) { // console.log('edge flag: ' + flag); } -export function triangulate(contours: [number, number][][]) { +export function triangulate( + contours: [number, number][][], + fillRule: CanvasFillRule, +) { + const tessy = new libtess.GluTesselator(); + tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + fillRule === 'evenodd' + ? libtess.windingRule.GLU_TESS_WINDING_ODD + : libtess.windingRule.GLU_TESS_WINDING_NONZERO, + ); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, vertexCallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_BEGIN, begincallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_ERROR, errorcallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, combinecallback); + tessy.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, edgeCallback); + // libtess will take 3d verts and flatten to a plane for tesselation // since only doing 2d tesselation here, provide z=1 normal to skip // iterating over verts only to get the same answer. diff --git a/packages/site/docs/.vitepress/config/en.js b/packages/site/docs/.vitepress/config/en.js index 191f680..12e9732 100644 --- a/packages/site/docs/.vitepress/config/en.js +++ b/packages/site/docs/.vitepress/config/en.js @@ -168,6 +168,10 @@ export const en = defineConfig({ text: 'Draw holes in Path', link: 'holes', }, + { + text: 'Fill rule', + link: 'fill-rule', + }, { text: 'Use SDF to draw text', link: 'sdf-text', diff --git a/packages/site/docs/.vitepress/config/zh.js b/packages/site/docs/.vitepress/config/zh.js index d6f5b40..8fb9e16 100644 --- a/packages/site/docs/.vitepress/config/zh.js +++ b/packages/site/docs/.vitepress/config/zh.js @@ -138,6 +138,10 @@ export const zh = defineConfig({ text: '绘制 Path 中的孔洞', link: 'holes', }, + { + text: '填充规则', + link: 'fill-rule', + }, { text: '使用 SDF 绘制文本', link: 'sdf-text', diff --git a/packages/site/docs/components/FillRule.vue b/packages/site/docs/components/FillRule.vue new file mode 100644 index 0000000..604e5f8 --- /dev/null +++ b/packages/site/docs/components/FillRule.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/site/docs/components/Harfbuzz.vue b/packages/site/docs/components/Harfbuzz.vue index 4f56c90..0baadac 100644 --- a/packages/site/docs/components/Harfbuzz.vue +++ b/packages/site/docs/components/Harfbuzz.vue @@ -1,5 +1,5 @@ diff --git a/packages/site/docs/example/fill-rule.md b/packages/site/docs/example/fill-rule.md new file mode 100644 index 0000000..86ef09f --- /dev/null +++ b/packages/site/docs/example/fill-rule.md @@ -0,0 +1,26 @@ +--- +publish: false +--- + + + +Same as [fill-rule] attribute in SVG, the left is `nonzero`, the right is `evenodd`. + + + +Since [earcut] is not supported self-intersecting paths well, we use [libtess.js] to tesselate the path. + +```ts +const star = new Path({ + d: 'M150 0 L121 90 L198 35 L102 35 L179 90 Z', + fill: '#F67676', + fillRule: 'evenodd', + tessellationMethod: TesselationMethod.LIBTESS, // instead of earcut +}); +``` + +[fill-rule]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule +[earcut]: https://github.com/mapbox/earcut +[libtess.js]: https://github.com/brendankenny/libtess.js diff --git a/packages/site/docs/guide/lesson-013.md b/packages/site/docs/guide/lesson-013.md index 1d17395..da17041 100644 --- a/packages/site/docs/guide/lesson-013.md +++ b/packages/site/docs/guide/lesson-013.md @@ -12,6 +12,7 @@ head: # Lesson 13 - Drawing a Path & Hand Drawn Styles @@ -418,6 +419,27 @@ You can also reverse the clockwise direction in your definition, for example: [D +### Fill rule {#fill-rule} + +The [fill-rule] in SVG is used to determine the fill area of a Path. In the example below, the left one uses nonzero rule, while the right one uses evenodd rule. + + + +Taking a point in the center hollow area as an example, the ray intersects with the shape an even number of times, therefore it is determined to be outside the shape and does not need to be filled. See details at [how does fill-rule="evenodd" work on a star SVG]。 + +![fill-rule evenodd](/fill-rule-evenodd.png) + +Since earcut doesn't support self-intersecting paths, we use libtess.js for path triangulation. + +```ts +tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + fillRule === 'evenodd' + ? libtess.windingRule.GLU_TESS_WINDING_ODD + : libtess.windingRule.GLU_TESS_WINDING_NONZERO, +); +``` + ## Bounding box and picking {#bounding-box-picking} The bounding box can be estimated in the same way as in the previous lesson for polyline. We focus on the implementation of how to determine if a point is inside a Path. @@ -742,3 +764,5 @@ export function exportRough( [OffscreenCanvas]: /guide/lesson-011#offscreen-canvas [PickingPlugin]: /guide/lesson-006#picking-plugin [Draw a hollow circle in SVG]: https://stackoverflow.com/questions/8193675/draw-a-hollow-circle-in-svg +[fill-rule]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule +[how does fill-rule="evenodd" work on a star SVG]: https://stackoverflow.com/a/46145333/4639324 diff --git a/packages/site/docs/guide/lesson-016.md b/packages/site/docs/guide/lesson-016.md index 6c01a98..5993646 100644 --- a/packages/site/docs/guide/lesson-016.md +++ b/packages/site/docs/guide/lesson-016.md @@ -30,7 +30,7 @@ Below we'll show examples of rendering text using opentype.js and harfbuzzjs, bo ### opentype.js {#opentypejs} -opentype.js provides a `getPath` method that completes Shaping and obtains SVG commands given text content, position, and font size. +opentype.js provides the `getPath` method, which completes Shaping and obtains SVG [path-commands] given text content, position, and font size. ```ts opentype.load('fonts/Roboto-Black.ttf', function (err, font) { @@ -43,12 +43,46 @@ opentype.load('fonts/Roboto-Black.ttf', function (err, font) { ### harfbuzzjs {#harfbuzzjs} +First initialize harfbuzzjs WASM using Vite's ?init syntax. Then load the font file and create a font object. + ```ts import init from 'harfbuzzjs/hb.wasm?init'; -import hbjs, { HBBlob, HBFace, HBFont, HBHandle } from 'harfbuzzjs/hbjs.js'; +import hbjs from 'harfbuzzjs/hbjs.js'; + +const instance = await init(); +hb = hbjs(instance); + +const data = await ( + await window.fetch('/fonts/NotoSans-Regular.ttf') +).arrayBuffer(); +blob = hb.createBlob(data); +face = hb.createFace(blob, 0); +font = hb.createFont(face); +font.setScale(32, 32); // Set font size +``` + +Then create a buffer object and add text content. As mentioned before, harfbuzz doesn't handle BiDi, so we need to manually set the text direction. Finally, call hb.shape method to perform Shaping calculation. + +```ts +buffer = hb.createBuffer(); +buffer.addText('Hello, world!'); +buffer.guessSegmentProperties(); +// TODO: use BiDi +// buffer.setDirection(segment.direction); + +hb.shape(font, buffer); +const result = buffer.json(font); +``` -init().then((instance) => { - const hb = hbjs(instance); +Now we have the glyph data, and we can use Path to draw it + +```ts +result.forEach(function (x) { + const d = font.glyphToPath(x.g); + const path = new Path({ + d, + fill: '#F67676', + }); }); ``` diff --git a/packages/site/docs/public/fill-rule-evenodd.png b/packages/site/docs/public/fill-rule-evenodd.png new file mode 100644 index 0000000000000000000000000000000000000000..dd8d66ac8972d8661bfd047e14adf7b8931b7bd9 GIT binary patch literal 35085 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 a^xBDSmfa!52aIYG<;NP2${sv__x}K{%TWdZ literal 0 HcmV?d00001 diff --git a/packages/site/docs/reference/environment.md b/packages/site/docs/reference/environment.md index 4e66192..aeff1c1 100644 --- a/packages/site/docs/reference/environment.md +++ b/packages/site/docs/reference/environment.md @@ -116,6 +116,15 @@ splitGraphemes: (s: string) => string[]; - In browser and WebWorker environments: uses [Intl.Segmenter] - In Node.js environment: can use [grapheme-splitter] +```ts +import GraphemeSplitter from 'grapheme-splitter'; + +splitGraphemes: (s: string) => { + const splitter = new GraphemeSplitter(); + return splitter.splitGraphemes(s); +}, +``` + ## requestAnimationFrame Used for camera animations. diff --git a/packages/site/docs/zh/example/fill-rule.md b/packages/site/docs/zh/example/fill-rule.md new file mode 100644 index 0000000..8a88d1d --- /dev/null +++ b/packages/site/docs/zh/example/fill-rule.md @@ -0,0 +1,26 @@ +--- +publish: false +--- + + + +和 SVG 中的 [fill-rule] 属性一致,左边是 `nonzero`,右边是 `evenodd`. + + + +由于 [earcut] 不支持自相交路径,我们使用 [libtess.js] 来三角化路径。 + +```ts +const star = new Path({ + d: 'M150 0 L121 90 L198 35 L102 35 L179 90 Z', + fill: '#F67676', + fillRule: 'evenodd', + tessellationMethod: TesselationMethod.LIBTESS, // instead of earcut +}); +``` + +[fill-rule]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule +[earcut]: https://github.com/mapbox/earcut +[libtess.js]: https://github.com/brendankenny/libtess.js diff --git a/packages/site/docs/zh/guide/lesson-013.md b/packages/site/docs/zh/guide/lesson-013.md index f49f981..f353a33 100644 --- a/packages/site/docs/zh/guide/lesson-013.md +++ b/packages/site/docs/zh/guide/lesson-013.md @@ -12,6 +12,7 @@ head: # 课程 13 - 绘制 Path & 手绘风格 @@ -418,6 +419,27 @@ M0 0 L100 0 L100 100 L0 100 Z M50 50 L50 75 L75 75 L75 50 Z M25 25 L25 +### 填充规则 {#fill-rule} + +SVG 中的 [fill-rule] 用来判定 Path 的填充区域,下面的例子中左边是 `nonzero`,右边是 `evenodd`。 + + + +以中心挖空区域中的点为例,作射线与图形的交点为偶数,因此判定为图形外部,无需填充。详见 [how does fill-rule="evenodd" work on a star SVG]。 + +![fill-rule evenodd](/fill-rule-evenodd.png) + +由于 earcut 不支持自相交路径,我们使用 libtess.js 来三角化路径。 + +```ts +tessy.gluTessProperty( + libtess.gluEnum.GLU_TESS_WINDING_RULE, + fillRule === 'evenodd' + ? libtess.windingRule.GLU_TESS_WINDING_ODD + : libtess.windingRule.GLU_TESS_WINDING_NONZERO, +); +``` + ## 包围盒与拾取 {#bounding-box-picking} 包围盒可以沿用上一节课针对折线的估计方式。我们重点关注如何判定点是否在 Path 内的实现。 @@ -743,3 +765,5 @@ export function exportRough( [OffscreenCanvas]: /zh/guide/lesson-011#offscreen-canvas [PickingPlugin]: /zh/guide/lesson-006#picking-plugin [Draw a hollow circle in SVG]: https://stackoverflow.com/questions/8193675/draw-a-hollow-circle-in-svg +[fill-rule]: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule +[how does fill-rule="evenodd" work on a star SVG]: https://stackoverflow.com/a/46145333/4639324 diff --git a/packages/site/docs/zh/guide/lesson-016.md b/packages/site/docs/zh/guide/lesson-016.md index b9f4879..291a935 100644 --- a/packages/site/docs/zh/guide/lesson-016.md +++ b/packages/site/docs/zh/guide/lesson-016.md @@ -30,7 +30,7 @@ import TeXMath from '../../components/TeXMath.vue'; ### opentype.js {#opentypejs} -opentype.js 提供了 `getPath` 方法,给定文本内容、位置和字体大小,就可以完成 Shaping 并获取 SVG commands。 +opentype.js 提供了 `getPath` 方法,给定文本内容、位置和字体大小,就可以完成 Shaping 并获取 SVG [path-commands],其中包含 `M`、`L`、`C`、`Q`、`Z` 命令,我们将它转换为 Path 的 `d` 属性。 ```ts opentype.load('fonts/Roboto-Black.ttf', function (err, font) { @@ -43,12 +43,46 @@ opentype.load('fonts/Roboto-Black.ttf', function (err, font) { ### harfbuzzjs {#harfbuzzjs} +首先初始化 harfbuzzjs WASM,这里使用 Vite 的 ?init 语法。然后加载字体文件,并创建 font 对象。 + ```ts import init from 'harfbuzzjs/hb.wasm?init'; -import hbjs, { HBBlob, HBFace, HBFont, HBHandle } from 'harfbuzzjs/hbjs.js'; +import hbjs from 'harfbuzzjs/hbjs.js'; + +const instance = await init(); +hb = hbjs(instance); + +const data = await ( + await window.fetch('/fonts/NotoSans-Regular.ttf') +).arrayBuffer(); +blob = hb.createBlob(data); +face = hb.createFace(blob, 0); +font = hb.createFont(face); +font.setScale(32, 32); // 设置字体大小 +``` + +然后创建一个 buffer 对象,并添加文本内容。我们之前提过 harfbuzz 并不处理 BiDi,因此这里需要手动设置文本方向。最后调用 hb.shape 方法进行 Shaping 计算。 + +```ts +buffer = hb.createBuffer(); +buffer.addText('Hello, world!'); +buffer.guessSegmentProperties(); +// TODO: use BiDi +// buffer.setDirection(segment.direction); + +hb.shape(font, buffer); +const result = buffer.json(font); +``` -init().then((instance) => { - const hb = hbjs(instance); +此时我们就得到字形数据了,随后可以使用 Path 绘制 + +```ts +result.forEach(function (x) { + const d = font.glyphToPath(x.g); + const path = new Path({ + d, + fill: '#F67676', + }); }); ``` @@ -190,3 +224,4 @@ WebFont.load({ [MathJax]: https://github.com/mathjax/MathJax-src [LaTeX in motion-canvas]: https://github.com/motion-canvas/motion-canvas/issues/190 [课程 10 - 从 SVGElement 到序列化节点]: /zh/guide/lesson-010#svgelement-to-serialized-node +[path-commands]: https://github.com/opentypejs/opentype.js?tab=readme-ov-file#path-commands diff --git a/packages/site/docs/zh/reference/environment.md b/packages/site/docs/zh/reference/environment.md index 2846aa9..ca18192 100644 --- a/packages/site/docs/zh/reference/environment.md +++ b/packages/site/docs/zh/reference/environment.md @@ -100,12 +100,23 @@ setCursor: (canvas: Canvas, cursor: Cursor) => void; ## splitGraphemes -将字符串分割成单个字符,考虑复合字符。在浏览器和 WebWorker 中使用 [Intl.Segmenter],在 Node.js 中可以使用 [grapheme-splitter]。 +将字符串分割成单个字符,考虑复合字符。在浏览器和 WebWorker 中使用 [Intl.Segmenter] ```ts splitGraphemes: (s: string) => string[]; ``` +在 Node.js 中可以使用 [grapheme-splitter]。 + +```ts +import GraphemeSplitter from 'grapheme-splitter'; + +splitGraphemes: (s: string) => { + const splitter = new GraphemeSplitter(); + return splitter.splitGraphemes(s); +}, +``` + ## requestAnimationFrame 执行相机动画时会使用到。在浏览器环境和 WebWorker 中使用 [requestAnimationFrame],在 Node.js 环境中使用 `setTimeout`。