Add fraktur E and G (#2448)
* Add - MATHEMATICAL FRAKTUR CAPITAL E (`U+1D508`) (#444). - MATHEMATICAL FRAKTUR CAPITAL G (`U+1D50A`) (#444). * Cleanup * Refine
This commit is contained in:
parent
d08e7db129
commit
a9a0556b17
14 changed files with 716 additions and 361 deletions
|
@ -9,6 +9,8 @@
|
|||
- LATIN SMALL LETTER BLACKLETTER E (`U+AB32`) (#2443).
|
||||
- LATIN SMALL LETTER BLACKLETTER O (`U+AB3D`) (#2443).
|
||||
- LATIN SMALL LETTER BLACKLETTER O WITH STROKE (`U+AB3E`) (#2443).
|
||||
- MATHEMATICAL FRAKTUR CAPITAL E (`U+1D508`) (#444).
|
||||
- MATHEMATICAL FRAKTUR CAPITAL G (`U+1D50A`) (#444).
|
||||
- MATHEMATICAL FRAKTUR CAPITAL J (`U+1D50D`) (#444).
|
||||
- MATHEMATICAL FRAKTUR SMALL E (`U+1D522`) (#444).
|
||||
- MATHEMATICAL FRAKTUR SMALL O (`U+1D52C`) (#444).
|
||||
|
|
|
@ -4,7 +4,7 @@ import [mix linreg clamp fallback boole boolePn] from "@iosevka/util"
|
|||
import [Transform] from "@iosevka/geometry/transform"
|
||||
import [Interpolator WithKnotProxy] from "@iosevka/geometry/spiro-control"
|
||||
import [RadicalGeometry StrokeGeometry RemoveHolesGeometry] from "@iosevka/geometry"
|
||||
import [CMixCoord] from "@iosevka/font-kits/derived-coordinates"
|
||||
import [CMixCoord CopyBackKnotProxy] from "@iosevka/font-kits/derived-coordinates"
|
||||
|
||||
glyph-module
|
||||
|
||||
|
@ -512,7 +512,7 @@ glyph-block CommonShapes : begin
|
|||
return : HookShape before after false args
|
||||
|
||||
define [hookProxy args] : begin
|
||||
return : g4 [new CMixCoord 0.5] args.y
|
||||
return : new CopyBackKnotProxy [g4 [new CMixCoord 0.5] args.y] args 2
|
||||
|
||||
glyph-block-export hookstart
|
||||
define flex-params [hookstart] : begin
|
||||
|
@ -566,17 +566,40 @@ glyph-block CommonShapes : begin
|
|||
if args.rtl g2.left.mid g2.right.mid
|
||||
return : knotType (args.x + italicAdj) (args.y + overshoot) af
|
||||
|
||||
define [mixR w v u sw] : begin
|
||||
if (w < v) : return : 1 - [mixR v w u sw]
|
||||
local superness DesignParameters.superness
|
||||
local r : 1 / ([Math.pow (1 - [Math.pow (1 - v / w) superness]) (1 / superness)] + 1)
|
||||
set r : 0.5 + (r - 0.5) * (v + w) / (u * 2)
|
||||
return r
|
||||
define [superSin angle superness] : begin
|
||||
if [not angle] : return 0
|
||||
local s : Math.sin (angle * Math.PI / 180)
|
||||
local sign : if (s < 0) (-1) 1
|
||||
return : sign * [Math.pow [Math.abs s] (2 / superness)]
|
||||
|
||||
define [superCos angle superness] : begin
|
||||
if [not angle] : return 1
|
||||
local c : Math.cos (angle * Math.PI / 180)
|
||||
local sign : if (c < 0) (-1) 1
|
||||
return : sign * [Math.pow [Math.abs c] (2 / superness)]
|
||||
|
||||
define [mixR w angW v angV u] : begin
|
||||
if (w < v) : return : 1 - [mixR v angV w angW u]
|
||||
|
||||
local super DesignParameters.superness
|
||||
|
||||
if (angV != nothing && angW != nothing)
|
||||
: then : begin
|
||||
local radV : v / (1 - [superSin angV super])
|
||||
local radW : w / (1 - [superSin angW super])
|
||||
local chrV : radV * [superCos angV super]
|
||||
local chrW : radW * [superCos angW super]
|
||||
return : chrW / (chrV + chrW)
|
||||
: else : begin
|
||||
local r : 1 / ([Math.pow (1 - [Math.pow (1 - v / w) super]) (1 / super)] + 1)
|
||||
set r : 0.5 + (r - 0.5) * (v + w) / (u * 2)
|
||||
return r
|
||||
|
||||
define [archBlenderProxy args] : begin
|
||||
return : [if args.compact g2 g4]
|
||||
local p : [if args.compact g2 g4]
|
||||
new CMixCoord [fallback args.p 0.5] 0 args.mockPre args.mockPost
|
||||
begin args.y
|
||||
return : new CopyBackKnotProxy p args 2
|
||||
|
||||
define [archBlender _before _after args] : begin
|
||||
local before : fallback args.mockPre _before
|
||||
|
@ -585,8 +608,7 @@ glyph-block CommonShapes : begin
|
|||
local u : Math.abs (after.x - before.x)
|
||||
local v : Math.abs (after.y - args.y)
|
||||
local w : Math.abs (before.y - args.y)
|
||||
|
||||
local mixRatio : mixR w v u args.sw
|
||||
local mixRatio : mixR w args.anglePre v args.anglePost u
|
||||
|
||||
set args.x : mix before.x after.x [fallback args.p mixRatio]
|
||||
set args.rtl : before.x > after.x
|
||||
|
@ -603,13 +625,15 @@ glyph-block CommonShapes : begin
|
|||
local-parameter : sw -- Stroke
|
||||
local-parameter : compact -- false
|
||||
local-parameter : o -- O
|
||||
local-parameter : anglePre -- nothing
|
||||
local-parameter : anglePost -- nothing
|
||||
local-parameter : swBefore -- sw
|
||||
local-parameter : swAfter -- sw
|
||||
local-parameter : mockPre -- nothing
|
||||
local-parameter : mockPost -- nothing
|
||||
local-parameter : blendPre -- [arcvh]
|
||||
local-parameter : blendPost -- [archv]
|
||||
local args : object [lhs true] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost
|
||||
local-parameter : blendPre -- [if anglePre nothing [arcvh]]
|
||||
local-parameter : blendPost -- [if anglePost nothing [archv]]
|
||||
local args : object [lhs true] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost anglePre anglePost
|
||||
return : WithKnotProxy [archBlenderProxy args] : Interpolator archBlender args
|
||||
|
||||
export : define flex-params [rhs] : begin
|
||||
|
@ -618,13 +642,15 @@ glyph-block CommonShapes : begin
|
|||
local-parameter : sw -- Stroke
|
||||
local-parameter : compact -- false
|
||||
local-parameter : o -- O
|
||||
local-parameter : anglePre -- nothing
|
||||
local-parameter : anglePost -- nothing
|
||||
local-parameter : swBefore -- sw
|
||||
local-parameter : swAfter -- sw
|
||||
local-parameter : mockPre -- nothing
|
||||
local-parameter : mockPost -- nothing
|
||||
local-parameter : blendPre -- [arcvh]
|
||||
local-parameter : blendPost -- [archv]
|
||||
local args : object [lhs false] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost
|
||||
local-parameter : blendPre -- [if anglePre nothing [arcvh]]
|
||||
local-parameter : blendPost -- [if anglePost nothing [archv]]
|
||||
local args : object [lhs false] y p sw compact o swBefore swAfter mockPre mockPost blendPre blendPost anglePre anglePost
|
||||
return : WithKnotProxy [archBlenderProxy args] : Interpolator archBlender args
|
||||
|
||||
foreach side {lhs rhs} : begin
|
||||
|
|
|
@ -11,6 +11,7 @@ import [SpiroPenGeometry] from "@iosevka/geometry"
|
|||
import [Vec2] from "@iosevka/geometry/point"
|
||||
import [Box] from "@iosevka/geometry/box"
|
||||
import [Interpolator] from "@iosevka/geometry/spiro-control"
|
||||
import [PenKnotCollector] from "@iosevka/geometry/spiro-pen-expand"
|
||||
|
||||
glyph-module
|
||||
|
||||
|
@ -30,14 +31,23 @@ glyph-block LetterLike-Fraktur-Shared : begin
|
|||
this.knots = knots
|
||||
|
||||
public [applyToGlyph glyph] : begin
|
||||
local c : spiro-collect glyph this.knots
|
||||
local geom : new SpiroPenGeometry
|
||||
begin c.gizmo
|
||||
begin c.collector.closed
|
||||
this.profile.getPenShape c.gizmo
|
||||
c.collector.controls.map : function [k] [k.toMono]
|
||||
return : glyph.includeGeometry geom
|
||||
local defaultProfile : this.profile.getPenShape glyph.gizmo
|
||||
local collector : new PenKnotCollector glyph.gizmo defaultProfile
|
||||
local c : spiro-collect collector this.knots
|
||||
|
||||
local geom : new SpiroPenGeometry
|
||||
begin glyph.gizmo
|
||||
begin defaultProfile
|
||||
begin collector.closed
|
||||
begin collector.knots
|
||||
|
||||
glyph.includeGeometry geom
|
||||
return collector.knots
|
||||
|
||||
# Directive to change the profile
|
||||
glyph-block-export change-profile
|
||||
define [change-profile newProfile] : function : begin
|
||||
this.setProfile : newProfile.getPenShape this.gizmo
|
||||
|
||||
# A pen profile describes a virtual flat-tip pen. We use a 45-degree arrangement to
|
||||
# simplify the math.
|
||||
|
@ -126,47 +136,88 @@ glyph-block LetterLike-Fraktur-Shared : begin
|
|||
glyph-block LetterLike-Fraktur : begin
|
||||
glyph-block-import Common-Derivatives
|
||||
glyph-block-import CommonShapes
|
||||
glyph-block-import LetterLike-Fraktur-Shared : S F T fraktur-stroke
|
||||
glyph-block-import LetterLike-Fraktur-Shared : S F T fraktur-stroke change-profile
|
||||
glyph-block-import LetterLike-Fraktur-Shared : DecoSizeX DecoSizeY SlopeA SlopeB
|
||||
glyph-block-import LetterLike-Fraktur-Shared : Wave LbFootRise FHook PHexTop PHexBot
|
||||
|
||||
|
||||
define [LtDecorativeWave sx sy ey] : fraktur-stroke F
|
||||
g2.ld.start [F.connL S : S.xl sx] [F.connB S : S.yt sy]
|
||||
g2.ld.start sx sy
|
||||
~~~ [Wave.vc (-Wave.DepthX)]
|
||||
g2.ld.end pre@ (ey - 2 * S.thick)
|
||||
g2.ld.end pre@ (ey - 2 * S.thick)
|
||||
|
||||
create-glyph "frak/C" 0x212D : glyph-proc
|
||||
include : MarkSet.capital
|
||||
do "C, E, and G"
|
||||
define flex-params [FrakCTopAndCenterStroke] : glyph-proc
|
||||
local-parameter : top -- [S.yt CAP]
|
||||
local-parameter : bot -- [S.yp 0 CAP 0.375]
|
||||
local-parameter : left -- [S.xl SB]
|
||||
local-parameter : right -- [S.xr RightSB]
|
||||
|
||||
local xCenter : [mix SB RightSB 0.5] + 0.75 * F.thick
|
||||
local xCenter : [mix left right 0.5] + 0.25 * F.thick
|
||||
local deco : Math.max DecoSizeX (0.5 * ArchDepthA - 2 * S.thick)
|
||||
local waveTop : top - deco
|
||||
|
||||
# Top-right stroke
|
||||
include : fraktur-stroke S
|
||||
g2 [S.xr RightSB] [S.yt CAP]
|
||||
g2.left.mid [mix (xCenter + DecoSizeX) [S.xr RightSB] 0.5] ([S.yt CAP] - 0.5 * FHook)
|
||||
corner (xCenter + DecoSizeX) [S.yt CAP]
|
||||
corner xCenter ([S.yt CAP] - DecoSizeX)
|
||||
# Top-right stroke
|
||||
include : fraktur-stroke S
|
||||
g2 right top
|
||||
g2.left.mid [mix@ 0.5] (top - 0.5 * FHook)
|
||||
corner (post@ <+> deco) top
|
||||
corner xCenter (top - deco)
|
||||
|
||||
# Left and bottom stroke
|
||||
include : fraktur-stroke S
|
||||
g4.ld.start [S.xp SB RightSB 0.1] ([S.yt CAP] - 0.1 * ArchDepthA)
|
||||
flat [S.xl SB] ([S.yt CAP] - 0.6 * ArchDepthA)
|
||||
curl pre@ ([S.yb 0] + ArchDepthB)
|
||||
~~~ [hookend [S.yb 0] (sw -- S.thick)]
|
||||
g2 [S.xr RightSB] ([S.yb 0] + SHook)
|
||||
# Inner decoration
|
||||
include : fraktur-stroke F
|
||||
g2.ld.start [F.connL S xCenter] [F.connB S : top - deco]
|
||||
~~~ [Wave.vc (-Wave.DepthX)]
|
||||
g2.ld.end pre@ bot
|
||||
|
||||
# A thin connection between the two strokes
|
||||
include : fraktur-stroke T
|
||||
g4.ru.start [T.connR S : S.xp SB RightSB 0.1] [T.connT S : [S.yt CAP] - 0.1 * ArchDepthA]
|
||||
~~~ [arch.lhs [T.yt CAP] 0.6 (sw -- T.thick) (blendPre -- null) (blendPost -- null)]
|
||||
g4.ld.end [T.connL S xCenter] [T.connB S : [S.yt CAP] - DecoSizeX]
|
||||
return : object xCenter [waveTop : top - deco] [waveBot bot]
|
||||
|
||||
# Inner decoration
|
||||
include : fraktur-stroke F
|
||||
g2.ld.start [F.connL S xCenter] [F.connB S : [S.yt CAP] - DecoSizeX]
|
||||
~~~ [Wave.vc (-Wave.DepthX)]
|
||||
g2.ld.end pre@ (CAP * 0.375)
|
||||
define flex-params [FrakCOutlineShape] : glyph-proc
|
||||
local-parameter : mode -- 'C'
|
||||
local-parameter : top -- [S.yt CAP]
|
||||
local-parameter : bot -- [S.yb 0]
|
||||
local-parameter : left -- [S.xl SB]
|
||||
local-parameter : right -- [S.xr RightSB]
|
||||
|
||||
local [object xCenter waveTop waveBot] : include : FrakCTopAndCenterStroke
|
||||
bot -- ([mix bot top 0.1] + 2 * Wave.DepthY + 0.5 * S.thick)
|
||||
|
||||
# Left and bottom stroke
|
||||
local mainStroke : include : fraktur-stroke S
|
||||
g4.ru.start [T.connL S xCenter] [T.connB S waveTop] [change-profile T]
|
||||
arch.rhs [T.connT S top] (sw -- T.thick) (anglePre -- (-45)) (anglePost -- 0)
|
||||
flat left (pre@ <-> 0.6 * ArchDepthA) [change-profile S]
|
||||
curl pre@ (post@ <+> ArchDepthB)
|
||||
match mode
|
||||
: [Just 'C'] : list # C and E
|
||||
hookend (sw -- S.thick) bot
|
||||
g2 [T.connR S right] (pre@ <+> [T.connT S SHook]) [change-profile T]
|
||||
: [Just 'G'] : list # G
|
||||
arch.lhs (sw -- S.thick) bot
|
||||
flat right (pre@ <+> ArchDepthA)
|
||||
decor@ : curl pre@ (post@ <-> 0.5 * ArchDepthB)
|
||||
decor@@ : g2 [post@tang-out 1] [post@tang-out SlopeB]
|
||||
decor@ : corner [mix@rev PHexTop] [post@slope SlopeA]
|
||||
decor@ : corner xCenter [mix waveTop waveBot 0.5]
|
||||
|
||||
return : object xCenter waveTop waveBot
|
||||
|
||||
create-glyph "frak/C" 0x212D : glyph-proc
|
||||
include : MarkSet.capital
|
||||
include : FrakCOutlineShape (mode -- 'C')
|
||||
|
||||
create-glyph "frak/E" 0x1D508 : glyph-proc
|
||||
include [refer-glyph "frak/C"] AS_BASE ALSO_METRICS
|
||||
local [object xCenter waveTop waveBot] : include : FrakCOutlineShape (mode -- 'C')
|
||||
|
||||
include : fraktur-stroke F
|
||||
corner [F.connL S xCenter] [F.connB S : mix waveTop waveBot 0.5]
|
||||
corner (post@ <-> 0.5 * DecoSizeX) [pre@slope SlopeA]
|
||||
corner [F.xr RightSB] (pre@ <-> 0.5 * DecoSizeY)
|
||||
|
||||
create-glyph "frak/G" 0x1D50A : glyph-proc
|
||||
include : MarkSet.capital
|
||||
include : FrakCOutlineShape (mode -- 'G')
|
||||
|
||||
create-glyph "frak/H" 0x210C : glyph-proc
|
||||
include : MarkSet.capDesc
|
||||
|
@ -200,37 +251,39 @@ glyph-block LetterLike-Fraktur : begin
|
|||
g2.down.mid [S.xp xLeftStem RightSB 0.75] [S.yp Descender(XH - FHook) 0.25]
|
||||
g2 [S.xr RightSB] [S.yb Descender]
|
||||
|
||||
define [IJTopStroke] : glyph-proc
|
||||
include : fraktur-stroke S
|
||||
g2.ld.start [S.xr RightSB] [S.yt CAP]
|
||||
~~~ [Wave.h]
|
||||
g2.ld.mid [S.xl SB] [S.yt (CAP - Wave.DepthY)]
|
||||
include : LtDecorativeWave [S.xl SB] (CAP - Wave.DepthY) (CAP * 0.625)
|
||||
do "I, J"
|
||||
define [IJTopStroke] : glyph-proc
|
||||
include : fraktur-stroke S
|
||||
g2.ld.start [S.xr RightSB] [S.yt CAP]
|
||||
~~~ [Wave.h]
|
||||
g2.ld.end [S.xl SB] (pre@ <-> Wave.DepthY) [change-profile F]
|
||||
~~~ [Wave.vc (-Wave.DepthX)]
|
||||
g2.ld.end pre@ (CAP * 0.625 - 2 * F.thick)
|
||||
|
||||
create-glyph "frak/I" 0x2111 : glyph-proc
|
||||
include : MarkSet.capital
|
||||
include : IJTopStroke
|
||||
create-glyph "frak/I" 0x2111 : glyph-proc
|
||||
include : MarkSet.capital
|
||||
include : IJTopStroke
|
||||
|
||||
local iBox : S.box CAP 0 SB RightSB
|
||||
include : fraktur-stroke S
|
||||
g2.ld.start iBox.right iBox.top
|
||||
g4 [iBox.xp 0.7] [mix@ 0.375]
|
||||
g4 iBox.right (post@ <+> ArchDepthA)
|
||||
hookend (sw -- S.thick) iBox.bot
|
||||
g2 iBox.left (pre@ <+> SHook)
|
||||
local iBox : S.box CAP 0 SB RightSB
|
||||
include : fraktur-stroke S
|
||||
g2.ld.start iBox.right iBox.top
|
||||
g4 [iBox.xp 0.7] [mix@ 0.375]
|
||||
g4 iBox.right (post@ <+> ArchDepthA)
|
||||
hookend (sw -- S.thick) iBox.bot
|
||||
g2 iBox.left (pre@ <+> SHook)
|
||||
|
||||
create-glyph "frak/J" 0x1D50D : glyph-proc
|
||||
include : MarkSet.capDesc
|
||||
include : IJTopStroke
|
||||
create-glyph "frak/J" 0x1D50D : glyph-proc
|
||||
include : MarkSet.capDesc
|
||||
include : IJTopStroke
|
||||
|
||||
local jBox : S.box [S.yt (CAP - 1.5 * Wave.DepthY - 2 * S.thick)] Descender SB RightSB
|
||||
include : fraktur-stroke S
|
||||
g4 jBox.right (post@ <-> FHook)
|
||||
hookstart (sw -- S.thick) jBox.top
|
||||
g4 ([jBox.xp 0.6] - S.thick) (pre@ <-> 0.6 * ArchDepthA)
|
||||
g4 jBox.right (post@ <+> ArchDepthA)
|
||||
hookend (sw -- S.thick) jBox.bot
|
||||
g2 jBox.left (pre@ <+> SHook)
|
||||
local jBox : S.box [S.yt (CAP - 1.5 * Wave.DepthY - 2 * S.thick)] Descender SB RightSB
|
||||
include : fraktur-stroke S
|
||||
g4 [T.connR S jBox.right] (post@ <-> FHook) [change-profile T]
|
||||
hookstart (sw -- S.thick) [T.connT S jBox.top]
|
||||
g4 ([jBox.xp 0.6] - S.thick) (pre@ <-> 0.6 * ArchDepthA) [change-profile S]
|
||||
g4 jBox.right (post@ <+> ArchDepthA)
|
||||
hookend (sw -- S.thick) jBox.bot
|
||||
g2 jBox.left (pre@ <+> SHook)
|
||||
|
||||
create-glyph "frak/R" 0x211C : glyph-proc
|
||||
include : MarkSet.capital
|
||||
|
@ -242,7 +295,7 @@ glyph-block LetterLike-Fraktur : begin
|
|||
local adb : 0.6 * ArchDepthB
|
||||
|
||||
# Deocration at top-left
|
||||
include : LtDecorativeWave xExt (CAP - ltHook) (CAP * 0.625)
|
||||
# include : LtDecorativeWave [S.xl xExt] [S.yt (CAP - Wave.DepthY)] (CAP * 0.625)
|
||||
|
||||
local xMidStrokeL : S.xl xLeftStem
|
||||
local xMidStrokeR : S.xp xLeftStem RightSB 0.625
|
||||
|
@ -255,9 +308,11 @@ glyph-block LetterLike-Fraktur : begin
|
|||
local yArchStart : yArchTop - SlopeA * (xArchTop - xArchStart)
|
||||
|
||||
# Left stroke
|
||||
include : fraktur-stroke S
|
||||
g2.ru.start [S.xl xExt] [S.yt (CAP - ltHook)]
|
||||
~~~ [arch.rhs [S.yt CAP] 0.6 (blendPre -- null)]
|
||||
include : fraktur-stroke F
|
||||
g2.ru.start post@ (CAP * 0.625 - 2 * F.thick)
|
||||
~~~ [Wave.vc Wave.DepthX]
|
||||
g2.ru.start [S.xl xExt] (post@ <-> ltHook) [change-profile S]
|
||||
arch.rhs (blendPre -- null) [S.yt CAP] 0.625
|
||||
flat [S.xl xLeftStem] [Math.max ([S.yb CAP] - ada) yArchStart]
|
||||
curl (pre@ <+> 0) yMidStrokeL
|
||||
corner [S.xl xExt] (post@ <+> LbFootRise)
|
||||
|
|
|
@ -288,13 +288,13 @@ define-macro glyph-block : syntax-rules
|
|||
AdviceGlottalStopArchDepth StrokeWidthBlend ArchDepthAOf ArchDepthBOf SmoothAdjust
|
||||
MidJutSide MidJutCenter compositeBaseAnchors YSmoothMidR YSmoothMidL HSwToV
|
||||
NarrowUnicodeT WideUnicodeT VERY-FAR TINY]
|
||||
define spiroFnImports `[g4 g2 corner flat curl close end straight g2c cg2 flatc ccurl widths
|
||||
disable-contrast heading unimportant important alsoThru alsoThruThem bezControls
|
||||
define spiroFnImports `[g4 g2 corner flat curl virt close end straight g2c cg2 flatc ccurl
|
||||
widths disable-contrast heading unimportant important alsoThru alsoThruThem bezControls
|
||||
quadControls archv arcvh dispiro spiro-outline spiro-collect]
|
||||
|
||||
define booleFnImports `[union intersection difference]
|
||||
|
||||
define drvCoordImports `[pre@ post@ mix@ pre@slope post@slope
|
||||
define drvCoordImports `[pre@ post@ mix@ mix@rev pre@slope post@slope
|
||||
decor@ decor@@ decor@@@ pre@tang-in post@tang-in pre@tang-out post@tang-out]
|
||||
|
||||
dirty `[$GlyphBlocks$.push : lambda [$Capture_Ext$] : begin \\
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
DEP_SAME_X,
|
||||
DEP_SAME_Y,
|
||||
DerivedCoordinateBase,
|
||||
InterpolatorBase,
|
||||
} from "@iosevka/geometry/spiro-control";
|
||||
|
||||
const TINY = 1 / 128;
|
||||
|
@ -29,6 +28,7 @@ export function SetupBuilders(_bindings) {
|
|||
// mix@: mix between pre and post point's X or Y coordinates
|
||||
// usage [mix@ proportion] or [mix@ proportion delta]
|
||||
"mix@": (p, delta) => new CMixCoord(p, delta),
|
||||
"mix@rev": (p, delta) => new CMixCoord(1 - p, delta),
|
||||
|
||||
// pre@slope, post@slope: Get the coordiante using the pre/post point's coordinate and a
|
||||
// slope. An optional delta can be added to the result. See the definitions for more
|
||||
|
@ -169,3 +169,29 @@ class CAtSlopePost extends DerivedCoordinateBase {
|
|||
return post.x + (curr.y - post.y) / this.slope + this.delta;
|
||||
}
|
||||
}
|
||||
|
||||
const KBP_X = 1;
|
||||
const KBP_Y = 2;
|
||||
export class CopyBackKnotProxy {
|
||||
constructor(delegate, bag, options) {
|
||||
this.delegate = delegate;
|
||||
this.bag = bag;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
getDependency(stage) {
|
||||
return this.delegate.getDependency(stage);
|
||||
}
|
||||
getKernelKnot() {
|
||||
return this.delegate.getKernelKnot();
|
||||
}
|
||||
resolveCoordiantePropogation(ic, pre, post) {
|
||||
let r = this.delegate.resolveCoordiantePropogation(ic, pre, post);
|
||||
if (this.options & KBP_X) this.bag.x = this.delegate.x;
|
||||
if (this.options & KBP_Y) this.bag.y = this.delegate.y;
|
||||
return r;
|
||||
}
|
||||
resolveInterpolation() {
|
||||
return this.delegate.resolveInterpolation();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,68 @@
|
|||
import { DiSpiroGeometry, SpiroGeometry } from "@iosevka/geometry";
|
||||
import {
|
||||
BiKnotCollector,
|
||||
Interpolator,
|
||||
SpiroFlattener,
|
||||
TerminateInstruction,
|
||||
UserCloseKnotPair,
|
||||
UserControlKnot,
|
||||
VirtualControlKnot,
|
||||
} from "@iosevka/geometry/spiro-control";
|
||||
import { bez3, fallback, mix } from "@iosevka/util";
|
||||
import { BiKnotCollector } from "../../geometry/src/spiro-expand.mjs";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class SpiroImplBase {
|
||||
constructor(bindings, args) {
|
||||
constructor(bindings, controls) {
|
||||
this.bindings = bindings;
|
||||
this.args = args;
|
||||
this.controls = controls;
|
||||
}
|
||||
|
||||
createCollector(glyph) {
|
||||
const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
|
||||
|
||||
collectTo(collector) {
|
||||
const flattener = new SpiroFlattener();
|
||||
for (const control of this.args) flattener.add(control);
|
||||
for (const control of this.controls) flattener.add(control);
|
||||
flattener.flatten();
|
||||
|
||||
const collector = new BiKnotCollector(this.bindings.Contrast);
|
||||
flattener.pipe(collector);
|
||||
|
||||
return { gizmo, collector };
|
||||
}
|
||||
}
|
||||
|
||||
class DispiroImpl extends SpiroImplBase {
|
||||
constructor(bindings, args) {
|
||||
super(bindings, args);
|
||||
constructor(bindings, controls) {
|
||||
super(bindings, controls);
|
||||
}
|
||||
applyToGlyph(glyph) {
|
||||
const { gizmo, collector } = this.createCollector(glyph);
|
||||
const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
|
||||
const collector = new BiKnotCollector(this.bindings.Contrast);
|
||||
this.collectTo(collector);
|
||||
const dsp = new DiSpiroProxy(gizmo, collector);
|
||||
glyph.includeGeometry(dsp.geometry);
|
||||
return dsp;
|
||||
}
|
||||
}
|
||||
|
||||
class SpiroOutlineImpl extends SpiroImplBase {
|
||||
constructor(bindings, args) {
|
||||
super(bindings, args);
|
||||
constructor(bindings, controls) {
|
||||
super(bindings, controls);
|
||||
}
|
||||
applyToGlyph(glyph) {
|
||||
const { gizmo, collector } = this.createCollector(glyph);
|
||||
const gizmo = glyph.gizmo || this.bindings.GlobalTransform;
|
||||
const collector = new BiKnotCollector(this.bindings.Contrast);
|
||||
this.collectTo(collector);
|
||||
return glyph.includeGeometry(
|
||||
new SpiroGeometry(
|
||||
gizmo,
|
||||
collector.closed,
|
||||
collector.controls.map(k => k.toMono()),
|
||||
),
|
||||
new SpiroGeometry(gizmo, collector.closed, collector.getMonoKnots()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiSpiroProxy {
|
||||
constructor(gizmo, collector) {
|
||||
this.geometry = new DiSpiroGeometry(
|
||||
gizmo,
|
||||
collector.contrast,
|
||||
collector.closed,
|
||||
collector.controls,
|
||||
collector.knots,
|
||||
);
|
||||
this.m_origKnots = collector.controls;
|
||||
this.m_origKnots = collector.knots;
|
||||
}
|
||||
get knots() {
|
||||
return this.m_origKnots;
|
||||
|
@ -87,6 +84,12 @@ function KnotType(type) {
|
|||
};
|
||||
}
|
||||
|
||||
function virtualKnot(x, y, f) {
|
||||
if (!UserControlKnot.isCoordinateValid(x)) throw new TypeError("NaN detected for X");
|
||||
if (!UserControlKnot.isCoordinateValid(y)) throw new TypeError("NaN detected for Y");
|
||||
return new VirtualControlKnot(x, y, f);
|
||||
}
|
||||
|
||||
/// The builder for directed knot pairs
|
||||
class DirectedKnotPairBuilder {
|
||||
constructor(bindings, kPre, kCenter, kPost, deltaX, deltaY) {
|
||||
|
@ -113,6 +116,7 @@ export function SetupBuilders(bindings) {
|
|||
const corner = KnotType("corner");
|
||||
const flat = KnotType("left");
|
||||
const curl = KnotType("right");
|
||||
const virt = virtualKnot;
|
||||
const close = f => new TerminateInstruction("close", f);
|
||||
const end = f => new TerminateInstruction("end", f);
|
||||
|
||||
|
@ -401,16 +405,22 @@ export function SetupBuilders(bindings) {
|
|||
const s = fallback(_s, Superness);
|
||||
return 1 - Math.pow(1 - Math.pow(px, s), 1 / s);
|
||||
};
|
||||
archv.sCos = function (angle, _s) {
|
||||
return Math.pow(Math.cos((angle / 180) * Math.PI), 2 / fallback(_s, Superness));
|
||||
};
|
||||
archv.sSin = function (angle, _s) {
|
||||
return Math.pow(Math.sin((angle / 180) * Math.PI), 2 / fallback(_s, Superness));
|
||||
};
|
||||
|
||||
function dispiro(...args) {
|
||||
return new DispiroImpl(bindings, args);
|
||||
function dispiro(...controls) {
|
||||
return new DispiroImpl(bindings, controls);
|
||||
}
|
||||
function spiroOutline(...args) {
|
||||
return new SpiroOutlineImpl(bindings, args);
|
||||
function spiroOutline(...controls) {
|
||||
return new SpiroOutlineImpl(bindings, controls);
|
||||
}
|
||||
function spiroCollect(glyph, ...args) {
|
||||
const spb = new SpiroImplBase(bindings, args);
|
||||
return spb.createCollector(glyph);
|
||||
function spiroCollect(collector, ...controls) {
|
||||
const spb = new SpiroImplBase(bindings, controls);
|
||||
return spb.collectTo(collector);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -419,6 +429,7 @@ export function SetupBuilders(bindings) {
|
|||
corner,
|
||||
flat,
|
||||
curl,
|
||||
virt,
|
||||
close,
|
||||
end,
|
||||
straight,
|
||||
|
|
|
@ -5,7 +5,7 @@ import zlib from "zlib";
|
|||
import * as CurveUtil from "@iosevka/geometry/curve-util";
|
||||
import { encode, decode } from "@msgpack/msgpack";
|
||||
|
||||
const Edition = 44;
|
||||
const Edition = 45;
|
||||
const MAX_AGE = 16;
|
||||
class GfEntry {
|
||||
constructor(age, value) {
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"./curve-util": "./src/curve-util.mjs",
|
||||
"./point": "./src/point.mjs",
|
||||
"./transform": "./src/transform.mjs",
|
||||
"./spiro-control": "./src/spiro-control.mjs"
|
||||
"./spiro-control": "./src/spiro-control.mjs",
|
||||
"./spiro-expand": "./src/spiro-expand.mjs",
|
||||
"./spiro-pen-expand": "./src/spiro-pen-expand.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iosevka/util": "31.1.0",
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as CurveUtil from "./curve-util.mjs";
|
|||
import { Point } from "./point.mjs";
|
||||
import { QuadifySink } from "./quadify.mjs";
|
||||
import { SpiroExpander } from "./spiro-expand.mjs";
|
||||
import { createSpiroPenGeometry } from "./spiro-pen-expander.mjs";
|
||||
import { PenSpiroExpander } from "./spiro-pen-expand.mjs";
|
||||
import { spiroToOutlineWithSimplification } from "./spiro-to-outline.mjs";
|
||||
import { strokeArcs } from "./stroke.mjs";
|
||||
import { Transform } from "./transform.mjs";
|
||||
|
@ -141,23 +141,23 @@ export class SpiroGeometry extends CachedGeometry {
|
|||
}
|
||||
|
||||
export class SpiroPenGeometry extends CachedGeometry {
|
||||
constructor(gizmo, closed, pen, knots) {
|
||||
constructor(gizmo, penProfile, closed, knots) {
|
||||
super();
|
||||
this.m_gizmo = gizmo;
|
||||
this.m_penProfile = penProfile;
|
||||
this.m_closed = closed;
|
||||
this.m_knots = knots;
|
||||
this.m_pen = pen;
|
||||
}
|
||||
|
||||
toContoursImpl() {
|
||||
let contours = createSpiroPenGeometry(
|
||||
const expander = new PenSpiroExpander(
|
||||
this.m_gizmo,
|
||||
this.m_penProfile,
|
||||
this.m_closed,
|
||||
this.m_knots,
|
||||
this.m_pen,
|
||||
);
|
||||
|
||||
if (!contours.length) return [];
|
||||
let contours = expander.getGeometry();
|
||||
if (!contours || !contours.length) return [];
|
||||
|
||||
let stack = [];
|
||||
for (const [i, c] of contours.entries()) {
|
||||
|
@ -189,7 +189,7 @@ export class SpiroPenGeometry extends CachedGeometry {
|
|||
|
||||
measureComplexity() {
|
||||
let cplx = CPLX_NON_EMPTY | CPLX_NON_SIMPLE;
|
||||
for (const z of this.m_pen) {
|
||||
for (const z of this.m_penProfile) {
|
||||
if (!isFinite(z.x) || !isFinite(z.y)) cplx |= CPLX_BROKEN;
|
||||
}
|
||||
for (const z of this.m_knots) {
|
||||
|
@ -204,8 +204,8 @@ export class SpiroPenGeometry extends CachedGeometry {
|
|||
h.bool(this.m_closed);
|
||||
|
||||
// Serialize the pen
|
||||
h.beginArray(this.m_pen.length);
|
||||
for (const z of this.m_pen) h.point(z);
|
||||
h.beginArray(this.m_penProfile.length);
|
||||
for (const z of this.m_penProfile) h.point(z);
|
||||
h.endArray();
|
||||
|
||||
// Serialize the knots
|
||||
|
|
|
@ -37,6 +37,7 @@ export class SpiroFlattener {
|
|||
for (const fn of this.preControlFunctions) fn.call(collector);
|
||||
for (const control of this.controls) collector.pushKnot(control);
|
||||
for (const postControl of this.postControls) postControl.applyTo(collector);
|
||||
collector.finish();
|
||||
}
|
||||
|
||||
/// Add a control object (or list) to a sink
|
||||
|
@ -326,6 +327,28 @@ export class UserCloseKnotPair {
|
|||
}
|
||||
}
|
||||
|
||||
export class VirtualControlKnot {
|
||||
constructor(x, y, af) {
|
||||
this.center = new UserControlKnot("corner", x, y, af);
|
||||
}
|
||||
|
||||
getDependency(stage) {
|
||||
return this.center.getDependency(stage);
|
||||
}
|
||||
getKernelKnot() {
|
||||
return this.center.getKernelKnot();
|
||||
}
|
||||
resolveCoordiantePropogation(ic, pre, post) {
|
||||
this.center.resolveCoordiantePropogation(ic, pre, post);
|
||||
}
|
||||
resolveNonInterpolated() {
|
||||
return [];
|
||||
}
|
||||
resolveInterpolation() {
|
||||
throw new Error("Unreachable");
|
||||
}
|
||||
}
|
||||
|
||||
export class InterpolatorBase {
|
||||
constructor() {}
|
||||
|
||||
|
@ -415,6 +438,8 @@ export function WithKnotProxy(proxy, actual) {
|
|||
return new KnotProxyInterpolator(proxy, actual);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class TerminateInstruction {
|
||||
constructor(type, af) {
|
||||
this.type = type;
|
||||
|
@ -443,139 +468,3 @@ export class DerivedCoordinateBase {
|
|||
throw new Error("Unimplemented");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class BiKnotCollector {
|
||||
constructor(contrast) {
|
||||
this.contrast = contrast; // stroke contrast
|
||||
this.defaultD1 = 0; // default LHS
|
||||
this.defaultD2 = 0; // default RHS sw
|
||||
this.lastKnot = null; // last knot in the processed items
|
||||
|
||||
this.controls = []; // all the control items
|
||||
this.closed = false; // whether the shape is closed
|
||||
}
|
||||
|
||||
pushKnot(c) {
|
||||
let k;
|
||||
if (this.lastKnot) {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2);
|
||||
} else {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
||||
}
|
||||
|
||||
this.controls.push(k);
|
||||
this.lastKnot = k;
|
||||
|
||||
c.applyTo(this);
|
||||
}
|
||||
setWidth(l, r) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.d1 = l;
|
||||
this.lastKnot.d2 = r;
|
||||
} else {
|
||||
this.defaultD1 = l;
|
||||
this.defaultD2 = r;
|
||||
}
|
||||
}
|
||||
headsTo(direction) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.proposedNormal = direction;
|
||||
}
|
||||
}
|
||||
setUnimportant() {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.unimportant = 1;
|
||||
}
|
||||
}
|
||||
setContrast(c) {
|
||||
this.contrast = c;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class MonoKnot {
|
||||
constructor(type, unimportant, x, y) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.unimportant = unimportant;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new MonoKnot(this.type, this.x, this.y, this.unimportant);
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("MonoKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
reverseType() {
|
||||
if (this.type === "left") {
|
||||
this.type = "right";
|
||||
} else if (this.type === "right") {
|
||||
this.type = "left";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class BiKnot {
|
||||
constructor(type, x, y, d1, d2) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.d1 = d1;
|
||||
this.d2 = d2;
|
||||
this.proposedNormal = null;
|
||||
this.unimportant = 0;
|
||||
|
||||
// Derived properties
|
||||
this.origTangent = null;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent;
|
||||
k1.proposedNormal = this.proposedNormal;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
withGizmo(gizmo) {
|
||||
const tfZ = gizmo.applyXY(this.x, this.y);
|
||||
const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null;
|
||||
k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("BiKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
|
||||
h.bool(this.d1 != null);
|
||||
if (this.d1 != null) h.f64(this.d1);
|
||||
h.bool(this.d2 != null);
|
||||
if (this.d2 != null) h.f64(this.d2);
|
||||
|
||||
h.bool(this.proposedNormal != null);
|
||||
if (this.proposedNormal) {
|
||||
h.f64(this.proposedNormal.x);
|
||||
h.f64(this.proposedNormal.y);
|
||||
}
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
toMono() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,129 @@ import { linreg, mix } from "@iosevka/util";
|
|||
import * as SpiroJs from "spiro";
|
||||
|
||||
import { Vec2 } from "./point.mjs";
|
||||
import { MonoKnot } from "./spiro-control.mjs";
|
||||
import { MonoKnot } from "./spiro-to-outline.mjs";
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class BiKnotCollector {
|
||||
constructor(contrast) {
|
||||
this.contrast = contrast; // stroke contrast
|
||||
this.defaultD1 = 0; // default LHS
|
||||
this.defaultD2 = 0; // default RHS sw
|
||||
this.lastKnot = null; // last knot in the processed items
|
||||
|
||||
this.knots = []; // all the control items
|
||||
this.closed = false; // whether the shape is closed
|
||||
|
||||
this.m_finished = false;
|
||||
}
|
||||
|
||||
get controls() {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.m_finished = true;
|
||||
}
|
||||
pushKnot(c) {
|
||||
if (this.m_finished) throw new Error("Cannot push knot after finish");
|
||||
|
||||
let k;
|
||||
if (this.lastKnot) {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.lastKnot.d1, this.lastKnot.d2);
|
||||
} else {
|
||||
k = new BiKnot(c.type, c.x, c.y, this.defaultD1, this.defaultD2);
|
||||
}
|
||||
|
||||
this.knots.push(k);
|
||||
this.lastKnot = k;
|
||||
|
||||
c.applyTo(this);
|
||||
}
|
||||
setWidth(l, r) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.d1 = l;
|
||||
this.lastKnot.d2 = r;
|
||||
} else {
|
||||
this.defaultD1 = l;
|
||||
this.defaultD2 = r;
|
||||
}
|
||||
}
|
||||
headsTo(direction) {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.proposedNormal = direction;
|
||||
}
|
||||
}
|
||||
setUnimportant() {
|
||||
if (this.lastKnot) {
|
||||
this.lastKnot.unimportant = 1;
|
||||
}
|
||||
}
|
||||
setContrast(c) {
|
||||
this.contrast = c;
|
||||
}
|
||||
|
||||
getMonoKnots() {
|
||||
let a = [];
|
||||
for (const c of this.knots) {
|
||||
a.push(c.toMono());
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
export class BiKnot {
|
||||
constructor(type, x, y, d1, d2) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.d1 = d1;
|
||||
this.d2 = d2;
|
||||
this.proposedNormal = null;
|
||||
this.unimportant = 0;
|
||||
|
||||
// Derived properties
|
||||
this.origTangent = null;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new BiKnot(this.type, this.x, this.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent;
|
||||
k1.proposedNormal = this.proposedNormal;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
withGizmo(gizmo) {
|
||||
const tfZ = gizmo.applyXY(this.x, this.y);
|
||||
const k1 = new BiKnot(this.type, tfZ.x, tfZ.y, this.d1, this.d2);
|
||||
k1.origTangent = this.origTangent ? gizmo.applyOffset(this.origTangent) : null;
|
||||
k1.proposedNormal = this.proposedNormal ? gizmo.applyOffset(this.proposedNormal) : null;
|
||||
k1.unimportant = this.unimportant;
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("BiKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
|
||||
h.bool(this.d1 != null);
|
||||
if (this.d1 != null) h.f64(this.d1);
|
||||
h.bool(this.d2 != null);
|
||||
if (this.d2 != null) h.f64(this.d2);
|
||||
|
||||
h.bool(this.proposedNormal != null);
|
||||
if (this.proposedNormal) {
|
||||
h.f64(this.proposedNormal.x);
|
||||
h.f64(this.proposedNormal.y);
|
||||
}
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
toMono() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -78,19 +200,47 @@ export class SpiroExpander {
|
|||
return { lhs: lhsT, rhs: rhsT, lhsUntransformed: lhsU, rhsUntransformed: rhsU };
|
||||
}
|
||||
interpolateUnimportantKnots(lhsT, rhsT, lhsU, rhsU) {
|
||||
for (let j = 0; j < this.m_biKnotsU.length; j++) {
|
||||
const knotU = this.m_biKnotsU[j];
|
||||
if (!knotU.unimportant) continue;
|
||||
let jBefore, jAfter;
|
||||
for (jBefore = j - 1; cyNth(this.m_biKnotsU, jBefore).unimportant; jBefore--);
|
||||
for (jAfter = j + 1; cyNth(this.m_biKnotsU, jAfter).unimportant; jAfter++);
|
||||
let firstImportantIdx = -1;
|
||||
let lastImportantIdx = -1;
|
||||
|
||||
const knotUBefore = cyNth(this.m_biKnotsU, jBefore),
|
||||
knotUAfter = cyNth(this.m_biKnotsU, jAfter),
|
||||
lhsUBefore = cyNth(lhsU, jBefore),
|
||||
lhsUAfter = cyNth(lhsU, jAfter),
|
||||
rhsUBefore = cyNth(rhsU, jBefore),
|
||||
rhsUAfter = cyNth(rhsU, jAfter);
|
||||
for (let j = 0; j < this.m_biKnotsU.length; j++) {
|
||||
// If the current knot is unimportant, skip it
|
||||
if (this.m_biKnotsU[j].unimportant) continue;
|
||||
|
||||
// If we've scanned an important knot before, interpolate the unimportant knots between
|
||||
if (lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, lastImportantIdx, j);
|
||||
}
|
||||
|
||||
if (firstImportantIdx === -1) firstImportantIdx = j;
|
||||
lastImportantIdx = j;
|
||||
}
|
||||
|
||||
// Handle the last important ... first important wraparound
|
||||
if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantKnotsRg(
|
||||
lhsT,
|
||||
rhsT,
|
||||
lhsU,
|
||||
rhsU,
|
||||
lastImportantIdx,
|
||||
firstImportantIdx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interpolateUnimportantKnotsRg(lhsT, rhsT, lhsU, rhsU, jBefore, jAfter) {
|
||||
let count = jAfter > jBefore ? jAfter - jBefore : lhsT.length - jBefore + jAfter;
|
||||
for (let offset = 1; offset < count; offset++) {
|
||||
let j = (jBefore + offset) % lhsT.length;
|
||||
|
||||
const knotUBefore = this.m_biKnotsU[jBefore],
|
||||
knotU = this.m_biKnotsU[j],
|
||||
knotUAfter = this.m_biKnotsU[jAfter],
|
||||
lhsUBefore = lhsU[jBefore],
|
||||
lhsUAfter = lhsU[jAfter],
|
||||
rhsUBefore = rhsU[jBefore],
|
||||
rhsUAfter = rhsU[jAfter];
|
||||
|
||||
lhsU[j].x = linreg(knotUBefore.x, lhsUBefore.x, knotUAfter.x, lhsUAfter.x, knotU.x);
|
||||
lhsU[j].y = linreg(knotUBefore.y, lhsUBefore.y, knotUAfter.y, lhsUAfter.y, knotU.y);
|
||||
|
|
243
packages/geometry/src/spiro-pen-expand.mjs
Normal file
243
packages/geometry/src/spiro-pen-expand.mjs
Normal file
|
@ -0,0 +1,243 @@
|
|||
import * as SpiroJs from "spiro";
|
||||
|
||||
import { linreg } from "@iosevka/util";
|
||||
import * as CurveUtil from "./curve-util.mjs";
|
||||
import { Point } from "./point.mjs";
|
||||
import { MonoKnot } from "./spiro-to-outline.mjs";
|
||||
|
||||
export class PenKnotCollector {
|
||||
constructor(gizmo, defaultProfile) {
|
||||
this.gizmo = gizmo;
|
||||
this.m_profile = defaultProfile;
|
||||
this.m_lastKnot = null;
|
||||
this.m_finished = false;
|
||||
|
||||
this.knots = [];
|
||||
this.closed = false;
|
||||
}
|
||||
finish() {
|
||||
this.m_finished = true;
|
||||
}
|
||||
pushKnot(c) {
|
||||
if (this.m_finished) throw new Error("Cannot push knot after finish");
|
||||
let k = new PenKnot(c.type, c.x, c.y, this.m_profile);
|
||||
this.knots.push(k);
|
||||
this.m_lastKnot = k;
|
||||
|
||||
c.applyTo(this);
|
||||
}
|
||||
|
||||
setWidth() {}
|
||||
headsTo() {}
|
||||
setUnimportant() {
|
||||
if (this.m_lastKnot) this.m_lastKnot.profile = null;
|
||||
}
|
||||
setContrast() {}
|
||||
|
||||
setProfile(profile) {
|
||||
if (profile.length !== this.m_profile.length)
|
||||
throw new Error("Pen profile length mismatch");
|
||||
if (this.m_lastKnot) this.m_lastKnot.profile = profile;
|
||||
this.m_profile = profile;
|
||||
}
|
||||
}
|
||||
|
||||
export class PenKnot {
|
||||
constructor(type, x, y, profile) {
|
||||
this.type = type;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.profile = profile;
|
||||
}
|
||||
clone() {
|
||||
const k1 = new PenKnot(this.type, this.x, this.y, this.profile);
|
||||
return k1;
|
||||
}
|
||||
withGizmo(gizmo) {
|
||||
const tfZ = gizmo.applyXY(this.x, this.y);
|
||||
const k1 = new PenKnot(this.type, tfZ.x, tfZ.y, this.profile);
|
||||
return k1;
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("PenKnot");
|
||||
h.str(this.type);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
h.bool(this.profile != null);
|
||||
if (this.profile) {
|
||||
h.beginArray(this.profile.length);
|
||||
for (let i = 0; i < this.profile.length; i++) {
|
||||
h.f64(this.profile[i].x);
|
||||
h.f64(this.profile[i].y);
|
||||
}
|
||||
h.endArray();
|
||||
}
|
||||
h.endStruct();
|
||||
}
|
||||
}
|
||||
|
||||
export class PenSpiroExpander {
|
||||
constructor(gizmo, profile, closed, knots) {
|
||||
this.m_gizmo = gizmo;
|
||||
this.m_closed = closed;
|
||||
this.m_knotsU = Array.from(knots);
|
||||
this.m_knotsT = knots.map(k => k.withGizmo(gizmo));
|
||||
this.m_profileEdges = profile.length;
|
||||
|
||||
this.m_traces = [];
|
||||
}
|
||||
|
||||
getGeometry() {
|
||||
this.traceAll();
|
||||
|
||||
const contours = [];
|
||||
for (let i = 0; i < this.m_traces.length; i++) {
|
||||
const iNext = (i + 1) % this.m_traces.length;
|
||||
if (this.m_traces[i].length !== this.m_traces[iNext].length) {
|
||||
throw new Error("Different number of arcs in traces");
|
||||
}
|
||||
for (let j = 0; j < this.m_traces[i].length; j++) {
|
||||
const arcForward = this.m_traces[i][j];
|
||||
const arcBackward = this.m_traces[iNext][j];
|
||||
makeProfiledStroke(contours, arcForward, arcBackward);
|
||||
}
|
||||
}
|
||||
|
||||
return contours;
|
||||
}
|
||||
|
||||
traceAll() {
|
||||
for (let i = 0; i < this.m_profileEdges; i++) {
|
||||
this.calculateShiftedProfile(i);
|
||||
}
|
||||
}
|
||||
calculateShiftedProfile(iEdge) {
|
||||
let traceT = [],
|
||||
traceU = [];
|
||||
for (let i = 0; i < this.m_knotsT.length; i++) {
|
||||
const trT = this.getTrace(this.m_knotsT[i], iEdge);
|
||||
const trU = trT.clone();
|
||||
this.m_gizmo.unapplyToSink(trT, trU);
|
||||
traceT.push(trT), traceU.push(trU);
|
||||
}
|
||||
this.interpolateUnimportantTraceKnots(traceU, traceT);
|
||||
|
||||
let arcc = new SimplyCollectArcs();
|
||||
SpiroJs.spiroToArcsOnContext(traceT, this.m_closed, arcc);
|
||||
this.m_traces.push(arcc.arcs);
|
||||
}
|
||||
getTrace(k, iEdge) {
|
||||
if (k.profile) {
|
||||
return new MonoKnot(k.type, false, k.x + k.profile[iEdge].x, k.y + k.profile[iEdge].y);
|
||||
} else {
|
||||
return new MonoKnot(k.type, true, k.x, k.y);
|
||||
}
|
||||
}
|
||||
interpolateUnimportantTraceKnots(traceU, traceT) {
|
||||
let firstImportantIdx = -1;
|
||||
let lastImportantIdx = -1;
|
||||
|
||||
for (let i = 0; i < traceU.length; i++) {
|
||||
if (traceU[i].unimportant) continue;
|
||||
|
||||
if (lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantTraceKnotsRg(traceU, traceT, lastImportantIdx, i);
|
||||
}
|
||||
|
||||
if (firstImportantIdx === -1) firstImportantIdx = i;
|
||||
lastImportantIdx = i;
|
||||
}
|
||||
|
||||
if (firstImportantIdx !== -1 && lastImportantIdx !== -1) {
|
||||
this.interpolateUnimportantTraceKnotsRg(
|
||||
traceU,
|
||||
traceT,
|
||||
lastImportantIdx,
|
||||
firstImportantIdx,
|
||||
);
|
||||
}
|
||||
}
|
||||
interpolateUnimportantTraceKnotsRg(traceU, traceT, last, next) {
|
||||
let count = next > last ? next - last : traceU.length - last + next;
|
||||
for (let offset = 1; offset < count; offset++) {
|
||||
let i = (last + offset) % traceU.length;
|
||||
this.interpolateKnot(
|
||||
this.m_knotsU[last],
|
||||
traceU[last],
|
||||
this.m_knotsU[i],
|
||||
traceU[i],
|
||||
traceT[i],
|
||||
this.m_knotsU[next],
|
||||
traceU[next],
|
||||
);
|
||||
}
|
||||
}
|
||||
interpolateKnot(rBefore, uBefore, rCurr, uCurr, tCurr, rAfter, uAfter) {
|
||||
uCurr.x = linreg(rBefore.x, uBefore.x, rAfter.x, uAfter.x, rCurr.x);
|
||||
uCurr.y = linreg(rBefore.y, uBefore.y, rAfter.y, uAfter.y, rCurr.y);
|
||||
this.m_gizmo.applyToSink(uCurr, tCurr);
|
||||
}
|
||||
}
|
||||
|
||||
class SimplyCollectArcs {
|
||||
constructor() {
|
||||
this.arcs = [];
|
||||
}
|
||||
beginShape() {}
|
||||
endShape() {}
|
||||
moveTo() {}
|
||||
arcTo(arc) {
|
||||
this.arcs.push(arc);
|
||||
}
|
||||
}
|
||||
|
||||
// arcForward and arcBackward must be spiro arcs.
|
||||
function makeProfiledStroke(contours, arcForward, arcBackward) {
|
||||
const [subdividesForward, subdividesBackward] = subdivideKnotPair(
|
||||
arcForward,
|
||||
arcBackward,
|
||||
CurveUtil.GEOMETRY_PRECISION,
|
||||
);
|
||||
for (let i = 0; i < subdividesForward.length; i++) {
|
||||
const [a1, b1, c1, d1] = subdividesForward[i].toCubicBezier();
|
||||
const [a2, b2, c2, d2] = subdividesBackward[i].toCubicBezier();
|
||||
contours.push([
|
||||
Point.from(Point.Type.Corner, a1),
|
||||
Point.from(Point.Type.CubicStart, b1),
|
||||
Point.from(Point.Type.CubicEnd, c1),
|
||||
Point.from(Point.Type.Corner, d1),
|
||||
Point.from(Point.Type.Corner, d2),
|
||||
Point.from(Point.Type.CubicStart, c2),
|
||||
Point.from(Point.Type.CubicEnd, b2),
|
||||
Point.from(Point.Type.Corner, a2),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
function subdivideKnotPair(arcForward, arcBackward, delta) {
|
||||
const MAX_STOPS = 16;
|
||||
|
||||
let sinkForward = [],
|
||||
sinkBackward = [];
|
||||
for (let stops = 1; stops < MAX_STOPS; stops++) {
|
||||
sinkForward.length = 0;
|
||||
sinkBackward.length = 0;
|
||||
|
||||
uniformSubdivide(arcForward, stops, sinkForward);
|
||||
uniformSubdivide(arcBackward, stops, sinkBackward);
|
||||
|
||||
let flatEnough = true;
|
||||
for (const fwd of sinkForward) if (fwd.bend > delta) flatEnough = false;
|
||||
for (const bwd of sinkBackward) if (bwd.bend > delta) flatEnough = false;
|
||||
|
||||
if (flatEnough) break;
|
||||
}
|
||||
return [sinkForward, sinkBackward];
|
||||
}
|
||||
function uniformSubdivide(arc, stops, sink) {
|
||||
for (; stops > 1; stops--) {
|
||||
const f = arc.subdivide(1 / stops);
|
||||
sink.push(f[0]), (arc = f[1]);
|
||||
}
|
||||
sink.push(arc);
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import * as SpiroJs from "spiro";
|
||||
import * as CurveUtil from "./curve-util.mjs";
|
||||
|
||||
import { Point } from "./point.mjs";
|
||||
|
||||
export function createSpiroPenGeometry(gizmo, closed, knots, pen) {
|
||||
const collector = new ArcCollector(gizmo, pen);
|
||||
SpiroJs.spiroToBezierOnContext(knots, closed, collector, CurveUtil.GEOMETRY_PRECISION);
|
||||
return collector.contoursCollected;
|
||||
}
|
||||
|
||||
class ArcCollector {
|
||||
constructor(gizmo, pen) {
|
||||
this.gizmo = gizmo;
|
||||
this.lastX = 0;
|
||||
this.lastY = 0;
|
||||
this.m_pen = pen;
|
||||
this.contoursCollected = [];
|
||||
}
|
||||
|
||||
beginShape() {}
|
||||
endShape() {}
|
||||
|
||||
moveTo(x, y) {
|
||||
const lastTf = this.gizmo.applyXY(x, y);
|
||||
this.lastX = lastTf.x;
|
||||
this.lastY = lastTf.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
lineTo(x1, y1) {
|
||||
const z1 = this.gizmo.applyXY(x1, y1);
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let penPrev = this.m_pen[i];
|
||||
let penNext = this.m_pen[(i + 1) % this.m_pen.length];
|
||||
|
||||
const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y);
|
||||
const l2 = new Point(Point.Type.Corner, z1.x + penPrev.x, z1.y + penPrev.y);
|
||||
const r2 = new Point(Point.Type.Corner, z1.x + penNext.x, z1.y + penNext.y);
|
||||
const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y);
|
||||
|
||||
this.contoursCollected.push([l1, l2, r2, r1]);
|
||||
}
|
||||
this.lastX = z1.x;
|
||||
this.lastY = z1.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
cubicTo(x2, y2, x3, y3, x4, y4) {
|
||||
const z2 = this.gizmo.applyXY(x2, y2);
|
||||
const z3 = this.gizmo.applyXY(x3, y3);
|
||||
const z4 = this.gizmo.applyXY(x4, y4);
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let penPrev = this.m_pen[i];
|
||||
let penNext = this.m_pen[(i + 1) % this.m_pen.length];
|
||||
|
||||
const l1 = new Point(Point.Type.Corner, this.lastX + penPrev.x, this.lastY + penPrev.y);
|
||||
const l2 = new Point(Point.Type.CubicStart, z2.x + penPrev.x, z2.y + penPrev.y);
|
||||
const l3 = new Point(Point.Type.CubicEnd, z3.x + penPrev.x, z3.y + penPrev.y);
|
||||
const l4 = new Point(Point.Type.Corner, z4.x + penPrev.x, z4.y + penPrev.y);
|
||||
const r4 = new Point(Point.Type.Corner, z4.x + penNext.x, z4.y + penNext.y);
|
||||
const r3 = new Point(Point.Type.CubicStart, z3.x + penNext.x, z3.y + penNext.y);
|
||||
const r2 = new Point(Point.Type.CubicEnd, z2.x + penNext.x, z2.y + penNext.y);
|
||||
const r1 = new Point(Point.Type.Corner, this.lastX + penNext.x, this.lastY + penNext.y);
|
||||
|
||||
this.contoursCollected.push([l1, l2, l3, l4, r4, r3, r2, r1]);
|
||||
}
|
||||
this.lastX = z4.x;
|
||||
this.lastY = z4.y;
|
||||
this.addPenProfileAt(this.lastX, this.lastY);
|
||||
}
|
||||
|
||||
addPenProfileAt(x, y) {
|
||||
let c = [];
|
||||
for (let i = 0; i < this.m_pen.length; i++) {
|
||||
let pen = this.m_pen[i];
|
||||
c.push(new Point(Point.Type.Corner, x + pen.x, y + pen.y));
|
||||
}
|
||||
this.contoursCollected.push(c);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,38 @@ export function spiroToOutlineWithSimplification(knots, fClosed, gizmo) {
|
|||
return sink.contours;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export class MonoKnot {
|
||||
constructor(type, unimportant, x, y) {
|
||||
this.type = type;
|
||||
this.unimportant = unimportant;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
clone() {
|
||||
return new MonoKnot(this.type, this.unimportant, this.x, this.y);
|
||||
}
|
||||
hash(h) {
|
||||
h.beginStruct("MonoKnot");
|
||||
h.str(this.type);
|
||||
h.bool(this.unimportant);
|
||||
h.f64(this.x);
|
||||
h.f64(this.y);
|
||||
h.endStruct();
|
||||
}
|
||||
|
||||
reverseType() {
|
||||
if (this.type === "left") {
|
||||
this.type = "right";
|
||||
} else if (this.type === "right") {
|
||||
this.type = "left";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
class SpiroSimplifier {
|
||||
constructor(knots) {
|
||||
this.m_knots = knots;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue