rotulo 0.1.0__py3-none-any.whl
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.
- rotulo/__init__.py +4 -0
- rotulo/__main__.py +26 -0
- rotulo/accents.py +50 -0
- rotulo/build.py +97 -0
- rotulo/capsule.py +104 -0
- rotulo/chars.py +13 -0
- rotulo/context.py +142 -0
- rotulo/figures.py +217 -0
- rotulo/glyphset.py +112 -0
- rotulo/lowercase.py +280 -0
- rotulo/outline.py +45 -0
- rotulo/params.py +51 -0
- rotulo/py.typed +0 -0
- rotulo/skeleton.py +164 -0
- rotulo/spacing.py +81 -0
- rotulo/symbols.py +259 -0
- rotulo/uppercase.py +212 -0
- rotulo/variable.py +77 -0
- rotulo/web.py +23 -0
- rotulo-0.1.0.dist-info/METADATA +161 -0
- rotulo-0.1.0.dist-info/RECORD +24 -0
- rotulo-0.1.0.dist-info/WHEEL +4 -0
- rotulo-0.1.0.dist-info/entry_points.txt +2 -0
- rotulo-0.1.0.dist-info/licenses/LICENSE +93 -0
rotulo/__init__.py
ADDED
rotulo/__main__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from rotulo.build import build_font
|
|
5
|
+
from rotulo.params import DISPLAY, READING
|
|
6
|
+
from rotulo.variable import build_variable
|
|
7
|
+
from rotulo.web import to_woff2
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(prog="rotulo", description="Build the Rotulo fonts.")
|
|
12
|
+
parser.add_argument("--out", type=Path, default=Path("build"))
|
|
13
|
+
parser.add_argument("--no-variable", action="store_true", help="skip the variable font")
|
|
14
|
+
parser.add_argument("--web", type=Path, help="also write WOFF2 files into this directory")
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
for params, style, weight_class in ((DISPLAY, "Display", 200), (READING, "Lectura", 500)):
|
|
17
|
+
print(build_font(params, style, weight_class, args.out))
|
|
18
|
+
if not args.no_variable:
|
|
19
|
+
print(build_variable(args.out))
|
|
20
|
+
if args.web:
|
|
21
|
+
for path in to_woff2(args.out, args.web):
|
|
22
|
+
print(path)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
rotulo/accents.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from rotulo.chars import DOTLESS_I
|
|
2
|
+
from rotulo.context import Context
|
|
3
|
+
from rotulo.skeleton import Stroke
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_accents(ctx: Context) -> None:
|
|
7
|
+
g, xh, pen, sh = ctx.glyphs, ctx.xh, ctx.pen, ctx.sharp
|
|
8
|
+
|
|
9
|
+
def compose(ch: str, base: str, extra: list[Stroke]) -> None:
|
|
10
|
+
b = g[base]
|
|
11
|
+
ctx.define(ch, b.width, b.left, b.right, b.strokes, [*b.details, *extra], b.ax)
|
|
12
|
+
|
|
13
|
+
def grave(x: float, y: float) -> Stroke:
|
|
14
|
+
return pen().m(x + xh * 0.06, y).l(x - xh * 0.08, y + xh * 0.18)
|
|
15
|
+
|
|
16
|
+
def circumflex(x: float, y: float) -> Stroke:
|
|
17
|
+
return pen(sharp=sh).m(x - xh * 0.12, y).l(x, y + xh * 0.16).l(x + xh * 0.12, y)
|
|
18
|
+
|
|
19
|
+
def cedilla(x: float) -> Stroke:
|
|
20
|
+
y0, r = -(ctx.w + ctx.gap * 0.5), xh * 0.08
|
|
21
|
+
return pen().m(x, y0).a(x, y0 - r, r, r, 90, -150)
|
|
22
|
+
|
|
23
|
+
marks = {
|
|
24
|
+
"acute": ctx.acute,
|
|
25
|
+
"grave": grave,
|
|
26
|
+
"circumflex": circumflex,
|
|
27
|
+
}
|
|
28
|
+
lower = {"a": "a", "e": "e", "i": DOTLESS_I, "o": "o", "u": "u"}
|
|
29
|
+
upper = {"A": "A", "E": "E", "I": "I", "O": "O", "U": "U"}
|
|
30
|
+
table = {
|
|
31
|
+
"acute": ("áéíóú", "ÁÉÍÓÚ"),
|
|
32
|
+
"grave": ("àèìòù", "ÀÈÌÒÙ"),
|
|
33
|
+
"circumflex": ("âêîôû", "ÂÊÎÔÛ"),
|
|
34
|
+
}
|
|
35
|
+
for mark, (low, up) in table.items():
|
|
36
|
+
draw = marks[mark]
|
|
37
|
+
for ch, base in zip(low, lower.values(), strict=True):
|
|
38
|
+
compose(ch, base, [draw(0 if base == DOTLESS_I else g[base].ax, ctx.yt)])
|
|
39
|
+
for ch, base in zip(up, upper.values(), strict=True):
|
|
40
|
+
compose(ch, base, [draw(g[base].ax, ctx.yc)])
|
|
41
|
+
for ch, base in (("ñ", "n"), ("ã", "a"), ("õ", "o")):
|
|
42
|
+
compose(ch, base, [ctx.tilde(g[base].ax, ctx.yt)])
|
|
43
|
+
for ch, base in (("Ñ", "N"), ("Ã", "A"), ("Õ", "O")):
|
|
44
|
+
compose(ch, base, [ctx.tilde(g[base].ax, ctx.yc)])
|
|
45
|
+
for ch, base in (("ü", "u"), ("ë", "e"), ("ï", DOTLESS_I), ("ÿ", "y")):
|
|
46
|
+
compose(ch, base, ctx.dieresis(0 if base == DOTLESS_I else g[base].ax, ctx.yt))
|
|
47
|
+
for ch, base in (("Ü", "U"), ("Ë", "E"), ("Ï", "I")):
|
|
48
|
+
compose(ch, base, ctx.dieresis(g[base].ax, ctx.yc))
|
|
49
|
+
compose("ç", "c", [cedilla(g["c"].ax)])
|
|
50
|
+
compose("Ç", "C", [cedilla(g["C"].ax)])
|
rotulo/build.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fontTools.colorLib.builder import ColorPaletteType, buildCOLR, buildCPAL
|
|
5
|
+
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
|
|
6
|
+
from fontTools.fontBuilder import FontBuilder
|
|
7
|
+
|
|
8
|
+
from rotulo.glyphset import GlyphSet, build_context, collect, glyph_name
|
|
9
|
+
from rotulo.params import Params
|
|
10
|
+
from rotulo.spacing import kerning
|
|
11
|
+
|
|
12
|
+
FAMILY = "Rotulo"
|
|
13
|
+
VERSION = "0.1.0"
|
|
14
|
+
LICENSE = "This Font Software is licensed under the SIL Open Font License, Version 1.1."
|
|
15
|
+
LICENSE_URL = "https://openfontlicense.org"
|
|
16
|
+
DETAIL_LIGHT = (0x2F / 255, 0x5D / 255, 0x62 / 255, 1.0)
|
|
17
|
+
DETAIL_DARK = (0x7F / 255, 0xB7 / 255, 0xB5 / 255, 1.0)
|
|
18
|
+
|
|
19
|
+
__all__ = ["build_context", "build_font", "font_builder", "glyph_name"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def features(gs: GlyphSet) -> str:
|
|
23
|
+
blocks = []
|
|
24
|
+
rules = [
|
|
25
|
+
f" pos {gs.names[a]} {gs.names[b]} {v};"
|
|
26
|
+
for (a, b), v in sorted(kerning(gs.ctx).items())
|
|
27
|
+
if a in gs.names and b in gs.names
|
|
28
|
+
]
|
|
29
|
+
if rules:
|
|
30
|
+
blocks.append("feature kern {\n" + "\n".join(rules) + "\n} kern;")
|
|
31
|
+
if gs.alternates:
|
|
32
|
+
subs = "\n".join(f" sub {base} by {alt};" for base, alt in gs.alternates.items())
|
|
33
|
+
blocks.append(
|
|
34
|
+
'feature ss01 {\n featureNames { name "Lectura"; };\n' + subs + "\n} ss01;"
|
|
35
|
+
)
|
|
36
|
+
return "\n".join(blocks) + "\n"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def font_builder(params: Params, style: str, weight_class: int, gs: GlyphSet) -> Any:
|
|
40
|
+
fb = FontBuilder(1000, isTTF=True)
|
|
41
|
+
fb.setupGlyphOrder(gs.order)
|
|
42
|
+
fb.setupCharacterMap(gs.cmap)
|
|
43
|
+
fb.setupGlyf(gs.glyphs)
|
|
44
|
+
glyf = fb.font["glyf"]
|
|
45
|
+
fb.setupHorizontalMetrics({n: (gs.advances[n], getattr(glyf[n], "xMin", 0)) for n in gs.order})
|
|
46
|
+
y_max = max(getattr(glyf[n], "yMax", 0) for n in gs.order)
|
|
47
|
+
y_min = min(getattr(glyf[n], "yMin", 0) for n in gs.order)
|
|
48
|
+
fb.setupHorizontalHeader(ascent=y_max, descent=y_min)
|
|
49
|
+
ps = f"{FAMILY}-{style}"
|
|
50
|
+
fb.setupNameTable(
|
|
51
|
+
{
|
|
52
|
+
"familyName": FAMILY,
|
|
53
|
+
"styleName": style,
|
|
54
|
+
"uniqueFontIdentifier": f"{ps};{VERSION}",
|
|
55
|
+
"fullName": f"{FAMILY} {style}",
|
|
56
|
+
"psName": ps,
|
|
57
|
+
"version": f"Version {VERSION}",
|
|
58
|
+
"licenseDescription": LICENSE,
|
|
59
|
+
"licenseInfoURL": LICENSE_URL,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
fb.setupOS2(
|
|
63
|
+
usWeightClass=weight_class,
|
|
64
|
+
sTypoAscender=round(params.ascender),
|
|
65
|
+
sTypoDescender=round(params.descender),
|
|
66
|
+
sTypoLineGap=200,
|
|
67
|
+
usWinAscent=y_max,
|
|
68
|
+
usWinDescent=-y_min,
|
|
69
|
+
sxHeight=round(params.x_height),
|
|
70
|
+
sCapHeight=round(params.cap_height),
|
|
71
|
+
fsType=0,
|
|
72
|
+
)
|
|
73
|
+
fb.setupPost()
|
|
74
|
+
if gs.layers:
|
|
75
|
+
fb.font["COLR"] = buildCOLR(gs.layers, version=0)
|
|
76
|
+
fb.font["CPAL"] = buildCPAL(
|
|
77
|
+
[[DETAIL_LIGHT], [DETAIL_DARK]],
|
|
78
|
+
paletteTypes=[
|
|
79
|
+
ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND,
|
|
80
|
+
ColorPaletteType.USABLE_WITH_DARK_BACKGROUND,
|
|
81
|
+
],
|
|
82
|
+
)
|
|
83
|
+
addOpenTypeFeaturesFromString(fb.font, features(gs))
|
|
84
|
+
return fb
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_font(params: Params, style: str, weight_class: int, out: Path, hint: bool = True) -> Path:
|
|
88
|
+
gs = collect(params)
|
|
89
|
+
fb = font_builder(params, style, weight_class, gs)
|
|
90
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
path = out / f"{FAMILY}-{style}.ttf"
|
|
92
|
+
fb.save(str(path))
|
|
93
|
+
if hint:
|
|
94
|
+
from ttfautohint import ttfautohint
|
|
95
|
+
|
|
96
|
+
ttfautohint(in_file=str(path), out_file=str(path))
|
|
97
|
+
return path
|
rotulo/capsule.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
from rotulo.skeleton import Point, Stroke
|
|
5
|
+
|
|
6
|
+
Cubic = tuple[Point, Point, Point, Point]
|
|
7
|
+
Contour = list[Cubic]
|
|
8
|
+
|
|
9
|
+
KAPPA = 4 / 3 * math.tan(math.pi / 8)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sub(a: Point, b: Point) -> Point:
|
|
13
|
+
return (a[0] - b[0], a[1] - b[1])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _add(a: Point, b: Point, s: float = 1.0) -> Point:
|
|
17
|
+
return (a[0] + b[0] * s, a[1] + b[1] * s)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _unit(v: Point) -> Point:
|
|
21
|
+
n = math.hypot(*v)
|
|
22
|
+
return (v[0] / n, v[1] / n) if n > 1e-9 else (1.0, 0.0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _normal(t: Point) -> Point:
|
|
26
|
+
return (-t[1], t[0])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _split(c: Cubic) -> tuple[Cubic, Cubic]:
|
|
30
|
+
p0, p1, p2, p3 = c
|
|
31
|
+
|
|
32
|
+
def mid(a: Point, b: Point) -> Point:
|
|
33
|
+
return ((a[0] + b[0]) / 2, (a[1] + b[1]) / 2)
|
|
34
|
+
|
|
35
|
+
a, b, e = mid(p0, p1), mid(p1, p2), mid(p2, p3)
|
|
36
|
+
f, g = mid(a, b), mid(b, e)
|
|
37
|
+
h = mid(f, g)
|
|
38
|
+
return (p0, a, f, h), (h, g, e, p3)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _tangents(c: Cubic) -> tuple[Point, Point]:
|
|
42
|
+
p0, p1, p2, p3 = c
|
|
43
|
+
start = _sub(p1, p0) if math.hypot(*_sub(p1, p0)) > 1e-6 else _sub(p2, p0)
|
|
44
|
+
end = _sub(p3, p2) if math.hypot(*_sub(p3, p2)) > 1e-6 else _sub(p3, p1)
|
|
45
|
+
return _unit(start), _unit(end)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _offset(c: Cubic, h: float) -> Cubic:
|
|
49
|
+
p0, p1, p2, p3 = c
|
|
50
|
+
t0, t3 = _tangents(c)
|
|
51
|
+
turn = math.acos(max(-1.0, min(1.0, t0[0] * t3[0] + t0[1] * t3[1])))
|
|
52
|
+
chord = math.hypot(*_sub(p3, p0))
|
|
53
|
+
if turn > 1e-3 and chord > 1e-6:
|
|
54
|
+
radius = chord / (2 * math.sin(turn / 2))
|
|
55
|
+
cross = t0[0] * t3[1] - t0[1] * t3[0]
|
|
56
|
+
if (cross > 0) == (h > 0):
|
|
57
|
+
h = math.copysign(min(abs(h), radius * 0.95), h)
|
|
58
|
+
q0 = _add(p0, _normal(t0), h)
|
|
59
|
+
q3 = _add(p3, _normal(t3), h)
|
|
60
|
+
scale = math.hypot(*_sub(q3, q0)) / chord if chord > 1e-6 else 1.0
|
|
61
|
+
return q0, _add(q0, _sub(p1, p0), scale), _add(q3, _sub(p2, p3), scale), q3
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cap(center: Point, tangent: Point, h: float) -> list[Cubic]:
|
|
65
|
+
n = _normal(tangent)
|
|
66
|
+
left, tip, right = _add(center, n, h), _add(center, tangent, h), _add(center, n, -h)
|
|
67
|
+
k = KAPPA * h
|
|
68
|
+
return [
|
|
69
|
+
(left, _add(left, tangent, k), _add(tip, n, k), tip),
|
|
70
|
+
(tip, _add(tip, n, -k), _add(right, tangent, k), right),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _piece(c: Cubic, h: float) -> Contour:
|
|
75
|
+
t0, t3 = _tangents(c)
|
|
76
|
+
left = _offset(c, h)
|
|
77
|
+
right = _offset(c, -h)
|
|
78
|
+
back: Cubic = (right[3], right[2], right[1], right[0])
|
|
79
|
+
return [left, *_cap(c[3], t3, h), back, *_cap(c[0], (-t0[0], -t0[1]), h)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _cubics(stroke: Stroke, dx: float) -> list[Cubic]:
|
|
83
|
+
out: list[Cubic] = []
|
|
84
|
+
cur: Point | None = None
|
|
85
|
+
for kind, pts in stroke.finish():
|
|
86
|
+
moved = [(x + dx, y) for x, y in pts]
|
|
87
|
+
if kind == "M":
|
|
88
|
+
cur = moved[0]
|
|
89
|
+
continue
|
|
90
|
+
assert cur is not None
|
|
91
|
+
if kind == "L":
|
|
92
|
+
end = moved[0]
|
|
93
|
+
d = _sub(end, cur)
|
|
94
|
+
out.append((cur, _add(cur, d, 1 / 3), _add(cur, d, 2 / 3), end))
|
|
95
|
+
else:
|
|
96
|
+
c1, c2, end = moved
|
|
97
|
+
out.extend(_split((cur, c1, c2, end)))
|
|
98
|
+
cur = moved[-1]
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def capsules(strokes: Sequence[Stroke], weight: float, dx: float) -> list[Contour]:
|
|
103
|
+
h = weight / 2
|
|
104
|
+
return [_piece(c, h) for stroke in strokes for c in _cubics(stroke, dx)]
|
rotulo/chars.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
DOTLESS_I = chr(0x131)
|
|
2
|
+
EN_DASH = chr(0x2013)
|
|
3
|
+
EM_DASH = chr(0x2014)
|
|
4
|
+
PRIME = chr(0x2032)
|
|
5
|
+
DOUBLE_PRIME = chr(0x2033)
|
|
6
|
+
LEFT_SINGLE = chr(0x2018)
|
|
7
|
+
RIGHT_SINGLE = chr(0x2019)
|
|
8
|
+
LEFT_DOUBLE = chr(0x201C)
|
|
9
|
+
RIGHT_DOUBLE = chr(0x201D)
|
|
10
|
+
LEFT_GUILLEMET = chr(0xAB)
|
|
11
|
+
RIGHT_GUILLEMET = chr(0xBB)
|
|
12
|
+
ELLIPSIS = chr(0x2026)
|
|
13
|
+
MIDDLE_DOT = chr(0xB7)
|
rotulo/context.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from rotulo.params import Params
|
|
7
|
+
from rotulo.skeleton import DEG, Point, Stroke, Transform
|
|
8
|
+
|
|
9
|
+
Side = Literal["h", "o", "v"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Glyph:
|
|
14
|
+
width: float
|
|
15
|
+
left: Side
|
|
16
|
+
right: Side
|
|
17
|
+
strokes: list[Stroke]
|
|
18
|
+
details: list[Stroke]
|
|
19
|
+
ax: float
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class BarFillet:
|
|
24
|
+
th: float
|
|
25
|
+
c: Point
|
|
26
|
+
phi: float
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Context:
|
|
31
|
+
p: Params
|
|
32
|
+
glyphs: dict[str, Glyph] = field(default_factory=dict)
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
p = self.p
|
|
36
|
+
self.xh = p.x_height
|
|
37
|
+
self.cap = p.cap_height
|
|
38
|
+
self.asc = p.ascender
|
|
39
|
+
self.desc = p.descender
|
|
40
|
+
self.w = p.weight
|
|
41
|
+
self.o = self.xh * p.overshoot / 100
|
|
42
|
+
self.gap = p.gap
|
|
43
|
+
self.bowl = self.xh * p.width * 0.96
|
|
44
|
+
self.nw = self.bowl * 0.9
|
|
45
|
+
self.h2 = self.nw / 2
|
|
46
|
+
self.ry = self.xh / 2 + self.o
|
|
47
|
+
self.yt = self.xh + self.w + self.gap
|
|
48
|
+
self.yc = self.cap + self.w + self.gap
|
|
49
|
+
self.dot_len = self.xh * 0.1 + self.w * 0.3
|
|
50
|
+
self.dv = (
|
|
51
|
+
0.0 if p.dot == "round" else self.dot_len if p.dot == "line" else self.dot_len * 0.7
|
|
52
|
+
)
|
|
53
|
+
self.hr = self.xh * 0.3
|
|
54
|
+
self.hs = p.tail_close
|
|
55
|
+
self.hx = self.hr * p.tail
|
|
56
|
+
self.lr = self.xh * 0.2
|
|
57
|
+
self.sharp = 0.35
|
|
58
|
+
self.fig = self.cap * 0.58
|
|
59
|
+
|
|
60
|
+
def pen(self, transform: Transform | None = None, sharp: float = 1.0) -> Stroke:
|
|
61
|
+
return Stroke(self.p.corner * sharp, transform)
|
|
62
|
+
|
|
63
|
+
def define(
|
|
64
|
+
self,
|
|
65
|
+
ch: str,
|
|
66
|
+
width: float,
|
|
67
|
+
left: Side,
|
|
68
|
+
right: Side,
|
|
69
|
+
strokes: Sequence[Stroke],
|
|
70
|
+
details: Sequence[Stroke] = (),
|
|
71
|
+
ax: float | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
self.glyphs[ch] = Glyph(
|
|
74
|
+
width, left, right, list(strokes), list(details), width / 2 if ax is None else ax
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def ring(
|
|
78
|
+
self, cx: float, cy: float, rx: float, ry: float, transform: Transform | None = None
|
|
79
|
+
) -> Stroke:
|
|
80
|
+
return self.pen(transform).a(cx, cy, rx, ry, 90, 450)
|
|
81
|
+
|
|
82
|
+
def dot(self, x: float, y: float, transform: Transform | None = None) -> Stroke:
|
|
83
|
+
s = self.pen(transform)
|
|
84
|
+
if self.p.dot == "round":
|
|
85
|
+
return s.m(x, y).l(x + 0.01, y)
|
|
86
|
+
if self.p.dot == "line":
|
|
87
|
+
return s.m(x, y).l(x, y + self.dot_len)
|
|
88
|
+
return s.m(x - self.dot_len * 0.35, y).l(x + self.dot_len * 0.35, y + self.dot_len * 0.7)
|
|
89
|
+
|
|
90
|
+
def acute(self, x: float, y: float) -> Stroke:
|
|
91
|
+
return self.pen().m(x - self.xh * 0.06, y).l(x + self.xh * 0.08, y + self.xh * 0.18)
|
|
92
|
+
|
|
93
|
+
def dieresis(self, x: float, y: float) -> list[Stroke]:
|
|
94
|
+
return [self.dot(x - self.xh * 0.15, y), self.dot(x + self.xh * 0.15, y)]
|
|
95
|
+
|
|
96
|
+
def tilde(self, x: float, y: float) -> Stroke:
|
|
97
|
+
t, a = self.xh * 0.2, self.xh * 0.06
|
|
98
|
+
return (
|
|
99
|
+
self.pen().a(x - t / 2, y + a, t / 2, a, 180, 0).a(x + t / 2, y + a, t / 2, a, 180, 360)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def soft_tail(
|
|
103
|
+
self,
|
|
104
|
+
x: float,
|
|
105
|
+
top: float,
|
|
106
|
+
wx: float,
|
|
107
|
+
sweep: float,
|
|
108
|
+
base: Stroke | None = None,
|
|
109
|
+
bottom: float | None = None,
|
|
110
|
+
) -> Stroke:
|
|
111
|
+
bot = self.desc if bottom is None else bottom
|
|
112
|
+
hr = self.hr
|
|
113
|
+
ys = bot + hr * 1.6
|
|
114
|
+
s = base if base is not None else self.pen().m(x, top)
|
|
115
|
+
return (
|
|
116
|
+
s.l(x, ys)
|
|
117
|
+
.c(x, ys - (ys - bot) * 0.55, x - wx * 0.45, bot, x - wx, bot)
|
|
118
|
+
.a(x - wx, bot + hr, wx * 0.8, hr, -90, -sweep)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def bar_fillet(
|
|
122
|
+
self, cx: float, cy: float, rx: float, ry: float, bar_y: float, r: float, side: int
|
|
123
|
+
) -> BarFillet:
|
|
124
|
+
def at(th: float) -> tuple[Point, Point]:
|
|
125
|
+
nx, ny = math.cos(th) / rx, math.sin(th) / ry
|
|
126
|
+
nl = math.hypot(nx, ny)
|
|
127
|
+
e = (cx + rx * math.cos(th), cy + ry * math.sin(th))
|
|
128
|
+
return e, (e[0] - r * nx / nl, e[1] - r * ny / nl)
|
|
129
|
+
|
|
130
|
+
def f(th: float) -> float:
|
|
131
|
+
return at(th)[1][1] - (bar_y + side * r)
|
|
132
|
+
|
|
133
|
+
lo, hi = (0.0, math.pi / 2) if side > 0 else (-math.pi / 2, 0.0)
|
|
134
|
+
for _ in range(50):
|
|
135
|
+
mid = (lo + hi) / 2
|
|
136
|
+
if math.copysign(1, f(mid)) == math.copysign(1, f(lo)):
|
|
137
|
+
lo = mid
|
|
138
|
+
else:
|
|
139
|
+
hi = mid
|
|
140
|
+
th = (lo + hi) / 2
|
|
141
|
+
e, c = at(th)
|
|
142
|
+
return BarFillet(th / DEG, c, math.atan2(e[1] - c[1], e[0] - c[0]) / DEG)
|
rotulo/figures.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from rotulo.chars import DOUBLE_PRIME, EN_DASH, PRIME
|
|
4
|
+
from rotulo.context import Context
|
|
5
|
+
from rotulo.skeleton import DEG, Stroke
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def two_end(cx: float, cy: float, rx: float, ry: float) -> float:
|
|
9
|
+
def f(deg: float) -> float:
|
|
10
|
+
t = deg * DEG
|
|
11
|
+
px, py = cx + rx * math.cos(t), cy + ry * math.sin(t)
|
|
12
|
+
tx, ty = rx * math.sin(t), -ry * math.cos(t)
|
|
13
|
+
return tx * (0 - py) - ty * (0 - px)
|
|
14
|
+
|
|
15
|
+
prev = 150.0
|
|
16
|
+
for step in range(1, 300):
|
|
17
|
+
cur = 150.0 - step
|
|
18
|
+
if math.copysign(1, f(cur)) != math.copysign(1, f(prev)):
|
|
19
|
+
lo, hi = cur, prev
|
|
20
|
+
for _ in range(40):
|
|
21
|
+
mid = (lo + hi) / 2
|
|
22
|
+
if math.copysign(1, f(mid)) == math.copysign(1, f(lo)):
|
|
23
|
+
lo = mid
|
|
24
|
+
else:
|
|
25
|
+
hi = mid
|
|
26
|
+
return (lo + hi) / 2
|
|
27
|
+
prev = cur
|
|
28
|
+
return -25.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def add_figures(ctx: Context) -> None:
|
|
32
|
+
p, pen, d = ctx.p, ctx.pen, ctx.define
|
|
33
|
+
cap, xh, asc, desc, o, w, gap, dot_len, dv, fig = (
|
|
34
|
+
ctx.cap,
|
|
35
|
+
ctx.xh,
|
|
36
|
+
ctx.asc,
|
|
37
|
+
ctx.desc,
|
|
38
|
+
ctx.o,
|
|
39
|
+
ctx.w,
|
|
40
|
+
ctx.gap,
|
|
41
|
+
ctx.dot_len,
|
|
42
|
+
ctx.dv,
|
|
43
|
+
ctx.fig,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def zero_detail() -> list[Stroke]:
|
|
47
|
+
if p.zero == "plain":
|
|
48
|
+
return []
|
|
49
|
+
if p.zero == "dot":
|
|
50
|
+
return [ctx.dot(fig / 2, cap / 2 - dv / 2)]
|
|
51
|
+
th = math.atan(1.6)
|
|
52
|
+
c, s = math.cos(th), math.sin(th)
|
|
53
|
+
re = 1 / math.sqrt((c / (fig / 2)) ** 2 + (s / (cap / 2)) ** 2)
|
|
54
|
+
ln = max(12.0, re - w - gap)
|
|
55
|
+
return [pen().m(fig / 2 - ln * c, cap / 2 - ln * s).l(fig / 2 + ln * c, cap / 2 + ln * s)]
|
|
56
|
+
|
|
57
|
+
d("0", fig, "o", "o", [ctx.ring(fig / 2, cap / 2, fig / 2, cap / 2 + o)], zero_detail())
|
|
58
|
+
if p.one_flag:
|
|
59
|
+
d(
|
|
60
|
+
"1",
|
|
61
|
+
fig,
|
|
62
|
+
"o",
|
|
63
|
+
"o",
|
|
64
|
+
[
|
|
65
|
+
pen()
|
|
66
|
+
.m(fig * 0.05, cap * 0.68)
|
|
67
|
+
.c(fig * 0.3, cap * 0.76, fig * 0.5, cap * 0.86, fig * 0.6, cap)
|
|
68
|
+
.l(fig * 0.6, 0)
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
d("1", fig, "o", "o", [pen().m(fig * 0.2, cap * 0.82).l(fig * 0.6, cap).l(fig * 0.6, 0)])
|
|
73
|
+
end2 = two_end(fig / 2, cap - fig / 2, fig / 2, fig / 2 + o)
|
|
74
|
+
d(
|
|
75
|
+
"2",
|
|
76
|
+
fig,
|
|
77
|
+
"o",
|
|
78
|
+
"o",
|
|
79
|
+
[pen().a(fig / 2, cap - fig / 2, fig / 2, fig / 2 + o, 155, end2).l(0, 0).l(fig, 0)],
|
|
80
|
+
)
|
|
81
|
+
d(
|
|
82
|
+
"3",
|
|
83
|
+
fig,
|
|
84
|
+
"o",
|
|
85
|
+
"o",
|
|
86
|
+
[
|
|
87
|
+
pen()
|
|
88
|
+
.a(fig / 2, cap * 0.76, fig * 0.45, cap * 0.24, 150, -90)
|
|
89
|
+
.a(fig / 2, cap * 0.26, fig / 2, cap * 0.26, 90, -150)
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
d(
|
|
93
|
+
"4",
|
|
94
|
+
fig,
|
|
95
|
+
"o",
|
|
96
|
+
"o",
|
|
97
|
+
[pen().m(fig * 0.72, 0).l(fig * 0.72, cap).l(0, cap * 0.3).l(fig * 0.72, cap * 0.3)],
|
|
98
|
+
)
|
|
99
|
+
d(
|
|
100
|
+
"5",
|
|
101
|
+
fig,
|
|
102
|
+
"o",
|
|
103
|
+
"o",
|
|
104
|
+
[
|
|
105
|
+
pen()
|
|
106
|
+
.m(fig * 0.9, cap)
|
|
107
|
+
.l(fig * 0.12, cap)
|
|
108
|
+
.a(fig / 2, cap * 0.31, fig / 2, cap * 0.31 + o, 140, -145)
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
d(
|
|
112
|
+
"6",
|
|
113
|
+
fig,
|
|
114
|
+
"o",
|
|
115
|
+
"o",
|
|
116
|
+
[
|
|
117
|
+
pen()
|
|
118
|
+
.m(fig * 0.8, cap)
|
|
119
|
+
.c(fig * 0.38, cap * 0.98, 0, cap * 0.78, 0, cap * 0.31)
|
|
120
|
+
.a(fig / 2, cap * 0.31, fig / 2, cap * 0.31 + o, 180, 540)
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
d("7", fig, "o", "o", [pen().m(0, cap).l(fig, cap).l(fig * 0.3, 0)])
|
|
124
|
+
d(
|
|
125
|
+
"8",
|
|
126
|
+
fig,
|
|
127
|
+
"o",
|
|
128
|
+
"o",
|
|
129
|
+
[
|
|
130
|
+
ctx.ring(fig / 2, cap * 0.76, fig * 0.42, cap * 0.24),
|
|
131
|
+
ctx.ring(fig / 2, cap * 0.26, fig / 2, cap * 0.26 + o),
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
if p.nine_straight:
|
|
135
|
+
d(
|
|
136
|
+
"9",
|
|
137
|
+
fig,
|
|
138
|
+
"o",
|
|
139
|
+
"o",
|
|
140
|
+
[
|
|
141
|
+
ctx.ring(fig / 2, cap * 0.69, fig / 2, cap * 0.31 + o),
|
|
142
|
+
pen().m(fig, cap * 0.69).l(fig, 0),
|
|
143
|
+
],
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
d(
|
|
147
|
+
"9",
|
|
148
|
+
fig,
|
|
149
|
+
"o",
|
|
150
|
+
"o",
|
|
151
|
+
[pen().a(fig / 2, cap * 0.69, fig / 2, cap * 0.31 + o, -20, 340).l(fig * 0.22, 0)],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
d(".", 0, "v", "v", [], [ctx.dot(0, 0)])
|
|
155
|
+
d(",", 0, "v", "v", [], [pen().m(0, dot_len).l(-dot_len * 0.3, -dot_len * 0.8)])
|
|
156
|
+
d(":", 0, "v", "v", [], [ctx.dot(0, 0), ctx.dot(0, xh - dv)])
|
|
157
|
+
d(
|
|
158
|
+
";",
|
|
159
|
+
0,
|
|
160
|
+
"v",
|
|
161
|
+
"v",
|
|
162
|
+
[],
|
|
163
|
+
[pen().m(0, dot_len).l(-dot_len * 0.3, -dot_len * 0.8), ctx.dot(0, xh - dv)],
|
|
164
|
+
)
|
|
165
|
+
d("-", xh * 0.4, "v", "v", [pen().m(0, xh * 0.48).l(xh * 0.4, xh * 0.48)])
|
|
166
|
+
d(EN_DASH, xh * 0.8, "v", "v", [pen().m(0, xh * 0.48).l(xh * 0.8, xh * 0.48)])
|
|
167
|
+
qw, qs = cap * 0.5, dv + w + gap
|
|
168
|
+
|
|
169
|
+
def inv(x: float, y: float) -> tuple[float, float]:
|
|
170
|
+
return qw - x, xh - y
|
|
171
|
+
|
|
172
|
+
d(
|
|
173
|
+
"?",
|
|
174
|
+
qw,
|
|
175
|
+
"o",
|
|
176
|
+
"o",
|
|
177
|
+
[pen().a(qw / 2, cap - qw / 2, qw / 2, qw / 2 + o, 160, -90).l(qw / 2, qs)],
|
|
178
|
+
[ctx.dot(qw / 2, 0)],
|
|
179
|
+
)
|
|
180
|
+
d(
|
|
181
|
+
"¿",
|
|
182
|
+
qw,
|
|
183
|
+
"o",
|
|
184
|
+
"o",
|
|
185
|
+
[pen(inv).a(qw / 2, cap - qw / 2, qw / 2, qw / 2 + o, 160, -90).l(qw / 2, qs)],
|
|
186
|
+
[ctx.dot(qw / 2, xh - dv)],
|
|
187
|
+
)
|
|
188
|
+
d("!", 0, "o", "o", [pen().m(0, cap).l(0, qs)], [ctx.dot(0, 0)])
|
|
189
|
+
d("¡", 0, "o", "o", [pen().m(0, xh - qs).l(0, xh - cap)], [ctx.dot(0, xh - dv)])
|
|
190
|
+
dr = xh * 0.13
|
|
191
|
+
d("°", dr * 2, "v", "v", [ctx.ring(dr, cap - dr, dr, dr)])
|
|
192
|
+
d(PRIME, xh * 0.12, "v", "v", [pen().m(xh * 0.12, cap).l(0, cap - xh * 0.3)])
|
|
193
|
+
d(
|
|
194
|
+
DOUBLE_PRIME,
|
|
195
|
+
xh * 0.32,
|
|
196
|
+
"v",
|
|
197
|
+
"v",
|
|
198
|
+
[
|
|
199
|
+
pen().m(xh * 0.12, cap).l(0, cap - xh * 0.3),
|
|
200
|
+
pen().m(xh * 0.32, cap).l(xh * 0.2, cap - xh * 0.3),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
d("'", 0, "v", "v", [pen().m(0, cap).l(0, cap - xh * 0.28)])
|
|
204
|
+
d(
|
|
205
|
+
'"',
|
|
206
|
+
xh * 0.2,
|
|
207
|
+
"v",
|
|
208
|
+
"v",
|
|
209
|
+
[
|
|
210
|
+
pen().m(0, cap).l(0, cap - xh * 0.28),
|
|
211
|
+
pen().m(xh * 0.2, cap).l(xh * 0.2, cap - xh * 0.28),
|
|
212
|
+
],
|
|
213
|
+
)
|
|
214
|
+
pw, pm, ph = xh * 0.24, (asc + desc) / 2, (asc - desc) / 2
|
|
215
|
+
d("(", pw, "v", "v", [pen().a(pw * 2.4, pm, pw * 2.4, ph, 130, 230)])
|
|
216
|
+
d(")", pw, "v", "v", [pen().a(-pw * 1.4, pm, pw * 2.4, ph, 50, -50)])
|
|
217
|
+
d("/", xh * 0.5, "v", "v", [pen().m(0, desc * 0.5).l(xh * 0.5, asc)])
|