@glyphcss/fonts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Layoutit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @glyphcss/fonts
2
+
3
+ Turn **fonts + text into extruded 3D polygon meshes** for [glyphcss](https://github.com/apresmoi/glyphcss). Framework-agnostic: it returns plain `Polygon[]`, so the same call works in the vanilla, React, and Vue renderers — no per-framework wrappers.
4
+
5
+ ```bash
6
+ pnpm add @glyphcss/fonts glyphcss
7
+ ```
8
+
9
+ ```ts
10
+ import { loadGoogleFont, textPolygons } from "@glyphcss/fonts";
11
+ import { createGlyphScene, createGlyphOrthographicCamera } from "glyphcss";
12
+
13
+ const font = await loadGoogleFont({ /* FontEntry from listGoogleFonts() */ }, 700);
14
+ const polygons = textPolygons(font, "Hello", { depth: 24, profile: "bevel" });
15
+
16
+ const scene = createGlyphScene(host, { camera: createGlyphOrthographicCamera({ rotX: 28, zoom: 0.06 }) });
17
+ scene.add(polygons);
18
+ ```
19
+
20
+ ## Two layers
21
+
22
+ **Pure** (no browser globals — runs in Node too):
23
+
24
+ - `parseFont(bytes)` → `ParsedFont` — a small, dependency-free TrueType (`glyf`) reader: sfnt tables → glyph outlines + advance widths.
25
+ - `textPolygons(font, text, options)` → `Polygon[]` — triangulates caps (holes included), builds the depth profile, extrudes, and lays glyphs out by advance width.
26
+ - `composeText(font, text, options)` → `Polygon[]` — the full WordArt composer on top of `textPolygons`: multi-line text, alignment, line height, glyph scale, underline / strike bars, envelope warps, and a layered two-color look.
27
+
28
+ **Browser** (uses `fetch`):
29
+
30
+ - `listGoogleFonts()` → every Google font (via the Fontsource API).
31
+ - `googleFontUrl(entry, weight)` / `loadFont(url)` / `loadGoogleFont(entry, weight)`.
32
+
33
+ ## `textPolygons` options
34
+
35
+ | Option | Default | Notes |
36
+ |---|---|---|
37
+ | `size` | `100` | Cap-em size in world units. |
38
+ | `depth` | `size * 0.2` | Extrusion depth along world Z. |
39
+ | `profile` | `"flat"` | `"flat"` slab · `"round"` bullnose · `"bevel"` chamfered edge. |
40
+ | `curveSteps` | `6` | Bézier flattening — higher is smoother, more polygons. |
41
+ | `letterSpacing` | `0` | Extra space between glyphs. |
42
+ | `color` / `sideColor` | gold | Cap and wall colors (sideColor defaults to a darker shade). |
43
+ | `profileSegments` | `6` | Ring count for round/bevel edges. |
44
+
45
+ ## `composeText` — WordArt composer
46
+
47
+ `composeText(font, text, options)` is the full composer (`\n` starts a new line). The options group into five concerns instead of one flat bag:
48
+
49
+ ```ts
50
+ import { composeText, resolveFace } from "@glyphcss/fonts";
51
+
52
+ const polygons = composeText(font, "Glyph\nCSS", {
53
+ // 1 · type & layout
54
+ size: 100, depth: 24, align: "center", scale: [1, 1],
55
+ letterSpacing: 0, lineHeight: 1.25, underline: false, strike: false,
56
+ warp: { shape: "arch", amount: 0.6 }, simplify: 0,
57
+
58
+ // 2 · cross-section / edge profile (one union)
59
+ profile: { edge: "bevel", coverage: "front" },
60
+
61
+ // 3 · per-face material — one `Face` shape for all three
62
+ faces: {
63
+ front: resolveFace({ kind: "gradient", from: "#ffe14d", to: "#ff7a1a" }),
64
+ sides: { color: "#7c4a12" },
65
+ back: { color: "#3a86ff" },
66
+ },
67
+
68
+ // 4 · outline
69
+ outline: { color: "#1a1a2e", width: 3 },
70
+ });
71
+ ```
72
+
73
+ | Group | Options |
74
+ |---|---|
75
+ | **Layout** | `size` · `depth` (0 = flat slab, no edges) · `curveSteps` · `letterSpacing` · `lineHeight` · `align` · `scale: [x,y]` · `underline` · `strike` · `warp` · `simplify` |
76
+ | **`profile`** | `"flat"` · `{ edge: "bevel"\|"round", raised?, segments? }` · `{ curve: CubicBezier, segments? }` |
77
+ | **`faces`** | `{ front?, sides?, back? }` · a single `Face` · `FaceStop[]` |
78
+ | **`outline`** | `{ color, width }` — a colored halo around the front face |
79
+
80
+ - **`profile` (shape)** and **`faces` (color)** are independent functions of the same depth axis `t ∈ [0,1]` (0 = front, 1 = back). `edge` bevels/rounds the edges (`raised` flips a round to a convex dome); `curve` is a custom edge from a CSS `cubic-bezier` easing.
81
+ - **`Face`** = `{ color?, texture?, tile? }`. `texture` is an already-rendered URL/data-URL UV-mapped across the whole word; `tile` repeats it every N units (blocks) vs stretching (gradients/photos).
82
+ - **`faces` resolves to material stops down the axis** — each polygon takes the nearest stop to its depth:
83
+ - `{ front, sides, back }` → 3 stops at `{0, .5, 1}` (omit `sides` → the front rounds straight into the back, **no side band**).
84
+ - a single `Face` → one material for the whole solid.
85
+ - `FaceStop[]` (`Face & { at }`) → **N** materials distributed down the axis.
86
+ - **Flat drop shadow** — `depth: 0` + `faces.back.offset: [x, y]` with a distinct `back.color`.
87
+
88
+ ### Fills — `resolveFace` & `makeFillTexture` (browser)
89
+
90
+ `composeText` is pure and takes already-rendered textures. The browser helpers turn a high-level fill into a `Face`:
91
+
92
+ ```ts
93
+ resolveFace({ kind: "gradient", from: "#ffe14d", to: "#ff7a1a", angle: 270 })
94
+ // → { color?, texture: "data:image/png;…" }
95
+ ```
96
+
97
+ `FaceFillSpec` (the `kind`): `"solid"` · `"gradient"` (`from`, `to`, `angle?`) · `"rainbow"` (`angle?`) · `"texture"` (`url`, `tile?`) · `"image"` (`src`). `makeFillTexture(FillSpec)` is the lower-level canvas painter if you want the data URL directly.
98
+
99
+ ## Scope / limitations
100
+
101
+ This is a focused reader, not a full font library:
102
+
103
+ - **TrueType (`.ttf`, `glyf`) only.** CFF/OpenType (`.otf`, "OTTO") is rejected with a clear error. Google Fonts ship TrueType, so this covers the common case.
104
+ - **Uncompressed sfnt only** — woff/woff2 are not unpacked (the Google Fonts loader fetches raw `.ttf`).
105
+ - No shaping, kerning, ligatures, or variable-font axes — each character maps to one glyph plus its advance width.
106
+ - Script fonts with heavily self-overlapping contours can leave minor triangulation artifacts.
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var St=Object.create;var q=Object.defineProperty;var Ut=Object.getOwnPropertyDescriptor;var wt=Object.getOwnPropertyNames;var Et=Object.getPrototypeOf,kt=Object.prototype.hasOwnProperty;var vt=(t,n)=>{for(var e in n)q(t,e,{get:n[e],enumerable:!0})},lt=(t,n,e,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of wt(n))!kt.call(t,o)&&o!==e&&q(t,o,{get:()=>n[o],enumerable:!(r=Ut(n,o))||r.enumerable});return t};var Ot=(t,n,e)=>(e=t!=null?St(Et(t)):{},lt(n||!t||!t.__esModule?q(e,"default",{value:t,enumerable:!0}):e,t)),It=t=>lt(q({},"__esModule",{value:!0}),t);var Zt={};vt(Zt,{composeText:()=>xt,cssCubicBezier:()=>ot,googleFontUrl:()=>ct,listGoogleFonts:()=>Ct,loadFont:()=>ut,loadGoogleFont:()=>Ft,makeFillTexture:()=>tt,parseFont:()=>N,pickWeight:()=>it,resolveFace:()=>Pt,textPolygons:()=>bt});module.exports=It(Zt);function Tt(t,n){return String.fromCharCode(t.getUint8(n),t.getUint8(n+1),t.getUint8(n+2),t.getUint8(n+3))}function N(t,n=8){let e=t instanceof Uint8Array?t:new Uint8Array(t),r=new DataView(e.buffer,e.byteOffset,e.byteLength),o=r.getUint32(0);if(o===1330926671)throw new Error("parseFont: CFF/OpenType (.otf) outlines are not supported \u2014 use a TrueType (.ttf) font");if(o!==65536&&o!==1953658213)throw new Error(`parseFont: not a TrueType font (sfnt 0x${o.toString(16)})`);let c=r.getUint16(4),i=new Map;for(let p=0;p<c;p++){let x=12+p*16;i.set(Tt(r,x),{offset:r.getUint32(x+8),length:r.getUint32(x+12)})}let s=p=>{let x=i.get(p);if(!x)throw new Error(`parseFont: missing required '${p}' table`);return x.offset},u=s("head"),m=r.getUint16(u+18),f=r.getInt16(u+50),a=r.getUint16(s("maxp")+4),l=s("hhea"),h=r.getInt16(l+4),d=r.getInt16(l+6),b=r.getInt16(l+8),w=r.getUint16(l+34),z=s("hmtx"),A=p=>{let x=p<w?p:w-1;return r.getUint16(z+x*4)},G=s("loca"),V=s("glyf"),D=p=>f===0?[V+r.getUint16(G+p*2)*2,V+r.getUint16(G+(p+1)*2)*2]:[V+r.getUint32(G+p*4),V+r.getUint32(G+(p+1)*4)],$=zt(r,s("cmap")),L=(p,x=0)=>{if(p<0||p>=a||x>8)return[];let[T,v]=D(p);if(T>=v)return[];let g=T,O=r.getInt16(g);if(g+=10,O>=0){let E=[];for(let F=0;F<O;F++)E.push(r.getUint16(g)),g+=2;let I=E.length?E[E.length-1]+1:0,M=r.getUint16(g);g+=2+M;let C=new Uint8Array(I);for(let F=0;F<I;){let U=r.getUint8(g++);if(C[F++]=U,U&8){let W=r.getUint8(g++);for(;W-- >0&&F<I;)C[F++]=U}}let y=(F,U)=>{let W=new Array(I),X=0;for(let H=0;H<I;H++){let nt=C[H];if(nt&F){let at=r.getUint8(g++);X+=nt&U?at:-at}else nt&U||(X+=r.getInt16(g),g+=2);W[H]=X}return W},S=y(2,16),Y=y(4,32),P=[],k=0;for(let F of E){let U=[],W=[];for(let X=k;X<=F;X++)U.push([S[X],Y[X]]),W.push((C[X]&1)!==0);U.length&&P.push({pts:U,on:W}),k=F+1}return P}let B=[],R=!0;for(;R;){let E=r.getUint16(g),I=r.getUint16(g+2);g+=4;let M=0,C=0;E&1?(M=r.getInt16(g),C=r.getInt16(g+2),g+=4):(M=r.getInt8(g),C=r.getInt8(g+1),g+=2);let y=U=>r.getInt16(U)/16384,S=1,Y=0,P=0,k=1;E&8?(S=k=y(g),g+=2):E&64?(S=y(g),k=y(g+2),g+=4):E&128&&(S=y(g),Y=y(g+2),P=y(g+4),k=y(g+6),g+=8);let F=(E&2)!==0;for(let U of L(I,x+1))B.push({on:U.on,pts:U.pts.map(([W,X])=>[S*W+P*X+(F?M:0),Y*W+k*X+(F?C:0)])});R=(E&32)!==0}return B};return{unitsPerEm:m,ascender:h,descender:d,lineGap:b,glyph:(p,x=n)=>{let T=$(p),v=Math.max(1,Math.round(x));return{contours:L(T).map(O=>Bt(O.pts,O.on,v)).filter(O=>O.length>=2),advanceWidth:A(T)}}}}function zt(t,n){let e=t.getUint16(n+2),r=-1,o=-1;for(let i=0;i<e;i++){let s=n+4+i*8,u=t.getUint16(s),m=t.getUint16(s+2),f=t.getUint32(s+4),a=t.getUint16(n+f),l=0;if(a===12)l+=4;else if(a===4)l+=2;else continue;u===3&&(m===1||m===10)&&(l+=1),u===0&&(l+=1),l>o&&(o=l,r=n+f)}if(r<0)throw new Error("parseFont: no supported cmap subtable (need format 4 or 12)");return t.getUint16(r)===12?At(t,r):Wt(t,r)}function Wt(t,n){let e=t.getUint16(n+6),r=e/2,o=n+14,c=o+e+2,i=c+e,s=i+e;return u=>{if(u>65535)return 0;for(let m=0;m<r;m++){if(t.getUint16(o+m*2)<u)continue;if(t.getUint16(c+m*2)>u)return 0;let f=t.getInt16(i+m*2),a=t.getUint16(s+m*2);if(a===0)return u+f&65535;let l=t.getUint16(c+m*2),h=s+m*2+a+(u-l)*2,d=t.getUint16(h);return d===0?0:d+f&65535}return 0}}function At(t,n){let e=t.getUint32(n+12),r=n+16;return o=>{for(let c=0;c<e;c++){let i=r+c*12,s=t.getUint32(i),u=t.getUint32(i+4);if(o<s)return 0;if(o<=u)return t.getUint32(i+8)+(o-s)}return 0}}function Bt(t,n,e){let r=t.length;if(r<2)return t.slice();let o=[],c=[];for(let a=0;a<r;a++){o.push(t[a]),c.push(n[a]);let l=(a+1)%r;!n[a]&&!n[l]&&(o.push([(t[a][0]+t[l][0])/2,(t[a][1]+t[l][1])/2]),c.push(!0))}let i=c.indexOf(!0);if(i<0){let a=o.length;o.unshift([(o[a-1][0]+o[0][0])/2,(o[a-1][1]+o[0][1])/2]),c.unshift(!0),i=0}let s=o.length,u=[o[i]],m=o[i],f=1;for(;f<=s;){let a=(i+f)%s;if(c[a])u.push(o[a]),m=o[a],f+=1;else{let l=o[a],h=o[(i+f+1)%s];for(let d=1;d<=e;d++){let b=d/e,w=1-b;u.push([w*w*m[0]+2*w*b*l[0]+b*b*h[0],w*w*m[1]+2*w*b*l[1]+b*b*h[1]])}m=h,f+=2}}if(u.length>1){let a=u[0],l=u[u.length-1];Math.hypot(a[0]-l[0],a[1]-l[1])<1e-6&&u.pop()}return u}var ht=Ot(require("earcut"),1);function ot([t,n,e,r]){let o=3*t,c=3*(e-t)-o,i=1-o-c,s=3*n,u=3*(r-n)-s,m=1-s-u,f=h=>((i*h+c)*h+o)*h,a=h=>((m*h+u)*h+s)*h,l=h=>(3*i*h+2*c)*h+o;return h=>{let d=Math.min(1,Math.max(0,h)),b=d;for(let w=0;w<8;w++){let z=f(b)-d;if(Math.abs(z)<1e-6)break;let A=l(b);if(Math.abs(A)<1e-6)break;b-=z/A}return a(Math.min(1,Math.max(0,b)))}}var Rt=(t,n,e)=>(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0]);function Gt(t,n){let e=t.length;if(e<3||new Set(t).size!==e)return!1;let r=0;for(let o=0;o<e;o++){let c=Rt(n(t[o]),n(t[(o+1)%e]),n(t[(o+2)%e]));if(Math.abs(c)<1e-9)continue;let i=c>0?1:-1;if(r===0)r=i;else if(i!==r)return!1}return!0}function Vt(t,n,e,r){let o=a=>{let l=a.indexOf(e);if(l<0)return null;if(a[(l+1)%a.length]===r)return[e,r];let h=a.indexOf(r);return h>=0&&a[(h+1)%a.length]===e?[r,e]:null},c=o(t),i=o(n);if(!c||!i||c[0]===i[0])return null;let[s,u]=c,m=(a,l,h)=>{let d=[],b=a.indexOf(l);for(let w=0;w<a.length&&(d.push(a[b]),a[b]!==h);w++)b=(b+1)%a.length;return d},f=m(t,u,s).concat(m(n,s,u).slice(1,-1));return f.length>=3?f:null}function Yt(t,n){let e=i=>[t[i*2],t[i*2+1]],r=[];for(let i=0;i<n.length;i+=3)r.push([n[i],n[i+1],n[i+2]]);let o=t.length/2+1,c=!0;for(;c;){c=!1;let i=new Map;for(let s=0;s<r.length;s++){let u=r[s];if(u)for(let m=0;m<u.length;m++){let f=u[m],a=u[(m+1)%u.length],l=f<a?f*o+a:a*o+f,h=i.get(l);h||i.set(l,h=[]),h.push(s)}}for(let[s,u]of i){if(u.length!==2)continue;let[m,f]=u,a=r[m],l=r[f];if(!a||!l)continue;let h=Math.floor(s/o),d=s%o,b=Vt(a,l,h,d);b&&Gt(b,e)&&(r[m]=b,r[f]=null,c=!0)}}return r.filter(i=>i!==null)}var Xt=(t,n)=>[-t[1],t[0],n];function K(t,n){let{profile:e,profileSegments:r,maxInset:o}=n,c=n.layered??!1,i=n.zOffset??0,s=c?Math.max(n.depth,1):n.depth,u=i+s/2,m=i-s/2,f=Dt(e,u,m,s,r,o,n.roundConvex??!1,n.profileBezier),a=[],l=[0,0],h=n.backOffset??l,d=(p,x,T)=>Xt([p[0]+T[0],p[1]+T[1]],x),b=n.faceUvBounds,w=b?Math.max(b.maxX-b.minX,1e-6):1,z=b?Math.max(b.maxY-b.minY,1e-6):1,A=(p,x)=>x>0?[(p[0]-b.minX)/x,(p[1]-b.minY)/x]:[Math.min(1,Math.max(0,(p[0]-b.minX)/w)),Math.min(1,Math.max(0,(p[1]-b.minY)/z))],G={s:"repeat",t:"repeat"},V=n.outlineColor?Math.max(0,n.outlineWidth??0):0,D=n.stops.length?[...n.stops].sort((p,x)=>p.at-x.at):[{at:.5}],$=p=>{let x=D[0],T=1/0;for(let v of D){let g=Math.abs(v.at-p);g<=T&&(T=g,x=v)}return x},L=p=>s>0?Math.min(1,Math.max(0,(u-p)/s)):0,j=f.reduce((p,x)=>Math.max(p,x.inset),0);for(let p of t){let x=[p.outer,...p.holes],T=j>1e-6?Math.min(1,$t(x,j)/j):1,v=O=>O*T,g=(O,B,R,E,I)=>{let M=[],C=[];for(let P=0;P<x.length;P++){P>0&&C.push(M.length/2);for(let[k,F]of rt(x[P],O))M.push(k,F)}let y=(0,ht.default)(M,C,2),S=P=>[M[P*2],M[P*2+1]],Y=I.tile??0;for(let P of Yt(M,y)){let F=(E?P.slice().reverse():P).map(S),U={vertices:F.map(W=>d(W,B,R)),color:I.color??"#cccccc"};I.texture&&b&&(U.texture=I.texture,U.material={texture:I.texture},U.uvs=F.map(W=>A(W,Y)),Y>0&&(U.textureWrap=G)),a.push(U)}};if(V>0&&g(-V,u-.001,l,!1,{at:0,color:n.outlineColor}),g(v(f[0].inset),f[0].z,l,!1,$(0)),c){g(v(f[f.length-1].inset),m,h,!0,$(1));continue}g(v(f[f.length-1].inset),f[f.length-1].z,l,!0,$(1));for(let O of x){let B=rt(O,v(f[0].inset));for(let R=1;R<f.length;R++){let E=rt(O,v(f[R].inset)),I=f[R-1].z,M=f[R].z,C=$(L((I+M)/2)),y=C.tile??0;for(let S=0,Y=O.length;S<Y;S++){let P=(S+1)%Y,k={vertices:[d(E[S],M,l),d(E[P],M,l),d(B[P],I,l),d(B[S],I,l)],color:C.color??"#cccccc"};C.texture&&(k.texture=C.texture,k.material={texture:C.texture},y>0||!b?k.uvs=[[0,1],[1,1],[1,0],[0,0]]:k.uvs=[A(E[S],0),A(E[P],0),A(B[P],0),A(B[S],0)]),a.push(k)}B=E}}}return a}function Dt(t,n,e,r,o,c,i,s){if(t==="flat"||r<=0)return[{z:n,inset:0},{z:e,inset:0}];let u=Math.min(c,r/2),m=t==="bevel"?1:Math.max(2,o),f=t==="bevel"?l=>1-l:t==="custom"&&s?(l=>h=>1-l(h))(ot(s)):i?l=>1-Math.sin(l*Math.PI/2):l=>Math.cos(l*Math.PI/2),a=[];for(let l=0;l<=m;l++){let h=l/m;a.push({z:n-h*u,inset:u*f(h)})}e+u<n-u-1e-6&&a.push({z:e+u,inset:0});for(let l=1;l<=m;l++){let h=1-l/m;a.push({z:e+u*h,inset:u*f(h)})}return a}function $t(t,n){let e=[];t.forEach((o,c)=>{let i=o.length;for(let s=0;s<i;s++){let u=o[s],m=o[(s+1)%i];e.push({x:u[0],y:u[1],c,e:s,n:i}),e.push({x:(u[0]+m[0])/2,y:(u[1]+m[1])/2,c,e:s+.5,n:i})}});let r=1/0;for(let o=0;o<e.length;o++)for(let c=o+1;c<e.length;c++){if(e[o].c===e[c].c){let m=Math.abs(e[o].e-e[c].e);if(m<=1.5||m>=e[o].n-1.5)continue}let i=e[o].x-e[c].x,s=e[o].y-e[c].y,u=i*i+s*s;u<r&&(r=u)}return Math.min(n,Math.sqrt(r)*.4)}function ft(t,n){let e=n[0]-t[0],r=n[1]-t[1],o=Math.hypot(e,r)||1;return[-r/o,e/o]}function rt(t,n){if(n===0)return t;let e=t.length,r=new Array(e);for(let o=0;o<e;o++){let c=t[(o-1+e)%e],i=t[o],s=t[(o+1)%e],u=ft(c,i),m=ft(i,s),f=u[0]+m[0],a=u[1]+m[1],l=Math.hypot(f,a)||1;f/=l,a/=l;let h=f*u[0]+a*u[1],d=n*Math.min(1.5,1/Math.max(h,.001));r[o]=[i[0]+f*d,i[1]+a*d]}return r}function jt(t,n,e){let r=e[0]-n[0],o=e[1]-n[1],c=Math.hypot(r,o);return c<1e-9?Math.hypot(t[0]-n[0],t[1]-n[1]):Math.abs((t[0]-n[0])*o-(t[1]-n[1])*r)/c}function J(t,n){if(t.length<3)return t;let e=t[0],r=t[t.length-1],o=0,c=0;for(let i=1;i<t.length-1;i++){let s=jt(t[i],e,r);s>o&&(o=s,c=i)}if(o>n){let i=J(t.slice(0,c+1),n),s=J(t.slice(c),n);return i.slice(0,-1).concat(s)}return[e,r]}function pt(t,n){if(n<=0||t.length<5)return t;let e=1/0,r=-1/0,o=1/0,c=-1/0;for(let[l,h]of t)l<e&&(e=l),l>r&&(r=l),h<o&&(o=h),h>c&&(c=h);let i=Math.min(n,Math.hypot(r-e,c-o)*.12);if(i<=.001)return t;let s=0,u=-1;for(let l=1;l<t.length;l++){let h=Math.hypot(t[l][0]-t[0][0],t[l][1]-t[0][1]);h>u&&(u=h,s=l)}let m=J(t.slice(0,s+1),i),f=J([...t.slice(s),t[0]],i),a=m.concat(f.slice(1,-1));return a.length>=3?a:t}function Q(t,n=.05){let e=[];for(let r of t){let o=e[e.length-1];(!o||Math.hypot(r[0]-o[0],r[1]-o[1])>n)&&e.push(r)}for(;e.length>1;){let r=e[0],o=e[e.length-1];if(Math.hypot(r[0]-o[0],r[1]-o[1])<=n)e.pop();else break}return e}function gt(t){let n=0;for(let e=0,r=t.length;e<r;e++){let[o,c]=t[e],[i,s]=t[(e+1)%r];n+=o*s-i*c}return n/2}function Lt(t,n){let e=!1;for(let r=0,o=n.length-1;r<n.length;o=r++){let[c,i]=n[r],[s,u]=n[o];i>t[1]!=u>t[1]&&t[0]<(s-c)*(t[1]-i)/(u-i)+c&&(e=!e)}return e}function mt(t,n){return gt(t)>0===n?t:t.slice().reverse()}function Z(t){let n=t.filter(s=>s.length>=3),e=n.length,r=new Array(e).fill(0),o=new Array(e).fill(-1);for(let s=0;s<e;s++){let u=n[s][0],m=-1,f=1/0;for(let a=0;a<e;a++)if(s!==a&&Lt(u,n[a])){r[s]++;let l=Math.abs(gt(n[a]));l<f&&(f=l,m=a)}o[s]=m}let c=[],i=new Map;for(let s=0;s<e;s++)r[s]%2===0&&(i.set(s,c.length),c.push({outer:mt(n[s],!0),holes:[]}));for(let s=0;s<e;s++)if(r[s]%2===1){let u=i.get(o[s]);u!==void 0&&c[u].holes.push(mt(n[s],!1))}return c}function _(t,n){let e=/^#?([0-9a-f]{6})$/i.exec(t.trim());if(!e)return t;let r=parseInt(e[1],16),o=Math.round((r>>16&255)*n),c=Math.round((r>>8&255)*n),i=Math.round((r&255)*n);return`#${(o<<16|c<<8|i).toString(16).padStart(6,"0")}`}function bt(t,n,e={}){let r=e.size??100,o=e.depth??r*.2,c=Math.max(1,Math.round(e.curveSteps??6)),i=e.letterSpacing??0,s=e.color??"#d4a82a",u=e.sideColor??_(s,.72),m=e.profile??"flat",f=Math.max(1,Math.round(e.profileSegments??6)),a=r/t.unitsPerEm,h=[...n].map(z=>t.glyph(z.codePointAt(0)??0,c)),d=0;for(let z of h)d+=z.advanceWidth*a+i;let b=-d/2,w=[];for(let z of h){if(z.contours.length){let A=z.contours.map(G=>Q(G.map(([V,D])=>[V*a+b,D*a])));w.push(...Z(A))}b+=z.advanceWidth*a+i}return K(w,{depth:o,profile:m,profileSegments:f,maxInset:r*.045,stops:[{at:0,color:s},{at:.5,color:u},{at:1,color:s}]})}function _t(t){return!t||t==="flat"?{profile:"flat"}:"edge"in t?{profile:t.edge,roundConvex:t.raised,segments:t.segments}:{profile:"custom",profileBezier:t.curve,segments:t.segments}}function Ht(t,n){if(t<=n||t<=1e-6)return 2;let e=Math.ceil(Math.PI/(2*Math.acos(1-n/t)));return Math.min(6,Math.max(2,e))}function qt(t,n){let e=(r,o,c)=>({at:o,color:r.color??c,texture:r.texture,tile:r.tile});if(Array.isArray(t))return{stops:t.length?t.map(r=>e(r,r.at,n)):[{at:.5,color:n}]};if(t&&("front"in t||"sides"in t||"back"in t)){let r=t.front??{},o=r.color??n,c=[{f:r,color:o}];t.sides&&c.push({f:t.sides,color:_(o,.72)}),t.back!==!1&&c.push({f:t.back??{},color:o});let i=c.length;return{stops:c.map((u,m)=>e(u.f,(m+.5)/i,u.color)),backOffset:t.back?t.back.offset:void 0}}return t?{stops:[e(t,.5,n)]}:{stops:[{at:1/6,color:n},{at:.5,color:_(n,.72)},{at:5/6,color:n}]}}function xt(t,n,e={}){let r=e.size??100,o=e.depth??r*.2,c=Math.max(1,Math.round(e.curveSteps??6)),i=e.letterSpacing??0,s=(e.lineHeight??1.25)*r,u=e.align??"center",m=Math.max(0,e.simplify??0),[f,a]=e.scale??[1,1],{stops:l,backOffset:h}=qt(e.faces,"#d4a82a"),d=_t(e.profile),b=Math.min(r*.045,o/2),w=d.profile==="round"?Ht(b,r*.004):6,z=Math.max(1,Math.round(d.segments??w)),A=o<=0,G=r/t.unitsPerEm,V=r*.06,D=-r*.14,$=r*.26,j=n.split(`
2
+ `).map(M=>{let C=[...M].map(S=>t.glyph(S.codePointAt(0)??0,c)),y=0;for(let S of C)y+=S.advanceWidth*G*f+i;return y=Math.max(0,y-i),{glyphs:C,width:y}}),p=Math.max(1,...j.map(M=>M.width)),x=j.length,T=Nt(e.warp,-p/2,p,r),v=[];j.forEach((M,C)=>{let y=((x-1)/2-C)*s-r*.34,S=-p/2;u==="center"?S+=(p-M.width)/2:u==="right"&&(S+=p-M.width);let Y=S;for(let P of M.glyphs){if(P.contours.length){let k=P.contours.map(F=>Q(F.map(([U,W])=>[U*G*f+Y,W*G*a+y])));for(let F of Z(k)){let U=m>0&&F.holes.length===0?{outer:pt(F.outer,m),holes:F.holes}:F;v.push(st(U,T))}}Y+=P.advanceWidth*G*f+i}if(M.width>0){let P=S,k=S+M.width;e.underline&&v.push(st(dt(P,k,y+D-V,y+D),T)),e.strike&&v.push(st(dt(P,k,y+$-V/2,y+$+V/2),T))}});let g=1/0,O=1/0,B=-1/0,R=-1/0;for(let M of v)for(let[C,y]of M.outer)C<g&&(g=C),C>B&&(B=C),y<O&&(O=y),y>R&&(R=y);let E=v.length?{minX:g,minY:O,maxX:B,maxY:R}:void 0;return K(v,{depth:o,profile:d.profile,roundConvex:d.roundConvex,profileBezier:d.profileBezier,profileSegments:z,maxInset:r*.045,stops:l,faceUvBounds:E,backOffset:h,layered:A,outlineColor:e.outline?.color,outlineWidth:e.outline?.width})}function st(t,n){return n?{outer:t.outer.map(n),holes:t.holes.map(e=>e.map(n))}:t}function dt(t,n,e,r,o=24){let c=[];for(let i=0;i<=o;i++)c.push([t+(n-t)*i/o,e]);for(let i=o;i>=0;i--)c.push([t+(n-t)*i/o,r]);return{outer:c,holes:[]}}function Nt(t,n,e,r){if(!t||t.shape==="none")return null;let o=Math.max(0,Math.min(1,t.amount??.5));if(o===0)return null;let c=s=>(s-n)/e,i=s=>1-(2*s-1)*(2*s-1);switch(t.shape){case"arch":return s=>[s[0],s[1]+o*r*.7*i(c(s[0]))];case"archDown":return s=>[s[0],s[1]-o*r*.7*i(c(s[0]))];case"wave":return s=>[s[0],s[1]+o*r*.4*Math.sin(2*Math.PI*c(s[0]))];case"bulge":return s=>[s[0],s[1]*(1+o*i(c(s[0])))];case"cone":return s=>[s[0],s[1]*(1-o*.75*c(s[0]))];case"slantUp":return s=>[s[0],s[1]+o*r*.6*(c(s[0])-.5)];case"slantDown":return s=>[s[0],s[1]-o*r*.6*(c(s[0])-.5)];case"arc":{let s=Math.max(.08,o)*Math.PI,u=e/s,m=n+e/2;return f=>{let a=(c(f[0])-.5)*s,l=u+f[1];return[m+l*Math.sin(a),-u+l*Math.cos(a)]}}default:return null}}function Pt(t){switch(t.kind){case"solid":return{color:t.color};case"gradient":return{color:t.color,texture:tt({type:"gradient",from:t.from,to:t.to,angle:t.angle})};case"rainbow":return{color:t.color,texture:tt({type:"rainbow",angle:t.angle})};case"texture":return{color:t.color,texture:t.url||void 0,tile:t.tile};case"image":return{color:t.color,texture:t.src||void 0}}}var yt=["#ff3b30","#ff9500","#ffcc00","#34c759","#00c7be","#007aff","#5856d6","#af52de"];function Jt(t,n){let e=document.createElement("canvas");return e.width=t,e.height=n,e}function Mt(t,n,e,r,o){let c=r*Math.PI/180,i=Math.cos(c),s=-Math.sin(c),u=n/2,m=e/2,f=(Math.abs(i)*n+Math.abs(s)*e)/2,a=t.createLinearGradient(u-i*f,m-s*f,u+i*f,m+s*f);for(let[l,h]of o)a.addColorStop(l,h);t.fillStyle=a,t.fillRect(0,0,n,e)}function tt(t){if(t.type==="solid")return;if(t.type==="image")return t.src||void 0;let n=256,e=Jt(n,n),r=e.getContext("2d");if(r){if(t.type==="rainbow"){let o=t.angle??0,c=yt.map((i,s)=>[s/(yt.length-1),i]);Mt(r,n,n,o,c)}else{let o=t.angle??270;Mt(r,n,n,o,[[0,t.from],[1,t.to]])}return e.toDataURL("image/png")}}var Kt="https://api.fontsource.org/v1/fonts",Qt=[700,400,500,600,800,300,900,200,100],et=null;async function Ct(){if(et)return et;let t=await fetch(Kt);if(!t.ok)throw new Error(`font list ${t.status}`);return et=(await t.json()).filter(e=>e.type==="google"&&e.styles.includes("normal")).sort((e,r)=>e.family.localeCompare(r.family)),et}function it(t,n){if(n&&t.weights.includes(n))return n;for(let e of Qt)if(t.weights.includes(e))return e;return t.weights[0]??400}function ct(t,n,e="normal"){let r=it(t,n),o=t.subsets.includes("latin")?"latin":t.defSubset,c=e==="italic"&&t.styles.includes("italic")?"italic":"normal";return`https://cdn.jsdelivr.net/fontsource/fonts/${t.id}@latest/${o}-${r}-${c}.ttf`}async function ut(t){let n=await fetch(t);if(!n.ok)throw new Error(`load font ${n.status}: ${t}`);return N(await n.arrayBuffer())}async function Ft(t,n,e="normal"){return ut(ct(t,n,e))}0&&(module.exports={composeText,cssCubicBezier,googleFontUrl,listGoogleFonts,loadFont,loadGoogleFont,makeFillTexture,parseFont,pickWeight,resolveFace,textPolygons});
@@ -0,0 +1,302 @@
1
+ import { Vec2, Polygon } from '@glyphcss/core';
2
+
3
+ /**
4
+ * Minimal TrueType (`glyf`) font reader — bytes → glyph outlines + metrics.
5
+ *
6
+ * A font file is an sfnt container: a table directory pointing at named binary
7
+ * tables. We read only what's needed to lay out and outline text:
8
+ *
9
+ * head → unitsPerEm + loca format maxp → glyph count
10
+ * hhea/hmtx → advance widths cmap → codepoint → glyph index
11
+ * loca → glyph offsets into glyf glyf → the outline vectors
12
+ *
13
+ * Scope is deliberately narrow — this is a small, dependency-free reader for
14
+ * the common case, not a full font library:
15
+ * - TrueType outlines only (`glyf`). CFF/OpenType (".otf", magic "OTTO") is
16
+ * a different outline format (Type2 charstrings) and is rejected with a
17
+ * clear error. Google Fonts ship TrueType, so this covers most fonts.
18
+ * - Uncompressed sfnt only — woff/woff2 wrappers are not unpacked.
19
+ * - cmap formats 4 (BMP) and 12 (full Unicode). No shaping, kerning,
20
+ * ligatures, or variable-font axes: each character maps to one glyph plus
21
+ * its advance width.
22
+ *
23
+ * TrueType glyph space: font units, y-up, origin on the baseline.
24
+ */
25
+
26
+ interface FontGlyph {
27
+ /** Closed contours as flattened polylines, font units, y-up. */
28
+ contours: Vec2[][];
29
+ /** Advance width in font units. */
30
+ advanceWidth: number;
31
+ }
32
+ interface ParsedFont {
33
+ /** Font design units per em (the scale denominator). */
34
+ unitsPerEm: number;
35
+ /** Typographic ascender in font units. */
36
+ ascender: number;
37
+ /** Typographic descender in font units (usually negative). */
38
+ descender: number;
39
+ /** Recommended extra line spacing in font units. */
40
+ lineGap: number;
41
+ /** Outline + advance for a Unicode codepoint. Empty contours for blanks. */
42
+ glyph(codePoint: number, curveSteps?: number): FontGlyph;
43
+ }
44
+ declare function parseFont(data: ArrayBuffer | Uint8Array, defaultCurveSteps?: number): ParsedFont;
45
+
46
+ /**
47
+ * Cross-section of the extrusion along its depth:
48
+ * - "flat" — straight slab with vertical walls (a depth-only extrude).
49
+ * - "round" — a quarter-circle round-over on the front/back edges (bullnose).
50
+ * - "bevel" — a straight 45° chamfer on the front/back edges.
51
+ * - "custom" — the edge profile is the CSS easing curve `profileBezier`
52
+ * (`cubic-bezier(x1,y1,x2,y2)` semantics), sampled along the depth.
53
+ */
54
+ type ExtrudeProfile = "flat" | "round" | "bevel" | "custom";
55
+ /** CSS cubic-bezier control points [x1, y1, x2, y2] (P0=(0,0), P3=(1,1)). */
56
+ type CubicBezier = [number, number, number, number];
57
+ /**
58
+ * CSS `cubic-bezier(x1,y1,x2,y2)` easing → a function mapping x∈[0,1] to y.
59
+ * Solves x(t)=x by Newton's method (same approach browsers use), then reads y.
60
+ */
61
+ declare function cssCubicBezier([x1, y1, x2, y2]: CubicBezier): (x: number) => number;
62
+ /**
63
+ * A material stop on the axial (depth) axis: `at` runs 0 (front face) → 1 (back
64
+ * face). Each emitted polygon is colored/textured by the nearest stop to its
65
+ * own depth, so any number of stops can band the solid down its length.
66
+ */
67
+ interface MaterialStop {
68
+ at: number;
69
+ /** Solid color (and fallback if a texture fails to load). */
70
+ color?: string;
71
+ /** Texture URL / data URL — UV-mapped across the word (caps & stretch walls). */
72
+ texture?: string;
73
+ /** Tile the texture every N world units (block look) instead of stretching. */
74
+ tile?: number;
75
+ }
76
+
77
+ /**
78
+ * Extrude a single line of text into a 3D polygon mesh glyphcss can render.
79
+ * For multiline / styled / WordArt composition see `composeText`.
80
+ *
81
+ * parseFont emits glyph space as font units, y-up, baseline at 0. We place
82
+ * each glyph into a flat "type plane" (x → right along the baseline, y → up),
83
+ * centered on the origin, then hand the grouped shapes to `extrudeContours`.
84
+ */
85
+
86
+ interface TextPolygonsOptions {
87
+ /** Cap-em size in world units. Defaults to 100. */
88
+ size?: number;
89
+ /** Extrusion depth along the world Z axis, in world units. Defaults to size*0.2. */
90
+ depth?: number;
91
+ /** Bézier flattening: segments per curve. Higher = smoother, more polys. */
92
+ curveSteps?: number;
93
+ /** Extra space between glyphs, in world units. */
94
+ letterSpacing?: number;
95
+ /** Front/back cap color. */
96
+ color?: string;
97
+ /** Side-wall color. Defaults to a slightly darker shade of `color`. */
98
+ sideColor?: string;
99
+ /** Depth cross-section shape. Defaults to "flat". */
100
+ profile?: ExtrudeProfile;
101
+ /** Rings used to sample a round/bevel profile. Higher = smoother, more polys. */
102
+ profileSegments?: number;
103
+ }
104
+ declare function textPolygons(font: ParsedFont, text: string, options?: TextPolygonsOptions): Polygon[];
105
+
106
+ /**
107
+ * Compose styled, multi-line text into a 3D polygon mesh.
108
+ *
109
+ * Builds on the same type-plane → extrude pipeline as `textPolygons`, adding
110
+ * line breaks (`\n`), per-line alignment, line height, underline /
111
+ * strikethrough bars, and classic WordArt-style **warps** (arch / arc / wave /
112
+ * bulge / slant). The warp deforms every point in the flat type plane before
113
+ * extrusion, so the 3D walls follow the curve too. Bold/italic are chosen by
114
+ * the caller by passing the appropriate weight/style `ParsedFont`.
115
+ */
116
+
117
+ /** Classic WordArt envelope shapes. */
118
+ type WarpShape = "none" | "arch" | "archDown" | "arc" | "wave" | "bulge" | "cone" | "slantUp" | "slantDown";
119
+ interface WarpOptions {
120
+ shape: WarpShape;
121
+ /** Warp strength, 0..1. Defaults to 0.5. */
122
+ amount?: number;
123
+ }
124
+ /** A paintable face: a solid color and/or an already-rendered texture. */
125
+ interface Face {
126
+ /** Solid color; also the fallback if the texture fails to load. */
127
+ color?: string;
128
+ /**
129
+ * Texture URL / data URL — a gradient, rainbow, image, or block texture
130
+ * already rendered (see `resolveFace` / `makeFillTexture`). UV-mapped across
131
+ * the whole word so the fill flows over every glyph.
132
+ */
133
+ texture?: string;
134
+ /** Tile the texture every N world units (block look); omit to stretch one copy. */
135
+ tile?: number;
136
+ }
137
+ /** The back face, which can be offset for the flat WordArt drop shadow (depth 0). */
138
+ interface BackFace extends Face {
139
+ /** [rightward, upward] shift of the back relative to the front (world units). */
140
+ offset?: [number, number];
141
+ }
142
+ /** A `Face` pinned at a position on the depth axis (`at`: 0 = front, 1 = back). */
143
+ interface FaceStop extends Face {
144
+ at: number;
145
+ }
146
+ /**
147
+ * Extrusion cross-section / edge profile:
148
+ * - `"flat"` — straight slab (no edge shaping).
149
+ * - `{ edge }` — a bevel chamfer or round bullnose on the edge (mirrored
150
+ * front/back). `raised` flips a round to a convex dome.
151
+ * - `{ curve }` — a custom edge whose cross-section is a CSS cubic-bezier easing.
152
+ */
153
+ type Profile = "flat" | {
154
+ edge: "bevel" | "round";
155
+ raised?: boolean;
156
+ segments?: number;
157
+ } | {
158
+ curve: CubicBezier;
159
+ segments?: number;
160
+ };
161
+ interface ComposeTextOptions {
162
+ /** Cap-em size in world units. Defaults to 100. */
163
+ size?: number;
164
+ /** Extrusion depth (world units). 0 = a flat slab with no edges. Defaults to size*0.2. */
165
+ depth?: number;
166
+ /** Bézier flattening: segments per glyph curve. Higher = smoother. Defaults to 6. */
167
+ curveSteps?: number;
168
+ /** Extra space between glyphs (world units). */
169
+ letterSpacing?: number;
170
+ /** Line advance as a multiple of `size`. Defaults to 1.25. */
171
+ lineHeight?: number;
172
+ /** Horizontal alignment of each line within the block. Defaults to "center". */
173
+ align?: "left" | "center" | "right";
174
+ /** Non-uniform glyph stretch [x, y] (WordArt-style). Defaults to [1, 1]. */
175
+ scale?: [number, number];
176
+ /** Draw an underline / strikethrough bar under each line. */
177
+ underline?: boolean;
178
+ strike?: boolean;
179
+ /** WordArt envelope warp applied to the whole block. */
180
+ warp?: WarpOptions;
181
+ /** Outline simplification tolerance (world units, 0 = exact; hole-less glyphs only). */
182
+ simplify?: number;
183
+ /** Extrusion cross-section / edge profile. Defaults to "flat". */
184
+ profile?: Profile;
185
+ /**
186
+ * Material along the depth axis. Three shapes, simplest → most general:
187
+ * - `{ front?, sides?, back? }` — the classic faces (sugar for 3 stops). Omit
188
+ * `sides` for no side band (front rounds into back), or set `sides` / `back`
189
+ * to `false` to skip that geometry entirely.
190
+ * - a single `Face` — one material for the whole solid.
191
+ * - `FaceStop[]` — N materials distributed down the axis (`at` 0→1).
192
+ */
193
+ faces?: Face | FaceStop[] | {
194
+ front?: Face;
195
+ sides?: Face | false;
196
+ back?: BackFace | false;
197
+ };
198
+ /** Outline stroke drawn as a halo around the front face. */
199
+ outline?: {
200
+ color: string;
201
+ width: number;
202
+ };
203
+ }
204
+ declare function composeText(font: ParsedFont, text: string, options?: ComposeTextOptions): Polygon[];
205
+
206
+ /**
207
+ * Browser-only helpers that paint a WordArt "master fill" onto a `<canvas>` and
208
+ * return it as a data URL, plus `resolveFace` which turns a high-level fill
209
+ * spec into the pure-layer `Face` (`{ color?, texture?, tile? }`) that
210
+ * `composeText` consumes. `composeText` then UV-maps the whole word's face to
211
+ * that single texture, so a gradient / rainbow / image / block flows
212
+ * continuously across every glyph (not per-letter).
213
+ *
214
+ * Pure-layer code (`composeText`, `extrudeContours`) never imports this — it
215
+ * only receives the resulting strings — so the Node-testable path stays free of
216
+ * browser globals.
217
+ */
218
+
219
+ /** A WordArt face fill. `solid` means "no texture, use the flat color". */
220
+ type FillSpec = {
221
+ type: "solid";
222
+ } | {
223
+ type: "gradient";
224
+ from: string;
225
+ to: string;
226
+ angle?: number;
227
+ } | {
228
+ type: "rainbow";
229
+ angle?: number;
230
+ } | {
231
+ type: "image";
232
+ src: string;
233
+ };
234
+ /** High-level per-face fill the UI works with; `resolveFace` renders it to a `Face`. */
235
+ type FaceFillSpec = {
236
+ kind: "solid";
237
+ color: string;
238
+ } | {
239
+ kind: "gradient";
240
+ color?: string;
241
+ from: string;
242
+ to: string;
243
+ angle?: number;
244
+ } | {
245
+ kind: "rainbow";
246
+ color?: string;
247
+ angle?: number;
248
+ } | {
249
+ kind: "texture";
250
+ color?: string;
251
+ url: string;
252
+ tile?: number;
253
+ } | {
254
+ kind: "image";
255
+ color?: string;
256
+ src: string;
257
+ };
258
+ /**
259
+ * Resolve a high-level face fill into the pure `Face` `composeText` takes —
260
+ * rendering gradients / rainbows to a data URL via `makeFillTexture`, and
261
+ * passing block / image URLs straight through. Keeps `composeText` browser-free.
262
+ */
263
+ declare function resolveFace(spec: FaceFillSpec): Face;
264
+ /**
265
+ * Build the master fill texture for a face. Returns a data URL, or `undefined`
266
+ * for `solid` (no texture). For `image`, the source is returned as-is — the
267
+ * renderer can use any `background-image` URL directly.
268
+ */
269
+ declare function makeFillTexture(spec: FillSpec): string | undefined;
270
+
271
+ /**
272
+ * Browser-side loading helpers. These are the only part of the package that
273
+ * touches the network (`fetch`); the parse + extrude core stays pure.
274
+ *
275
+ * Fonts come from the Fontsource API/CDN, which mirrors every Google font and
276
+ * serves plain **.ttf** with open CORS — exactly what `parseFont` needs
277
+ * (Google's default woff2 is not supported). No API key required.
278
+ */
279
+
280
+ interface FontEntry {
281
+ id: string;
282
+ family: string;
283
+ weights: number[];
284
+ styles: string[];
285
+ subsets: string[];
286
+ defSubset: string;
287
+ category: string;
288
+ type: string;
289
+ }
290
+ /** All Google fonts that ship a normal (upright) style, sorted by family. */
291
+ declare function listGoogleFonts(): Promise<FontEntry[]>;
292
+ /** Pick the requested weight if available, else the closest sensible default. */
293
+ declare function pickWeight(font: FontEntry, preferred?: number): number;
294
+ type FontStyle = "normal" | "italic";
295
+ /** Direct .ttf URL for a font at a given weight/style (open CORS, parseFont-ready). */
296
+ declare function googleFontUrl(font: FontEntry, weight?: number, style?: FontStyle): string;
297
+ /** Fetch a .ttf from any URL and parse it into a `ParsedFont`. */
298
+ declare function loadFont(url: string): Promise<ParsedFont>;
299
+ /** Fetch + parse a specific Google font family/weight/style. */
300
+ declare function loadGoogleFont(font: FontEntry, weight?: number, style?: FontStyle): Promise<ParsedFont>;
301
+
302
+ export { type BackFace, type ComposeTextOptions, type CubicBezier, type ExtrudeProfile, type Face, type FaceFillSpec, type FaceStop, type FillSpec, type FontEntry, type FontGlyph, type FontStyle, type MaterialStop, type ParsedFont, type Profile, type TextPolygonsOptions, type WarpOptions, type WarpShape, composeText, cssCubicBezier, googleFontUrl, listGoogleFonts, loadFont, loadGoogleFont, makeFillTexture, parseFont, pickWeight, resolveFace, textPolygons };
@@ -0,0 +1,302 @@
1
+ import { Vec2, Polygon } from '@glyphcss/core';
2
+
3
+ /**
4
+ * Minimal TrueType (`glyf`) font reader — bytes → glyph outlines + metrics.
5
+ *
6
+ * A font file is an sfnt container: a table directory pointing at named binary
7
+ * tables. We read only what's needed to lay out and outline text:
8
+ *
9
+ * head → unitsPerEm + loca format maxp → glyph count
10
+ * hhea/hmtx → advance widths cmap → codepoint → glyph index
11
+ * loca → glyph offsets into glyf glyf → the outline vectors
12
+ *
13
+ * Scope is deliberately narrow — this is a small, dependency-free reader for
14
+ * the common case, not a full font library:
15
+ * - TrueType outlines only (`glyf`). CFF/OpenType (".otf", magic "OTTO") is
16
+ * a different outline format (Type2 charstrings) and is rejected with a
17
+ * clear error. Google Fonts ship TrueType, so this covers most fonts.
18
+ * - Uncompressed sfnt only — woff/woff2 wrappers are not unpacked.
19
+ * - cmap formats 4 (BMP) and 12 (full Unicode). No shaping, kerning,
20
+ * ligatures, or variable-font axes: each character maps to one glyph plus
21
+ * its advance width.
22
+ *
23
+ * TrueType glyph space: font units, y-up, origin on the baseline.
24
+ */
25
+
26
+ interface FontGlyph {
27
+ /** Closed contours as flattened polylines, font units, y-up. */
28
+ contours: Vec2[][];
29
+ /** Advance width in font units. */
30
+ advanceWidth: number;
31
+ }
32
+ interface ParsedFont {
33
+ /** Font design units per em (the scale denominator). */
34
+ unitsPerEm: number;
35
+ /** Typographic ascender in font units. */
36
+ ascender: number;
37
+ /** Typographic descender in font units (usually negative). */
38
+ descender: number;
39
+ /** Recommended extra line spacing in font units. */
40
+ lineGap: number;
41
+ /** Outline + advance for a Unicode codepoint. Empty contours for blanks. */
42
+ glyph(codePoint: number, curveSteps?: number): FontGlyph;
43
+ }
44
+ declare function parseFont(data: ArrayBuffer | Uint8Array, defaultCurveSteps?: number): ParsedFont;
45
+
46
+ /**
47
+ * Cross-section of the extrusion along its depth:
48
+ * - "flat" — straight slab with vertical walls (a depth-only extrude).
49
+ * - "round" — a quarter-circle round-over on the front/back edges (bullnose).
50
+ * - "bevel" — a straight 45° chamfer on the front/back edges.
51
+ * - "custom" — the edge profile is the CSS easing curve `profileBezier`
52
+ * (`cubic-bezier(x1,y1,x2,y2)` semantics), sampled along the depth.
53
+ */
54
+ type ExtrudeProfile = "flat" | "round" | "bevel" | "custom";
55
+ /** CSS cubic-bezier control points [x1, y1, x2, y2] (P0=(0,0), P3=(1,1)). */
56
+ type CubicBezier = [number, number, number, number];
57
+ /**
58
+ * CSS `cubic-bezier(x1,y1,x2,y2)` easing → a function mapping x∈[0,1] to y.
59
+ * Solves x(t)=x by Newton's method (same approach browsers use), then reads y.
60
+ */
61
+ declare function cssCubicBezier([x1, y1, x2, y2]: CubicBezier): (x: number) => number;
62
+ /**
63
+ * A material stop on the axial (depth) axis: `at` runs 0 (front face) → 1 (back
64
+ * face). Each emitted polygon is colored/textured by the nearest stop to its
65
+ * own depth, so any number of stops can band the solid down its length.
66
+ */
67
+ interface MaterialStop {
68
+ at: number;
69
+ /** Solid color (and fallback if a texture fails to load). */
70
+ color?: string;
71
+ /** Texture URL / data URL — UV-mapped across the word (caps & stretch walls). */
72
+ texture?: string;
73
+ /** Tile the texture every N world units (block look) instead of stretching. */
74
+ tile?: number;
75
+ }
76
+
77
+ /**
78
+ * Extrude a single line of text into a 3D polygon mesh glyphcss can render.
79
+ * For multiline / styled / WordArt composition see `composeText`.
80
+ *
81
+ * parseFont emits glyph space as font units, y-up, baseline at 0. We place
82
+ * each glyph into a flat "type plane" (x → right along the baseline, y → up),
83
+ * centered on the origin, then hand the grouped shapes to `extrudeContours`.
84
+ */
85
+
86
+ interface TextPolygonsOptions {
87
+ /** Cap-em size in world units. Defaults to 100. */
88
+ size?: number;
89
+ /** Extrusion depth along the world Z axis, in world units. Defaults to size*0.2. */
90
+ depth?: number;
91
+ /** Bézier flattening: segments per curve. Higher = smoother, more polys. */
92
+ curveSteps?: number;
93
+ /** Extra space between glyphs, in world units. */
94
+ letterSpacing?: number;
95
+ /** Front/back cap color. */
96
+ color?: string;
97
+ /** Side-wall color. Defaults to a slightly darker shade of `color`. */
98
+ sideColor?: string;
99
+ /** Depth cross-section shape. Defaults to "flat". */
100
+ profile?: ExtrudeProfile;
101
+ /** Rings used to sample a round/bevel profile. Higher = smoother, more polys. */
102
+ profileSegments?: number;
103
+ }
104
+ declare function textPolygons(font: ParsedFont, text: string, options?: TextPolygonsOptions): Polygon[];
105
+
106
+ /**
107
+ * Compose styled, multi-line text into a 3D polygon mesh.
108
+ *
109
+ * Builds on the same type-plane → extrude pipeline as `textPolygons`, adding
110
+ * line breaks (`\n`), per-line alignment, line height, underline /
111
+ * strikethrough bars, and classic WordArt-style **warps** (arch / arc / wave /
112
+ * bulge / slant). The warp deforms every point in the flat type plane before
113
+ * extrusion, so the 3D walls follow the curve too. Bold/italic are chosen by
114
+ * the caller by passing the appropriate weight/style `ParsedFont`.
115
+ */
116
+
117
+ /** Classic WordArt envelope shapes. */
118
+ type WarpShape = "none" | "arch" | "archDown" | "arc" | "wave" | "bulge" | "cone" | "slantUp" | "slantDown";
119
+ interface WarpOptions {
120
+ shape: WarpShape;
121
+ /** Warp strength, 0..1. Defaults to 0.5. */
122
+ amount?: number;
123
+ }
124
+ /** A paintable face: a solid color and/or an already-rendered texture. */
125
+ interface Face {
126
+ /** Solid color; also the fallback if the texture fails to load. */
127
+ color?: string;
128
+ /**
129
+ * Texture URL / data URL — a gradient, rainbow, image, or block texture
130
+ * already rendered (see `resolveFace` / `makeFillTexture`). UV-mapped across
131
+ * the whole word so the fill flows over every glyph.
132
+ */
133
+ texture?: string;
134
+ /** Tile the texture every N world units (block look); omit to stretch one copy. */
135
+ tile?: number;
136
+ }
137
+ /** The back face, which can be offset for the flat WordArt drop shadow (depth 0). */
138
+ interface BackFace extends Face {
139
+ /** [rightward, upward] shift of the back relative to the front (world units). */
140
+ offset?: [number, number];
141
+ }
142
+ /** A `Face` pinned at a position on the depth axis (`at`: 0 = front, 1 = back). */
143
+ interface FaceStop extends Face {
144
+ at: number;
145
+ }
146
+ /**
147
+ * Extrusion cross-section / edge profile:
148
+ * - `"flat"` — straight slab (no edge shaping).
149
+ * - `{ edge }` — a bevel chamfer or round bullnose on the edge (mirrored
150
+ * front/back). `raised` flips a round to a convex dome.
151
+ * - `{ curve }` — a custom edge whose cross-section is a CSS cubic-bezier easing.
152
+ */
153
+ type Profile = "flat" | {
154
+ edge: "bevel" | "round";
155
+ raised?: boolean;
156
+ segments?: number;
157
+ } | {
158
+ curve: CubicBezier;
159
+ segments?: number;
160
+ };
161
+ interface ComposeTextOptions {
162
+ /** Cap-em size in world units. Defaults to 100. */
163
+ size?: number;
164
+ /** Extrusion depth (world units). 0 = a flat slab with no edges. Defaults to size*0.2. */
165
+ depth?: number;
166
+ /** Bézier flattening: segments per glyph curve. Higher = smoother. Defaults to 6. */
167
+ curveSteps?: number;
168
+ /** Extra space between glyphs (world units). */
169
+ letterSpacing?: number;
170
+ /** Line advance as a multiple of `size`. Defaults to 1.25. */
171
+ lineHeight?: number;
172
+ /** Horizontal alignment of each line within the block. Defaults to "center". */
173
+ align?: "left" | "center" | "right";
174
+ /** Non-uniform glyph stretch [x, y] (WordArt-style). Defaults to [1, 1]. */
175
+ scale?: [number, number];
176
+ /** Draw an underline / strikethrough bar under each line. */
177
+ underline?: boolean;
178
+ strike?: boolean;
179
+ /** WordArt envelope warp applied to the whole block. */
180
+ warp?: WarpOptions;
181
+ /** Outline simplification tolerance (world units, 0 = exact; hole-less glyphs only). */
182
+ simplify?: number;
183
+ /** Extrusion cross-section / edge profile. Defaults to "flat". */
184
+ profile?: Profile;
185
+ /**
186
+ * Material along the depth axis. Three shapes, simplest → most general:
187
+ * - `{ front?, sides?, back? }` — the classic faces (sugar for 3 stops). Omit
188
+ * `sides` for no side band (front rounds into back), or set `sides` / `back`
189
+ * to `false` to skip that geometry entirely.
190
+ * - a single `Face` — one material for the whole solid.
191
+ * - `FaceStop[]` — N materials distributed down the axis (`at` 0→1).
192
+ */
193
+ faces?: Face | FaceStop[] | {
194
+ front?: Face;
195
+ sides?: Face | false;
196
+ back?: BackFace | false;
197
+ };
198
+ /** Outline stroke drawn as a halo around the front face. */
199
+ outline?: {
200
+ color: string;
201
+ width: number;
202
+ };
203
+ }
204
+ declare function composeText(font: ParsedFont, text: string, options?: ComposeTextOptions): Polygon[];
205
+
206
+ /**
207
+ * Browser-only helpers that paint a WordArt "master fill" onto a `<canvas>` and
208
+ * return it as a data URL, plus `resolveFace` which turns a high-level fill
209
+ * spec into the pure-layer `Face` (`{ color?, texture?, tile? }`) that
210
+ * `composeText` consumes. `composeText` then UV-maps the whole word's face to
211
+ * that single texture, so a gradient / rainbow / image / block flows
212
+ * continuously across every glyph (not per-letter).
213
+ *
214
+ * Pure-layer code (`composeText`, `extrudeContours`) never imports this — it
215
+ * only receives the resulting strings — so the Node-testable path stays free of
216
+ * browser globals.
217
+ */
218
+
219
+ /** A WordArt face fill. `solid` means "no texture, use the flat color". */
220
+ type FillSpec = {
221
+ type: "solid";
222
+ } | {
223
+ type: "gradient";
224
+ from: string;
225
+ to: string;
226
+ angle?: number;
227
+ } | {
228
+ type: "rainbow";
229
+ angle?: number;
230
+ } | {
231
+ type: "image";
232
+ src: string;
233
+ };
234
+ /** High-level per-face fill the UI works with; `resolveFace` renders it to a `Face`. */
235
+ type FaceFillSpec = {
236
+ kind: "solid";
237
+ color: string;
238
+ } | {
239
+ kind: "gradient";
240
+ color?: string;
241
+ from: string;
242
+ to: string;
243
+ angle?: number;
244
+ } | {
245
+ kind: "rainbow";
246
+ color?: string;
247
+ angle?: number;
248
+ } | {
249
+ kind: "texture";
250
+ color?: string;
251
+ url: string;
252
+ tile?: number;
253
+ } | {
254
+ kind: "image";
255
+ color?: string;
256
+ src: string;
257
+ };
258
+ /**
259
+ * Resolve a high-level face fill into the pure `Face` `composeText` takes —
260
+ * rendering gradients / rainbows to a data URL via `makeFillTexture`, and
261
+ * passing block / image URLs straight through. Keeps `composeText` browser-free.
262
+ */
263
+ declare function resolveFace(spec: FaceFillSpec): Face;
264
+ /**
265
+ * Build the master fill texture for a face. Returns a data URL, or `undefined`
266
+ * for `solid` (no texture). For `image`, the source is returned as-is — the
267
+ * renderer can use any `background-image` URL directly.
268
+ */
269
+ declare function makeFillTexture(spec: FillSpec): string | undefined;
270
+
271
+ /**
272
+ * Browser-side loading helpers. These are the only part of the package that
273
+ * touches the network (`fetch`); the parse + extrude core stays pure.
274
+ *
275
+ * Fonts come from the Fontsource API/CDN, which mirrors every Google font and
276
+ * serves plain **.ttf** with open CORS — exactly what `parseFont` needs
277
+ * (Google's default woff2 is not supported). No API key required.
278
+ */
279
+
280
+ interface FontEntry {
281
+ id: string;
282
+ family: string;
283
+ weights: number[];
284
+ styles: string[];
285
+ subsets: string[];
286
+ defSubset: string;
287
+ category: string;
288
+ type: string;
289
+ }
290
+ /** All Google fonts that ship a normal (upright) style, sorted by family. */
291
+ declare function listGoogleFonts(): Promise<FontEntry[]>;
292
+ /** Pick the requested weight if available, else the closest sensible default. */
293
+ declare function pickWeight(font: FontEntry, preferred?: number): number;
294
+ type FontStyle = "normal" | "italic";
295
+ /** Direct .ttf URL for a font at a given weight/style (open CORS, parseFont-ready). */
296
+ declare function googleFontUrl(font: FontEntry, weight?: number, style?: FontStyle): string;
297
+ /** Fetch a .ttf from any URL and parse it into a `ParsedFont`. */
298
+ declare function loadFont(url: string): Promise<ParsedFont>;
299
+ /** Fetch + parse a specific Google font family/weight/style. */
300
+ declare function loadGoogleFont(font: FontEntry, weight?: number, style?: FontStyle): Promise<ParsedFont>;
301
+
302
+ export { type BackFace, type ComposeTextOptions, type CubicBezier, type ExtrudeProfile, type Face, type FaceFillSpec, type FaceStop, type FillSpec, type FontEntry, type FontGlyph, type FontStyle, type MaterialStop, type ParsedFont, type Profile, type TextPolygonsOptions, type WarpOptions, type WarpShape, composeText, cssCubicBezier, googleFontUrl, listGoogleFonts, loadFont, loadGoogleFont, makeFillTexture, parseFont, pickWeight, resolveFace, textPolygons };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ function bt(t,n){return String.fromCharCode(t.getUint8(n),t.getUint8(n+1),t.getUint8(n+2),t.getUint8(n+3))}function tt(t,n=8){let e=t instanceof Uint8Array?t:new Uint8Array(t),r=new DataView(e.buffer,e.byteOffset,e.byteLength),s=r.getUint32(0);if(s===1330926671)throw new Error("parseFont: CFF/OpenType (.otf) outlines are not supported \u2014 use a TrueType (.ttf) font");if(s!==65536&&s!==1953658213)throw new Error(`parseFont: not a TrueType font (sfnt 0x${s.toString(16)})`);let c=r.getUint16(4),i=new Map;for(let p=0;p<c;p++){let x=12+p*16;i.set(bt(r,x),{offset:r.getUint32(x+8),length:r.getUint32(x+12)})}let o=p=>{let x=i.get(p);if(!x)throw new Error(`parseFont: missing required '${p}' table`);return x.offset},u=o("head"),m=r.getUint16(u+18),f=r.getInt16(u+50),a=r.getUint16(o("maxp")+4),l=o("hhea"),h=r.getInt16(l+4),d=r.getInt16(l+6),b=r.getInt16(l+8),w=r.getUint16(l+34),z=o("hmtx"),A=p=>{let x=p<w?p:w-1;return r.getUint16(z+x*4)},G=o("loca"),V=o("glyf"),D=p=>f===0?[V+r.getUint16(G+p*2)*2,V+r.getUint16(G+(p+1)*2)*2]:[V+r.getUint32(G+p*4),V+r.getUint32(G+(p+1)*4)],$=dt(r,o("cmap")),L=(p,x=0)=>{if(p<0||p>=a||x>8)return[];let[T,v]=D(p);if(T>=v)return[];let g=T,O=r.getInt16(g);if(g+=10,O>=0){let E=[];for(let F=0;F<O;F++)E.push(r.getUint16(g)),g+=2;let I=E.length?E[E.length-1]+1:0,M=r.getUint16(g);g+=2+M;let C=new Uint8Array(I);for(let F=0;F<I;){let U=r.getUint8(g++);if(C[F++]=U,U&8){let W=r.getUint8(g++);for(;W-- >0&&F<I;)C[F++]=U}}let y=(F,U)=>{let W=new Array(I),X=0;for(let H=0;H<I;H++){let Z=C[H];if(Z&F){let ot=r.getUint8(g++);X+=Z&U?ot:-ot}else Z&U||(X+=r.getInt16(g),g+=2);W[H]=X}return W},S=y(2,16),Y=y(4,32),P=[],k=0;for(let F of E){let U=[],W=[];for(let X=k;X<=F;X++)U.push([S[X],Y[X]]),W.push((C[X]&1)!==0);U.length&&P.push({pts:U,on:W}),k=F+1}return P}let B=[],R=!0;for(;R;){let E=r.getUint16(g),I=r.getUint16(g+2);g+=4;let M=0,C=0;E&1?(M=r.getInt16(g),C=r.getInt16(g+2),g+=4):(M=r.getInt8(g),C=r.getInt8(g+1),g+=2);let y=U=>r.getInt16(U)/16384,S=1,Y=0,P=0,k=1;E&8?(S=k=y(g),g+=2):E&64?(S=y(g),k=y(g+2),g+=4):E&128&&(S=y(g),Y=y(g+2),P=y(g+4),k=y(g+6),g+=8);let F=(E&2)!==0;for(let U of L(I,x+1))B.push({on:U.on,pts:U.pts.map(([W,X])=>[S*W+P*X+(F?M:0),Y*W+k*X+(F?C:0)])});R=(E&32)!==0}return B};return{unitsPerEm:m,ascender:h,descender:d,lineGap:b,glyph:(p,x=n)=>{let T=$(p),v=Math.max(1,Math.round(x));return{contours:L(T).map(O=>Mt(O.pts,O.on,v)).filter(O=>O.length>=2),advanceWidth:A(T)}}}}function dt(t,n){let e=t.getUint16(n+2),r=-1,s=-1;for(let i=0;i<e;i++){let o=n+4+i*8,u=t.getUint16(o),m=t.getUint16(o+2),f=t.getUint32(o+4),a=t.getUint16(n+f),l=0;if(a===12)l+=4;else if(a===4)l+=2;else continue;u===3&&(m===1||m===10)&&(l+=1),u===0&&(l+=1),l>s&&(s=l,r=n+f)}if(r<0)throw new Error("parseFont: no supported cmap subtable (need format 4 or 12)");return t.getUint16(r)===12?yt(t,r):xt(t,r)}function xt(t,n){let e=t.getUint16(n+6),r=e/2,s=n+14,c=s+e+2,i=c+e,o=i+e;return u=>{if(u>65535)return 0;for(let m=0;m<r;m++){if(t.getUint16(s+m*2)<u)continue;if(t.getUint16(c+m*2)>u)return 0;let f=t.getInt16(i+m*2),a=t.getUint16(o+m*2);if(a===0)return u+f&65535;let l=t.getUint16(c+m*2),h=o+m*2+a+(u-l)*2,d=t.getUint16(h);return d===0?0:d+f&65535}return 0}}function yt(t,n){let e=t.getUint32(n+12),r=n+16;return s=>{for(let c=0;c<e;c++){let i=r+c*12,o=t.getUint32(i),u=t.getUint32(i+4);if(s<o)return 0;if(s<=u)return t.getUint32(i+8)+(s-o)}return 0}}function Mt(t,n,e){let r=t.length;if(r<2)return t.slice();let s=[],c=[];for(let a=0;a<r;a++){s.push(t[a]),c.push(n[a]);let l=(a+1)%r;!n[a]&&!n[l]&&(s.push([(t[a][0]+t[l][0])/2,(t[a][1]+t[l][1])/2]),c.push(!0))}let i=c.indexOf(!0);if(i<0){let a=s.length;s.unshift([(s[a-1][0]+s[0][0])/2,(s[a-1][1]+s[0][1])/2]),c.unshift(!0),i=0}let o=s.length,u=[s[i]],m=s[i],f=1;for(;f<=o;){let a=(i+f)%o;if(c[a])u.push(s[a]),m=s[a],f+=1;else{let l=s[a],h=s[(i+f+1)%o];for(let d=1;d<=e;d++){let b=d/e,w=1-b;u.push([w*w*m[0]+2*w*b*l[0]+b*b*h[0],w*w*m[1]+2*w*b*l[1]+b*b*h[1]])}m=h,f+=2}}if(u.length>1){let a=u[0],l=u[u.length-1];Math.hypot(a[0]-l[0],a[1]-l[1])<1e-6&&u.pop()}return u}import Pt from"earcut";function ct([t,n,e,r]){let s=3*t,c=3*(e-t)-s,i=1-s-c,o=3*n,u=3*(r-n)-o,m=1-o-u,f=h=>((i*h+c)*h+s)*h,a=h=>((m*h+u)*h+o)*h,l=h=>(3*i*h+2*c)*h+s;return h=>{let d=Math.min(1,Math.max(0,h)),b=d;for(let w=0;w<8;w++){let z=f(b)-d;if(Math.abs(z)<1e-6)break;let A=l(b);if(Math.abs(A)<1e-6)break;b-=z/A}return a(Math.min(1,Math.max(0,b)))}}var Ct=(t,n,e)=>(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0]);function Ft(t,n){let e=t.length;if(e<3||new Set(t).size!==e)return!1;let r=0;for(let s=0;s<e;s++){let c=Ct(n(t[s]),n(t[(s+1)%e]),n(t[(s+2)%e]));if(Math.abs(c)<1e-9)continue;let i=c>0?1:-1;if(r===0)r=i;else if(i!==r)return!1}return!0}function St(t,n,e,r){let s=a=>{let l=a.indexOf(e);if(l<0)return null;if(a[(l+1)%a.length]===r)return[e,r];let h=a.indexOf(r);return h>=0&&a[(h+1)%a.length]===e?[r,e]:null},c=s(t),i=s(n);if(!c||!i||c[0]===i[0])return null;let[o,u]=c,m=(a,l,h)=>{let d=[],b=a.indexOf(l);for(let w=0;w<a.length&&(d.push(a[b]),a[b]!==h);w++)b=(b+1)%a.length;return d},f=m(t,u,o).concat(m(n,o,u).slice(1,-1));return f.length>=3?f:null}function Ut(t,n){let e=i=>[t[i*2],t[i*2+1]],r=[];for(let i=0;i<n.length;i+=3)r.push([n[i],n[i+1],n[i+2]]);let s=t.length/2+1,c=!0;for(;c;){c=!1;let i=new Map;for(let o=0;o<r.length;o++){let u=r[o];if(u)for(let m=0;m<u.length;m++){let f=u[m],a=u[(m+1)%u.length],l=f<a?f*s+a:a*s+f,h=i.get(l);h||i.set(l,h=[]),h.push(o)}}for(let[o,u]of i){if(u.length!==2)continue;let[m,f]=u,a=r[m],l=r[f];if(!a||!l)continue;let h=Math.floor(o/s),d=o%s,b=St(a,l,h,d);b&&Ft(b,e)&&(r[m]=b,r[f]=null,c=!0)}}return r.filter(i=>i!==null)}var wt=(t,n)=>[-t[1],t[0],n];function N(t,n){let{profile:e,profileSegments:r,maxInset:s}=n,c=n.layered??!1,i=n.zOffset??0,o=c?Math.max(n.depth,1):n.depth,u=i+o/2,m=i-o/2,f=Et(e,u,m,o,r,s,n.roundConvex??!1,n.profileBezier),a=[],l=[0,0],h=n.backOffset??l,d=(p,x,T)=>wt([p[0]+T[0],p[1]+T[1]],x),b=n.faceUvBounds,w=b?Math.max(b.maxX-b.minX,1e-6):1,z=b?Math.max(b.maxY-b.minY,1e-6):1,A=(p,x)=>x>0?[(p[0]-b.minX)/x,(p[1]-b.minY)/x]:[Math.min(1,Math.max(0,(p[0]-b.minX)/w)),Math.min(1,Math.max(0,(p[1]-b.minY)/z))],G={s:"repeat",t:"repeat"},V=n.outlineColor?Math.max(0,n.outlineWidth??0):0,D=n.stops.length?[...n.stops].sort((p,x)=>p.at-x.at):[{at:.5}],$=p=>{let x=D[0],T=1/0;for(let v of D){let g=Math.abs(v.at-p);g<=T&&(T=g,x=v)}return x},L=p=>o>0?Math.min(1,Math.max(0,(u-p)/o)):0,j=f.reduce((p,x)=>Math.max(p,x.inset),0);for(let p of t){let x=[p.outer,...p.holes],T=j>1e-6?Math.min(1,kt(x,j)/j):1,v=O=>O*T,g=(O,B,R,E,I)=>{let M=[],C=[];for(let P=0;P<x.length;P++){P>0&&C.push(M.length/2);for(let[k,F]of et(x[P],O))M.push(k,F)}let y=Pt(M,C,2),S=P=>[M[P*2],M[P*2+1]],Y=I.tile??0;for(let P of Ut(M,y)){let F=(E?P.slice().reverse():P).map(S),U={vertices:F.map(W=>d(W,B,R)),color:I.color??"#cccccc"};I.texture&&b&&(U.texture=I.texture,U.material={texture:I.texture},U.uvs=F.map(W=>A(W,Y)),Y>0&&(U.textureWrap=G)),a.push(U)}};if(V>0&&g(-V,u-.001,l,!1,{at:0,color:n.outlineColor}),g(v(f[0].inset),f[0].z,l,!1,$(0)),c){g(v(f[f.length-1].inset),m,h,!0,$(1));continue}g(v(f[f.length-1].inset),f[f.length-1].z,l,!0,$(1));for(let O of x){let B=et(O,v(f[0].inset));for(let R=1;R<f.length;R++){let E=et(O,v(f[R].inset)),I=f[R-1].z,M=f[R].z,C=$(L((I+M)/2)),y=C.tile??0;for(let S=0,Y=O.length;S<Y;S++){let P=(S+1)%Y,k={vertices:[d(E[S],M,l),d(E[P],M,l),d(B[P],I,l),d(B[S],I,l)],color:C.color??"#cccccc"};C.texture&&(k.texture=C.texture,k.material={texture:C.texture},y>0||!b?k.uvs=[[0,1],[1,1],[1,0],[0,0]]:k.uvs=[A(E[S],0),A(E[P],0),A(B[P],0),A(B[S],0)]),a.push(k)}B=E}}}return a}function Et(t,n,e,r,s,c,i,o){if(t==="flat"||r<=0)return[{z:n,inset:0},{z:e,inset:0}];let u=Math.min(c,r/2),m=t==="bevel"?1:Math.max(2,s),f=t==="bevel"?l=>1-l:t==="custom"&&o?(l=>h=>1-l(h))(ct(o)):i?l=>1-Math.sin(l*Math.PI/2):l=>Math.cos(l*Math.PI/2),a=[];for(let l=0;l<=m;l++){let h=l/m;a.push({z:n-h*u,inset:u*f(h)})}e+u<n-u-1e-6&&a.push({z:e+u,inset:0});for(let l=1;l<=m;l++){let h=1-l/m;a.push({z:e+u*h,inset:u*f(h)})}return a}function kt(t,n){let e=[];t.forEach((s,c)=>{let i=s.length;for(let o=0;o<i;o++){let u=s[o],m=s[(o+1)%i];e.push({x:u[0],y:u[1],c,e:o,n:i}),e.push({x:(u[0]+m[0])/2,y:(u[1]+m[1])/2,c,e:o+.5,n:i})}});let r=1/0;for(let s=0;s<e.length;s++)for(let c=s+1;c<e.length;c++){if(e[s].c===e[c].c){let m=Math.abs(e[s].e-e[c].e);if(m<=1.5||m>=e[s].n-1.5)continue}let i=e[s].x-e[c].x,o=e[s].y-e[c].y,u=i*i+o*o;u<r&&(r=u)}return Math.min(n,Math.sqrt(r)*.4)}function st(t,n){let e=n[0]-t[0],r=n[1]-t[1],s=Math.hypot(e,r)||1;return[-r/s,e/s]}function et(t,n){if(n===0)return t;let e=t.length,r=new Array(e);for(let s=0;s<e;s++){let c=t[(s-1+e)%e],i=t[s],o=t[(s+1)%e],u=st(c,i),m=st(i,o),f=u[0]+m[0],a=u[1]+m[1],l=Math.hypot(f,a)||1;f/=l,a/=l;let h=f*u[0]+a*u[1],d=n*Math.min(1.5,1/Math.max(h,.001));r[s]=[i[0]+f*d,i[1]+a*d]}return r}function vt(t,n,e){let r=e[0]-n[0],s=e[1]-n[1],c=Math.hypot(r,s);return c<1e-9?Math.hypot(t[0]-n[0],t[1]-n[1]):Math.abs((t[0]-n[0])*s-(t[1]-n[1])*r)/c}function q(t,n){if(t.length<3)return t;let e=t[0],r=t[t.length-1],s=0,c=0;for(let i=1;i<t.length-1;i++){let o=vt(t[i],e,r);o>s&&(s=o,c=i)}if(s>n){let i=q(t.slice(0,c+1),n),o=q(t.slice(c),n);return i.slice(0,-1).concat(o)}return[e,r]}function ut(t,n){if(n<=0||t.length<5)return t;let e=1/0,r=-1/0,s=1/0,c=-1/0;for(let[l,h]of t)l<e&&(e=l),l>r&&(r=l),h<s&&(s=h),h>c&&(c=h);let i=Math.min(n,Math.hypot(r-e,c-s)*.12);if(i<=.001)return t;let o=0,u=-1;for(let l=1;l<t.length;l++){let h=Math.hypot(t[l][0]-t[0][0],t[l][1]-t[0][1]);h>u&&(u=h,o=l)}let m=q(t.slice(0,o+1),i),f=q([...t.slice(o),t[0]],i),a=m.concat(f.slice(1,-1));return a.length>=3?a:t}function J(t,n=.05){let e=[];for(let r of t){let s=e[e.length-1];(!s||Math.hypot(r[0]-s[0],r[1]-s[1])>n)&&e.push(r)}for(;e.length>1;){let r=e[0],s=e[e.length-1];if(Math.hypot(r[0]-s[0],r[1]-s[1])<=n)e.pop();else break}return e}function at(t){let n=0;for(let e=0,r=t.length;e<r;e++){let[s,c]=t[e],[i,o]=t[(e+1)%r];n+=s*o-i*c}return n/2}function Ot(t,n){let e=!1;for(let r=0,s=n.length-1;r<n.length;s=r++){let[c,i]=n[r],[o,u]=n[s];i>t[1]!=u>t[1]&&t[0]<(o-c)*(t[1]-i)/(u-i)+c&&(e=!e)}return e}function it(t,n){return at(t)>0===n?t:t.slice().reverse()}function K(t){let n=t.filter(o=>o.length>=3),e=n.length,r=new Array(e).fill(0),s=new Array(e).fill(-1);for(let o=0;o<e;o++){let u=n[o][0],m=-1,f=1/0;for(let a=0;a<e;a++)if(o!==a&&Ot(u,n[a])){r[o]++;let l=Math.abs(at(n[a]));l<f&&(f=l,m=a)}s[o]=m}let c=[],i=new Map;for(let o=0;o<e;o++)r[o]%2===0&&(i.set(o,c.length),c.push({outer:it(n[o],!0),holes:[]}));for(let o=0;o<e;o++)if(r[o]%2===1){let u=i.get(s[o]);u!==void 0&&c[u].holes.push(it(n[o],!1))}return c}function _(t,n){let e=/^#?([0-9a-f]{6})$/i.exec(t.trim());if(!e)return t;let r=parseInt(e[1],16),s=Math.round((r>>16&255)*n),c=Math.round((r>>8&255)*n),i=Math.round((r&255)*n);return`#${(s<<16|c<<8|i).toString(16).padStart(6,"0")}`}function It(t,n,e={}){let r=e.size??100,s=e.depth??r*.2,c=Math.max(1,Math.round(e.curveSteps??6)),i=e.letterSpacing??0,o=e.color??"#d4a82a",u=e.sideColor??_(o,.72),m=e.profile??"flat",f=Math.max(1,Math.round(e.profileSegments??6)),a=r/t.unitsPerEm,h=[...n].map(z=>t.glyph(z.codePointAt(0)??0,c)),d=0;for(let z of h)d+=z.advanceWidth*a+i;let b=-d/2,w=[];for(let z of h){if(z.contours.length){let A=z.contours.map(G=>J(G.map(([V,D])=>[V*a+b,D*a])));w.push(...K(A))}b+=z.advanceWidth*a+i}return N(w,{depth:s,profile:m,profileSegments:f,maxInset:r*.045,stops:[{at:0,color:o},{at:.5,color:u},{at:1,color:o}]})}function Tt(t){return!t||t==="flat"?{profile:"flat"}:"edge"in t?{profile:t.edge,roundConvex:t.raised,segments:t.segments}:{profile:"custom",profileBezier:t.curve,segments:t.segments}}function zt(t,n){if(t<=n||t<=1e-6)return 2;let e=Math.ceil(Math.PI/(2*Math.acos(1-n/t)));return Math.min(6,Math.max(2,e))}function Wt(t,n){let e=(r,s,c)=>({at:s,color:r.color??c,texture:r.texture,tile:r.tile});if(Array.isArray(t))return{stops:t.length?t.map(r=>e(r,r.at,n)):[{at:.5,color:n}]};if(t&&("front"in t||"sides"in t||"back"in t)){let r=t.front??{},s=r.color??n,c=[{f:r,color:s}];t.sides&&c.push({f:t.sides,color:_(s,.72)}),t.back!==!1&&c.push({f:t.back??{},color:s});let i=c.length;return{stops:c.map((u,m)=>e(u.f,(m+.5)/i,u.color)),backOffset:t.back?t.back.offset:void 0}}return t?{stops:[e(t,.5,n)]}:{stops:[{at:1/6,color:n},{at:.5,color:_(n,.72)},{at:5/6,color:n}]}}function At(t,n,e={}){let r=e.size??100,s=e.depth??r*.2,c=Math.max(1,Math.round(e.curveSteps??6)),i=e.letterSpacing??0,o=(e.lineHeight??1.25)*r,u=e.align??"center",m=Math.max(0,e.simplify??0),[f,a]=e.scale??[1,1],{stops:l,backOffset:h}=Wt(e.faces,"#d4a82a"),d=Tt(e.profile),b=Math.min(r*.045,s/2),w=d.profile==="round"?zt(b,r*.004):6,z=Math.max(1,Math.round(d.segments??w)),A=s<=0,G=r/t.unitsPerEm,V=r*.06,D=-r*.14,$=r*.26,j=n.split(`
2
+ `).map(M=>{let C=[...M].map(S=>t.glyph(S.codePointAt(0)??0,c)),y=0;for(let S of C)y+=S.advanceWidth*G*f+i;return y=Math.max(0,y-i),{glyphs:C,width:y}}),p=Math.max(1,...j.map(M=>M.width)),x=j.length,T=Bt(e.warp,-p/2,p,r),v=[];j.forEach((M,C)=>{let y=((x-1)/2-C)*o-r*.34,S=-p/2;u==="center"?S+=(p-M.width)/2:u==="right"&&(S+=p-M.width);let Y=S;for(let P of M.glyphs){if(P.contours.length){let k=P.contours.map(F=>J(F.map(([U,W])=>[U*G*f+Y,W*G*a+y])));for(let F of K(k)){let U=m>0&&F.holes.length===0?{outer:ut(F.outer,m),holes:F.holes}:F;v.push(nt(U,T))}}Y+=P.advanceWidth*G*f+i}if(M.width>0){let P=S,k=S+M.width;e.underline&&v.push(nt(lt(P,k,y+D-V,y+D),T)),e.strike&&v.push(nt(lt(P,k,y+$-V/2,y+$+V/2),T))}});let g=1/0,O=1/0,B=-1/0,R=-1/0;for(let M of v)for(let[C,y]of M.outer)C<g&&(g=C),C>B&&(B=C),y<O&&(O=y),y>R&&(R=y);let E=v.length?{minX:g,minY:O,maxX:B,maxY:R}:void 0;return N(v,{depth:s,profile:d.profile,roundConvex:d.roundConvex,profileBezier:d.profileBezier,profileSegments:z,maxInset:r*.045,stops:l,faceUvBounds:E,backOffset:h,layered:A,outlineColor:e.outline?.color,outlineWidth:e.outline?.width})}function nt(t,n){return n?{outer:t.outer.map(n),holes:t.holes.map(e=>e.map(n))}:t}function lt(t,n,e,r,s=24){let c=[];for(let i=0;i<=s;i++)c.push([t+(n-t)*i/s,e]);for(let i=s;i>=0;i--)c.push([t+(n-t)*i/s,r]);return{outer:c,holes:[]}}function Bt(t,n,e,r){if(!t||t.shape==="none")return null;let s=Math.max(0,Math.min(1,t.amount??.5));if(s===0)return null;let c=o=>(o-n)/e,i=o=>1-(2*o-1)*(2*o-1);switch(t.shape){case"arch":return o=>[o[0],o[1]+s*r*.7*i(c(o[0]))];case"archDown":return o=>[o[0],o[1]-s*r*.7*i(c(o[0]))];case"wave":return o=>[o[0],o[1]+s*r*.4*Math.sin(2*Math.PI*c(o[0]))];case"bulge":return o=>[o[0],o[1]*(1+s*i(c(o[0])))];case"cone":return o=>[o[0],o[1]*(1-s*.75*c(o[0]))];case"slantUp":return o=>[o[0],o[1]+s*r*.6*(c(o[0])-.5)];case"slantDown":return o=>[o[0],o[1]-s*r*.6*(c(o[0])-.5)];case"arc":{let o=Math.max(.08,s)*Math.PI,u=e/o,m=n+e/2;return f=>{let a=(c(f[0])-.5)*o,l=u+f[1];return[m+l*Math.sin(a),-u+l*Math.cos(a)]}}default:return null}}function Rt(t){switch(t.kind){case"solid":return{color:t.color};case"gradient":return{color:t.color,texture:rt({type:"gradient",from:t.from,to:t.to,angle:t.angle})};case"rainbow":return{color:t.color,texture:rt({type:"rainbow",angle:t.angle})};case"texture":return{color:t.color,texture:t.url||void 0,tile:t.tile};case"image":return{color:t.color,texture:t.src||void 0}}}var ft=["#ff3b30","#ff9500","#ffcc00","#34c759","#00c7be","#007aff","#5856d6","#af52de"];function Gt(t,n){let e=document.createElement("canvas");return e.width=t,e.height=n,e}function mt(t,n,e,r,s){let c=r*Math.PI/180,i=Math.cos(c),o=-Math.sin(c),u=n/2,m=e/2,f=(Math.abs(i)*n+Math.abs(o)*e)/2,a=t.createLinearGradient(u-i*f,m-o*f,u+i*f,m+o*f);for(let[l,h]of s)a.addColorStop(l,h);t.fillStyle=a,t.fillRect(0,0,n,e)}function rt(t){if(t.type==="solid")return;if(t.type==="image")return t.src||void 0;let n=256,e=Gt(n,n),r=e.getContext("2d");if(r){if(t.type==="rainbow"){let s=t.angle??0,c=ft.map((i,o)=>[o/(ft.length-1),i]);mt(r,n,n,s,c)}else{let s=t.angle??270;mt(r,n,n,s,[[0,t.from],[1,t.to]])}return e.toDataURL("image/png")}}var Vt="https://api.fontsource.org/v1/fonts",Yt=[700,400,500,600,800,300,900,200,100],Q=null;async function Xt(){if(Q)return Q;let t=await fetch(Vt);if(!t.ok)throw new Error(`font list ${t.status}`);return Q=(await t.json()).filter(e=>e.type==="google"&&e.styles.includes("normal")).sort((e,r)=>e.family.localeCompare(r.family)),Q}function ht(t,n){if(n&&t.weights.includes(n))return n;for(let e of Yt)if(t.weights.includes(e))return e;return t.weights[0]??400}function pt(t,n,e="normal"){let r=ht(t,n),s=t.subsets.includes("latin")?"latin":t.defSubset,c=e==="italic"&&t.styles.includes("italic")?"italic":"normal";return`https://cdn.jsdelivr.net/fontsource/fonts/${t.id}@latest/${s}-${r}-${c}.ttf`}async function gt(t){let n=await fetch(t);if(!n.ok)throw new Error(`load font ${n.status}: ${t}`);return tt(await n.arrayBuffer())}async function Dt(t,n,e="normal"){return gt(pt(t,n,e))}export{At as composeText,ct as cssCubicBezier,pt as googleFontUrl,Xt as listGoogleFonts,gt as loadFont,Dt as loadGoogleFont,rt as makeFillTexture,tt as parseFont,ht as pickWeight,Rt as resolveFace,It as textPolygons};
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@glyphcss/fonts",
3
+ "version": "0.0.1",
4
+ "description": "Turn fonts + text into extruded 3D polygon meshes for glyphcss. Framework-agnostic — returns Polygon[].",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "keywords": [
10
+ "glyphcss",
11
+ "ascii",
12
+ "font",
13
+ "text",
14
+ "3d",
15
+ "extrude",
16
+ "mesh",
17
+ "ttf",
18
+ "opentype",
19
+ "truetype"
20
+ ],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/apresmoi/glyphcss.git",
25
+ "directory": "packages/fonts"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/apresmoi/glyphcss/issues"
29
+ },
30
+ "homepage": "https://github.com/apresmoi/glyphcss#readme",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js",
38
+ "require": "./dist/index.cjs"
39
+ }
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "earcut": "^3.0.1",
46
+ "@glyphcss/core": "^0.0.5"
47
+ },
48
+ "devDependencies": {
49
+ "@types/earcut": "^3.0.0",
50
+ "tsup": "^8.0.1",
51
+ "typescript": "^5.3.3",
52
+ "vitest": "^3.1.1",
53
+ "@vitest/coverage-v8": "^3.1.1"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "test": "vitest run --passWithNoTests",
58
+ "test:coverage": "vitest run --coverage --passWithNoTests"
59
+ }
60
+ }