diff --git a/.github/scripts/package_release.sh b/.github/scripts/package_release.sh index 15231d4..6bed987 100755 --- a/.github/scripts/package_release.sh +++ b/.github/scripts/package_release.sh @@ -1,14 +1,14 @@ #!/usr/bin/env bash # Make the release files for one family or for the site. -# Usage: .github/scripts/package_release.sh weft|sans|heddle|site VERSION OUT_DIR +# Usage: .github/scripts/package_release.sh weft|sans|heddle|warp|site VERSION OUT_DIR # VERSION is the version from the tag, for example 0.1.0. # OUT_DIR receives the zip file and the single files to attach. # Run from the repository root, after the build and the checks. -# For weft, OUT_DIR/readability.json must exist already (the release -# workflow writes it with weft/tools/readability.py --gate). +# For weft and warp, OUT_DIR/readability.json must exist already (the +# release workflow writes it with /tools/readability.py --gate). set -euo pipefail -fam="${1:?family: weft, sans, heddle or site}" +fam="${1:?family: weft, sans, heddle, warp or site}" version="${2:?version, for example 0.1.0}" out="${3:?output folder}" mkdir -p "$out" @@ -135,6 +135,37 @@ case "$fam" in fonts/heddle/webfonts/MachineMadeHeddle-Bold.woff2 \ "fonts/heddle/variable/MachineMadeHeddle[wdth,wght].ttf" "$out/" ;; + warp) + name="machine-made-warp-${version}" + need fonts/warp/ttf/MachineMadeWarp-Regular.ttf fonts/warp/ttf/MachineMadeWarp-Italic.ttf \ + fonts/warp/ttf/MachineMadeWarp-Bold.ttf fonts/warp/ttf/MachineMadeWarp-BoldItalic.ttf \ + fonts/warp/ttf/MachineMadeWarpDisplay-Regular.ttf fonts/warp/ttf/MachineMadeWarpDisplay-Italic.ttf \ + fonts/warp/otf/MachineMadeWarp-Regular.otf fonts/warp/webfonts/MachineMadeWarp-Regular.woff2 \ + warp/OFL.txt "warp/RELEASE-NOTES-${notes_version}.md" "$out/readability.json" + mkdir -p "$stage/$name" + cp -r fonts/warp/ttf fonts/warp/otf fonts/warp/webfonts "$stage/$name/" + cp warp/OFL.txt "$stage/$name/OFL.txt" + cp "warp/RELEASE-NOTES-${notes_version}.md" "$stage/$name/RELEASE-NOTES.md" + cp "$out/readability.json" "$stage/$name/readability.json" + { + echo "# machine-made Warp ${version}" + echo + echo "A Modern serif for science, drawn by a pen model in code." + echo "machine-made Warp: Regular, Italic, Bold, Bold Italic (text, opsz 10)." + echo "machine-made Warp Display: Regular, Italic (titles, opsz 60)." + echo + echo "- ttf/, otf/: the six static fonts" + echo "- webfonts/: WOFF2 of the six fonts" + echo "- readability.json: the OCR readability report of this release" + echo "- RELEASE-NOTES.md: what is in this release" + echo + echo "Licence: SIL Open Font License 1.1, Reserved Font Name \"machine-made\" (OFL.txt)." + echo "Source: https://github.com/machine-made/type" + echo "Specimen: https://machine-made.github.io/type/warp/" + } > "$stage/$name/README.md" + make_zip "$stage" "$out/$name.zip" + cp fonts/warp/webfonts/*.woff2 fonts/warp/ttf/*.ttf "$out/" + ;; site) name="machine-made-type-site-${version}" need site/_site/index.html site/_site/data/readability.json @@ -143,7 +174,7 @@ case "$fam" in make_zip "$stage" "$out/$name.zip" ;; *) - echo "error: unknown family $fam (use weft, sans, heddle or site)." >&2 + echo "error: unknown family $fam (use weft, sans, heddle, warp or site)." >&2 exit 1 ;; esac diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 388755f..5f908ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,7 @@ name: release # weft-vX.Y.Z machine-made Weft # sans-vX.Y.Z machine-made Sans # heddle-vX.Y.Z machine-made Heddle +# warp-vX.Y.Z machine-made Warp and machine-made Warp Display # site-vX.Y.Z the specimen site (zip of site/_site; the live deploy is site.yml) # Each job builds again from the tagged commit, fails if the build differs # from the committed fonts, runs the checks, makes the zip and the single @@ -19,11 +20,12 @@ on: - "weft-v*" - "sans-v*" - "heddle-v*" + - "warp-v*" - "site-v*" workflow_dispatch: inputs: tag: - description: "Tag to make and release: weft-vX.Y.Z, sans-vX.Y.Z, heddle-vX.Y.Z or site-vX.Y.Z" + description: "Tag to make and release: weft-vX.Y.Z, sans-vX.Y.Z, heddle-vX.Y.Z, warp-vX.Y.Z or site-vX.Y.Z" required: true type: string @@ -424,6 +426,143 @@ jobs: "$OUT/MachineMadeHeddle\[wdth,wght\].ttf" \ "$OUT/SHA256SUMS" + warp: + if: startsWith(inputs.tag || github.ref_name, 'warp-v') + runs-on: ubuntu-24.04 + timeout-minutes: 40 + permissions: + contents: write + id-token: write + attestations: write + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Get the version from the tag + run: | + version="${RELEASE_TAG#warp-v}" + if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Tag ${RELEASE_TAG} is not warp-vX.Y.Z." + exit 1 + fi + echo "VERSION=$version" >> "$GITHUB_ENV" + + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.14" + + - name: Install dependencies + working-directory: warp + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends tesseract-ocr tesseract-ocr-eng fonts-dejavu-core + tesseract --version + python -m pip install --upgrade pip + python -m pip install -e ".[test]" + node --version + + - name: Run tests + working-directory: warp + run: python -m pytest -q + + - name: Build fonts + working-directory: warp + run: scripts/build.sh + + - name: Check that the build is the same as the committed fonts + run: | + if [ -n "$(git status --porcelain -- fonts/warp)" ]; then + git status --short -- fonts/warp + echo "::error::The build differs from the committed fonts/warp. Do not release these files." + exit 1 + fi + echo "The build has the same bytes as the committed fonts/warp." + + - name: Check the outlines against the pen sweep + working-directory: warp + run: python tools/fidelity.py --gate --out "$RUNNER_TEMP/fidelity.json" + + - name: Install Fontspector + run: | + name="fontspector-${FONTSPECTOR_VERSION}-x86_64-unknown-linux-gnu" + curl -sSfL -o "$RUNNER_TEMP/fontspector.tar.gz" \ + "https://github.com/fonttools/fontspector/releases/download/fontspector-v${FONTSPECTOR_VERSION}/${name}.tar.gz" + echo "${FONTSPECTOR_SHA256} $RUNNER_TEMP/fontspector.tar.gz" | sha256sum --check + tar xzf "$RUNNER_TEMP/fontspector.tar.gz" -C "$RUNNER_TEMP" + sudo install -m 755 "$RUNNER_TEMP/${name}/fontspector" /usr/local/bin/fontspector + fontspector --version + + - name: Check fonts (Fontspector, universal profile) + working-directory: warp + run: scripts/check.sh fontspector + + - name: Readability gate (Tesseract OCR) + working-directory: warp + run: | + mkdir -p "$OUT" + python tools/readability.py --gate --out "$OUT/readability.json" + + - name: Make the release files + run: .github/scripts/package_release.sh warp "$VERSION" "$OUT" + + - name: Attest build provenance + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + ${{ env.OUT }}/machine-made-warp-*.zip + ${{ env.OUT }}/*.woff2 + ${{ env.OUT }}/*.ttf + ${{ env.OUT }}/readability.json + + - name: Make and push the tag (manual run only) + if: github.event_name == 'workflow_dispatch' + run: | + if [ "$GITHUB_REF" != "refs/heads/main" ]; then + echo "::error::Run the release from main, not ${GITHUB_REF}." + exit 1 + fi + # A tag that exists already is fine when its commit has the same release files + # (fonts/warp warp) as this commit: that is a re-run after a failed upload, and the + # release goes onto that tag. Any other change needs a new version. + existing="$(git ls-remote --tags origin "refs/tags/${RELEASE_TAG}^{}" "refs/tags/${RELEASE_TAG}" | awk '{print $1}' | tail -n 1)" + if [ -n "$existing" ]; then + git fetch --quiet --depth=1 origin "$existing" + if git diff --quiet "$existing" "$GITHUB_SHA" -- fonts/warp warp; then + echo "Tag ${RELEASE_TAG} exists already on ${existing} with the same release files. Re-running the release." + exit 0 + fi + echo "::error::Tag ${RELEASE_TAG} exists already on ${existing}, and the release files differ. Use a new version." + exit 1 + fi + git tag "$RELEASE_TAG" "$GITHUB_SHA" + git push origin "refs/tags/${RELEASE_TAG}" + + - name: Create the release + # gh reads each file argument as a glob. The Warp file names have no + # brackets; any bracket in a future name must be escaped (\[ \]). + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "$RELEASE_TAG" --verify-tag \ + --title "machine-made Warp ${VERSION}" \ + --notes-file "warp/RELEASE-NOTES-${VERSION%.*}.md" \ + "$OUT/machine-made-warp-${VERSION}.zip" \ + "$OUT/MachineMadeWarp-Regular.woff2" \ + "$OUT/MachineMadeWarp-Italic.woff2" \ + "$OUT/MachineMadeWarp-Bold.woff2" \ + "$OUT/MachineMadeWarp-BoldItalic.woff2" \ + "$OUT/MachineMadeWarpDisplay-Regular.woff2" \ + "$OUT/MachineMadeWarpDisplay-Italic.woff2" \ + "$OUT/MachineMadeWarp-Regular.ttf" \ + "$OUT/MachineMadeWarp-Italic.ttf" \ + "$OUT/MachineMadeWarp-Bold.ttf" \ + "$OUT/MachineMadeWarp-BoldItalic.ttf" \ + "$OUT/MachineMadeWarpDisplay-Regular.ttf" \ + "$OUT/MachineMadeWarpDisplay-Italic.ttf" \ + "$OUT/readability.json" \ + "$OUT/SHA256SUMS" + site: if: startsWith(inputs.tag || github.ref_name, 'site-v') runs-on: ubuntu-24.04 diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index b0f8f04..79b021b 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -47,7 +47,11 @@ jobs: run: | for f in "fonts/weft/MachineMadeWeft[FRAG,wght].woff2" "fonts/sans/MachineMadeSans[wght].woff2" \ "fonts/sans/MachineMadeSans-Italic[wght].woff2" "fonts/heddle/MachineMadeHeddle[wdth,wght].woff2" \ - data/readability.json data/heddle.json index.html weft/index.html heddle/index.html; do + fonts/warp/MachineMadeWarp-Regular.woff2 fonts/warp/MachineMadeWarp-Italic.woff2 \ + fonts/warp/MachineMadeWarp-Bold.woff2 fonts/warp/MachineMadeWarp-BoldItalic.woff2 \ + fonts/warp/MachineMadeWarpDisplay-Regular.woff2 fonts/warp/MachineMadeWarpDisplay-Italic.woff2 \ + data/readability.json data/heddle.json data/warp.json index.html weft/index.html heddle/index.html \ + warp/index.html; do test -s "site/_site/$f" || { echo "::error::site/_site/$f is missing."; exit 1; } done python -c 'import json,sys; r=json.load(open("site/_site/data/readability.json")); sys.exit(0 if r.get("rows") else 1)' diff --git a/.github/workflows/warp.yml b/.github/workflows/warp.yml new file mode 100644 index 0000000..68d9356 --- /dev/null +++ b/.github/workflows/warp.yml @@ -0,0 +1,103 @@ +name: warp + +on: + push: + branches: [main] + paths: ["warp/**", "fonts/warp/**", "proofs/warp/**", ".github/workflows/warp.yml"] + pull_request: + paths: ["warp/**", "fonts/warp/**", "proofs/warp/**", ".github/workflows/warp.yml"] + workflow_dispatch: + +permissions: + contents: read + +env: + FONTSPECTOR_VERSION: "1.9.0" + # SHA-256 of fontspector-1.9.0-x86_64-unknown-linux-gnu.tar.gz. + FONTSPECTOR_SHA256: "0d4a7fbc577f7f99a084dec2a7acc13f64f923cbe44c0e3c5e3d76469786b8e2" + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 40 + defaults: + run: + working-directory: warp + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.14" + + - name: Install dependencies + run: | + sudo apt-get update + # Raqm sets the proofs with OpenType features. Tesseract and DejaVu + # Serif are for the readability gate. + sudo apt-get install -y --no-install-recommends libraqm0 libfribidi0 \ + tesseract-ocr tesseract-ocr-eng fonts-dejavu-core + tesseract --version + python -m pip install --upgrade pip + python -m pip install -e ".[test]" + # The tests and the proofs run the sketch engine (reference/warp-sketch-0.2.js). + node --version + + - name: Run tests + run: python -m pytest -q + + - name: Build fonts + run: scripts/build.sh + + - name: Check that the committed fonts are current + run: | + if [ -n "$(git -C .. status --porcelain -- fonts/warp)" ]; then + git -C .. status --short -- fonts/warp + echo "::error::fonts/warp differs from a fresh build. Run warp/scripts/build.sh and commit the fonts." + exit 1 + fi + + - name: Check the outlines against the pen sweep (fidelity report) + run: | + python tools/fidelity.py --gate + if [ -n "$(git -C .. status --porcelain -- proofs/warp/fidelity.json)" ]; then + git -C .. diff --stat -- proofs/warp/fidelity.json + echo "::error::proofs/warp/fidelity.json differs. Run warp/tools/fidelity.py and commit the report." + exit 1 + fi + + - name: Readability gate (Tesseract OCR) + run: python tools/readability.py --gate --out build/reports/readability.json + + - name: Install Fontspector + run: | + name="fontspector-${FONTSPECTOR_VERSION}-x86_64-unknown-linux-gnu" + curl -sSfL -o fontspector.tar.gz \ + "https://github.com/fonttools/fontspector/releases/download/fontspector-v${FONTSPECTOR_VERSION}/${name}.tar.gz" + echo "${FONTSPECTOR_SHA256} fontspector.tar.gz" | sha256sum --check + tar xzf fontspector.tar.gz + sudo install -m 755 "${name}/fontspector" /usr/local/bin/fontspector + fontspector --version + + - name: Check fonts (Fontspector, universal profile) + run: scripts/check.sh fontspector + + - name: Add check reports to the summary + if: always() + run: cat build/reports/*.md >> "$GITHUB_STEP_SUMMARY" || true + + - name: Render proof sheets + run: python tools/proof.py + + - name: Upload fonts, proofs and reports + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: warp-0.1 + path: | + fonts/warp/ + proofs/warp/ + warp/build/reports/ + if-no-files-found: warn diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6bbba..4275c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,21 @@ Each family keeps its own changelog: - machine-made Weft: [`weft/CHANGELOG.md`](weft/CHANGELOG.md) - machine-made Sans: [`sans/CHANGELOG.md`](sans/CHANGELOG.md) - machine-made Heddle: [`heddle/CHANGELOG.md`](heddle/CHANGELOG.md) +- machine-made Warp: [`warp/CHANGELOG.md`](warp/CHANGELOG.md) -Release tags: `weft-vX.Y.Z`, `sans-vX.Y.Z`, `heddle-vX.Y.Z`, `site-vX.Y.Z`. +Release tags: `weft-vX.Y.Z`, `sans-vX.Y.Z`, `heddle-vX.Y.Z`, `warp-vX.Y.Z`, `site-vX.Y.Z`. + +## 2026-10-27: Warp 0.1 + +- machine-made Warp 0.1.0 (font version 0.100): machine-made Warp + Regular, Italic, Bold, Bold Italic and machine-made Warp Display + Regular, Italic. See `warp/CHANGELOG.md`. +- Add `.github/workflows/warp.yml` (tests, build, byte check, fidelity + report, OCR gate, Fontspector, proofs) and a `warp` job in + `release.yml` for `warp-v*` tags. `package_release.sh` packs Warp. +- Site: add the Warp page (style switch, a paragraph from a paper, + Greek, figures and small caps, equations, glyph map), Warp on the + home and download pages, and Warp in the nav. ## 2026-10-27: Heddle 0.1 diff --git a/LICENSING.md b/LICENSING.md index 210008c..010f201 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -10,11 +10,14 @@ code. | `fonts/heddle/**` | SIL Open Font License 1.1, Reserved Font Name "machine-made" | `heddle/OFL.txt` | | `heddle/loom/glyphs.py`, `heddle/loom/kerning.py` (the glyph and kerning tables that define the outlines and spacing; each file says so in its header) | SIL Open Font License 1.1, Reserved Font Name "machine-made" | `heddle/OFL.txt` | | `heddle/reference/**` | The approved sketch (engine, page, screenshot): same licence as the font it defines | `heddle/OFL.txt` | +| `fonts/warp/**` | SIL Open Font License 1.1, Reserved Font Name "machine-made" | `warp/OFL.txt` | +| `warp/loom/sketch.py`, `warp/loom/extended.py`, `warp/loom/kerning.py` (the glyph and kerning tables that define the outlines and spacing; each file says so in its header) | SIL Open Font License 1.1, Reserved Font Name "machine-made" | `warp/OFL.txt` | +| `warp/reference/**` | The approved sketch (engine, page): same licence as the font it defines | `warp/OFL.txt` | | `fonts/sans/**` | SIL Open Font License 1.1, Reserved Font Name "machine-made" (copyright: Plus Jakarta Sans Project Authors and machine-made Sans Project Authors) | `sans/OFL.txt` | | `sans/sources/**` | SIL Open Font License 1.1, as for `fonts/sans/` | `sans/OFL.txt` | | `sans/upstream/**` | Upstream Plus Jakarta Sans credit files, unchanged (SIL OFL 1.1) | `sans/upstream/OFL.txt` | -| `proofs/**` | Images and reports made from the fonts: same licence as the font they show | `weft/OFL.txt`, `sans/OFL.txt` or `heddle/OFL.txt` | -| Every other file, for example the rest of `weft/loom/` and `heddle/loom/`, `weft/tools/`, `weft/scripts/`, `weft/tests/`, `heddle/tools/`, `heddle/scripts/`, `heddle/tests/`, `sans/scripts/`, `sans/tests/`, `site/`, `.github/` | Apache License 2.0 | `LICENSE` | +| `proofs/**` | Images and reports made from the fonts: same licence as the font they show | `weft/OFL.txt`, `sans/OFL.txt`, `heddle/OFL.txt` or `warp/OFL.txt` | +| Every other file, for example the rest of `weft/loom/`, `heddle/loom/` and `warp/loom/`, `weft/tools/`, `weft/scripts/`, `weft/tests/`, `heddle/tools/`, `heddle/scripts/`, `heddle/tests/`, `warp/tools/`, `warp/scripts/`, `warp/tests/`, `sans/scripts/`, `sans/tests/`, `site/`, `.github/` | Apache License 2.0 | `LICENSE` | The Reserved Font Name "machine-made" means: if you change a font and distribute it, give it a name that does not contain "machine-made". diff --git a/README.md b/README.md index 7898976..20afad9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # machine-made type -Three typefaces made by code, free under the SIL Open Font License. +Four typefaces made by code, free under the SIL Open Font License. - **machine-made Weft**: a grid-built monospace. Variable font with a weight axis (`wght` 100 to 900) and a fragmentation axis (`FRAG` 0 @@ -15,6 +15,11 @@ Three typefaces made by code, free under the SIL Open Font License. 100 to 900) and a width axis (`wdth` 70 to 140, Compressed to Extended). Every letter is built from four parts (rect, superellipse ring, parallelogram, V) and level cuts. +- **machine-made Warp**: a Modern serif for science, drawn by a pen + model: a level nib sweeps a few centre-lines per letter. Regular, + Italic, Bold and Bold Italic for text, and **machine-made Warp + Display** (Regular, Italic) for titles. Full basic Greek, maths + symbols, small capitals, old-style and lining figures. Specimen site: @@ -25,9 +30,10 @@ Specimen site: | machine-made Weft 0.1 | [weft-v0.1.0](https://github.com/machine-made/type/releases/tag/weft-v0.1.0) | Variable TTF, 9 static TTF and OTF, WOFF2, readability report | | machine-made Sans 0.1 | [sans-v0.1.0](https://github.com/machine-made/type/releases/tag/sans-v0.1.0) | 2 variable TTF, 14 static TTF and OTF, WOFF2 | | machine-made Heddle 0.1 | [heddle-v0.1.0](https://github.com/machine-made/type/releases/tag/heddle-v0.1.0) | Variable TTF, 11 static TTF, WOFF2 | +| machine-made Warp 0.1 | [warp-v0.1.0](https://github.com/machine-made/type/releases/tag/warp-v0.1.0) | 6 static TTF and OTF (text and Display), WOFF2, readability report | The same files are in this repository in `fonts/weft/`, -`fonts/sans/` and `fonts/heddle/`. Each release has build provenance attestations: check a +`fonts/sans/`, `fonts/heddle/` and `fonts/warp/`. Each release has build provenance attestations: check a file with `gh attestation verify FILE --repo machine-made/type`. ## machine-made Weft @@ -57,6 +63,19 @@ approved sketch match the sketch engine at every corner of the designspace (`proofs/heddle/heddle-sketch-vs-font-charset.png`). Details: `heddle/RELEASE-NOTES-0.1.md`. +## machine-made Warp + +![Warp equations](proofs/warp/warp-equations.png) + +Every letter is a few centre-lines with a pressure per node, swept by +one level nib; the vertical stress, hairlines, serifs and teardrops +come out of the pen. The font matches the approved sketch engine +(`proofs/warp/warp-sketch-vs-font-regular.png`), every glyph matches +its pen sweep (`proofs/warp/fidelity.json`), and an OCR gate compares +it with DejaVu Serif (`proofs/warp/readability.json`). Statics in 0.1; +the MATH table is planned for 0.2 and the variable font for 0.3. +Details: `warp/RELEASE-NOTES-0.1.md`. + ## How to build Needs Python 3.14 and, for the checks, the @@ -101,6 +120,19 @@ python tools/drift.py # proofs/heddle/drift.json python tools/sketch_compare.py # sketch engine vs font (needs Playwright and Chromium) ``` +Warp (from `warp/`): + +```sh +cd warp +python -m pip install -e ".[test]" +python -m pytest -q # generator tests (some need node) +scripts/build.sh # fonts/warp/{ttf,otf,webfonts} +scripts/check.sh fontspector # Fontspector, universal profile +python tools/fidelity.py --gate # outlines against the pen sweep +python tools/readability.py --gate # OCR gate (needs tesseract-ocr, fonts-dejavu-core) +python tools/proof.py # proofs/warp/ (needs node) +``` + Site (from the repository root): ```sh @@ -115,17 +147,18 @@ python site/build.py # site/_site/ | `weft/` | Weft generator (`loom/`), tools, tests, brief, notes, `OFL.txt` | | `sans/` | Sans sources (fork of Plus Jakarta Sans), scripts, tests, upstream credits, notes, `OFL.txt` | | `heddle/` | Heddle generator (`loom/`), tools, tests, sketch reference, brief, notes, `OFL.txt` | +| `warp/` | Warp generator (`loom/`), tools, tests, sketch reference, brief, notes, `OFL.txt` | | `site/` | Specimen site generator, pages and assets | -| `fonts/weft/`, `fonts/sans/`, `fonts/heddle/` | Committed build outputs: `variable/`, `ttf/`, `otf/` (Weft, Sans), `webfonts/` | -| `proofs/weft/`, `proofs/sans/`, `proofs/heddle/` | Proof sheets, the Weft readability report, the Heddle sketch comparison and drift report | -| `.github/workflows/` | `weft.yml`, `sans.yml`, `heddle.yml` (checks), `site.yml` (Pages deploy), `release.yml` (releases on tags) | +| `fonts/weft/`, `fonts/sans/`, `fonts/heddle/`, `fonts/warp/` | Committed build outputs: `variable/`, `ttf/`, `otf/` (Weft, Sans, Warp), `webfonts/` | +| `proofs/weft/`, `proofs/sans/`, `proofs/heddle/`, `proofs/warp/` | Proof sheets, the Weft and Warp readability reports, the Heddle and Warp sketch comparisons, the Heddle drift report, the Warp fidelity report | +| `.github/workflows/` | `weft.yml`, `sans.yml`, `heddle.yml`, `warp.yml` (checks), `site.yml` (Pages deploy), `release.yml` (releases on tags) | ## Licence - **Fonts**: SIL Open Font License 1.1 with the Reserved Font Name "machine-made". Weft: `weft/OFL.txt`. Sans: `sans/OFL.txt`, which also keeps the Plus Jakarta Sans copyright line. Heddle: - `heddle/OFL.txt`. + `heddle/OFL.txt`. Warp: `warp/OFL.txt`. - **Code** (generators, scripts, tests, CI, site): Apache-2.0, see `LICENSE`. diff --git a/STATUS.md b/STATUS.md index 4579da1..4971a3e 100644 --- a/STATUS.md +++ b/STATUS.md @@ -6,18 +6,35 @@ | House | machine-made | | Line | identity | | Visibility | public after the 0.1 release (founder approved) | -| Stage | released 0.1: machine-made Weft 0.1.0, machine-made Sans 0.1.0, specimen site 0.1.0. machine-made Heddle 0.1 built in PR #4 | -| Gate now | Heddle: Jac reviews and merges PR #4; the founder reviews the proofs; then the tags `heddle-v0.1.0` and a new `site-v*` | -| Next task | Heddle 0.2 (see `heddle/STATUS.md`). Weft: polish (look-alikes l/1, I/1, O/0; heavy accented capitals), width axis, Thread style. Sans 0.2: redraw in the style of Weft and match Weft's x-height (see `sans/STATUS.md`). npm packages for both families. | +| Stage | released 0.1: machine-made Weft 0.1.0, machine-made Sans 0.1.0, specimen site 0.1.0, machine-made Heddle 0.1.0. machine-made Warp 0.1 built on branch `warp/v0.1` | +| Gate now | Warp: Jac reviews the PR from `warp/v0.1`; the founder reviews the proofs; then the tags `warp-v0.1.0` and a new `site-v*` | +| Next task | Warp 0.2: the OpenType MATH table; Warp 0.3: the variable font (see `warp/STATUS.md`). Heddle 0.2 (see `heddle/STATUS.md`). Weft: polish (look-alikes l/1, I/1, O/0; heavy accented capitals), width axis, Thread style. Sans 0.2: redraw in the style of Weft and match Weft's x-height (see `sans/STATUS.md`). npm packages for both families. | | Blocked by | none | | Last green CI | none yet (first runs are on the release PR). Local end-to-end run of all steps on 2026-10-26: green | | Owner model | to assign | Family notes: `weft/STATUS.md`, `sans/STATUS.md`, -`heddle/STATUS.md`. Changes: `CHANGELOG.md`. +`heddle/STATUS.md`, `warp/STATUS.md`. Changes: `CHANGELOG.md`. ## Proven (local run, 2026-10-27) +### Warp 0.1 + +- 44 pytest tests pass (one runs the sketch engine in node and compares + advances and ink bounds with the port). +- The build is byte-reproducible: a fresh build equals the committed + `fonts/warp/`. +- Fontspector 1.9.0, universal profile: 0 FAIL, 0 ERROR on 6 TTF and 6 + OTF (accepted WARNs in `warp/STATUS.md`). +- Every glyph matches its pen sweep within 5 % of the ink + (`proofs/warp/fidelity.json`; largest 4.7 %). +- OCR gate (Tesseract 5.3.4, 10/12/14 px): Regular CER 2.3 %, Italic + 1.9 %; DejaVu Serif 0.1 % and 0 %. Gate (reference plus 3 points) + passes. See `warp/STATUS.md`, "Readability". +- The site builds with the Warp page; no console errors, no failed or + external requests, no horizontal scroll at 360 px; all six Warp + styles load. + ### Heddle 0.1 - 334 pytest tests pass (one runs the sketch engine in node and @@ -106,5 +123,11 @@ Family notes: `weft/STATUS.md`, `sans/STATUS.md`, - **Heddle CI has not run on GitHub.** `heddle.yml` and the `heddle` release job ran only as local copies of their steps (Python 3.13 locally, 3.14 in CI). +- **Warp name.** Several display faces are named "Warp" (Intellecta + Design 2007, Tilt Warp). Warp ships only as "machine-made Warp" and + "machine-made Warp Display". Web search only. See `warp/BRIEF.md`. +- **Warp CI has not run on GitHub.** `warp.yml` and the `warp` release + job ran only as local copies of their steps (Python 3.13 locally, + 3.14 in CI). The OCR numbers can move with another Tesseract build. - **Fontspector network checks** (`fontdata_namecheck` and others) do not run (`--skip-network`). diff --git a/fonts/warp/otf/MachineMadeWarp-Bold.otf b/fonts/warp/otf/MachineMadeWarp-Bold.otf new file mode 100644 index 0000000..b9a6835 Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarp-Bold.otf differ diff --git a/fonts/warp/otf/MachineMadeWarp-BoldItalic.otf b/fonts/warp/otf/MachineMadeWarp-BoldItalic.otf new file mode 100644 index 0000000..417322c Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarp-BoldItalic.otf differ diff --git a/fonts/warp/otf/MachineMadeWarp-Italic.otf b/fonts/warp/otf/MachineMadeWarp-Italic.otf new file mode 100644 index 0000000..fd80a3d Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarp-Italic.otf differ diff --git a/fonts/warp/otf/MachineMadeWarp-Regular.otf b/fonts/warp/otf/MachineMadeWarp-Regular.otf new file mode 100644 index 0000000..aa810b6 Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarp-Regular.otf differ diff --git a/fonts/warp/otf/MachineMadeWarpDisplay-Italic.otf b/fonts/warp/otf/MachineMadeWarpDisplay-Italic.otf new file mode 100644 index 0000000..223eff7 Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarpDisplay-Italic.otf differ diff --git a/fonts/warp/otf/MachineMadeWarpDisplay-Regular.otf b/fonts/warp/otf/MachineMadeWarpDisplay-Regular.otf new file mode 100644 index 0000000..5f469d5 Binary files /dev/null and b/fonts/warp/otf/MachineMadeWarpDisplay-Regular.otf differ diff --git a/fonts/warp/ttf/MachineMadeWarp-Bold.ttf b/fonts/warp/ttf/MachineMadeWarp-Bold.ttf new file mode 100644 index 0000000..0fc6041 Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarp-Bold.ttf differ diff --git a/fonts/warp/ttf/MachineMadeWarp-BoldItalic.ttf b/fonts/warp/ttf/MachineMadeWarp-BoldItalic.ttf new file mode 100644 index 0000000..13bc08c Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarp-BoldItalic.ttf differ diff --git a/fonts/warp/ttf/MachineMadeWarp-Italic.ttf b/fonts/warp/ttf/MachineMadeWarp-Italic.ttf new file mode 100644 index 0000000..a5ed91b Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarp-Italic.ttf differ diff --git a/fonts/warp/ttf/MachineMadeWarp-Regular.ttf b/fonts/warp/ttf/MachineMadeWarp-Regular.ttf new file mode 100644 index 0000000..48918cb Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarp-Regular.ttf differ diff --git a/fonts/warp/ttf/MachineMadeWarpDisplay-Italic.ttf b/fonts/warp/ttf/MachineMadeWarpDisplay-Italic.ttf new file mode 100644 index 0000000..1e3394d Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarpDisplay-Italic.ttf differ diff --git a/fonts/warp/ttf/MachineMadeWarpDisplay-Regular.ttf b/fonts/warp/ttf/MachineMadeWarpDisplay-Regular.ttf new file mode 100644 index 0000000..982b8b3 Binary files /dev/null and b/fonts/warp/ttf/MachineMadeWarpDisplay-Regular.ttf differ diff --git a/fonts/warp/webfonts/MachineMadeWarp-Bold.woff2 b/fonts/warp/webfonts/MachineMadeWarp-Bold.woff2 new file mode 100644 index 0000000..3db320b Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarp-Bold.woff2 differ diff --git a/fonts/warp/webfonts/MachineMadeWarp-BoldItalic.woff2 b/fonts/warp/webfonts/MachineMadeWarp-BoldItalic.woff2 new file mode 100644 index 0000000..06b58f2 Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarp-BoldItalic.woff2 differ diff --git a/fonts/warp/webfonts/MachineMadeWarp-Italic.woff2 b/fonts/warp/webfonts/MachineMadeWarp-Italic.woff2 new file mode 100644 index 0000000..aadcd5f Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarp-Italic.woff2 differ diff --git a/fonts/warp/webfonts/MachineMadeWarp-Regular.woff2 b/fonts/warp/webfonts/MachineMadeWarp-Regular.woff2 new file mode 100644 index 0000000..4d19480 Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarp-Regular.woff2 differ diff --git a/fonts/warp/webfonts/MachineMadeWarpDisplay-Italic.woff2 b/fonts/warp/webfonts/MachineMadeWarpDisplay-Italic.woff2 new file mode 100644 index 0000000..e890c35 Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarpDisplay-Italic.woff2 differ diff --git a/fonts/warp/webfonts/MachineMadeWarpDisplay-Regular.woff2 b/fonts/warp/webfonts/MachineMadeWarpDisplay-Regular.woff2 new file mode 100644 index 0000000..4c2fa7d Binary files /dev/null and b/fonts/warp/webfonts/MachineMadeWarpDisplay-Regular.woff2 differ diff --git a/proofs/warp/fidelity.json b/proofs/warp/fidelity.json new file mode 100644 index 0000000..651e292 --- /dev/null +++ b/proofs/warp/fidelity.json @@ -0,0 +1,11467 @@ +{ + "about": "Difference between each font glyph (compiled TTF) and its pen sweep, as a fraction of the ink, at several pixels per em. See warp/tools/fidelity.py.", + "sizes_ppem": [ + 16, + 32, + 64, + 128 + ], + "base_ppem": 2048, + "tolerance": { + "16": 0.05, + "32": 0.05, + "64": 0.05, + "128": 0.05 + }, + "styles": { + "Regular": { + "font": "MachineMadeWarp-Regular.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.03938, + "max_glyph": "underscore", + "mean": 0.01049, + "p95": 0.01763, + "pass": true + }, + "32": { + "max": 0.03938, + "max_glyph": "underscore", + "mean": 0.01253, + "p95": 0.02087, + "pass": true + }, + "64": { + "max": 0.03938, + "max_glyph": "underscore", + "mean": 0.01366, + "p95": 0.02266, + "pass": true + }, + "128": { + "max": 0.03941, + "max_glyph": "underscore", + "mean": 0.01412, + "p95": 0.02345, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.00933, + "32": 0.01146, + "64": 0.01298, + "128": 0.01325 + }, + "quotedbl": { + "16": 0.00909, + "32": 0.0118, + "64": 0.01408, + "128": 0.01493 + }, + "numbersign": { + "16": 0.00854, + "32": 0.00907, + "64": 0.01244, + "128": 0.01265 + }, + "dollar": { + "16": 0.01278, + "32": 0.01494, + "64": 0.01595, + "128": 0.01657 + }, + "percent": { + "16": 0.00817, + "32": 0.01026, + "64": 0.01077, + "128": 0.01099 + }, + "ampersand": { + "16": 0.01008, + "32": 0.01227, + "64": 0.01316, + "128": 0.01367 + }, + "quotesingle": { + "16": 0.01103, + "32": 0.01191, + "64": 0.01416, + "128": 0.01543 + }, + "parenleft": { + "16": 0.00806, + "32": 0.00871, + "64": 0.00932, + "128": 0.00962 + }, + "parenright": { + "16": 0.0066, + "32": 0.00719, + "64": 0.0078, + "128": 0.00837 + }, + "asterisk": { + "16": 0.01091, + "32": 0.01519, + "64": 0.01736, + "128": 0.01868 + }, + "plus": { + "16": 0.00674, + "32": 0.007, + "64": 0.0076, + "128": 0.00794 + }, + "comma": { + "16": 0.00783, + "32": 0.01012, + "64": 0.01151, + "128": 0.01174 + }, + "hyphen": { + "16": 0.00907, + "32": 0.01322, + "64": 0.01387, + "128": 0.01417 + }, + "period": { + "16": 0.01323, + "32": 0.02349, + "64": 0.02444, + "128": 0.02468 + }, + "slash": { + "16": 0.00902, + "32": 0.01017, + "64": 0.01057, + "128": 0.01079 + }, + "zero": { + "16": 0.00654, + "32": 0.00767, + "64": 0.00796, + "128": 0.00815 + }, + "one": { + "16": 0.01272, + "32": 0.01274, + "64": 0.0134, + "128": 0.01349 + }, + "two": { + "16": 0.00866, + "32": 0.0117, + "64": 0.01261, + "128": 0.01287 + }, + "three": { + "16": 0.01128, + "32": 0.01437, + "64": 0.01552, + "128": 0.01596 + }, + "four": { + "16": 0.00839, + "32": 0.00935, + "64": 0.00975, + "128": 0.0099 + }, + "five": { + "16": 0.01177, + "32": 0.01303, + "64": 0.01353, + "128": 0.01409 + }, + "six": { + "16": 0.00817, + "32": 0.00971, + "64": 0.01062, + "128": 0.011 + }, + "seven": { + "16": 0.00584, + "32": 0.00623, + "64": 0.0067, + "128": 0.00681 + }, + "eight": { + "16": 0.00844, + "32": 0.01005, + "64": 0.0107, + "128": 0.01094 + }, + "nine": { + "16": 0.00949, + "32": 0.01126, + "64": 0.01185, + "128": 0.0122 + }, + "colon": { + "16": 0.0081, + "32": 0.0186, + "64": 0.02053, + "128": 0.02077 + }, + "semicolon": { + "16": 0.00878, + "32": 0.01127, + "64": 0.012, + "128": 0.01311 + }, + "less": { + "16": 0.01336, + "32": 0.01336, + "64": 0.01336, + "128": 0.01342 + }, + "equal": { + "16": 0.01184, + "32": 0.0134, + "64": 0.01368, + "128": 0.01387 + }, + "greater": { + "16": 0.02028, + "32": 0.02028, + "64": 0.02041, + "128": 0.02055 + }, + "question": { + "16": 0.01266, + "32": 0.01703, + "64": 0.01764, + "128": 0.01816 + }, + "at": { + "16": 0.00988, + "32": 0.01172, + "64": 0.01264, + "128": 0.01296 + }, + "A": { + "16": 0.01849, + "32": 0.02046, + "64": 0.02129, + "128": 0.02198 + }, + "B": { + "16": 0.00728, + "32": 0.00958, + "64": 0.01032, + "128": 0.01067 + }, + "C": { + "16": 0.01123, + "32": 0.0126, + "64": 0.01417, + "128": 0.0147 + }, + "D": { + "16": 0.0069, + "32": 0.00841, + "64": 0.00888, + "128": 0.00916 + }, + "E": { + "16": 0.00793, + "32": 0.01052, + "64": 0.01086, + "128": 0.01119 + }, + "F": { + "16": 0.00738, + "32": 0.00948, + "64": 0.00959, + "128": 0.00978 + }, + "G": { + "16": 0.01187, + "32": 0.01326, + "64": 0.01419, + "128": 0.01473 + }, + "H": { + "16": 0.00698, + "32": 0.00793, + "64": 0.00824, + "128": 0.00846 + }, + "I": { + "16": 0.0067, + "32": 0.00781, + "64": 0.00791, + "128": 0.00824 + }, + "J": { + "16": 0.0107, + "32": 0.01289, + "64": 0.01347, + "128": 0.01374 + }, + "K": { + "16": 0.00931, + "32": 0.01108, + "64": 0.01252, + "128": 0.01329 + }, + "L": { + "16": 0.0072, + "32": 0.00845, + "64": 0.00871, + "128": 0.00925 + }, + "M": { + "16": 0.00983, + "32": 0.01045, + "64": 0.01066, + "128": 0.01094 + }, + "N": { + "16": 0.01915, + "32": 0.01988, + "64": 0.02018, + "128": 0.02039 + }, + "O": { + "16": 0.00868, + "32": 0.00975, + "64": 0.01017, + "128": 0.01043 + }, + "P": { + "16": 0.00846, + "32": 0.00921, + "64": 0.00999, + "128": 0.01025 + }, + "Q": { + "16": 0.00734, + "32": 0.00867, + "64": 0.00977, + "128": 0.01016 + }, + "R": { + "16": 0.00751, + "32": 0.00947, + "64": 0.01047, + "128": 0.01091 + }, + "S": { + "16": 0.01127, + "32": 0.01345, + "64": 0.0148, + "128": 0.01512 + }, + "T": { + "16": 0.00963, + "32": 0.01152, + "64": 0.01216, + "128": 0.01244 + }, + "U": { + "16": 0.01611, + "32": 0.01725, + "64": 0.01825, + "128": 0.01859 + }, + "V": { + "16": 0.01854, + "32": 0.02052, + "64": 0.02161, + "128": 0.02219 + }, + "W": { + "16": 0.01018, + "32": 0.01099, + "64": 0.01192, + "128": 0.01245 + }, + "X": { + "16": 0.01015, + "32": 0.01206, + "64": 0.01311, + "128": 0.01391 + }, + "Y": { + "16": 0.00828, + "32": 0.00902, + "64": 0.00959, + "128": 0.01023 + }, + "Z": { + "16": 0.0074, + "32": 0.00807, + "64": 0.00823, + "128": 0.00836 + }, + "bracketleft": { + "16": 0.00051, + "32": 0.00051, + "64": 0.00051, + "128": 0.00051 + }, + "backslash": { + "16": 0.00809, + "32": 0.0084, + "64": 0.00856, + "128": 0.00886 + }, + "bracketright": { + "16": 0.00035, + "32": 0.00035, + "64": 0.00037, + "128": 0.00037 + }, + "asciicircum": { + "16": 0.0116, + "32": 0.01229, + "64": 0.01243, + "128": 0.01243 + }, + "underscore": { + "16": 0.03938, + "32": 0.03938, + "64": 0.03938, + "128": 0.03941 + }, + "grave": { + "16": 0.00687, + "32": 0.01151, + "64": 0.01399, + "128": 0.0148 + }, + "a": { + "16": 0.01273, + "32": 0.01534, + "64": 0.01704, + "128": 0.01742 + }, + "b": { + "16": 0.01242, + "32": 0.01363, + "64": 0.01449, + "128": 0.01468 + }, + "c": { + "16": 0.01076, + "32": 0.01323, + "64": 0.01485, + "128": 0.01532 + }, + "d": { + "16": 0.01408, + "32": 0.01539, + "64": 0.01631, + "128": 0.01672 + }, + "e": { + "16": 0.01032, + "32": 0.01217, + "64": 0.01323, + "128": 0.0136 + }, + "f": { + "16": 0.00634, + "32": 0.00716, + "64": 0.00737, + "128": 0.00754 + }, + "g": { + "16": 0.00925, + "32": 0.01163, + "64": 0.01338, + "128": 0.01401 + }, + "h": { + "16": 0.01311, + "32": 0.01393, + "64": 0.01441, + "128": 0.01465 + }, + "i": { + "16": 0.0159, + "32": 0.01821, + "64": 0.01903, + "128": 0.01937 + }, + "j": { + "16": 0.01129, + "32": 0.01319, + "64": 0.01454, + "128": 0.01483 + }, + "k": { + "16": 0.01103, + "32": 0.0122, + "64": 0.01298, + "128": 0.01324 + }, + "l": { + "16": 0.01601, + "32": 0.01661, + "64": 0.01681, + "128": 0.017 + }, + "m": { + "16": 0.0138, + "32": 0.01538, + "64": 0.01607, + "128": 0.01627 + }, + "n": { + "16": 0.01356, + "32": 0.01493, + "64": 0.01533, + "128": 0.0155 + }, + "o": { + "16": 0.00762, + "32": 0.00877, + "64": 0.00932, + "128": 0.00956 + }, + "p": { + "16": 0.01327, + "32": 0.01456, + "64": 0.01515, + "128": 0.01537 + }, + "q": { + "16": 0.01256, + "32": 0.01385, + "64": 0.0148, + "128": 0.01495 + }, + "r": { + "16": 0.01388, + "32": 0.01543, + "64": 0.01663, + "128": 0.01711 + }, + "s": { + "16": 0.01499, + "32": 0.01696, + "64": 0.01819, + "128": 0.01862 + }, + "t": { + "16": 0.01351, + "32": 0.01547, + "64": 0.01618, + "128": 0.01645 + }, + "u": { + "16": 0.0142, + "32": 0.01698, + "64": 0.01747, + "128": 0.01779 + }, + "v": { + "16": 0.01074, + "32": 0.01194, + "64": 0.01358, + "128": 0.01404 + }, + "w": { + "16": 0.02095, + "32": 0.0218, + "64": 0.02379, + "128": 0.02427 + }, + "x": { + "16": 0.01072, + "32": 0.01172, + "64": 0.01401, + "128": 0.01434 + }, + "y": { + "16": 0.01367, + "32": 0.01446, + "64": 0.01567, + "128": 0.01627 + }, + "z": { + "16": 0.01101, + "32": 0.01184, + "64": 0.01201, + "128": 0.01214 + }, + "braceleft": { + "16": 0.01357, + "32": 0.01602, + "64": 0.01659, + "128": 0.01723 + }, + "bar": { + "16": 0.00045, + "32": 0.00049, + "64": 0.00054, + "128": 0.00057 + }, + "braceright": { + "16": 0.00848, + "32": 0.01032, + "64": 0.01164, + "128": 0.0123 + }, + "asciitilde": { + "16": 0.01755, + "32": 0.02195, + "64": 0.02279, + "128": 0.02353 + }, + "quoteleft": { + "16": 0.00793, + "32": 0.009, + "64": 0.01043, + "128": 0.0108 + }, + "quoteright": { + "16": 0.00919, + "32": 0.01319, + "64": 0.014, + "128": 0.01464 + }, + "quotedblleft": { + "16": 0.00807, + "32": 0.00926, + "64": 0.01038, + "128": 0.01119 + }, + "quotedblright": { + "16": 0.0097, + "32": 0.01321, + "64": 0.0143, + "128": 0.01487 + }, + "quotesinglbase": { + "16": 0.00783, + "32": 0.01012, + "64": 0.01151, + "128": 0.01174 + }, + "quotedblbase": { + "16": 0.0081, + "32": 0.01059, + "64": 0.012, + "128": 0.01242 + }, + "guilsinglleft": { + "16": 0.01519, + "32": 0.02168, + "64": 0.02255, + "128": 0.02363 + }, + "guilsinglright": { + "16": 0.0139, + "32": 0.01632, + "64": 0.01732, + "128": 0.01805 + }, + "guillemotleft": { + "16": 0.01576, + "32": 0.02076, + "64": 0.02195, + "128": 0.02298 + }, + "guillemotright": { + "16": 0.01387, + "32": 0.01699, + "64": 0.01873, + "128": 0.01931 + }, + "ellipsis": { + "16": 0.01782, + "32": 0.0237, + "64": 0.02511, + "128": 0.02537 + }, + "bullet": { + "16": 0.00835, + "32": 0.01182, + "64": 0.012, + "128": 0.01213 + }, + "periodcentered": { + "16": 0.00688, + "32": 0.01495, + "64": 0.01699, + "128": 0.01841 + }, + "Euro": { + "16": 0.01004, + "32": 0.01194, + "64": 0.01287, + "128": 0.01352 + }, + "copyright": { + "16": 0.01195, + "32": 0.01428, + "64": 0.01568, + "128": 0.01622 + }, + "registered": { + "16": 0.01283, + "32": 0.0149, + "64": 0.01645, + "128": 0.01691 + }, + "degree": { + "16": 0.00593, + "32": 0.01, + "64": 0.01287, + "128": 0.0137 + }, + "emdash": { + "16": 0.00029, + "32": 0.00029, + "64": 0.00029, + "128": 0.00063 + }, + "endash": { + "16": 0.00143, + "32": 0.00153, + "64": 0.00153, + "128": 0.00167 + }, + "exclamdown": { + "16": 0.00655, + "32": 0.01103, + "64": 0.01179, + "128": 0.01229 + }, + "questiondown": { + "16": 0.00727, + "32": 0.01183, + "64": 0.01383, + "128": 0.01442 + }, + "section": { + "16": 0.0096, + "32": 0.01245, + "64": 0.01388, + "128": 0.01429 + }, + "paragraph": { + "16": 0.001, + "32": 0.00116, + "64": 0.00142, + "128": 0.00152 + }, + "cent": { + "16": 0.0116, + "32": 0.01392, + "64": 0.01519, + "128": 0.01572 + }, + "sterling": { + "16": 0.0127, + "32": 0.01444, + "64": 0.0152, + "128": 0.01558 + }, + "yen": { + "16": 0.01217, + "32": 0.01335, + "64": 0.01717, + "128": 0.01768 + }, + "brokenbar": { + "16": 0.00088, + "32": 0.00092, + "64": 0.00111, + "128": 0.00128 + }, + "logicalnot": { + "16": 0.01814, + "32": 0.02159, + "64": 0.02199, + "128": 0.02222 + }, + "dieresis": { + "16": 0.01437, + "32": 0.02096, + "64": 0.02248, + "128": 0.02329 + }, + "acute": { + "16": 0.00607, + "32": 0.00893, + "64": 0.01161, + "128": 0.01347 + }, + "macron": { + "16": 0.00304, + "32": 0.00304, + "64": 0.0033, + "128": 0.00384 + }, + "cedilla": { + "16": 0.01352, + "32": 0.01731, + "64": 0.02088, + "128": 0.02165 + }, + "circumflex": { + "16": 0.00965, + "32": 0.01757, + "64": 0.01924, + "128": 0.02083 + }, + "tilde": { + "16": 0.01849, + "32": 0.02255, + "64": 0.02519, + "128": 0.02691 + }, + "ring": { + "16": 0.00711, + "32": 0.01042, + "64": 0.01235, + "128": 0.01318 + }, + "caron": { + "16": 0.01369, + "32": 0.01835, + "64": 0.02079, + "128": 0.02157 + }, + "dotaccent": { + "16": 0.01258, + "32": 0.01805, + "64": 0.02036, + "128": 0.02128 + }, + "AE": { + "16": 0.00973, + "32": 0.01181, + "64": 0.01268, + "128": 0.01315 + }, + "Eth": { + "16": 0.00513, + "32": 0.00653, + "64": 0.00701, + "128": 0.00733 + }, + "Oslash": { + "16": 0.0116, + "32": 0.01299, + "64": 0.01373, + "128": 0.01402 + }, + "Thorn": { + "16": 0.007, + "32": 0.0091, + "64": 0.0098, + "128": 0.01015 + }, + "germandbls": { + "16": 0.00947, + "32": 0.01072, + "64": 0.01108, + "128": 0.01156 + }, + "ae": { + "16": 0.01192, + "32": 0.0148, + "64": 0.01708, + "128": 0.01759 + }, + "eth": { + "16": 0.00878, + "32": 0.00974, + "64": 0.01049, + "128": 0.01069 + }, + "oslash": { + "16": 0.00899, + "32": 0.01072, + "64": 0.01177, + "128": 0.01211 + }, + "thorn": { + "16": 0.01254, + "32": 0.01333, + "64": 0.01407, + "128": 0.01424 + }, + "OE": { + "16": 0.00782, + "32": 0.00917, + "64": 0.00995, + "128": 0.0103 + }, + "oe": { + "16": 0.0093, + "32": 0.01143, + "64": 0.01223, + "128": 0.01268 + }, + "dotlessi": { + "16": 0.01542, + "32": 0.01708, + "64": 0.01739, + "128": 0.01774 + }, + "dotlessj": { + "16": 0.01063, + "32": 0.01273, + "64": 0.01345, + "128": 0.01362 + }, + "f_f": { + "16": 0.00863, + "32": 0.00907, + "64": 0.00933, + "128": 0.00958 + }, + "fi": { + "16": 0.00998, + "32": 0.01063, + "64": 0.0108, + "128": 0.01093 + }, + "fl": { + "16": 0.01044, + "32": 0.01063, + "64": 0.01079, + "128": 0.01091 + }, + "f_f_i": { + "16": 0.01021, + "32": 0.01078, + "64": 0.01114, + "128": 0.01142 + }, + "f_f_l": { + "16": 0.01046, + "32": 0.01078, + "64": 0.01101, + "128": 0.01126 + }, + "Gamma": { + "16": 0.00606, + "32": 0.00762, + "64": 0.00775, + "128": 0.00792 + }, + "Delta": { + "16": 0.01037, + "32": 0.01344, + "64": 0.01648, + "128": 0.01699 + }, + "Theta": { + "16": 0.00948, + "32": 0.01056, + "64": 0.01097, + "128": 0.01122 + }, + "Lambda": { + "16": 0.01853, + "32": 0.0206, + "64": 0.02153, + "128": 0.02223 + }, + "Xi": { + "16": 0.01731, + "32": 0.01811, + "64": 0.01898, + "128": 0.01954 + }, + "Pi": { + "16": 0.00876, + "32": 0.0093, + "64": 0.00948, + "128": 0.00973 + }, + "Sigma": { + "16": 0.00956, + "32": 0.01005, + "64": 0.01041, + "128": 0.01064 + }, + "Phi": { + "16": 0.00827, + "32": 0.00936, + "64": 0.00992, + "128": 0.01019 + }, + "Psi": { + "16": 0.0147, + "32": 0.01693, + "64": 0.01762, + "128": 0.01792 + }, + "Omega": { + "16": 0.01019, + "32": 0.01131, + "64": 0.01238, + "128": 0.01282 + }, + "alpha": { + "16": 0.0134, + "32": 0.01642, + "64": 0.01736, + "128": 0.01758 + }, + "beta": { + "16": 0.0054, + "32": 0.00611, + "64": 0.00693, + "128": 0.00728 + }, + "gamma": { + "16": 0.0108, + "32": 0.01148, + "64": 0.01197, + "128": 0.0125 + }, + "delta": { + "16": 0.00808, + "32": 0.00937, + "64": 0.0103, + "128": 0.01064 + }, + "epsilon": { + "16": 0.01302, + "32": 0.01679, + "64": 0.01929, + "128": 0.02 + }, + "zeta": { + "16": 0.00959, + "32": 0.01272, + "64": 0.01454, + "128": 0.01508 + }, + "eta": { + "16": 0.00945, + "32": 0.01107, + "64": 0.01195, + "128": 0.01211 + }, + "theta": { + "16": 0.00629, + "32": 0.00695, + "64": 0.00725, + "128": 0.00744 + }, + "iota": { + "16": 0.00213, + "32": 0.00292, + "64": 0.0041, + "128": 0.00433 + }, + "kappa": { + "16": 0.00786, + "32": 0.00973, + "64": 0.01143, + "128": 0.01186 + }, + "lambda": { + "16": 0.01012, + "32": 0.01176, + "64": 0.01204, + "128": 0.01233 + }, + "mu": { + "16": 0.00295, + "32": 0.00432, + "64": 0.0051, + "128": 0.00539 + }, + "nu": { + "16": 0.01116, + "32": 0.01273, + "64": 0.01368, + "128": 0.01406 + }, + "xi": { + "16": 0.01062, + "32": 0.01307, + "64": 0.01476, + "128": 0.01537 + }, + "pi": { + "16": 0.00857, + "32": 0.00943, + "64": 0.01005, + "128": 0.01018 + }, + "rho": { + "16": 0.00825, + "32": 0.00908, + "64": 0.00959, + "128": 0.00984 + }, + "sigmafinal": { + "16": 0.01197, + "32": 0.01459, + "64": 0.01612, + "128": 0.01674 + }, + "sigma": { + "16": 0.00721, + "32": 0.00945, + "64": 0.01023, + "128": 0.01051 + }, + "tau": { + "16": 0.01145, + "32": 0.01298, + "64": 0.01387, + "128": 0.01403 + }, + "upsilon": { + "16": 0.00474, + "32": 0.00516, + "64": 0.00596, + "128": 0.00653 + }, + "phi": { + "16": 0.00621, + "32": 0.00666, + "64": 0.00723, + "128": 0.00747 + }, + "chi": { + "16": 0.00701, + "32": 0.00815, + "64": 0.0093, + "128": 0.0095 + }, + "psi": { + "16": 0.00356, + "32": 0.00406, + "64": 0.00472, + "128": 0.00492 + }, + "omega": { + "16": 0.01209, + "32": 0.01395, + "64": 0.015, + "128": 0.01546 + }, + "uni20B9": { + "16": 0.0159, + "32": 0.0171, + "64": 0.0198, + "128": 0.02018 + }, + "minus": { + "16": 0.02192, + "32": 0.02501, + "64": 0.02549, + "128": 0.02579 + }, + "multiply": { + "16": 0.00922, + "32": 0.01151, + "64": 0.01192, + "128": 0.01235 + }, + "divide": { + "16": 0.00375, + "32": 0.00526, + "64": 0.0055, + "128": 0.00577 + }, + "plusminus": { + "16": 0.01111, + "32": 0.01265, + "64": 0.01331, + "128": 0.01362 + }, + "notequal": { + "16": 0.00713, + "32": 0.00778, + "64": 0.0081, + "128": 0.00834 + }, + "lessequal": { + "16": 0.01527, + "32": 0.01641, + "64": 0.01682, + "128": 0.01703 + }, + "greaterequal": { + "16": 0.01378, + "32": 0.01521, + "64": 0.01566, + "128": 0.01601 + }, + "summation": { + "16": 0.01969, + "32": 0.0203, + "64": 0.021, + "128": 0.02106 + }, + "product": { + "16": 0.00886, + "32": 0.00942, + "64": 0.00961, + "128": 0.00966 + }, + "integral": { + "16": 0.00803, + "32": 0.00882, + "64": 0.00937, + "128": 0.00949 + }, + "radical": { + "16": 0.01249, + "32": 0.01393, + "64": 0.01631, + "128": 0.01665 + }, + "infinity": { + "16": 0.00707, + "32": 0.00887, + "64": 0.0096, + "128": 0.0099 + }, + "partialdiff": { + "16": 0.0075, + "32": 0.00869, + "64": 0.00963, + "128": 0.00997 + }, + "nabla": { + "16": 0.00883, + "32": 0.00965, + "64": 0.0108, + "128": 0.01093 + }, + "minute": { + "16": 0.00833, + "32": 0.01064, + "64": 0.01294, + "128": 0.01325 + }, + "second": { + "16": 0.00787, + "32": 0.01025, + "64": 0.01215, + "128": 0.0125 + }, + "ringoperator": { + "16": 0.01325, + "32": 0.01629, + "64": 0.01868, + "128": 0.01946 + }, + "arrowright": { + "16": 0.00996, + "32": 0.01176, + "64": 0.01286, + "128": 0.0132 + }, + "arrowleft": { + "16": 0.01017, + "32": 0.01162, + "64": 0.01236, + "128": 0.01289 + }, + "arrowboth": { + "16": 0.01034, + "32": 0.0111, + "64": 0.01193, + "128": 0.01251 + }, + "element": { + "16": 0.01911, + "32": 0.0208, + "64": 0.02159, + "128": 0.02194 + }, + "notelement": { + "16": 0.00663, + "32": 0.00864, + "64": 0.0091, + "128": 0.00928 + }, + "propersubset": { + "16": 0.01627, + "32": 0.0177, + "64": 0.01859, + "128": 0.01892 + }, + "reflexsubset": { + "16": 0.01246, + "32": 0.01351, + "64": 0.01421, + "128": 0.01445 + }, + "union": { + "16": 0.01431, + "32": 0.01507, + "64": 0.01529, + "128": 0.01556 + }, + "intersection": { + "16": 0.00971, + "32": 0.01224, + "64": 0.01301, + "128": 0.01332 + }, + "universal": { + "16": 0.01211, + "32": 0.01259, + "64": 0.013, + "128": 0.01322 + }, + "existential": { + "16": 0.00882, + "32": 0.00901, + "64": 0.00904, + "128": 0.00919 + }, + "emptyset": { + "16": 0.00683, + "32": 0.00766, + "64": 0.00812, + "128": 0.00826 + }, + "approxequal": { + "16": 0.01542, + "32": 0.01884, + "64": 0.0196, + "128": 0.02068 + }, + "equivalence": { + "16": 0.02158, + "32": 0.02436, + "64": 0.02526, + "128": 0.0256 + }, + "proportional": { + "16": 0.01137, + "32": 0.0135, + "64": 0.01418, + "128": 0.0145 + }, + "fraction": { + "16": 0.01591, + "32": 0.01633, + "64": 0.01688, + "128": 0.01701 + }, + "onesuperior": { + "16": 0.0156, + "32": 0.01693, + "64": 0.01813, + "128": 0.01856 + }, + "twosuperior": { + "16": 0.01432, + "32": 0.01598, + "64": 0.01739, + "128": 0.01841 + }, + "threesuperior": { + "16": 0.01438, + "32": 0.01918, + "64": 0.02161, + "128": 0.02261 + }, + "ordfeminine": { + "16": 0.01406, + "32": 0.0191, + "64": 0.02384, + "128": 0.0249 + }, + "ordmasculine": { + "16": 0.00815, + "32": 0.01122, + "64": 0.01271, + "128": 0.01363 + }, + "dottedcircle": { + "16": 0.00999, + "32": 0.01703, + "64": 0.02328, + "128": 0.02692 + }, + "gravecomb": { + "16": 0.00831, + "32": 0.01251, + "64": 0.01489, + "128": 0.01599 + }, + "gravecomb.case": { + "16": 0.00565, + "32": 0.01798, + "64": 0.01903, + "128": 0.02037 + }, + "acutecomb": { + "16": 0.01328, + "32": 0.01438, + "64": 0.01658, + "128": 0.01758 + }, + "acutecomb.case": { + "16": 0.01185, + "32": 0.01616, + "64": 0.01825, + "128": 0.02099 + }, + "circumflexcomb": { + "16": 0.00927, + "32": 0.01568, + "64": 0.01751, + "128": 0.01887 + }, + "circumflexcomb.case": { + "16": 0.01035, + "32": 0.02198, + "64": 0.02513, + "128": 0.02642 + }, + "tildecomb": { + "16": 0.01773, + "32": 0.02162, + "64": 0.0256, + "128": 0.02814 + }, + "tildecomb.case": { + "16": 0.0164, + "32": 0.02126, + "64": 0.02704, + "128": 0.02841 + }, + "macroncomb": { + "16": 0.00294, + "32": 0.00294, + "64": 0.00327, + "128": 0.00374 + }, + "dieresiscomb": { + "16": 0.01393, + "32": 0.01876, + "64": 0.02006, + "128": 0.02172 + }, + "dieresiscomb.case": { + "16": 0.01642, + "32": 0.02176, + "64": 0.02187, + "128": 0.02254 + }, + "ringcomb": { + "16": 0.01066, + "32": 0.01242, + "64": 0.01513, + "128": 0.01545 + }, + "ringcomb.case": { + "16": 0.00813, + "32": 0.00986, + "64": 0.01165, + "128": 0.01236 + }, + "caroncomb": { + "16": 0.01322, + "32": 0.01923, + "64": 0.02312, + "128": 0.02339 + }, + "dotaccentcomb": { + "16": 0.01647, + "32": 0.01667, + "64": 0.02411, + "128": 0.02464 + }, + "cedillacomb": { + "16": 0.01256, + "32": 0.01518, + "64": 0.02186, + "128": 0.02287 + }, + "zero.lf": { + "16": 0.00683, + "32": 0.00786, + "64": 0.00814, + "128": 0.00834 + }, + "one.lf": { + "16": 0.0051, + "32": 0.00512, + "64": 0.0052, + "128": 0.00551 + }, + "two.lf": { + "16": 0.0092, + "32": 0.01067, + "64": 0.01154, + "128": 0.01195 + }, + "three.lf": { + "16": 0.01225, + "32": 0.01445, + "64": 0.01596, + "128": 0.01644 + }, + "four.lf": { + "16": 0.0141, + "32": 0.01626, + "64": 0.01685, + "128": 0.01709 + }, + "five.lf": { + "16": 0.00983, + "32": 0.0112, + "64": 0.01165, + "128": 0.01184 + }, + "six.lf": { + "16": 0.00742, + "32": 0.00921, + "64": 0.00999, + "128": 0.01047 + }, + "seven.lf": { + "16": 0.00425, + "32": 0.00506, + "64": 0.00724, + "128": 0.0076 + }, + "eight.lf": { + "16": 0.00919, + "32": 0.01096, + "64": 0.01171, + "128": 0.01203 + }, + "nine.lf": { + "16": 0.00915, + "32": 0.01071, + "64": 0.01131, + "128": 0.01158 + }, + "zero.osf": { + "16": 0.00734, + "32": 0.00785, + "64": 0.00839, + "128": 0.00858 + }, + "one.osf": { + "16": 0.01055, + "32": 0.01154, + "64": 0.01204, + "128": 0.01231 + }, + "two.osf": { + "16": 0.01408, + "32": 0.01644, + "64": 0.01806, + "128": 0.0185 + }, + "three.osf": { + "16": 0.01467, + "32": 0.01704, + "64": 0.01876, + "128": 0.01922 + }, + "four.osf": { + "16": 0.01416, + "32": 0.01683, + "64": 0.02028, + "128": 0.0206 + }, + "five.osf": { + "16": 0.0133, + "32": 0.01602, + "64": 0.01753, + "128": 0.01828 + }, + "six.osf": { + "16": 0.00872, + "32": 0.01095, + "64": 0.01195, + "128": 0.0123 + }, + "seven.osf": { + "16": 0.00707, + "32": 0.00839, + "64": 0.00891, + "128": 0.00916 + }, + "eight.osf": { + "16": 0.00836, + "32": 0.00945, + "64": 0.01001, + "128": 0.01026 + }, + "nine.osf": { + "16": 0.00829, + "32": 0.00937, + "64": 0.01031, + "128": 0.01073 + }, + "zero.tosf": { + "16": 0.00768, + "32": 0.00846, + "64": 0.00887, + "128": 0.0091 + }, + "one.tosf": { + "16": 0.00622, + "32": 0.00696, + "64": 0.0077, + "128": 0.00808 + }, + "two.tosf": { + "16": 0.01335, + "32": 0.01729, + "64": 0.01907, + "128": 0.01962 + }, + "three.tosf": { + "16": 0.01301, + "32": 0.01468, + "64": 0.0165, + "128": 0.01709 + }, + "four.tosf": { + "16": 0.0068, + "32": 0.00774, + "64": 0.00933, + "128": 0.00953 + }, + "five.tosf": { + "16": 0.01014, + "32": 0.01287, + "64": 0.01436, + "128": 0.01478 + }, + "six.tosf": { + "16": 0.00823, + "32": 0.011, + "64": 0.01204, + "128": 0.01241 + }, + "seven.tosf": { + "16": 0.00704, + "32": 0.00818, + "64": 0.00877, + "128": 0.00902 + }, + "eight.tosf": { + "16": 0.00779, + "32": 0.00895, + "64": 0.00961, + "128": 0.00994 + }, + "nine.tosf": { + "16": 0.00821, + "32": 0.00973, + "64": 0.01035, + "128": 0.01068 + }, + "two.dnom": { + "16": 0.01432, + "32": 0.01598, + "64": 0.01739, + "128": 0.01841 + }, + "four.dnom": { + "16": 0.01271, + "32": 0.01609, + "64": 0.01854, + "128": 0.01938 + }, + "a.sc": { + "16": 0.00753, + "32": 0.01462, + "64": 0.01634, + "128": 0.01683 + }, + "b.sc": { + "16": 0.00824, + "32": 0.00994, + "64": 0.01139, + "128": 0.01171 + }, + "c.sc": { + "16": 0.01545, + "32": 0.01644, + "64": 0.01755, + "128": 0.01788 + }, + "d.sc": { + "16": 0.00534, + "32": 0.00627, + "64": 0.0071, + "128": 0.00753 + }, + "e.sc": { + "16": 0.01066, + "32": 0.01229, + "64": 0.0128, + "128": 0.01313 + }, + "f.sc": { + "16": 0.01303, + "32": 0.01377, + "64": 0.01408, + "128": 0.01429 + }, + "g.sc": { + "16": 0.01225, + "32": 0.01401, + "64": 0.0147, + "128": 0.01511 + }, + "h.sc": { + "16": 0.00852, + "32": 0.01035, + "64": 0.01142, + "128": 0.01185 + }, + "i.sc": { + "16": 0.00683, + "32": 0.01037, + "64": 0.01144, + "128": 0.01196 + }, + "j.sc": { + "16": 0.00457, + "32": 0.00522, + "64": 0.00643, + "128": 0.00669 + }, + "k.sc": { + "16": 0.00813, + "32": 0.01139, + "64": 0.01247, + "128": 0.01326 + }, + "l.sc": { + "16": 0.0093, + "32": 0.01154, + "64": 0.01248, + "128": 0.01294 + }, + "m.sc": { + "16": 0.01084, + "32": 0.01261, + "64": 0.01443, + "128": 0.01493 + }, + "n.sc": { + "16": 0.02066, + "32": 0.02226, + "64": 0.02445, + "128": 0.02509 + }, + "o.sc": { + "16": 0.00847, + "32": 0.01013, + "64": 0.0107, + "128": 0.01097 + }, + "p.sc": { + "16": 0.00713, + "32": 0.00844, + "64": 0.00961, + "128": 0.01 + }, + "q.sc": { + "16": 0.00958, + "32": 0.01139, + "64": 0.01197, + "128": 0.01236 + }, + "r.sc": { + "16": 0.00771, + "32": 0.00955, + "64": 0.01091, + "128": 0.01144 + }, + "s.sc": { + "16": 0.01106, + "32": 0.01312, + "64": 0.01397, + "128": 0.01464 + }, + "t.sc": { + "16": 0.01588, + "32": 0.017, + "64": 0.0173, + "128": 0.01762 + }, + "u.sc": { + "16": 0.01748, + "32": 0.01934, + "64": 0.02154, + "128": 0.022 + }, + "v.sc": { + "16": 0.00784, + "32": 0.01024, + "64": 0.01266, + "128": 0.01298 + }, + "w.sc": { + "16": 0.01426, + "32": 0.01619, + "64": 0.01828, + "128": 0.01877 + }, + "x.sc": { + "16": 0.01345, + "32": 0.01686, + "64": 0.01901, + "128": 0.0199 + }, + "y.sc": { + "16": 0.01105, + "32": 0.0139, + "64": 0.01643, + "128": 0.01703 + }, + "z.sc": { + "16": 0.00842, + "32": 0.01065, + "64": 0.01094, + "128": 0.01115 + }, + "ae.sc": { + "16": 0.00813, + "32": 0.01058, + "64": 0.01182, + "128": 0.01227 + }, + "oe.sc": { + "16": 0.01141, + "32": 0.01346, + "64": 0.01446, + "128": 0.01467 + }, + "oslash.sc": { + "16": 0.01219, + "32": 0.01358, + "64": 0.01401, + "128": 0.01429 + }, + "eth.sc": { + "16": 0.01071, + "32": 0.01231, + "64": 0.0131, + "128": 0.01334 + }, + "thorn.sc": { + "16": 0.01121, + "32": 0.01347, + "64": 0.01489, + "128": 0.01531 + }, + "gamma.sc": { + "16": 0.01194, + "32": 0.01346, + "64": 0.01361, + "128": 0.01386 + }, + "delta.sc": { + "16": 0.00502, + "32": 0.00568, + "64": 0.00652, + "128": 0.00665 + }, + "theta.sc": { + "16": 0.00995, + "32": 0.01163, + "64": 0.01273, + "128": 0.01305 + }, + "lambda.sc": { + "16": 0.00805, + "32": 0.01257, + "64": 0.01456, + "128": 0.01513 + }, + "xi.sc": { + "16": 0.01446, + "32": 0.01593, + "64": 0.01635, + "128": 0.01696 + }, + "pi.sc": { + "16": 0.00638, + "32": 0.00678, + "64": 0.00707, + "128": 0.00716 + }, + "sigma.sc": { + "16": 0.01288, + "32": 0.01383, + "64": 0.01417, + "128": 0.01443 + }, + "phi.sc": { + "16": 0.0098, + "32": 0.0122, + "64": 0.01286, + "128": 0.01317 + }, + "psi.sc": { + "16": 0.0111, + "32": 0.01254, + "64": 0.01393, + "128": 0.01446 + }, + "omega.sc": { + "16": 0.01046, + "32": 0.01123, + "64": 0.01213, + "128": 0.01254 + } + } + }, + "Italic": { + "font": "MachineMadeWarp-Italic.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.03955, + "max_glyph": "underscore", + "mean": 0.01004, + "p95": 0.0161, + "pass": true + }, + "32": { + "max": 0.03955, + "max_glyph": "underscore", + "mean": 0.01212, + "p95": 0.02037, + "pass": true + }, + "64": { + "max": 0.03955, + "max_glyph": "underscore", + "mean": 0.0133, + "p95": 0.02232, + "pass": true + }, + "128": { + "max": 0.03955, + "max_glyph": "underscore", + "mean": 0.01376, + "p95": 0.02352, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.00851, + "32": 0.01259, + "64": 0.01359, + "128": 0.01381 + }, + "quotedbl": { + "16": 0.00841, + "32": 0.01288, + "64": 0.01398, + "128": 0.01441 + }, + "numbersign": { + "16": 0.00769, + "32": 0.00815, + "64": 0.0116, + "128": 0.0118 + }, + "dollar": { + "16": 0.01221, + "32": 0.01469, + "64": 0.01628, + "128": 0.01673 + }, + "percent": { + "16": 0.00864, + "32": 0.01077, + "64": 0.01123, + "128": 0.01146 + }, + "ampersand": { + "16": 0.01012, + "32": 0.01226, + "64": 0.01334, + "128": 0.01368 + }, + "quotesingle": { + "16": 0.00686, + "32": 0.01436, + "64": 0.01544, + "128": 0.01582 + }, + "parenleft": { + "16": 0.00582, + "32": 0.00713, + "64": 0.00811, + "128": 0.0085 + }, + "parenright": { + "16": 0.00882, + "32": 0.00966, + "64": 0.01059, + "128": 0.01099 + }, + "asterisk": { + "16": 0.01376, + "32": 0.01569, + "64": 0.01799, + "128": 0.01956 + }, + "plus": { + "16": 0.00674, + "32": 0.007, + "64": 0.0076, + "128": 0.00794 + }, + "comma": { + "16": 0.00778, + "32": 0.00972, + "64": 0.01158, + "128": 0.01223 + }, + "hyphen": { + "16": 0.00924, + "32": 0.0136, + "64": 0.01415, + "128": 0.01445 + }, + "period": { + "16": 0.01133, + "32": 0.02372, + "64": 0.02468, + "128": 0.02492 + }, + "slash": { + "16": 0.00683, + "32": 0.00794, + "64": 0.00841, + "128": 0.00868 + }, + "zero": { + "16": 0.00647, + "32": 0.00726, + "64": 0.0076, + "128": 0.00773 + }, + "one": { + "16": 0.0168, + "32": 0.01727, + "64": 0.0176, + "128": 0.01792 + }, + "two": { + "16": 0.00906, + "32": 0.01073, + "64": 0.01191, + "128": 0.0124 + }, + "three": { + "16": 0.01102, + "32": 0.01323, + "64": 0.01428, + "128": 0.01485 + }, + "four": { + "16": 0.01087, + "32": 0.01236, + "64": 0.01295, + "128": 0.01307 + }, + "five": { + "16": 0.00743, + "32": 0.00966, + "64": 0.01069, + "128": 0.01109 + }, + "six": { + "16": 0.00823, + "32": 0.0097, + "64": 0.01038, + "128": 0.0106 + }, + "seven": { + "16": 0.00672, + "32": 0.00884, + "64": 0.00938, + "128": 0.00964 + }, + "eight": { + "16": 0.00792, + "32": 0.00896, + "64": 0.00955, + "128": 0.00988 + }, + "nine": { + "16": 0.00772, + "32": 0.00867, + "64": 0.00957, + "128": 0.00994 + }, + "colon": { + "16": 0.00738, + "32": 0.01889, + "64": 0.02184, + "128": 0.02202 + }, + "semicolon": { + "16": 0.0075, + "32": 0.01254, + "64": 0.01503, + "128": 0.0157 + }, + "less": { + "16": 0.01336, + "32": 0.01336, + "64": 0.01336, + "128": 0.01342 + }, + "equal": { + "16": 0.01184, + "32": 0.0134, + "64": 0.01368, + "128": 0.01387 + }, + "greater": { + "16": 0.02028, + "32": 0.02028, + "64": 0.02041, + "128": 0.02055 + }, + "question": { + "16": 0.01198, + "32": 0.01469, + "64": 0.01525, + "128": 0.01565 + }, + "at": { + "16": 0.01108, + "32": 0.01387, + "64": 0.01551, + "128": 0.01579 + }, + "A": { + "16": 0.01137, + "32": 0.013, + "64": 0.01414, + "128": 0.01466 + }, + "B": { + "16": 0.00894, + "32": 0.01087, + "64": 0.01164, + "128": 0.01189 + }, + "C": { + "16": 0.01017, + "32": 0.0123, + "64": 0.01299, + "128": 0.01348 + }, + "D": { + "16": 0.00865, + "32": 0.0106, + "64": 0.01116, + "128": 0.01141 + }, + "E": { + "16": 0.00757, + "32": 0.00872, + "64": 0.00913, + "128": 0.0094 + }, + "F": { + "16": 0.0066, + "32": 0.00812, + "64": 0.00871, + "128": 0.00887 + }, + "G": { + "16": 0.01084, + "32": 0.01292, + "64": 0.01448, + "128": 0.01505 + }, + "H": { + "16": 0.00716, + "32": 0.00786, + "64": 0.00809, + "128": 0.0082 + }, + "I": { + "16": 0.008, + "32": 0.00885, + "64": 0.00924, + "128": 0.0094 + }, + "J": { + "16": 0.00481, + "32": 0.00628, + "64": 0.00669, + "128": 0.00696 + }, + "K": { + "16": 0.0097, + "32": 0.01154, + "64": 0.01251, + "128": 0.01274 + }, + "L": { + "16": 0.0092, + "32": 0.01006, + "64": 0.01031, + "128": 0.0106 + }, + "M": { + "16": 0.01073, + "32": 0.01179, + "64": 0.01268, + "128": 0.01304 + }, + "N": { + "16": 0.00947, + "32": 0.01078, + "64": 0.01176, + "128": 0.01218 + }, + "O": { + "16": 0.0081, + "32": 0.00952, + "64": 0.01007, + "128": 0.01029 + }, + "P": { + "16": 0.00843, + "32": 0.00934, + "64": 0.00979, + "128": 0.0099 + }, + "Q": { + "16": 0.0098, + "32": 0.01075, + "64": 0.01148, + "128": 0.01183 + }, + "R": { + "16": 0.00787, + "32": 0.0099, + "64": 0.01093, + "128": 0.0112 + }, + "S": { + "16": 0.01, + "32": 0.01134, + "64": 0.01254, + "128": 0.0131 + }, + "T": { + "16": 0.00466, + "32": 0.00536, + "64": 0.00584, + "128": 0.00601 + }, + "U": { + "16": 0.01134, + "32": 0.01473, + "64": 0.01577, + "128": 0.01612 + }, + "V": { + "16": 0.01065, + "32": 0.01324, + "64": 0.01401, + "128": 0.01436 + }, + "W": { + "16": 0.00988, + "32": 0.01092, + "64": 0.0119, + "128": 0.01212 + }, + "X": { + "16": 0.00993, + "32": 0.01155, + "64": 0.01453, + "128": 0.01517 + }, + "Y": { + "16": 0.00632, + "32": 0.00895, + "64": 0.01038, + "128": 0.0108 + }, + "Z": { + "16": 0.00931, + "32": 0.0101, + "64": 0.01024, + "128": 0.0103 + }, + "bracketleft": { + "16": 0.00487, + "32": 0.00487, + "64": 0.00488, + "128": 0.00491 + }, + "backslash": { + "16": 0.01119, + "32": 0.01119, + "64": 0.01183, + "128": 0.01204 + }, + "bracketright": { + "16": 0.00628, + "32": 0.00628, + "64": 0.00628, + "128": 0.00629 + }, + "asciicircum": { + "16": 0.0116, + "32": 0.01229, + "64": 0.01243, + "128": 0.01243 + }, + "underscore": { + "16": 0.03955, + "32": 0.03955, + "64": 0.03955, + "128": 0.03955 + }, + "grave": { + "16": 0.00909, + "32": 0.01102, + "64": 0.01368, + "128": 0.01461 + }, + "a": { + "16": 0.00798, + "32": 0.00937, + "64": 0.01118, + "128": 0.01159 + }, + "b": { + "16": 0.00852, + "32": 0.00966, + "64": 0.01029, + "128": 0.01056 + }, + "c": { + "16": 0.01122, + "32": 0.01359, + "64": 0.01465, + "128": 0.01518 + }, + "d": { + "16": 0.00755, + "32": 0.0091, + "64": 0.01023, + "128": 0.01058 + }, + "e": { + "16": 0.00916, + "32": 0.01208, + "64": 0.01446, + "128": 0.01504 + }, + "f": { + "16": 0.00938, + "32": 0.01, + "64": 0.01071, + "128": 0.011 + }, + "g": { + "16": 0.00866, + "32": 0.00934, + "64": 0.01046, + "128": 0.01093 + }, + "h": { + "16": 0.00656, + "32": 0.00785, + "64": 0.00859, + "128": 0.0089 + }, + "i": { + "16": 0.00946, + "32": 0.01067, + "64": 0.0121, + "128": 0.01265 + }, + "j": { + "16": 0.00745, + "32": 0.00845, + "64": 0.00903, + "128": 0.00928 + }, + "k": { + "16": 0.00784, + "32": 0.00848, + "64": 0.00946, + "128": 0.00982 + }, + "l": { + "16": 0.0059, + "32": 0.00668, + "64": 0.00715, + "128": 0.00766 + }, + "m": { + "16": 0.0076, + "32": 0.00861, + "64": 0.00935, + "128": 0.00976 + }, + "n": { + "16": 0.00652, + "32": 0.00843, + "64": 0.00928, + "128": 0.00963 + }, + "o": { + "16": 0.00932, + "32": 0.0101, + "64": 0.01067, + "128": 0.01095 + }, + "p": { + "16": 0.0094, + "32": 0.01039, + "64": 0.01157, + "128": 0.01188 + }, + "q": { + "16": 0.00962, + "32": 0.01074, + "64": 0.01188, + "128": 0.01205 + }, + "r": { + "16": 0.01014, + "32": 0.01028, + "64": 0.01064, + "128": 0.01086 + }, + "s": { + "16": 0.01221, + "32": 0.01642, + "64": 0.01792, + "128": 0.01828 + }, + "t": { + "16": 0.00695, + "32": 0.00737, + "64": 0.00911, + "128": 0.00953 + }, + "u": { + "16": 0.00754, + "32": 0.00951, + "64": 0.01007, + "128": 0.01052 + }, + "v": { + "16": 0.00953, + "32": 0.01107, + "64": 0.01183, + "128": 0.01218 + }, + "w": { + "16": 0.01046, + "32": 0.0117, + "64": 0.0127, + "128": 0.0132 + }, + "x": { + "16": 0.01069, + "32": 0.01444, + "64": 0.01665, + "128": 0.01721 + }, + "y": { + "16": 0.00762, + "32": 0.00887, + "64": 0.0093, + "128": 0.00976 + }, + "z": { + "16": 0.011, + "32": 0.01361, + "64": 0.01643, + "128": 0.0171 + }, + "braceleft": { + "16": 0.01234, + "32": 0.01364, + "64": 0.01497, + "128": 0.01572 + }, + "bar": { + "16": 0.01576, + "32": 0.01869, + "64": 0.01902, + "128": 0.01903 + }, + "braceright": { + "16": 0.01163, + "32": 0.01459, + "64": 0.01575, + "128": 0.01642 + }, + "asciitilde": { + "16": 0.01755, + "32": 0.02195, + "64": 0.02279, + "128": 0.02353 + }, + "quoteleft": { + "16": 0.00803, + "32": 0.01134, + "64": 0.01414, + "128": 0.01465 + }, + "quoteright": { + "16": 0.00827, + "32": 0.00941, + "64": 0.01161, + "128": 0.01215 + }, + "quotedblleft": { + "16": 0.00819, + "32": 0.01147, + "64": 0.01355, + "128": 0.01408 + }, + "quotedblright": { + "16": 0.00658, + "32": 0.00946, + "64": 0.01141, + "128": 0.0121 + }, + "quotesinglbase": { + "16": 0.00778, + "32": 0.00972, + "64": 0.01158, + "128": 0.01223 + }, + "quotedblbase": { + "16": 0.00817, + "32": 0.01027, + "64": 0.01213, + "128": 0.01265 + }, + "guilsinglleft": { + "16": 0.01949, + "32": 0.02083, + "64": 0.02244, + "128": 0.0236 + }, + "guilsinglright": { + "16": 0.01583, + "32": 0.01939, + "64": 0.02156, + "128": 0.02271 + }, + "guillemotleft": { + "16": 0.01754, + "32": 0.01973, + "64": 0.02124, + "128": 0.02196 + }, + "guillemotright": { + "16": 0.01607, + "32": 0.01815, + "64": 0.01979, + "128": 0.02041 + }, + "ellipsis": { + "16": 0.01415, + "32": 0.02432, + "64": 0.02571, + "128": 0.02608 + }, + "bullet": { + "16": 0.00942, + "32": 0.01091, + "64": 0.0112, + "128": 0.01148 + }, + "periodcentered": { + "16": 0.00688, + "32": 0.01495, + "64": 0.01699, + "128": 0.01841 + }, + "Euro": { + "16": 0.01159, + "32": 0.01387, + "64": 0.01566, + "128": 0.0164 + }, + "copyright": { + "16": 0.01096, + "32": 0.01405, + "64": 0.01572, + "128": 0.01623 + }, + "registered": { + "16": 0.01039, + "32": 0.01336, + "64": 0.01493, + "128": 0.01539 + }, + "degree": { + "16": 0.01367, + "32": 0.01557, + "64": 0.01691, + "128": 0.01752 + }, + "emdash": { + "16": 0.00069, + "32": 0.00069, + "64": 0.00075, + "128": 0.0008 + }, + "endash": { + "16": 0.00108, + "32": 0.00118, + "64": 0.00118, + "128": 0.00129 + }, + "exclamdown": { + "16": 0.00707, + "32": 0.01108, + "64": 0.01238, + "128": 0.01305 + }, + "questiondown": { + "16": 0.01264, + "32": 0.01587, + "64": 0.01727, + "128": 0.01771 + }, + "section": { + "16": 0.01237, + "32": 0.01416, + "64": 0.01517, + "128": 0.01563 + }, + "paragraph": { + "16": 0.00336, + "32": 0.00361, + "64": 0.00383, + "128": 0.00392 + }, + "cent": { + "16": 0.01241, + "32": 0.01482, + "64": 0.01558, + "128": 0.01607 + }, + "sterling": { + "16": 0.01102, + "32": 0.01222, + "64": 0.01292, + "128": 0.01341 + }, + "yen": { + "16": 0.00882, + "32": 0.01091, + "64": 0.01489, + "128": 0.0155 + }, + "brokenbar": { + "16": 0.01614, + "32": 0.01975, + "64": 0.02033, + "128": 0.0205 + }, + "logicalnot": { + "16": 0.01814, + "32": 0.02159, + "64": 0.02199, + "128": 0.02222 + }, + "dieresis": { + "16": 0.01427, + "32": 0.01997, + "64": 0.02223, + "128": 0.02424 + }, + "acute": { + "16": 0.00588, + "32": 0.0118, + "64": 0.01388, + "128": 0.01513 + }, + "macron": { + "16": 0.0002, + "32": 0.00027, + "64": 0.00127, + "128": 0.0016 + }, + "cedilla": { + "16": 0.01007, + "32": 0.01397, + "64": 0.01985, + "128": 0.02054 + }, + "circumflex": { + "16": 0.01506, + "32": 0.01722, + "64": 0.02135, + "128": 0.02251 + }, + "tilde": { + "16": 0.01064, + "32": 0.01794, + "64": 0.02127, + "128": 0.02294 + }, + "ring": { + "16": 0.01266, + "32": 0.01599, + "64": 0.01739, + "128": 0.0184 + }, + "caron": { + "16": 0.01466, + "32": 0.02045, + "64": 0.02381, + "128": 0.02454 + }, + "dotaccent": { + "16": 0.01318, + "32": 0.01864, + "64": 0.02075, + "128": 0.02134 + }, + "AE": { + "16": 0.00994, + "32": 0.01083, + "64": 0.01139, + "128": 0.01177 + }, + "Eth": { + "16": 0.00726, + "32": 0.00855, + "64": 0.00932, + "128": 0.00956 + }, + "Oslash": { + "16": 0.0085, + "32": 0.01063, + "64": 0.01141, + "128": 0.0117 + }, + "Thorn": { + "16": 0.00693, + "32": 0.00833, + "64": 0.00904, + "128": 0.00919 + }, + "germandbls": { + "16": 0.00977, + "32": 0.01094, + "64": 0.01196, + "128": 0.0124 + }, + "ae": { + "16": 0.00719, + "32": 0.00994, + "64": 0.01188, + "128": 0.01232 + }, + "eth": { + "16": 0.00838, + "32": 0.00955, + "64": 0.01, + "128": 0.01033 + }, + "oslash": { + "16": 0.00736, + "32": 0.00814, + "64": 0.0088, + "128": 0.00917 + }, + "thorn": { + "16": 0.00628, + "32": 0.00726, + "64": 0.00828, + "128": 0.00849 + }, + "OE": { + "16": 0.01005, + "32": 0.01122, + "64": 0.01195, + "128": 0.01216 + }, + "oe": { + "16": 0.00986, + "32": 0.01159, + "64": 0.0127, + "128": 0.01313 + }, + "dotlessi": { + "16": 0.00771, + "32": 0.00891, + "64": 0.00959, + "128": 0.01027 + }, + "dotlessj": { + "16": 0.00863, + "32": 0.01028, + "64": 0.01117, + "128": 0.01144 + }, + "f_f": { + "16": 0.0071, + "32": 0.00815, + "64": 0.00894, + "128": 0.00918 + }, + "fi": { + "16": 0.00834, + "32": 0.00915, + "64": 0.01031, + "128": 0.01061 + }, + "fl": { + "16": 0.00827, + "32": 0.00889, + "64": 0.00948, + "128": 0.00987 + }, + "f_f_i": { + "16": 0.00708, + "32": 0.0082, + "64": 0.00914, + "128": 0.00938 + }, + "f_f_l": { + "16": 0.00746, + "32": 0.00856, + "64": 0.0092, + "128": 0.00952 + }, + "Gamma": { + "16": 0.00848, + "32": 0.00958, + "64": 0.01004, + "128": 0.01009 + }, + "Delta": { + "16": 0.01562, + "32": 0.01719, + "64": 0.0178, + "128": 0.01805 + }, + "Theta": { + "16": 0.01058, + "32": 0.0121, + "64": 0.01266, + "128": 0.01296 + }, + "Lambda": { + "16": 0.00906, + "32": 0.01162, + "64": 0.01314, + "128": 0.01357 + }, + "Xi": { + "16": 0.02245, + "32": 0.02465, + "64": 0.02561, + "128": 0.02603 + }, + "Pi": { + "16": 0.00694, + "32": 0.00742, + "64": 0.00762, + "128": 0.00777 + }, + "Sigma": { + "16": 0.01206, + "32": 0.01276, + "64": 0.01303, + "128": 0.01322 + }, + "Phi": { + "16": 0.00819, + "32": 0.01005, + "64": 0.01046, + "128": 0.01083 + }, + "Psi": { + "16": 0.00803, + "32": 0.00992, + "64": 0.01064, + "128": 0.01097 + }, + "Omega": { + "16": 0.00842, + "32": 0.01003, + "64": 0.0108, + "128": 0.01109 + }, + "alpha": { + "16": 0.01238, + "32": 0.01436, + "64": 0.01596, + "128": 0.01648 + }, + "beta": { + "16": 0.00724, + "32": 0.00875, + "64": 0.00956, + "128": 0.00969 + }, + "gamma": { + "16": 0.00882, + "32": 0.0101, + "64": 0.01084, + "128": 0.01124 + }, + "delta": { + "16": 0.00829, + "32": 0.00904, + "64": 0.00981, + "128": 0.01024 + }, + "epsilon": { + "16": 0.01539, + "32": 0.01625, + "64": 0.01737, + "128": 0.01854 + }, + "zeta": { + "16": 0.00924, + "32": 0.01192, + "64": 0.01367, + "128": 0.01411 + }, + "eta": { + "16": 0.00401, + "32": 0.00444, + "64": 0.00507, + "128": 0.00541 + }, + "theta": { + "16": 0.00592, + "32": 0.00695, + "64": 0.00727, + "128": 0.0076 + }, + "iota": { + "16": 0.00508, + "32": 0.00617, + "64": 0.007, + "128": 0.00726 + }, + "kappa": { + "16": 0.00716, + "32": 0.0099, + "64": 0.01108, + "128": 0.01164 + }, + "lambda": { + "16": 0.01123, + "32": 0.01217, + "64": 0.01325, + "128": 0.01349 + }, + "mu": { + "16": 0.00662, + "32": 0.00705, + "64": 0.00775, + "128": 0.00814 + }, + "nu": { + "16": 0.00839, + "32": 0.01038, + "64": 0.01143, + "128": 0.01233 + }, + "xi": { + "16": 0.01043, + "32": 0.01214, + "64": 0.01373, + "128": 0.01472 + }, + "pi": { + "16": 0.00702, + "32": 0.00921, + "64": 0.00971, + "128": 0.01012 + }, + "rho": { + "16": 0.00778, + "32": 0.0094, + "64": 0.00958, + "128": 0.00994 + }, + "sigmafinal": { + "16": 0.01186, + "32": 0.01371, + "64": 0.0159, + "128": 0.01654 + }, + "sigma": { + "16": 0.00629, + "32": 0.00724, + "64": 0.00799, + "128": 0.00847 + }, + "tau": { + "16": 0.00939, + "32": 0.01019, + "64": 0.01139, + "128": 0.01179 + }, + "upsilon": { + "16": 0.00853, + "32": 0.01014, + "64": 0.01074, + "128": 0.01114 + }, + "phi": { + "16": 0.00729, + "32": 0.00815, + "64": 0.00839, + "128": 0.00862 + }, + "chi": { + "16": 0.00537, + "32": 0.00546, + "64": 0.00596, + "128": 0.00627 + }, + "psi": { + "16": 0.00573, + "32": 0.00633, + "64": 0.00704, + "128": 0.00725 + }, + "omega": { + "16": 0.00873, + "32": 0.01119, + "64": 0.01218, + "128": 0.01265 + }, + "uni20B9": { + "16": 0.01492, + "32": 0.01656, + "64": 0.01752, + "128": 0.01796 + }, + "minus": { + "16": 0.02192, + "32": 0.02501, + "64": 0.02549, + "128": 0.02579 + }, + "multiply": { + "16": 0.00922, + "32": 0.01151, + "64": 0.01192, + "128": 0.01235 + }, + "divide": { + "16": 0.00375, + "32": 0.00526, + "64": 0.0055, + "128": 0.00577 + }, + "plusminus": { + "16": 0.01111, + "32": 0.01265, + "64": 0.01331, + "128": 0.01362 + }, + "notequal": { + "16": 0.00713, + "32": 0.00778, + "64": 0.0081, + "128": 0.00834 + }, + "lessequal": { + "16": 0.01527, + "32": 0.01641, + "64": 0.01682, + "128": 0.01703 + }, + "greaterequal": { + "16": 0.01378, + "32": 0.01521, + "64": 0.01566, + "128": 0.01601 + }, + "summation": { + "16": 0.01969, + "32": 0.0203, + "64": 0.021, + "128": 0.02106 + }, + "product": { + "16": 0.00886, + "32": 0.00942, + "64": 0.00961, + "128": 0.00966 + }, + "integral": { + "16": 0.00803, + "32": 0.00882, + "64": 0.00937, + "128": 0.00949 + }, + "radical": { + "16": 0.01249, + "32": 0.01393, + "64": 0.01631, + "128": 0.01665 + }, + "infinity": { + "16": 0.01115, + "32": 0.01225, + "64": 0.01309, + "128": 0.01343 + }, + "partialdiff": { + "16": 0.00728, + "32": 0.00898, + "64": 0.0101, + "128": 0.01046 + }, + "nabla": { + "16": 0.00883, + "32": 0.00965, + "64": 0.0108, + "128": 0.01093 + }, + "minute": { + "16": 0.01013, + "32": 0.01364, + "64": 0.01424, + "128": 0.01495 + }, + "second": { + "16": 0.01057, + "32": 0.01281, + "64": 0.01381, + "128": 0.01448 + }, + "ringoperator": { + "16": 0.01325, + "32": 0.01629, + "64": 0.01868, + "128": 0.01946 + }, + "arrowright": { + "16": 0.00996, + "32": 0.01176, + "64": 0.01286, + "128": 0.0132 + }, + "arrowleft": { + "16": 0.01017, + "32": 0.01162, + "64": 0.01236, + "128": 0.01289 + }, + "arrowboth": { + "16": 0.01034, + "32": 0.0111, + "64": 0.01193, + "128": 0.01251 + }, + "element": { + "16": 0.01911, + "32": 0.0208, + "64": 0.02159, + "128": 0.02194 + }, + "notelement": { + "16": 0.00663, + "32": 0.00864, + "64": 0.0091, + "128": 0.00928 + }, + "propersubset": { + "16": 0.01627, + "32": 0.0177, + "64": 0.01859, + "128": 0.01892 + }, + "reflexsubset": { + "16": 0.01246, + "32": 0.01351, + "64": 0.01421, + "128": 0.01445 + }, + "union": { + "16": 0.01431, + "32": 0.01507, + "64": 0.01529, + "128": 0.01556 + }, + "intersection": { + "16": 0.00971, + "32": 0.01224, + "64": 0.01301, + "128": 0.01332 + }, + "universal": { + "16": 0.01211, + "32": 0.01259, + "64": 0.013, + "128": 0.01322 + }, + "existential": { + "16": 0.00882, + "32": 0.00901, + "64": 0.00904, + "128": 0.00919 + }, + "emptyset": { + "16": 0.00683, + "32": 0.00766, + "64": 0.00812, + "128": 0.00826 + }, + "approxequal": { + "16": 0.01542, + "32": 0.01884, + "64": 0.0196, + "128": 0.02068 + }, + "equivalence": { + "16": 0.02158, + "32": 0.02436, + "64": 0.02526, + "128": 0.0256 + }, + "proportional": { + "16": 0.01137, + "32": 0.0135, + "64": 0.01418, + "128": 0.0145 + }, + "fraction": { + "16": 0.00528, + "32": 0.00535, + "64": 0.00571, + "128": 0.00598 + }, + "onesuperior": { + "16": 0.00663, + "32": 0.00864, + "64": 0.0097, + "128": 0.00992 + }, + "twosuperior": { + "16": 0.01445, + "32": 0.01659, + "64": 0.01814, + "128": 0.01916 + }, + "threesuperior": { + "16": 0.01393, + "32": 0.01673, + "64": 0.01891, + "128": 0.0205 + }, + "ordfeminine": { + "16": 0.0114, + "32": 0.01338, + "64": 0.01483, + "128": 0.01552 + }, + "ordmasculine": { + "16": 0.00951, + "32": 0.01142, + "64": 0.01302, + "128": 0.01345 + }, + "dottedcircle": { + "16": 0.00999, + "32": 0.01703, + "64": 0.02328, + "128": 0.02692 + }, + "gravecomb": { + "16": 0.00799, + "32": 0.01168, + "64": 0.0136, + "128": 0.01489 + }, + "gravecomb.case": { + "16": 0.01193, + "32": 0.01738, + "64": 0.02133, + "128": 0.02277 + }, + "acutecomb": { + "16": 0.00682, + "32": 0.01024, + "64": 0.01287, + "128": 0.01412 + }, + "acutecomb.case": { + "16": 0.00922, + "32": 0.017, + "64": 0.0192, + "128": 0.02169 + }, + "circumflexcomb": { + "16": 0.01366, + "32": 0.01617, + "64": 0.02068, + "128": 0.02184 + }, + "circumflexcomb.case": { + "16": 0.01568, + "32": 0.02099, + "64": 0.02371, + "128": 0.02496 + }, + "tildecomb": { + "16": 0.00938, + "32": 0.01655, + "64": 0.02014, + "128": 0.02151 + }, + "tildecomb.case": { + "16": 0.01677, + "32": 0.02358, + "64": 0.02843, + "128": 0.03059 + }, + "macroncomb": { + "16": 0.0002, + "32": 0.00027, + "64": 0.00127, + "128": 0.0016 + }, + "dieresiscomb": { + "16": 0.01552, + "32": 0.022, + "64": 0.02524, + "128": 0.02669 + }, + "dieresiscomb.case": { + "16": 0.00649, + "32": 0.02057, + "64": 0.0243, + "128": 0.02469 + }, + "ringcomb": { + "16": 0.01117, + "32": 0.01254, + "64": 0.01395, + "128": 0.01461 + }, + "ringcomb.case": { + "16": 0.01141, + "32": 0.01523, + "64": 0.02077, + "128": 0.02214 + }, + "caroncomb": { + "16": 0.01444, + "32": 0.02154, + "64": 0.02331, + "128": 0.02351 + }, + "dotaccentcomb": { + "16": 0.017, + "32": 0.01719, + "64": 0.02451, + "128": 0.02503 + }, + "cedillacomb": { + "16": 0.01117, + "32": 0.01523, + "64": 0.02053, + "128": 0.02113 + }, + "zero.lf": { + "16": 0.00642, + "32": 0.00731, + "64": 0.00766, + "128": 0.0078 + }, + "one.lf": { + "16": 0.00971, + "32": 0.00993, + "64": 0.01021, + "128": 0.01041 + }, + "two.lf": { + "16": 0.00853, + "32": 0.01103, + "64": 0.01212, + "128": 0.01241 + }, + "three.lf": { + "16": 0.01218, + "32": 0.01464, + "64": 0.01613, + "128": 0.0165 + }, + "four.lf": { + "16": 0.01166, + "32": 0.01334, + "64": 0.01415, + "128": 0.01439 + }, + "five.lf": { + "16": 0.00922, + "32": 0.0121, + "64": 0.01314, + "128": 0.01346 + }, + "six.lf": { + "16": 0.00819, + "32": 0.0099, + "64": 0.01072, + "128": 0.01095 + }, + "seven.lf": { + "16": 0.00835, + "32": 0.00928, + "64": 0.0097, + "128": 0.00999 + }, + "eight.lf": { + "16": 0.00757, + "32": 0.00912, + "64": 0.00965, + "128": 0.00997 + }, + "nine.lf": { + "16": 0.0082, + "32": 0.00933, + "64": 0.01003, + "128": 0.01043 + }, + "zero.osf": { + "16": 0.00801, + "32": 0.00939, + "64": 0.01003, + "128": 0.01015 + }, + "one.osf": { + "16": 0.00829, + "32": 0.00947, + "64": 0.01023, + "128": 0.01036 + }, + "two.osf": { + "16": 0.00953, + "32": 0.01214, + "64": 0.01326, + "128": 0.01366 + }, + "three.osf": { + "16": 0.01366, + "32": 0.01779, + "64": 0.01906, + "128": 0.0196 + }, + "four.osf": { + "16": 0.00693, + "32": 0.00881, + "64": 0.00948, + "128": 0.00984 + }, + "five.osf": { + "16": 0.01035, + "32": 0.01275, + "64": 0.01355, + "128": 0.01405 + }, + "six.osf": { + "16": 0.00928, + "32": 0.01055, + "64": 0.01123, + "128": 0.01148 + }, + "seven.osf": { + "16": 0.00797, + "32": 0.00972, + "64": 0.01084, + "128": 0.01129 + }, + "eight.osf": { + "16": 0.0099, + "32": 0.01094, + "64": 0.01195, + "128": 0.01216 + }, + "nine.osf": { + "16": 0.00979, + "32": 0.01095, + "64": 0.01191, + "128": 0.01219 + }, + "zero.tosf": { + "16": 0.00621, + "32": 0.0076, + "64": 0.00823, + "128": 0.00846 + }, + "one.tosf": { + "16": 0.00821, + "32": 0.00945, + "64": 0.01021, + "128": 0.01036 + }, + "two.tosf": { + "16": 0.00834, + "32": 0.01098, + "64": 0.01199, + "128": 0.01245 + }, + "three.tosf": { + "16": 0.01424, + "32": 0.01661, + "64": 0.01799, + "128": 0.01858 + }, + "four.tosf": { + "16": 0.00896, + "32": 0.01093, + "64": 0.01242, + "128": 0.01288 + }, + "five.tosf": { + "16": 0.01155, + "32": 0.013, + "64": 0.01379, + "128": 0.01419 + }, + "six.tosf": { + "16": 0.00775, + "32": 0.00975, + "64": 0.01057, + "128": 0.01084 + }, + "seven.tosf": { + "16": 0.00853, + "32": 0.01112, + "64": 0.01206, + "128": 0.01222 + }, + "eight.tosf": { + "16": 0.00921, + "32": 0.01116, + "64": 0.01193, + "128": 0.01227 + }, + "nine.tosf": { + "16": 0.00832, + "32": 0.01014, + "64": 0.01101, + "128": 0.01144 + }, + "two.dnom": { + "16": 0.01445, + "32": 0.01659, + "64": 0.01814, + "128": 0.01916 + }, + "four.dnom": { + "16": 0.01127, + "32": 0.01533, + "64": 0.01684, + "128": 0.01783 + }, + "a.sc": { + "16": 0.00952, + "32": 0.01239, + "64": 0.01413, + "128": 0.01502 + }, + "b.sc": { + "16": 0.01154, + "32": 0.01318, + "64": 0.01427, + "128": 0.01483 + }, + "c.sc": { + "16": 0.01066, + "32": 0.01206, + "64": 0.01279, + "128": 0.01318 + }, + "d.sc": { + "16": 0.00907, + "32": 0.01066, + "64": 0.012, + "128": 0.01242 + }, + "e.sc": { + "16": 0.01265, + "32": 0.01347, + "64": 0.01401, + "128": 0.01474 + }, + "f.sc": { + "16": 0.01414, + "32": 0.01528, + "64": 0.01613, + "128": 0.01672 + }, + "g.sc": { + "16": 0.01216, + "32": 0.01271, + "64": 0.01384, + "128": 0.0143 + }, + "h.sc": { + "16": 0.01156, + "32": 0.01344, + "64": 0.01464, + "128": 0.01504 + }, + "i.sc": { + "16": 0.01094, + "32": 0.01213, + "64": 0.01372, + "128": 0.01412 + }, + "j.sc": { + "16": 0.00893, + "32": 0.01007, + "64": 0.01073, + "128": 0.01101 + }, + "k.sc": { + "16": 0.01139, + "32": 0.01387, + "64": 0.01586, + "128": 0.01642 + }, + "l.sc": { + "16": 0.01221, + "32": 0.01292, + "64": 0.01403, + "128": 0.01449 + }, + "m.sc": { + "16": 0.00859, + "32": 0.01009, + "64": 0.01095, + "128": 0.01139 + }, + "n.sc": { + "16": 0.00969, + "32": 0.01218, + "64": 0.01305, + "128": 0.01357 + }, + "o.sc": { + "16": 0.00916, + "32": 0.0104, + "64": 0.01124, + "128": 0.01159 + }, + "p.sc": { + "16": 0.01025, + "32": 0.01162, + "64": 0.01314, + "128": 0.01347 + }, + "q.sc": { + "16": 0.01056, + "32": 0.01254, + "64": 0.0135, + "128": 0.0139 + }, + "r.sc": { + "16": 0.01146, + "32": 0.01384, + "64": 0.01506, + "128": 0.01551 + }, + "s.sc": { + "16": 0.01236, + "32": 0.01482, + "64": 0.0158, + "128": 0.01629 + }, + "t.sc": { + "16": 0.00894, + "32": 0.01048, + "64": 0.01069, + "128": 0.01094 + }, + "u.sc": { + "16": 0.01226, + "32": 0.01486, + "64": 0.01722, + "128": 0.01774 + }, + "v.sc": { + "16": 0.00981, + "32": 0.01334, + "64": 0.01459, + "128": 0.0149 + }, + "w.sc": { + "16": 0.00698, + "32": 0.00899, + "64": 0.01005, + "128": 0.01052 + }, + "x.sc": { + "16": 0.01014, + "32": 0.01239, + "64": 0.01503, + "128": 0.01565 + }, + "y.sc": { + "16": 0.01079, + "32": 0.01285, + "64": 0.01491, + "128": 0.01533 + }, + "z.sc": { + "16": 0.01372, + "32": 0.01417, + "64": 0.01441, + "128": 0.01455 + }, + "ae.sc": { + "16": 0.00932, + "32": 0.01326, + "64": 0.01478, + "128": 0.01515 + }, + "oe.sc": { + "16": 0.01021, + "32": 0.01231, + "64": 0.01289, + "128": 0.01328 + }, + "oslash.sc": { + "16": 0.0094, + "32": 0.01163, + "64": 0.01231, + "128": 0.01265 + }, + "eth.sc": { + "16": 0.00675, + "32": 0.0077, + "64": 0.00896, + "128": 0.0093 + }, + "thorn.sc": { + "16": 0.01121, + "32": 0.01367, + "64": 0.01634, + "128": 0.01684 + }, + "gamma.sc": { + "16": 0.01599, + "32": 0.01681, + "64": 0.01763, + "128": 0.01814 + }, + "delta.sc": { + "16": 0.00613, + "32": 0.00673, + "64": 0.00747, + "128": 0.0076 + }, + "theta.sc": { + "16": 0.00733, + "32": 0.00956, + "64": 0.01056, + "128": 0.01089 + }, + "lambda.sc": { + "16": 0.00842, + "32": 0.00992, + "64": 0.01049, + "128": 0.01069 + }, + "xi.sc": { + "16": 0.01557, + "32": 0.01701, + "64": 0.01751, + "128": 0.01784 + }, + "pi.sc": { + "16": 0.01274, + "32": 0.01419, + "64": 0.01471, + "128": 0.01487 + }, + "sigma.sc": { + "16": 0.01143, + "32": 0.01302, + "64": 0.01325, + "128": 0.01355 + }, + "phi.sc": { + "16": 0.01171, + "32": 0.01473, + "64": 0.01553, + "128": 0.01586 + }, + "psi.sc": { + "16": 0.00858, + "32": 0.01204, + "64": 0.0136, + "128": 0.01423 + }, + "omega.sc": { + "16": 0.00981, + "32": 0.01142, + "64": 0.01209, + "128": 0.01251 + } + } + }, + "Bold": { + "font": "MachineMadeWarp-Bold.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.01679, + "max_glyph": "equivalence", + "mean": 0.0077, + "p95": 0.01357, + "pass": true + }, + "32": { + "max": 0.0208, + "max_glyph": "tildecomb", + "mean": 0.00891, + "p95": 0.01591, + "pass": true + }, + "64": { + "max": 0.02577, + "max_glyph": "dottedcircle", + "mean": 0.0095, + "p95": 0.01675, + "pass": true + }, + "128": { + "max": 0.02786, + "max_glyph": "dottedcircle", + "mean": 0.00982, + "p95": 0.01693, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.00628, + "32": 0.00663, + "64": 0.0074, + "128": 0.00769 + }, + "quotedbl": { + "16": 0.00991, + "32": 0.01192, + "64": 0.01289, + "128": 0.01321 + }, + "numbersign": { + "16": 0.00579, + "32": 0.00617, + "64": 0.00639, + "128": 0.00647 + }, + "dollar": { + "16": 0.01006, + "32": 0.01179, + "64": 0.01279, + "128": 0.01311 + }, + "percent": { + "16": 0.00686, + "32": 0.00709, + "64": 0.00733, + "128": 0.00752 + }, + "ampersand": { + "16": 0.00638, + "32": 0.00749, + "64": 0.00823, + "128": 0.00847 + }, + "quotesingle": { + "16": 0.01098, + "32": 0.01185, + "64": 0.01301, + "128": 0.0133 + }, + "parenleft": { + "16": 0.00505, + "32": 0.00598, + "64": 0.00652, + "128": 0.00679 + }, + "parenright": { + "16": 0.0051, + "32": 0.00566, + "64": 0.00621, + "128": 0.00638 + }, + "asterisk": { + "16": 0.00767, + "32": 0.01085, + "64": 0.01192, + "128": 0.01265 + }, + "plus": { + "16": 0.01445, + "32": 0.01481, + "64": 0.01535, + "128": 0.0155 + }, + "comma": { + "16": 0.00628, + "32": 0.00684, + "64": 0.00734, + "128": 0.00781 + }, + "hyphen": { + "16": 0.00715, + "32": 0.0094, + "64": 0.01051, + "128": 0.01051 + }, + "period": { + "16": 0.0092, + "32": 0.01026, + "64": 0.01065, + "128": 0.01075 + }, + "slash": { + "16": 0.00897, + "32": 0.00937, + "64": 0.00964, + "128": 0.00971 + }, + "zero": { + "16": 0.00579, + "32": 0.00607, + "64": 0.0062, + "128": 0.00629 + }, + "one": { + "16": 0.00164, + "32": 0.00165, + "64": 0.00187, + "128": 0.00209 + }, + "two": { + "16": 0.00705, + "32": 0.0081, + "64": 0.0085, + "128": 0.00872 + }, + "three": { + "16": 0.00724, + "32": 0.0086, + "64": 0.00946, + "128": 0.00983 + }, + "four": { + "16": 0.0066, + "32": 0.00748, + "64": 0.00783, + "128": 0.00813 + }, + "five": { + "16": 0.00665, + "32": 0.00759, + "64": 0.00825, + "128": 0.00845 + }, + "six": { + "16": 0.0065, + "32": 0.00747, + "64": 0.00768, + "128": 0.0078 + }, + "seven": { + "16": 0.00497, + "32": 0.00557, + "64": 0.0059, + "128": 0.0061 + }, + "eight": { + "16": 0.00553, + "32": 0.00635, + "64": 0.0067, + "128": 0.00691 + }, + "nine": { + "16": 0.00641, + "32": 0.00728, + "64": 0.00756, + "128": 0.00774 + }, + "colon": { + "16": 0.00652, + "32": 0.00812, + "64": 0.00846, + "128": 0.00863 + }, + "semicolon": { + "16": 0.00542, + "32": 0.00682, + "64": 0.00749, + "128": 0.00779 + }, + "less": { + "16": 0.01083, + "32": 0.01153, + "64": 0.01172, + "128": 0.01186 + }, + "equal": { + "16": 0.01669, + "32": 0.01671, + "64": 0.01688, + "128": 0.01693 + }, + "greater": { + "16": 0.01369, + "32": 0.01408, + "64": 0.01465, + "128": 0.01491 + }, + "question": { + "16": 0.00672, + "32": 0.00812, + "64": 0.00869, + "128": 0.00895 + }, + "at": { + "16": 0.0065, + "32": 0.00765, + "64": 0.00835, + "128": 0.00861 + }, + "A": { + "16": 0.00665, + "32": 0.00708, + "64": 0.00754, + "128": 0.00767 + }, + "B": { + "16": 0.00663, + "32": 0.00722, + "64": 0.0077, + "128": 0.00799 + }, + "C": { + "16": 0.00768, + "32": 0.00882, + "64": 0.00962, + "128": 0.0099 + }, + "D": { + "16": 0.00608, + "32": 0.00681, + "64": 0.00692, + "128": 0.00726 + }, + "E": { + "16": 0.00682, + "32": 0.0079, + "64": 0.0085, + "128": 0.00879 + }, + "F": { + "16": 0.0064, + "32": 0.0067, + "64": 0.00727, + "128": 0.00775 + }, + "G": { + "16": 0.00741, + "32": 0.00854, + "64": 0.00914, + "128": 0.00946 + }, + "H": { + "16": 0.00753, + "32": 0.00779, + "64": 0.00787, + "128": 0.00849 + }, + "I": { + "16": 0.00697, + "32": 0.00715, + "64": 0.00728, + "128": 0.00794 + }, + "J": { + "16": 0.00685, + "32": 0.00708, + "64": 0.00742, + "128": 0.00783 + }, + "K": { + "16": 0.00541, + "32": 0.00592, + "64": 0.00648, + "128": 0.00702 + }, + "L": { + "16": 0.00523, + "32": 0.00563, + "64": 0.00581, + "128": 0.00634 + }, + "M": { + "16": 0.00899, + "32": 0.00942, + "64": 0.00977, + "128": 0.01022 + }, + "N": { + "16": 0.00983, + "32": 0.0104, + "64": 0.01075, + "128": 0.01124 + }, + "O": { + "16": 0.00538, + "32": 0.0067, + "64": 0.00695, + "128": 0.00709 + }, + "P": { + "16": 0.00659, + "32": 0.00691, + "64": 0.00733, + "128": 0.00775 + }, + "Q": { + "16": 0.0051, + "32": 0.00607, + "64": 0.0064, + "128": 0.00662 + }, + "R": { + "16": 0.00716, + "32": 0.00783, + "64": 0.00845, + "128": 0.00891 + }, + "S": { + "16": 0.00808, + "32": 0.00986, + "64": 0.01092, + "128": 0.01124 + }, + "T": { + "16": 0.00698, + "32": 0.00743, + "64": 0.00803, + "128": 0.00851 + }, + "U": { + "16": 0.0072, + "32": 0.00856, + "64": 0.00898, + "128": 0.00962 + }, + "V": { + "16": 0.012, + "32": 0.01226, + "64": 0.01268, + "128": 0.01308 + }, + "W": { + "16": 0.00795, + "32": 0.00921, + "64": 0.01032, + "128": 0.01068 + }, + "X": { + "16": 0.00561, + "32": 0.00627, + "64": 0.00677, + "128": 0.0074 + }, + "Y": { + "16": 0.0055, + "32": 0.00619, + "64": 0.00667, + "128": 0.00726 + }, + "Z": { + "16": 0.0068, + "32": 0.00796, + "64": 0.00826, + "128": 0.00843 + }, + "bracketleft": { + "16": 0.0066, + "32": 0.00702, + "64": 0.00709, + "128": 0.00723 + }, + "backslash": { + "16": 0.00896, + "32": 0.00932, + "64": 0.00951, + "128": 0.00977 + }, + "bracketright": { + "16": 0.00383, + "32": 0.00383, + "64": 0.00385, + "128": 0.00393 + }, + "asciicircum": { + "16": 0.00687, + "32": 0.00748, + "64": 0.00816, + "128": 0.00843 + }, + "underscore": { + "16": 0.0145, + "32": 0.01473, + "64": 0.01503, + "128": 0.01516 + }, + "grave": { + "16": 0.01191, + "32": 0.01428, + "64": 0.01476, + "128": 0.01528 + }, + "a": { + "16": 0.00661, + "32": 0.00778, + "64": 0.00865, + "128": 0.00885 + }, + "b": { + "16": 0.00766, + "32": 0.00832, + "64": 0.00863, + "128": 0.00892 + }, + "c": { + "16": 0.00716, + "32": 0.00822, + "64": 0.00887, + "128": 0.0091 + }, + "d": { + "16": 0.00591, + "32": 0.00677, + "64": 0.00715, + "128": 0.00732 + }, + "e": { + "16": 0.00693, + "32": 0.00758, + "64": 0.0081, + "128": 0.00833 + }, + "f": { + "16": 0.0049, + "32": 0.00505, + "64": 0.00576, + "128": 0.00587 + }, + "g": { + "16": 0.00683, + "32": 0.00814, + "64": 0.00923, + "128": 0.00952 + }, + "h": { + "16": 0.00718, + "32": 0.00736, + "64": 0.00759, + "128": 0.00787 + }, + "i": { + "16": 0.00811, + "32": 0.00859, + "64": 0.009, + "128": 0.00927 + }, + "j": { + "16": 0.00391, + "32": 0.00425, + "64": 0.00439, + "128": 0.00455 + }, + "k": { + "16": 0.00666, + "32": 0.00824, + "64": 0.00864, + "128": 0.00905 + }, + "l": { + "16": 0.00725, + "32": 0.00726, + "64": 0.0075, + "128": 0.00773 + }, + "m": { + "16": 0.00796, + "32": 0.00862, + "64": 0.00908, + "128": 0.00929 + }, + "n": { + "16": 0.00753, + "32": 0.00787, + "64": 0.00828, + "128": 0.00855 + }, + "o": { + "16": 0.00577, + "32": 0.00641, + "64": 0.00685, + "128": 0.00697 + }, + "p": { + "16": 0.00782, + "32": 0.0088, + "64": 0.00913, + "128": 0.00931 + }, + "q": { + "16": 0.00591, + "32": 0.007, + "64": 0.00737, + "128": 0.00752 + }, + "r": { + "16": 0.0056, + "32": 0.00665, + "64": 0.00737, + "128": 0.00808 + }, + "s": { + "16": 0.00754, + "32": 0.00938, + "64": 0.01024, + "128": 0.0107 + }, + "t": { + "16": 0.00577, + "32": 0.0065, + "64": 0.00716, + "128": 0.00739 + }, + "u": { + "16": 0.00617, + "32": 0.00684, + "64": 0.00751, + "128": 0.00777 + }, + "v": { + "16": 0.00806, + "32": 0.00942, + "64": 0.01102, + "128": 0.01163 + }, + "w": { + "16": 0.00838, + "32": 0.00931, + "64": 0.01087, + "128": 0.01127 + }, + "x": { + "16": 0.00694, + "32": 0.00861, + "64": 0.01016, + "128": 0.01069 + }, + "y": { + "16": 0.00883, + "32": 0.0101, + "64": 0.01137, + "128": 0.01183 + }, + "z": { + "16": 0.00589, + "32": 0.00715, + "64": 0.00788, + "128": 0.00797 + }, + "braceleft": { + "16": 0.00696, + "32": 0.00775, + "64": 0.00827, + "128": 0.00844 + }, + "bar": { + "16": 0.00071, + "32": 0.00071, + "64": 0.00071, + "128": 0.00071 + }, + "braceright": { + "16": 0.00606, + "32": 0.00828, + "64": 0.00882, + "128": 0.00913 + }, + "asciitilde": { + "16": 0.01022, + "32": 0.01242, + "64": 0.01284, + "128": 0.0133 + }, + "quoteleft": { + "16": 0.00703, + "32": 0.00728, + "64": 0.00743, + "128": 0.00767 + }, + "quoteright": { + "16": 0.00634, + "32": 0.007, + "64": 0.00741, + "128": 0.00783 + }, + "quotedblleft": { + "16": 0.00637, + "32": 0.00677, + "64": 0.00718, + "128": 0.00753 + }, + "quotedblright": { + "16": 0.00627, + "32": 0.00688, + "64": 0.00727, + "128": 0.00759 + }, + "quotesinglbase": { + "16": 0.00628, + "32": 0.00684, + "64": 0.00734, + "128": 0.00781 + }, + "quotedblbase": { + "16": 0.00636, + "32": 0.00686, + "64": 0.00718, + "128": 0.00755 + }, + "guilsinglleft": { + "16": 0.01473, + "32": 0.01622, + "64": 0.01719, + "128": 0.01782 + }, + "guilsinglright": { + "16": 0.01484, + "32": 0.01601, + "64": 0.01731, + "128": 0.01788 + }, + "guillemotleft": { + "16": 0.01303, + "32": 0.01635, + "64": 0.01728, + "128": 0.01809 + }, + "guillemotright": { + "16": 0.01226, + "32": 0.0146, + "64": 0.01603, + "128": 0.01684 + }, + "ellipsis": { + "16": 0.00901, + "32": 0.00977, + "64": 0.01007, + "128": 0.01021 + }, + "bullet": { + "16": 0.00378, + "32": 0.004, + "64": 0.00413, + "128": 0.00419 + }, + "periodcentered": { + "16": 0.01376, + "32": 0.01917, + "64": 0.01955, + "128": 0.0199 + }, + "Euro": { + "16": 0.00984, + "32": 0.01173, + "64": 0.01238, + "128": 0.01268 + }, + "copyright": { + "16": 0.01011, + "32": 0.01181, + "64": 0.01262, + "128": 0.01297 + }, + "registered": { + "16": 0.01049, + "32": 0.01172, + "64": 0.01256, + "128": 0.01282 + }, + "degree": { + "16": 0.00499, + "32": 0.00708, + "64": 0.00843, + "128": 0.00887 + }, + "emdash": { + "16": 0.01519, + "32": 0.01519, + "64": 0.01528, + "128": 0.01535 + }, + "endash": { + "16": 0.01488, + "32": 0.01511, + "64": 0.01529, + "128": 0.01562 + }, + "exclamdown": { + "16": 0.00541, + "32": 0.00617, + "64": 0.00664, + "128": 0.00697 + }, + "questiondown": { + "16": 0.00747, + "32": 0.00849, + "64": 0.00901, + "128": 0.00918 + }, + "section": { + "16": 0.00674, + "32": 0.00829, + "64": 0.00944, + "128": 0.00979 + }, + "paragraph": { + "16": 0.00601, + "32": 0.00622, + "64": 0.00644, + "128": 0.00647 + }, + "cent": { + "16": 0.00894, + "32": 0.01016, + "64": 0.0108, + "128": 0.01116 + }, + "sterling": { + "16": 0.0073, + "32": 0.00886, + "64": 0.00959, + "128": 0.0099 + }, + "yen": { + "16": 0.00536, + "32": 0.00585, + "64": 0.00657, + "128": 0.00722 + }, + "brokenbar": { + "16": 0.00182, + "32": 0.00182, + "64": 0.00182, + "128": 0.00182 + }, + "logicalnot": { + "16": 0.00974, + "32": 0.00989, + "64": 0.01033, + "128": 0.01054 + }, + "dieresis": { + "16": 0.01146, + "32": 0.01151, + "64": 0.01209, + "128": 0.01221 + }, + "acute": { + "16": 0.01296, + "32": 0.01681, + "64": 0.01756, + "128": 0.01781 + }, + "macron": { + "16": 0.01463, + "32": 0.01588, + "64": 0.01607, + "128": 0.01693 + }, + "cedilla": { + "16": 0.0074, + "32": 0.00923, + "64": 0.01056, + "128": 0.01174 + }, + "circumflex": { + "16": 0.00854, + "32": 0.01403, + "64": 0.01589, + "128": 0.01683 + }, + "tilde": { + "16": 0.01347, + "32": 0.02034, + "64": 0.02205, + "128": 0.02255 + }, + "ring": { + "16": 0.00703, + "32": 0.01001, + "64": 0.01049, + "128": 0.01097 + }, + "caron": { + "16": 0.00966, + "32": 0.01287, + "64": 0.01438, + "128": 0.01494 + }, + "dotaccent": { + "16": 0.0104, + "32": 0.01123, + "64": 0.01123, + "128": 0.01182 + }, + "AE": { + "16": 0.01099, + "32": 0.01175, + "64": 0.01201, + "128": 0.01227 + }, + "Eth": { + "16": 0.00713, + "32": 0.00791, + "64": 0.00822, + "128": 0.00839 + }, + "Oslash": { + "16": 0.00716, + "32": 0.00813, + "64": 0.00844, + "128": 0.00856 + }, + "Thorn": { + "16": 0.00787, + "32": 0.00826, + "64": 0.00855, + "128": 0.00904 + }, + "germandbls": { + "16": 0.00549, + "32": 0.00632, + "64": 0.00678, + "128": 0.00697 + }, + "ae": { + "16": 0.00715, + "32": 0.00889, + "64": 0.00986, + "128": 0.01019 + }, + "eth": { + "16": 0.00793, + "32": 0.00904, + "64": 0.00967, + "128": 0.00983 + }, + "oslash": { + "16": 0.00559, + "32": 0.00607, + "64": 0.00653, + "128": 0.00683 + }, + "thorn": { + "16": 0.0075, + "32": 0.00809, + "64": 0.00834, + "128": 0.00859 + }, + "OE": { + "16": 0.00679, + "32": 0.00812, + "64": 0.00858, + "128": 0.00873 + }, + "oe": { + "16": 0.00679, + "32": 0.00739, + "64": 0.00794, + "128": 0.0082 + }, + "dotlessi": { + "16": 0.00768, + "32": 0.00802, + "64": 0.00886, + "128": 0.00895 + }, + "dotlessj": { + "16": 0.0024, + "32": 0.00278, + "64": 0.00322, + "128": 0.00342 + }, + "f_f": { + "16": 0.00556, + "32": 0.00594, + "64": 0.00626, + "128": 0.00639 + }, + "fi": { + "16": 0.00465, + "32": 0.00517, + "64": 0.00567, + "128": 0.00581 + }, + "fl": { + "16": 0.00378, + "32": 0.00413, + "64": 0.00469, + "128": 0.00481 + }, + "f_f_i": { + "16": 0.0051, + "32": 0.00556, + "64": 0.00595, + "128": 0.00608 + }, + "f_f_l": { + "16": 0.00489, + "32": 0.00533, + "64": 0.00564, + "128": 0.00578 + }, + "Gamma": { + "16": 0.00819, + "32": 0.00844, + "64": 0.00884, + "128": 0.00922 + }, + "Delta": { + "16": 0.01076, + "32": 0.01213, + "64": 0.01268, + "128": 0.01275 + }, + "Theta": { + "16": 0.00638, + "32": 0.0079, + "64": 0.0082, + "128": 0.00836 + }, + "Lambda": { + "16": 0.00937, + "32": 0.00972, + "64": 0.01019, + "128": 0.01033 + }, + "Xi": { + "16": 0.01262, + "32": 0.01688, + "64": 0.01776, + "128": 0.01804 + }, + "Pi": { + "16": 0.00691, + "32": 0.00705, + "64": 0.00717, + "128": 0.00761 + }, + "Sigma": { + "16": 0.00998, + "32": 0.01168, + "64": 0.01204, + "128": 0.01234 + }, + "Phi": { + "16": 0.0052, + "32": 0.00569, + "64": 0.00601, + "128": 0.00633 + }, + "Psi": { + "16": 0.00712, + "32": 0.00752, + "64": 0.00794, + "128": 0.00864 + }, + "Omega": { + "16": 0.00545, + "32": 0.00614, + "64": 0.00648, + "128": 0.00675 + }, + "alpha": { + "16": 0.01017, + "32": 0.01131, + "64": 0.0118, + "128": 0.01213 + }, + "beta": { + "16": 0.0041, + "32": 0.00503, + "64": 0.00545, + "128": 0.00561 + }, + "gamma": { + "16": 0.00629, + "32": 0.00762, + "64": 0.00818, + "128": 0.00834 + }, + "delta": { + "16": 0.00495, + "32": 0.00597, + "64": 0.00635, + "128": 0.00658 + }, + "epsilon": { + "16": 0.007, + "32": 0.00944, + "64": 0.01006, + "128": 0.01057 + }, + "zeta": { + "16": 0.00867, + "32": 0.00996, + "64": 0.01137, + "128": 0.01172 + }, + "eta": { + "16": 0.00362, + "32": 0.0041, + "64": 0.00456, + "128": 0.00479 + }, + "theta": { + "16": 0.00395, + "32": 0.00429, + "64": 0.00455, + "128": 0.00467 + }, + "iota": { + "16": 0.00195, + "32": 0.00279, + "64": 0.00346, + "128": 0.00392 + }, + "kappa": { + "16": 0.00522, + "32": 0.0062, + "64": 0.00678, + "128": 0.00701 + }, + "lambda": { + "16": 0.00681, + "32": 0.00787, + "64": 0.00806, + "128": 0.00826 + }, + "mu": { + "16": 0.00184, + "32": 0.00295, + "64": 0.00341, + "128": 0.00367 + }, + "nu": { + "16": 0.00593, + "32": 0.0074, + "64": 0.0079, + "128": 0.00828 + }, + "xi": { + "16": 0.00861, + "32": 0.01064, + "64": 0.01159, + "128": 0.01203 + }, + "pi": { + "16": 0.00377, + "32": 0.00448, + "64": 0.00474, + "128": 0.00493 + }, + "rho": { + "16": 0.00476, + "32": 0.00537, + "64": 0.00562, + "128": 0.0058 + }, + "sigmafinal": { + "16": 0.00729, + "32": 0.00854, + "64": 0.00955, + "128": 0.00988 + }, + "sigma": { + "16": 0.00506, + "32": 0.00544, + "64": 0.00571, + "128": 0.00589 + }, + "tau": { + "16": 0.00488, + "32": 0.00557, + "64": 0.00603, + "128": 0.00622 + }, + "upsilon": { + "16": 0.00529, + "32": 0.00613, + "64": 0.0066, + "128": 0.0068 + }, + "phi": { + "16": 0.00485, + "32": 0.00551, + "64": 0.00566, + "128": 0.00584 + }, + "chi": { + "16": 0.00637, + "32": 0.00693, + "64": 0.00712, + "128": 0.00732 + }, + "psi": { + "16": 0.00358, + "32": 0.00404, + "64": 0.00417, + "128": 0.00437 + }, + "omega": { + "16": 0.00712, + "32": 0.0087, + "64": 0.00931, + "128": 0.00968 + }, + "uni20B9": { + "16": 0.01044, + "32": 0.01131, + "64": 0.01166, + "128": 0.0122 + }, + "minus": { + "16": 0.01652, + "32": 0.01654, + "64": 0.01671, + "128": 0.01673 + }, + "multiply": { + "16": 0.00525, + "32": 0.00564, + "64": 0.00651, + "128": 0.00675 + }, + "divide": { + "16": 0.01578, + "32": 0.01972, + "64": 0.02009, + "128": 0.02016 + }, + "plusminus": { + "16": 0.01293, + "32": 0.01312, + "64": 0.01357, + "128": 0.01374 + }, + "notequal": { + "16": 0.01198, + "32": 0.01254, + "64": 0.01289, + "128": 0.01305 + }, + "lessequal": { + "16": 0.00976, + "32": 0.01007, + "64": 0.01026, + "128": 0.01033 + }, + "greaterequal": { + "16": 0.01053, + "32": 0.01098, + "64": 0.01131, + "128": 0.0114 + }, + "summation": { + "16": 0.00893, + "32": 0.0096, + "64": 0.0101, + "128": 0.01032 + }, + "product": { + "16": 0.00583, + "32": 0.00593, + "64": 0.00608, + "128": 0.00617 + }, + "integral": { + "16": 0.00354, + "32": 0.0038, + "64": 0.00403, + "128": 0.00414 + }, + "radical": { + "16": 0.0106, + "32": 0.01152, + "64": 0.01198, + "128": 0.01219 + }, + "infinity": { + "16": 0.00631, + "32": 0.00807, + "64": 0.00864, + "128": 0.00882 + }, + "partialdiff": { + "16": 0.00649, + "32": 0.00777, + "64": 0.00824, + "128": 0.00844 + }, + "nabla": { + "16": 0.0088, + "32": 0.0099, + "64": 0.01036, + "128": 0.01047 + }, + "minute": { + "16": 0.00782, + "32": 0.00884, + "64": 0.00907, + "128": 0.00943 + }, + "second": { + "16": 0.00773, + "32": 0.00877, + "64": 0.00899, + "128": 0.00936 + }, + "ringoperator": { + "16": 0.0124, + "32": 0.01382, + "64": 0.0155, + "128": 0.01573 + }, + "arrowright": { + "16": 0.01244, + "32": 0.01275, + "64": 0.01297, + "128": 0.01336 + }, + "arrowleft": { + "16": 0.01294, + "32": 0.01319, + "64": 0.01365, + "128": 0.014 + }, + "arrowboth": { + "16": 0.01095, + "32": 0.01167, + "64": 0.01235, + "128": 0.01283 + }, + "element": { + "16": 0.01194, + "32": 0.0123, + "64": 0.01249, + "128": 0.01262 + }, + "notelement": { + "16": 0.0092, + "32": 0.00985, + "64": 0.01019, + "128": 0.01032 + }, + "propersubset": { + "16": 0.01157, + "32": 0.01211, + "64": 0.01236, + "128": 0.01257 + }, + "reflexsubset": { + "16": 0.01323, + "32": 0.01354, + "64": 0.01385, + "128": 0.01415 + }, + "union": { + "16": 0.00806, + "32": 0.00887, + "64": 0.00902, + "128": 0.00918 + }, + "intersection": { + "16": 0.00907, + "32": 0.00951, + "64": 0.00985, + "128": 0.0101 + }, + "universal": { + "16": 0.01083, + "32": 0.01149, + "64": 0.01172, + "128": 0.01184 + }, + "existential": { + "16": 0.00959, + "32": 0.00995, + "64": 0.01003, + "128": 0.0101 + }, + "emptyset": { + "16": 0.00525, + "32": 0.00559, + "64": 0.00584, + "128": 0.006 + }, + "approxequal": { + "16": 0.01249, + "32": 0.0135, + "64": 0.01421, + "128": 0.01452 + }, + "equivalence": { + "16": 0.01679, + "32": 0.01682, + "64": 0.01697, + "128": 0.01705 + }, + "proportional": { + "16": 0.00784, + "32": 0.00829, + "64": 0.00869, + "128": 0.00899 + }, + "fraction": { + "16": 0.01168, + "32": 0.01226, + "64": 0.01275, + "128": 0.01293 + }, + "onesuperior": { + "16": 0.00775, + "32": 0.00789, + "64": 0.00812, + "128": 0.00822 + }, + "twosuperior": { + "16": 0.01055, + "32": 0.01117, + "64": 0.01211, + "128": 0.01247 + }, + "threesuperior": { + "16": 0.01296, + "32": 0.01561, + "64": 0.0165, + "128": 0.01689 + }, + "ordfeminine": { + "16": 0.00789, + "32": 0.0098, + "64": 0.01112, + "128": 0.01146 + }, + "ordmasculine": { + "16": 0.0094, + "32": 0.00984, + "64": 0.01038, + "128": 0.01075 + }, + "dottedcircle": { + "16": 0.01125, + "32": 0.0194, + "64": 0.02577, + "128": 0.02786 + }, + "gravecomb": { + "16": 0.01324, + "32": 0.01594, + "64": 0.01697, + "128": 0.01728 + }, + "gravecomb.case": { + "16": 0.00799, + "32": 0.01355, + "64": 0.0144, + "128": 0.01488 + }, + "acutecomb": { + "16": 0.01274, + "32": 0.01652, + "64": 0.01743, + "128": 0.01795 + }, + "acutecomb.case": { + "16": 0.00708, + "32": 0.01168, + "64": 0.01223, + "128": 0.0129 + }, + "circumflexcomb": { + "16": 0.00755, + "32": 0.01223, + "64": 0.01314, + "128": 0.01395 + }, + "circumflexcomb.case": { + "16": 0.01112, + "32": 0.01459, + "64": 0.01681, + "128": 0.01778 + }, + "tildecomb": { + "16": 0.01381, + "32": 0.0208, + "64": 0.0223, + "128": 0.02345 + }, + "tildecomb.case": { + "16": 0.0157, + "32": 0.01906, + "64": 0.023, + "128": 0.02445 + }, + "macroncomb": { + "16": 0.01416, + "32": 0.0145, + "64": 0.01522, + "128": 0.01584 + }, + "dieresiscomb": { + "16": 0.01143, + "32": 0.01189, + "64": 0.01244, + "128": 0.01259 + }, + "dieresiscomb.case": { + "16": 0.00861, + "32": 0.00986, + "64": 0.00991, + "128": 0.01015 + }, + "ringcomb": { + "16": 0.00505, + "32": 0.00692, + "64": 0.00723, + "128": 0.00763 + }, + "ringcomb.case": { + "16": 0.00764, + "32": 0.00949, + "64": 0.01052, + "128": 0.01094 + }, + "caroncomb": { + "16": 0.00765, + "32": 0.01166, + "64": 0.0135, + "128": 0.01417 + }, + "dotaccentcomb": { + "16": 0.01035, + "32": 0.01118, + "64": 0.01118, + "128": 0.01177 + }, + "cedillacomb": { + "16": 0.00793, + "32": 0.01035, + "64": 0.01119, + "128": 0.01201 + }, + "zero.lf": { + "16": 0.00502, + "32": 0.00575, + "64": 0.00597, + "128": 0.0061 + }, + "one.lf": { + "16": 0.00361, + "32": 0.0039, + "64": 0.00425, + "128": 0.00464 + }, + "two.lf": { + "16": 0.00708, + "32": 0.00849, + "64": 0.00884, + "128": 0.00898 + }, + "three.lf": { + "16": 0.00662, + "32": 0.00772, + "64": 0.00872, + "128": 0.00918 + }, + "four.lf": { + "16": 0.00708, + "32": 0.00842, + "64": 0.00947, + "128": 0.01001 + }, + "five.lf": { + "16": 0.00723, + "32": 0.00813, + "64": 0.00876, + "128": 0.00901 + }, + "six.lf": { + "16": 0.00623, + "32": 0.00693, + "64": 0.00727, + "128": 0.00742 + }, + "seven.lf": { + "16": 0.0053, + "32": 0.00581, + "64": 0.00616, + "128": 0.00641 + }, + "eight.lf": { + "16": 0.00503, + "32": 0.00592, + "64": 0.00634, + "128": 0.0065 + }, + "nine.lf": { + "16": 0.00539, + "32": 0.00634, + "64": 0.00672, + "128": 0.00694 + }, + "zero.osf": { + "16": 0.00581, + "32": 0.00642, + "64": 0.00662, + "128": 0.00675 + }, + "one.osf": { + "16": 0.00934, + "32": 0.00965, + "64": 0.00984, + "128": 0.01009 + }, + "two.osf": { + "16": 0.00795, + "32": 0.00885, + "64": 0.00973, + "128": 0.00995 + }, + "three.osf": { + "16": 0.00781, + "32": 0.00954, + "64": 0.01034, + "128": 0.01072 + }, + "four.osf": { + "16": 0.00746, + "32": 0.00956, + "64": 0.0102, + "128": 0.01031 + }, + "five.osf": { + "16": 0.0094, + "32": 0.01129, + "64": 0.01237, + "128": 0.01271 + }, + "six.osf": { + "16": 0.00597, + "32": 0.00671, + "64": 0.00709, + "128": 0.00727 + }, + "seven.osf": { + "16": 0.00588, + "32": 0.00706, + "64": 0.0072, + "128": 0.00748 + }, + "eight.osf": { + "16": 0.00519, + "32": 0.00624, + "64": 0.00672, + "128": 0.00688 + }, + "nine.osf": { + "16": 0.005, + "32": 0.00586, + "64": 0.00629, + "128": 0.00644 + }, + "zero.tosf": { + "16": 0.00436, + "32": 0.005, + "64": 0.00518, + "128": 0.00529 + }, + "one.tosf": { + "16": 0.00911, + "32": 0.0098, + "64": 0.00994, + "128": 0.01023 + }, + "two.tosf": { + "16": 0.00784, + "32": 0.00873, + "64": 0.00964, + "128": 0.00986 + }, + "three.tosf": { + "16": 0.00809, + "32": 0.00993, + "64": 0.01061, + "128": 0.01092 + }, + "four.tosf": { + "16": 0.00742, + "32": 0.00953, + "64": 0.01018, + "128": 0.01028 + }, + "five.tosf": { + "16": 0.00876, + "32": 0.01085, + "64": 0.01208, + "128": 0.01231 + }, + "six.tosf": { + "16": 0.00598, + "32": 0.00729, + "64": 0.00762, + "128": 0.00783 + }, + "seven.tosf": { + "16": 0.00523, + "32": 0.00646, + "64": 0.00684, + "128": 0.00718 + }, + "eight.tosf": { + "16": 0.00466, + "32": 0.006, + "64": 0.00637, + "128": 0.00657 + }, + "nine.tosf": { + "16": 0.00648, + "32": 0.007, + "64": 0.00749, + "128": 0.00764 + }, + "two.dnom": { + "16": 0.01055, + "32": 0.01117, + "64": 0.01211, + "128": 0.01247 + }, + "four.dnom": { + "16": 0.00613, + "32": 0.00692, + "64": 0.00768, + "128": 0.00793 + }, + "a.sc": { + "16": 0.00875, + "32": 0.0095, + "64": 0.0098, + "128": 0.01066 + }, + "b.sc": { + "16": 0.00808, + "32": 0.00904, + "64": 0.00957, + "128": 0.00992 + }, + "c.sc": { + "16": 0.00789, + "32": 0.00911, + "64": 0.00957, + "128": 0.00981 + }, + "d.sc": { + "16": 0.00731, + "32": 0.00797, + "64": 0.00938, + "128": 0.00967 + }, + "e.sc": { + "16": 0.00725, + "32": 0.01005, + "64": 0.01057, + "128": 0.01082 + }, + "f.sc": { + "16": 0.00712, + "32": 0.01, + "64": 0.01076, + "128": 0.01108 + }, + "g.sc": { + "16": 0.00853, + "32": 0.00997, + "64": 0.01018, + "128": 0.01046 + }, + "h.sc": { + "16": 0.00641, + "32": 0.00734, + "64": 0.00754, + "128": 0.00809 + }, + "i.sc": { + "16": 0.00763, + "32": 0.00841, + "64": 0.00853, + "128": 0.00913 + }, + "j.sc": { + "16": 0.006, + "32": 0.00657, + "64": 0.00698, + "128": 0.00728 + }, + "k.sc": { + "16": 0.00627, + "32": 0.00765, + "64": 0.00823, + "128": 0.00904 + }, + "l.sc": { + "16": 0.00714, + "32": 0.00779, + "64": 0.00792, + "128": 0.00836 + }, + "m.sc": { + "16": 0.00654, + "32": 0.00764, + "64": 0.00809, + "128": 0.00856 + }, + "n.sc": { + "16": 0.00692, + "32": 0.00804, + "64": 0.00843, + "128": 0.00902 + }, + "o.sc": { + "16": 0.00604, + "32": 0.00773, + "64": 0.00792, + "128": 0.0081 + }, + "p.sc": { + "16": 0.00795, + "32": 0.00919, + "64": 0.0109, + "128": 0.01123 + }, + "q.sc": { + "16": 0.00452, + "32": 0.00626, + "64": 0.00662, + "128": 0.00692 + }, + "r.sc": { + "16": 0.00854, + "32": 0.00956, + "64": 0.01039, + "128": 0.01076 + }, + "s.sc": { + "16": 0.00697, + "32": 0.00911, + "64": 0.01002, + "128": 0.01048 + }, + "t.sc": { + "16": 0.00372, + "32": 0.0071, + "64": 0.0079, + "128": 0.00807 + }, + "u.sc": { + "16": 0.00714, + "32": 0.00882, + "64": 0.0096, + "128": 0.01016 + }, + "v.sc": { + "16": 0.00623, + "32": 0.00788, + "64": 0.00843, + "128": 0.009 + }, + "w.sc": { + "16": 0.00613, + "32": 0.00822, + "64": 0.00906, + "128": 0.00949 + }, + "x.sc": { + "16": 0.00488, + "32": 0.00564, + "64": 0.00639, + "128": 0.00708 + }, + "y.sc": { + "16": 0.00816, + "32": 0.00981, + "64": 0.01017, + "128": 0.01101 + }, + "z.sc": { + "16": 0.00499, + "32": 0.0078, + "64": 0.00824, + "128": 0.00833 + }, + "ae.sc": { + "16": 0.00424, + "32": 0.00708, + "64": 0.00783, + "128": 0.00804 + }, + "oe.sc": { + "16": 0.00502, + "32": 0.00759, + "64": 0.0079, + "128": 0.00806 + }, + "oslash.sc": { + "16": 0.00596, + "32": 0.00685, + "64": 0.0072, + "128": 0.00749 + }, + "eth.sc": { + "16": 0.00554, + "32": 0.00633, + "64": 0.00801, + "128": 0.0083 + }, + "thorn.sc": { + "16": 0.00852, + "32": 0.00969, + "64": 0.0107, + "128": 0.01126 + }, + "gamma.sc": { + "16": 0.00699, + "32": 0.01041, + "64": 0.01111, + "128": 0.01149 + }, + "delta.sc": { + "16": 0.00947, + "32": 0.00964, + "64": 0.00976, + "128": 0.00986 + }, + "theta.sc": { + "16": 0.00766, + "32": 0.00956, + "64": 0.00998, + "128": 0.01022 + }, + "lambda.sc": { + "16": 0.00923, + "32": 0.01029, + "64": 0.01066, + "128": 0.01156 + }, + "xi.sc": { + "16": 0.00578, + "32": 0.0124, + "64": 0.0131, + "128": 0.01342 + }, + "pi.sc": { + "16": 0.00572, + "32": 0.00612, + "64": 0.00841, + "128": 0.00879 + }, + "sigma.sc": { + "16": 0.00601, + "32": 0.01109, + "64": 0.01155, + "128": 0.01163 + }, + "phi.sc": { + "16": 0.00444, + "32": 0.00535, + "64": 0.00557, + "128": 0.00585 + }, + "psi.sc": { + "16": 0.00588, + "32": 0.00735, + "64": 0.00768, + "128": 0.00806 + }, + "omega.sc": { + "16": 0.00581, + "32": 0.0069, + "64": 0.0074, + "128": 0.00772 + } + } + }, + "BoldItalic": { + "font": "MachineMadeWarp-BoldItalic.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.01679, + "max_glyph": "equivalence", + "mean": 0.00737, + "p95": 0.01357, + "pass": true + }, + "32": { + "max": 0.01972, + "max_glyph": "divide", + "mean": 0.00865, + "p95": 0.01575, + "pass": true + }, + "64": { + "max": 0.02577, + "max_glyph": "dottedcircle", + "mean": 0.00924, + "p95": 0.01687, + "pass": true + }, + "128": { + "max": 0.02786, + "max_glyph": "dottedcircle", + "mean": 0.00954, + "p95": 0.01709, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.00651, + "32": 0.00732, + "64": 0.00801, + "128": 0.00827 + }, + "quotedbl": { + "16": 0.00745, + "32": 0.00948, + "64": 0.01051, + "128": 0.01094 + }, + "numbersign": { + "16": 0.00453, + "32": 0.00469, + "64": 0.00502, + "128": 0.00523 + }, + "dollar": { + "16": 0.01005, + "32": 0.01329, + "64": 0.01433, + "128": 0.01467 + }, + "percent": { + "16": 0.00873, + "32": 0.00933, + "64": 0.00955, + "128": 0.00973 + }, + "ampersand": { + "16": 0.00577, + "32": 0.00702, + "64": 0.00749, + "128": 0.00779 + }, + "quotesingle": { + "16": 0.00703, + "32": 0.00888, + "64": 0.0101, + "128": 0.01051 + }, + "parenleft": { + "16": 0.00636, + "32": 0.00708, + "64": 0.00766, + "128": 0.00779 + }, + "parenright": { + "16": 0.00796, + "32": 0.00893, + "64": 0.00916, + "128": 0.00929 + }, + "asterisk": { + "16": 0.00936, + "32": 0.01085, + "64": 0.01209, + "128": 0.01294 + }, + "plus": { + "16": 0.01445, + "32": 0.01481, + "64": 0.01535, + "128": 0.0155 + }, + "comma": { + "16": 0.00867, + "32": 0.0091, + "64": 0.00935, + "128": 0.00965 + }, + "hyphen": { + "16": 0.00801, + "32": 0.01007, + "64": 0.01072, + "128": 0.01072 + }, + "period": { + "16": 0.00793, + "32": 0.00863, + "64": 0.00941, + "128": 0.00955 + }, + "slash": { + "16": 0.00714, + "32": 0.00757, + "64": 0.00781, + "128": 0.00789 + }, + "zero": { + "16": 0.00463, + "32": 0.00483, + "64": 0.00502, + "128": 0.00519 + }, + "one": { + "16": 0.00294, + "32": 0.00329, + "64": 0.00352, + "128": 0.0039 + }, + "two": { + "16": 0.00702, + "32": 0.00808, + "64": 0.00857, + "128": 0.0088 + }, + "three": { + "16": 0.00734, + "32": 0.00845, + "64": 0.00904, + "128": 0.00935 + }, + "four": { + "16": 0.00779, + "32": 0.00857, + "64": 0.00892, + "128": 0.00928 + }, + "five": { + "16": 0.00694, + "32": 0.00832, + "64": 0.00895, + "128": 0.00926 + }, + "six": { + "16": 0.00552, + "32": 0.00661, + "64": 0.00699, + "128": 0.00716 + }, + "seven": { + "16": 0.00472, + "32": 0.0054, + "64": 0.00586, + "128": 0.00605 + }, + "eight": { + "16": 0.00504, + "32": 0.00606, + "64": 0.00633, + "128": 0.00658 + }, + "nine": { + "16": 0.00502, + "32": 0.00647, + "64": 0.00693, + "128": 0.00708 + }, + "colon": { + "16": 0.00617, + "32": 0.00824, + "64": 0.00897, + "128": 0.00925 + }, + "semicolon": { + "16": 0.0077, + "32": 0.00882, + "64": 0.00925, + "128": 0.00963 + }, + "less": { + "16": 0.01083, + "32": 0.01153, + "64": 0.01172, + "128": 0.01186 + }, + "equal": { + "16": 0.01669, + "32": 0.01671, + "64": 0.01688, + "128": 0.01693 + }, + "greater": { + "16": 0.01369, + "32": 0.01408, + "64": 0.01465, + "128": 0.01491 + }, + "question": { + "16": 0.00731, + "32": 0.0085, + "64": 0.00915, + "128": 0.0093 + }, + "at": { + "16": 0.00705, + "32": 0.00791, + "64": 0.0085, + "128": 0.00874 + }, + "A": { + "16": 0.00671, + "32": 0.00864, + "64": 0.00977, + "128": 0.01007 + }, + "B": { + "16": 0.00792, + "32": 0.00871, + "64": 0.00959, + "128": 0.00983 + }, + "C": { + "16": 0.00725, + "32": 0.00835, + "64": 0.00878, + "128": 0.00902 + }, + "D": { + "16": 0.00624, + "32": 0.00671, + "64": 0.00708, + "128": 0.00735 + }, + "E": { + "16": 0.00819, + "32": 0.00952, + "64": 0.00995, + "128": 0.01014 + }, + "F": { + "16": 0.00473, + "32": 0.00536, + "64": 0.0057, + "128": 0.00604 + }, + "G": { + "16": 0.0079, + "32": 0.00884, + "64": 0.00942, + "128": 0.00965 + }, + "H": { + "16": 0.00465, + "32": 0.00468, + "64": 0.00471, + "128": 0.0052 + }, + "I": { + "16": 0.00654, + "32": 0.00656, + "64": 0.00662, + "128": 0.00714 + }, + "J": { + "16": 0.00306, + "32": 0.00345, + "64": 0.00356, + "128": 0.00371 + }, + "K": { + "16": 0.00478, + "32": 0.00519, + "64": 0.00545, + "128": 0.006 + }, + "L": { + "16": 0.00703, + "32": 0.00719, + "64": 0.00741, + "128": 0.00788 + }, + "M": { + "16": 0.0075, + "32": 0.00852, + "64": 0.00873, + "128": 0.0089 + }, + "N": { + "16": 0.00752, + "32": 0.00861, + "64": 0.00924, + "128": 0.00947 + }, + "O": { + "16": 0.00384, + "32": 0.00487, + "64": 0.00517, + "128": 0.00535 + }, + "P": { + "16": 0.00696, + "32": 0.00755, + "64": 0.00795, + "128": 0.0083 + }, + "Q": { + "16": 0.00446, + "32": 0.00564, + "64": 0.00604, + "128": 0.00623 + }, + "R": { + "16": 0.00664, + "32": 0.00767, + "64": 0.00819, + "128": 0.00853 + }, + "S": { + "16": 0.00941, + "32": 0.01119, + "64": 0.01175, + "128": 0.01202 + }, + "T": { + "16": 0.0033, + "32": 0.00386, + "64": 0.00421, + "128": 0.00443 + }, + "U": { + "16": 0.00668, + "32": 0.00873, + "64": 0.0096, + "128": 0.00991 + }, + "V": { + "16": 0.0061, + "32": 0.00762, + "64": 0.00826, + "128": 0.00853 + }, + "W": { + "16": 0.00757, + "32": 0.0089, + "64": 0.00974, + "128": 0.00999 + }, + "X": { + "16": 0.00485, + "32": 0.00502, + "64": 0.00553, + "128": 0.00588 + }, + "Y": { + "16": 0.00509, + "32": 0.00531, + "64": 0.00558, + "128": 0.00588 + }, + "Z": { + "16": 0.00711, + "32": 0.00779, + "64": 0.00792, + "128": 0.00806 + }, + "bracketleft": { + "16": 0.0059, + "32": 0.0059, + "64": 0.00595, + "128": 0.00601 + }, + "backslash": { + "16": 0.00441, + "32": 0.00462, + "64": 0.00466, + "128": 0.00469 + }, + "bracketright": { + "16": 0.00262, + "32": 0.00266, + "64": 0.00274, + "128": 0.00277 + }, + "asciicircum": { + "16": 0.00687, + "32": 0.00748, + "64": 0.00816, + "128": 0.00843 + }, + "underscore": { + "16": 0.01483, + "32": 0.01501, + "64": 0.01539, + "128": 0.01539 + }, + "grave": { + "16": 0.0112, + "32": 0.01528, + "64": 0.01621, + "128": 0.01674 + }, + "a": { + "16": 0.00556, + "32": 0.00628, + "64": 0.00667, + "128": 0.00687 + }, + "b": { + "16": 0.00453, + "32": 0.0054, + "64": 0.00575, + "128": 0.00605 + }, + "c": { + "16": 0.0072, + "32": 0.00801, + "64": 0.00851, + "128": 0.00871 + }, + "d": { + "16": 0.00428, + "32": 0.00506, + "64": 0.00528, + "128": 0.00549 + }, + "e": { + "16": 0.00951, + "32": 0.01119, + "64": 0.01221, + "128": 0.01248 + }, + "f": { + "16": 0.00486, + "32": 0.00567, + "64": 0.0059, + "128": 0.00614 + }, + "g": { + "16": 0.00758, + "32": 0.00822, + "64": 0.00858, + "128": 0.00883 + }, + "h": { + "16": 0.00428, + "32": 0.00506, + "64": 0.00554, + "128": 0.00578 + }, + "i": { + "16": 0.00518, + "32": 0.00577, + "64": 0.00669, + "128": 0.00694 + }, + "j": { + "16": 0.00403, + "32": 0.00474, + "64": 0.00502, + "128": 0.00525 + }, + "k": { + "16": 0.00595, + "32": 0.00664, + "64": 0.00709, + "128": 0.00752 + }, + "l": { + "16": 0.00255, + "32": 0.00308, + "64": 0.00328, + "128": 0.00347 + }, + "m": { + "16": 0.0067, + "32": 0.00746, + "64": 0.00777, + "128": 0.00794 + }, + "n": { + "16": 0.00461, + "32": 0.00558, + "64": 0.00608, + "128": 0.00638 + }, + "o": { + "16": 0.00492, + "32": 0.00558, + "64": 0.00588, + "128": 0.00602 + }, + "p": { + "16": 0.00472, + "32": 0.00564, + "64": 0.00628, + "128": 0.00652 + }, + "q": { + "16": 0.00547, + "32": 0.00624, + "64": 0.00657, + "128": 0.00679 + }, + "r": { + "16": 0.00566, + "32": 0.00639, + "64": 0.00693, + "128": 0.00708 + }, + "s": { + "16": 0.0057, + "32": 0.00778, + "64": 0.00899, + "128": 0.00922 + }, + "t": { + "16": 0.00738, + "32": 0.00898, + "64": 0.00956, + "128": 0.00983 + }, + "u": { + "16": 0.00533, + "32": 0.00606, + "64": 0.00667, + "128": 0.00688 + }, + "v": { + "16": 0.00709, + "32": 0.00887, + "64": 0.00922, + "128": 0.00945 + }, + "w": { + "16": 0.0058, + "32": 0.00693, + "64": 0.00755, + "128": 0.00772 + }, + "x": { + "16": 0.00879, + "32": 0.01033, + "64": 0.01249, + "128": 0.01332 + }, + "y": { + "16": 0.00493, + "32": 0.00552, + "64": 0.0059, + "128": 0.00609 + }, + "z": { + "16": 0.00784, + "32": 0.00993, + "64": 0.0105, + "128": 0.01068 + }, + "braceleft": { + "16": 0.00616, + "32": 0.00683, + "64": 0.00731, + "128": 0.0075 + }, + "bar": { + "16": 0.00398, + "32": 0.00433, + "64": 0.00446, + "128": 0.00455 + }, + "braceright": { + "16": 0.00712, + "32": 0.00803, + "64": 0.00877, + "128": 0.00917 + }, + "asciitilde": { + "16": 0.01022, + "32": 0.01242, + "64": 0.01284, + "128": 0.0133 + }, + "quoteleft": { + "16": 0.007, + "32": 0.00779, + "64": 0.00845, + "128": 0.0087 + }, + "quoteright": { + "16": 0.00871, + "32": 0.00948, + "64": 0.00963, + "128": 0.00976 + }, + "quotedblleft": { + "16": 0.00642, + "32": 0.00724, + "64": 0.00788, + "128": 0.00815 + }, + "quotedblright": { + "16": 0.00864, + "32": 0.00919, + "64": 0.00943, + "128": 0.00954 + }, + "quotesinglbase": { + "16": 0.00867, + "32": 0.0091, + "64": 0.00935, + "128": 0.00965 + }, + "quotedblbase": { + "16": 0.00849, + "32": 0.00886, + "64": 0.00927, + "128": 0.0095 + }, + "guilsinglleft": { + "16": 0.0148, + "32": 0.01578, + "64": 0.01693, + "128": 0.01721 + }, + "guilsinglright": { + "16": 0.00993, + "32": 0.01361, + "64": 0.01603, + "128": 0.01643 + }, + "guillemotleft": { + "16": 0.01388, + "32": 0.0165, + "64": 0.01764, + "128": 0.01803 + }, + "guillemotright": { + "16": 0.00978, + "32": 0.01273, + "64": 0.01478, + "128": 0.01524 + }, + "ellipsis": { + "16": 0.00918, + "32": 0.00987, + "64": 0.01053, + "128": 0.01063 + }, + "bullet": { + "16": 0.00323, + "32": 0.00339, + "64": 0.00356, + "128": 0.00379 + }, + "periodcentered": { + "16": 0.01376, + "32": 0.01917, + "64": 0.01955, + "128": 0.0199 + }, + "Euro": { + "16": 0.00784, + "32": 0.00869, + "64": 0.00926, + "128": 0.00964 + }, + "copyright": { + "16": 0.00912, + "32": 0.01042, + "64": 0.01133, + "128": 0.0117 + }, + "registered": { + "16": 0.00836, + "32": 0.00995, + "64": 0.01064, + "128": 0.01101 + }, + "degree": { + "16": 0.00684, + "32": 0.0109, + "64": 0.01169, + "128": 0.01199 + }, + "emdash": { + "16": 0.01509, + "32": 0.01523, + "64": 0.0153, + "128": 0.01549 + }, + "endash": { + "16": 0.01462, + "32": 0.01573, + "64": 0.01576, + "128": 0.01621 + }, + "exclamdown": { + "16": 0.00666, + "32": 0.00768, + "64": 0.00833, + "128": 0.00857 + }, + "questiondown": { + "16": 0.00624, + "32": 0.00799, + "64": 0.00857, + "128": 0.00882 + }, + "section": { + "16": 0.00823, + "32": 0.00916, + "64": 0.01028, + "128": 0.01066 + }, + "paragraph": { + "16": 0.00445, + "32": 0.00484, + "64": 0.00502, + "128": 0.00513 + }, + "cent": { + "16": 0.00775, + "32": 0.00881, + "64": 0.00938, + "128": 0.00995 + }, + "sterling": { + "16": 0.00759, + "32": 0.00864, + "64": 0.00932, + "128": 0.00953 + }, + "yen": { + "16": 0.00452, + "32": 0.00537, + "64": 0.00604, + "128": 0.00664 + }, + "brokenbar": { + "16": 0.00447, + "32": 0.00489, + "64": 0.00505, + "128": 0.0052 + }, + "logicalnot": { + "16": 0.00974, + "32": 0.00989, + "64": 0.01033, + "128": 0.01054 + }, + "dieresis": { + "16": 0.01179, + "32": 0.01241, + "64": 0.01312, + "128": 0.01334 + }, + "acute": { + "16": 0.00985, + "32": 0.01655, + "64": 0.01737, + "128": 0.01746 + }, + "macron": { + "16": 0.01421, + "32": 0.01455, + "64": 0.01522, + "128": 0.01584 + }, + "cedilla": { + "16": 0.00717, + "32": 0.00943, + "64": 0.01153, + "128": 0.01221 + }, + "circumflex": { + "16": 0.01308, + "32": 0.01515, + "64": 0.01785, + "128": 0.01844 + }, + "tilde": { + "16": 0.01328, + "32": 0.01897, + "64": 0.01955, + "128": 0.02072 + }, + "ring": { + "16": 0.00671, + "32": 0.0096, + "64": 0.01089, + "128": 0.01152 + }, + "caron": { + "16": 0.01076, + "32": 0.01543, + "64": 0.01651, + "128": 0.01715 + }, + "dotaccent": { + "16": 0.01023, + "32": 0.01155, + "64": 0.01158, + "128": 0.01181 + }, + "AE": { + "16": 0.00869, + "32": 0.00989, + "64": 0.01038, + "128": 0.01062 + }, + "Eth": { + "16": 0.00545, + "32": 0.00592, + "64": 0.00631, + "128": 0.00649 + }, + "Oslash": { + "16": 0.00583, + "32": 0.0061, + "64": 0.00637, + "128": 0.00658 + }, + "Thorn": { + "16": 0.00728, + "32": 0.00792, + "64": 0.00818, + "128": 0.00862 + }, + "germandbls": { + "16": 0.0057, + "32": 0.00655, + "64": 0.00692, + "128": 0.00722 + }, + "ae": { + "16": 0.00725, + "32": 0.00853, + "64": 0.00913, + "128": 0.00942 + }, + "eth": { + "16": 0.00598, + "32": 0.00709, + "64": 0.00738, + "128": 0.0076 + }, + "oslash": { + "16": 0.00631, + "32": 0.00735, + "64": 0.00764, + "128": 0.00779 + }, + "thorn": { + "16": 0.00519, + "32": 0.00604, + "64": 0.00626, + "128": 0.00648 + }, + "OE": { + "16": 0.00764, + "32": 0.00845, + "64": 0.00899, + "128": 0.00916 + }, + "oe": { + "16": 0.00624, + "32": 0.00749, + "64": 0.00812, + "128": 0.00837 + }, + "dotlessi": { + "16": 0.00369, + "32": 0.0051, + "64": 0.00565, + "128": 0.00575 + }, + "dotlessj": { + "16": 0.00612, + "32": 0.00678, + "64": 0.00748, + "128": 0.00763 + }, + "f_f": { + "16": 0.00368, + "32": 0.00411, + "64": 0.00431, + "128": 0.00446 + }, + "fi": { + "16": 0.00509, + "32": 0.00574, + "64": 0.00618, + "128": 0.00647 + }, + "fl": { + "16": 0.00502, + "32": 0.00571, + "64": 0.00593, + "128": 0.00613 + }, + "f_f_i": { + "16": 0.00404, + "32": 0.00463, + "64": 0.00497, + "128": 0.00517 + }, + "f_f_l": { + "16": 0.00342, + "32": 0.00385, + "64": 0.00405, + "128": 0.00419 + }, + "Gamma": { + "16": 0.00589, + "32": 0.00612, + "64": 0.00635, + "128": 0.00672 + }, + "Delta": { + "16": 0.01213, + "32": 0.01296, + "64": 0.01347, + "128": 0.01352 + }, + "Theta": { + "16": 0.00583, + "32": 0.00689, + "64": 0.0073, + "128": 0.00759 + }, + "Lambda": { + "16": 0.00747, + "32": 0.00821, + "64": 0.00863, + "128": 0.00884 + }, + "Xi": { + "16": 0.01484, + "32": 0.01623, + "64": 0.01687, + "128": 0.0176 + }, + "Pi": { + "16": 0.00558, + "32": 0.00559, + "64": 0.00561, + "128": 0.00589 + }, + "Sigma": { + "16": 0.00682, + "32": 0.00781, + "64": 0.00808, + "128": 0.00836 + }, + "Phi": { + "16": 0.00287, + "32": 0.00347, + "64": 0.00382, + "128": 0.00416 + }, + "Psi": { + "16": 0.00417, + "32": 0.00432, + "64": 0.00446, + "128": 0.00504 + }, + "Omega": { + "16": 0.00616, + "32": 0.00676, + "64": 0.0071, + "128": 0.00741 + }, + "alpha": { + "16": 0.00604, + "32": 0.00778, + "64": 0.00817, + "128": 0.00857 + }, + "beta": { + "16": 0.00604, + "32": 0.00689, + "64": 0.00749, + "128": 0.00761 + }, + "gamma": { + "16": 0.00556, + "32": 0.0064, + "64": 0.00705, + "128": 0.00718 + }, + "delta": { + "16": 0.00556, + "32": 0.0067, + "64": 0.00699, + "128": 0.00723 + }, + "epsilon": { + "16": 0.01102, + "32": 0.01414, + "64": 0.01472, + "128": 0.01524 + }, + "zeta": { + "16": 0.00658, + "32": 0.00829, + "64": 0.00916, + "128": 0.00951 + }, + "eta": { + "16": 0.00594, + "32": 0.00631, + "64": 0.00655, + "128": 0.00667 + }, + "theta": { + "16": 0.00407, + "32": 0.0045, + "64": 0.00463, + "128": 0.00475 + }, + "iota": { + "16": 0.00358, + "32": 0.00457, + "64": 0.00521, + "128": 0.00541 + }, + "kappa": { + "16": 0.00423, + "32": 0.00546, + "64": 0.00578, + "128": 0.00611 + }, + "lambda": { + "16": 0.00776, + "32": 0.00887, + "64": 0.00939, + "128": 0.00961 + }, + "mu": { + "16": 0.00425, + "32": 0.00494, + "64": 0.00533, + "128": 0.00559 + }, + "nu": { + "16": 0.00568, + "32": 0.00611, + "64": 0.00703, + "128": 0.00745 + }, + "xi": { + "16": 0.00953, + "32": 0.01191, + "64": 0.01285, + "128": 0.01346 + }, + "pi": { + "16": 0.00574, + "32": 0.00625, + "64": 0.00649, + "128": 0.00669 + }, + "rho": { + "16": 0.00526, + "32": 0.00573, + "64": 0.00588, + "128": 0.0061 + }, + "sigmafinal": { + "16": 0.00844, + "32": 0.00942, + "64": 0.01069, + "128": 0.0111 + }, + "sigma": { + "16": 0.00473, + "32": 0.00537, + "64": 0.00587, + "128": 0.006 + }, + "tau": { + "16": 0.00518, + "32": 0.00633, + "64": 0.00669, + "128": 0.00686 + }, + "upsilon": { + "16": 0.00602, + "32": 0.00702, + "64": 0.00742, + "128": 0.00767 + }, + "phi": { + "16": 0.00301, + "32": 0.0035, + "64": 0.00367, + "128": 0.0038 + }, + "chi": { + "16": 0.00596, + "32": 0.00663, + "64": 0.00717, + "128": 0.00732 + }, + "psi": { + "16": 0.00388, + "32": 0.00426, + "64": 0.0046, + "128": 0.00481 + }, + "omega": { + "16": 0.0074, + "32": 0.00874, + "64": 0.0094, + "128": 0.00991 + }, + "uni20B9": { + "16": 0.01147, + "32": 0.01219, + "64": 0.01281, + "128": 0.01366 + }, + "minus": { + "16": 0.01652, + "32": 0.01654, + "64": 0.01671, + "128": 0.01673 + }, + "multiply": { + "16": 0.00525, + "32": 0.00564, + "64": 0.00651, + "128": 0.00675 + }, + "divide": { + "16": 0.01578, + "32": 0.01972, + "64": 0.02009, + "128": 0.02016 + }, + "plusminus": { + "16": 0.01293, + "32": 0.01312, + "64": 0.01357, + "128": 0.01374 + }, + "notequal": { + "16": 0.01198, + "32": 0.01254, + "64": 0.01289, + "128": 0.01305 + }, + "lessequal": { + "16": 0.00976, + "32": 0.01007, + "64": 0.01026, + "128": 0.01033 + }, + "greaterequal": { + "16": 0.01053, + "32": 0.01098, + "64": 0.01131, + "128": 0.0114 + }, + "summation": { + "16": 0.00893, + "32": 0.0096, + "64": 0.0101, + "128": 0.01032 + }, + "product": { + "16": 0.00583, + "32": 0.00593, + "64": 0.00608, + "128": 0.00617 + }, + "integral": { + "16": 0.00354, + "32": 0.0038, + "64": 0.00403, + "128": 0.00414 + }, + "radical": { + "16": 0.0106, + "32": 0.01152, + "64": 0.01198, + "128": 0.01219 + }, + "infinity": { + "16": 0.00623, + "32": 0.00731, + "64": 0.00753, + "128": 0.00766 + }, + "partialdiff": { + "16": 0.00719, + "32": 0.00802, + "64": 0.00867, + "128": 0.00889 + }, + "nabla": { + "16": 0.0088, + "32": 0.0099, + "64": 0.01036, + "128": 0.01047 + }, + "minute": { + "16": 0.00773, + "32": 0.01088, + "64": 0.01111, + "128": 0.0116 + }, + "second": { + "16": 0.00808, + "32": 0.0107, + "64": 0.01095, + "128": 0.01139 + }, + "ringoperator": { + "16": 0.0124, + "32": 0.01382, + "64": 0.0155, + "128": 0.01573 + }, + "arrowright": { + "16": 0.01244, + "32": 0.01275, + "64": 0.01297, + "128": 0.01336 + }, + "arrowleft": { + "16": 0.01294, + "32": 0.01319, + "64": 0.01365, + "128": 0.014 + }, + "arrowboth": { + "16": 0.01095, + "32": 0.01167, + "64": 0.01235, + "128": 0.01283 + }, + "element": { + "16": 0.01194, + "32": 0.0123, + "64": 0.01249, + "128": 0.01262 + }, + "notelement": { + "16": 0.0092, + "32": 0.00985, + "64": 0.01019, + "128": 0.01032 + }, + "propersubset": { + "16": 0.01157, + "32": 0.01211, + "64": 0.01236, + "128": 0.01257 + }, + "reflexsubset": { + "16": 0.01323, + "32": 0.01354, + "64": 0.01385, + "128": 0.01415 + }, + "union": { + "16": 0.00806, + "32": 0.00887, + "64": 0.00902, + "128": 0.00918 + }, + "intersection": { + "16": 0.00907, + "32": 0.00951, + "64": 0.00985, + "128": 0.0101 + }, + "universal": { + "16": 0.01083, + "32": 0.01149, + "64": 0.01172, + "128": 0.01184 + }, + "existential": { + "16": 0.00959, + "32": 0.00995, + "64": 0.01003, + "128": 0.0101 + }, + "emptyset": { + "16": 0.00525, + "32": 0.00559, + "64": 0.00584, + "128": 0.006 + }, + "approxequal": { + "16": 0.01249, + "32": 0.0135, + "64": 0.01421, + "128": 0.01452 + }, + "equivalence": { + "16": 0.01679, + "32": 0.01682, + "64": 0.01697, + "128": 0.01705 + }, + "proportional": { + "16": 0.00784, + "32": 0.00829, + "64": 0.00869, + "128": 0.00899 + }, + "fraction": { + "16": 0.01068, + "32": 0.01142, + "64": 0.0116, + "128": 0.01171 + }, + "onesuperior": { + "16": 0.00509, + "32": 0.00511, + "64": 0.00522, + "128": 0.00565 + }, + "twosuperior": { + "16": 0.00762, + "32": 0.00963, + "64": 0.0105, + "128": 0.01112 + }, + "threesuperior": { + "16": 0.01107, + "32": 0.01411, + "64": 0.01502, + "128": 0.01573 + }, + "ordfeminine": { + "16": 0.00882, + "32": 0.01081, + "64": 0.01193, + "128": 0.01271 + }, + "ordmasculine": { + "16": 0.00711, + "32": 0.00912, + "64": 0.00988, + "128": 0.01003 + }, + "dottedcircle": { + "16": 0.01125, + "32": 0.0194, + "64": 0.02577, + "128": 0.02786 + }, + "gravecomb": { + "16": 0.01245, + "32": 0.01623, + "64": 0.01815, + "128": 0.01833 + }, + "gravecomb.case": { + "16": 0.00719, + "32": 0.01054, + "64": 0.01118, + "128": 0.01223 + }, + "acutecomb": { + "16": 0.00999, + "32": 0.01665, + "64": 0.01748, + "128": 0.01757 + }, + "acutecomb.case": { + "16": 0.01179, + "32": 0.01476, + "64": 0.01584, + "128": 0.01655 + }, + "circumflexcomb": { + "16": 0.01348, + "32": 0.01662, + "64": 0.01847, + "128": 0.01908 + }, + "circumflexcomb.case": { + "16": 0.01444, + "32": 0.0162, + "64": 0.01784, + "128": 0.01845 + }, + "tildecomb": { + "16": 0.01164, + "32": 0.01788, + "64": 0.01867, + "128": 0.01983 + }, + "tildecomb.case": { + "16": 0.00966, + "32": 0.01494, + "64": 0.02056, + "128": 0.02155 + }, + "macroncomb": { + "16": 0.0147, + "32": 0.01523, + "64": 0.01556, + "128": 0.01638 + }, + "dieresiscomb": { + "16": 0.01233, + "32": 0.01266, + "64": 0.01302, + "128": 0.01327 + }, + "dieresiscomb.case": { + "16": 0.00734, + "32": 0.00998, + "64": 0.01017, + "128": 0.01024 + }, + "ringcomb": { + "16": 0.00736, + "32": 0.01025, + "64": 0.01105, + "128": 0.01154 + }, + "ringcomb.case": { + "16": 0.00806, + "32": 0.01141, + "64": 0.01207, + "128": 0.0127 + }, + "caroncomb": { + "16": 0.01092, + "32": 0.01379, + "64": 0.01529, + "128": 0.01588 + }, + "dotaccentcomb": { + "16": 0.0107, + "32": 0.01155, + "64": 0.01158, + "128": 0.01191 + }, + "cedillacomb": { + "16": 0.00853, + "32": 0.01031, + "64": 0.01197, + "128": 0.01257 + }, + "zero.lf": { + "16": 0.00377, + "32": 0.00415, + "64": 0.00434, + "128": 0.00451 + }, + "one.lf": { + "16": 0.00469, + "32": 0.00507, + "64": 0.00547, + "128": 0.00589 + }, + "two.lf": { + "16": 0.00671, + "32": 0.00839, + "64": 0.00881, + "128": 0.00908 + }, + "three.lf": { + "16": 0.00642, + "32": 0.00757, + "64": 0.00838, + "128": 0.00874 + }, + "four.lf": { + "16": 0.00619, + "32": 0.00638, + "64": 0.00681, + "128": 0.00728 + }, + "five.lf": { + "16": 0.00647, + "32": 0.00818, + "64": 0.00881, + "128": 0.00908 + }, + "six.lf": { + "16": 0.00542, + "32": 0.00626, + "64": 0.00671, + "128": 0.00689 + }, + "seven.lf": { + "16": 0.00445, + "32": 0.00504, + "64": 0.0056, + "128": 0.00586 + }, + "eight.lf": { + "16": 0.00511, + "32": 0.00617, + "64": 0.00634, + "128": 0.00662 + }, + "nine.lf": { + "16": 0.00506, + "32": 0.0062, + "64": 0.00666, + "128": 0.00682 + }, + "zero.osf": { + "16": 0.00555, + "32": 0.00605, + "64": 0.00619, + "128": 0.00638 + }, + "one.osf": { + "16": 0.01118, + "32": 0.01181, + "64": 0.01231, + "128": 0.01274 + }, + "two.osf": { + "16": 0.0088, + "32": 0.01039, + "64": 0.01162, + "128": 0.01205 + }, + "three.osf": { + "16": 0.00967, + "32": 0.01115, + "64": 0.01199, + "128": 0.0123 + }, + "four.osf": { + "16": 0.00665, + "32": 0.00863, + "64": 0.00884, + "128": 0.00914 + }, + "five.osf": { + "16": 0.00852, + "32": 0.01005, + "64": 0.01053, + "128": 0.01087 + }, + "six.osf": { + "16": 0.00692, + "32": 0.00778, + "64": 0.00806, + "128": 0.0082 + }, + "seven.osf": { + "16": 0.00604, + "32": 0.00628, + "64": 0.00655, + "128": 0.00674 + }, + "eight.osf": { + "16": 0.00629, + "32": 0.00721, + "64": 0.00756, + "128": 0.00769 + }, + "nine.osf": { + "16": 0.00752, + "32": 0.0082, + "64": 0.0086, + "128": 0.00882 + }, + "zero.tosf": { + "16": 0.00406, + "32": 0.00509, + "64": 0.00539, + "128": 0.00556 + }, + "one.tosf": { + "16": 0.00763, + "32": 0.00833, + "64": 0.00859, + "128": 0.00897 + }, + "two.tosf": { + "16": 0.00862, + "32": 0.01014, + "64": 0.01149, + "128": 0.0119 + }, + "three.tosf": { + "16": 0.00843, + "32": 0.01086, + "64": 0.01183, + "128": 0.01219 + }, + "four.tosf": { + "16": 0.00653, + "32": 0.00859, + "64": 0.0088, + "128": 0.00916 + }, + "five.tosf": { + "16": 0.00914, + "32": 0.01034, + "64": 0.01092, + "128": 0.01111 + }, + "six.tosf": { + "16": 0.0085, + "32": 0.00892, + "64": 0.00915, + "128": 0.00931 + }, + "seven.tosf": { + "16": 0.00608, + "32": 0.00652, + "64": 0.00686, + "128": 0.00708 + }, + "eight.tosf": { + "16": 0.0054, + "32": 0.00655, + "64": 0.00694, + "128": 0.00708 + }, + "nine.tosf": { + "16": 0.00672, + "32": 0.00786, + "64": 0.00821, + "128": 0.0084 + }, + "two.dnom": { + "16": 0.00761, + "32": 0.00962, + "64": 0.01052, + "128": 0.01113 + }, + "four.dnom": { + "16": 0.00694, + "32": 0.00848, + "64": 0.00918, + "128": 0.00987 + }, + "a.sc": { + "16": 0.00701, + "32": 0.01028, + "64": 0.01139, + "128": 0.01168 + }, + "b.sc": { + "16": 0.0056, + "32": 0.00662, + "64": 0.0072, + "128": 0.0075 + }, + "c.sc": { + "16": 0.008, + "32": 0.00894, + "64": 0.00971, + "128": 0.00996 + }, + "d.sc": { + "16": 0.00459, + "32": 0.00542, + "64": 0.00594, + "128": 0.00619 + }, + "e.sc": { + "16": 0.00407, + "32": 0.00704, + "64": 0.00749, + "128": 0.00808 + }, + "f.sc": { + "16": 0.00357, + "32": 0.0068, + "64": 0.00731, + "128": 0.00767 + }, + "g.sc": { + "16": 0.00608, + "32": 0.00861, + "64": 0.00922, + "128": 0.00952 + }, + "h.sc": { + "16": 0.00411, + "32": 0.00479, + "64": 0.00521, + "128": 0.00563 + }, + "i.sc": { + "16": 0.0048, + "32": 0.0054, + "64": 0.0058, + "128": 0.00624 + }, + "j.sc": { + "16": 0.00532, + "32": 0.00608, + "64": 0.00675, + "128": 0.00716 + }, + "k.sc": { + "16": 0.00553, + "32": 0.00674, + "64": 0.00745, + "128": 0.00801 + }, + "l.sc": { + "16": 0.00502, + "32": 0.00551, + "64": 0.00585, + "128": 0.00619 + }, + "m.sc": { + "16": 0.00689, + "32": 0.00858, + "64": 0.00939, + "128": 0.00969 + }, + "n.sc": { + "16": 0.00763, + "32": 0.00924, + "64": 0.00993, + "128": 0.01005 + }, + "o.sc": { + "16": 0.00499, + "32": 0.00616, + "64": 0.00656, + "128": 0.00669 + }, + "p.sc": { + "16": 0.00413, + "32": 0.00503, + "64": 0.00576, + "128": 0.00609 + }, + "q.sc": { + "16": 0.00562, + "32": 0.00688, + "64": 0.0073, + "128": 0.00755 + }, + "r.sc": { + "16": 0.00534, + "32": 0.00676, + "64": 0.00761, + "128": 0.00802 + }, + "s.sc": { + "16": 0.00808, + "32": 0.01053, + "64": 0.01148, + "128": 0.01181 + }, + "t.sc": { + "16": 0.00482, + "32": 0.00829, + "64": 0.0087, + "128": 0.00882 + }, + "u.sc": { + "16": 0.00515, + "32": 0.00713, + "64": 0.00788, + "128": 0.00842 + }, + "v.sc": { + "16": 0.00602, + "32": 0.00709, + "64": 0.00739, + "128": 0.00777 + }, + "w.sc": { + "16": 0.00702, + "32": 0.00767, + "64": 0.00829, + "128": 0.00862 + }, + "x.sc": { + "16": 0.00661, + "32": 0.008, + "64": 0.00877, + "128": 0.00938 + }, + "y.sc": { + "16": 0.0047, + "32": 0.00605, + "64": 0.00683, + "128": 0.00722 + }, + "z.sc": { + "16": 0.0031, + "32": 0.00611, + "64": 0.00622, + "128": 0.00632 + }, + "ae.sc": { + "16": 0.00572, + "32": 0.00839, + "64": 0.00915, + "128": 0.00942 + }, + "oe.sc": { + "16": 0.00567, + "32": 0.00796, + "64": 0.00844, + "128": 0.00864 + }, + "oslash.sc": { + "16": 0.00635, + "32": 0.00741, + "64": 0.00771, + "128": 0.00796 + }, + "eth.sc": { + "16": 0.00596, + "32": 0.00699, + "64": 0.00762, + "128": 0.00797 + }, + "thorn.sc": { + "16": 0.00583, + "32": 0.00691, + "64": 0.00747, + "128": 0.00788 + }, + "gamma.sc": { + "16": 0.00387, + "32": 0.00739, + "64": 0.0079, + "128": 0.00823 + }, + "delta.sc": { + "16": 0.00459, + "32": 0.00489, + "64": 0.00508, + "128": 0.00524 + }, + "theta.sc": { + "16": 0.00582, + "32": 0.00749, + "64": 0.00842, + "128": 0.00853 + }, + "lambda.sc": { + "16": 0.00762, + "32": 0.00818, + "64": 0.00859, + "128": 0.00903 + }, + "xi.sc": { + "16": 0.00628, + "32": 0.0126, + "64": 0.0137, + "128": 0.01418 + }, + "pi.sc": { + "16": 0.00408, + "32": 0.00453, + "64": 0.00678, + "128": 0.00707 + }, + "sigma.sc": { + "16": 0.00571, + "32": 0.00998, + "64": 0.01082, + "128": 0.01108 + }, + "phi.sc": { + "16": 0.0044, + "32": 0.00546, + "64": 0.00593, + "128": 0.00624 + }, + "psi.sc": { + "16": 0.00549, + "32": 0.00782, + "64": 0.00823, + "128": 0.00882 + }, + "omega.sc": { + "16": 0.0054, + "32": 0.00634, + "64": 0.0067, + "128": 0.00704 + } + } + }, + "Display": { + "font": "MachineMadeWarpDisplay-Regular.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.03669, + "max_glyph": "xi.sc", + "mean": 0.01242, + "p95": 0.02197, + "pass": true + }, + "32": { + "max": 0.04227, + "max_glyph": "xi.sc", + "mean": 0.0145, + "p95": 0.02476, + "pass": true + }, + "64": { + "max": 0.0456, + "max_glyph": "xi.sc", + "mean": 0.01589, + "p95": 0.02721, + "pass": true + }, + "128": { + "max": 0.04721, + "max_glyph": "xi.sc", + "mean": 0.01677, + "p95": 0.02872, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.01001, + "32": 0.01346, + "64": 0.01425, + "128": 0.01494 + }, + "quotedbl": { + "16": 0.01461, + "32": 0.01821, + "64": 0.0194, + "128": 0.0201 + }, + "numbersign": { + "16": 0.01024, + "32": 0.0122, + "64": 0.01644, + "128": 0.01708 + }, + "dollar": { + "16": 0.01917, + "32": 0.02157, + "64": 0.02323, + "128": 0.02389 + }, + "percent": { + "16": 0.01025, + "32": 0.01169, + "64": 0.01245, + "128": 0.01277 + }, + "ampersand": { + "16": 0.01171, + "32": 0.01439, + "64": 0.01556, + "128": 0.01612 + }, + "quotesingle": { + "16": 0.01569, + "32": 0.0163, + "64": 0.01815, + "128": 0.01852 + }, + "parenleft": { + "16": 0.00979, + "32": 0.01121, + "64": 0.01194, + "128": 0.01245 + }, + "parenright": { + "16": 0.01295, + "32": 0.01386, + "64": 0.01474, + "128": 0.01505 + }, + "asterisk": { + "16": 0.0143, + "32": 0.01902, + "64": 0.02258, + "128": 0.02456 + }, + "plus": { + "16": 0.02442, + "32": 0.02604, + "64": 0.02657, + "128": 0.02724 + }, + "comma": { + "16": 0.00733, + "32": 0.00974, + "64": 0.011, + "128": 0.0119 + }, + "hyphen": { + "16": 0.01122, + "32": 0.01642, + "64": 0.01648, + "128": 0.01648 + }, + "period": { + "16": 0.0094, + "32": 0.01089, + "64": 0.01684, + "128": 0.01777 + }, + "slash": { + "16": 0.00424, + "32": 0.00536, + "64": 0.00593, + "128": 0.00618 + }, + "zero": { + "16": 0.0074, + "32": 0.00953, + "64": 0.00995, + "128": 0.01015 + }, + "one": { + "16": 0.00424, + "32": 0.00487, + "64": 0.00511, + "128": 0.00548 + }, + "two": { + "16": 0.00939, + "32": 0.01319, + "64": 0.0146, + "128": 0.01512 + }, + "three": { + "16": 0.01463, + "32": 0.0167, + "64": 0.01851, + "128": 0.01946 + }, + "four": { + "16": 0.00816, + "32": 0.00898, + "64": 0.00925, + "128": 0.01477 + }, + "five": { + "16": 0.01183, + "32": 0.01287, + "64": 0.01414, + "128": 0.01477 + }, + "six": { + "16": 0.01022, + "32": 0.01255, + "64": 0.01327, + "128": 0.01379 + }, + "seven": { + "16": 0.00803, + "32": 0.00907, + "64": 0.00949, + "128": 0.0103 + }, + "eight": { + "16": 0.00947, + "32": 0.01214, + "64": 0.01252, + "128": 0.0128 + }, + "nine": { + "16": 0.01001, + "32": 0.01285, + "64": 0.01387, + "128": 0.01421 + }, + "colon": { + "16": 0.01141, + "32": 0.01842, + "64": 0.02264, + "128": 0.02338 + }, + "semicolon": { + "16": 0.01029, + "32": 0.0155, + "64": 0.01622, + "128": 0.01734 + }, + "less": { + "16": 0.02242, + "32": 0.02318, + "64": 0.02335, + "128": 0.02361 + }, + "equal": { + "16": 0.02065, + "32": 0.02075, + "64": 0.0212, + "128": 0.02195 + }, + "greater": { + "16": 0.0143, + "32": 0.01483, + "64": 0.01534, + "128": 0.01557 + }, + "question": { + "16": 0.01161, + "32": 0.01547, + "64": 0.01722, + "128": 0.01764 + }, + "at": { + "16": 0.0131, + "32": 0.01539, + "64": 0.01706, + "128": 0.01789 + }, + "A": { + "16": 0.0136, + "32": 0.01704, + "64": 0.01836, + "128": 0.01914 + }, + "B": { + "16": 0.01537, + "32": 0.0165, + "64": 0.01695, + "128": 0.01784 + }, + "C": { + "16": 0.01711, + "32": 0.01797, + "64": 0.02054, + "128": 0.02088 + }, + "D": { + "16": 0.01207, + "32": 0.01297, + "64": 0.0134, + "128": 0.01404 + }, + "E": { + "16": 0.01738, + "32": 0.01784, + "64": 0.01802, + "128": 0.01824 + }, + "F": { + "16": 0.01602, + "32": 0.01645, + "64": 0.0168, + "128": 0.017 + }, + "G": { + "16": 0.01056, + "32": 0.01199, + "64": 0.0139, + "128": 0.01443 + }, + "H": { + "16": 0.01187, + "32": 0.01302, + "64": 0.01356, + "128": 0.01379 + }, + "I": { + "16": 0.01445, + "32": 0.01459, + "64": 0.01482, + "128": 0.01511 + }, + "J": { + "16": 0.01459, + "32": 0.01511, + "64": 0.01569, + "128": 0.01583 + }, + "K": { + "16": 0.01507, + "32": 0.01584, + "64": 0.0171, + "128": 0.01848 + }, + "L": { + "16": 0.01804, + "32": 0.01902, + "64": 0.01927, + "128": 0.01951 + }, + "M": { + "16": 0.0185, + "32": 0.01958, + "64": 0.0201, + "128": 0.02055 + }, + "N": { + "16": 0.01895, + "32": 0.01923, + "64": 0.01987, + "128": 0.02095 + }, + "O": { + "16": 0.0076, + "32": 0.00994, + "64": 0.01065, + "128": 0.01102 + }, + "P": { + "16": 0.01391, + "32": 0.01475, + "64": 0.01519, + "128": 0.01585 + }, + "Q": { + "16": 0.00772, + "32": 0.00865, + "64": 0.01019, + "128": 0.01072 + }, + "R": { + "16": 0.01273, + "32": 0.01386, + "64": 0.01439, + "128": 0.0152 + }, + "S": { + "16": 0.01853, + "32": 0.02068, + "64": 0.02365, + "128": 0.02438 + }, + "T": { + "16": 0.00742, + "32": 0.00759, + "64": 0.00775, + "128": 0.0078 + }, + "U": { + "16": 0.01352, + "32": 0.01432, + "64": 0.01642, + "128": 0.01745 + }, + "V": { + "16": 0.03304, + "32": 0.03486, + "64": 0.0374, + "128": 0.03957 + }, + "W": { + "16": 0.0144, + "32": 0.01611, + "64": 0.01779, + "128": 0.01915 + }, + "X": { + "16": 0.01077, + "32": 0.0123, + "64": 0.01287, + "128": 0.01344 + }, + "Y": { + "16": 0.0062, + "32": 0.00787, + "64": 0.00918, + "128": 0.00992 + }, + "Z": { + "16": 0.01368, + "32": 0.01374, + "64": 0.01389, + "128": 0.01394 + }, + "bracketleft": { + "16": 0.00225, + "32": 0.00225, + "64": 0.00225, + "128": 0.00239 + }, + "backslash": { + "16": 0.03175, + "32": 0.03256, + "64": 0.03272, + "128": 0.03306 + }, + "bracketright": { + "16": 0.00796, + "32": 0.00833, + "64": 0.00876, + "128": 0.00936 + }, + "asciicircum": { + "16": 0.0112, + "32": 0.0117, + "64": 0.01178, + "128": 0.01194 + }, + "underscore": { + "16": 0.03346, + "32": 0.03346, + "64": 0.03414, + "128": 0.03631 + }, + "grave": { + "16": 0.01872, + "32": 0.02033, + "64": 0.02152, + "128": 0.02342 + }, + "a": { + "16": 0.01161, + "32": 0.01471, + "64": 0.01654, + "128": 0.01779 + }, + "b": { + "16": 0.01035, + "32": 0.01173, + "64": 0.01214, + "128": 0.01241 + }, + "c": { + "16": 0.01414, + "32": 0.01558, + "64": 0.01696, + "128": 0.01938 + }, + "d": { + "16": 0.01098, + "32": 0.01147, + "64": 0.01242, + "128": 0.01276 + }, + "e": { + "16": 0.01209, + "32": 0.01469, + "64": 0.01641, + "128": 0.01708 + }, + "f": { + "16": 0.01486, + "32": 0.01554, + "64": 0.01634, + "128": 0.01674 + }, + "g": { + "16": 0.01419, + "32": 0.01712, + "64": 0.01856, + "128": 0.01916 + }, + "h": { + "16": 0.00898, + "32": 0.0098, + "64": 0.00998, + "128": 0.01028 + }, + "i": { + "16": 0.00778, + "32": 0.00855, + "64": 0.00915, + "128": 0.00953 + }, + "j": { + "16": 0.00921, + "32": 0.00932, + "64": 0.00996, + "128": 0.01016 + }, + "k": { + "16": 0.00885, + "32": 0.00927, + "64": 0.00989, + "128": 0.01121 + }, + "l": { + "16": 0.00677, + "32": 0.00684, + "64": 0.00694, + "128": 0.00702 + }, + "m": { + "16": 0.00923, + "32": 0.01069, + "64": 0.01131, + "128": 0.0117 + }, + "n": { + "16": 0.00968, + "32": 0.01191, + "64": 0.01229, + "128": 0.01265 + }, + "o": { + "16": 0.00956, + "32": 0.01015, + "64": 0.01139, + "128": 0.01168 + }, + "p": { + "16": 0.01019, + "32": 0.01178, + "64": 0.01216, + "128": 0.01244 + }, + "q": { + "16": 0.011, + "32": 0.01219, + "64": 0.0131, + "128": 0.01337 + }, + "r": { + "16": 0.00881, + "32": 0.00973, + "64": 0.01073, + "128": 0.01152 + }, + "s": { + "16": 0.01455, + "32": 0.01765, + "64": 0.02016, + "128": 0.02092 + }, + "t": { + "16": 0.00766, + "32": 0.00915, + "64": 0.01066, + "128": 0.01116 + }, + "u": { + "16": 0.0098, + "32": 0.01132, + "64": 0.01276, + "128": 0.01298 + }, + "v": { + "16": 0.00771, + "32": 0.0089, + "64": 0.01069, + "128": 0.01224 + }, + "w": { + "16": 0.01165, + "32": 0.01319, + "64": 0.01371, + "128": 0.01416 + }, + "x": { + "16": 0.01527, + "32": 0.01684, + "64": 0.01781, + "128": 0.01875 + }, + "y": { + "16": 0.01425, + "32": 0.01608, + "64": 0.01774, + "128": 0.01856 + }, + "z": { + "16": 0.01377, + "32": 0.01581, + "64": 0.01844, + "128": 0.02499 + }, + "braceleft": { + "16": 0.00668, + "32": 0.0093, + "64": 0.01038, + "128": 0.01084 + }, + "bar": { + "16": 0.03209, + "32": 0.03209, + "64": 0.03209, + "128": 0.03274 + }, + "braceright": { + "16": 0.01296, + "32": 0.01632, + "64": 0.0172, + "128": 0.01785 + }, + "asciitilde": { + "16": 0.01235, + "32": 0.01538, + "64": 0.01632, + "128": 0.01734 + }, + "quoteleft": { + "16": 0.01577, + "32": 0.01718, + "64": 0.01737, + "128": 0.01833 + }, + "quoteright": { + "16": 0.00586, + "32": 0.00768, + "64": 0.01031, + "128": 0.01143 + }, + "quotedblleft": { + "16": 0.01141, + "32": 0.0132, + "64": 0.01491, + "128": 0.01554 + }, + "quotedblright": { + "16": 0.00623, + "32": 0.0092, + "64": 0.01139, + "128": 0.01236 + }, + "quotesinglbase": { + "16": 0.00733, + "32": 0.00974, + "64": 0.011, + "128": 0.0119 + }, + "quotedblbase": { + "16": 0.00862, + "32": 0.01105, + "64": 0.01317, + "128": 0.01386 + }, + "guilsinglleft": { + "16": 0.0287, + "32": 0.03385, + "64": 0.03464, + "128": 0.03587 + }, + "guilsinglright": { + "16": 0.01726, + "32": 0.02149, + "64": 0.0259, + "128": 0.0265 + }, + "guillemotleft": { + "16": 0.02245, + "32": 0.02805, + "64": 0.03083, + "128": 0.03221 + }, + "guillemotright": { + "16": 0.01661, + "32": 0.01984, + "64": 0.02377, + "128": 0.02556 + }, + "ellipsis": { + "16": 0.01315, + "32": 0.01365, + "64": 0.01703, + "128": 0.01774 + }, + "bullet": { + "16": 0.00696, + "32": 0.01028, + "64": 0.01067, + "128": 0.01106 + }, + "periodcentered": { + "16": 0.01597, + "32": 0.02072, + "64": 0.0243, + "128": 0.02462 + }, + "Euro": { + "16": 0.01879, + "32": 0.02104, + "64": 0.02263, + "128": 0.02333 + }, + "copyright": { + "16": 0.01965, + "32": 0.0225, + "64": 0.02517, + "128": 0.02617 + }, + "registered": { + "16": 0.01446, + "32": 0.01834, + "64": 0.02094, + "128": 0.02185 + }, + "degree": { + "16": 0.01609, + "32": 0.01785, + "64": 0.02135, + "128": 0.02225 + }, + "emdash": { + "16": 0.00146, + "32": 0.00146, + "64": 0.00146, + "128": 0.00146 + }, + "endash": { + "16": 0.00363, + "32": 0.00363, + "64": 0.00363, + "128": 0.00363 + }, + "exclamdown": { + "16": 0.01068, + "32": 0.01471, + "64": 0.01547, + "128": 0.01614 + }, + "questiondown": { + "16": 0.01082, + "32": 0.01483, + "64": 0.01798, + "128": 0.01862 + }, + "section": { + "16": 0.01128, + "32": 0.01471, + "64": 0.01698, + "128": 0.01818 + }, + "paragraph": { + "16": 0.00284, + "32": 0.00301, + "64": 0.00324, + "128": 0.00328 + }, + "cent": { + "16": 0.01038, + "32": 0.0121, + "64": 0.01501, + "128": 0.01666 + }, + "sterling": { + "16": 0.0091, + "32": 0.0108, + "64": 0.01248, + "128": 0.01296 + }, + "yen": { + "16": 0.01316, + "32": 0.01391, + "64": 0.01512, + "128": 0.02038 + }, + "brokenbar": { + "16": 0.03179, + "32": 0.03179, + "64": 0.03179, + "128": 0.03355 + }, + "logicalnot": { + "16": 0.01201, + "32": 0.01275, + "64": 0.01335, + "128": 0.01454 + }, + "dieresis": { + "16": 0.01457, + "32": 0.02081, + "64": 0.02215, + "128": 0.0227 + }, + "acute": { + "16": 0.01477, + "32": 0.01865, + "64": 0.0196, + "128": 0.02127 + }, + "macron": { + "16": 0.00364, + "32": 0.00364, + "64": 0.00364, + "128": 0.00364 + }, + "cedilla": { + "16": 0.01105, + "32": 0.01905, + "64": 0.02295, + "128": 0.02696 + }, + "circumflex": { + "16": 0.01673, + "32": 0.02372, + "64": 0.02689, + "128": 0.02866 + }, + "tilde": { + "16": 0.01735, + "32": 0.02079, + "64": 0.02514, + "128": 0.02744 + }, + "ring": { + "16": 0.00841, + "32": 0.01746, + "64": 0.02018, + "128": 0.02097 + }, + "caron": { + "16": 0.01333, + "32": 0.01935, + "64": 0.02462, + "128": 0.02704 + }, + "dotaccent": { + "16": 0.00642, + "32": 0.01423, + "64": 0.01793, + "128": 0.01982 + }, + "AE": { + "16": 0.01655, + "32": 0.01732, + "64": 0.01878, + "128": 0.0198 + }, + "Eth": { + "16": 0.00497, + "32": 0.0058, + "64": 0.00625, + "128": 0.0069 + }, + "Oslash": { + "16": 0.00827, + "32": 0.01019, + "64": 0.0114, + "128": 0.01175 + }, + "Thorn": { + "16": 0.01315, + "32": 0.01386, + "64": 0.01436, + "128": 0.01492 + }, + "germandbls": { + "16": 0.00997, + "32": 0.01165, + "64": 0.01226, + "128": 0.01269 + }, + "ae": { + "16": 0.01334, + "32": 0.01617, + "64": 0.01794, + "128": 0.01869 + }, + "eth": { + "16": 0.01309, + "32": 0.01395, + "64": 0.01507, + "128": 0.01566 + }, + "oslash": { + "16": 0.01065, + "32": 0.01226, + "64": 0.01322, + "128": 0.01367 + }, + "thorn": { + "16": 0.00958, + "32": 0.01047, + "64": 0.01076, + "128": 0.011 + }, + "OE": { + "16": 0.00968, + "32": 0.01003, + "64": 0.01123, + "128": 0.01176 + }, + "oe": { + "16": 0.01401, + "32": 0.01506, + "64": 0.0165, + "128": 0.01693 + }, + "dotlessi": { + "16": 0.0069, + "32": 0.00711, + "64": 0.00764, + "128": 0.00782 + }, + "dotlessj": { + "16": 0.00837, + "32": 0.00879, + "64": 0.00903, + "128": 0.0092 + }, + "f_f": { + "16": 0.01158, + "32": 0.01235, + "64": 0.01349, + "128": 0.01382 + }, + "fi": { + "16": 0.01386, + "32": 0.01436, + "64": 0.01505, + "128": 0.01543 + }, + "fl": { + "16": 0.01303, + "32": 0.01341, + "64": 0.01401, + "128": 0.01417 + }, + "f_f_i": { + "16": 0.01159, + "32": 0.01225, + "64": 0.01317, + "128": 0.01353 + }, + "f_f_l": { + "16": 0.01011, + "32": 0.01062, + "64": 0.01142, + "128": 0.01169 + }, + "Gamma": { + "16": 0.01493, + "32": 0.01545, + "64": 0.01553, + "128": 0.01563 + }, + "Delta": { + "16": 0.02161, + "32": 0.02312, + "64": 0.02342, + "128": 0.02373 + }, + "Theta": { + "16": 0.00996, + "32": 0.01255, + "64": 0.01353, + "128": 0.01417 + }, + "Lambda": { + "16": 0.01147, + "32": 0.01341, + "64": 0.01459, + "128": 0.01526 + }, + "Xi": { + "16": 0.03227, + "32": 0.03227, + "64": 0.03254, + "128": 0.03281 + }, + "Pi": { + "16": 0.01315, + "32": 0.01326, + "64": 0.01326, + "128": 0.01334 + }, + "Sigma": { + "16": 0.02877, + "32": 0.03068, + "64": 0.03139, + "128": 0.03159 + }, + "Phi": { + "16": 0.0065, + "32": 0.00889, + "64": 0.00973, + "128": 0.00992 + }, + "Psi": { + "16": 0.01318, + "32": 0.01563, + "64": 0.01739, + "128": 0.0187 + }, + "Omega": { + "16": 0.00989, + "32": 0.01155, + "64": 0.01253, + "128": 0.01293 + }, + "alpha": { + "16": 0.01572, + "32": 0.01876, + "64": 0.02117, + "128": 0.0222 + }, + "beta": { + "16": 0.00692, + "32": 0.00819, + "64": 0.00931, + "128": 0.00984 + }, + "gamma": { + "16": 0.00883, + "32": 0.0116, + "64": 0.01403, + "128": 0.01627 + }, + "delta": { + "16": 0.00958, + "32": 0.01092, + "64": 0.01235, + "128": 0.01337 + }, + "epsilon": { + "16": 0.01643, + "32": 0.01988, + "64": 0.02243, + "128": 0.02377 + }, + "zeta": { + "16": 0.01314, + "32": 0.01529, + "64": 0.01662, + "128": 0.01831 + }, + "eta": { + "16": 0.00885, + "32": 0.01027, + "64": 0.01131, + "128": 0.01164 + }, + "theta": { + "16": 0.0073, + "32": 0.00862, + "64": 0.00901, + "128": 0.00931 + }, + "iota": { + "16": 0.00571, + "32": 0.007, + "64": 0.00867, + "128": 0.00899 + }, + "kappa": { + "16": 0.00951, + "32": 0.01302, + "64": 0.01403, + "128": 0.01514 + }, + "lambda": { + "16": 0.01437, + "32": 0.01537, + "64": 0.01682, + "128": 0.0173 + }, + "mu": { + "16": 0.00517, + "32": 0.00587, + "64": 0.00693, + "128": 0.00711 + }, + "nu": { + "16": 0.00674, + "32": 0.00878, + "64": 0.01012, + "128": 0.01089 + }, + "xi": { + "16": 0.01158, + "32": 0.01476, + "64": 0.01629, + "128": 0.01819 + }, + "pi": { + "16": 0.00728, + "32": 0.00853, + "64": 0.00952, + "128": 0.01016 + }, + "rho": { + "16": 0.00779, + "32": 0.0096, + "64": 0.01059, + "128": 0.01094 + }, + "sigmafinal": { + "16": 0.01509, + "32": 0.01666, + "64": 0.01824, + "128": 0.01907 + }, + "sigma": { + "16": 0.0092, + "32": 0.01038, + "64": 0.01076, + "128": 0.01114 + }, + "tau": { + "16": 0.00976, + "32": 0.01065, + "64": 0.01164, + "128": 0.0119 + }, + "upsilon": { + "16": 0.01137, + "32": 0.01202, + "64": 0.01266, + "128": 0.01331 + }, + "phi": { + "16": 0.00832, + "32": 0.00876, + "64": 0.00913, + "128": 0.00925 + }, + "chi": { + "16": 0.00884, + "32": 0.00969, + "64": 0.01069, + "128": 0.0112 + }, + "psi": { + "16": 0.00663, + "32": 0.00731, + "64": 0.00786, + "128": 0.00815 + }, + "omega": { + "16": 0.0156, + "32": 0.01886, + "64": 0.02072, + "128": 0.02142 + }, + "uni20B9": { + "16": 0.01098, + "32": 0.0127, + "64": 0.01434, + "128": 0.0154 + }, + "minus": { + "16": 0.01416, + "32": 0.01416, + "64": 0.01461, + "128": 0.01526 + }, + "multiply": { + "16": 0.00645, + "32": 0.0083, + "64": 0.00844, + "128": 0.00847 + }, + "divide": { + "16": 0.02519, + "32": 0.02711, + "64": 0.02933, + "128": 0.02957 + }, + "plusminus": { + "16": 0.02142, + "32": 0.02198, + "64": 0.02269, + "128": 0.02335 + }, + "notequal": { + "16": 0.02026, + "32": 0.0215, + "64": 0.02273, + "128": 0.02314 + }, + "lessequal": { + "16": 0.01464, + "32": 0.0149, + "64": 0.01521, + "128": 0.01552 + }, + "greaterequal": { + "16": 0.01181, + "32": 0.01229, + "64": 0.01234, + "128": 0.01265 + }, + "summation": { + "16": 0.02705, + "32": 0.02739, + "64": 0.02761, + "128": 0.02797 + }, + "product": { + "16": 0.0089, + "32": 0.00904, + "64": 0.00931, + "128": 0.00936 + }, + "integral": { + "16": 0.00524, + "32": 0.00603, + "64": 0.00657, + "128": 0.00681 + }, + "radical": { + "16": 0.01616, + "32": 0.01674, + "64": 0.01768, + "128": 0.01857 + }, + "infinity": { + "16": 0.01137, + "32": 0.01342, + "64": 0.01483, + "128": 0.01559 + }, + "partialdiff": { + "16": 0.00916, + "32": 0.0113, + "64": 0.01258, + "128": 0.01335 + }, + "nabla": { + "16": 0.01601, + "32": 0.01739, + "64": 0.01776, + "128": 0.0178 + }, + "minute": { + "16": 0.00756, + "32": 0.01304, + "64": 0.01436, + "128": 0.01567 + }, + "second": { + "16": 0.0072, + "32": 0.01102, + "64": 0.01298, + "128": 0.01379 + }, + "ringoperator": { + "16": 0.01634, + "32": 0.01969, + "64": 0.02546, + "128": 0.0288 + }, + "arrowright": { + "16": 0.0194, + "32": 0.021, + "64": 0.02206, + "128": 0.02265 + }, + "arrowleft": { + "16": 0.02046, + "32": 0.02147, + "64": 0.0224, + "128": 0.02325 + }, + "arrowboth": { + "16": 0.01731, + "32": 0.01836, + "64": 0.01908, + "128": 0.02018 + }, + "element": { + "16": 0.02044, + "32": 0.02163, + "64": 0.02204, + "128": 0.02236 + }, + "notelement": { + "16": 0.01313, + "32": 0.0149, + "64": 0.01579, + "128": 0.01624 + }, + "propersubset": { + "16": 0.01654, + "32": 0.01929, + "64": 0.01979, + "128": 0.02016 + }, + "reflexsubset": { + "16": 0.01923, + "32": 0.02127, + "64": 0.02178, + "128": 0.02229 + }, + "union": { + "16": 0.01022, + "32": 0.01229, + "64": 0.01253, + "128": 0.01291 + }, + "intersection": { + "16": 0.0111, + "32": 0.01345, + "64": 0.01407, + "128": 0.01451 + }, + "universal": { + "16": 0.01392, + "32": 0.01529, + "64": 0.01642, + "128": 0.01709 + }, + "existential": { + "16": 0.0169, + "32": 0.01751, + "64": 0.01799, + "128": 0.01812 + }, + "emptyset": { + "16": 0.00851, + "32": 0.00964, + "64": 0.01021, + "128": 0.01044 + }, + "approxequal": { + "16": 0.01443, + "32": 0.01813, + "64": 0.01915, + "128": 0.02011 + }, + "equivalence": { + "16": 0.02276, + "32": 0.02281, + "64": 0.02357, + "128": 0.02413 + }, + "proportional": { + "16": 0.01095, + "32": 0.01345, + "64": 0.01405, + "128": 0.01455 + }, + "fraction": { + "16": 0.02638, + "32": 0.0277, + "64": 0.03008, + "128": 0.03053 + }, + "onesuperior": { + "16": 0.00321, + "32": 0.00412, + "64": 0.00429, + "128": 0.00482 + }, + "twosuperior": { + "16": 0.01602, + "32": 0.02042, + "64": 0.02243, + "128": 0.02412 + }, + "threesuperior": { + "16": 0.01279, + "32": 0.01908, + "64": 0.02158, + "128": 0.02338 + }, + "ordfeminine": { + "16": 0.01567, + "32": 0.01971, + "64": 0.02258, + "128": 0.02422 + }, + "ordmasculine": { + "16": 0.01224, + "32": 0.01396, + "64": 0.01537, + "128": 0.01674 + }, + "dottedcircle": { + "16": 0.01235, + "32": 0.01604, + "64": 0.02052, + "128": 0.02272 + }, + "gravecomb": { + "16": 0.01229, + "32": 0.01402, + "64": 0.01711, + "128": 0.01949 + }, + "gravecomb.case": { + "16": 0.01976, + "32": 0.02849, + "64": 0.03397, + "128": 0.03552 + }, + "acutecomb": { + "16": 0.0086, + "32": 0.01212, + "64": 0.01462, + "128": 0.017 + }, + "acutecomb.case": { + "16": 0.01862, + "32": 0.02773, + "64": 0.03092, + "128": 0.03247 + }, + "circumflexcomb": { + "16": 0.01246, + "32": 0.0216, + "64": 0.02487, + "128": 0.02745 + }, + "circumflexcomb.case": { + "16": 0.01858, + "32": 0.02299, + "64": 0.02536, + "128": 0.02806 + }, + "tildecomb": { + "16": 0.01588, + "32": 0.02259, + "64": 0.02501, + "128": 0.02743 + }, + "tildecomb.case": { + "16": 0.01145, + "32": 0.01981, + "64": 0.03008, + "128": 0.03426 + }, + "macroncomb": { + "16": 0.0041, + "32": 0.0041, + "64": 0.0041, + "128": 0.0041 + }, + "dieresiscomb": { + "16": 0.01254, + "32": 0.02062, + "64": 0.02326, + "128": 0.02406 + }, + "dieresiscomb.case": { + "16": 0.01425, + "32": 0.02271, + "64": 0.02422, + "128": 0.02527 + }, + "ringcomb": { + "16": 0.0102, + "32": 0.01668, + "64": 0.02, + "128": 0.02093 + }, + "ringcomb.case": { + "16": 0.00606, + "32": 0.00884, + "64": 0.01396, + "128": 0.01714 + }, + "caroncomb": { + "16": 0.01451, + "32": 0.01892, + "64": 0.02537, + "128": 0.02639 + }, + "dotaccentcomb": { + "16": 0.00568, + "32": 0.01442, + "64": 0.01986, + "128": 0.02144 + }, + "cedillacomb": { + "16": 0.01426, + "32": 0.01914, + "64": 0.02344, + "128": 0.0258 + }, + "zero.lf": { + "16": 0.00734, + "32": 0.00946, + "64": 0.00972, + "128": 0.00993 + }, + "one.lf": { + "16": 0.00917, + "32": 0.00984, + "64": 0.01008, + "128": 0.01027 + }, + "two.lf": { + "16": 0.01236, + "32": 0.01549, + "64": 0.01701, + "128": 0.01753 + }, + "three.lf": { + "16": 0.01578, + "32": 0.01973, + "64": 0.02121, + "128": 0.02188 + }, + "four.lf": { + "16": 0.00785, + "32": 0.00918, + "64": 0.00995, + "128": 0.01589 + }, + "five.lf": { + "16": 0.01297, + "32": 0.01443, + "64": 0.01564, + "128": 0.01624 + }, + "six.lf": { + "16": 0.01009, + "32": 0.01215, + "64": 0.013, + "128": 0.01352 + }, + "seven.lf": { + "16": 0.00916, + "32": 0.00949, + "64": 0.01007, + "128": 0.01014 + }, + "eight.lf": { + "16": 0.01011, + "32": 0.01227, + "64": 0.01272, + "128": 0.0129 + }, + "nine.lf": { + "16": 0.00971, + "32": 0.01234, + "64": 0.01337, + "128": 0.01376 + }, + "zero.osf": { + "16": 0.00642, + "32": 0.00756, + "64": 0.00827, + "128": 0.00867 + }, + "one.osf": { + "16": 0.00679, + "32": 0.00808, + "64": 0.00898, + "128": 0.00972 + }, + "two.osf": { + "16": 0.01426, + "32": 0.01689, + "64": 0.01818, + "128": 0.01849 + }, + "three.osf": { + "16": 0.01287, + "32": 0.01696, + "64": 0.01931, + "128": 0.01998 + }, + "four.osf": { + "16": 0.00639, + "32": 0.00671, + "64": 0.00671, + "128": 0.0119 + }, + "five.osf": { + "16": 0.01631, + "32": 0.01997, + "64": 0.0219, + "128": 0.02236 + }, + "six.osf": { + "16": 0.00938, + "32": 0.01123, + "64": 0.01209, + "128": 0.01265 + }, + "seven.osf": { + "16": 0.01025, + "32": 0.01097, + "64": 0.01186, + "128": 0.01828 + }, + "eight.osf": { + "16": 0.00922, + "32": 0.01063, + "64": 0.01128, + "128": 0.01165 + }, + "nine.osf": { + "16": 0.01058, + "32": 0.01223, + "64": 0.01361, + "128": 0.01408 + }, + "zero.tosf": { + "16": 0.00622, + "32": 0.00745, + "64": 0.00796, + "128": 0.00837 + }, + "one.tosf": { + "16": 0.00741, + "32": 0.0082, + "64": 0.00869, + "128": 0.00918 + }, + "two.tosf": { + "16": 0.01282, + "32": 0.01574, + "64": 0.0168, + "128": 0.01718 + }, + "three.tosf": { + "16": 0.01171, + "32": 0.01584, + "64": 0.01802, + "128": 0.01885 + }, + "four.tosf": { + "16": 0.01508, + "32": 0.01537, + "64": 0.01543, + "128": 0.02061 + }, + "five.tosf": { + "16": 0.01279, + "32": 0.0159, + "64": 0.01733, + "128": 0.01781 + }, + "six.tosf": { + "16": 0.01218, + "32": 0.01424, + "64": 0.01505, + "128": 0.01556 + }, + "seven.tosf": { + "16": 0.01041, + "32": 0.01087, + "64": 0.01169, + "128": 0.01841 + }, + "eight.tosf": { + "16": 0.0095, + "32": 0.01135, + "64": 0.01212, + "128": 0.01234 + }, + "nine.tosf": { + "16": 0.01287, + "32": 0.01439, + "64": 0.01583, + "128": 0.0163 + }, + "two.dnom": { + "16": 0.01602, + "32": 0.02042, + "64": 0.02243, + "128": 0.02412 + }, + "four.dnom": { + "16": 0.01439, + "32": 0.01585, + "64": 0.01835, + "128": 0.02126 + }, + "a.sc": { + "16": 0.01229, + "32": 0.01339, + "64": 0.01411, + "128": 0.01498 + }, + "b.sc": { + "16": 0.00983, + "32": 0.01137, + "64": 0.01327, + "128": 0.01384 + }, + "c.sc": { + "16": 0.01577, + "32": 0.018, + "64": 0.01932, + "128": 0.01981 + }, + "d.sc": { + "16": 0.00831, + "32": 0.00942, + "64": 0.01053, + "128": 0.01096 + }, + "e.sc": { + "16": 0.01162, + "32": 0.01249, + "64": 0.01371, + "128": 0.01386 + }, + "f.sc": { + "16": 0.00799, + "32": 0.00906, + "64": 0.01115, + "128": 0.01125 + }, + "g.sc": { + "16": 0.01295, + "32": 0.01528, + "64": 0.01684, + "128": 0.01762 + }, + "h.sc": { + "16": 0.00259, + "32": 0.00343, + "64": 0.00348, + "128": 0.00348 + }, + "i.sc": { + "16": 0.0023, + "32": 0.00362, + "64": 0.00362, + "128": 0.00362 + }, + "j.sc": { + "16": 0.01235, + "32": 0.01489, + "64": 0.01503, + "128": 0.01559 + }, + "k.sc": { + "16": 0.00768, + "32": 0.00934, + "64": 0.01029, + "128": 0.01084 + }, + "l.sc": { + "16": 0.00546, + "32": 0.00622, + "64": 0.00742, + "128": 0.00758 + }, + "m.sc": { + "16": 0.00814, + "32": 0.00918, + "64": 0.01007, + "128": 0.01038 + }, + "n.sc": { + "16": 0.01654, + "32": 0.01787, + "64": 0.01921, + "128": 0.02008 + }, + "o.sc": { + "16": 0.01017, + "32": 0.01121, + "64": 0.01174, + "128": 0.01201 + }, + "p.sc": { + "16": 0.00901, + "32": 0.01027, + "64": 0.01084, + "128": 0.0113 + }, + "q.sc": { + "16": 0.009, + "32": 0.0109, + "64": 0.01185, + "128": 0.0126 + }, + "r.sc": { + "16": 0.00934, + "32": 0.01065, + "64": 0.01196, + "128": 0.01286 + }, + "s.sc": { + "16": 0.01293, + "32": 0.01717, + "64": 0.0208, + "128": 0.02144 + }, + "t.sc": { + "16": 0.01019, + "32": 0.0113, + "64": 0.01204, + "128": 0.0124 + }, + "u.sc": { + "16": 0.00787, + "32": 0.00917, + "64": 0.01078, + "128": 0.0126 + }, + "v.sc": { + "16": 0.0088, + "32": 0.01106, + "64": 0.01409, + "128": 0.01599 + }, + "w.sc": { + "16": 0.01311, + "32": 0.01523, + "64": 0.01717, + "128": 0.01844 + }, + "x.sc": { + "16": 0.01158, + "32": 0.01487, + "64": 0.01806, + "128": 0.01915 + }, + "y.sc": { + "16": 0.00857, + "32": 0.01113, + "64": 0.01273, + "128": 0.01321 + }, + "z.sc": { + "16": 0.01527, + "32": 0.01593, + "64": 0.01739, + "128": 0.01747 + }, + "ae.sc": { + "16": 0.01001, + "32": 0.01264, + "64": 0.01371, + "128": 0.01432 + }, + "oe.sc": { + "16": 0.01687, + "32": 0.01939, + "64": 0.02042, + "128": 0.02063 + }, + "oslash.sc": { + "16": 0.01474, + "32": 0.01606, + "64": 0.01758, + "128": 0.01781 + }, + "eth.sc": { + "16": 0.00934, + "32": 0.01001, + "64": 0.01222, + "128": 0.01259 + }, + "thorn.sc": { + "16": 0.00824, + "32": 0.01017, + "64": 0.01035, + "128": 0.01053 + }, + "gamma.sc": { + "16": 0.00622, + "32": 0.00723, + "64": 0.00903, + "128": 0.00942 + }, + "delta.sc": { + "16": 0.01498, + "32": 0.01596, + "64": 0.01828, + "128": 0.01894 + }, + "theta.sc": { + "16": 0.01266, + "32": 0.01477, + "64": 0.01529, + "128": 0.01566 + }, + "lambda.sc": { + "16": 0.0164, + "32": 0.01802, + "64": 0.01865, + "128": 0.01956 + }, + "xi.sc": { + "16": 0.03669, + "32": 0.04227, + "64": 0.0456, + "128": 0.04721 + }, + "pi.sc": { + "16": 0.00892, + "32": 0.00968, + "64": 0.01034, + "128": 0.01034 + }, + "sigma.sc": { + "16": 0.02026, + "32": 0.02159, + "64": 0.02397, + "128": 0.0245 + }, + "phi.sc": { + "16": 0.00801, + "32": 0.01105, + "64": 0.01309, + "128": 0.01369 + }, + "psi.sc": { + "16": 0.01393, + "32": 0.01624, + "64": 0.01924, + "128": 0.0197 + }, + "omega.sc": { + "16": 0.00921, + "32": 0.01012, + "64": 0.01119, + "128": 0.01175 + } + } + }, + "DisplayItalic": { + "font": "MachineMadeWarpDisplay-Italic.ttf", + "glyphs_checked": 312, + "summary": { + "16": { + "max": 0.03668, + "max_glyph": "xi.sc", + "mean": 0.01175, + "p95": 0.02052, + "pass": true + }, + "32": { + "max": 0.03756, + "max_glyph": "xi.sc", + "mean": 0.01398, + "p95": 0.02317, + "pass": true + }, + "64": { + "max": 0.04095, + "max_glyph": "xi.sc", + "mean": 0.01547, + "p95": 0.02664, + "pass": true + }, + "128": { + "max": 0.04186, + "max_glyph": "xi.sc", + "mean": 0.01637, + "p95": 0.02834, + "pass": true + } + }, + "glyphs": { + "exclam": { + "16": 0.01027, + "32": 0.01298, + "64": 0.0135, + "128": 0.01447 + }, + "quotedbl": { + "16": 0.01116, + "32": 0.0148, + "64": 0.01725, + "128": 0.01803 + }, + "numbersign": { + "16": 0.01011, + "32": 0.01146, + "64": 0.01574, + "128": 0.01653 + }, + "dollar": { + "16": 0.01295, + "32": 0.0171, + "64": 0.02044, + "128": 0.022 + }, + "percent": { + "16": 0.0129, + "32": 0.01458, + "64": 0.01527, + "128": 0.01556 + }, + "ampersand": { + "16": 0.01156, + "32": 0.01471, + "64": 0.01577, + "128": 0.0162 + }, + "quotesingle": { + "16": 0.01109, + "32": 0.01771, + "64": 0.01942, + "128": 0.01983 + }, + "parenleft": { + "16": 0.00867, + "32": 0.01027, + "64": 0.01159, + "128": 0.01197 + }, + "parenright": { + "16": 0.00753, + "32": 0.00861, + "64": 0.01001, + "128": 0.01077 + }, + "asterisk": { + "16": 0.01639, + "32": 0.01947, + "64": 0.0224, + "128": 0.02411 + }, + "plus": { + "16": 0.02442, + "32": 0.02604, + "64": 0.02657, + "128": 0.02724 + }, + "comma": { + "16": 0.01243, + "32": 0.01347, + "64": 0.0142, + "128": 0.01575 + }, + "hyphen": { + "16": 0.01198, + "32": 0.01774, + "64": 0.01774, + "128": 0.01808 + }, + "period": { + "16": 0.00908, + "32": 0.01028, + "64": 0.01624, + "128": 0.01716 + }, + "slash": { + "16": 0.01172, + "32": 0.01471, + "64": 0.01529, + "128": 0.01558 + }, + "zero": { + "16": 0.00651, + "32": 0.0079, + "64": 0.00817, + "128": 0.00843 + }, + "one": { + "16": 0.01254, + "32": 0.01322, + "64": 0.01398, + "128": 0.01457 + }, + "two": { + "16": 0.00902, + "32": 0.01164, + "64": 0.01321, + "128": 0.01368 + }, + "three": { + "16": 0.01439, + "32": 0.01976, + "64": 0.02084, + "128": 0.02144 + }, + "four": { + "16": 0.00823, + "32": 0.00927, + "64": 0.01042, + "128": 0.01625 + }, + "five": { + "16": 0.01214, + "32": 0.01469, + "64": 0.01606, + "128": 0.01682 + }, + "six": { + "16": 0.00875, + "32": 0.01153, + "64": 0.0125, + "128": 0.01288 + }, + "seven": { + "16": 0.00637, + "32": 0.00694, + "64": 0.00802, + "128": 0.00827 + }, + "eight": { + "16": 0.00792, + "32": 0.00959, + "64": 0.01063, + "128": 0.01095 + }, + "nine": { + "16": 0.01074, + "32": 0.0127, + "64": 0.01388, + "128": 0.01421 + }, + "colon": { + "16": 0.01375, + "32": 0.01966, + "64": 0.02257, + "128": 0.0237 + }, + "semicolon": { + "16": 0.01311, + "32": 0.0155, + "64": 0.01824, + "128": 0.01892 + }, + "less": { + "16": 0.02242, + "32": 0.02318, + "64": 0.02335, + "128": 0.02361 + }, + "equal": { + "16": 0.02065, + "32": 0.02075, + "64": 0.0212, + "128": 0.02195 + }, + "greater": { + "16": 0.0143, + "32": 0.01483, + "64": 0.01534, + "128": 0.01557 + }, + "question": { + "16": 0.00865, + "32": 0.01324, + "64": 0.01475, + "128": 0.01516 + }, + "at": { + "16": 0.01167, + "32": 0.01377, + "64": 0.01609, + "128": 0.0169 + }, + "A": { + "16": 0.01149, + "32": 0.01317, + "64": 0.01434, + "128": 0.01533 + }, + "B": { + "16": 0.01123, + "32": 0.01186, + "64": 0.0125, + "128": 0.01344 + }, + "C": { + "16": 0.01056, + "32": 0.01231, + "64": 0.01359, + "128": 0.01433 + }, + "D": { + "16": 0.01052, + "32": 0.01094, + "64": 0.01112, + "128": 0.01172 + }, + "E": { + "16": 0.01315, + "32": 0.01396, + "64": 0.01482, + "128": 0.01506 + }, + "F": { + "16": 0.01139, + "32": 0.01221, + "64": 0.01262, + "128": 0.01316 + }, + "G": { + "16": 0.00893, + "32": 0.01134, + "64": 0.01297, + "128": 0.01453 + }, + "H": { + "16": 0.00934, + "32": 0.00958, + "64": 0.00974, + "128": 0.00996 + }, + "I": { + "16": 0.00922, + "32": 0.00922, + "64": 0.00955, + "128": 0.00982 + }, + "J": { + "16": 0.00698, + "32": 0.00749, + "64": 0.0081, + "128": 0.0083 + }, + "K": { + "16": 0.00983, + "32": 0.01077, + "64": 0.01165, + "128": 0.01212 + }, + "L": { + "16": 0.01122, + "32": 0.01169, + "64": 0.01216, + "128": 0.01231 + }, + "M": { + "16": 0.01208, + "32": 0.01358, + "64": 0.01562, + "128": 0.01768 + }, + "N": { + "16": 0.01081, + "32": 0.01315, + "64": 0.01548, + "128": 0.01715 + }, + "O": { + "16": 0.00823, + "32": 0.00976, + "64": 0.01038, + "128": 0.01072 + }, + "P": { + "16": 0.00984, + "32": 0.0108, + "64": 0.01175, + "128": 0.01269 + }, + "Q": { + "16": 0.00954, + "32": 0.01111, + "64": 0.01188, + "128": 0.01234 + }, + "R": { + "16": 0.00948, + "32": 0.0112, + "64": 0.01222, + "128": 0.01311 + }, + "S": { + "16": 0.0157, + "32": 0.01775, + "64": 0.01939, + "128": 0.02054 + }, + "T": { + "16": 0.00759, + "32": 0.00882, + "64": 0.00964, + "128": 0.01021 + }, + "U": { + "16": 0.00995, + "32": 0.01157, + "64": 0.01265, + "128": 0.01317 + }, + "V": { + "16": 0.01201, + "32": 0.01384, + "64": 0.01527, + "128": 0.01587 + }, + "W": { + "16": 0.01322, + "32": 0.01479, + "64": 0.01704, + "128": 0.01876 + }, + "X": { + "16": 0.01431, + "32": 0.01529, + "64": 0.01638, + "128": 0.01767 + }, + "Y": { + "16": 0.01063, + "32": 0.01187, + "64": 0.01202, + "128": 0.01235 + }, + "Z": { + "16": 0.01011, + "32": 0.01048, + "64": 0.01097, + "128": 0.01126 + }, + "bracketleft": { + "16": 0.00856, + "32": 0.00856, + "64": 0.00878, + "128": 0.00878 + }, + "backslash": { + "16": 0.00605, + "32": 0.00626, + "64": 0.00703, + "128": 0.00707 + }, + "bracketright": { + "16": 0.01072, + "32": 0.01104, + "64": 0.01158, + "128": 0.01219 + }, + "asciicircum": { + "16": 0.0112, + "32": 0.0117, + "64": 0.01178, + "128": 0.01194 + }, + "underscore": { + "16": 0.03401, + "32": 0.03401, + "64": 0.03563, + "128": 0.0378 + }, + "grave": { + "16": 0.01171, + "32": 0.01462, + "64": 0.01692, + "128": 0.01982 + }, + "a": { + "16": 0.00822, + "32": 0.01044, + "64": 0.01262, + "128": 0.01331 + }, + "b": { + "16": 0.00661, + "32": 0.00814, + "64": 0.00908, + "128": 0.00962 + }, + "c": { + "16": 0.00948, + "32": 0.0124, + "64": 0.01553, + "128": 0.01618 + }, + "d": { + "16": 0.0067, + "32": 0.00871, + "64": 0.00995, + "128": 0.01058 + }, + "e": { + "16": 0.01006, + "32": 0.01424, + "64": 0.01709, + "128": 0.01851 + }, + "f": { + "16": 0.00966, + "32": 0.01086, + "64": 0.01219, + "128": 0.0124 + }, + "g": { + "16": 0.01057, + "32": 0.01254, + "64": 0.01411, + "128": 0.01465 + }, + "h": { + "16": 0.00642, + "32": 0.00804, + "64": 0.00884, + "128": 0.00941 + }, + "i": { + "16": 0.00579, + "32": 0.00831, + "64": 0.00987, + "128": 0.01043 + }, + "j": { + "16": 0.00534, + "32": 0.00683, + "64": 0.00759, + "128": 0.0081 + }, + "k": { + "16": 0.00842, + "32": 0.01015, + "64": 0.01167, + "128": 0.01235 + }, + "l": { + "16": 0.00672, + "32": 0.00733, + "64": 0.00781, + "128": 0.00839 + }, + "m": { + "16": 0.0108, + "32": 0.01253, + "64": 0.01343, + "128": 0.01413 + }, + "n": { + "16": 0.00743, + "32": 0.0094, + "64": 0.0105, + "128": 0.01153 + }, + "o": { + "16": 0.00785, + "32": 0.00912, + "64": 0.00969, + "128": 0.01002 + }, + "p": { + "16": 0.01021, + "32": 0.0115, + "64": 0.01239, + "128": 0.01297 + }, + "q": { + "16": 0.00643, + "32": 0.00812, + "64": 0.00947, + "128": 0.00981 + }, + "r": { + "16": 0.00871, + "32": 0.01228, + "64": 0.01366, + "128": 0.01424 + }, + "s": { + "16": 0.01301, + "32": 0.01584, + "64": 0.01727, + "128": 0.01799 + }, + "t": { + "16": 0.01101, + "32": 0.01161, + "64": 0.01278, + "128": 0.01361 + }, + "u": { + "16": 0.0072, + "32": 0.00879, + "64": 0.01032, + "128": 0.01105 + }, + "v": { + "16": 0.00972, + "32": 0.01297, + "64": 0.01513, + "128": 0.01564 + }, + "w": { + "16": 0.01061, + "32": 0.01318, + "64": 0.01425, + "128": 0.01486 + }, + "x": { + "16": 0.01383, + "32": 0.01796, + "64": 0.01915, + "128": 0.02072 + }, + "y": { + "16": 0.00794, + "32": 0.01007, + "64": 0.01089, + "128": 0.01152 + }, + "z": { + "16": 0.01363, + "32": 0.01579, + "64": 0.02003, + "128": 0.02203 + }, + "braceleft": { + "16": 0.01437, + "32": 0.01666, + "64": 0.01813, + "128": 0.01886 + }, + "bar": { + "16": 0.02868, + "32": 0.02868, + "64": 0.02868, + "128": 0.02901 + }, + "braceright": { + "16": 0.01598, + "32": 0.01985, + "64": 0.02082, + "128": 0.02137 + }, + "asciitilde": { + "16": 0.01235, + "32": 0.01538, + "64": 0.01632, + "128": 0.01734 + }, + "quoteleft": { + "16": 0.00852, + "32": 0.00859, + "64": 0.01154, + "128": 0.01279 + }, + "quoteright": { + "16": 0.00567, + "32": 0.00748, + "64": 0.01021, + "128": 0.01208 + }, + "quotedblleft": { + "16": 0.00774, + "32": 0.0084, + "64": 0.01137, + "128": 0.01225 + }, + "quotedblright": { + "16": 0.00518, + "32": 0.00818, + "64": 0.01155, + "128": 0.01277 + }, + "quotesinglbase": { + "16": 0.01243, + "32": 0.01347, + "64": 0.0142, + "128": 0.01575 + }, + "quotedblbase": { + "16": 0.0091, + "32": 0.01024, + "64": 0.01181, + "128": 0.01306 + }, + "guilsinglleft": { + "16": 0.01581, + "32": 0.02429, + "64": 0.02463, + "128": 0.02559 + }, + "guilsinglright": { + "16": 0.01355, + "32": 0.02274, + "64": 0.02729, + "128": 0.03023 + }, + "guillemotleft": { + "16": 0.01956, + "32": 0.02609, + "64": 0.02716, + "128": 0.02852 + }, + "guillemotright": { + "16": 0.01488, + "32": 0.0213, + "64": 0.02525, + "128": 0.02698 + }, + "ellipsis": { + "16": 0.01346, + "32": 0.01476, + "64": 0.01798, + "128": 0.01878 + }, + "bullet": { + "16": 0.00678, + "32": 0.00953, + "64": 0.0099, + "128": 0.01001 + }, + "periodcentered": { + "16": 0.01597, + "32": 0.02072, + "64": 0.0243, + "128": 0.02462 + }, + "Euro": { + "16": 0.0142, + "32": 0.0167, + "64": 0.01775, + "128": 0.01919 + }, + "copyright": { + "16": 0.01422, + "32": 0.01715, + "64": 0.01959, + "128": 0.02159 + }, + "registered": { + "16": 0.01115, + "32": 0.01632, + "64": 0.01868, + "128": 0.02068 + }, + "degree": { + "16": 0.01304, + "32": 0.01684, + "64": 0.01851, + "128": 0.01901 + }, + "emdash": { + "16": 0.00183, + "32": 0.00183, + "64": 0.00183, + "128": 0.00183 + }, + "endash": { + "16": 0.00363, + "32": 0.00363, + "64": 0.00363, + "128": 0.00363 + }, + "exclamdown": { + "16": 0.01163, + "32": 0.01384, + "64": 0.01531, + "128": 0.01581 + }, + "questiondown": { + "16": 0.01169, + "32": 0.015, + "64": 0.01633, + "128": 0.01697 + }, + "section": { + "16": 0.01172, + "32": 0.01502, + "64": 0.01673, + "128": 0.01764 + }, + "paragraph": { + "16": 0.00482, + "32": 0.00505, + "64": 0.00508, + "128": 0.00513 + }, + "cent": { + "16": 0.01102, + "32": 0.01416, + "64": 0.01821, + "128": 0.02081 + }, + "sterling": { + "16": 0.00973, + "32": 0.0117, + "64": 0.01298, + "128": 0.01341 + }, + "yen": { + "16": 0.01659, + "32": 0.01786, + "64": 0.01879, + "128": 0.02318 + }, + "brokenbar": { + "16": 0.02926, + "32": 0.02954, + "64": 0.02988, + "128": 0.03029 + }, + "logicalnot": { + "16": 0.01201, + "32": 0.01275, + "64": 0.01335, + "128": 0.01454 + }, + "dieresis": { + "16": 0.00997, + "32": 0.01847, + "64": 0.02266, + "128": 0.02353 + }, + "acute": { + "16": 0.01148, + "32": 0.01749, + "64": 0.02456, + "128": 0.0259 + }, + "macron": { + "16": 0.00324, + "32": 0.00324, + "64": 0.00324, + "128": 0.00324 + }, + "cedilla": { + "16": 0.03041, + "32": 0.03347, + "64": 0.03598, + "128": 0.03669 + }, + "circumflex": { + "16": 0.0181, + "32": 0.02323, + "64": 0.02574, + "128": 0.02794 + }, + "tilde": { + "16": 0.00995, + "32": 0.01757, + "64": 0.02278, + "128": 0.02596 + }, + "ring": { + "16": 0.01634, + "32": 0.01848, + "64": 0.02547, + "128": 0.02732 + }, + "caron": { + "16": 0.01937, + "32": 0.0248, + "64": 0.02795, + "128": 0.03104 + }, + "dotaccent": { + "16": 0.00445, + "32": 0.0106, + "64": 0.01178, + "128": 0.0132 + }, + "AE": { + "16": 0.01278, + "32": 0.01347, + "64": 0.01527, + "128": 0.01649 + }, + "Eth": { + "16": 0.00724, + "32": 0.00842, + "64": 0.00892, + "128": 0.00949 + }, + "Oslash": { + "16": 0.0097, + "32": 0.01163, + "64": 0.01263, + "128": 0.01311 + }, + "Thorn": { + "16": 0.00878, + "32": 0.01005, + "64": 0.01083, + "128": 0.01119 + }, + "germandbls": { + "16": 0.01041, + "32": 0.01244, + "64": 0.013, + "128": 0.01389 + }, + "ae": { + "16": 0.0102, + "32": 0.01343, + "64": 0.01628, + "128": 0.01719 + }, + "eth": { + "16": 0.0102, + "32": 0.01177, + "64": 0.01311, + "128": 0.01366 + }, + "oslash": { + "16": 0.00826, + "32": 0.00988, + "64": 0.01065, + "128": 0.01108 + }, + "thorn": { + "16": 0.01141, + "32": 0.01255, + "64": 0.01316, + "128": 0.01355 + }, + "OE": { + "16": 0.01418, + "32": 0.01483, + "64": 0.0157, + "128": 0.01611 + }, + "oe": { + "16": 0.00843, + "32": 0.0104, + "64": 0.01278, + "128": 0.01344 + }, + "dotlessi": { + "16": 0.00508, + "32": 0.00692, + "64": 0.00748, + "128": 0.0084 + }, + "dotlessj": { + "16": 0.00504, + "32": 0.00605, + "64": 0.00686, + "128": 0.00712 + }, + "f_f": { + "16": 0.00875, + "32": 0.00953, + "64": 0.01043, + "128": 0.01067 + }, + "fi": { + "16": 0.01025, + "32": 0.01178, + "64": 0.01317, + "128": 0.01392 + }, + "fl": { + "16": 0.00807, + "32": 0.00923, + "64": 0.0103, + "128": 0.01057 + }, + "f_f_i": { + "16": 0.00848, + "32": 0.00919, + "64": 0.01044, + "128": 0.01104 + }, + "f_f_l": { + "16": 0.00819, + "32": 0.00901, + "64": 0.00991, + "128": 0.01025 + }, + "Gamma": { + "16": 0.01297, + "32": 0.01297, + "64": 0.01315, + "128": 0.01338 + }, + "Delta": { + "16": 0.01256, + "32": 0.01364, + "64": 0.01398, + "128": 0.01446 + }, + "Theta": { + "16": 0.01156, + "32": 0.01324, + "64": 0.0143, + "128": 0.01477 + }, + "Lambda": { + "16": 0.0161, + "32": 0.0174, + "64": 0.01795, + "128": 0.01804 + }, + "Xi": { + "16": 0.02144, + "32": 0.02773, + "64": 0.03256, + "128": 0.03468 + }, + "Pi": { + "16": 0.00891, + "32": 0.00919, + "64": 0.00928, + "128": 0.0095 + }, + "Sigma": { + "16": 0.0186, + "32": 0.02032, + "64": 0.02193, + "128": 0.02306 + }, + "Phi": { + "16": 0.00945, + "32": 0.01029, + "64": 0.01087, + "128": 0.0111 + }, + "Psi": { + "16": 0.01135, + "32": 0.01298, + "64": 0.0143, + "128": 0.01534 + }, + "Omega": { + "16": 0.01173, + "32": 0.01416, + "64": 0.01477, + "128": 0.0151 + }, + "alpha": { + "16": 0.01559, + "32": 0.01872, + "64": 0.02193, + "128": 0.02314 + }, + "beta": { + "16": 0.00809, + "32": 0.00975, + "64": 0.01087, + "128": 0.01117 + }, + "gamma": { + "16": 0.01405, + "32": 0.01551, + "64": 0.01697, + "128": 0.01759 + }, + "delta": { + "16": 0.00876, + "32": 0.01119, + "64": 0.01232, + "128": 0.01285 + }, + "epsilon": { + "16": 0.01457, + "32": 0.01811, + "64": 0.02211, + "128": 0.0238 + }, + "zeta": { + "16": 0.01342, + "32": 0.01537, + "64": 0.0171, + "128": 0.01849 + }, + "eta": { + "16": 0.00635, + "32": 0.00805, + "64": 0.00867, + "128": 0.00943 + }, + "theta": { + "16": 0.00717, + "32": 0.00835, + "64": 0.00885, + "128": 0.00914 + }, + "iota": { + "16": 0.009, + "32": 0.00976, + "64": 0.01093, + "128": 0.01162 + }, + "kappa": { + "16": 0.01014, + "32": 0.01172, + "64": 0.01363, + "128": 0.01482 + }, + "lambda": { + "16": 0.01579, + "32": 0.01684, + "64": 0.01759, + "128": 0.01812 + }, + "mu": { + "16": 0.00737, + "32": 0.00884, + "64": 0.00971, + "128": 0.01012 + }, + "nu": { + "16": 0.00779, + "32": 0.01048, + "64": 0.0124, + "128": 0.01322 + }, + "xi": { + "16": 0.0157, + "32": 0.01712, + "64": 0.01917, + "128": 0.02058 + }, + "pi": { + "16": 0.00615, + "32": 0.00793, + "64": 0.0091, + "128": 0.00976 + }, + "rho": { + "16": 0.00846, + "32": 0.00977, + "64": 0.01035, + "128": 0.01065 + }, + "sigmafinal": { + "16": 0.0107, + "32": 0.01322, + "64": 0.01475, + "128": 0.01594 + }, + "sigma": { + "16": 0.00569, + "32": 0.00774, + "64": 0.00848, + "128": 0.00872 + }, + "tau": { + "16": 0.00575, + "32": 0.00686, + "64": 0.00808, + "128": 0.00855 + }, + "upsilon": { + "16": 0.01021, + "32": 0.01241, + "64": 0.01385, + "128": 0.01425 + }, + "phi": { + "16": 0.00706, + "32": 0.00778, + "64": 0.0082, + "128": 0.00832 + }, + "chi": { + "16": 0.00901, + "32": 0.00965, + "64": 0.01095, + "128": 0.01216 + }, + "psi": { + "16": 0.00707, + "32": 0.00769, + "64": 0.00818, + "128": 0.00854 + }, + "omega": { + "16": 0.01132, + "32": 0.01441, + "64": 0.01626, + "128": 0.01712 + }, + "uni20B9": { + "16": 0.01052, + "32": 0.01305, + "64": 0.01436, + "128": 0.0154 + }, + "minus": { + "16": 0.01416, + "32": 0.01416, + "64": 0.01461, + "128": 0.01526 + }, + "multiply": { + "16": 0.00645, + "32": 0.0083, + "64": 0.00844, + "128": 0.00847 + }, + "divide": { + "16": 0.02519, + "32": 0.02711, + "64": 0.02933, + "128": 0.02957 + }, + "plusminus": { + "16": 0.02142, + "32": 0.02198, + "64": 0.02269, + "128": 0.02335 + }, + "notequal": { + "16": 0.02026, + "32": 0.0215, + "64": 0.02273, + "128": 0.02314 + }, + "lessequal": { + "16": 0.01464, + "32": 0.0149, + "64": 0.01521, + "128": 0.01552 + }, + "greaterequal": { + "16": 0.01181, + "32": 0.01229, + "64": 0.01234, + "128": 0.01265 + }, + "summation": { + "16": 0.02705, + "32": 0.02739, + "64": 0.02761, + "128": 0.02797 + }, + "product": { + "16": 0.0089, + "32": 0.00904, + "64": 0.00931, + "128": 0.00936 + }, + "integral": { + "16": 0.00524, + "32": 0.00603, + "64": 0.00657, + "128": 0.00681 + }, + "radical": { + "16": 0.01616, + "32": 0.01674, + "64": 0.01768, + "128": 0.01857 + }, + "infinity": { + "16": 0.0111, + "32": 0.0128, + "64": 0.01466, + "128": 0.01555 + }, + "partialdiff": { + "16": 0.01205, + "32": 0.01354, + "64": 0.01482, + "128": 0.0152 + }, + "nabla": { + "16": 0.01601, + "32": 0.01739, + "64": 0.01776, + "128": 0.0178 + }, + "minute": { + "16": 0.01766, + "32": 0.01865, + "64": 0.01945, + "128": 0.02002 + }, + "second": { + "16": 0.01414, + "32": 0.01635, + "64": 0.01744, + "128": 0.01824 + }, + "ringoperator": { + "16": 0.01634, + "32": 0.01969, + "64": 0.02546, + "128": 0.0288 + }, + "arrowright": { + "16": 0.0194, + "32": 0.021, + "64": 0.02206, + "128": 0.02265 + }, + "arrowleft": { + "16": 0.02046, + "32": 0.02147, + "64": 0.0224, + "128": 0.02325 + }, + "arrowboth": { + "16": 0.01731, + "32": 0.01836, + "64": 0.01908, + "128": 0.02018 + }, + "element": { + "16": 0.02044, + "32": 0.02163, + "64": 0.02204, + "128": 0.02236 + }, + "notelement": { + "16": 0.01313, + "32": 0.0149, + "64": 0.01579, + "128": 0.01624 + }, + "propersubset": { + "16": 0.01654, + "32": 0.01929, + "64": 0.01979, + "128": 0.02016 + }, + "reflexsubset": { + "16": 0.01923, + "32": 0.02127, + "64": 0.02178, + "128": 0.02229 + }, + "union": { + "16": 0.01022, + "32": 0.01229, + "64": 0.01253, + "128": 0.01291 + }, + "intersection": { + "16": 0.0111, + "32": 0.01345, + "64": 0.01407, + "128": 0.01451 + }, + "universal": { + "16": 0.01392, + "32": 0.01529, + "64": 0.01642, + "128": 0.01709 + }, + "existential": { + "16": 0.0169, + "32": 0.01751, + "64": 0.01799, + "128": 0.01812 + }, + "emptyset": { + "16": 0.00851, + "32": 0.00964, + "64": 0.01021, + "128": 0.01044 + }, + "approxequal": { + "16": 0.01443, + "32": 0.01813, + "64": 0.01915, + "128": 0.02011 + }, + "equivalence": { + "16": 0.02276, + "32": 0.02281, + "64": 0.02357, + "128": 0.02413 + }, + "proportional": { + "16": 0.01095, + "32": 0.01345, + "64": 0.01405, + "128": 0.01455 + }, + "fraction": { + "16": 0.01009, + "32": 0.01131, + "64": 0.01273, + "128": 0.01295 + }, + "onesuperior": { + "16": 0.00619, + "32": 0.00722, + "64": 0.00774, + "128": 0.00801 + }, + "twosuperior": { + "16": 0.01732, + "32": 0.02054, + "64": 0.02268, + "128": 0.02428 + }, + "threesuperior": { + "16": 0.01368, + "32": 0.0196, + "64": 0.0249, + "128": 0.02685 + }, + "ordfeminine": { + "16": 0.01303, + "32": 0.01828, + "64": 0.02169, + "128": 0.02388 + }, + "ordmasculine": { + "16": 0.00975, + "32": 0.01328, + "64": 0.01406, + "128": 0.01486 + }, + "dottedcircle": { + "16": 0.01235, + "32": 0.01604, + "64": 0.02052, + "128": 0.02272 + }, + "gravecomb": { + "16": 0.01413, + "32": 0.01795, + "64": 0.02031, + "128": 0.02176 + }, + "gravecomb.case": { + "16": 0.00936, + "32": 0.02317, + "64": 0.02581, + "128": 0.02875 + }, + "acutecomb": { + "16": 0.00844, + "32": 0.01895, + "64": 0.02409, + "128": 0.02573 + }, + "acutecomb.case": { + "16": 0.01407, + "32": 0.02134, + "64": 0.02789, + "128": 0.0324 + }, + "circumflexcomb": { + "16": 0.00962, + "32": 0.01834, + "64": 0.02187, + "128": 0.02427 + }, + "circumflexcomb.case": { + "16": 0.01123, + "32": 0.02139, + "64": 0.02519, + "128": 0.02761 + }, + "tildecomb": { + "16": 0.01988, + "32": 0.02312, + "64": 0.02737, + "128": 0.03031 + }, + "tildecomb.case": { + "16": 0.02379, + "32": 0.02951, + "64": 0.03755, + "128": 0.04001 + }, + "macroncomb": { + "16": 0.00556, + "32": 0.00556, + "64": 0.00556, + "128": 0.00556 + }, + "dieresiscomb": { + "16": 0.01332, + "32": 0.0206, + "64": 0.02207, + "128": 0.02261 + }, + "dieresiscomb.case": { + "16": 0.01234, + "32": 0.02256, + "64": 0.02499, + "128": 0.02587 + }, + "ringcomb": { + "16": 0.0156, + "32": 0.02056, + "64": 0.02673, + "128": 0.0282 + }, + "ringcomb.case": { + "16": 0.00731, + "32": 0.01365, + "64": 0.01626, + "128": 0.01864 + }, + "caroncomb": { + "16": 0.01649, + "32": 0.02033, + "64": 0.02269, + "128": 0.02653 + }, + "dotaccentcomb": { + "16": 0.00603, + "32": 0.01462, + "64": 0.02006, + "128": 0.02156 + }, + "cedillacomb": { + "16": 0.02482, + "32": 0.02671, + "64": 0.02957, + "128": 0.03106 + }, + "zero.lf": { + "16": 0.00641, + "32": 0.00781, + "64": 0.0081, + "128": 0.00837 + }, + "one.lf": { + "16": 0.00417, + "32": 0.00518, + "64": 0.00586, + "128": 0.00629 + }, + "two.lf": { + "16": 0.00979, + "32": 0.01255, + "64": 0.01404, + "128": 0.01465 + }, + "three.lf": { + "16": 0.01457, + "32": 0.02011, + "64": 0.02097, + "128": 0.02148 + }, + "four.lf": { + "16": 0.00459, + "32": 0.00545, + "64": 0.00617, + "128": 0.01151 + }, + "five.lf": { + "16": 0.01247, + "32": 0.01514, + "64": 0.01643, + "128": 0.01694 + }, + "six.lf": { + "16": 0.00842, + "32": 0.01114, + "64": 0.01227, + "128": 0.01257 + }, + "seven.lf": { + "16": 0.00703, + "32": 0.00777, + "64": 0.00849, + "128": 0.00884 + }, + "eight.lf": { + "16": 0.00775, + "32": 0.00951, + "64": 0.01058, + "128": 0.01089 + }, + "nine.lf": { + "16": 0.01062, + "32": 0.0123, + "64": 0.01344, + "128": 0.01386 + }, + "zero.osf": { + "16": 0.00889, + "32": 0.0099, + "64": 0.01063, + "128": 0.01086 + }, + "one.osf": { + "16": 0.01221, + "32": 0.0129, + "64": 0.01357, + "128": 0.01413 + }, + "two.osf": { + "16": 0.01085, + "32": 0.01337, + "64": 0.01527, + "128": 0.01611 + }, + "three.osf": { + "16": 0.00988, + "32": 0.01516, + "64": 0.01734, + "128": 0.01819 + }, + "four.osf": { + "16": 0.00788, + "32": 0.00872, + "64": 0.01032, + "128": 0.01626 + }, + "five.osf": { + "16": 0.01158, + "32": 0.01411, + "64": 0.01523, + "128": 0.01586 + }, + "six.osf": { + "16": 0.01186, + "32": 0.01303, + "64": 0.01427, + "128": 0.01456 + }, + "seven.osf": { + "16": 0.00819, + "32": 0.00883, + "64": 0.00991, + "128": 0.0162 + }, + "eight.osf": { + "16": 0.00792, + "32": 0.01038, + "64": 0.01105, + "128": 0.01136 + }, + "nine.osf": { + "16": 0.0112, + "32": 0.01285, + "64": 0.01355, + "128": 0.01415 + }, + "zero.tosf": { + "16": 0.00842, + "32": 0.00955, + "64": 0.01029, + "128": 0.01051 + }, + "one.tosf": { + "16": 0.00813, + "32": 0.00841, + "64": 0.00895, + "128": 0.00938 + }, + "two.tosf": { + "16": 0.01017, + "32": 0.01343, + "64": 0.01528, + "128": 0.01598 + }, + "three.tosf": { + "16": 0.01036, + "32": 0.01568, + "64": 0.01795, + "128": 0.01865 + }, + "four.tosf": { + "16": 0.00641, + "32": 0.00692, + "64": 0.00829, + "128": 0.01423 + }, + "five.tosf": { + "16": 0.0143, + "32": 0.0155, + "64": 0.01611, + "128": 0.01667 + }, + "six.tosf": { + "16": 0.01126, + "32": 0.01229, + "64": 0.01315, + "128": 0.01359 + }, + "seven.tosf": { + "16": 0.00619, + "32": 0.00702, + "64": 0.00816, + "128": 0.01457 + }, + "eight.tosf": { + "16": 0.00893, + "32": 0.01088, + "64": 0.01166, + "128": 0.01196 + }, + "nine.tosf": { + "16": 0.01125, + "32": 0.01265, + "64": 0.01345, + "128": 0.01406 + }, + "two.dnom": { + "16": 0.01732, + "32": 0.02054, + "64": 0.02268, + "128": 0.02428 + }, + "four.dnom": { + "16": 0.00831, + "32": 0.01074, + "64": 0.01209, + "128": 0.01262 + }, + "a.sc": { + "16": 0.01558, + "32": 0.01744, + "64": 0.0186, + "128": 0.01912 + }, + "b.sc": { + "16": 0.01038, + "32": 0.01369, + "64": 0.01507, + "128": 0.01541 + }, + "c.sc": { + "16": 0.01495, + "32": 0.01595, + "64": 0.01692, + "128": 0.01774 + }, + "d.sc": { + "16": 0.01195, + "32": 0.01344, + "64": 0.014, + "128": 0.01457 + }, + "e.sc": { + "16": 0.01657, + "32": 0.01808, + "64": 0.01871, + "128": 0.01883 + }, + "f.sc": { + "16": 0.01756, + "32": 0.01974, + "64": 0.02007, + "128": 0.02042 + }, + "g.sc": { + "16": 0.0113, + "32": 0.01316, + "64": 0.01586, + "128": 0.01625 + }, + "h.sc": { + "16": 0.00878, + "32": 0.00996, + "64": 0.01057, + "128": 0.01083 + }, + "i.sc": { + "16": 0.01129, + "32": 0.01237, + "64": 0.01269, + "128": 0.01279 + }, + "j.sc": { + "16": 0.00808, + "32": 0.00943, + "64": 0.01046, + "128": 0.01093 + }, + "k.sc": { + "16": 0.01068, + "32": 0.01403, + "64": 0.01662, + "128": 0.01754 + }, + "l.sc": { + "16": 0.01382, + "32": 0.01456, + "64": 0.01525, + "128": 0.01533 + }, + "m.sc": { + "16": 0.01204, + "32": 0.01446, + "64": 0.01531, + "128": 0.01627 + }, + "n.sc": { + "16": 0.00832, + "32": 0.01072, + "64": 0.0121, + "128": 0.01278 + }, + "o.sc": { + "16": 0.00914, + "32": 0.01136, + "64": 0.01192, + "128": 0.01221 + }, + "p.sc": { + "16": 0.01493, + "32": 0.01705, + "64": 0.01772, + "128": 0.01803 + }, + "q.sc": { + "16": 0.01124, + "32": 0.01323, + "64": 0.01443, + "128": 0.01482 + }, + "r.sc": { + "16": 0.01139, + "32": 0.013, + "64": 0.01495, + "128": 0.01543 + }, + "s.sc": { + "16": 0.01233, + "32": 0.01534, + "64": 0.01675, + "128": 0.01777 + }, + "t.sc": { + "16": 0.01559, + "32": 0.01623, + "64": 0.01783, + "128": 0.01799 + }, + "u.sc": { + "16": 0.01592, + "32": 0.01655, + "64": 0.01814, + "128": 0.01942 + }, + "v.sc": { + "16": 0.00486, + "32": 0.00817, + "64": 0.01049, + "128": 0.01138 + }, + "w.sc": { + "16": 0.00987, + "32": 0.01166, + "64": 0.0125, + "128": 0.01306 + }, + "x.sc": { + "16": 0.0117, + "32": 0.01389, + "64": 0.01619, + "128": 0.01693 + }, + "y.sc": { + "16": 0.01155, + "32": 0.01384, + "64": 0.01527, + "128": 0.01591 + }, + "z.sc": { + "16": 0.01376, + "32": 0.01385, + "64": 0.01445, + "128": 0.01467 + }, + "ae.sc": { + "16": 0.01552, + "32": 0.01636, + "64": 0.01764, + "128": 0.01794 + }, + "oe.sc": { + "16": 0.01317, + "32": 0.01383, + "64": 0.01516, + "128": 0.01549 + }, + "oslash.sc": { + "16": 0.01187, + "32": 0.01395, + "64": 0.01531, + "128": 0.01579 + }, + "eth.sc": { + "16": 0.00919, + "32": 0.01074, + "64": 0.01245, + "128": 0.01306 + }, + "thorn.sc": { + "16": 0.01455, + "32": 0.01589, + "64": 0.01704, + "128": 0.0175 + }, + "gamma.sc": { + "16": 0.01348, + "32": 0.01473, + "64": 0.01489, + "128": 0.01497 + }, + "delta.sc": { + "16": 0.01769, + "32": 0.01862, + "64": 0.01973, + "128": 0.02009 + }, + "theta.sc": { + "16": 0.01214, + "32": 0.01403, + "64": 0.0149, + "128": 0.01535 + }, + "lambda.sc": { + "16": 0.01144, + "32": 0.01362, + "64": 0.01467, + "128": 0.01602 + }, + "xi.sc": { + "16": 0.03668, + "32": 0.03756, + "64": 0.04095, + "128": 0.04186 + }, + "pi.sc": { + "16": 0.00896, + "32": 0.01014, + "64": 0.01034, + "128": 0.01034 + }, + "sigma.sc": { + "16": 0.02059, + "32": 0.02193, + "64": 0.02447, + "128": 0.02543 + }, + "phi.sc": { + "16": 0.01078, + "32": 0.01418, + "64": 0.01563, + "128": 0.0161 + }, + "psi.sc": { + "16": 0.01019, + "32": 0.01161, + "64": 0.01285, + "128": 0.01378 + }, + "omega.sc": { + "16": 0.01052, + "32": 0.01149, + "64": 0.01295, + "128": 0.01339 + } + } + } + }, + "pass": true +} diff --git a/proofs/warp/readability.json b/proofs/warp/readability.json new file mode 100644 index 0000000..3adf034 --- /dev/null +++ b/proofs/warp/readability.json @@ -0,0 +1,69 @@ +{ + "about": "OCR character error rate (CER). Lower is better. See warp/tools/readability.py.", + "tesseract": "tesseract 5.3.4", + "paragraph": "We study the drawing of a thin viscous thread from a heated preform. When the draw ratio exceeds a critical value near 20.2, the steady state loses stability through a Hopf bifurcation and the radius of the fibre oscillates, a regime known as draw resonance. Writing the axial velocity as a base flow plus a small perturbation, we obtain a linear eigenvalue problem, whose spectrum we compute by Chebyshev collocation on 128 points. The leading mode depends only weakly on the capillary number; inertia and cooling, not surface tension, set the threshold.", + "sizes_px": [ + 10, + 12, + 14 + ], + "upscale": 3, + "warp": { + "font": "machine-made Warp 0.1 (text, opsz 10)", + "Regular": { + "cer_by_px": { + "10": 0.0342, + "12": 0.0198, + "14": 0.0162 + }, + "cer_mean": 0.0234 + }, + "Italic": { + "cer_by_px": { + "10": 0.0486, + "12": 0.0036, + "14": 0.0054 + }, + "cer_mean": 0.0192 + } + }, + "reference": { + "font": "DejaVu Serif (Book, Italic)", + "Regular": { + "cer_by_px": { + "10": 0.0, + "12": 0.0, + "14": 0.0036 + }, + "cer_mean": 0.0012 + }, + "Italic": { + "cer_by_px": { + "10": 0.0, + "12": 0.0, + "14": 0.0 + }, + "cer_mean": 0.0 + } + }, + "gate": { + "margin": 0.03, + "checks": [ + { + "style": "Regular", + "cer": 0.0234, + "reference_cer": 0.0012, + "limit": 0.0312, + "pass": true + }, + { + "style": "Italic", + "cer": 0.0192, + "reference_cer": 0.0, + "limit": 0.03, + "pass": true + } + ], + "pass": true + } +} diff --git a/proofs/warp/sketch-compare.json b/proofs/warp/sketch-compare.json new file mode 100644 index 0000000..28a8f5f --- /dev/null +++ b/proofs/warp/sketch-compare.json @@ -0,0 +1,761 @@ +{ + "about": "IoU of the sketch 0.2 engine and the font, per character, at 120 px per em (1.0 = the same pixels). See warp/tools/proof.py.", + "styles": { + "Regular": { + "params_sketch": { + "ital": false, + "opsz": 10 + }, + "mean_iou": 0.954, + "min_iou": 0.0409, + "min_glyph": "×", + "iou": { + "a": 0.9759, + "b": 0.9752, + "c": 0.9652, + "d": 0.9634, + "e": 0.9636, + "f": 0.9902, + "g": 0.9663, + "h": 0.9773, + "i": 0.9675, + "j": 0.9827, + "k": 0.9668, + "l": 0.9621, + "m": 0.9725, + "n": 0.9703, + "o": 0.9766, + "p": 0.9677, + "q": 0.9731, + "r": 0.9935, + "s": 0.9812, + "t": 0.9942, + "u": 0.986, + "v": 0.9099, + "w": 0.9433, + "x": 0.9309, + "y": 0.9556, + "z": 0.9557, + "A": 0.9373, + "B": 0.9717, + "C": 0.9548, + "D": 0.9674, + "E": 0.9541, + "F": 0.9919, + "G": 0.9655, + "H": 0.9959, + "I": 0.9912, + "J": 0.9938, + "K": 0.9636, + "L": 0.9692, + "M": 0.9659, + "N": 0.9895, + "O": 0.9657, + "P": 0.9846, + "Q": 0.951, + "R": 0.9807, + "S": 0.9588, + "T": 0.9937, + "U": 0.9839, + "V": 0.9563, + "W": 0.9365, + "X": 0.9081, + "Y": 0.9389, + "Z": 0.9531, + "0": 0.9751, + "1": 0.9935, + "2": 0.9425, + "3": 0.964, + "4": 0.9719, + "5": 0.9743, + "6": 0.9699, + "7": 0.9746, + "8": 0.9662, + "9": 0.9678, + ".": 0.9706, + ",": 0.9819, + ":": 0.9636, + ";": 0.969, + "!": 0.9786, + "?": 0.9607, + "'": 0.9744, + "’": 0.9713, + "-": 1.0, + "–": 1.0, + "—": 0.9957, + "/": 0.9976, + "(": 0.9716, + ")": 0.9701, + "[": 0.9936, + "]": 0.9924, + "&": 0.9642, + "fi": 0.9891, + "fl": 0.9897, + "ff": 0.9807, + "α": 0.9521, + "β": 0.9822, + "γ": 0.975, + "δ": 0.9716, + "ε": 0.9721, + "θ": 0.9836, + "λ": 0.9402, + "μ": 0.9854, + "π": 0.9894, + "ρ": 0.9621, + "σ": 0.9795, + "φ": 0.9814, + "ψ": 0.9819, + "ω": 0.9679, + "Γ": 0.9924, + "Δ": 0.9261, + "Σ": 0.9259, + "Ω": 0.9332, + "+": 0.9987, + "−": 1.0, + "×": 0.0409, + "=": 1.0, + "<": 0.5177, + ">": 0.5093, + "∑": 0.9682, + "∫": 0.9896, + "√": 0.8889, + "∞": 0.961, + "∂": 0.9737, + "′": 0.9493, + "∇": 0.9682, + "·": 0.9792 + } + }, + "Italic": { + "params_sketch": { + "ital": true, + "opsz": 10 + }, + "mean_iou": 0.9452, + "min_iou": 0.0409, + "min_glyph": "×", + "iou": { + "a": 0.9717, + "b": 0.9623, + "c": 0.962, + "d": 0.9697, + "e": 0.9624, + "f": 0.9649, + "g": 0.9668, + "h": 0.963, + "i": 0.9889, + "j": 0.9837, + "k": 0.9577, + "l": 0.9964, + "m": 0.9664, + "n": 0.9626, + "o": 0.9743, + "p": 0.9588, + "q": 0.9733, + "r": 0.9502, + "s": 0.9726, + "t": 0.9927, + "u": 0.9704, + "v": 0.9595, + "w": 0.9697, + "x": 0.9736, + "y": 0.971, + "z": 0.9493, + "A": 0.9312, + "B": 0.9428, + "C": 0.9637, + "D": 0.947, + "E": 0.9123, + "F": 0.9535, + "G": 0.9659, + "H": 0.9419, + "I": 0.9282, + "J": 0.968, + "K": 0.9241, + "L": 0.9109, + "M": 0.932, + "N": 0.9437, + "O": 0.9644, + "P": 0.9415, + "Q": 0.9721, + "R": 0.9542, + "S": 0.9583, + "T": 0.9674, + "U": 0.9435, + "V": 0.9634, + "W": 0.9379, + "X": 0.9314, + "Y": 0.9442, + "Z": 0.9388, + "0": 0.9806, + "1": 0.9405, + "2": 0.9324, + "3": 0.9573, + "4": 0.9439, + "5": 0.9685, + "6": 0.9685, + "7": 0.9562, + "8": 0.9669, + "9": 0.9663, + ".": 0.9362, + ",": 0.9855, + ":": 0.9468, + ";": 0.9661, + "!": 0.9707, + "?": 0.9596, + "'": 0.9826, + "’": 0.9507, + "-": 0.9878, + "–": 1.0, + "—": 1.0, + "/": 1.0, + "(": 0.9554, + ")": 0.9653, + "[": 0.958, + "]": 0.9476, + "&": 0.9565, + "fi": 0.9727, + "fl": 0.9728, + "ff": 0.9647, + "α": 0.9538, + "β": 0.982, + "γ": 0.9437, + "δ": 0.9668, + "ε": 0.9712, + "θ": 0.9836, + "λ": 0.9396, + "μ": 0.991, + "π": 0.9741, + "ρ": 0.9673, + "σ": 0.976, + "φ": 0.9686, + "ψ": 0.9794, + "ω": 0.9769, + "Γ": 0.9519, + "Δ": 0.8848, + "Σ": 0.9342, + "Ω": 0.9488, + "+": 0.9987, + "−": 1.0, + "×": 0.0409, + "=": 1.0, + "<": 0.5177, + ">": 0.5093, + "∑": 0.9682, + "∫": 0.9896, + "√": 0.8889, + "∞": 0.9628, + "∂": 0.9678, + "′": 0.9355, + "∇": 0.9682, + "·": 0.9792 + } + }, + "Bold": { + "params_sketch": { + "contrast": 11.3, + "ital": false, + "opsz": 10, + "serif": 0.65, + "stem": 134 + }, + "mean_iou": 0.808, + "min_iou": 0.1218, + "min_glyph": "×", + "iou": { + "a": 0.8732, + "b": 0.811, + "c": 0.8857, + "d": 0.8051, + "e": 0.8826, + "f": 0.87, + "g": 0.849, + "h": 0.8379, + "i": 0.9019, + "j": 0.8163, + "k": 0.8397, + "l": 0.9032, + "m": 0.7434, + "n": 0.8163, + "o": 0.8877, + "p": 0.8108, + "q": 0.8126, + "r": 0.8941, + "s": 0.8917, + "t": 0.8471, + "u": 0.8084, + "v": 0.6481, + "w": 0.6262, + "x": 0.7026, + "y": 0.6931, + "z": 0.8338, + "A": 0.6952, + "B": 0.7999, + "C": 0.7934, + "D": 0.7719, + "E": 0.8016, + "F": 0.843, + "G": 0.7972, + "H": 0.7912, + "I": 0.9375, + "J": 0.8103, + "K": 0.7832, + "L": 0.8703, + "M": 0.6055, + "N": 0.6355, + "O": 0.8092, + "P": 0.8418, + "Q": 0.7935, + "R": 0.7977, + "S": 0.8529, + "T": 0.7738, + "U": 0.6812, + "V": 0.6082, + "W": 0.5834, + "X": 0.674, + "Y": 0.6811, + "Z": 0.7902, + "0": 0.8156, + "1": 0.8147, + "2": 0.7592, + "3": 0.7453, + "4": 0.7367, + "5": 0.7419, + "6": 0.8236, + "7": 0.7863, + "8": 0.8252, + "9": 0.8075, + ".": 0.9726, + ",": 0.9102, + ":": 0.9695, + ";": 0.9129, + "!": 0.9881, + "?": 0.8079, + "'": 0.9886, + "’": 0.9115, + "-": 0.8907, + "–": 0.9456, + "—": 0.9446, + "/": 0.7675, + "(": 0.9358, + ")": 0.8772, + "[": 0.9457, + "]": 0.8989, + "&": 0.8273, + "fi": 0.8344, + "fl": 0.8214, + "ff": 0.7969, + "α": 0.7686, + "β": 0.881, + "γ": 0.7443, + "δ": 0.8791, + "ε": 0.8825, + "θ": 0.8958, + "λ": 0.8077, + "μ": 0.8991, + "π": 0.8055, + "ρ": 0.9099, + "σ": 0.8951, + "φ": 0.8951, + "ψ": 0.8239, + "ω": 0.7667, + "Γ": 0.8762, + "Δ": 0.7483, + "Σ": 0.7564, + "Ω": 0.805, + "+": 0.771, + "−": 0.9482, + "×": 0.1218, + "=": 0.9497, + "<": 0.5072, + ">": 0.5277, + "∑": 0.6922, + "∫": 0.8075, + "√": 0.5874, + "∞": 0.7639, + "∂": 0.8451, + "′": 0.9321, + "∇": 0.7312, + "·": 0.9643 + } + }, + "BoldItalic": { + "params_sketch": { + "contrast": 11.3, + "ital": true, + "opsz": 10, + "serif": 0.65, + "stem": 134 + }, + "mean_iou": 0.8114, + "min_iou": 0.1218, + "min_glyph": "×", + "iou": { + "a": 0.884, + "b": 0.8449, + "c": 0.9171, + "d": 0.8853, + "e": 0.8975, + "f": 0.8256, + "g": 0.8348, + "h": 0.8396, + "i": 0.9134, + "j": 0.8196, + "k": 0.8634, + "l": 0.9274, + "m": 0.7325, + "n": 0.8006, + "o": 0.9025, + "p": 0.8237, + "q": 0.8906, + "r": 0.8855, + "s": 0.842, + "t": 0.8693, + "u": 0.7983, + "v": 0.7991, + "w": 0.7269, + "x": 0.7753, + "y": 0.8267, + "z": 0.8735, + "A": 0.6971, + "B": 0.7834, + "C": 0.8376, + "D": 0.7512, + "E": 0.7769, + "F": 0.8094, + "G": 0.8223, + "H": 0.7641, + "I": 0.8976, + "J": 0.8253, + "K": 0.7664, + "L": 0.8353, + "M": 0.6604, + "N": 0.6204, + "O": 0.8296, + "P": 0.8131, + "Q": 0.821, + "R": 0.7756, + "S": 0.8474, + "T": 0.7824, + "U": 0.6591, + "V": 0.6865, + "W": 0.6054, + "X": 0.6348, + "Y": 0.6746, + "Z": 0.8057, + "0": 0.8144, + "1": 0.8264, + "2": 0.7557, + "3": 0.7486, + "4": 0.7521, + "5": 0.7529, + "6": 0.8119, + "7": 0.7769, + "8": 0.8238, + "9": 0.8129, + ".": 0.9662, + ",": 0.915, + ":": 0.963, + ";": 0.9218, + "!": 0.9801, + "?": 0.8081, + "'": 0.9859, + "’": 0.9358, + "-": 0.8835, + "–": 0.9433, + "—": 0.9449, + "/": 0.77, + "(": 0.9287, + ")": 0.8771, + "[": 0.9255, + "]": 0.8616, + "&": 0.83, + "fi": 0.7647, + "fl": 0.7663, + "ff": 0.7814, + "α": 0.7689, + "β": 0.8738, + "γ": 0.7543, + "δ": 0.8804, + "ε": 0.8822, + "θ": 0.8924, + "λ": 0.8088, + "μ": 0.8838, + "π": 0.8061, + "ρ": 0.904, + "σ": 0.8956, + "φ": 0.886, + "ψ": 0.8096, + "ω": 0.7619, + "Γ": 0.843, + "Δ": 0.7325, + "Σ": 0.7532, + "Ω": 0.8059, + "+": 0.771, + "−": 0.9482, + "×": 0.1218, + "=": 0.9497, + "<": 0.5072, + ">": 0.5277, + "∑": 0.6922, + "∫": 0.8075, + "√": 0.5874, + "∞": 0.7677, + "∂": 0.8376, + "′": 0.9365, + "∇": 0.7312, + "·": 0.9643 + } + }, + "Display": { + "params_sketch": { + "ital": false, + "opsz": 60 + }, + "mean_iou": 0.9501, + "min_iou": 0.0476, + "min_glyph": "×", + "iou": { + "a": 0.9729, + "b": 0.9726, + "c": 0.9521, + "d": 0.924, + "e": 0.951, + "f": 0.9825, + "g": 0.9626, + "h": 0.9914, + "i": 0.9905, + "j": 0.9846, + "k": 0.9798, + "l": 0.9945, + "m": 0.9876, + "n": 0.993, + "o": 0.9651, + "p": 0.9772, + "q": 0.9304, + "r": 0.9909, + "s": 0.9587, + "t": 0.9828, + "u": 0.9838, + "v": 0.9769, + "w": 0.9304, + "x": 0.9422, + "y": 0.9305, + "z": 0.9808, + "A": 0.9336, + "B": 0.9858, + "C": 0.9514, + "D": 0.9784, + "E": 0.9984, + "F": 0.9982, + "G": 0.9633, + "H": 0.9995, + "I": 1.0, + "J": 0.9937, + "K": 0.9574, + "L": 1.0, + "M": 0.9538, + "N": 0.9536, + "O": 0.9513, + "P": 0.9828, + "Q": 0.9619, + "R": 0.9829, + "S": 0.9535, + "T": 0.9802, + "U": 0.9874, + "V": 0.9616, + "W": 0.8929, + "X": 0.9189, + "Y": 0.9398, + "Z": 0.9701, + "0": 0.9723, + "1": 0.9947, + "2": 0.9608, + "3": 0.961, + "4": 0.9218, + "5": 0.9691, + "6": 0.969, + "7": 0.9556, + "8": 0.9681, + "9": 0.9701, + ".": 0.9417, + ",": 0.9641, + ":": 0.954, + ";": 0.9648, + "!": 0.9875, + "?": 0.9615, + "'": 0.9951, + "’": 0.9399, + "-": 0.9935, + "–": 0.9947, + "—": 1.0, + "/": 0.9973, + "(": 0.9439, + ")": 0.95, + "[": 0.9846, + "]": 0.9846, + "&": 0.9571, + "fi": 0.9925, + "fl": 0.9937, + "ff": 0.9917, + "α": 0.9491, + "β": 0.9775, + "γ": 0.9485, + "δ": 0.9664, + "ε": 0.9467, + "θ": 0.9762, + "λ": 0.9395, + "μ": 0.9609, + "π": 0.9797, + "ρ": 0.9733, + "σ": 0.9777, + "φ": 0.9823, + "ψ": 0.9929, + "ω": 0.9549, + "Γ": 0.999, + "Δ": 0.9124, + "Σ": 0.958, + "Ω": 0.9454, + "+": 0.9203, + "−": 1.0, + "×": 0.0476, + "=": 0.9977, + "<": 0.4048, + ">": 0.4455, + "∑": 0.9648, + "∫": 0.9543, + "√": 0.9286, + "∞": 0.9586, + "∂": 0.9715, + "′": 0.9918, + "∇": 0.9608, + "·": 0.9535 + } + }, + "DisplayItalic": { + "params_sketch": { + "ital": true, + "opsz": 60 + }, + "mean_iou": 0.9432, + "min_iou": 0.0476, + "min_glyph": "×", + "iou": { + "a": 0.9638, + "b": 0.9742, + "c": 0.9622, + "d": 0.9603, + "e": 0.9614, + "f": 0.9626, + "g": 0.9663, + "h": 0.9767, + "i": 0.9648, + "j": 0.9824, + "k": 0.9583, + "l": 0.9912, + "m": 0.9589, + "n": 0.9554, + "o": 0.9565, + "p": 0.9597, + "q": 0.9645, + "r": 0.9465, + "s": 0.9702, + "t": 0.9861, + "u": 0.9567, + "v": 0.9575, + "w": 0.9566, + "x": 0.9636, + "y": 0.9629, + "z": 0.9496, + "A": 0.9327, + "B": 0.9507, + "C": 0.9448, + "D": 0.9537, + "E": 0.9444, + "F": 0.9625, + "G": 0.9591, + "H": 0.9586, + "I": 0.9559, + "J": 0.9889, + "K": 0.9334, + "L": 0.9579, + "M": 0.941, + "N": 0.949, + "O": 0.9644, + "P": 0.961, + "Q": 0.9546, + "R": 0.9532, + "S": 0.9432, + "T": 0.9526, + "U": 0.976, + "V": 0.9556, + "W": 0.9419, + "X": 0.9038, + "Y": 0.962, + "Z": 0.9627, + "0": 0.9762, + "1": 0.9519, + "2": 0.9664, + "3": 0.9518, + "4": 0.9394, + "5": 0.9462, + "6": 0.9659, + "7": 0.9563, + "8": 0.9612, + "9": 0.9694, + ".": 0.9496, + ",": 0.9818, + ":": 0.95, + ";": 0.9735, + "!": 0.9717, + "?": 0.961, + "'": 0.9805, + "’": 0.9651, + "-": 0.9872, + "–": 1.0, + "—": 1.0, + "/": 0.9972, + "(": 0.957, + ")": 0.9519, + "[": 0.9562, + "]": 0.9454, + "&": 0.9529, + "fi": 0.9705, + "fl": 0.9704, + "ff": 0.9665, + "α": 0.9371, + "β": 0.9658, + "γ": 0.9602, + "δ": 0.9582, + "ε": 0.9613, + "θ": 0.9722, + "λ": 0.9502, + "μ": 0.974, + "π": 0.9767, + "ρ": 0.9683, + "σ": 0.9744, + "φ": 0.9697, + "ψ": 0.98, + "ω": 0.9612, + "Γ": 0.9545, + "Δ": 0.9382, + "Σ": 0.9667, + "Ω": 0.9257, + "+": 0.9203, + "−": 1.0, + "×": 0.0476, + "=": 0.9977, + "<": 0.4048, + ">": 0.4455, + "∑": 0.9648, + "∫": 0.9543, + "√": 0.9286, + "∞": 0.9557, + "∂": 0.9584, + "′": 0.9373, + "∇": 0.9608, + "·": 0.9535 + } + } + } +} diff --git a/proofs/warp/warp-accents.png b/proofs/warp/warp-accents.png new file mode 100644 index 0000000..e5abbc9 Binary files /dev/null and b/proofs/warp/warp-accents.png differ diff --git a/proofs/warp/warp-charset-bold.png b/proofs/warp/warp-charset-bold.png new file mode 100644 index 0000000..1149512 Binary files /dev/null and b/proofs/warp/warp-charset-bold.png differ diff --git a/proofs/warp/warp-charset-bolditalic.png b/proofs/warp/warp-charset-bolditalic.png new file mode 100644 index 0000000..0f992b1 Binary files /dev/null and b/proofs/warp/warp-charset-bolditalic.png differ diff --git a/proofs/warp/warp-charset-display.png b/proofs/warp/warp-charset-display.png new file mode 100644 index 0000000..bfa9e10 Binary files /dev/null and b/proofs/warp/warp-charset-display.png differ diff --git a/proofs/warp/warp-charset-displayitalic.png b/proofs/warp/warp-charset-displayitalic.png new file mode 100644 index 0000000..dac4918 Binary files /dev/null and b/proofs/warp/warp-charset-displayitalic.png differ diff --git a/proofs/warp/warp-charset-italic.png b/proofs/warp/warp-charset-italic.png new file mode 100644 index 0000000..82386c9 Binary files /dev/null and b/proofs/warp/warp-charset-italic.png differ diff --git a/proofs/warp/warp-charset-regular.png b/proofs/warp/warp-charset-regular.png new file mode 100644 index 0000000..ec5a515 Binary files /dev/null and b/proofs/warp/warp-charset-regular.png differ diff --git a/proofs/warp/warp-equations.png b/proofs/warp/warp-equations.png new file mode 100644 index 0000000..e468db7 Binary files /dev/null and b/proofs/warp/warp-equations.png differ diff --git a/proofs/warp/warp-greek.png b/proofs/warp/warp-greek.png new file mode 100644 index 0000000..10a6d2e Binary files /dev/null and b/proofs/warp/warp-greek.png differ diff --git a/proofs/warp/warp-kerning.png b/proofs/warp/warp-kerning.png new file mode 100644 index 0000000..c2e38f7 Binary files /dev/null and b/proofs/warp/warp-kerning.png differ diff --git a/proofs/warp/warp-paragraph.png b/proofs/warp/warp-paragraph.png new file mode 100644 index 0000000..3939a75 Binary files /dev/null and b/proofs/warp/warp-paragraph.png differ diff --git a/proofs/warp/warp-sketch-vs-font-bold.png b/proofs/warp/warp-sketch-vs-font-bold.png new file mode 100644 index 0000000..b75e299 Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-bold.png differ diff --git a/proofs/warp/warp-sketch-vs-font-bolditalic.png b/proofs/warp/warp-sketch-vs-font-bolditalic.png new file mode 100644 index 0000000..7693c4c Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-bolditalic.png differ diff --git a/proofs/warp/warp-sketch-vs-font-display.png b/proofs/warp/warp-sketch-vs-font-display.png new file mode 100644 index 0000000..2a29ba3 Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-display.png differ diff --git a/proofs/warp/warp-sketch-vs-font-displayitalic.png b/proofs/warp/warp-sketch-vs-font-displayitalic.png new file mode 100644 index 0000000..2d4d288 Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-displayitalic.png differ diff --git a/proofs/warp/warp-sketch-vs-font-italic.png b/proofs/warp/warp-sketch-vs-font-italic.png new file mode 100644 index 0000000..95c52d6 Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-italic.png differ diff --git a/proofs/warp/warp-sketch-vs-font-regular.png b/proofs/warp/warp-sketch-vs-font-regular.png new file mode 100644 index 0000000..53c7410 Binary files /dev/null and b/proofs/warp/warp-sketch-vs-font-regular.png differ diff --git a/proofs/warp/warp-smallcaps-figures.png b/proofs/warp/warp-smallcaps-figures.png new file mode 100644 index 0000000..46aa5ac Binary files /dev/null and b/proofs/warp/warp-smallcaps-figures.png differ diff --git a/proofs/warp/warp-styles.png b/proofs/warp/warp-styles.png new file mode 100644 index 0000000..7b1d628 Binary files /dev/null and b/proofs/warp/warp-styles.png differ diff --git a/site/assets/site.css b/site/assets/site.css index a8abe58..1fb851c 100644 --- a/site/assets/site.css +++ b/site/assets/site.css @@ -12,6 +12,8 @@ --sans: "mm Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --mono: "mm Weft", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; --heddle: "mm Heddle", "Helvetica Neue", Arial, sans-serif; + --warp: "mm Warp", Georgia, "Times New Roman", serif; + --warp-display: "mm Warp Display", "mm Warp", Georgia, serif; --step-0: 0.8125rem; --step-1: 1rem; --step-2: 1.25rem; @@ -77,6 +79,7 @@ main:focus { outline: none; } .weft { font-family: var(--mono); font-feature-settings: normal; } .sans { font-family: var(--sans); } .heddle { font-family: var(--heddle); font-feature-settings: normal; } +.warp { font-family: var(--warp); font-feature-settings: normal; } /* Header */ .site-head { border-bottom: 1px solid var(--rule); } @@ -324,6 +327,114 @@ input[type=range] { width: 100%; accent-color: var(--red); margin: 4px 0; min-he } .heddle-dl-title { font-family: var(--heddle); font-weight: 700; letter-spacing: 0; } +/* Warp: a journal frontispiece, after the approved sketch page. Ivory + paper, hairline rules, small capitals, one deep ink blue. Set in Warp. */ +.page-warp { + --paper: #F7F2E6; + --panel: #EFE8D8; + --ink: #1B1712; + --muted: #6E6556; + --rule: #D8CEBA; + --accent: #1E3466; +} +@media (prefers-color-scheme: dark) { + .page-warp { + --paper: #15120E; + --panel: #1D1914; + --ink: #ECE4D2; + --muted: #A39985; + --rule: #3A332A; + --accent: #AFBEE6; + } +} +.page-warp main { font-family: var(--warp); font-size: 1.0625rem; line-height: 1.5; } +.page-warp main h2, .page-warp main h3 { font-family: var(--warp-display); font-weight: 400; letter-spacing: 0; } +.page-warp main h2 { font-size: clamp(1.75rem, 1.3rem + 1.8vw, 2.5rem); } +.page-warp main h3 { font-size: 1.375rem; } +.page-warp main code { font-size: 0.8em; color: var(--muted); } +.page-warp :focus-visible { outline-color: var(--accent); } +.page-warp .btn { background: var(--ink); border-color: var(--ink); color: var(--paper); font-family: var(--warp); font-variant-caps: all-small-caps; letter-spacing: 0.08em; font-size: 1.0625rem; padding: 8px 18px; } +.page-warp .btn:hover, .page-warp .btn-ghost { background: transparent; color: var(--ink); } +.page-warp .btn-ghost:hover { background: var(--ink); color: var(--paper); } +.warp-sc { font-variant-caps: all-small-caps; letter-spacing: 0.12em; color: var(--muted); max-width: none; font-variant-numeric: oldstyle-nums; } +.warp-hero { display: grid; justify-items: center; text-align: center; gap: 10px; padding-block: 44px 52px; border-bottom: 1px solid var(--rule); } +.warp-title { font-family: var(--warp-display); font-weight: 400; font-size: clamp(5rem, 2rem + 16vw, 13rem); line-height: 0.9; letter-spacing: 0; margin-top: 8px; } +.warp-rule { display: block; width: 72px; height: 1px; background: var(--accent); margin-block: 14px 6px; } +.warp-sub { font-family: var(--warp-display); font-style: italic; font-size: clamp(1.375rem, 1rem + 1.6vw, 2.125rem); line-height: 1.2; max-width: none; } +.warp-ornament { font-size: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem); font-style: italic; margin-block: 6px; max-width: none; } +.warp-ornament sup { font-size: 0.6em; } +.warp-lede { font-size: clamp(1.125rem, 1rem + 0.5vw, 1.3125rem); line-height: 1.5; max-width: 34em; } +.warp-band { display: grid; gap: 28px; padding-block: 52px; border-bottom: 1px solid var(--rule); } +.warp-sh { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 10px 24px; align-items: start; } +.warp-num { grid-column: 1 / span 3; padding-top: 10px; font-variant-caps: all-small-caps; letter-spacing: 0.12em; color: var(--accent); font-size: 1.125rem; } +.warp-ttl { grid-column: 4 / span 9; display: grid; gap: 10px; } +.warp-intro { color: var(--muted); font-style: italic; } +.warp-body { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 24px; } +.warp-main { grid-column: 4 / span 9; min-width: 0; display: grid; gap: 18px; } +@media (max-width: 760px) { + .warp-sh, .warp-body { grid-template-columns: minmax(0, 1fr); } + .warp-num, .warp-ttl, .warp-main { grid-column: auto; } + .warp-num { padding-top: 0; } +} +.warp-play { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 36px; align-items: start; } +@media (max-width: 860px) { .warp-play { grid-template-columns: minmax(0, 1fr); } } +.warp-controls { display: grid; gap: 16px; padding: 16px 0; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--rule); } +.warp-field { display: grid; gap: 4px; } +.warp-field-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; } +.warp-label, .warp-check { font-variant-caps: all-small-caps; letter-spacing: 0.12em; color: var(--muted); font-size: 1.0625rem; } +.warp-seg { display: flex; flex-wrap: wrap; gap: 4px 14px; } +.warp-seg button { appearance: none; border: 0; background: none; padding: 2px 0; font: inherit; font-variant-caps: all-small-caps; letter-spacing: 0.1em; color: var(--muted); cursor: pointer; border-bottom: 1px solid transparent; min-height: 32px; } +.warp-seg button[aria-pressed="true"] { color: var(--ink); border-bottom-color: var(--accent); } +.page-warp textarea { font: 1rem/1.45 var(--warp); background: transparent; border: 0; border-bottom: 1px solid var(--rule); border-radius: 0; padding: 4px 0 8px; min-height: 64px; } +.page-warp textarea:focus-visible { outline: none; border-bottom-color: var(--accent); } +.page-warp input[type=range], .page-warp input[type=checkbox] { accent-color: var(--accent); } +.warp-checks { display: grid; gap: 6px; } +.warp-check { display: flex; gap: 8px; align-items: center; } +.warp-check input { width: 16px; height: 16px; margin: 0; } +.warp-stage { min-width: 0; padding-block: 12px; border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule); min-height: 200px; overflow: hidden; } +.warp-stage-text { font-size: 64px; line-height: 1.12; white-space: pre-wrap; overflow-wrap: anywhere; max-width: none; margin: 0; } +.w-regular { font-family: var(--warp); font-weight: 400; font-style: normal; } +.w-italic { font-family: var(--warp); font-weight: 400; font-style: italic; } +.w-bold { font-family: var(--warp); font-weight: 700; font-style: normal; } +.w-bolditalic { font-family: var(--warp); font-weight: 700; font-style: italic; } +.w-display { font-family: var(--warp-display); font-weight: 400; font-style: normal; } +.warp-paper.w-bolditalic { font-style: normal; } +.warp-paper.w-bolditalic i { font-style: italic; } +.warp-paper.w-italic i { font-style: normal; } +.warp-p { max-width: 36em; hyphens: auto; font-variant-numeric: oldstyle-nums proportional-nums; } +.warp-p-l { font-size: 1.125rem; line-height: 1.55; } +.warp-p-s { font-size: 0.9375rem; line-height: 1.5; } +.warp-p sup, .warp-p sub { font-size: 0.7em; line-height: 0; } +.warp-scl { font-variant-caps: small-caps; letter-spacing: 0.04em; } +.warp-rows { display: grid; } +.warp-row { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 6px 24px; align-items: baseline; padding-block: 14px; border-top: 1px solid var(--rule); } +.warp-row:first-child { border-top: 0; } +.warp-rl { grid-column: 1 / span 3; min-width: 0; font-variant-caps: all-small-caps; letter-spacing: 0.1em; color: var(--muted); } +.warp-rl code { font-variant-caps: normal; letter-spacing: 0; display: block; } +.warp-rc { grid-column: 4 / span 9; min-width: 0; overflow-wrap: anywhere; } +@media (max-width: 760px) { + .warp-row { grid-template-columns: minmax(0, 1fr); } + .warp-rl, .warp-rc { grid-column: auto; } +} +.warp-big { font-size: clamp(1.75rem, 1rem + 3vw, 3.25rem); line-height: 1.15; letter-spacing: 0.01em; } +.warp-mid { font-size: clamp(1.375rem, 1rem + 1.5vw, 2.25rem); line-height: 1.25; } +.warp-tnum { font-variant-numeric: lining-nums tabular-nums; } +.warp-onum { font-variant-numeric: oldstyle-nums; } +.warp-pnum { font-variant-numeric: lining-nums proportional-nums; } +.warp-smcp { font-variant-caps: small-caps; } +.warp-c2sc { font-variant-caps: all-small-caps; letter-spacing: 0.06em; } +.page-warp .glyph-grid button, .page-warp .glyph-big { font-family: var(--warp); } +.page-warp .glyph-grid button { font-size: 30px; } +.page-warp .glyph-group h3 { font-family: var(--warp); font-variant-caps: all-small-caps; text-transform: none; letter-spacing: 0.12em; font-size: 1.0625rem; font-weight: 400; } +.page-warp .glyph-meta { font-size: 0.9375rem; } +.page-warp .card h3 { font-family: var(--warp-display); } +.warp-cta { justify-items: center; text-align: center; } +.warp-dl-title { font-family: var(--warp-display); font-weight: 400; letter-spacing: 0; } +.fam-sample.warp { font-family: var(--warp-display); letter-spacing: 0; } +.fam-text.warp { font-family: var(--warp); font-style: italic; } +.fam-warp { grid-column: 1 / -1; border-top: 1px solid var(--rule); } +.fam + .fam.fam-warp { border-left: 0; } + /* Download */ .page-download h3 { margin-top: 6px; } .snippet { position: relative; min-width: 0; } diff --git a/site/assets/warp.js b/site/assets/warp.js new file mode 100644 index 0000000..85cd41d --- /dev/null +++ b/site/assets/warp.js @@ -0,0 +1,103 @@ +/* Warp page: style switch, playground and glyph map. + The data comes from data/warp.json, which site/build.py writes from the font. */ +(function () { + "use strict"; + + var root = document.body.getAttribute("data-root") || "./"; + var $ = function (id) { return document.getElementById(id); }; + + function el(tag, attrs, text) { + var n = document.createElement(tag); + if (attrs) { + Object.keys(attrs).forEach(function (k) { n.setAttribute(k, attrs[k]); }); + } + if (text !== undefined) n.textContent = text; + return n; + } + + function hex(u) { + var s = u.toString(16).toUpperCase(); + while (s.length < 4) s = "0" + s; + return "U+" + s; + } + + /* ---------- style switch and playground ---------- */ + var STYLES = ["regular", "italic", "bold", "bolditalic", "display"]; + + function setupPlayground() { + var stage = $("stage-text"), txt = $("txt"), size = $("size"); + var onum = $("onum"), smcp = $("smcp"), paper = $("paper-text"); + var buttons = Array.prototype.slice.call(document.querySelectorAll(".warp-seg button")); + if (window.innerWidth < 640) size.value = 40; + + function setStyle(name) { + STYLES.forEach(function (s) { + stage.classList.toggle("w-" + s, s === name); + if (paper) paper.classList.toggle("w-" + s, s === name); + }); + buttons.forEach(function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-style") === name)); }); + } + function apply() { + stage.style.fontSize = size.value + "px"; + $("size-out").textContent = size.value + " px"; + var f = []; + if (onum.checked) f.push('"onum" 1'); + if (smcp.checked) f.push('"smcp" 1'); + stage.style.fontFeatureSettings = f.length ? f.join(", ") : "normal"; + } + buttons.forEach(function (b) { + b.addEventListener("click", function () { setStyle(b.getAttribute("data-style")); }); + }); + txt.addEventListener("input", function () { stage.textContent = txt.value || " "; }); + [size, onum, smcp].forEach(function (i) { i.addEventListener("input", apply); }); + setStyle("regular"); + apply(); + } + + /* ---------- glyph map ---------- */ + function setupGlyphs(data) { + var map = $("glyph-map"); + map.textContent = ""; + var count = 0, first = null; + var big = $("glyph-big"), nm = $("glyph-name"), uu = $("glyph-u"), gg = $("glyph-g"); + var pressed = null; + + function show(btn, c) { + if (pressed) pressed.setAttribute("aria-pressed", "false"); + btn.setAttribute("aria-pressed", "true"); + pressed = btn; + big.textContent = String.fromCodePoint(c.u); + nm.textContent = c.n || "(no name)"; + uu.textContent = hex(c.u); + gg.textContent = c.g; + } + + (data.glyphs || []).forEach(function (grp) { + var sec = el("div", { "class": "glyph-group" }); + var hid = "gg-" + grp.name.toLowerCase().replace(/[^a-z]+/g, "-"); + sec.appendChild(el("h3", { id: hid }, grp.name + " (" + grp.chars.length + ")")); + var grid = el("div", { "class": "glyph-grid", role: "group", "aria-labelledby": hid }); + grp.chars.forEach(function (c) { + var b = el("button", { type: "button", "aria-pressed": "false", + "aria-label": (c.n || hex(c.u)) + ", " + hex(c.u), title: hex(c.u) + " " + c.n }, + String.fromCodePoint(c.u)); + b.addEventListener("click", function () { show(b, c); }); + grid.appendChild(b); + if (!first) first = [b, c]; + count++; + }); + sec.appendChild(grid); + map.appendChild(sec); + }); + $("glyph-count").textContent = count; + if (first) show(first[0], first[1]); + } + + setupPlayground(); + fetch(root + "data/warp.json", { cache: "no-cache" }).then(function (r) { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.json(); + }).then(setupGlyphs).catch(function (err) { + $("glyph-map").textContent = "The character map cannot load: " + err.message + "."; + }); +})(); diff --git a/site/build.py b/site/build.py index 2d45fd5..a65e6a7 100644 --- a/site/build.py +++ b/site/build.py @@ -51,6 +51,7 @@ "weft": REPO_URL + "/releases/tag/weft-v0.1.0", "sans": REPO_URL + "/releases/tag/sans-v0.1.0", "heddle": REPO_URL + "/releases/tag/heddle-v0.1.0", + "warp": REPO_URL + "/releases/tag/warp-v0.1.0", } # Font files per family, in fonts//webfonts/. Every role is @@ -86,6 +87,21 @@ "licence": ROOT / "heddle" / "OFL.txt", "planned_axes": {"wght": [100, 400, 900], "wdth": [70, 100, 140]}, }, + # Warp 0.1 is six static fonts; "vf" is the Regular (the pages read + # its character map). The variable font is planned for 0.3. + "warp": { + "dir": ROOT / "fonts" / "warp" / "webfonts", + "files": { + "vf": "MachineMadeWarp-Regular.woff2", + "italic": "MachineMadeWarp-Italic.woff2", + "bold": "MachineMadeWarp-Bold.woff2", + "bolditalic": "MachineMadeWarp-BoldItalic.woff2", + "display": "MachineMadeWarpDisplay-Regular.woff2", + "displayitalic": "MachineMadeWarpDisplay-Italic.woff2", + }, + "licence": ROOT / "warp" / "OFL.txt", + "planned_axes": {}, + }, } # Heddle rows on its page: width names from the STAT table of the font. @@ -165,6 +181,8 @@ def stylistic_sets(font: TTFont) -> dict: def char_group(cp: int) -> str: ch = chr(cp) cat = unicodedata.category(ch) + if 0x370 <= cp <= 0x3FF: + return "Greek" if cp < 0x80: if cat == "Lu": return "Uppercase" @@ -181,7 +199,7 @@ def char_group(cp: int) -> str: GROUP_ORDER = ["Uppercase", "Lowercase", "Figures", "Punctuation and symbols", - "Accented and other letters", "Other figures", "Other symbols"] + "Accented and other letters", "Greek", "Other figures", "Other symbols"] def glyph_map(font: TTFont) -> list: @@ -432,7 +450,7 @@ def commits(limit: int = 10) -> list: # --------------------------------------------------------------------------- PAGES = ["index.html", "weft/index.html", "sans/index.html", "heddle/index.html", - "download/index.html", "404.html"] + "warp/index.html", "download/index.html", "404.html"] TOKEN = re.compile(r"\{\{\s*([A-Z0-9_]+)\s*\}\}") META = re.compile(r"\A\s*", re.S) @@ -539,6 +557,19 @@ def font_face_css(chosen: dict, info: dict) -> str: f"font-weight:{sh_w[0]:g} {sh_w[2]:g};font-stretch:{sh_d[0]:g}% {sh_d[2]:g}%;" "font-style:normal;font-display:swap}" ) + # Warp: six static fonts in two families. Text: Regular, Italic, Bold, + # Bold Italic (opsz 10). Display: Regular, Italic (opsz 60). + wf = chosen["warp"]["files"] + for fam_name, role, weight, style in ( + ("mm Warp", "vf", 400, "normal"), ("mm Warp", "italic", 400, "italic"), + ("mm Warp", "bold", 700, "normal"), ("mm Warp", "bolditalic", 700, "italic"), + ("mm Warp Display", "display", 400, "normal"), ("mm Warp Display", "displayitalic", 400, "italic"), + ): + out.append( + f"@font-face{{font-family:\"{fam_name}\";" + f"src:url(\"../fonts/warp/{url_path(wf[role].name)}\") format(\"woff2\");" + f"font-weight:{weight};font-style:{style};font-display:swap}}" + ) # machine-made Sans 0.1 has the straight forms as the default, so no # feature is on. The sharp forms are in ss02. out.append(":root{--sans-features:normal}") @@ -591,6 +622,9 @@ def build(out: Path) -> None: info["weft"]["commits"] = commits() heddle_font = TTFont(chosen["heddle"]["files"]["vf"]) info["heddle"]["glyphs"] = glyph_map(heddle_font) + warp_font = TTFont(chosen["warp"]["files"]["vf"]) + info["warp"]["glyphs"] = glyph_map(warp_font) + info["warp"]["styles"] = {role: "fonts/warp/" + url_path(p.name) for role, p in chosen["warp"]["files"].items()} heddle_widths = stat_widths(heddle_font) if not heddle_widths: raise BuildError("The Heddle font has no wdth values in STAT.") @@ -598,6 +632,7 @@ def build(out: Path) -> None: (out / "data" / "weft.json").write_text(json.dumps(info["weft"], ensure_ascii=False, indent=1), encoding="utf-8") (out / "data" / "sans.json").write_text(json.dumps(info["sans"], ensure_ascii=False, indent=1), encoding="utf-8") (out / "data" / "heddle.json").write_text(json.dumps(info["heddle"], ensure_ascii=False, indent=1), encoding="utf-8") + (out / "data" / "warp.json").write_text(json.dumps(info["warp"], ensure_ascii=False, indent=1), encoding="utf-8") (out / "data" / "readability.json").write_text(json.dumps(report, indent=1), encoding="utf-8") (out / "assets" / "fonts.css").write_text(font_face_css(chosen, info), encoding="utf-8") @@ -617,6 +652,10 @@ def build(out: Path) -> None: "WEFT_RELEASE": RELEASES["weft"], "SANS_RELEASE": RELEASES["sans"], "HEDDLE_RELEASE": RELEASES["heddle"], + "WARP_RELEASE": RELEASES["warp"], + "WARP_VERSION": html.escape(short_version(info["warp"])), + "WARP_CHAR_COUNT": info["warp"]["char_count"], + "WARP_GLYPH_COUNT": info["warp"]["glyph_count"], "HEDDLE_VERSION": html.escape(short_version(info["heddle"])), "HEDDLE_CHAR_COUNT": info["heddle"]["char_count"], "HEDDLE_WGHT_RANGE": fmt_range(heddle_axes.get("wght") or planned_sh["wght"]), @@ -646,7 +685,7 @@ def build(out: Path) -> None: depth = rel.count("/") root = "../" * depth if depth else "./" nav = {f"NAV_{k}": ' aria-current="page"' if meta.get("nav") == k.lower() else "" - for k in ("HOME", "WEFT", "SANS", "HEDDLE", "DOWNLOAD")} + for k in ("HOME", "WEFT", "SANS", "HEDDLE", "WARP", "DOWNLOAD")} scripts = "".join(f'' for s in meta.get("scripts", "").split(",") if s.strip()) # The 404 page can open at any depth, so it uses absolute paths. @@ -672,6 +711,7 @@ def build(out: Path) -> None: print(f" {fam}: {sel['files']['vf'].name}, axes {info[fam]['axes'] or 'none'}") print(f" readability: {len(report['rows'])} FRAG rows from {READABILITY.relative_to(ROOT)}") print(f" heddle widths: {', '.join(f'{v:g} {n}' for v, n in heddle_widths)}") + print(f" warp: {len(chosen['warp']['files'])} styles, {info['warp']['char_count']} characters") def main(argv: list[str]) -> int: diff --git a/site/pages/download/index.html b/site/pages/download/index.html index 1eb28dc..da30d61 100644 --- a/site/pages/download/index.html +++ b/site/pages/download/index.html @@ -1,6 +1,6 @@ +
+

machine-made type

+

machine-made Warp

+ +

A Modern serif for science, drawn by a pen

+

{{WARP_VERSION}} · {{WARP_CHAR_COUNT}} characters · six styles

+

∫ e−x² dx = √π

+

Warp is the fourth machine-made typeface: a Modern with vertical stress, for papers and maths. No outline is drawn by hand. Every letter is a few centre-lines swept by one level pen, and its hairlines, serifs and teardrops all come out of that pen.

+
+ +
+
+

i.

+
+

Set a line

+

Four styles for text at optical size 10, and Display for titles at optical size 60. The switch also sets the paragraph below.

+
+
+
+
+
+ Style +
+ + + + + +
+
+
+ + +
+
+
64 px
+ +
+
+ + +
+
+
+

Draw resonance in viscous threads, 1972

+
+
+
+ +
+
+

ii.

+
+

A paragraph from a paper

+

Set at 18 px and at 15 px, with old-style figures in the text and the italic for terms.

+
+
+
+
+

Abstract. We study the drawing of a thin viscous thread from a heated preform. When the draw ratio exceeds a critical value near 20.2, the steady state loses stability through a Hopf bifurcation and the radius of the fibre oscillates, a regime known as draw resonance. Writing the axial velocity as u(x, t) = U(x) + ε u′(x) eλt, we obtain a linear eigenvalue problem for λ, whose spectrum we compute by Chebyshev collocation on 128 points.

+

The leading mode depends only weakly on the capillary number Ca; inertia and cooling, not surface tension, set the threshold. Above it, the amplitude grows as (D − Dc)1/2, which is the signature of a supercritical bifurcation (Fig. 3, Table 1). The same scaling holds for drawn glass fibres and for polymer films at Reynolds numbers below 10−3.

+
+
+
+ +
+
+

iii.

+
+

Greek

+

All 24 capitals, 24 lowercase and the final sigma. Single pen movements, no slab serifs on the lowercase. The capitals that look like Latin ones are the Latin ones.

+
+
+
+
CapitalsΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
+
Lowercaseαβγδεζηθικλμνξοπρσςτυφχψω
+
Italicαβγδεζηθικλμνξοπρσςτυφχψω
+
Next to Latinaα oο uυ vν nη pρ wω
+
Maths∂u/∂t + (u · ∇)u = −∇p/ρ + ν∇²u   ∑ ∏ ∫ √ ∞ ≤ ≥ ≈ ≡ ∝ ∈ ∉ ⊂ ⊆ ∪ ∩ ∀ ∃ ∅ → ↔
+
+
+ +
+
+

iv.

+
+

Figures and small capitals

+

The default figures are lining and tabular, for tables and papers. Old-style figures are one feature away, as in the sketch. Small capitals are real small capitals: drawn at 1.1 times the x-height, with heavier hairlines.

+
+
+
+
Lining, tabular default0123456789 · 1,204.25 · 2026
+
Old-style onum0123456789 · 1,204.25 · 2026
+
Lining, proportional pnum0123456789 · 1,204.25 · 2026
+
Small capitals smcpFigure 3 and Table 1 in Section 4
+
All small capitals c2scNASA, ESA AND CERN
+
Ligatures ligafibre, flow, effect, office, baffle
+
+
+ +
+
+

v.

+
+

{{WARP_CHAR_COUNT}} characters

+

Read from the font's character map when the site is built. Select a character to see it large. {{WARP_GLYPH_COUNT}} glyphs in all, with the small capitals, the four figure styles and the ligatures.

+
+
+
+
+

a

+
+
Character
Latin Small Letter A
+
Unicode
U+0061
+
Glyph
a
+
+
+

Loading the character map.

+
+
+ +
+
+

vi.

+
+

From a sketch to a font

+

Agents write the generator, the tests and the CI. People set the goals and review the proofs.

+
+
+
+
+

The pen

+

A Python port of the approved sketch engine. Each stroke is a centre-line with a pressure per node; a level nib sweeps it. skia-pathops joins the sweep and the loom fits it with a few cubic curves. No outline comes from another font.

+

Read the loom

+
+
+

Proofs

+

The sketch engine and the font overlaid for every style; every glyph compared with its pen sweep; equations set by the sketch's maths setter with the font's glyphs; an OCR test against DejaVu Serif.

+

See the proofs

+
+
+

Next

+

0.2 adds the OpenType MATH table: stretchy delimiters, radicals and big operators drawn by the pen at each size. 0.3 makes Warp a variable font with weight and optical size axes.

+

Read the status

+
+
+
+ +
+

Get Warp 0.1

+

Download from GitHub Install and @font-face

+
diff --git a/site/templates/layout.html b/site/templates/layout.html index 44d2fe6..ec25a24 100644 --- a/site/templates/layout.html +++ b/site/templates/layout.html @@ -27,6 +27,7 @@
  • Weft
  • Sans
  • Heddle
  • +
  • Warp
  • Download
  • GitHub
  • diff --git a/warp/BRIEF.md b/warp/BRIEF.md new file mode 100644 index 0000000..9acc4bd --- /dev/null +++ b/warp/BRIEF.md @@ -0,0 +1,168 @@ +# Warp brief + +machine-made Warp is a Modern serif for science: papers, theses, +journals and maths. A level pen draws every letter. Its stress is +vertical, its hairlines are horizontal, its serifs are thin and lightly +bracketed, and its terminals are teardrops. This file sets the metrics, +the styles, the glyph set, the build and the name check. Source of all +shapes: the approved sketch 0.2 (`reference/warp-sketch-0.2.js`, glyph +tables `G` and `GI`; specimen page `reference/warp-sketch-0.2.html`). + +## Name check: "Warp" + +Result (2026-10-27, web search; first done for the sketch): **several +typefaces named "Warp" exist, all display or novelty faces. None is a +text serif.** The family ships only as "machine-made Warp" and +"machine-made Warp Display". The name table never has a bare "Warp". + +| Finding | Link | Risk | +| --- | --- | --- | +| Warp by Intellecta Design (Brazil), MyFonts, 2007, 3 styles, display. An exact name. | [myfonts.com/fonts/intellecta/warp](https://www.myfonts.com/fonts/intellecta/warp/), [myfonts.com/collections/warp-font-intellecta-design](https://www.myfonts.com/collections/warp-font-intellecta-design) | Medium for a bare "Warp". Low for "machine-made Warp": a different genre. | +| Tilt Warp by Andy Clymer, Google Fonts (OFL), a variable display face of the Tilt family. | [fonts.google.com/specimen/Tilt+Warp/about](https://fonts.google.com/specimen/Tilt+Warp/about), [github.com/googlefonts/Tilt-Fonts](https://github.com/googlefonts/Tilt-Fonts) | Low. Display; the name is "Tilt Warp". | +| Warp Three NF (Nick's Fonts), Warp Anark and Warp Letunical (Ingrimayne Type), West Warp (EVCCO), Funky Warp, Warp Thruster (free). All display or novelty. | [myfonts.com/fonts/nicksfonts/warp-three-nf](https://www.myfonts.com/fonts/nicksfonts/warp-three-nf), [myfonts.com/fonts/ingrimayne/warp-anark](https://myfonts.com/fonts/ingrimayne/warp-anark), [myfonts.com/fonts/evcco/west-warp](https://www.myfonts.com/fonts/evcco/west-warp/) | Low. | +| No "Warp" family on Adobe Fonts and no Fonts In Use typeface page turned up. | | None found. | +| Trademarks: USPTO WARP, reg. 6556914, class 42 (Warp, a terminal app for developers). Other WARP marks are in software classes. None found for fonts. | [trademark.justia.com/903/42/warp-90342560.html](https://trademark.justia.com/903/42/warp-90342560.html) | Low for a typeface. | + +Limits of this check: + +* A web search, not a trademark clearance. Before a wide release, do a + register search (USPTO, EUIPO eSearch, Swissreg, WIPO Global Brand + Database) for WARP in classes 9, 16 and 42. +* Some store search pages were not reachable from the build machine; + site-restricted web searches stood in for them. +* Fontspector's network checks (`fontdata_namecheck`) do not run + (`--skip-network`). + +## Styles + +Six static fonts. Pen sweeps are unions of nib hulls; their point +structure changes with the stem, the contrast and the optical size, so +masters are not point-compatible. 0.1 ships statics. The variable font +(wght and opsz) is planned for 0.3. + +| Family | Style | File | opsz | Weight class | +| --- | --- | --- | --- | --- | +| machine-made Warp | Regular, Italic, Bold, Bold Italic | `MachineMadeWarp-*.ttf` | 10 | 400, 700 | +| machine-made Warp Display | Regular, Italic | `MachineMadeWarpDisplay-*.ttf` | 60 | 400 | + +Every family name plus style name fits in 32 characters. The full name +of Display Regular is "machine-made Warp Display" ("Regular" elided; +with it the name is 33 characters). + +### Text optical size: opsz 10 + +opsz is the engine's optical-size parameter in points. It sets the +contrast, the hairline floor, the x-height, the side bearings and the +skeleton width. 0.1 sets the text styles at opsz 10: + +* it is the sturdiest value in the 10–12 range: hairlines 16.6 units + (Regular), the x-height 430, side bearings 44; +* the sketch sets its text below 20 px at opsz 10, and its paragraph at + opsz 9; +* the OCR test reads opsz 9, 10, 11 and 12 equally well within its noise + (mean CER over 9 to 16 px: 0.022, 0.026, 0.037, 0.018 for Regular); + the differences come from pixel rounding, not from the optical size. + +Display uses opsz 60, the sketch's display setting. + +### Parameters + +The sketch 0.2 defaults: contrast 9, pen angle 0 (level nib, vertical +stress), bracket 0.2, ball 1, serif 1, stem 84, x-height 412 at display, +superellipse exponent 2.1, smootherstep pressure, teardrop terminals. + +| Style | Stem A | Hairline B | Serif S | Serif length | Teardrop | Side bearing | Skeleton | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Regular, Italic (opsz 10) | 87.0 | 16.6 | 20.6 | 64.4 | 79.2 | 44.3 | x 1.032 | +| Bold, Bold Italic (opsz 10) | 138.7 (+59 %) | 21.5 (+30 %) | 27.0 (+31 %) | 66.7 | 126.4 | 44.3 | x 1.094 | +| Display, Display Italic (opsz 60) | 79.7 | 8.9 | 9.7 | 59.0 | 64.6 | 31.3 | x 1.003 | + +Bold: engine `stem` 134 (Regular 84), `contrast` 11.3, serif thickness +x 0.82 (`serifw`, new), serif length 0.65 (the same length as the +Regular's), skeleton x 1.06 (`width`, new) so that the counters stay +open. Capitals are 8 % lighter and 4.5 % narrower than the lowercase, +as in the sketch. + +## Metrics + +1000 units per em. Values in font units. + +| Metric | Text (opsz 10) | Display (opsz 60) | +| --- | --- | --- | +| x-height | 430 | 414 | +| Cap height | 670 | 670 | +| Ascender | 760 | 760 | +| Descender | -250 | -250 | +| Overshoot | 11 | 11 | +| Maths axis | 250 | 250 | +| Typo / hhea ascender, descender, line gap | 900, -300, 0 (line height 1200) | same | +| win ascent / descent | from the bounds of all six styles (941 / 441) | same | +| Underline position / thickness | -150 / 25 | same | +| Italic angle | -14.04° (slant 0.25) | same | + +## Outlines + +1. **Sweep** (`loom/engine.py`, a line-by-line port of the sketch + engine): each stroke is a centre-line with a pressure per node. The + nib is a superellipse (exponent 3.2), `a` long and `b` thick, level. + The ink is the convex hull of each pair of consecutive nib + positions, plus discs, teardrops and bracket fillets. +2. **Union** (`loom/outline.py`): skia-pathops joins the polygons + (nonzero fill). Slivers under 40 square units are dropped. +3. **Fit**: each contour is cut at its corners. Each run is fitted with + as few cubic curves as a 0.9-unit tolerance allows (Schneider's + method). Straight runs become lines. Curves are split at their x and + y extrema. No segment is shorter than 4 units. Outer contours wind + counter-clockwise (PostScript order). +4. **Check** (`tools/fidelity.py`): every font glyph, read back from the + compiled TTF, is compared with an independent fill of its sweep at + 16, 32, 64 and 128 pixels per em. Limit: 5 % of the ink. See + `proofs/warp/fidelity.json`. + +Median 76 points per glyph (Regular), at most 198. + +## Glyph set (0.1) + +429 glyphs, 308 code points (306 visible characters, space, no-break +space). 314 drawn glyphs, 114 composites. + +| Set | Characters | +| --- | --- | +| ASCII | all printable ASCII | +| Latin-1 | the letters with accents (composites of base and mark), `ß Æ æ Ø ø Ð ð Þ þ`, and `Œ œ Ÿ ı ȷ` | +| Punctuation and symbols | `‘ ’ “ ” ‚ „ ‹ › « » … • · € ₹ © ® ° — – ¡ ¿ § ¶ ¢ £ ¥ ¦ ¬ ª º ¹ ² ³ ¼ ½ ¾ µ`, the spacing accents, the no-break space | +| Greek | 24 capitals, 24 lowercase and final sigma. Drawn: `Γ Δ Θ Λ Ξ Π Σ Φ Ψ Ω` and all lowercase. References to Latin: `Α Β Ε Ζ Η Ι Κ Μ Ν Ο Ρ Τ Υ Χ`; `ο` refers to `o`, `µ` to `μ` | +| Maths | `+ − × ÷ ± = ≠ < > ≤ ≥ ∑ ∏ ∫ √ ∞ ∂ ∇ ′ ″ · ∘ → ← ↔ ∈ ∉ ⊂ ⊆ ∪ ∩ ∀ ∃ ∅ ≈ ≡ ∝` | +| Marks | combining grave, acute, circumflex, tilde, macron, dot, dieresis, ring, caron, cedilla (U+0300–0327), capital forms (`.case`), dotted circle | +| Ligatures | `ff fi fl ffi ffl` | +| Figures | lining tabular (default), lining proportional (`.lf`), old-style proportional (`.osf`), old-style tabular (`.tosf`), superiors and two denominators for ¹ ² ³ ¼ ½ ¾ | +| Small capitals | a–z, the accented lowercase, `æ œ ø ð þ` and the Greek lowercase (`.sc`) | + +The OpenType MATH table is planned for 0.2. + +## OpenType features + +| Feature | What | +| --- | --- | +| `ccmp` | i and j lose the dot before a combining mark above | +| `liga` | ff, fi, fl, ffi, ffl | +| `onum`, `lnum`, `pnum`, `tnum` | figures; default lining tabular; `onum` gives the sketch's old-style figures, proportional | +| `smcp`, `c2sc` | small capitals (ß becomes two small-cap S) | +| `kern` | class kerning (see below) | +| `mark`, `mkmk` | marks on base letters and on marks, from anchors | + +Kerning starts from the sketch's table (To Te Ta Tr Ty TA T. Av Aw AV +AT AY A' Va Ve Vo Ya Yo LT LV LY L' P. P, F. r. f' quotes and others) +and turns it into classes: accented letters and Greek look-alikes join +the Latin classes (ΤΑ, ΓΑ, Το, ΥΑ, Àv). The proportional figures have +a few pairs (7. 74). The default tabular figures are not kerned. Italic +uses 70 % of the values, as in the sketch. + +## Rules + +* All outlines come from `loom/sketch.py` and `loom/extended.py` + through the pen. Do not trace, import or copy outlines from any other + font. No outline or metric was taken from Computer Modern, Latin + Modern, STIX or Century. +* Fonts: SIL OFL 1.1, Reserved Font Name "machine-made" + (`warp/OFL.txt`). Generator code: Apache-2.0 (`LICENSE`). diff --git a/warp/CHANGELOG.md b/warp/CHANGELOG.md new file mode 100644 index 0000000..d4356f5 --- /dev/null +++ b/warp/CHANGELOG.md @@ -0,0 +1,38 @@ +# Warp changelog + +## 0.100 (Warp 0.1) + +### Added + +- The loom (`loom/`): a Python port of the approved sketch engine + (`reference/warp-sketch-0.2.js`): parameters, superellipse nib, + centre-line paths with pressure, serifs, beaks, teardrops, the rule + pen for maths, and every glyph of the sketch (`loom/sketch.py`). +- New glyphs drawn with the same pen (`loom/extended.py`): the rest of + printable ASCII, the Latin-1 letters and symbols, the marks for + accents, ffi and ffl, the rest of basic Greek (ζ η ι κ ν ξ τ υ χ ς, + Θ Λ Ξ Π Φ Ψ), 23 maths symbols, ₹ and the dotted circle. +- Outlines: the sweep polygons are joined with skia-pathops and fitted + with cubic curves (`loom/outline.py`). +- Six static fonts: machine-made Warp Regular, Italic, Bold, Bold + Italic (opsz 10) and machine-made Warp Display Regular, Italic + (opsz 60). TTF, OTF and WOFF2. +- 429 glyphs, 308 code points: ASCII, Latin-1, Œ œ Ÿ ı ȷ, basic Greek, + maths, marks, ligatures, four figure styles, small capitals. +- OpenType: ccmp, liga, onum, lnum, pnum, tnum, smcp, c2sc, kern, mark, + mkmk. Class kerning from the sketch's table. +- `tools/fidelity.py` (outlines against the sweep), + `tools/readability.py` (OCR gate against DejaVu Serif), + `tools/proof.py` and `tools/sketch_node.py` (proofs; the sketch engine + in node), `tools/finish.py` (STAT, WOFF2), `scripts/build.sh`, + `scripts/check.sh`. +- Tests (`tests/test_warp.py`): metrics, glyph set, the port against the + sketch engine, outlines, kerning, features, names, reports, + determinism. +- `warp/OFL.txt`. + +### Changed from the sketch + +- The bar of A, Æ and ∀ reaches the hairline (the counter was open). +- The maths operators share one advance. +- See `STATUS.md`, "Deviations from the sketch". diff --git a/warp/OFL.txt b/warp/OFL.txt new file mode 100644 index 0000000..f09aee4 --- /dev/null +++ b/warp/OFL.txt @@ -0,0 +1,94 @@ +Copyright 2026 The machine-made Warp Project Authors (https://github.com/machine-made/type), +with Reserved Font Name "machine-made". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/warp/RELEASE-NOTES-0.1.md b/warp/RELEASE-NOTES-0.1.md new file mode 100644 index 0000000..4c81c23 --- /dev/null +++ b/warp/RELEASE-NOTES-0.1.md @@ -0,0 +1,67 @@ +# machine-made Warp 0.1 + +Warp is a Modern serif for science, made by code. A level pen draws +every letter: each glyph is a few centre-lines with a pressure per +node, swept by one nib. Vertical stress, hairlines, thin bracketed +serifs and teardrop terminals all come out of that pen. Warp is set for +papers: lining tabular figures by default, old-style figures and small +capitals on request, full basic Greek, and the common maths symbols on +the maths axis. + +## What is in 0.1 + +**machine-made Warp** (text, optical size 10, for 9 to 12 pt): +Regular, Italic, Bold, Bold Italic. + +**machine-made Warp Display** (optical size 60, for titles): Regular, +Italic. + +Static fonts in `ttf/` and `otf/`, WOFF2 in `webfonts/`. The variable +font (weight and optical size) is planned for 0.3. + +**Characters**: printable ASCII; the Latin-1 letters and symbols; Œ œ Ÿ; +basic Greek (24 capitals, 24 lowercase, final sigma); + − × ÷ ± = ≠ < > +≤ ≥ ∑ ∏ ∫ √ ∞ ∂ ∇ ′ ″ · ∘ → ← ↔ ∈ ∉ ⊂ ⊆ ∪ ∩ ∀ ∃ ∅ ≈ ≡ ∝; € ₹ © ®; combining +marks. 306 characters, 429 glyphs. + +**Features**: ff fi fl ffi ffl ligatures (`liga`); old-style figures +(`onum`); lining, proportional and tabular figures (`lnum`, `pnum`, +`tnum`); small capitals (`smcp`, `c2sc`); class kerning (`kern`); marks +(`mark`, `mkmk`). + +**Italic**: a true cursive lowercase. The capitals A C G M N O Q V W +have their own italic forms; the others are the roman capitals, slanted, +with unbracketed serifs. + +## Proofs + +`proofs/warp/`: the sketch engine and the font overlaid for every style, +a paragraph from a paper at 9 to 12 pt, the Greek set, equations set by +the sketch's maths setter with the font's glyphs, kerning, accents, +small capitals and figures, the six styles, the full character set, and +the reports `fidelity.json`, `readability.json`, `sketch-compare.json`. + +## Checks + +- Fontspector 1.9.0, universal profile: no FAIL, no ERROR. +- Every glyph matches its pen sweep within 5 % of the ink at 16 to 128 + pixels per em. +- OCR (Tesseract) at 10, 12 and 14 px: character error rate 2.3 % + (Regular) and 1.9 % (Italic), against 0.1 % and 0 % for DejaVu Serif. +- The build is byte-reproducible. + +## Known limits + +- No OpenType MATH table yet (0.2): stretchy delimiters, radicals and + big operators come in one size. For TeX, use Warp for text next to a + maths font. +- At 10 px on screen, OCR reads g, f, fi and fl less well than in DejaVu + Serif. No hinting. +- Latin-1 and basic Greek only: no Latin Extended-A beyond Œ œ Ÿ, no + Greek accents, no Cyrillic. +- Italic capitals other than A C G M N O Q V W are slanted roman. + +## Licence + +Fonts: SIL Open Font License 1.1, Reserved Font Name "machine-made". +Generator code: Apache-2.0. diff --git a/warp/STATUS.md b/warp/STATUS.md new file mode 100644 index 0000000..5b24a3b --- /dev/null +++ b/warp/STATUS.md @@ -0,0 +1,103 @@ +# Warp status + +| Field | Value | +| --- | --- | +| Family | machine-made Warp, machine-made Warp Display | +| Version | 0.1 (font version 0.100) | +| Pull request | branch `warp/v0.1` (PR to open) | +| Stage | Warp 0.1 built: 6 static styles (TTF, OTF, WOFF2) | +| Gate now | Jac reviews the PR; the founder reviews the proofs by eye | +| Next | Release 0.1 (tag `warp-v0.1.0`). Then 0.2 (MATH table) and 0.3 (variable font) | +| Last local run | 2026-10-27: tests, build, byte check, fidelity, OCR gate, Fontspector, site build all green | +| Last green CI | none yet (first run is on the PR) | + +## Proven (local run, 2026-10-27) + +- 44 pytest tests pass. One runs the sketch engine in node and compares + the advances and the ink bounds of the 120 sketch characters, the + old-style figures and the small capitals with the port (Regular, + Italic, Display, Display Italic; within 0.02 unit). +- The build is byte-reproducible: two builds give the same files (also + one with one process and one with two). +- Fontspector 1.9.0, universal profile: 0 FAIL, 0 ERROR on the 6 TTF and + the 6 OTF (accepted WARNs below). +- Outline fidelity (`proofs/warp/fidelity.json`): every drawn glyph of + every style, read back from the compiled TTF, against an independent + fill of its pen sweep. Largest difference 4.7 % of the ink (Display + ξ small cap, 128 ppem; hairlines of 9 units and integer points), mean + 0.7 to 1.7 %. Limit 5 %. +- Sketch comparison (`proofs/warp/sketch-compare.json`): mean IoU with + the sketch engine 0.95 (Regular), 0.95 (Italic), 0.95 (Display), + 0.94 (Display Italic). × < > score low because they moved (see + "Deviations"). Bold and Bold Italic score 0.81: the bold skeleton is + 6 % wider on purpose. +- OCR readability gate (`proofs/warp/readability.json`, Tesseract 5.3.4, + 10, 12 and 14 px): see below. Passes. + +## Readability (OCR) + +| Style | Warp CER | DejaVu Serif CER | Limit | Result | +| --- | --- | --- | --- | --- | +| Regular | 0.0234 (10 px 0.034, 12 px 0.020, 14 px 0.016) | 0.0012 | 0.0312 | pass | +| Italic | 0.0192 (10 px 0.049, 12 px 0.004, 14 px 0.005) | 0.0000 | 0.0300 | pass | + +Warp reads a little worse than DejaVu Serif, a screen face. The gap is +about 2 points at 10 px and closes at 12–14 px in the italic. The +errors come from approved forms: Tesseract reads the open lower loop of +g as a comma ("ng, t"), f as t, fi and fl as h and H, and joins words at +the narrow word space. The optical size does not change this (opsz 9 +to 12 all give about 2 %). The margin (3 points) stops a regression; it +does not prove parity. + +## Fontspector WARNs (accepted) + +| Check | Files | Reason | +| --- | --- | --- | +| `name/family_and_style_max_length` | Display TTF and OTF | PostScript names over 27 characters (`MachineMadeWarpDisplay-Regular`, `-Italic`). Only old PostScript printers care. | +| `unreachable_glyphs` | OTF | The capital marks (`*comb.case`) and `two.dnom`, `four.dnom` are used only inside composites; CFF flattens composites. The TTF keep them as components. | + +## Deviations from the sketch + +| What | Sketch 0.2 | Font 0.1 | Why | +| --- | --- | --- | --- | +| Outlines | canvas fill of hull polygons | union with skia-pathops, cubic fit (0.9 unit), extrema points | Clean outlines with few points. Checked by `tools/fidelity.py`. | +| Bar of A (roman and italic), Æ, ∀ | starts at x 95 (A) or 105 (italic A) | starts at 72 or 84 | The bar stopped a few units short of the hairline: the counter was open (Fontspector `contour_count`). | +| × < > | own advances | the advance of + (with ÷ ± ≠ ≤ ≥ ∈ ∉ ⊂ ⊆ ≈ ≡ ∝ ¬) | Operators line up in tables (Fontspector `math_signs_width`). The ink is the sketch's. | +| Lowercase head serifs | the engine has a wedge serif, but it reads `p.lc`, which `params()` never sets | flat serifs, as the sketch draws them | The port follows what the approved page shows. | +| Bold | not in the sketch (the sketch has the `stem` parameter) | stem 134, contrast 11.3, serif x 0.82 thick and 0.65 long, skeleton x 1.06 | A true bold, not a smear (see `proofs/warp/warp-styles.png`, `warp-paragraph.png`). | +| Italic capitals other than A C G M N O Q V W | slanted roman | slanted roman with unbracketed serifs | Didone italics are unbracketed; slanted brackets look lopsided. No roman serif sits where the italic draws a hook or a drop (those letters have their own forms). | +| Optical sizes | continuous `opsz` | two statics: opsz 10 (text) and 60 (Display) | Statics in 0.1; `opsz` axis in 0.3. Maths scripts in the setter use smaller optical sizes; the font has one. | +| Stretchy delimiters, radicals, big operators at size | drawn by the pen at any height | one size each | The MATH table (variants, assemblies) is 0.2. | +| Nib cache | the engine caches nibs by keys rounded to 0.01 | no cache | Same shapes within 0.01 unit. | + +New glyphs (not in the sketch), drawn with the same pen: the rest of +ASCII, the Latin-1 letters and symbols, the marks, ffi and ffl, the +rest of Greek, 23 maths symbols, ₹, the dotted circle. + +## Open issues for 0.2 + +- OpenType MATH table: constants (axis 250, rule thickness, script + sizes and shifts), italic correction, top accent attachment, size + variants and assemblies for ( ) [ ] { } √ ∫ ∑ ∏, drawn by the pen at + each height. +- OCR: the open loop of g, f, fi and fl at 10 px; the word space (270 + units) is narrow for text. Try a wider space at text size and a + firmer f hook, then re-run the gate. +- Ξ is all horizontal strokes, so the level pen makes it light (Σ too, a + little). Consider a heavier middle bar. +- Italic capitals other than A C G M N O Q V W: own cursive forms; the + italic & is the roman one, slanted. +- More kerning: lowercase pairs, Greek lowercase, italic-specific pairs. +- Latin Extended-A, Greek tonos and dialytika, more symbols (‰ ‡ † ℓ ℏ). +- Superior and inferior figures as a feature (`sups`, `subs`, `frac`). +- Name: register search for WARP before a wide release (see `BRIEF.md`). +- No hinting. + +## Planned for 0.3 + +- The variable font, axes `wght` and `opsz`, from the same pen model. + Pen sweeps are not point-compatible across masters, so 0.3 needs a + fixed outline topology per glyph (for example an analytic sweep: + offset each segment by the nib's support point, join with nib arcs, + with a fixed point count), then masters at the axis corners and a + drift report as for Heddle. diff --git a/warp/loom/__init__.py b/warp/loom/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/warp/loom/build.py b/warp/loom/build.py new file mode 100644 index 0000000..07dafc9 --- /dev/null +++ b/warp/loom/build.py @@ -0,0 +1,415 @@ +"""Build the machine-made Warp 0.1 sources: one UFO per static style. + +Usage: + python -m loom.build [--out build] [--styles Regular,Italic,...] + +Writes build/ufo/MachineMadeWarp-{Style}.ufo and +build/ufo/MachineMadeWarpDisplay-{Style}.ufo. fontmake compiles them +(scripts/build.sh). + +Each glyph is swept by the pen model (loom/engine.py), its sweep is +unioned with skia-pathops and fitted with cubic curves +(loom/outline.py). Accented letters, the Greek capitals that look like +Latin ones, µ, ο and the fractions are composites. + +Why statics and not a variable font: a pen sweep is a union of nib +hulls. Its point structure changes with the stem, the contrast and the +optical size, so masters are not point-compatible without a fixed +outline topology. The variable font (wght, opsz) is planned for 0.3. + +Apache-2.0. +""" + +from __future__ import annotations + +import argparse +import math +import shutil +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +import ufoLib2 +from ufoLib2.objects import Anchor, Component + +from loom import charset as CS +from loom import engine as E +from loom import features as F +from loom import kerning as K +from loom import outline as O +from loom.sketch import G, GI + +VERSION = (0, 100) # 0.100: Warp 0.1 +COPYRIGHT = "Copyright 2026 The machine-made Warp Project Authors (https://github.com/machine-made/type)" +LICENSE_DESCRIPTION = ( + "This Font Software is licensed under the SIL Open Font License, " + "Version 1.1. This license is available with a FAQ at: " + "https://openfontlicense.org" +) +LICENSE_URL = "https://openfontlicense.org" +UPM = 1000 +TYPO_ASCENDER = 900 +TYPO_DESCENDER = -300 +UNDERLINE_POSITION = -150 +UNDERLINE_THICKNESS = 25 + +# Optical sizes of the statics (the engine's opsz parameter, in points). +# Text: opsz 10 sets best at 9 to 12 pt (tools/readability.py compares +# 9, 10, 11 and 12; see BRIEF.md). Display: opsz 60, for titles. +TEXT_OPSZ = 10 +DISPLAY_OPSZ = 60 + +# Bold: stem +59.5 % (84 -> 134 units at opsz 72), hairline +30 % +# (contrast 11.3 instead of 9), serifs +30 % thick (serifw 0.82) and as +# long as the Regular's (serif 0.65 of the bold stem), skeleton 6 % wider +# so that the counters stay open. +BOLD = dict(stem=134, contrast=11.3, serifw=0.82, serif=0.65, width=1.06) + + +@dataclass(frozen=True) +class Style: + key: str # file style: Regular, Italic, Bold, BoldItalic, Display, DisplayItalic + family: str + style: str # name table style + ps_family: str + ps_style: str + weight: int + italic: bool + opsz: int + params: tuple # engine parameters + + def p(self) -> E.Params: + return E.params(**dict(self.params)) + + +def _style(key, family, style, ps_family, ps_style, weight, italic, opsz, **extra): + prm = dict(opsz=opsz, ital=italic, **extra) + return Style(key, family, style, ps_family, ps_style, weight, italic, opsz, tuple(sorted(prm.items()))) + + +FAMILY = "machine-made Warp" +DISPLAY_FAMILY = "machine-made Warp Display" +PS_FAMILY = "MachineMadeWarp" +PS_DISPLAY = "MachineMadeWarpDisplay" + +STYLES = { + "Regular": _style("Regular", FAMILY, "Regular", PS_FAMILY, "Regular", 400, False, TEXT_OPSZ), + "Italic": _style("Italic", FAMILY, "Italic", PS_FAMILY, "Italic", 400, True, TEXT_OPSZ), + "Bold": _style("Bold", FAMILY, "Bold", PS_FAMILY, "Bold", 700, False, TEXT_OPSZ, **BOLD), + "BoldItalic": _style("BoldItalic", FAMILY, "Bold Italic", PS_FAMILY, "BoldItalic", 700, True, TEXT_OPSZ, **BOLD), + "Display": _style("Display", DISPLAY_FAMILY, "Regular", PS_DISPLAY, "Regular", 400, False, DISPLAY_OPSZ), + "DisplayItalic": _style("DisplayItalic", DISPLAY_FAMILY, "Italic", PS_DISPLAY, "Italic", 400, True, DISPLAY_OPSZ), +} + + +def ps_name(st: Style) -> str: + return f"{st.ps_family}-{st.ps_style}" + + +def rnd(v: float) -> int: + return int(math.floor(v + 0.5)) + + +# ------------------------------------------------------------- variants + +def sup_mods(p: E.Params, top: float) -> dict: + """Superior forms: the drawing at 60 % size, lighter, narrower, raised so its top is at `top`.""" + k = 0.6 + q = {"cap": p.cap * k, "xh": p.xh * k, "A": p.A * 0.72, "B": max(p.B * 0.85, 5), "S": p.S * 0.8, + "sl": p.sl * 0.6, "bd": p.bd * 0.7, "os": p.os * k, "sb": p.sb * 0.6} + return {"p": q, "wdx": 0.62, "prop": True, "dy": top} + + +def variant_mods(gl: CS.Glyph, p: E.Params) -> dict: + v = gl.variant + if v is None: + return {} + if v == "sc": + return E.sc_mods(p) + if v == "osf": + return E.osf_mods(p, gl.key) + if v == "tosf": + m = E.osf_mods(p, gl.key) + m["prop"] = False + return m + if v == "lf": + return {"prop": True} + if v == "sup": + m = sup_mods(p, 0) + h = m["p"]["xh"] if gl.key in ("a", "o") else m["p"]["cap"] + m["dy"] = p.cap - h + (p.os if gl.key in ("a", "o") else 0) + return m + if v == "dnom": + return sup_mods(p, 0) + raise ValueError(v) + + +# ---------------------------------------------------------------- glyphs + +@dataclass +class Drawn: + contours: list # fitted contours (segments), already moved by dx + adv: float + center: float # unslanted ink centre of the middle band, in glyph coordinates + yc: float + sl: float + top: float # height where a top mark sits + ymax: float + sweep: object = None + dx: float = 0.0 + + +def table_def(key: str, ital: bool): + return (GI.get(key) if ital else None) or G[key] + + +def band_center(sw: E.Swept, lo: float, hi: float) -> float | None: + xs = [] + for poly in sw.polys: + for x, y in poly: + if lo <= y <= hi: + xs.append(x - (y - sw.yc) * sw.sl) + if not xs: + return None + return (min(xs) + max(xs)) / 2 + + +@lru_cache(maxsize=None) +def drawn(style_key: str, name: str) -> Drawn: + st = STYLES[style_key] + p = st.p() + gl = CS.BY_NAME[name] + if gl.variant == "notdef": + return notdef(p) + d = table_def(gl.key, p.ital) + mods = variant_mods(gl, p) + if p.ital and d.cap and gl.key not in GI: + # Italic capitals without their own form are the roman ones, + # slanted, with unbracketed serifs (as in Didone italics). + mods = dict(mods) + mods["p"] = {**mods.get("p", {}), "bk": 0} + sw = E.compose(d, p, mods) + adv, dx = sw.adv, sw.dx + if gl.variant is None and E.is_greek_lower(gl.key): + adv += p.sb * 0.5 + dx += p.sb * 0.25 + if gl.category == "mark": + adv, dx = 0.0, 0.0 + if gl.variant is None and gl.key in CS.EQUAL_WIDTH_MATHS and gl.key != "+": + adv = drawn(style_key, "plus").adv + dx = (adv - (sw.xmax - sw.xmin)) / 2 - sw.xmin + contours = O.outline(sw.polys) + contours = [[(s[0], s[1] + (dx, 0), s[2] + (dx, 0)) if s[0] == "line" else (s[0], s[1] + (dx, 0)) for s in c] + for c in contours] + if gl.height == "cap": + top = p.cap + elif gl.height == "sc": + top = p.xh * 1.1 + else: + top = p.xh + c = band_center(sw, top * 0.2, top * 0.8) + if c is None: + c = (sw.xmin + sw.xmax) / 2 + return Drawn(contours, adv, c + dx, sw.yc, sw.sl, top, sw.ymax, sw, dx) + + +def notdef(p: E.Params) -> Drawn: + import numpy as np + w, h, t = 480, p.cap, 50 + x0 = 60 + + def box(a, b, c, d, ccw=True): + pts = [(a, b), (c, b), (c, d), (a, d)] + if not ccw: + pts = pts[::-1] + return [("line", np.array(pts[i], float), np.array(pts[(i + 1) % 4], float)) for i in range(4)] + contours = [box(x0, 0, x0 + w, h), box(x0 + t, t, x0 + w - t, h - t, ccw=False)] + return Drawn(contours, w + 2 * x0, x0 + w / 2, 0, 0, h, h) + + +def component_offsets(style_key: str, gl: CS.Glyph) -> tuple[list, float]: + """[(component, dx, dy)] and the advance of a composite glyph.""" + st = STYLES[style_key] + p = st.p() + if gl.comp_kind == "ref": + b = drawn(style_key, gl.comps[0]) + return [(gl.comps[0], 0, 0)], b.adv + if gl.comp_kind == "greek-ref": + b = drawn(style_key, gl.comps[0]) + return [(gl.comps[0], p.sb * 0.25, 0)], b.adv + p.sb * 0.5 + if gl.comp_kind == "frac": + parts, x = [], 0.0 + for k, nm in enumerate(gl.comps): + dd = drawn(style_key, nm) + parts.append((nm, x, 0)) + x += dd.adv + return parts, x + base, mark = gl.comps + b = drawn(style_key, base) + m = drawn(style_key, mark) + dy = 0.0 + if gl.comp_kind == "sc-accent" and mark != "cedillacomb": + dy = b.top - p.cap + dx = b.center + dy * b.sl + adv = b.adv + # a mark wider than a narrow base (ı, I) widens the glyph a little + return [(base, 0, 0), (mark, dx, dy)], adv + + +# -------------------------------------------------------------- anchors + +def anchors(style_key: str, name: str) -> list[tuple[str, float, float]]: + gl = CS.BY_NAME[name] + if gl.comps or gl.variant == "notdef": + return [] + d = drawn(style_key, name) + if gl.category == "mark": + from loom.extended import BELOW + p = STYLES[style_key].p() + base_y = p.cap if name.endswith(".case") else p.xh + if name.split(".")[0] in BELOW: + return [("_bottom", (0 - d.yc) * d.sl, 0)] + top = d.ymax + return [("_top", (base_y - d.yc) * d.sl, base_y), ("top", (top - d.yc) * d.sl, top)] + if gl.category == "ligature": + n = gl.name.count("_") + 1 if "_" in gl.name else 2 + return [(f"caret_{k}", d.adv * k / n, 0) for k in range(1, n)] + if not gl.unicodes or gl.variant: + return [] + ch = chr(gl.unicodes[0]) + if not ch.isalpha() or gl.category == "ligature": + return [] + out = [("top", d.center + (d.top - d.yc) * d.sl, d.top)] + out.append(("bottom", d.center + (0 - d.yc) * d.sl, 0)) + return out + + +# ------------------------------------------------------------------ UFO + +def set_info(font: ufoLib2.Font, st: Style) -> None: + p = st.p() + info = font.info + info.familyName = st.family + info.styleName = st.style + info.styleMapFamilyName = st.family + info.styleMapStyleName = {("Regular"): "regular", "Italic": "italic", "Bold": "bold", + "Bold Italic": "bold italic"}[st.style] + info.versionMajor, info.versionMinor = VERSION + info.copyright = COPYRIGHT + info.openTypeNameLicense = LICENSE_DESCRIPTION + info.openTypeNameLicenseURL = LICENSE_URL + info.openTypeNameManufacturer = "machine-made" + info.openTypeNameManufacturerURL = "https://github.com/machine-made" + info.openTypeNameDesigner = "machine-made agents" + info.openTypeNameDescription = ( + "Warp is a Modern serif for science by machine-made: every letter is a few " + "centre-lines swept by one level pen." + ) + info.postscriptFontName = ps_name(st) + info.postscriptIsFixedPitch = False + info.postscriptUnderlinePosition = UNDERLINE_POSITION + info.postscriptUnderlineThickness = UNDERLINE_THICKNESS + info.openTypeOS2VendorID = "NONE" + info.openTypeOS2WeightClass = st.weight + info.openTypeOS2WidthClass = 5 + info.openTypeOS2Type = [] + # Bit 7: USE_TYPO_METRICS. Bit 8: WWS (both families name only + # weight and slope in the style; "Display" is in the family name). + info.openTypeOS2Selection = [7, 8] + info.openTypeOS2Panose = [2, 0, 5 if st.weight == 400 else 8, 3, 0, 0, 0, 0, 0, 0] + info.unitsPerEm = UPM + info.xHeight = rnd(p.xh) + info.capHeight = rnd(p.cap) + info.ascender = rnd(p.asc) + info.descender = rnd(p.desc) + info.italicAngle = -round(math.degrees(math.atan(p.slant)), 2) if st.italic else 0 + info.openTypeOS2TypoAscender = TYPO_ASCENDER + info.openTypeOS2TypoDescender = TYPO_DESCENDER + info.openTypeOS2TypoLineGap = 0 + info.openTypeHheaAscender = TYPO_ASCENDER + info.openTypeHheaDescender = TYPO_DESCENDER + info.openTypeHheaLineGap = 0 + full = f"{st.family} {st.style}" + if len(full) > 32: + # "machine-made Warp Display Regular" is 33 characters: the full + # name elides "Regular", as many families do. + full = st.family + info.openTypeNameRecords = [ + {"nameID": 4, "platformID": 3, "encodingID": 1, "languageID": 0x409, "string": full}, + ] + assert len(full) <= 32 + font.lib["public.postscriptNames"] = {} + font.lib["com.github.googlei18n.ufo2ft.featureWriters"] = [ + {"class": "KernFeatureWriter"}, + {"class": "MarkFeatureWriter"}, + {"class": "GdefFeatureWriter"}, + ] + + +def build_font(style_key: str) -> ufoLib2.Font: + st = STYLES[style_key] + font = ufoLib2.Font() + set_info(font, st) + font.lib["public.glyphOrder"] = list(CS.ORDER) + cats = {} + for gl in CS.GLYPHS: + g = font.newGlyph(gl.name) + g.unicodes = list(gl.unicodes) + if gl.comps: + comps, adv = component_offsets(style_key, gl) + g.width = rnd(adv) + for base, dx, dy in comps: + g.components.append(Component(base, transformation=(1, 0, 0, 1, rnd(dx), rnd(dy)))) + else: + d = drawn(style_key, gl.name) + g.width = rnd(d.adv) + O.draw(d.contours, g.getPen()) + for nm, x, y in anchors(style_key, gl.name): + g.anchors.append(Anchor(rnd(x), rnd(y), nm)) + cats[gl.name] = gl.category + font.lib["public.openTypeCategories"] = cats + font.groups.update(K.groups()) + font.kerning.update(K.kerning(st)) + font.features.text = F.features(st) + return font + + +def win_metrics(fonts) -> tuple[int, int]: + y_min, y_max = 0.0, 0.0 + for f in fonts: + for glyph in f: + b = glyph.getBounds(f) + if b is not None: + y_min = min(y_min, b.yMin) + y_max = max(y_max, b.yMax) + return max(TYPO_ASCENDER, math.ceil(y_max)), max(-TYPO_DESCENDER, math.ceil(-y_min)) + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Build the Warp 0.1 UFO sources.") + ap.add_argument("--out", default="build") + ap.add_argument("--styles", default=",".join(STYLES)) + ap.add_argument("--jobs", type=int, default=1, help="Build styles in parallel processes.") + args = ap.parse_args(argv) + out = Path(args.out) / "ufo" + if out.exists(): + shutil.rmtree(out) + out.mkdir(parents=True) + keys = [k for k in args.styles.split(",") if k] + if args.jobs > 1: + from concurrent.futures import ProcessPoolExecutor + with ProcessPoolExecutor(args.jobs) as ex: + fonts = dict(zip(keys, ex.map(build_font, keys))) + else: + fonts = {k: build_font(k) for k in keys} + metrics = win_metrics(fonts.values()) + for k, f in fonts.items(): + f.info.openTypeOS2WinAscent, f.info.openTypeOS2WinDescent = metrics + path = out / f"{ps_name(STYLES[k])}.ufo" + f.save(path, overwrite=True) + print(f"Wrote {path} ({len(f)} glyphs).") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/warp/loom/charset.py b/warp/loom/charset.py new file mode 100644 index 0000000..c873662 --- /dev/null +++ b/warp/loom/charset.py @@ -0,0 +1,228 @@ +"""The glyph set of machine-made Warp 0.1: names, code points, sources. + +Every glyph is one of: + +* drawn: a key into the glyph tables (loom/sketch.py, loom/extended.py) + plus an optional variant (small cap, figure style, superior); +* composite: a base glyph and a mark (accented letters), or a single + reference (Greek capitals that look like Latin ones, µ, ο). + +Apache-2.0. +""" + +from __future__ import annotations + +import unicodedata +from dataclasses import dataclass, field + +from fontTools.agl import UV2AGL + +from loom import extended as X # noqa: F401 (registers the new glyphs) +from loom import sketch as S + +GREEK_NAMES = { + 0x391: "Alpha", 0x392: "Beta", 0x393: "Gamma", 0x394: "Delta", 0x395: "Epsilon", 0x396: "Zeta", + 0x397: "Eta", 0x398: "Theta", 0x399: "Iota", 0x39A: "Kappa", 0x39B: "Lambda", 0x39C: "Mu", + 0x39D: "Nu", 0x39E: "Xi", 0x39F: "Omicron", 0x3A0: "Pi", 0x3A1: "Rho", 0x3A3: "Sigma", + 0x3A4: "Tau", 0x3A5: "Upsilon", 0x3A6: "Phi", 0x3A7: "Chi", 0x3A8: "Psi", 0x3A9: "Omega", + 0x3B1: "alpha", 0x3B2: "beta", 0x3B3: "gamma", 0x3B4: "delta", 0x3B5: "epsilon", 0x3B6: "zeta", + 0x3B7: "eta", 0x3B8: "theta", 0x3B9: "iota", 0x3BA: "kappa", 0x3BB: "lambda", 0x3BC: "mu", + 0x3BD: "nu", 0x3BE: "xi", 0x3BF: "omicron", 0x3C0: "pi", 0x3C1: "rho", 0x3C2: "sigmafinal", + 0x3C3: "sigma", 0x3C4: "tau", 0x3C5: "upsilon", 0x3C6: "phi", 0x3C7: "chi", 0x3C8: "psi", + 0x3C9: "omega", +} +SPECIAL_NAMES = { + 0x00A0: "uni00A0", 0x00B9: "onesuperior", 0x00B2: "twosuperior", 0x00B3: "threesuperior", 0x00B5: "micro", 0x0237: "dotlessj", 0x2218: "ringoperator", 0x2207: "nabla", + 0x2211: "summation", 0x220F: "product", 0x2206: "increment", 0x25CC: "dottedcircle", + 0xFB00: "f_f", 0xFB01: "fi", 0xFB02: "fl", 0xFB03: "f_f_i", 0xFB04: "f_f_l", +} + +# Greek capitals that are Latin look-alikes: a reference to the Latin glyph. +GREEK_LOOKALIKE = {"Α": "A", "Β": "B", "Ε": "E", "Ζ": "Z", "Η": "H", "Ι": "I", "Κ": "K", "Μ": "M", + "Ν": "N", "Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X"} + +# Marks with a capital form (".case"): the ones the accented capitals use. +CASE_MARKS = ("gravecomb", "acutecomb", "circumflexcomb", "tildecomb", "dieresiscomb", "ringcomb") +# Maths operators that share one advance (the advance of +), so that +# they line up in tables. The sketch gave < > × their own widths. +EQUAL_WIDTH_MATHS = "+<=>±×÷−∈∉∝≈≠≡≤≥⊂⊆¬" + +FIGURES = "0123456789" +FIG_NAMES = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"] +# Figure styles. Default: lining, tabular (the sketch's figures). +FIG_STYLES = {"lf": "lining, proportional", "osf": "old-style, proportional", "tosf": "old-style, tabular"} + +ASCII = "".join(chr(c) for c in range(0x20, 0x7F)) +PUNCT = "‘’“”‚„‹›«»…•·€©®°—–¡¿§¶¢£¥¦¬¨´¯¸ˆ˜˚ˇ˙" +LATIN1_LETTERS = "".join(chr(c) for c in range(0xC0, 0x100) if unicodedata.category(chr(c)).startswith("L")) +LATIN_EXTRA = "ŒœŸıȷ" +GREEK = "".join(chr(c) for c in range(0x391, 0x3AA) if c != 0x3A2) + "".join(chr(c) for c in range(0x3B1, 0x3CA)) +RUPEE = "₹" +MATHS = "+−×÷±=≠<>≤≥∑∏∫√∞∂∇′″·∘→←↔∈∉⊂⊆∪∩∀∃∅≈≡∝" +OTHER = " ⁄¹²³¼½¾ªºµ◌" +LIGATURES = "fffiflffiffl" + + +def glyph_name(ch: str) -> str: + u = ord(ch) + if u in GREEK_NAMES: + return GREEK_NAMES[u] + if u in SPECIAL_NAMES: + return SPECIAL_NAMES[u] + from loom.extended import MARK_UNICODE + for n, mu in MARK_UNICODE.items(): + if mu == u: + return n + name = UV2AGL.get(u) + return name if name else f"uni{u:04X}" + + +@dataclass +class Glyph: + name: str + unicodes: list = field(default_factory=list) + key: str | None = None # drawing key (sketch / extended tables) + variant: str | None = None # sc, lf, osf, tosf, sup, dnom + comps: list | None = None # composite: [base] or [base, mark] + comp_kind: str | None = None # "accent", "sc-accent", "ref", "greek-ref" + category: str = "base" # base, mark, ligature + height: str = "x" # x, cap, sc: where a top mark sits + + +def _height_of(ch: str) -> str: + if ch.isupper() or ch in "ÆŒØÐÞ": + return "cap" + return "x" + + +def build_glyphs() -> list[Glyph]: + out: list[Glyph] = [] + seen: set[str] = set() + + def add(gl: Glyph): + if gl.name in seen: + raise ValueError(f"duplicate glyph {gl.name}") + seen.add(gl.name) + out.append(gl) + + add(Glyph(".notdef", key=None, variant="notdef")) + + def drawn(ch, key=None): + add(Glyph(glyph_name(ch), [ord(ch)], key=key or ch, height=_height_of(ch), + category="ligature" if ch in LIGATURES else "base")) + + accented = [] + for ch in ASCII: + drawn(ch) + drawn(" ", " ") + for ch in PUNCT: + drawn(ch) + for ch in LATIN1_LETTERS + LATIN_EXTRA: + dec = unicodedata.decomposition(ch) + if dec and not dec.startswith("<"): + accented.append(ch) + else: + drawn(ch) + # accented letters: base + mark + for ch in accented: + b, m = (chr(int(x, 16)) for x in unicodedata.decomposition(ch).split()) + base = "ı" if b == "i" else b + mark = glyph_name(m) + cap = b.isupper() + add(Glyph(glyph_name(ch), [ord(ch)], comps=[glyph_name(base), mark + (".case" if cap and mark != "cedillacomb" else "")], + comp_kind="accent", height="cap" if cap else "x")) + for ch in LIGATURES: + drawn(ch) + for ch in GREEK: + if ch in GREEK_LOOKALIKE: + add(Glyph(glyph_name(ch), [ord(ch)], comps=[glyph_name(GREEK_LOOKALIKE[ch])], comp_kind="ref", height="cap")) + elif ch == "ο": + add(Glyph(glyph_name(ch), [ord(ch)], comps=["o"], comp_kind="greek-ref")) + else: + drawn(ch) + drawn("₹") + for ch in MATHS: + if glyph_name(ch) not in seen: + drawn(ch) + # other: nbsp done above + drawn("⁄", "⁄") + for ch, f in (("¹", "1"), ("²", "2"), ("³", "3")): + add(Glyph(glyph_name(ch), [ord(ch)], key=f, variant="sup")) + add(Glyph("ordfeminine", [0xAA], key="a", variant="sup")) + add(Glyph("ordmasculine", [0xBA], key="o", variant="sup")) + add(Glyph("micro", [0xB5], comps=["mu"], comp_kind="ref")) + drawn("◌") + # marks: encoded combining marks and their capital forms + from loom.extended import BELOW, MARK_UNICODE + for name, u in MARK_UNICODE.items(): + add(Glyph(name, [u], key=name, category="mark")) + if name in CASE_MARKS: + add(Glyph(name + ".case", key=name + ".case", category="mark")) + # figures + for st in FIG_STYLES: + for f, n in zip(FIGURES, FIG_NAMES): + add(Glyph(f"{n}.{st}", key=f, variant=st)) + for f in "24": + add(Glyph(f"{FIG_NAMES[int(f)]}.dnom", key=f, variant="dnom")) + for ch, parts in (("¼", ["onesuperior", "fraction", "four.dnom"]), ("½", ["onesuperior", "fraction", "two.dnom"]), + ("¾", ["threesuperior", "fraction", "four.dnom"])): + add(Glyph(glyph_name(ch), [ord(ch)], comps=parts, comp_kind="frac")) + # small capitals + for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": + add(Glyph(f"{ch.lower()}.sc", key=ch, variant="sc", height="sc")) + for ch, key in (("æ", "Æ"), ("œ", "Œ"), ("ø", "Ø"), ("ð", "Ð"), ("þ", "Þ")): + add(Glyph(f"{glyph_name(ch)}.sc", key=key, variant="sc", height="sc")) + # Greek small capitals: drawn capitals at small-cap size; look-alikes refer to the Latin ones + for ch in GREEK: + if ch.isupper(): + low = ch.lower() + if ch in GREEK_LOOKALIKE: + add(Glyph(f"{glyph_name(low)}.sc", comps=[f"{GREEK_LOOKALIKE[ch].lower()}.sc"], comp_kind="ref", height="sc")) + else: + add(Glyph(f"{glyph_name(low)}.sc", key=ch, variant="sc", height="sc")) + for ch in accented: + if ch.islower(): + b, m = (chr(int(x, 16)) for x in unicodedata.decomposition(ch).split()) + mark = glyph_name(m) + add(Glyph(f"{glyph_name(ch)}.sc", comps=[f"{b}.sc", mark + ("" if mark == "cedillacomb" else ".case")], + comp_kind="sc-accent", height="sc")) + return out + + +GLYPHS = build_glyphs() +BY_NAME = {g.name: g for g in GLYPHS} +ORDER = [g.name for g in GLYPHS] + + +def cmap() -> dict[int, str]: + return {u: g.name for g in GLYPHS for u in g.unicodes} + + +def smcp_pairs() -> list[tuple[str, str]]: + """(lowercase glyph, small cap glyph) for smcp; c2sc maps the capitals.""" + pairs = [] + for g in GLYPHS: + if g.name.endswith(".sc"): + base = g.name[:-3] + if base in BY_NAME: + pairs.append((base, g.name)) + pairs.append(("dotlessi", "i.sc")) + pairs.append(("sigmafinal", "sigma.sc")) + return pairs + + +def c2sc_pairs() -> list[tuple[str, str]]: + pairs = [] + for low, sc in smcp_pairs(): + if low == "dotlessi": + continue + ch = next((chr(u) for u in BY_NAME[low].unicodes), None) + if ch is None: + continue + up = ch.upper() + if len(up) == 1 and up != ch and glyph_name(up) in BY_NAME: + pairs.append((glyph_name(up), sc)) + return pairs + + +SKETCH_CHARS = S.SKETCH_CHARS diff --git a/warp/loom/engine.py b/warp/loom/engine.py new file mode 100644 index 0000000..fcc68ac --- /dev/null +++ b/warp/loom/engine.py @@ -0,0 +1,535 @@ +"""The Warp pen model: a Python port of the sketch 0.2 engine. + +Source: reference/warp-sketch-0.2.js (parameters, nib, hull, centre-line +paths, glyph builder, sweep, teardrops, compose). Font units, y up, +1000 units per em. + +Every glyph is a set of centre-lines ("strokes"). Each stroke is swept +by a nib: a superellipse `a` long and `b` thick, turned by the pen angle. +Pressure (0..1) on each node sets the nib length between b (hairline) +and A (stem). With the nib level, verticals come out thick and +horizontals thin: vertical stress comes out of the pen. The ink is the +union of the convex hulls of consecutive nib positions, plus discs, +teardrops and bracket fillets. This module returns those polygons; the +outline step (loom/outline.py) unions them and fits curves. + +The port follows the engine line by line, so that the advances and the +ink are the same as in the sketch (tests/test_warp.py runs the engine in +node and compares). Additions for the font are marked "font:". + +Apache-2.0. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field, replace + +PI = math.pi +TAU = 2 * PI + + +def clamp(v, a, b): + return max(a, min(b, v)) + + +def lerp(a, b, t): + return a + (b - a) * t + + +def smooth(t): + """Smootherstep: unhurried swells, no abrupt pressure changes.""" + return t * t * t * (t * (6 * t - 15) + 10) + + +def _sign(v): + return (v > 0) - (v < 0) + + +# ------------------------------------------------------------ parameters + +DEFAULTS = dict(contrast=9, stress=0, xh=412, serif=1, bracket=0.2, ball=1, + opsz=12, ital=False, stem=84, + # font: multipliers for the bold styles (1 = the sketch). + serifw=1.0, width=1.0) + + +@dataclass(frozen=True) +class Params: + o: dict + k: float + A: float + B: float + S: float + ratio: float + ang: float + xh: float + cap: float + asc: float + desc: float + os: float + axis: float + sl: float + bk: float + bd: float + sb: float + wd: float + ital: bool + slant: float + + def but(self, **kw) -> "Params": + return replace(self, **kw) + + +def params(**kw) -> Params: + o = dict(DEFAULTS) + o.update(kw) + k = clamp(math.log(o["opsz"] / 6) / math.log(72 / 6), 0, 1) # 0 at 6 pt, 1 at 72 pt + A = o["stem"] * (1.06 - 0.12 * k) # stems a little lighter at display sizes + ratio = 1 + (o["contrast"] - 1) * (0.34 + 0.66 * min(1, k / 0.72)) # full contrast from opsz 36 up + B = max(A / ratio, 5) + S = max(B * 1.05, A * (0.27 - 0.16 * k)) * o["serifw"] # serif thickness, never thinner than hairline + return Params( + o=o, k=k, A=A, B=B, S=S, ratio=ratio, + ang=o["stress"] * PI / 180, + xh=o["xh"] + (1 - k) * 22, cap=670, asc=760, desc=-250, os=11, axis=250, + sl=A * 0.74 * o["serif"], + bk=o["bracket"], + bd=A * (0.8 + 0.14 * (1 - k)) * o["ball"] if o["ball"] > 0 else 0, + sb=30 + (1 - k) * 18, + wd=(1 + (1 - k) * 0.04) * o["width"], + ital=bool(o["ital"]), slant=0.25 if o["ital"] else 0, + ) + + +# ------------------------------------------------------------------- nib + +NK = 28 + + +def nib(a, b, ang, ne): + """Superellipse nib, counter-clockwise polygon.""" + v = [] + c, s = math.cos(ang), math.sin(ang) + for i in range(NK): + t = i / NK * TAU + ct, st = math.cos(t), math.sin(t) + x = a / 2 * _sign(ct) * abs(ct) ** (2 / ne) + y = b / 2 * _sign(st) * abs(st) ** (2 / ne) + v.append((x * c - y * s, x * s + y * c)) + return v + + +def hull(P): + """Monotone chain, counter-clockwise.""" + P = sorted(P, key=lambda q: (q[0], q[1])) + + def cr(o, a, b): + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + lo, up = [], [] + for p in P: + while len(lo) >= 2 and cr(lo[-2], lo[-1], p) <= 0: + lo.pop() + lo.append(p) + for p in reversed(P): + while len(up) >= 2 and cr(up[-2], up[-1], p) <= 0: + up.pop() + up.append(p) + lo.pop() + up.pop() + return lo + up + + +def area(poly): + s = 0.0 + n = len(poly) + for i in range(n): + a, b = poly[i], poly[(i + 1) % n] + s += a[0] * b[1] - b[0] * a[1] + return s / 2 + + +def ccw(poly): + return list(reversed(poly)) if area(poly) < 0 else list(poly) + + +# ------------------------------------------------------ centre-line paths + +def se_pt(cx, cy, rx, ry, t, e): + c, s = math.cos(t), math.sin(t) + return (cx + rx * _sign(c) * abs(c) ** (2 / e), cy + ry * _sign(s) * abs(s) ** (2 / e)) + + +class Path: + def __init__(self, g, x, y, pr=None): + self.g = g + self.pts = [[x, y, 1 if pr is None else pr]] + self.nodes = [(x, y)] + + @property + def last(self): + return self.pts[-1] + + def l(self, x, y, pr=None): # noqa: E741 + x0, y0, p0 = self.last + pr = p0 if pr is None else pr + n = 1 if p0 == pr else max(2, math.ceil(math.hypot(x - x0, y - y0) / 10)) + for i in range(1, n + 1): + t = i / n + u = smooth(t) + self.pts.append([lerp(x0, x, t), lerp(y0, y, t), lerp(p0, pr, u)]) + self.nodes.append((x, y)) + return self + + def c(self, x1, y1, x2, y2, x, y, pr=None): + x0, y0, p0 = self.last + pr = p0 if pr is None else pr + ln = math.hypot(x1 - x0, y1 - y0) + math.hypot(x2 - x1, y2 - y1) + math.hypot(x - x2, y - y2) + n = max(6, math.ceil(ln / 7)) + for i in range(1, n + 1): + t = i / n + m = 1 - t + self.pts.append([ + m * m * m * x0 + 3 * m * m * t * x1 + 3 * m * t * t * x2 + t * t * t * x, + m * m * m * y0 + 3 * m * m * t * y1 + 3 * m * t * t * y2 + t * t * t * y, + lerp(p0, pr, smooth(t)), + ]) + self.nodes.append((x, y)) + return self + + def arc(self, cx, cy, rx, ry, t0, t1, pr=None, e=None): + """Continue along a superellipse from angle t0 to t1.""" + e = e or self.g.ex + p0 = self.last[2] + pr = p0 if pr is None else pr + n = max(6, math.ceil(abs(t1 - t0) * max(rx, ry) / 7)) + for i in range(1, n + 1): + t = i / n + q = se_pt(cx, cy, rx, ry, lerp(t0, t1, t), e) + self.pts.append([q[0], q[1], lerp(p0, pr, smooth(t))]) + self.nodes.append(se_pt(cx, cy, rx, ry, t1, e)) + return self + + def draw(self, opts=None): + self.g.strokes.append({"pts": self.pts, "nodes": self.nodes, "o": dict(opts or {})}) + return self + + +# ---------------------------------------------------------- glyph builder + +class Builder: + """The `g` object of the engine: metrics plus drawing helpers.""" + + def __init__(self, p: Params): + self.p = p + self.A, self.B, self.S, self.sl, self.bk, self.bd = p.A, p.B, p.S, p.sl, p.bk, p.bd + self.X, self.C, self.AS, self.D, self.o = p.xh, p.cap, p.asc, p.desc, p.os + self.ex = 2.1 + self.strokes: list = [] + self.balls: list = [] + self.drops: list = [] + self.fills: list = [] + # The sketch reads `p.lc`, which its params() never sets: the + # lowercase wedge head serif is never drawn. The port keeps that. + self.lc = False + self.h = self.A / 2 + self.yt = self.X + self.o - self.B / 2 + self.yb = -self.o + self.B / 2 + + def P(self, x, y, pr=None): + return Path(self, x, y, pr) + + def E(self, cx, cy, rx, ry, t0, pr=None, e=None): + q = se_pt(cx, cy, rx, ry, t0, e or self.ex) + return Path(self, q[0], q[1], pr) + + def pt(self, cx, cy, rx, ry, t, e=None): + return se_pt(cx, cy, rx, ry, t, e or self.ex) + + def ring(self, cx, cy, rx, ry, pr=None, e=None): + pr = 1 if pr is None else pr + return self.E(cx, cy, rx, ry, 0, pr, e).arc(cx, cy, rx, ry, 0, TAU, pr, e).draw({"closed": True}) + + def stem(self, x, y0, y1, pr=None, o=None): + pr = 1 if pr is None else pr + return self.P(x, y0 + self.B / 2, pr).l(x, y1 - self.B / 2, pr).draw(o) + + def hair(self, x0, y0, x1, y1, o=None): + return self.P(x0, y0, 0).l(x1, y1, 0).draw(o) + + def bar(self, x0, x1, y): + """Horizontal hairline, ink x0..x1, centred on y.""" + a, b = min(x0, x1), max(x0, x1) + return self.P(a + self.B / 2, y, 0).l(b - self.B / 2, y, 0).draw({"role": "bar"}) + + def dot(self, x, y, d=None): + d = self.bd if d is None else d + if d > 0: + self.balls.append((x, y, d / 2)) + + def ball(self, x, y, d=None): + """A terminal: a teardrop that grows out of the nearest stroke end.""" + d = self.bd if d is None else d + if d > 0: + self.drops.append((x, y, d / 2)) + + def fill(self, pts, role=None): + self.fills.append({"pts": pts, "role": role or "fill"}) + + def serif(self, x, y, dir, side, w=None, ln=None): + """Serif on a stem of width w centred at x; y = outer edge. + + dir=+1 foot (serif rises from y), -1 head (hangs from y). + """ + w = self.A if w is None else w + ln = self.sl if ln is None else ln + if ln <= 1: + return + S, hw = self.S, w / 2 + cy = y + dir * S / 2 + L = x if side == "r" else x - hw - ln + R = x if side == "l" else x + hw + ln + if dir < 0 and side == "l" and self.lc: + dw = ln * 0.3 + self.P(L + S * 0.3, cy - dw, 0).l(x, cy, 0).draw({"a": S * 0.9, "b": S, "ne": 5, "role": "serif"}) + self.fill([(L + S * 0.2, cy - dw - S * 0.3), (x, y - S * 1.1), (x, y), (L + S * 0.2, cy - dw + S * 0.45)], "bracket") + else: + self.P(L + S / 2, cy, 0).l(R - S / 2, cy, 0).draw({"a": S, "b": S, "ne": 7, "role": "serif"}) + bh = self.bk * ln * 1.0 + bl = self.bk * ln * 0.85 + if bh > 2: + def br(xe, sx): + pts = [(xe - sx * 2, y + dir * S * 0.5), (xe - sx * 2, y + dir * (S + bh))] + c = (xe, y + dir * S) + e = (xe + sx * bl, y + dir * S) + s0 = (xe, y + dir * (S + bh)) + for i in range(11): + t = i / 10 + m = 1 - t + pts.append((m * m * s0[0] + 2 * m * t * c[0] + t * t * e[0], + m * m * s0[1] + 2 * m * t * c[1] + t * t * e[1])) + pts.append((xe + sx * bl, y + dir * S * 0.5)) + self.fill(pts, "bracket") + if side != "r": + br(x - hw, -1) + if side != "l": + br(x + hw, 1) + + def beak(self, x, y, dir, ln=None): + ln = self.A * 1.25 if ln is None else ln + S = self.S * 0.95 + self.P(x - S / 2, y, 0.15).l(x - S / 2, y + dir * ln, 0).draw({"a": S * 1.05, "b": S, "ne": 4, "role": "serif"}) + + # font: draw another glyph function into this glyph, moved by (dx, dy). + def use(self, fn, dx=0.0, dy=0.0): + return self.use_map(fn, lambda x, y: (x + dx, y + dy)) + + # font: draw a glyph function with its centre-lines mapped by f(x, y). + # The pen is not mapped: a rotated copy keeps the level nib. + def use_map(self, fn, f): + n = (len(self.strokes), len(self.balls), len(self.drops), len(self.fills)) + ret = fn(self) + for s in self.strokes[n[0]:]: + s["pts"] = [[*f(x, y), pr] for x, y, pr in s["pts"]] + s["nodes"] = [f(x, y) for x, y in s["nodes"]] + self.balls[n[1]:] = [(*f(x, y), r) for x, y, r in self.balls[n[1]:]] + self.drops[n[2]:] = [(*f(x, y), r) for x, y, r in self.drops[n[2]:]] + for fl in self.fills[n[3]:]: + fl["pts"] = [f(x, y) for x, y in fl["pts"]] + return ret + + +# ---------------------------------------------------------------- glyphs + +@dataclass +class GlyphDef: + fn: object + cap: bool = False + lc: bool = False + l: float | None = None # noqa: E741 + r: float | None = None + adv: float | None = None + empty: bool = False + upright: bool = False + bigop: bool = False + slant: float | None = None + yc: float | None = None + greek: bool = False + extra: dict = field(default_factory=dict) + + +def flags(ch: str) -> dict: + """The engine's flags: `cap` for /^[A-ZΓΔΣΩ]$/, `lc` for /^[a-zfiflff]$/. + + font: the new Greek capitals count as capitals too (Θ Λ Ξ Π Φ Ψ), so + that they get the same lighter stem and narrower frame. + """ + cap = len(ch) == 1 and (("A" <= ch <= "Z") or ch in "ΓΔΣΩΘΛΞΠΦΨ") + lc = len(ch) == 1 and (("a" <= ch <= "z") or ch in "fiflff") + return {"cap": cap, "lc": lc} + + +# ------------------------------------------------------------------ sweep + +def nib_for(p: Params, o: dict, pr: float): + b = o["b"] if o.get("b") is not None else p.B + a = o["a"] if o.get("a") is not None else (b + (p.A - b) * clamp(pr, 0, 1)) + ang = 0 if o.get("role") == "rule" else p.ang + (o.get("turn") or 0) + return nib(a, b, ang, o.get("ne") or 3.2) + + +def circle(x, y, r): + v = [] + for i in range(40): + t = i / 40 * TAU + v.append((x + r * math.cos(t), y + r * math.sin(t))) + return v + + +def teardrop(g: Builder, x, y, r): + """Find the stroke end this terminal belongs to; grow a drop out of it.""" + best = None + bd = r * 3.2 + for s in g.strokes: + P = s["pts"] + for end in (0, len(P) - 1): + q = P[end] + d = math.hypot(q[0] - x, q[1] - y) + if d < bd: + bd = d + best = (P, end) + if best is None: + return circle(x, y, r), True + P, end = best + dir = 1 if end == 0 else -1 + acc = 0.0 + i = end + tip = P[end] + while 0 <= i + dir < len(P) and acc < r * 2.1: + a, b = P[i], P[i + dir] + acc += math.hypot(b[0] - a[0], b[1] - a[1]) + i += dir + tip = P[i] + ux, uy = x - tip[0], y - tip[1] + ul = math.hypot(ux, uy) or 1 + cx, cy = x + ux / ul * r * 0.12, y + uy / ul * r * 0.12 + pts = circle(cx, cy, r) + [(tip[0], tip[1]), + (tip[0] + (cx - tip[0]) * 0.35, tip[1] + (cy - tip[1]) * 0.35)] + return hull(pts), False + + +@dataclass +class Swept: + polys: list + strokes: list + adv: float + dx: float + xmin: float + xmax: float + ymin: float + ymax: float + ret: object = None + yc: float = 0.0 + sl: float = 0.0 + p: object = None + + +def compose(d: GlyphDef, p: Params, mods: dict | None = None) -> Swept: + """Sweep a glyph. Returns its polygons (not yet moved by dx) and metrics.""" + mods = mods or {} + wdx = mods.get("wdx") or 1 + if d.cap and not mods.get("sc"): + p = p.but(A=p.A * 0.92) + wdx *= 0.955 + if mods.get("p"): + p = p.but(**mods["p"]) + g = Builder(p) + ret = d.fn(g) + dy = mods.get("dy") or 0 + wd = p.wd * wdx + sl = d.slant if d.slant is not None else (0 if d.upright else p.slant) + yc = d.yc if d.yc is not None else p.xh * 0.45 + + def T(x, y): + return (x * wd + (y + dy - yc) * sl, y + dy) + + polys = [] + strokes = [] + bb = [1e9, -1e9, 1e9, -1e9] + + def grow(ux, uy): + if ux < bb[0]: + bb[0] = ux + if ux > bb[1]: + bb[1] = ux + if uy < bb[2]: + bb[2] = uy + if uy > bb[3]: + bb[3] = uy + + for s in g.strokes: + prev = None + out = [] + n = len(s["pts"]) + for x, y, pr in s["pts"]: + nb = nib_for(p, s["o"], pr) + q = T(x, y) + for v in nb: + grow(x * wd + v[0], y + dy + v[1]) + cur = [(q[0] + v[0], q[1] + v[1]) for v in nb] + if prev is not None: + polys.append(hull(prev + cur)) + elif n == 1: + polys.append(cur) + prev = cur + out.append((q[0], q[1], pr)) + strokes.append({"pts": out, "nodes": [T(*nd) for nd in s["nodes"]], "role": s["o"].get("role") or "stroke"}) + for x, y, r in g.balls: + q = T(x, y) + polys.append(circle(q[0], q[1], r)) + grow(x * wd - r, y + dy - r) + grow(x * wd + r, y + dy + r) + for x, y, r in g.drops: + pts, rnd = teardrop(g, x, y, r) + g.fills.append({"pts": pts, "role": "ball" if rnd else "drop"}) + for f in g.fills: + pts = [] + for x, y in f["pts"]: + grow(x * wd, y + dy) + pts.append(T(x, y)) + polys.append(ccw(pts)) + x0, x1, y0, y1 = bb + if x0 > x1: + x0 = x1 = y0 = y1 = 0 + if d.adv is not None and not mods.get("prop"): + adv = d.adv * wd + dx = 0 if d.empty else (adv - (x1 - x0)) / 2 - x0 + else: + extra = mods.get("extra") or 0 + L = p.sb * (1 if d.l is None else d.l) + extra + R = p.sb * (1 if d.r is None else d.r) + extra + adv = x1 - x0 + L + R + dx = L - x0 + return Swept(polys=polys, strokes=strokes, adv=adv, dx=dx, xmin=x0, xmax=x1, ymin=y0, ymax=y1, ret=ret, + yc=yc, sl=sl, p=p) + + +# Old-style figures: x-height (x), descending (d) or ascending (a). +OSF = {"0": "x", "1": "x", "2": "x", "3": "d", "4": "d", "5": "d", "7": "d", "9": "d", "6": "a", "8": "a"} + + +def osf_mods(p: Params, base: str) -> dict: + k, X = OSF[base], p.xh + return {"p": {"cap": X * 1.02 if k == "x" else X * 1.42}, "dy": -X * 0.42 if k == "d" else 0, + "prop": True, "wdx": 0.97} + + +def sc_mods(p: Params) -> dict: + """Small cap: the capital drawn at 1.1 x-height, narrower, heavier hairlines.""" + return {"sc": True, "p": {"cap": p.xh * 1.1, "A": p.A * 0.94, "B": p.B * 1.3, "S": p.S * 1.2, "sl": p.sl * 0.8}, + "wdx": 0.83, "extra": p.sb * 0.15} + + +def is_greek_lower(ch: str) -> bool: + return len(ch) == 1 and "α" <= ch <= "ω" diff --git a/warp/loom/extended.py b/warp/loom/extended.py new file mode 100644 index 0000000..d3c309d --- /dev/null +++ b/warp/loom/extended.py @@ -0,0 +1,1011 @@ +"""Glyphs new in the font (not in sketch 0.2), drawn with the same pen. + +The rest of printable ASCII, typographic punctuation, Latin-1 letters +and symbols, the marks for accents, the rest of basic Greek, the maths +operators, ffi and ffl, and the dotted circle. Every glyph is centre- +lines plus pressure, swept by the same nib as the sketch glyphs, with +the sketch's helpers (serifs, beaks, teardrops, rule pen). + +Keys are the characters, or production names for unencoded glyphs +(marks for capitals end in ".case"). + +This file defines outlines of machine-made Warp: SIL Open Font License +1.1, Reserved Font Name "machine-made" (see ../OFL.txt). +""" + +from __future__ import annotations + +import math + +from loom.engine import PI, TAU, GlyphDef +from loom.sketch import (AX, FIG, G, GI, OPW, Rw, bowl_right, dbowl, dotD, entry, exit_t, iarch, ibowl, + it_f, paren, rom, ita, round_c, rule, vbeak) + +# Glyphs that are capitals but not A-Z: lighter stems and narrower frames, as the sketch's capitals. +CAP = {"cap": True} + + +def rom_cap(ch, **o): + def deco(fn): + G[ch] = GlyphDef(fn=fn, cap=True, **o) + return fn + return deco + + +def ita_cap(ch, **o): + def deco(fn): + GI[ch] = GlyphDef(fn=fn, cap=True, **o) + return fn + return deco + + +def fn_of(ch, ital=False): + return ((GI.get(ch) if ital else None) or G[ch]).fn + + +# ===================================================================== +# Marks. Drawn centred on x = 0. Lowercase marks sit above the x-height, +# capital marks (".case") above the cap height, flatter. +# ===================================================================== + +def _top(g, case): + return (g.C + 52, 0.78) if case else (g.X + 62, 1.0) + + +def acute(g, case=False): + y0, k = _top(g, case) + g.P(-48, y0, 0.12).l(58, y0 + 150 * k, 1).draw() + + +def grave(g, case=False): + y0, k = _top(g, case) + g.P(48, y0, 0.12).l(-58, y0 + 150 * k, 1).draw() + + +def circumflex(g, case=False): + y0, k = _top(g, case) + w = 125 + g.P(-w, y0, 0).l(-6, y0 + 140 * k, 0.45).draw() + g.P(6, y0 + 140 * k, 0.55).l(w, y0, 0.1).draw() + + +def caron(g, case=False): + y0, k = _top(g, case) + w = 125 + g.P(-w, y0 + 140 * k, 0.1).l(-6, y0, 0.55).draw() + g.P(6, y0, 0.45).l(w, y0 + 140 * k, 0).draw() + + +def dieresis(g, case=False): + y0, k = _top(g, case) + d = dotD(g) * 0.92 + for x in (-98, 98): + g.dot(x, y0 + d / 2 + 6, d) + + +def dotaccent(g, case=False): + y0, k = _top(g, case) + d = dotD(g) * 0.95 + g.dot(0, y0 + d / 2 + 6, d) + + +def tilde(g, case=False): + y0, k = _top(g, case) + h = 70 * k + (g.P(-138, y0 + 8, 0.1).c(-128, y0 + h * 0.95, -82, y0 + h * 1.25, -30, y0 + h * 0.8, 0.75) + .c(25, y0 + h * 0.35, 80, y0 + h * 0.1, 105, y0 + 15, 0.45).c(118, y0 + 22, 132, y0 + 55, 138, y0 + h * 1.1, 0.1).draw()) + + +def ring(g, case=False): + y0, k = _top(g, case) + r = 64 * (0.9 if case else 1) + g.ring(0, y0 + r + 4, r, r, 0.55) + + +def macron(g, case=False): + y0, k = _top(g, case) + g.P(-125, y0 + 40 * k, 0).l(125, y0 + 40 * k, 0).draw({"b": g.B * 1.5, "a": g.B * 1.5, "ne": 5}) + + +def cedilla(g, case=False): + (g.P(6, 4, 0.15).l(6, -58, 0.15).c(78, -60, 112, -92, 112, -132, 1) + .c(112, -184, 46, -214, -62, -206, 0).draw()) + + +MARKS = { + "gravecomb": grave, "acutecomb": acute, "circumflexcomb": circumflex, "tildecomb": tilde, + "macroncomb": macron, "dieresiscomb": dieresis, "ringcomb": ring, "caroncomb": caron, + "dotaccentcomb": dotaccent, "cedillacomb": cedilla, +} +MARK_UNICODE = { + "gravecomb": 0x0300, "acutecomb": 0x0301, "circumflexcomb": 0x0302, "tildecomb": 0x0303, + "macroncomb": 0x0304, "dieresiscomb": 0x0308, "ringcomb": 0x030A, "caroncomb": 0x030C, + "dotaccentcomb": 0x0307, "cedillacomb": 0x0327, +} +BELOW = {"cedillacomb"} + +for _name, _f in MARKS.items(): + G[_name] = GlyphDef(fn=(lambda f: (lambda g: f(g)))(_f), l=0, r=0) + if _name in ("gravecomb", "acutecomb", "circumflexcomb", "tildecomb", "dieresiscomb", "ringcomb"): + G[_name + ".case"] = GlyphDef(fn=(lambda f: (lambda g: f(g, True)))(_f), l=0, r=0) + +# Spacing marks (` ´ ¨ ¯ ¸ ^ ˜ ˆ ˚ ˇ ˙): the same drawings with side bearings. +for _ch, _f in (("`", grave), ("´", acute), ("¨", dieresis), ("¯", macron), ("¸", cedilla), + ("ˆ", circumflex), ("˜", tilde), ("˚", ring), ("ˇ", caron), ("˙", dotaccent)): + G[_ch] = GlyphDef(fn=(lambda f: (lambda g: f(g)))(_f), l=0.6, r=0.6) + + +# ===================================================================== +# ASCII +# ===================================================================== + +@rom('"', l=0.9, r=0.9) +def _quotedbl(g): + C = g.C + for x in (0, 150): + g.P(x, C - g.B / 2, 1).l(x, C * 0.66, 0.15).draw() + + +@rom("#", l=0.6, r=0.6) +def _numbersign(g): + C = g.C + g.P(205, C - 10, 0.7).l(125, 0, 0.7).draw() + g.P(395, C - 10, 0.7).l(315, 0, 0.7).draw() + g.bar(40, 480, C * 0.64) + g.bar(20, 460, C * 0.34) + + +def s_curve(g, W, H, y0=0.0, beaks=True): + """The S spine at any height (the sketch's S, scaled to H).""" + B, o, h = g.B, g.o, g.h + yt, yb = y0 + H + o - B / 2, y0 - o + B / 2 + Y = lambda f: y0 + H * f # noqa: E731 + (g.P(W - 16, Y(0.78), 0.15).c(W - 30, Y(0.95), W * 0.68, yt, W * 0.5, yt, 0.5).c(W * 0.22, yt, h + 2, Y(0.92), h + 2, Y(0.75), 1) + .c(h + 2, Y(0.5), W - h - 2, Y(0.52), W - h - 2, Y(0.26), 1).c(W - h - 2, Y(0.08), W * 0.76, yb, W * 0.5, yb, 0.5) + .c(W * 0.24, yb, 26, Y(0.06), 12, Y(0.2), 0.15).draw()) + if beaks: + vbeak(g, W - 16, Y(0.68), Y(0.92)); vbeak(g, 12, Y(0.08), Y(0.33)) + + +@rom("$", **FIG) +def _dollar(g): + C = g.C + s_curve(g, 400, C * 0.92, C * 0.02) + g.P(200, C + 70, 0).l(200, -90, 0).draw() + + +@rom("%", l=0.6, r=0.6) +def _percent(g): + C = g.C + g.ring(130, C * 0.76, 105 - g.h * 0.6, C * 0.21, 0.8) + g.ring(500, C * 0.22, 105 - g.h * 0.6, C * 0.21, 0.8) + g.P(530, C + 10, 0.4).l(100, -10, 0.4).draw() + + +@rom("*", l=0.6, r=0.6) +def _asterisk(g): + C = g.C + cx, cy, R = 170, C - 150, 140 + for k in range(5): + a = PI / 2 + k * TAU / 5 + g.P(cx, cy, 0.15).l(cx + math.cos(a) * R, cy + math.sin(a) * R, 0.9).draw() + g.dot(cx + math.cos(a) * (R - 6), cy + math.sin(a) * (R - 6), g.A * 0.62) + + +@rom("@", l=0.7, r=0.7) +def _at(g): + X = g.X + cx, cy, R = 400, X * 0.62, 380 + g.ring(362, X * 0.5, 120 - g.h * 0.35, X * 0.33, 0.6) + (g.P(492, X * 0.9, 0.55).l(492, X * 0.2, 0.55).c(492, X * 0.02, 530, -12, 590, -12, 0.3) + .c(705, -12, cx + R, cy - 160, cx + R, cy, 0.6).arc(cx, cy, R, R, 0, 0.5 * PI, 0.35, 2.0) + .arc(cx, cy, R, R, 0.5 * PI, PI, 0.6, 2.0).arc(cx, cy, R, R, PI, 1.5 * PI, 0.35, 2.0) + .arc(cx, cy, R, R, 1.5 * PI, 1.78 * PI, 0.05, 2.0).draw()) + + +@rom("\\", l=0.5, r=0.5) +def _backslash(g): + C = g.C + g.P(0, C + 60, 0.55).l(330, -120, 0.55).draw() + + +@rom("^", l=0.8, r=0.8, upright=True) +def _asciicircum(g): + r = Rw(g) + g.P(0, 330, 0).l(240, 670, 0).l(480, 330, 0).draw({"a": r, "b": r, "ne": 2.6, "role": "rule"}) + + +@rom("_", l=0.1, r=0.1) +def _underscore(g): + g.P(0, -130, 0).l(500, -130, 0).draw({"b": g.B * 1.5, "a": g.B * 1.5, "ne": 5}) + + +@rom("|", l=1.0, r=1.0) +def _bar(g): + g.P(0, 770, 0.3).l(0, -250, 0.3).draw() + + +@rom("¦", l=1.0, r=1.0) +def _brokenbar(g): + g.P(0, 770, 0.3).l(0, 330, 0.3).draw() + g.P(0, 190, 0.3).l(0, -250, 0.3).draw() + + +def brace(g, x0, w, top, bot, dir): + m = (top + bot) / 2 + xr, xs = x0 + w * dir, x0 + w * 0.45 * dir + (g.P(xr, top - g.B / 2, 0).c(xs + w * 0.2 * dir, top - 8, xs, top - 50, xs, top - 140, 0.55) + .l(xs, m + 110, 0.55).c(xs, m + 40, x0 + w * 0.2 * dir, m + 6, x0, m, 0.1).draw()) + (g.P(x0, m, 0.1).c(x0 + w * 0.2 * dir, m - 6, xs, m - 40, xs, m - 110, 0.55) + .l(xs, bot + 140, 0.55).c(xs, bot + 50, xs + w * 0.2 * dir, bot + 8, xr, bot + g.B / 2, 0).draw()) + + +@rom("{", l=0.9, r=0.6) +def _braceleft(g): + brace(g, 0, 170, 770, -250, 1) + + +@rom("}", l=0.6, r=0.9) +def _braceright(g): + brace(g, 170, 170, 770, -250, -1) + + +@rom("~", l=0.8, r=0.8, upright=True) +def _asciitilde(g): + r = Rw(g) + (g.P(0, AX - 40, 0).c(40, AX + 60, 120, AX + 75, 250, AX, 0).c(380, AX - 75, 460, AX - 60, 500, AX + 40, 0) + .draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + + +# ===================================================================== +# Punctuation +# ===================================================================== + +def drop_comma(g, x, y, flip=False): + """A comma: a drop at (x, y) with a tail. flip turns it by 180 degrees.""" + d = dotD(g) + s = -1 if flip else 1 + g.ball(x, y, d) + (g.P(x + s * d * 0.22, y - s * d * 0.18, 0.85) + .c(x + s * d * 0.3, y - s * d * 0.8, x, y - s * d * 1.4, x - s * d * 0.45, y - s * d * 1.75, 0).draw()) + + +@rom("‘", l=0.9, r=0.9) +def _quoteleft(g): + d = dotD(g) + drop_comma(g, 0, g.C - d * 1.75, True) + + +@rom("“", l=0.9, r=0.9) +def _quotedblleft(g): + d = dotD(g) + drop_comma(g, 0, g.C - d * 1.75, True); drop_comma(g, d * 1.45, g.C - d * 1.75, True) + + +@rom("”", l=0.9, r=0.9) +def _quotedblright(g): + d = dotD(g) + drop_comma(g, 0, g.C - d / 2); drop_comma(g, d * 1.45, g.C - d / 2) + + +@rom("‚", l=0.9, r=0.9) +def _quotesinglbase(g): + d = dotD(g) + drop_comma(g, 0, d / 2) + + +@rom("„", l=0.9, r=0.9) +def _quotedblbase(g): + d = dotD(g) + drop_comma(g, 0, d / 2); drop_comma(g, d * 1.45, d / 2) + + +def chevron(g, x, dir): + X = g.X + w = 150 + x0, x1 = (x + w, x) if dir < 0 else (x, x + w) + g.P(x0, X * 0.86, 0).l(x1, X * 0.45, 0.1).draw() + g.P(x1, X * 0.45, 0.25).l(x0, X * 0.04, 1).draw() + + +@rom("‹", l=0.6, r=0.6) +def _guilsinglleft(g): + chevron(g, 0, -1) + + +@rom("›", l=0.6, r=0.6) +def _guilsinglright(g): + chevron(g, 0, 1) + + +@rom("«", l=0.6, r=0.6) +def _guillemetleft(g): + chevron(g, 0, -1); chevron(g, 175, -1) + + +@rom("»", l=0.6, r=0.6) +def _guillemetright(g): + chevron(g, 0, 1); chevron(g, 175, 1) + + +@rom("…", l=0.9, r=0.9) +def _ellipsis(g): + d = dotD(g) + for k in range(3): + g.dot(k * d * 2.6, d / 2, d) + + +@rom("•", l=1.0, r=1.0) +def _bullet(g): + g.dot(0, g.X * 0.52, g.A * 2.1) + + +@rom("¡", l=0.9, r=0.9) +def _exclamdown(g): + X = g.X + g.use_map(fn_of("!"), lambda x, y: (x, X - y)) + + +@rom("¿", l=0.8, r=0.8) +def _questiondown(g): + X = g.X + g.use_map(fn_of("?"), lambda x, y: (325 - x, X - y)) + + +@rom("°", l=0.7, r=0.7) +def _degree(g): + C = g.C + g.ring(0, C - 92, 88 - g.h * 0.25, 92 - g.B / 2, 0.5) + + +@rom("§", l=0.7, r=0.7) +def _section(g): + C, D = g.C, g.D + s_curve(g, 330, C * 0.62, C * 0.4, beaks=False) + s_curve(g, 330, C * 0.62, -C * 0.07, beaks=False) + g.ball(330 - 16, C * 0.4 + C * 0.62 * 0.78, g.bd * 0.85) + g.ball(12, -C * 0.07 + C * 0.62 * 0.2, g.bd * 0.85) + + +@rom("¶", l=0.7, r=0.7) +def _paragraph(g): + C, D, B = g.C, g.D, g.B + g.P(250, C - B / 2, 0.6).l(250, D * 0.5, 0.6).draw() + g.P(380, C - B / 2, 0.6).l(380, D * 0.5, 0.6).draw() + g.bar(150, 380 + g.h * 0.6, C - B / 2) + cx, cy, rx, ry = 170, C * 0.7, 125, C * 0.3 - B / 2 + pts = [g.pt(cx, cy, rx, ry, PI / 2 + k / 32 * PI, 2.1) for k in range(33)] + g.fill(pts + [(250, cy - ry), (250, cy + ry)]) + + +@rom("¬", **OPW) +def _logicalnot(g): + rule(g, 0, AX + 60, 560, AX + 60) + rule(g, 560 - Rw(g) / 2, AX + 60, 560 - Rw(g) / 2, AX - 140) + + +@rom("₹", **FIG) +def _rupee(g): + C, B = g.C, g.B + g.bar(40, 460, C - B / 2); g.bar(40, 460, C * 0.75) + (g.P(130, C - B / 2, 0).c(300, C - B / 2, 372, C * 0.84, 372, C * 0.68, 1) + .c(372, C * 0.53, 300, C * 0.46, 170, C * 0.46, 0).l(60, C * 0.46, 0).draw()) + g.P(120, C * 0.46, 0.15).l(400, B / 2, 1).draw(); g.serif(400, 0, 1, "b", g.A * 0.95, g.sl * 0.8) + + +@rom("¢", **FIG) +def _cent(g): + X = g.X + cx, rx, cy, ry, t0 = 260, 175 - g.h * 0.2, X * 0.55, X * 0.55 + g.o - g.B / 2, 0.2 * PI + s = g.pt(cx, cy, rx, ry, t0) + (g.E(cx, cy, rx, ry, t0, 0.3).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.55).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.7).arc(cx, cy, rx, ry, 1.5 * PI, 1.84 * PI, 0).draw()) + g.ball(s[0] - g.bd * 0.2, s[1] - g.bd * 0.12) + g.P(270, X * 1.1 + 40, 0).l(250, -100, 0).draw() + + +@rom("£", **FIG) +def _sterling(g): + C, B, h, bd = g.C, g.B, g.h, g.bd + (g.P(405, C * 0.74, 0.25).c(400, C * 0.95, 340, C + g.o - B / 2, 280, C + g.o - B / 2, 0.5) + .c(195, C + g.o - B / 2, 160, C * 0.9, 160, C * 0.72, 1).l(160, C * 0.3, 1) + .c(160, C * 0.14, 120, 40, 50, B / 2, 0.5).draw()) + g.ball(405, C * 0.74) + g.bar(70, 340, C * 0.45) + g.bar(50, 455, B / 2); vbeak(g, 455 - g.S * 0.7, 0, 120) + + +@rom("¥", **FIG) +def _yen(g): + C, A, B = g.C, g.A, g.B + xl, xm, xr, yj = 20, 260, 500, C * 0.46 + g.P(xl + 30, C - B / 2, 1).l(xm, yj, 1).draw(); g.P(xr - 30, C - B / 2, 0).l(xm + A * 0.2, yj, 0).draw(); g.stem(xm, 0, yj + B) + g.serif(xl + 30, C, -1, "b", A, g.sl * 0.6); g.serif(xr - 30, C, -1, "b", B * 1.6, g.sl * 0.6); g.serif(xm, 0, 1, "b") + g.bar(90, 430, C * 0.4); g.bar(90, 430, C * 0.24) + + +@rom("€", **FIG) +def _euro(g): + C, h = g.C, g.h + cx = 300 + s, e = round_c(g, cx, 230 - h * 0.4, 0.2 * PI, 1.8 * PI) + vbeak(g, s[0] - 2, s[1] - 100, s[1] + 26); vbeak(g, e[0] - 2, e[1] - 20, e[1] + 90) + g.bar(10, 360, C * 0.6); g.bar(10, 340, C * 0.42) + + +@rom("©", l=0.7, r=0.7) +def _copyright(g): + C = g.C + cx, cy, R = 370, C / 2, 360 + g.ring(cx, cy, R - 30, R - 15, 0.35) + cy2, rx, ry, t0 = cy, 150 - g.h * 0.5, 190, 0.22 * PI + (g.E(cx + 10, cy2, rx, ry, t0, 0.2).arc(cx + 10, cy2, rx, ry, t0, 0.5 * PI, 0.45).arc(cx + 10, cy2, rx, ry, 0.5 * PI, PI, 0.8) + .arc(cx + 10, cy2, rx, ry, PI, 1.5 * PI, 0.45).arc(cx + 10, cy2, rx, ry, 1.5 * PI, 1.78 * PI, 0.1).draw()) + + +@rom("®", l=0.7, r=0.7) +def _registered(g): + C, B = g.C, g.B + cx, cy, R = 370, C / 2, 360 + g.ring(cx, cy, R - 30, R - 15, 0.35) + x0, y0, y1 = cx - 110, cy - 190, cy + 190 + g.P(x0, y0 + B / 2, 0.8).l(x0, y1 - B / 2, 0.8).draw() + ym = cy + 10 + (g.P(x0, y1 - B / 2, 0).l(x0 + 110, y1 - B / 2, 0).c(x0 + 190, y1 - B / 2, x0 + 215, y1 - 40, x0 + 215, (y1 + ym) / 2, 0.8) + .c(x0 + 215, ym + 40, x0 + 190, ym + B / 2, x0 + 110, ym + B / 2, 0).l(x0, ym + B / 2, 0).draw()) + g.P(x0 + 100, ym, 0.1).l(x0 + 225, y0 + B / 2, 0.8).draw() + + +@rom("ª", l=0.7, r=0.7) +def _ordfeminine(g): + y0, k = g.C * 0.45, 0.55 + g.use_map(fn_of("a"), lambda x, y: (x * k, y0 + y * k)) + g.bar(-10, 360 * k + 40, y0 - 70) + + +@rom("º", l=0.7, r=0.7) +def _ordmasculine(g): + y0, k = g.C * 0.45, 0.55 + g.use_map(fn_of("o"), lambda x, y: (x * k, y0 + y * k)) + g.bar(-10, 440 * k + 10, y0 - 70) + + +# ===================================================================== +# Latin-1 letters +# ===================================================================== + +@rom("ı") +def _dotlessi(g): + X = g.X + g.stem(0, 0, X); g.serif(0, X, -1, "l"); g.serif(0, 0, 1, "b") + + +@ita("ı", l=0.7, r=0.5) +def _idotlessi(g): + exit_t(entry(g, 0, g.X), g, 0).draw() + + +@rom("ȷ", l=0.5) +def _dotlessj(g): + X, D, B, bd, o = g.X, g.D, g.B, g.bd, g.o + yb = D - o + B / 2 + (g.P(0, X - B / 2, 1).l(0, D + 150, 1).c(0, D + 50, -40, yb, -105, yb, 0.5) + .c(-150, yb, -182, D + 22, -190, D + bd * 0.55 + 8, 0.25).draw()) + g.ball(-190, D + bd * 0.55 + 8); g.serif(0, X, -1, "l") + + +@rom("ß", r=0.7) +def _germandbls(g): + X, AS, B, o, yb = g.X, g.AS, g.B, g.o, g.yb + yt2 = AS + o - B / 2 + (g.P(0, B / 2, 1).l(0, AS - 190, 1).c(0, AS - 50, 75, yt2, 180, yt2, 0.5).c(285, yt2, 340, AS - 65, 340, AS - 170, 1) + .c(340, X + 95, 280, X + 48, 175, X + 40, 0).draw()) + (g.P(175, X + 40, 0).c(335, X + 20, 412, X * 0.74, 412, X * 0.38, 1).c(412, X * 0.08, 320, yb, 220, yb, 0.5) + .c(170, yb, 132, X * 0.04, 122, X * 0.13, 0.1).draw()) + g.serif(0, 0, 1, "b") + + +@ita("ß", l=0.7, r=0.7) +def _igermandbls(g): + X, AS, B, o, yb, D = g.X, g.AS, g.B, g.o, g.yb, g.D + yt2 = AS + o - B / 2 + (g.P(0, g.B / 2, 1).l(0, AS - 190, 1).c(0, AS - 50, 75, yt2, 180, yt2, 0.5).c(285, yt2, 340, AS - 65, 340, AS - 170, 1) + .c(340, X + 95, 280, X + 48, 175, X + 40, 0).draw()) + (g.P(175, X + 40, 0).c(335, X + 20, 412, X * 0.74, 412, X * 0.38, 1).c(412, X * 0.08, 320, yb, 220, yb, 0.5) + .c(170, yb, 132, X * 0.04, 122, X * 0.13, 0.1).draw()) + + +@rom("æ", l=0.82, r=0.75) +def _ae(g): + X, B, h, yt, yb, bd = g.X, g.B, g.h, g.yt, g.yb, g.bd + xr, xq = 300, h + 4 + xb, yB = h + max(bd, g.A) * 0.42, X * 0.7 + (g.P(xb, yB, 0.25).c(xb + 6, X * 0.94, xr - 205, yt, xr - 115, yt, 0.55).c(xr - 28, yt, xr, X * 0.86, xr, X * 0.62, 1) + .l(xr, X * 0.3, 1).draw()) + (g.P(xr - h * 0.3, X * 0.57, 0).c(xr - 110, X * 0.53, xq, X * 0.45, xq, X * 0.23, 1) + .c(xq, X * 0.05, xq + 48, yb, xq + 108, yb, 0.6).c(xq + 185, yb, xr - 40, X * 0.06, xr - h * 0.3, X * 0.22, 0).draw()) + g.ball(xb, yB) + g.use(fn_of("e"), xr - h) + + +@ita("æ", l=0.8, r=0.5) +def _iae(g): + X, h = g.X, g.h + x1 = 300 + ibowl(g, x1); g.P(x1, X, 0.5).l(x1, X - 44, 1).l(x1, X * 0.3, 1).draw() + g.use(fn_of("e", True), x1 - h * 0.6) + + +@rom("œ", l=0.82, r=0.75) +def _oe(g): + X, o, B, h = g.X, g.o, g.B, g.h + g.ring(220, X / 2, 220 - h, X / 2 + o - B / 2) + g.use(fn_of("e"), 440 - 2 * h) + + +@ita("œ", l=0.8, r=0.5) +def _ioe(g): + X, h = g.X, g.h + g.ring(200, X / 2, 200 - h, X / 2 + g.o - g.B / 2) + g.use(fn_of("e", True), 400 - 2 * h) + + +@rom("ø", l=0.82, r=0.82) +def _oslash(g): + g.use(fn_of("o")) + g.P(450, g.X + 45, 0.15).l(-10, -45, 0.15).draw() + + +@ita("ø", l=0.8, r=0.8) +def _ioslash(g): + g.use(fn_of("o", True)) + g.P(420, g.X + 45, 0.15).l(-20, -45, 0.15).draw() + + +@rom_cap("Ø", l=0.82, r=0.82) +def _Oslash(g): + g.use(fn_of("O")) + g.P(650, g.C + 45, 0.15).l(10, -45, 0.15).draw() + + +@rom_cap("Æ", l=0.45, r=0.6) +def _AE(g): + C, A, B, h = g.C, g.A, g.B, g.h + xs = 330 + g.P(0, B / 2, 0).l(xs - A * 0.2, C - B / 2, 0).draw() + g.stem(xs, 0, C); g.serif(xs, 0, 1, "b") + g.bar(xs - A * 0.6, xs + 385, C - B / 2); vbeak(g, xs + 385 - g.S * 0.7, C, C - 118) + g.bar(xs - h, xs + 330, C * 0.51); vbeak(g, xs + 330 - g.S * 0.7, C * 0.51 - 60, C * 0.51 + 60) + g.bar(xs - h, xs + 405, B / 2); vbeak(g, xs + 405 - g.S * 0.7, 0, 128) + g.bar(72, xs, C * 0.3); g.serif(0, 0, 1, "b", B * 1.6) + + +@rom_cap("Œ", l=0.82, r=0.6) +def _OE(g): + C, B, h = g.C, g.B, g.h + cx, xs = 320, 470 + cy, rx, ry = C / 2, cx - h, C / 2 - B / 2 + (g.P(xs, C - B / 2, 0).l(cx, C - B / 2, 0).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0).l(xs, B / 2, 0).draw()) + g.stem(xs, 0, C) + g.bar(xs, xs + 385, C - B / 2); vbeak(g, xs + 385 - g.S * 0.7, C, C - 118) + g.bar(xs - h, xs + 330, C * 0.51); vbeak(g, xs + 330 - g.S * 0.7, C * 0.51 - 60, C * 0.51 + 60) + g.bar(xs, xs + 405, B / 2); vbeak(g, xs + 405 - g.S * 0.7, 0, 128) + + +@rom_cap("Ð", r=0.85) +def _Eth(g): + g.use(fn_of("D")) + g.bar(-g.h - 70, g.h + 120, g.C * 0.49) + + +@rom_cap("Þ", r=0.7) +def _Thorn(g): + C = g.C + g.stem(0, 0, C); g.serif(0, C, -1, "b"); g.serif(0, 0, 1, "b"); dbowl(g, 0, C * 0.2, C * 0.8, 420, 190) + + +@rom("ð", l=0.82, r=0.8) +def _eth(g): + X, AS, B, o, h = g.X, g.AS, g.B, g.o, g.h + cx, cy, rx, ry = 222, X * 0.48, 222 - h, X * 0.48 + o - B / 2 + g.ring(cx, cy, rx, ry) + e = g.pt(cx, cy, rx, ry, 0.15 * PI) + g.P(e[0], e[1], 1).c(e[0] + 8, X * 1.05, 330, AS - 120, 150, AS - 8, 0).draw() + g.P(150, AS - 260, 0).l(390, AS - 140, 0).draw() + + +@rom("þ", r=0.82) +def _thorn(g): + AS, D = g.AS, g.D + g.stem(0, D, AS); g.serif(0, AS, -1, "l"); g.serif(0, D, 1, "b"); bowl_right(g, 0) + + +@ita("þ", l=0.7, r=0.8) +def _ithorn(g): + X, D, AS = g.X, g.D, g.AS + entry(g, 0, AS).l(0, D + g.B / 2, 1).draw(); g.serif(0, D, 1, "b") + (iarch(g, 0, 330, X).l(330, X * 0.4, 1).c(330, X * 0.1, 270, g.yb, 190, g.yb, 0.5) + .c(120, g.yb, 40, X * 0.08, g.h * 0.3, X * 0.25, 0).draw()) + + +# ---------- ligatures ffi, ffl ---------- + +def ff_first(g, x0, x2): + AS, B, o, h = g.AS, g.B, g.o, g.h + yt2 = AS + o - B / 2 + (g.P(x0, B / 2, 1).l(x0, AS - 178, 1).c(x0, AS - 45, x0 + 60, yt2, x0 + 140, yt2, 0.5) + .c(x0 + 200, yt2, x2 - h - 6, AS - 30, x2 - h * 0.4, AS - 82, 0.05).draw()) + g.serif(x0, 0, 1, "b", g.A, g.sl * 1.2) + + +@rom("ffi") +def _ffi(g): + x2 = 262 + ff_first(g, 0, x2) + g.bar(-g.h - 78, x2, g.X - g.B / 2) + g.use(fn_of("fi"), x2) + + +@rom("ffl") +def _ffl(g): + x2 = 262 + ff_first(g, 0, x2) + g.bar(-g.h - 78, x2, g.X - g.B / 2) + g.use(fn_of("fl"), x2) + + +@ita("ffi", l=0.3, r=0.5) +def _iffi(g): + x = 190 + it_f(g, x, x + 128) + g.bar(x - 130, x + 215, g.X - g.B / 2) + g.use(fn_of("fi", True), 215) + + +@ita("ffl", l=0.3, r=0.5) +def _iffl(g): + x = 190 + it_f(g, x, x + 128) + g.bar(x - 130, x + 215, g.X - g.B / 2) + g.use(fn_of("fl", True), 215) + + +# ===================================================================== +# Greek: the rest of the basic lowercase and the capitals that are not +# Latin look-alikes. Same rules as the sketch's Greek. +# ===================================================================== + +def gstem_top(g, x): + """A Greek stem start: light at the top, full by 60 units down.""" + return g.P(x, g.X - g.B / 2, 0.5).l(x, g.X - 60, 1) + + +def tail_hook(p, g, x, y0): + """The descending hook of ζ, ξ, ς.""" + return p.c(x + 110, y0 - 5, x + 140, y0 - 55, x + 125, y0 - 115, 0.9).c(x + 115, y0 - 165, x + 65, y0 - 190, x + 15, y0 - 195, 0.1) + + +@rom("ζ", l=0.8, r=0.6) +def _zeta(g): + X, AS = g.X, g.AS + g.P(70, AS - 40, 0.05).c(160, AS - 22, 260, AS - 18, 340, AS - 36, 0.3).draw() + p = (g.P(330, AS - 40, 0.3).c(170, X * 1.05, 25, X * 0.62, 25, X * 0.3, 1).c(25, 55, 95, 28, 205, 22, 0.35)) + tail_hook(p, g, 205, 22).draw() + + +@rom("η", l=0.7, r=0.6) +def _eta(g): + X, D, B, h = g.X, g.D, g.B, g.h + x1 = 330 + gstem_top(g, 0).l(0, B / 2, 1).draw() + yt = X + g.o - B / 2 + xm = x1 / 2 + 12 + (g.P(h * 0.3, X * 0.6, 0).c(h * 0.3 + 30, X * 0.88, xm - xm * 0.55, yt, xm, yt, 0.55) + .c(xm + (x1 - xm) * 0.6, yt, x1, X * 0.9, x1, X * 0.6, 1).l(x1, D + 40, 1).l(x1, D + B / 2, 0.7).draw()) + + +@rom("ι", l=0.7, r=0.5) +def _iota(g): + yb = g.yb + gstem_top(g, 0).l(0, 70, 1).c(0, 22, 22, yb, 60, yb, 0.3).c(80, yb, 93, 18, 96, 42, 0).draw() + + +@rom("κ", l=0.7, r=0.55) +def _kappa(g): + X, B, h, bd = g.X, g.B, g.h, g.bd + gstem_top(g, 0).l(0, B / 2, 1).draw() + g.P(h - 2, X * 0.36, 0).c(110, X * 0.5, 210, X * 0.9, 300, X - 30, 0).draw() + g.ball(306, X - 36, bd * 0.75) + (g.P(110, X * 0.55, 0.9).l(262, 70, 1).c(272, 30, 290, g.yb, 320, g.yb, 0.3) + .c(340, g.yb, 352, 18, 355, 42, 0).draw()) + + +@rom("ν", l=0.6, r=0.5) +def _nu(g): + X, A, B, o = g.X, g.A, g.B, g.o + g.P(-40, X - 45, 0.2).c(-20, X + 2, 20, X + 4, 30, X - 40, 1).l(205, B / 2 - o, 0.9).draw() + g.P(205 + A * 0.1, B / 2 - o, 0).c(300, X * 0.3, 385, X * 0.58, 392, X - 10, 0.25).draw() + + +@rom("ξ", l=0.8, r=0.6) +def _xi(g): + X, AS = g.X, g.AS + g.P(70, AS - 40, 0.05).c(160, AS - 22, 260, AS - 18, 340, AS - 36, 0.3).draw() + (g.P(320, AS - 40, 0.2).c(190, AS - 48, 95, AS - 100, 95, AS - 175, 1) + .c(95, X + 62, 170, X + 32, 270, X + 30, 0).draw()) + p = (g.P(270, X + 30, 0).c(130, X + 30, 25, X * 0.66, 25, X * 0.32, 1).c(25, 55, 95, 28, 205, 22, 0.35)) + tail_hook(p, g, 205, 22).draw() + + +@rom("τ", l=0.5, r=0.5) +def _tau(g): + X, A, B, o = g.X, g.A, g.B, g.o + yb, bb = -o + B / 2, A * 0.36 + yb2 = X - bb / 2 + g.P(-12, X * 0.76, 0.4).c(0, X * 0.92, 40, yb2, 110, yb2, 0.6).l(400, yb2, 0.3).draw({"b": bb}) + g.P(205, X - bb, 1).l(205, 78, 1).c(205, 25, 225, yb, 259, yb, 0.3).c(285, yb, 299, 20, 302, 46, 0).draw() + + +@rom("υ", l=0.7, r=0.75) +def _upsilon(g): + X, yb = g.X, g.yb + (gstem_top(g, 0).l(0, X * 0.4, 1).c(0, X * 0.08, 90, yb, 185, yb, 0.5).c(300, yb, 375, X * 0.25, 375, X * 0.58, 1) + .c(375, X * 0.84, 352, X * 0.96, 318, X + 6, 0.1).draw()) + + +@rom("χ", l=0.6, r=0.6) +def _chi(g): + X, D = g.X, g.D + g.P(-35, X - 50, 0.2).c(-15, X + 2, 25, X + 4, 45, X - 45, 1).l(365, D + 70, 1).c(375, D + 30, 395, D + 18, 425, D + 40, 0).draw() + g.P(405, X - 12, 0).l(5, D + 15, 0).draw() + + +@rom("ς", l=0.82, r=0.55) +def _sigmafinal(g): + X, yt, h, bd = g.X, g.yt, g.h, g.bd + p = (g.P(335, X * 0.79, 0.2).c(328, X * 0.97, 268, yt, 205, yt, 0.4).c(85, yt, h, X * 0.75, h, X * 0.42, 1) + .c(h, X * 0.1, 95, 26, 210, 20, 0.35)) + tail_hook(p, g, 210, 20).draw() + g.ball(335, X * 0.79, bd * 0.85) + + +@rom_cap("Θ", l=0.82, r=0.82) +def _Theta(g): + C = g.C + g.use(fn_of("O")) + g.bar(330 - 165, 330 + 165, C / 2) + for x in (330 - 165, 330 + 165): + vbeak(g, x + (g.S * 0.7 if x < 330 else -g.S * 0.7), C / 2 - 52, C / 2 + 52) + + +@rom_cap("Λ", l=0.45, r=0.45) +def _Lambda(g): + C, A, B, o = g.C, g.A, g.B, g.o + xl, xm, xr = 0, 280, 560 + g.P(xl, B / 2, 0).l(xm - A * 0.15, C + o - B / 2, 0).draw(); g.P(xm - A * 0.25, C + o - B / 2, 0.9).l(xr, B / 2, 1).draw() + g.serif(xl, 0, 1, "b", B * 1.6); g.serif(xr, 0, 1, "b") + + +@rom_cap("Ξ", l=0.6, r=0.6) +def _Xi(g): + C, B, S = g.C, g.B, g.S + W = 470 + g.bar(0, W, C - B / 2); vbeak(g, S * 0.7, C, C - 120); vbeak(g, W - S * 0.7, C, C - 120) + g.bar(70, W - 70, C * 0.5); vbeak(g, 70 + S * 0.7, C * 0.5 - 55, C * 0.5 + 55); vbeak(g, W - 70 - S * 0.7, C * 0.5 - 55, C * 0.5 + 55) + g.bar(0, W, B / 2); vbeak(g, S * 0.7, 0, 120); vbeak(g, W - S * 0.7, 0, 120) + + +@rom_cap("Π") +def _Pi(g): + C, B, h = g.C, g.B, g.h + x1 = 480 + g.stem(0, 0, C); g.stem(x1, 0, C); g.bar(-h, x1 + h, C - B / 2) + g.serif(0, C, -1, "l"); g.serif(x1, C, -1, "r"); g.serif(0, 0, 1, "b"); g.serif(x1, 0, 1, "b") + + +@rom_cap("Φ", l=0.82, r=0.82) +def _Phi(g): + C, B, o = g.C, g.B, g.o + cx = 330 + g.stem(cx, 0, C); g.serif(cx, C, -1, "b"); g.serif(cx, 0, 1, "b") + g.ring(cx, C / 2, 300 - g.h, C * 0.3, 1) + + +@rom_cap("Ψ", l=0.6, r=0.6) +def _Psi(g): + C, B, A, o = g.C, g.B, g.A, g.o + cx, w = 320, 285 + g.stem(cx, 0, C); g.serif(cx, C, -1, "b"); g.serif(cx, 0, 1, "b") + yb = C * 0.3 + (g.P(cx - w, C - B / 2, 1).l(cx - w, C * 0.66, 1).c(cx - w, C * 0.42, cx - w * 0.55, yb, cx, yb, 0.4) + .c(cx + w * 0.55, yb, cx + w, C * 0.42, cx + w, C * 0.66, 0).l(cx + w, C - B / 2, 0).draw()) + g.serif(cx - w, C, -1, "b"); g.serif(cx + w, C, -1, "b", B * 1.6, g.sl * 0.8) + + +# ===================================================================== +# Maths: the rule pen (square nib, R thick), centred on the axis (250). +# ===================================================================== + +@rom("÷", **OPW) +def _divide(g): + rule(g, 0, AX, 560, AX) + for y in (AX + 160, AX - 160): + g.dot(280, y, g.A * 0.95) + + +@rom("±", **OPW) +def _plusminus(g): + rule(g, 0, AX + 80, 560, AX + 80); rule(g, 280, AX - 170, 280, AX + 330); rule(g, 0, AX - 250, 560, AX - 250) + + +@rom("≠", **OPW) +def _notequal(g): + rule(g, 0, AX + 108, 560, AX + 108); rule(g, 0, AX - 108, 560, AX - 108); rule(g, 170, AX - 270, 390, AX + 270, 3) + + +@rom("≤", **OPW) +def _lessequal(g): + r = Rw(g) + g.P(500, AX + 310, 0).l(0, AX + 95, 0).l(500, AX - 120, 0).draw({"a": r, "b": r, "ne": 2.6, "role": "rule"}) + rule(g, 0, AX - 260, 500, AX - 260) + + +@rom("≥", **OPW) +def _greaterequal(g): + r = Rw(g) + g.P(0, AX + 310, 0).l(500, AX + 95, 0).l(0, AX - 120, 0).draw({"a": r, "b": r, "ne": 2.6, "role": "rule"}) + rule(g, 0, AX - 260, 500, AX - 260) + + +@rom("≈", **OPW) +def _approxequal(g): + r = Rw(g) + for dy in (95, -95): + y = AX + dy + (g.P(0, y - 40, 0).c(40, y + 60, 140, y + 75, 280, y, 0).c(420, y - 75, 520, y - 60, 560, y + 40, 0) + .draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + + +@rom("≡", **OPW) +def _equivalence(g): + for dy in (165, 0, -165): + rule(g, 0, AX + dy, 560, AX + dy) + + +def member(g, bar=True, raise_=0): + r = Rw(g) + t, b = AX + 270 + raise_, AX - 270 + raise_ + m = (t + b) / 2 + (g.P(520, t, 0).l(300, t, 0).c(130, t, 40, m + 150, 40, m, 0).c(40, m - 150, 130, b, 300, b, 0).l(520, b, 0) + .draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + if bar: + rule(g, 40, m, 500, m) + + +@rom("∈", **OPW) +def _element(g): + member(g) + + +@rom("∉", **OPW) +def _notelement(g): + member(g); rule(g, 150, AX - 330, 420, AX + 330, 3) + + +@rom("⊂", **OPW) +def _subset(g): + member(g, bar=False) + + +@rom("⊆", **OPW) +def _reflexsubset(g): + member(g, bar=False, raise_=80); rule(g, 40, AX - 290, 520, AX - 290) + + +@rom("∪", **OPW) +def _union(g): + r = Rw(g) + (g.P(0, AX + 300, 0).l(0, AX).c(0, AX - 180, 110, AX - 280, 280, AX - 280).c(450, AX - 280, 560, AX - 180, 560, AX) + .l(560, AX + 300).draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + + +@rom("∩", **OPW) +def _intersection(g): + r = Rw(g) + (g.P(0, AX - 300, 0).l(0, AX).c(0, AX + 180, 110, AX + 280, 280, AX + 280).c(450, AX + 280, 560, AX + 180, 560, AX) + .l(560, AX - 300).draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + + +@rom("∘", **OPW) +def _ring(g): + g.ring(0, AX, 95, 95, 0.3) + + +def arrow_head(g, x, dir): + r = Rw(g) + for s in (1, -1): + (g.P(x, AX, 0).c(x - dir * 40, AX + s * 20, x - dir * 85, AX + s * 70, x - dir * 120, AX + s * 150, 0) + .draw({"a": r, "b": r, "ne": 8, "role": "rule"})) + + +@rom("→", **OPW) +def _arrowright(g): + rule(g, 0, AX, 780, AX); arrow_head(g, 800, 1) + + +@rom("←", **OPW) +def _arrowleft(g): + rule(g, 20, AX, 800, AX); arrow_head(g, 0, -1) + + +@rom("↔", **OPW) +def _arrowboth(g): + rule(g, 20, AX, 880, AX); arrow_head(g, 0, -1); arrow_head(g, 900, 1) + + +@rom("∀", l=0.45, r=0.45, upright=True) +def _universal(g): + C, A, B, o = g.C, g.A, g.B, g.o + g.P(0, C - B / 2, 1).l(275 - A * 0.2, B / 2 - o, 0.9).draw(); g.P(275 + A * 0.1, B / 2 - o, 0).l(550, C - B / 2, 0).draw() + g.bar(75, 475, C * 0.62) + + +@rom("∃", l=0.6, r=0.6, upright=True) +def _existential(g): + C, B, h = g.C, g.B, g.h + x = 420 + g.stem(x, 0, C) + g.bar(0, x + h, C - B / 2); g.bar(60, x + h, C * 0.51); g.bar(0, x + h, B / 2) + + +@rom("∅", l=0.7, r=0.7, upright=True) +def _emptyset(g): + C, o, B, h = g.C, g.o, g.B, g.h + g.ring(290, C / 2, 250 - h, C / 2 + o - B / 2, 1, 2.05) + g.P(560, C + 50, 0.3).l(20, -50, 0.3).draw() + + +@rom("∝", **OPW) +def _proportional(g): + r = Rw(g) + a, b, cx = 330, 150, 290 + p = g.P(560, AX - 150, 0).c(470, AX - 150, 400, AX - 110, cx + 20, AX - 15, 0) + pts = [] + for i in range(0, 101): + t = PI + i / 100 * PI + pts.append((cx + a * 0.9 * math.sin(t) / (1 + 0.18 * math.cos(2 * t)), + AX + b * math.sin(2 * t) / (1 + 0.18 * math.cos(2 * t)) * 1.05)) + for x, y in pts: + p.l(x, y, 0) + p.c(cx + 80, AX + 90, 470, AX + 150, 560, AX + 150, 0).draw({"a": r, "b": r, "ne": 8, "role": "rule"}) + + +@rom("∏", l=0.6, r=0.6, bigop=True, upright=True) +def _product(g): + B, h = g.B, g.h + top, bot = AX + 520, AX - 520 + x0, x1 = 60, 640 + g.stem(x0, bot, top); g.stem(x1, bot, top) + g.bar(x0 - h - 40, x1 + h + 40, top - B / 2) + g.serif(x0, bot, 1, "b"); g.serif(x1, bot, 1, "b") + + +@rom("″", l=0.4, r=0.4) +def _second(g): + for dx in (0, 150): + g.P(150 + dx, 720, 1).l(40 + dx, 440, 0.1).draw() + + +@rom("◌", l=0.6, r=0.6, upright=True) +def _dottedcircle(g): + cx, cy, R = 300, g.X / 2, 270 + for k in range(12): + a = k * TAU / 12 + g.dot(cx + R * math.cos(a), cy + R * math.sin(a), 46) + + +@rom("⁄", l=-0.6, r=-0.6) +def _fraction(g): + C = g.C + g.P(330, C + 10, 0.35).l(-10, -20, 0.35).draw() diff --git a/warp/loom/features.py b/warp/loom/features.py new file mode 100644 index 0000000..7f61b77 --- /dev/null +++ b/warp/loom/features.py @@ -0,0 +1,100 @@ +"""OpenType feature code for machine-made Warp (GSUB). + +ufo2ft writes kern, mark, mkmk and GDEF from the UFO kerning, anchors +and glyph categories. This module writes the substitutions: + +* ccmp: i and j lose their dot before a combining mark above. +* liga: ff, fi, fl, ffi, ffl. +* Figures. Default: lining, tabular (the sketch's figures, for tables + and papers). onum: old-style, proportional (as in the sketch's running + text). lnum, pnum, tnum switch each property. The lookups run in the + order onum, lnum, pnum, tnum, so onum + tnum gives tabular old-style. +* smcp: lowercase to small capitals (ß to two small-cap S). c2sc: + capitals to small capitals. + +Apache-2.0. +""" + +from __future__ import annotations + +from loom import charset as CS + +FIG = CS.FIG_NAMES + + +def _cls(suffix: str) -> str: + return "[" + " ".join(f"{n}{suffix}" for n in FIG) + "]" + + +def features(st) -> str: + sm = CS.smcp_pairs() + c2 = CS.c2sc_pairs() + top_marks = " ".join(n for n in CS.ORDER if CS.BY_NAME[n].category == "mark" + and not n.endswith(".case") and n != "cedillacomb") + lines = [ + "languagesystem DFLT dflt;", + "languagesystem latn dflt;", + "languagesystem grek dflt;", + "", + f"@CombTop = [{top_marks}];", + "@Fig = " + _cls("") + ";", + "@FigLf = " + _cls(".lf") + ";", + "@FigOsf = " + _cls(".osf") + ";", + "@FigTosf = " + _cls(".tosf") + ";", + "@Lower = [" + " ".join(a for a, _ in sm) + "];", + "@LowerSc = [" + " ".join(b for _, b in sm) + "];", + "@Upper = [" + " ".join(a for a, _ in c2) + "];", + "@UpperSc = [" + " ".join(b for _, b in c2) + "];", + "", + "feature ccmp {", + " lookup dotless {", + " sub [i j]' @CombTop by [dotlessi dotlessj];", + " } dotless;", + "} ccmp;", + "", + "feature smcp {", + " sub germandbls by s.sc s.sc;", + " sub f_f_i by f.sc f.sc i.sc;", + " sub f_f_l by f.sc f.sc l.sc;", + " sub f_f by f.sc f.sc;", + " sub fi by f.sc i.sc;", + " sub fl by f.sc l.sc;", + " sub dotlessj by j.sc;", + " sub micro by mu.sc;", + " sub @Lower by @LowerSc;", + "} smcp;", + "", + "feature c2sc {", + " sub @Upper by @UpperSc;", + "} c2sc;", + "", + "feature liga {", + " sub f f i by f_f_i;", + " sub f f l by f_f_l;", + " sub f f by f_f;", + " sub f i by fi;", + " sub f l by fl;", + "} liga;", + "", + "feature onum {", + " sub @Fig by @FigOsf;", + " sub @FigLf by @FigOsf;", + "} onum;", + "", + "feature lnum {", + " sub @FigOsf by @FigLf;", + " sub @FigTosf by @Fig;", + "} lnum;", + "", + "feature pnum {", + " sub @Fig by @FigLf;", + " sub @FigTosf by @FigOsf;", + "} pnum;", + "", + "feature tnum {", + " sub @FigLf by @Fig;", + " sub @FigOsf by @FigTosf;", + "} tnum;", + "", + ] + return "\n".join(lines) diff --git a/warp/loom/kerning.py b/warp/loom/kerning.py new file mode 100644 index 0000000..cbb0158 --- /dev/null +++ b/warp/loom/kerning.py @@ -0,0 +1,101 @@ +"""Class kerning for machine-made Warp. + +Starts from the sketch's kerning table (`sketch_kern` in loom/sketch.py: +To Te Ta Tr Ty TA T. Av Aw AV AT AY A' Va Ve Vo Ya Yo LT LV LY L' P. P, +F. r. f' quotes and others) and turns it into classes: accented letters +and Greek letters join the class of their Latin look-alike, so that +Tά, Ταυ, ΓΑ, ΥΑ and Àv kern like To, TA and Av. Figures: the +proportional styles (lf, osf) get a few pairs; the default tabular +figures are not kerned. Italic uses 70 % of the values, as in the +sketch. + +This file defines the spacing of machine-made Warp: SIL Open Font +License 1.1, Reserved Font Name "machine-made" (see ../OFL.txt). +""" + +from __future__ import annotations + +from loom import charset as CS + + +def _names(chars: str, extra: tuple = ()) -> list[str]: + out = [] + for ch in chars: + n = CS.glyph_name(ch) + if n in CS.BY_NAME: + out.append(n) + out += [n for n in extra if n in CS.BY_NAME] + return out + + +# First glyph (its right side). +KERN1 = { + "A": _names("AÀÁÂÃÄÅΑΔΛ"), + "T": _names("TΤΓ"), + "V": _names("VW"), + "Y": _names("YÝŸΥ"), + "L": _names("L"), + "P": _names("PΡÞ"), + "F": _names("F"), + "R": _names("R"), + "K": _names("KΚXΧ"), + "r": _names("r"), + "f": _names("f"), + "k": _names("kκ"), + "o": _names("oòóôõöøbpþοσ"), + "v": _names("vwyýÿνγ"), + "quote": _names("'’\"”"), + "seven": ["seven.lf", "seven.osf"], +} + +# Second glyph (its left side). +KERN2 = { + "a": _names("aàáâãäåæα"), + "o": _names("ceoqdgçèéêëòóôõöøœðοσεθδφςω"), + "s": _names("s"), + "u": _names("ruùúûüıυικμ"), + "n": _names("nmpñηπρ"), + "v": _names("vwyýÿνγχ"), + "A": _names("AÀÁÂÃÄÅÆΑΔΛ"), + "O": _names("CGOQÇÒÓÔÕÖØŒΘΟΩ"), + "J": _names("J"), + "T": _names("TΤ"), + "V": _names("VW"), + "Y": _names("YÝŸΥ"), + "quote": _names("'’\"”"), + "period": _names(".,…‚„"), + "close": _names(")]}?!"), + "four": ["four.lf", "four.osf"], +} + +# (first class, second class): value in font units (roman). +PAIRS = { + ("T", "o"): -62, ("T", "a"): -62, ("T", "s"): -62, ("T", "u"): -62, ("T", "n"): -40, + ("T", "v"): -45, ("T", "A"): -55, ("T", "period"): -70, ("T", "J"): -40, + ("A", "T"): -58, ("A", "V"): -58, ("A", "Y"): -58, ("A", "v"): -34, ("A", "quote"): -60, ("A", "O"): -18, + ("V", "a"): -46, ("V", "o"): -46, ("V", "period"): -70, ("V", "A"): -55, ("V", "u"): -24, + ("Y", "a"): -64, ("Y", "o"): -64, ("Y", "period"): -80, ("Y", "A"): -60, ("Y", "u"): -36, + ("L", "T"): -70, ("L", "V"): -70, ("L", "Y"): -70, ("L", "quote"): -80, ("L", "v"): -30, + ("P", "period"): -80, ("P", "A"): -40, + ("F", "period"): -60, ("F", "a"): -20, ("F", "A"): -30, + ("R", "T"): -24, ("R", "V"): -24, ("R", "Y"): -24, + ("K", "o"): -18, ("K", "O"): -20, + ("r", "period"): -50, + ("v", "period"): -40, ("v", "o"): -10, ("v", "a"): -8, + ("f", "quote"): 30, ("f", "close"): 30, + ("quote", "s"): -25, ("quote", "A"): -55, + ("k", "o"): -12, + ("o", "v"): -10, + ("seven", "period"): -50, ("seven", "four"): -30, +} + + +def groups() -> dict[str, list[str]]: + g = {f"public.kern1.{k}": v for k, v in KERN1.items()} + g.update({f"public.kern2.{k}": v for k, v in KERN2.items()}) + return g + + +def kerning(st) -> dict[tuple[str, str], int]: + f = 0.7 if st.italic else 1.0 + return {(f"public.kern1.{a}", f"public.kern2.{b}"): int(round(v * f)) for (a, b), v in PAIRS.items()} diff --git a/warp/loom/outline.py b/warp/loom/outline.py new file mode 100644 index 0000000..5dfcc35 --- /dev/null +++ b/warp/loom/outline.py @@ -0,0 +1,462 @@ +"""From pen sweeps to clean outlines. + +1. union: the sweep polygons (convex hulls of consecutive nib positions, + discs, teardrops, bracket fillets) are joined with skia-pathops + (nonzero fill). The result is a set of polygons with many short edges. +2. fit: each polygon is cut at its corners and each run between corners + is fitted with as few cubic Bézier curves as the tolerance allows + (Schneider's algorithm, with Newton reparameterisation). Straight + runs become lines. Curves are split at their x and y extrema, so + that every extremum is an on-curve point. +3. clean: no segment shorter than MIN_SEG, contours wind counter- + clockwise outside and clockwise inside (PostScript order; ufo2ft + reverses them for TrueType), tiny slivers are dropped. + +loom/raster.py checks the result against an independent rasterised fill +of the sweep (tools/fidelity.py writes the numbers). + +Apache-2.0. +""" + +from __future__ import annotations + +import math + +import numpy as np +import pathops + +TOL = 0.9 # largest distance of the fit from the union, in font units +CORNER_DEG = 32.0 # a vertex that turns more than this is a corner +CORNER_MERGE = 7.0 # corners closer than this (arc length) are one corner +MIN_SEG = 4.0 # no segment shorter than this +MIN_AREA = 40.0 # contours with less area are dropped (union slivers) +LINE_TOL = 0.35 # a run within this of its chord is a line + + +# ------------------------------------------------------------------ union + +def union(polys) -> pathops.Path: + path = pathops.Path() + path.fillType = pathops.FillType.WINDING + pen = path.getPen() + for poly in polys: + if len(poly) < 3: + continue + pen.moveTo(poly[0]) + for q in poly[1:]: + pen.lineTo(q) + pen.closePath() + path.simplify(fix_winding=True, keep_starting_points=False, clockwise=False) + return path + + +def path_polygons(path: pathops.Path) -> list[np.ndarray]: + """The contours of a line-only pathops path, as arrays (no repeated end).""" + out = [] + for contour in path.contours: + pts = [] + for verb, args in contour.segments: + if verb == "moveTo": + pts = [args[0]] + elif verb == "lineTo": + pts.append(args[0]) + elif verb in ("qCurveTo", "curveTo"): + pts.append(args[-1]) + if len(pts) > 2 and pts[0] == pts[-1]: + pts = pts[:-1] + if len(pts) >= 3: + out.append(np.array(pts, dtype=float)) + return out + + +def signed_area(a: np.ndarray) -> float: + x, y = a[:, 0], a[:, 1] + return 0.5 * float(np.dot(x, np.roll(y, -1)) - np.dot(np.roll(x, -1), y)) + + +# -------------------------------------------------------------- cleanup + +def dedupe(a: np.ndarray, eps: float = 0.05) -> np.ndarray: + keep = [0] + for i in range(1, len(a)): + if np.hypot(*(a[i] - a[keep[-1]])) > eps: + keep.append(i) + a = a[keep] + if len(a) > 2 and np.hypot(*(a[0] - a[-1])) <= eps: + a = a[:-1] + return a + + +def drop_collinear(a: np.ndarray, eps: float = 1e-3) -> np.ndarray: + changed = True + while changed and len(a) > 3: + changed = False + n = len(a) + keep = [] + for i in range(n): + p0, p1, p2 = a[i - 1], a[i], a[(i + 1) % n] + d1, d2 = p1 - p0, p2 - p1 + cross = d1[0] * d2[1] - d1[1] * d2[0] + dot = d1[0] * d2[0] + d1[1] * d2[1] + if abs(cross) <= eps * np.hypot(*d1) * np.hypot(*d2) and dot > 0: + changed = True + continue + keep.append(i) + a = a[keep] + return a + + +def turning(a: np.ndarray) -> np.ndarray: + """Signed turning angle at each vertex, in degrees.""" + d_in = a - np.roll(a, 1, axis=0) + d_out = np.roll(a, -1, axis=0) - a + ang = np.arctan2(d_in[:, 0] * d_out[:, 1] - d_in[:, 1] * d_out[:, 0], + (d_in * d_out).sum(axis=1)) + return np.degrees(ang) + + +def corners(a: np.ndarray) -> list[int]: + """Indices of corner vertices. + + A vertex is a corner when it turns more than CORNER_DEG, or when the + vertices within CORNER_MERGE / 2 around it turn more than 2 x + CORNER_DEG together (a small rounded nib end). Corners closer than + CORNER_MERGE merge into the one that turns most. + """ + n = len(a) + th = turning(a) + seg = np.hypot(*(np.roll(a, -1, axis=0) - a).T) + cand = [] + half = CORNER_MERGE / 2 + for i in range(n): + if abs(th[i]) > CORNER_DEG: + cand.append(i) + continue + # turning in a short window + tot = th[i] + s = 0.0 + j = i + while True: + s += seg[j % n] + j += 1 + if s > half or j - i >= n: + break + tot += th[j % n] + s = 0.0 + j = i + while True: + j -= 1 + s += seg[j % n] + if s > half or i - j >= n: + break + tot += th[j % n] + if abs(tot) > 2 * CORNER_DEG and abs(th[i]) > 8: + cand.append(i) + if not cand: + return [] + # merge close candidates: keep the strongest of each cluster + pos = np.concatenate([[0], np.cumsum(seg)])[:n] + total = float(seg.sum()) + cand.sort() + clusters = [[cand[0]]] + for c in cand[1:]: + if pos[c] - pos[clusters[-1][-1]] < CORNER_MERGE: + clusters[-1].append(c) + else: + clusters.append([c]) + if len(clusters) > 1 and (pos[clusters[0][0]] + total - pos[clusters[-1][-1]]) < CORNER_MERGE: + clusters[0] = clusters.pop() + clusters[0] + return sorted(max(cl, key=lambda k: abs(th[k])) for cl in clusters) + + +# -------------------------------------------------------------- fitting + +def _bez(ctrl, t): + t = np.asarray(t)[:, None] + m = 1 - t + return m ** 3 * ctrl[0] + 3 * m * m * t * ctrl[1] + 3 * m * t * t * ctrl[2] + t ** 3 * ctrl[3] + + +def _bez_d1(ctrl, t): + t = np.asarray(t)[:, None] + m = 1 - t + return 3 * (m * m * (ctrl[1] - ctrl[0]) + 2 * m * t * (ctrl[2] - ctrl[1]) + t * t * (ctrl[3] - ctrl[2])) + + +def _bez_d2(ctrl, t): + t = np.asarray(t)[:, None] + m = 1 - t + return 6 * (m * (ctrl[2] - 2 * ctrl[1] + ctrl[0]) + t * (ctrl[3] - 2 * ctrl[2] + ctrl[1])) + + +def _chord_params(pts): + d = np.concatenate([[0.0], np.cumsum(np.hypot(*np.diff(pts, axis=0).T))]) + return d / d[-1] if d[-1] > 0 else d + + +def _unit(v): + n = np.hypot(*v) + return v / n if n > 0 else v + + +def _generate(pts, u, t1, t2): + p0, p3 = pts[0], pts[-1] + A1 = t1[None, :] * (3 * (1 - u) ** 2 * u)[:, None] + A2 = t2[None, :] * (3 * (1 - u) * u ** 2)[:, None] + C00 = (A1 * A1).sum() + C01 = (A1 * A2).sum() + C11 = (A2 * A2).sum() + base = _bez(np.array([p0, p0, p3, p3]), u) + tmp = pts - base + X0 = (A1 * tmp).sum() + X1 = (A2 * tmp).sum() + det = C00 * C11 - C01 * C01 + seg = np.hypot(*(p3 - p0)) + if abs(det) > 1e-12: + a1 = (X0 * C11 - X1 * C01) / det + a2 = (C00 * X1 - C01 * X0) / det + else: + a1 = a2 = seg / 3 + eps = 1e-6 * seg + if a1 < eps or a2 < eps or a1 > seg * 2 or a2 > seg * 2: + a1 = a2 = seg / 3 + return np.array([p0, p0 + t1 * a1, p3 + t2 * a2, p3]) + + +def _max_error(pts, ctrl, u): + d = np.hypot(*(_bez(ctrl, u) - pts).T) + i = int(np.argmax(d)) + return float(d[i]), i + + +def _reparam(pts, ctrl, u): + q = _bez(ctrl, u) + d1 = _bez_d1(ctrl, u) + d2 = _bez_d2(ctrl, u) + num = ((q - pts) * d1).sum(axis=1) + den = (d1 * d1).sum(axis=1) + ((q - pts) * d2).sum(axis=1) + with np.errstate(divide="ignore", invalid="ignore"): + nu = np.where(np.abs(den) > 1e-12, u - num / den, u) + nu = np.clip(nu, 0, 1) + nu[0], nu[-1] = 0.0, 1.0 + if np.any(np.diff(nu) < 0): + return u + return nu + + +def _true_error(pts, ctrl, samples=24): + """Largest distance from the polyline vertices to the curve, and from + the curve back to the polyline (so that a curve cannot bulge between + vertices).""" + tt = np.linspace(0, 1, samples * 4 + 1) + cp = _bez(ctrl, tt) + # vertex -> curve + d1 = np.min(np.hypot(pts[:, None, 0] - cp[None, :, 0], pts[:, None, 1] - cp[None, :, 1]), axis=1).max() + # curve -> polyline segments + a, b = pts[:-1], pts[1:] + ab = b - a + L2 = (ab * ab).sum(axis=1) + L2[L2 == 0] = 1 + best = np.full(len(cp), np.inf) + for k in range(len(a)): + t = np.clip(((cp - a[k]) @ ab[k]) / L2[k], 0, 1) + proj = a[k] + t[:, None] * ab[k] + best = np.minimum(best, np.hypot(*(cp - proj).T)) + return max(float(d1), float(best.max())) + + +def fit_cubics(pts, t1, t2, tol=TOL, depth=0): + """Fit a polyline (m x 2) with cubic segments. Returns a list of 4x2 arrays.""" + n = len(pts) + if n == 2: + # two points: a straight segment (the tangents of a corner run do not apply) + return [np.array([pts[0], pts[0] + (pts[1] - pts[0]) / 3, pts[1] - (pts[1] - pts[0]) / 3, pts[1]])] + u = _chord_params(pts) + ctrl = _generate(pts, u, t1, t2) + err, split = _max_error(pts, ctrl, u) + if err < tol and _true_error(pts, ctrl) < tol * 1.5: + return [ctrl] + if err < tol * 4: + for _ in range(12): + u = _reparam(pts, ctrl, u) + ctrl = _generate(pts, u, t1, t2) + err, split = _max_error(pts, ctrl, u) + if err < tol: + break + if err < tol and _true_error(pts, ctrl) < tol * 1.5: + return [ctrl] + if depth > 40: + return [ctrl] + split = min(max(split, 1), n - 2) + tc = _unit(pts[split - 1] - pts[split + 1]) + if not np.any(tc): + tc = _unit(pts[split - 1] - pts[split]) + return (fit_cubics(pts[: split + 1], t1, tc, tol, depth + 1) + + fit_cubics(pts[split:], -tc, t2, tol, depth + 1)) + + +def _is_line(pts, tol=LINE_TOL) -> bool: + a, b = pts[0], pts[-1] + ab = b - a + L = np.hypot(*ab) + if L == 0: + return False + d = np.abs((pts[:, 0] - a[0]) * ab[1] - (pts[:, 1] - a[1]) * ab[0]) / L + return float(d.max()) <= tol + + +def _tangent_out(run): + """Direction leaving the start of a run (first 3 units of arc).""" + acc = 0.0 + for k in range(1, len(run)): + acc += np.hypot(*(run[k] - run[k - 1])) + if acc >= 3.0: + return _unit(run[k] - run[0]) + return _unit(run[-1] - run[0]) + + +def _split_extrema(ctrl, min_len=12.0): + """Split a cubic at its interior x and y extrema.""" + p0, p1, p2, p3 = ctrl + ts = [] + for k in (0, 1): + a = -p0[k] + 3 * p1[k] - 3 * p2[k] + p3[k] + b = 2 * (p0[k] - 2 * p1[k] + p2[k]) + c = p1[k] - p0[k] + if abs(a) < 1e-9: + if abs(b) > 1e-9: + ts.append(-c / b) + else: + disc = b * b - 4 * a * c + if disc >= 0: + r = math.sqrt(disc) + ts += [(-b + r) / (2 * a), (-b - r) / (2 * a)] + ts = sorted(t for t in ts if 0.02 < t < 0.98) + if not ts: + return [ctrl] + pieces, rest, t_prev = [], ctrl, 0.0 + for t in ts: + tl = (t - t_prev) / (1 - t_prev) + left, right = _split_at(rest, tl) + if _arc_len(left) < min_len or _arc_len(right) < min_len: + continue + pieces.append(left) + rest, t_prev = right, t + pieces.append(rest) + return pieces + + +def _split_at(c, t): + p01 = c[0] + (c[1] - c[0]) * t + p12 = c[1] + (c[2] - c[1]) * t + p23 = c[2] + (c[3] - c[2]) * t + a = p01 + (p12 - p01) * t + b = p12 + (p23 - p12) * t + m = a + (b - a) * t + return np.array([c[0], p01, a, m]), np.array([m, b, p23, c[3]]) + + +def _arc_len(c): + q = _bez(c, np.linspace(0, 1, 9)) + return float(np.hypot(*np.diff(q, axis=0).T).sum()) + + +def fit_contour(a: np.ndarray, tol=TOL) -> list: + """Fit one closed polygon. Returns segments: ("line", p0, p1) or ("curve", 4x2).""" + a = drop_collinear(dedupe(a)) + n = len(a) + if n < 3: + return [] + cs = corners(a) + if not cs: + # smooth closed contour: start at the lowest point + i0 = int(np.lexsort((a[:, 0], a[:, 1]))[0]) + a = np.roll(a, -i0, axis=0) + cs_runs = [np.vstack([a, a[:1]])] + smooth_close = True + else: + cs_runs = [] + for k, c in enumerate(cs): + nxt = cs[(k + 1) % len(cs)] + idx = list(range(c, nxt + 1)) if nxt > c else list(range(c, n)) + list(range(0, nxt + 1)) + cs_runs.append(a[idx]) + smooth_close = False + segs = [] + for run in cs_runs: + if len(run) < 2: + continue + if _is_line(run): + segs.append(("line", run[0], run[-1])) + continue + if smooth_close: + t1 = _unit(run[1] - run[-2]) + t2 = -t1 + else: + t1 = _tangent_out(run) + t2 = _tangent_out(run[::-1]) + for c in fit_cubics(run, t1, t2, tol): + for piece in _split_extrema(c): + if _is_line(_bez(piece, np.linspace(0, 1, 9))): + segs.append(("line", piece[0], piece[3])) + else: + segs.append(("curve", piece)) + return merge_short(segs) + + +def _seg_len(s): + return float(np.hypot(*(s[2] - s[1]))) if s[0] == "line" else _arc_len(s[1]) + + +def merge_short(segs: list) -> list: + """Remove segments shorter than MIN_SEG by moving their end point.""" + changed = True + while changed and len(segs) > 2: + changed = False + for i, s in enumerate(segs): + if _seg_len(s) >= MIN_SEG: + continue + j = (i + 1) % len(segs) + start = s[1] if s[0] == "line" else s[1][0] + nxt = segs[j] + if nxt[0] == "line": + segs[j] = ("line", start, nxt[2]) + else: + c = nxt[1].copy() + c[1] = c[1] + (start - c[0]) + c[0] = start + segs[j] = ("curve", c) + del segs[i] + changed = True + break + return segs + + +def outline(polys, tol=TOL) -> list[list]: + """Union the sweep polygons and fit them. Returns contours of segments.""" + path = union(polys) + out = [] + for a in path_polygons(path): + if abs(signed_area(a)) < MIN_AREA: + continue + segs = fit_contour(a, tol) + if len(segs) >= 2: + out.append(segs) + return out + + +def draw(contours, pen, dx=0.0, rnd=True) -> None: + """Draw fitted contours into a segment pen, moved by dx, rounded.""" + def R(p): + x, y = float(p[0]) + dx, float(p[1]) + return (int(math.floor(x + 0.5)), int(math.floor(y + 0.5))) if rnd else (x, y) + + for segs in contours: + first = segs[0][1] if segs[0][0] == "line" else segs[0][1][0] + pen.moveTo(R(first)) + for s in segs: + if s[0] == "line": + pen.lineTo(R(s[2])) + else: + c = s[1] + pen.curveTo(R(c[1]), R(c[2]), R(c[3])) + pen.closePath() diff --git a/warp/loom/raster.py b/warp/loom/raster.py new file mode 100644 index 0000000..d0873ba --- /dev/null +++ b/warp/loom/raster.py @@ -0,0 +1,92 @@ +"""Small raster helpers shared by the tools (fidelity, proofs). + +PolyPen flattens a font glyph into polygons; fill_glyph fills them with +the nonzero rule for an overlap-free outline (nesting depth: even = ink, +odd = counter); fill_sweep fills pen-sweep polygons (their union). + +Apache-2.0. +""" + +from __future__ import annotations + +from fontTools.pens.basePen import BasePen +from PIL import ImageDraw + + +class PolyPen(BasePen): + """Flatten a glyph into closed polygons (font units).""" + + def __init__(self, glyphset=None, dx=0.0, dy=0.0, scale=1.0): + super().__init__(glyphset) + self.polys, self.cur = [], [] + self.dx, self.dy, self.k = dx, dy, scale + + def _t(self, p): + return (self.dx + p[0] * self.k, self.dy + p[1] * self.k) + + def _moveTo(self, p): + self.cur = [self._t(p)] + + def _lineTo(self, p): + self.cur.append(self._t(p)) + + def _curveToOne(self, p1, p2, p3): + p0 = self._raw + self._cubic(p0, p1, p2, p3) + + def _cubic(self, p0, p1, p2, p3): + for k in range(1, 13): + t = k / 12 + m = 1 - t + self.cur.append(self._t((m ** 3 * p0[0] + 3 * m * m * t * p1[0] + 3 * m * t * t * p2[0] + t ** 3 * p3[0], + m ** 3 * p0[1] + 3 * m * m * t * p1[1] + 3 * m * t * t * p2[1] + t ** 3 * p3[1]))) + + def _qCurveToOne(self, p1, p2): + p0 = self._raw + for k in range(1, 9): + t = k / 8 + m = 1 - t + self.cur.append(self._t((m * m * p0[0] + 2 * m * t * p1[0] + t * t * p2[0], + m * m * p0[1] + 2 * m * t * p1[1] + t * t * p2[1]))) + + @property + def _raw(self): + x, y = self._getCurrentPoint() + return (x, y) + + def _closePath(self): + if len(self.cur) >= 3: + self.polys.append(self.cur) + self.cur = [] + + _endPath = _closePath + + +def inside(pt, poly) -> bool: + """Even-odd point in polygon.""" + x, y = pt + c = False + n = len(poly) + for i in range(n): + x1, y1 = poly[i] + x2, y2 = poly[(i + 1) % n] + if (y1 > y) != (y2 > y) and x < x1 + (y - y1) * (x2 - x1) / (y2 - y1): + c = not c + return c + + +def fill_glyph(draw: ImageDraw.ImageDraw, polys, tr, ink=255, paper=0) -> None: + """Fill an overlap-free outline: even nesting depth is ink, odd is counter.""" + depth = [sum(inside(q[0], o) for j, o in enumerate(polys) if j != i) for i, q in enumerate(polys)] + for d, poly in sorted(zip(depth, polys), key=lambda t: t[0]): + pts = [tr(p) for p in poly] + if d % 2 == 0: + draw.polygon(pts, fill=ink) + else: + draw.polygon(pts, fill=paper, outline=ink) + + +def fill_sweep(draw: ImageDraw.ImageDraw, polys, tr, ink=255) -> None: + for poly in polys: + if len(poly) >= 3: + draw.polygon([tr(p) for p in poly], fill=ink) diff --git a/warp/loom/sketch.py b/warp/loom/sketch.py new file mode 100644 index 0000000..bbd09ea --- /dev/null +++ b/warp/loom/sketch.py @@ -0,0 +1,1484 @@ +"""The glyph tables of the approved sketch 0.2, ported to Python. + +Source: reference/warp-sketch-0.2.js, the `def(...)` and `idef(...)` +calls. Each function draws the centre-lines of one glyph with the +builder `g` (loom/engine.py). Coordinates and pressures are the +sketch's, unchanged. G holds the roman glyphs, GI the italic overrides. + +This file defines the outlines of machine-made Warp: SIL Open Font +License 1.1, Reserved Font Name "machine-made" (see ../OFL.txt). +""" + +from __future__ import annotations + +import math + +from loom.engine import PI, TAU, GlyphDef, flags + +G: dict[str, GlyphDef] = {} +GI: dict[str, GlyphDef] = {} + + +def rom(ch, **o): + def deco(fn): + G[ch] = GlyphDef(fn=fn, **{**flags(ch), **o}) + return fn + return deco + + +def ita(ch, **o): + def deco(fn): + GI[ch] = GlyphDef(fn=fn, **{**flags(ch), **o}) + return fn + return deco + + +# ===================================================================== +# ROMAN LOWERCASE. Coordinates are centre-lines. g.h = half stem. +# g.yt / g.yb = centre-line of round tops / bottoms. +# ===================================================================== + +def arch(g, x0, x1, top): + """Hairline branch out of the stem, over the shoulder, down the right stem.""" + B, h = g.B, g.h + yt = top + g.o - B / 2 + xm = (x0 + x1) / 2 + 12 + (g.P(x0 + h * 0.3, top * 0.6, 0).c(x0 + h * 0.3 + 30, top * 0.88, xm - (xm - x0) * 0.55, yt, xm, yt, 0.55) + .c(xm + (x1 - xm) * 0.6, yt, x1, top * 0.9, x1, top * 0.6, 1).l(x1, B / 2, 1).draw()) + g.serif(x1, 0, 1, "b") + + +def bowl_right(g, x0): + """b, p: bowl hanging off a left stem.""" + X, o, B = g.X, g.o, g.B + rx = 205 + cx, cy, ry, t0 = x0 + rx, X / 2, X / 2 + o - B / 2, 0.84 * PI + (g.E(cx, cy, rx, ry, t0, 0).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.6).arc(cx, cy, rx, ry, 0.5 * PI, 0, 1) + .arc(cx, cy, rx, ry, 0, -0.5 * PI, 0.6).arc(cx, cy, rx, ry, -0.5 * PI, -t0, 0).draw()) + + +def bowl_left(g, x1): + """d, q.""" + X, o, B = g.X, g.o, g.B + rx = 205 + cx, cy, ry, t0 = x1 - rx, X / 2, X / 2 + o - B / 2, 0.16 * PI + (g.E(cx, cy, rx, ry, t0, 0).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.6).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.6).arc(cx, cy, rx, ry, 1.5 * PI, 2 * PI - t0, 0).draw()) + + +def dotD(g): + return g.A * 1.16 + + +@rom("n") +def _n(g): + X = g.X + g.stem(0, 0, X); g.serif(0, X, -1, "l"); g.serif(0, 0, 1, "b"); arch(g, 0, 330, X) + + +@rom("m") +def _m(g): + X = g.X + g.stem(0, 0, X); g.serif(0, X, -1, "l"); g.serif(0, 0, 1, "b"); arch(g, 0, 290, X); arch(g, 290, 580, X) + + +@rom("h") +def _h(g): + X, AS = g.X, g.AS + g.stem(0, 0, AS); g.serif(0, AS, -1, "l"); g.serif(0, 0, 1, "b"); arch(g, 0, 330, X) + + +@rom("u") +def _u(g): + B, X, yb, h = g.B, g.X, g.yb, g.h + x0, x1 = 0, 330 + xm = (x0 + x1) / 2 - 12 + (g.P(x0, X - B / 2, 1).l(x0, X * 0.36, 1).c(x0, X * 0.08, xm - (xm - x0) * 0.6, yb, xm, yb, 0.55) + .c(xm + (x1 - xm) * 0.55, yb, x1 - h * 0.3 - 30, X * 0.12, x1 - h * 0.3, X * 0.4, 0).draw()) + g.serif(x0, X, -1, "l"); g.stem(x1, 0, X); g.serif(x1, X, -1, "l"); g.serif(x1, 0, 1, "r") + + +@rom("o", l=0.82, r=0.82) +def _o(g): + X, o, B, h = g.X, g.o, g.B, g.h + g.ring(220, X / 2, 220 - h, X / 2 + o - B / 2) + + +@rom("c", l=0.82, r=0.7) +def _c(g): + X, o, B, h, bd = g.X, g.o, g.B, g.h, g.bd + cx, rx, cy, ry, t0 = 220, 220 - h, X / 2, X / 2 + o - B / 2, 0.2 * PI + s = g.pt(cx, cy, rx, ry, t0) + (g.E(cx, cy, rx, ry, t0, 0.3).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.55).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.7).arc(cx, cy, rx, ry, 1.5 * PI, 1.84 * PI, 0).draw()) + g.ball(s[0] - bd * 0.2, s[1] - bd * 0.12) + + +@rom("e", l=0.82, r=0.75) +def _e(g): + X, o, B, h = g.X, g.o, g.B, g.h + cx, rx, cy, ry, yc = 222, 222 - h, X / 2, X / 2 + o - B / 2, X * 0.56 + t0 = math.asin(((yc - cy) / ry) ** (g.ex / 2)) + g.bar(cx - rx - h * 0.5, cx + rx + h * 0.5, yc) + (g.E(cx, cy, rx, ry, t0, 0.9).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.7).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.7).arc(cx, cy, rx, ry, 1.5 * PI, 1.86 * PI, 0).draw()) + + +@rom("a", r=0.6) +def _a(g): + X, B, h, yt, yb, bd = g.X, g.B, g.h, g.yt, g.yb, g.bd + xr, xq = 320, h + 4 + xb, yB = h + max(bd, g.A) * 0.42, X * 0.7 + (g.P(xb, yB, 0.25).c(xb + 6, X * 0.94, xr - 205, yt, xr - 115, yt, 0.55).c(xr - 28, yt, xr, X * 0.86, xr, X * 0.62, 1) + .l(xr, X * 0.13, 1).c(xr, X * 0.03, xr + 22, yb, xr + 56, yb, 0.25).c(xr + 78, yb, xr + 92, 16, xr + 96, 40, 0).draw()) + (g.P(xr - h * 0.3, X * 0.57, 0).c(xr - 110, X * 0.53, xq, X * 0.45, xq, X * 0.23, 1) + .c(xq, X * 0.05, xq + 48, yb, xq + 108, yb, 0.6).c(xq + 185, yb, xr - 40, X * 0.06, xr - h * 0.3, X * 0.22, 0).draw()) + g.ball(xb, yB) + + +@rom("b", r=0.82) +def _b(g): + AS = g.AS + g.stem(0, 0, AS); g.serif(0, AS, -1, "l"); bowl_right(g, 0) + + +@rom("p", r=0.82) +def _p(g): + X, D = g.X, g.D + g.stem(0, D, X); g.serif(0, X, -1, "l"); g.serif(0, D, 1, "b"); bowl_right(g, 0) + + +@rom("d", l=0.82) +def _d(g): + AS = g.AS + x1 = 410 + g.stem(x1, 0, AS); g.serif(x1, AS, -1, "l"); g.serif(x1, 0, 1, "r"); bowl_left(g, x1) + + +@rom("q", l=0.82) +def _q(g): + X, D = g.X, g.D + x1 = 410 + g.stem(x1, D, X); g.serif(x1, D, 1, "b"); bowl_left(g, x1) + + +@rom("i") +def _i(g): + X, AS = g.X, g.AS + g.stem(0, 0, X); g.serif(0, X, -1, "l"); g.serif(0, 0, 1, "b"); g.dot(-g.A * 0.04, X + (AS - X) * 0.66, dotD(g)) + + +@rom("j", l=0.5) +def _j(g): + X, AS, D, B, bd, o = g.X, g.AS, g.D, g.B, g.bd, g.o + x0, yb = 0, D - o + B / 2 + (g.P(x0, X - B / 2, 1).l(x0, D + 150, 1).c(x0, D + 50, x0 - 40, yb, x0 - 105, yb, 0.5) + .c(x0 - 150, yb, x0 - 182, D + 22, x0 - 190, D + bd * 0.55 + 8, 0.25).draw()) + g.ball(x0 - 190, D + bd * 0.55 + 8); g.serif(x0, X, -1, "l"); g.dot(-g.A * 0.04, X + (AS - X) * 0.66, dotD(g)) + + +@rom("k", r=0.6) +def _k(g): + X, AS, A, B, h = g.X, g.AS, g.A, g.B, g.h + x0 = 0 + ax = x0 + 262 + g.stem(x0, 0, AS); g.serif(x0, AS, -1, "l"); g.serif(x0, 0, 1, "b") + g.P(x0 + h - 4, X * 0.26, 0).l(ax, X - B / 2, 0).draw(); g.serif(ax, X, -1, "b", B * 1.6, g.sl * 0.8) + g.P(x0 + 128, X * 0.55, 0.85).l(x0 + 292, B / 2, 1).draw(); g.serif(x0 + 292, 0, 1, "b", A * 0.95) + + +@rom("l") +def _l(g): + AS = g.AS + g.stem(0, 0, AS); g.serif(0, AS, -1, "l"); g.serif(0, 0, 1, "b") + + +@rom("r", r=0.5) +def _r(g): + X, B, h, yt, bd, A = g.X, g.B, g.h, g.yt, g.bd, g.A + x0 = 0 + xe, ye = x0 + 228, X - max(bd, A * 0.6) * 0.45 - 8 + g.stem(x0, 0, X); g.serif(x0, X, -1, "l"); g.serif(x0, 0, 1, "b", A, g.sl * 1.15) + (g.P(x0 + h * 0.3, X * 0.6, 0).c(x0 + h * 0.3 + 28, X * 0.88, x0 + 98, yt, x0 + 158, yt, 0.5) + .c(x0 + 200, yt, xe, X * 0.96, xe, ye, 0.25).draw()) + g.ball(xe - bd * 0.05, ye) + + +@rom("s", l=0.8, r=0.8) +def _s(g): + X, B, yt, yb, h, S = g.X, g.B, g.yt, g.yb, g.h, g.S + W = 330 + (g.P(W - 14, X * 0.79, 0.12).c(W - 28, X * 0.95, W * 0.68, yt, W * 0.5, yt, 0.5).c(W * 0.24, yt, h + 2, X * 0.92, h + 2, X * 0.74, 1) + .c(h + 2, X * 0.5, W - h - 2, X * 0.52, W - h - 2, X * 0.27, 1).c(W - h - 2, X * 0.08, W * 0.76, yb, W * 0.5, yb, 0.5) + .c(W * 0.27, yb, 30, X * 0.06, 12, X * 0.21, 0.12).draw()) + g.P(W - 14, X * 0.66, 0).l(W - 14, X * 0.9, 0).draw({"a": S * 1.4, "b": S, "role": "serif"}) + g.P(12, X * 0.1, 0).l(12, X * 0.34, 0).draw({"a": S * 1.4, "b": S, "role": "serif"}) + + +@rom("t", l=0.7, r=0.6) +def _t(g): + X, B, yb, h = g.X, g.B, g.yb, g.h + x0 = 0 + (g.P(x0, X + 118, 0.45).l(x0, X * 0.8, 1).l(x0, X * 0.25, 1).c(x0, X * 0.04, x0 + 40, yb, x0 + 100, yb, 0.45) + .c(x0 + 150, yb, x0 + 186, X * 0.06, x0 + 196, X * 0.2, 0).draw()) + g.bar(x0 - h - 82, x0 + h + 108, X - B / 2) + + +@rom("v", l=0.5, r=0.5) +def _v(g): + X, A, B, o = g.X, g.A, g.B, g.o + xl, xm, xr = 0, 225, 445 + g.P(xl, X - B / 2, 1).l(xm - A * 0.2, B / 2 - o, 0.9).draw(); g.P(xm + A * 0.1, B / 2 - o, 0).l(xr, X - B / 2, 0).draw() + g.serif(xl, X, -1, "b", A); g.serif(xr, X, -1, "b", B * 1.6, g.sl * 0.85) + + +@rom("w", l=0.5, r=0.5) +def _w(g): + X, A, B, o = g.X, g.A, g.B, g.o + a, b, c, d, e = 0, 172, 318, 490, 640 + g.P(a, X - B / 2, 1).l(b - A * 0.2, B / 2 - o, 0.9).draw(); g.P(b + A * 0.1, B / 2 - o, 0).l(c, X - B / 2, 0).draw() + g.P(c, X - B / 2, 1).l(d - A * 0.2, B / 2 - o, 0.9).draw(); g.P(d + A * 0.1, B / 2 - o, 0).l(e, X - B / 2, 0).draw() + g.serif(a, X, -1, "b", A); g.serif(c, X, -1, "b", A * 1.1, g.sl * 0.7); g.serif(e, X, -1, "b", B * 1.6, g.sl * 0.85) + + +@rom("x", l=0.6, r=0.6) +def _x(g): + X, A, B = g.X, g.A, g.B + xl, xr = 0, 410 + g.P(xl + 8, X - B / 2, 1).l(xr - 8, B / 2, 1).draw(); g.P(xl + 12, B / 2, 0).l(xr - 12, X - B / 2, 0).draw() + g.serif(xl + 8, X, -1, "b", A); g.serif(xr - 8, 0, 1, "b", A) + g.serif(xl + 12, 0, 1, "b", B * 1.6, g.sl * 0.85); g.serif(xr - 12, X, -1, "b", B * 1.6, g.sl * 0.85) + + +@rom("y", l=0.5, r=0.5) +def _y(g): + X, A, B, D, bd, o = g.X, g.A, g.B, g.D, g.bd, g.o + xl, xm, xr = 0, 225, 445 + g.P(xl, X - B / 2, 1).l(xm - A * 0.15, B / 2 - o, 0.85).draw() + k = (xr - xm) / X + xb = xm - k * (-D * 0.55) + (g.P(xr, X - B / 2, 0).l(xb, D * 0.55, 0.1) + .c(xb - k * 60, D * 0.55 - 60, xm - 150, D + bd * 0.55 + 6, xm - 205, D + bd * 0.55 + 6, 0.35).draw()) + g.ball(xm - 205, D + bd * 0.55 + 6) + g.serif(xl, X, -1, "b", A); g.serif(xr, X, -1, "b", B * 1.6, g.sl * 0.85) + + +@rom("z", l=0.7, r=0.7) +def _z(g): + X, A, B, S = g.X, g.A, g.B, g.S + x0, x1 = 0, 370 + g.bar(x0, x1, X - B / 2); g.bar(x0, x1, B / 2) + g.P(x1 - A * 0.5, X - B, 1).l(x0 + A * 0.5, B, 1).draw() + g.P(x0 + S * 0.7, X, 0).l(x0 + S * 0.7, X - 115, 0).draw({"a": S * 1.4, "b": S, "role": "serif"}) + g.P(x1 - S * 0.7, 0, 0).l(x1 - S * 0.7, 125, 0).draw({"a": S * 1.4, "b": S, "role": "serif"}) + + +@rom("f", r=-0.7) +def _f(g): + X, AS, B, h, bd, A, o = g.X, g.AS, g.B, g.h, g.bd, g.A, g.o + x0 = 0 + yt2, xe, ye = AS + o - B / 2, x0 + 232, AS - max(bd, A * 0.6) * 0.5 - 12 + (g.P(x0, B / 2, 1).l(x0, AS - 175, 1).c(x0, AS - 45, x0 + 62, yt2, x0 + 150, yt2, 0.5) + .c(x0 + 205, yt2, xe, AS - 25, xe, ye, 0.25).draw()) + g.ball(xe, ye); g.bar(x0 - h - 78, x0 + h + 118, X - B / 2); g.serif(x0, 0, 1, "b", A, g.sl * 1.2) + + +# ===================================================================== +# ROMAN CAPITALS. Thick stems, hairline arms, beaks on arm ends. +# ===================================================================== + +def vbeak(g, x, y0, y1): + g.P(x, y0, 0).l(x, y1, 0).draw({"a": g.S * 1.4, "b": g.S, "role": "serif"}) + + +def dbowl(g, x0, y0, y1, xr, xf): + """Flat hairline top and bottom, round thick right side.""" + B = g.B + r, ym, a, b = (y1 - y0) / 2, (y0 + y1) / 2, y1 - B / 2, y0 + B / 2 + (g.P(x0, a, 0).l(xf, a, 0).c(xf + (xr - xf) * 0.62, a, xr, ym + r * 0.55, xr, ym, 1) + .c(xr, ym - r * 0.55, xf + (xr - xf) * 0.62, b, xf, b, 0).l(x0, b, 0).draw()) + + +@rom("H") +def _H(g): + C, B = g.C, g.B + x1 = 470 + g.stem(0, 0, C); g.stem(x1, 0, C); g.bar(0, x1, C * 0.5) + for x in (0, x1): + g.serif(x, C, -1, "b"); g.serif(x, 0, 1, "b") + + +@rom("I") +def _I(g): + C = g.C + g.stem(0, 0, C); g.serif(0, C, -1, "b"); g.serif(0, 0, 1, "b") + + +@rom("E", r=0.6) +def _E(g): + C, B, h = g.C, g.B, g.h + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "l") + g.bar(-h, 385, C - B / 2); vbeak(g, 385 - g.S * 0.7, C, C - 118) + g.bar(-h, 330, C * 0.51); vbeak(g, 330 - g.S * 0.7, C * 0.51 - 60, C * 0.51 + 60) + g.bar(-h, 405, B / 2); vbeak(g, 405 - g.S * 0.7, 0, 128) + + +@rom("F", r=0.5) +def _F(g): + C, B, h = g.C, g.B, g.h + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "b") + g.bar(-h, 385, C - B / 2); vbeak(g, 385 - g.S * 0.7, C, C - 118) + g.bar(-h, 325, C * 0.5); vbeak(g, 325 - g.S * 0.7, C * 0.5 - 60, C * 0.5 + 60) + + +@rom("L", r=0.6) +def _L(g): + C, B, h = g.C, g.B, g.h + g.stem(0, 0, C); g.serif(0, C, -1, "b"); g.serif(0, 0, 1, "l") + g.bar(-h, 395, B / 2); vbeak(g, 395 - g.S * 0.7, 0, 135) + + +@rom("T", l=0.5, r=0.5) +def _T(g): + C, B = g.C, g.B + W = 540 + g.bar(0, W, C - B / 2); vbeak(g, g.S * 0.7, C, C - 130); vbeak(g, W - g.S * 0.7, C, C - 130) + g.stem(W / 2, 0, C); g.serif(W / 2, 0, 1, "b") + + +@rom("D", r=0.85) +def _D(g): + C = g.C + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "l"); dbowl(g, 0, 0, C, 500, 200) + + +@rom("B", r=0.8) +def _B(g): + C = g.C + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "l") + dbowl(g, 0, C * 0.52 - g.B, C, 385, 170); dbowl(g, 0, 0, C * 0.52, 440, 190) + + +@rom("P", r=0.7) +def _P(g): + C = g.C + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "b"); dbowl(g, 0, C * 0.44, C, 420, 190) + + +def round_c(g, cx, rx, t0, t1): + C, o, B = g.C, g.o, g.B + cy, ry = C / 2, C / 2 + o - B / 2 + (g.E(cx, cy, rx, ry, t0, 0.2).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.6).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.6).arc(cx, cy, rx, ry, 1.5 * PI, t1, 0.15).draw()) + return g.pt(cx, cy, rx, ry, t0), g.pt(cx, cy, rx, ry, t1) + + +@rom("C", l=0.82, r=0.6) +def _C(g): + C, h = g.C, g.h + cx = 300 + s, e = round_c(g, cx, cx - h, 0.2 * PI, 1.8 * PI) + vbeak(g, s[0] - 2, s[1] - 110, s[1] + 30); vbeak(g, e[0] - 2, e[1] - 20, e[1] + 100) + + +@rom("O", l=0.82, r=0.82) +def _O(g): + C, o, B, h = g.C, g.o, g.B, g.h + g.ring(330, C / 2, 330 - h, C / 2 + o - B / 2) + + +@rom("S", l=0.8, r=0.8) +def _S(g): + C, B, o, h, S = g.C, g.B, g.o, g.h, g.S + W = 420 + yt, yb = C + o - B / 2, -o + B / 2 + (g.P(W - 16, C * 0.78, 0.15).c(W - 30, C * 0.95, W * 0.68, yt, W * 0.5, yt, 0.5).c(W * 0.22, yt, h + 2, C * 0.92, h + 2, C * 0.75, 1) + .c(h + 2, C * 0.5, W - h - 2, C * 0.52, W - h - 2, C * 0.26, 1).c(W - h - 2, C * 0.08, W * 0.76, yb, W * 0.5, yb, 0.5) + .c(W * 0.24, yb, 26, C * 0.06, 12, C * 0.2, 0.15).draw()) + vbeak(g, W - 16, C * 0.68, C * 0.92); vbeak(g, 12, C * 0.08, C * 0.33) + + +@rom("U") +def _U(g): + C, B, o, h, A = g.C, g.B, g.o, g.h, g.A + x0, x1 = 0, 480 + yb, xm = -o + B / 2, (x0 + x1) / 2 + (g.P(x0, C - B / 2, 1).l(x0, C * 0.32, 1).c(x0, C * 0.08, xm - 150, yb, xm, yb, 0.5) + .c(xm + 150, yb, x1, C * 0.08, x1, C * 0.32, 0).l(x1, C - B / 2, 0).draw()) + g.serif(x0, C, -1, "b"); g.serif(x1, C, -1, "b", B * 1.6, g.sl * 0.9) + + +@rom("V", l=0.45, r=0.45) +def _V(g): + C, A, B, o = g.C, g.A, g.B, g.o + xl, xm, xr = 0, 275, 550 + g.P(xl, C - B / 2, 1).l(xm - A * 0.22, B / 2 - o, 0.9).draw(); g.P(xm + A * 0.1, B / 2 - o, 0).l(xr, C - B / 2, 0).draw() + g.serif(xl, C, -1, "b"); g.serif(xr, C, -1, "b", B * 1.6) + + +@rom("W", l=0.45, r=0.45) +def _W(g): + C, A, B, o = g.C, g.A, g.B, g.o + a, b, c, d, e = 0, 200, 395, 600, 800 + g.P(a, C - B / 2, 1).l(b - A * 0.2, B / 2 - o, 0.9).draw(); g.P(b + A * 0.1, B / 2 - o, 0).l(c, C - B / 2, 0).draw() + g.P(c, C - B / 2, 1).l(d - A * 0.2, B / 2 - o, 0.9).draw(); g.P(d + A * 0.1, B / 2 - o, 0).l(e, C - B / 2, 0).draw() + g.serif(a, C, -1, "b"); g.serif(c, C, -1, "b", A * 1.1, g.sl * 0.7); g.serif(e, C, -1, "b", B * 1.6) + + +@rom("X", l=0.55, r=0.55) +def _X(g): + C, A, B = g.C, g.A, g.B + xl, xr = 0, 500 + g.P(xl + 8, C - B / 2, 1).l(xr - 8, B / 2, 1).draw(); g.P(xl + 14, B / 2, 0).l(xr - 14, C - B / 2, 0).draw() + g.serif(xl + 8, C, -1, "b"); g.serif(xr - 8, 0, 1, "b"); g.serif(xl + 14, 0, 1, "b", B * 1.6); g.serif(xr - 14, C, -1, "b", B * 1.6) + + +@rom("Y", l=0.45, r=0.45) +def _Y(g): + C, A, B = g.C, g.A, g.B + xl, xm, xr, yj = 0, 260, 520, C * 0.44 + g.P(xl, C - B / 2, 1).l(xm, yj, 1).draw(); g.P(xr, C - B / 2, 0).l(xm + A * 0.2, yj, 0).draw(); g.stem(xm, 0, yj + B) + g.serif(xl, C, -1, "b"); g.serif(xr, C, -1, "b", B * 1.6); g.serif(xm, 0, 1, "b") + + +@rom("Z", l=0.7, r=0.7) +def _Z(g): + C, A, B, S = g.C, g.A, g.B, g.S + x0, x1 = 0, 470 + g.bar(x0, x1, C - B / 2); g.bar(x0, x1, B / 2); g.P(x1 - A * 0.5, C - B, 1).l(x0 + A * 0.5, B, 1).draw() + vbeak(g, x0 + S * 0.7, C, C - 140); vbeak(g, x1 - S * 0.7, 0, 150) + + +@rom("K", r=0.5) +def _K(g): + C, A, B, h = g.C, g.A, g.B, g.h + x0 = 0 + g.stem(x0, 0, C); g.serif(x0, C, -1, "b"); g.serif(x0, 0, 1, "b") + g.P(x0 + h - 4, C * 0.3, 0).l(x0 + 410, C - B / 2, 0).draw(); g.serif(x0 + 410, C, -1, "b", B * 1.6, g.sl * 0.9) + g.P(x0 + 180, C * 0.53, 0.85).l(x0 + 452, B / 2, 1).draw(); g.serif(x0 + 452, 0, 1, "b", A * 0.95) + + +@rom("M") +def _M(g): + C, A, B = g.C, g.A, g.B + x0, xm, x1 = 0, 330, 660 + g.stem(x0, 0, C, 0); g.P(x0 + A * 0.05, C - B / 2, 1).l(xm - A * 0.2, B / 2 - g.o, 0.9).draw() + g.P(xm + A * 0.1, B / 2 - g.o, 0).l(x1 - A * 0.3, C - B / 2, 0).draw(); g.stem(x1, 0, C) + g.serif(x0, C, -1, "l", B * 1.6); g.serif(x0, 0, 1, "b", B * 1.6); g.serif(x1, C, -1, "r"); g.serif(x1, 0, 1, "b") + + +@rom("N") +def _N(g): + C, A, B, o = g.C, g.A, g.B, g.o + x0, x1 = 0, 500 + g.stem(x0, 0, C, 0); g.P(x0 + A * 0.1, C - B / 2, 1).l(x1 - A * 0.15, B / 2 - o * 2, 1).draw() + g.P(x1, C - B / 2, 0).l(x1, -o * 2, 0).draw() + g.serif(x0, C, -1, "l", B * 1.6); g.serif(x0, 0, 1, "b", B * 1.6); g.serif(x1, C, -1, "b", B * 1.6) + + +@rom("A", l=0.45, r=0.45) +def _A(g): + C, A, B, o = g.C, g.A, g.B, g.o + xl, xm, xr = 0, 280, 560 + g.P(xl, B / 2, 0).l(xm - A * 0.15, C + o - B / 2, 0).draw(); g.P(xm - A * 0.25, C + o - B / 2, 0.9).l(xr, B / 2, 1).draw() + # font: the sketch's bar starts at xl + 95 and stops a few units short + # of the hairline (the counter is open). It starts at xl + 72 here. + g.bar(xl + 72, xr - 85, C * 0.3); g.serif(xl, 0, 1, "b", B * 1.6); g.serif(xr, 0, 1, "b") + + +# ---------- 0.2: refined forms ---------- + +@rom("g", r=0.7) +def _g(g): + X, B, D, o, bd, h = g.X, g.B, g.D, g.o, g.bd, g.h + cx, rx, cy, ry, yb2 = 198, 138, X * 0.655, X * 0.345 - B / 2 + o * 0.5, D - o + B / 2 + g.ring(cx, cy, rx, ry) + e = g.pt(cx, cy, rx, ry, 0.3 * PI) + g.P(e[0], e[1], 0).c(e[0] + 26, X + 2, cx + rx + 18, X + 16, cx + rx + 66, X + 12, 0.12).draw() + g.ball(cx + rx + 70, X + 2, bd * 0.62) + s = g.pt(cx, cy, rx, ry, 1.22 * PI) + (g.P(s[0], s[1], 0.3).c(s[0] - 34, s[1] - 50, 46, 46, 140, 34, 0).l(300, 32, 0) + .c(398, 32, 436, -12, 436, -80, 1).c(436, -178, 342, yb2, 228, yb2, 0.5) + .c(108, yb2, 22, -188, 22, -130, 1).c(22, -82, 62, -54, 118, -46, 0).draw()) + + +@rom("G", l=0.82, r=0.6) +def _G(g): + C, A, B, h = g.C, g.A, g.B, g.h + cx = 300 + rx = cx - h + s, _ = round_c(g, cx, rx, 0.2 * PI, 1.88 * PI) + vbeak(g, s[0] - 2, s[1] - 100, s[1] + 24) + xr, yS = cx + rx * 0.99, C * 0.44 + g.P(xr, yS - B / 2, 1).l(xr, C * 0.12, 1).l(xr, -4, 0.35).draw() + g.serif(xr, yS, -1, "b", A, g.sl * 0.7) + + +@rom("R", r=0.5) +def _R(g): + C, A, B, o = g.C, g.A, g.B, g.o + yb = -o + B / 2 + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "b"); dbowl(g, 0, C * 0.47, C, 405, 180) + (g.P(170, C * 0.47 + B / 2, 0.1).c(298, C * 0.45, 352, C * 0.36, 360, C * 0.2, 1).l(363, C * 0.1, 1) + .c(365, C * 0.02, 392, yb, 428, yb, 0.35).c(458, yb, 476, 16, 482, 44, 0).draw()) + + +@rom("Q", l=0.82, r=0.45) +def _Q(g): + C, o, B, h = g.C, g.o, g.B, g.h + cx = 325 + g.ring(cx, C / 2, cx - h, C / 2 + o - B / 2) + (g.P(cx - 40, -o + B / 2 + 8, 0.25).c(cx + 10, -62, cx + 110, -122, cx + 235, -120, 0.8) + .c(cx + 305, -119, cx + 350, -102, cx + 390, -82, 0).draw()) + + +@rom("J", l=0.6) +def _J(g): + C, B, bd, D = g.C, g.B, g.bd, g.D + x0, yb = 230, D * 0.62 + (g.P(x0, C - B / 2, 1).l(x0, C * 0.04, 1).c(x0, yb * 0.55, x0 - 40, yb, x0 - 108, yb, 0.5) + .c(x0 - 162, yb, x0 - 196, yb * 0.76, x0 - 204, yb * 0.5, 0.25).draw()) + g.ball(x0 - 204, yb * 0.5); g.serif(x0, C, -1, "b") + + +@rom("&", l=0.7, r=0.55) +def _amp(g): + C, B, o, h = g.C, g.B, g.o, g.h + yt, yb = C + o - B / 2, -o + B / 2 + (g.P(615, yb, 0).c(540, yb, 470, 30, 410, C * 0.18, 0.9).c(330, C * 0.36, 215, C * 0.5, 215, C * 0.73, 1) + .c(215, C * 0.9, 255, yt, 305, yt, 0.5).c(360, yt, 392, C * 0.92, 392, C * 0.82, 1).c(392, C * 0.67, 300, C * 0.6, 215, C * 0.52, 0.3) + .c(110, C * 0.44, h + 4, C * 0.34, h + 4, C * 0.22, 1).c(h + 4, C * 0.06, 120, yb, 232, yb, 0.5) + .c(362, yb, 458, C * 0.18, 508, C * 0.41, 0.2).draw()) + g.bar(470, 620, C * 0.42) + + +# ---------- ligatures ---------- + +def f_top(g, x0, xe): + AS, B, o, bd, A = g.AS, g.B, g.o, g.bd, g.A + yt2 = AS + o - B / 2 + ye = AS - max(bd, A * 0.6) * 0.5 - 14 + return (g.P(x0, B / 2, 1).l(x0, AS - 178, 1).c(x0, AS - 45, x0 + 66, yt2, x0 + (xe - x0) * 0.62, yt2, 0.5) + .c(x0 + (xe - x0) * 0.88, yt2, xe, AS - 28, xe, ye, 0.25)) + + +@rom("fi") +def _fi(g): + X, AS, B, h, bd, A = g.X, g.AS, g.B, g.h, g.bd, g.A + x0, xi = 0, 292 + ye = AS - max(bd, A * 0.6) * 0.5 - 14 + f_top(g, x0, xi).draw(); g.ball(xi, ye); g.stem(xi, 0, X); g.bar(x0 - h - 78, xi, X - B / 2) + g.serif(x0, 0, 1, "b", A, g.sl * 1.2); g.serif(xi, 0, 1, "b") + + +@rom("fl") +def _fl(g): + X, AS, B, o, h, A = g.X, g.AS, g.B, g.o, g.h, g.A + x0, xl = 0, 292 + yt2 = AS + o - B / 2 + (g.P(x0, B / 2, 1).l(x0, AS - 178, 1).c(x0, AS - 45, x0 + 66, yt2, x0 + 160, yt2, 0.5) + .c(x0 + 240, yt2, xl, AS - 20, xl, AS - 110, 1).l(xl, B / 2, 1).draw()) + g.bar(x0 - h - 78, xl, X - B / 2); g.serif(x0, 0, 1, "b", A, g.sl * 1.2); g.serif(xl, 0, 1, "b") + + +@rom("ff", r=-0.7) +def _ff(g): + X, AS, B, o, h, bd, A = g.X, g.AS, g.B, g.o, g.h, g.bd, g.A + x0, x2 = 0, 262 + yt2 = AS + o - B / 2 + ye = AS - max(bd, A * 0.6) * 0.5 - 14 + (g.P(x0, B / 2, 1).l(x0, AS - 178, 1).c(x0, AS - 45, x0 + 60, yt2, x0 + 140, yt2, 0.5) + .c(x0 + 200, yt2, x2 - h - 6, AS - 30, x2 - h * 0.4, AS - 82, 0.05).draw()) + f_top(g, x2, x2 + 232).draw(); g.ball(x2 + 232, ye); g.bar(x0 - h - 78, x2 + h + 118, X - B / 2) + g.serif(x0, 0, 1, "b", A, g.sl * 1.2); g.serif(x2, 0, 1, "b", A, g.sl * 1.2) + + +# ---------- figures: lining, tabular ---------- + +FIG = {"adv": 520} + + +@rom("0", **FIG) +def _0(g): + C, o, B, h = g.C, g.o, g.B, g.h + g.ring(250, C / 2, 200 - h, C / 2 + o - B / 2, 1, 2.05) + + +@rom("1", **FIG) +def _1(g): + C, B, A = g.C, g.B, g.A + x = 260 + g.stem(x, 0, C); g.P(x - g.h + 6, C - B * 0.7, 0.35).l(x - 125, C * 0.8, 0).draw(); g.serif(x, 0, 1, "b", A, g.sl * 1.35) + + +@rom("2", **FIG) +def _2(g): + C, B, o, bd, S = g.C, g.B, g.o, g.bd, g.S + yt = C + o - B / 2 + (g.P(95, C * 0.71, 0.25).c(98, C * 0.93, 170, yt, 255, yt, 0.5).c(360, yt, 420, C * 0.86, 420, C * 0.68, 1) + .c(420, C * 0.46, 235, C * 0.3, 72, B, 0.85).draw()) + g.ball(95, C * 0.71); g.bar(52, 448, B / 2); vbeak(g, 448 - S * 0.7, 0, 125) + + +@rom("3", **FIG) +def _3(g): + C, B, o, bd = g.C, g.B, g.o, g.bd + yt, yb = C + o - B / 2, -o + B / 2 + (g.P(92, C * 0.75, 0.25).c(98, C * 0.95, 170, yt, 248, yt, 0.5).c(350, yt, 402, C * 0.88, 402, C * 0.76, 1) + .c(402, C * 0.62, 322, C * 0.545, 205, C * 0.545, 0).draw()) + (g.P(205, C * 0.545, 0).c(352, C * 0.545, 432, C * 0.45, 432, C * 0.28, 1).c(432, C * 0.08, 342, yb, 240, yb, 0.5) + .c(150, yb, 82, C * 0.06, 78, C * 0.21, 0.25).draw()) + g.ball(92, C * 0.75); g.ball(80, C * 0.2) + + +@rom("4", **FIG) +def _4(g): + C, B, A = g.C, g.B, g.A + x = 345 + g.P(x - A * 0.2, C - B / 2, 0).l(38, C * 0.28, 0).draw(); g.stem(x, 0, C) + g.bar(30, 462, C * 0.27); g.serif(x, 0, 1, "b", A, g.sl * 1.1) + + +@rom("5", **FIG) +def _5(g): + C, B, o = g.C, g.B, g.o + yb = -o + B / 2 + g.bar(118, 415, C - B / 2); g.P(124, C - B / 2, 0.45).l(124, C * 0.53, 0.45).draw() + (g.P(124, C * 0.54, 0).c(170, C * 0.62, 215, C * 0.635, 260, C * 0.635, 0.4).c(380, C * 0.635, 438, C * 0.5, 438, C * 0.32, 1) + .c(438, C * 0.1, 345, yb, 245, yb, 0.5).c(150, yb, 82, C * 0.06, 78, C * 0.21, 0.25).draw()) + g.ball(80, C * 0.2) + + +def six(g, M): + C, B, o = g.C, g.B, g.o + yt, ry = C + o - B / 2, C * 0.32 + o - B / 2 - 6 + + def T(x, y): + return (500 - x, C - y) if M else (x, y) + + q0 = T(405, C * 0.82) + p = g.P(q0[0], q0[1], 0.25) + + def c(a, b, c2, d, e, f, pr): + q1, q2, q3 = T(a, b), T(c2, d), T(e, f) + p.c(q1[0], q1[1], q2[0], q2[1], q3[0], q3[1], pr) + + c(398, C * 0.96, 330, yt, 262, yt, 0.5) + c(140, yt, 75, C * 0.75, 75, C * 0.36, 1) + p.draw() + q = T(250, C * 0.32 - 6) + g.ring(q[0], q[1], 175, ry) + b = T(405, C * 0.82) + g.ball(b[0], b[1]) + + +@rom("6", **FIG) +def _6(g): + six(g, False) + + +@rom("9", **FIG) +def _9(g): + six(g, True) + + +@rom("7", **FIG) +def _7(g): + C, B, A = g.C, g.B, g.A + g.bar(48, 452, C - B / 2); vbeak(g, 48 + g.S * 0.7, C, C - 130) + g.P(452 - A * 0.5, C - B, 0.9).c(390, C * 0.6, 240, C * 0.38, 222, B / 2, 1).draw() + + +@rom("8", **FIG) +def _8(g): + C, o, B = g.C, g.o, g.B + yj = C * 0.535 + g.ring(250, (yj + C + o - B / 2) / 2, 148, (C + o - B / 2 - yj) / 2) + g.ring(250, (yj - o + B / 2) / 2, 176, (yj + o - B / 2) / 2) + + +# ---------- punctuation and delimiters ---------- + +def paren(g, x0, w, top, bot, dir): + m, H = (top + bot) / 2, top - bot + xr, B = x0 + w * dir, g.B + (g.P(xr, top - B / 2, 0).c(xr - dir * w * 0.82, top - H * 0.2, x0, m + H * 0.26, x0, m, 1) + .c(x0, m - H * 0.26, xr - dir * w * 0.82, bot + H * 0.2, xr, bot + B / 2, 0).draw()) + + +def brack(g, x, top, bot, dir): + B = g.B + g.stem(x, bot, top, 1); g.bar(x - dir * g.h, x + dir * 130, top - B / 2); g.bar(x - dir * g.h, x + dir * 130, bot + B / 2) + + +@rom(" ", adv=270, empty=True) +def _space(g): + pass + + +@rom(".", l=0.9, r=0.9) +def _period(g): + g.dot(0, dotD(g) / 2, dotD(g)) + + +def comma_at(g, y): + d = dotD(g) + g.ball(0, y + d / 2, d) + g.P(d * 0.22, y + d * 0.32, 0.85).c(d * 0.3, y - d * 0.3, 0, y - d * 0.9, -d * 0.45, y - d * 1.25, 0).draw() + + +@rom(",", l=0.9, r=0.9) +def _comma(g): + d = dotD(g) + g.ball(0, d / 2, d); g.P(d * 0.22, d * 0.32, 0.85).c(d * 0.3, -d * 0.3, 0, -d * 0.9, -d * 0.45, -d * 1.25, 0).draw() + + +@rom(":", l=0.9, r=0.9) +def _colon(g): + d = dotD(g) + g.dot(0, d / 2, d); g.dot(0, g.X - d / 2, d) + + +@rom(";", l=0.9, r=0.9) +def _semicolon(g): + d = dotD(g) + g.dot(0, g.X - d / 2, d); g.ball(0, d / 2, d) + g.P(d * 0.22, d * 0.32, 0.85).c(d * 0.3, -d * 0.3, 0, -d * 0.9, -d * 0.45, -d * 1.25, 0).draw() + + +@rom("!", l=0.9, r=0.9) +def _exclam(g): + d, C, B = dotD(g), g.C, g.B + g.P(0, C - B / 2, 1).l(0, C * 0.6, 0.85).l(0, d + 70, 0.15).draw(); g.dot(0, d / 2, d) + + +@rom("?", l=0.8, r=0.8) +def _question(g): + d, C, B, o = dotD(g), g.C, g.B, g.o + yt = C + o - B / 2 + (g.P(48, C * 0.72, 0.25).c(52, C * 0.94, 120, yt, 190, yt, 0.5).c(280, yt, 325, C * 0.86, 325, C * 0.73, 1) + .c(325, C * 0.55, 190, C * 0.5, 190, C * 0.33, 0.6).l(190, d + 80, 0.2).draw()) + g.ball(48, C * 0.72); g.dot(190, d / 2, d) + + +@rom("'", l=0.9, r=0.9) +def _quotesingle(g): + C = g.C + g.P(0, C - g.B / 2, 1).l(0, C * 0.66, 0.15).draw() + + +@rom("’", l=0.9, r=0.9) +def _quoteright(g): + d, C = dotD(g), g.C + g.ball(0, C - d / 2, d) + g.P(d * 0.22, C - d * 0.68, 0.85).c(d * 0.3, C - d * 1.3, 0, C - d * 1.9, -d * 0.45, C - d * 2.25, 0).draw() + + +@rom("-", l=0.8, r=0.8) +def _hyphen(g): + X = g.X + g.P(0, X * 0.56, 0).l(170, X * 0.56, 0).draw({"b": g.A * 0.5, "a": g.A * 0.5, "ne": 5}) + + +@rom("–", l=0.4, r=0.4) +def _endash(g): + X = g.X + g.P(0, X * 0.56, 0).l(500, X * 0.56, 0).draw({"b": g.B * 1.5, "a": g.B * 1.5, "ne": 5}) + + +@rom("—", l=0.2, r=0.2) +def _emdash(g): + X = g.X + g.P(0, X * 0.56, 0).l(940, X * 0.56, 0).draw({"b": g.B * 1.5, "a": g.B * 1.5, "ne": 5}) + + +@rom("/", l=0.5, r=0.5) +def _slash(g): + C = g.C + g.P(330, C + 60, 0.55).l(0, -120, 0.55).draw() + + +@rom("(", l=0.9, r=0.6) +def _parenleft(g): + paren(g, 0, 150, 770, -250, 1) + + +@rom(")", l=0.6, r=0.9) +def _parenright(g): + paren(g, 150, 150, 770, -250, -1) + + +@rom("[", l=0.9, r=0.6) +def _bracketleft(g): + brack(g, 0, 770, -250, 1) + + +@rom("]", l=0.6, r=0.9) +def _bracketright(g): + brack(g, 130, 770, -250, -1) + + +# ===================================================================== +# GREEK. One continuous pen line where the hand would make one. +# ===================================================================== + +@rom("α", l=0.8, r=0.5) +def _alpha(g): + X, yt, yb, h = g.X, g.yt, g.yb, g.h + (g.P(448, X + 8, 0.1).c(425, X * 0.58, 335, yb, 215, yb, 0.6).c(92, yb, h + 2, X * 0.24, h + 2, X * 0.5, 1) + .c(h + 2, X * 0.78, 100, yt, 212, yt, 0.6).c(318, yt, 392, X * 0.42, 478, 4, 0.05).draw()) + + +@rom("β", r=0.8) +def _beta(g): + X, AS, D, B, o, h = g.X, g.AS, g.D, g.B, g.o, g.h + x0 = 0 + yt2, yb = AS + o - B / 2, -o + B / 2 + (g.P(x0, D, 0.7).l(x0, D + 50, 1).l(x0, AS * 0.66, 1).c(x0, AS - 12, 95, yt2, 190, yt2, 0.5).c(290, yt2, 345, AS * 0.9, 345, AS * 0.79, 1) + .c(345, X * 1.18, 268, X + 16, 165, X + 16, 0).c(335, X + 16, 412, X * 0.78, 412, X * 0.42, 1) + .c(412, X * 0.1, 322, yb, 215, yb, 0.5).c(118, yb, x0 + 30, X * 0.08, x0 + h * 0.3, X * 0.22, 0).draw()) + + +@rom("γ", l=0.6, r=0.5) +def _gamma(g): + X, D, B = g.X, g.D, g.B + (g.P(0, X * 0.72, 0.15).c(10, X + 4, 92, X + 6, 128, X * 0.76, 0.9).l(232, -30, 1).c(262, -95, 262, D + 70, 226, D + 70, 0.45) + .c(190, D + 70, 190, -95, 226, -30, 0.15).draw()) + g.P(450, X - B / 2, 0).l(232, -30, 0).draw() + + +@rom("δ", l=0.82, r=0.8) +def _delta(g): + X, AS, B, o, h, bd = g.X, g.AS, g.B, g.o, g.h, g.bd + cx, cy, rx, ry, yt2 = 225, X * 0.46, 225 - h, X * 0.46 + o - B / 2, AS + o - B / 2 + g.ring(cx, cy, rx, ry) + e = g.pt(cx, cy, rx, ry, 0.36 * PI) + (g.P(372, AS - bd * 0.45 - 10, 0.2).c(366, AS + 2, 300, yt2, 232, yt2, 0.45).c(150, yt2, 108, AS * 0.94, 108, AS * 0.86, 0.8) + .c(108, AS * 0.72, e[0] - 40, X * 1.04, e[0], e[1], 1).draw()) + g.ball(372, AS - bd * 0.45 - 10, bd * 0.9) + + +@rom("ε", l=0.82, r=0.7) +def _epsilon(g): + X, yt, yb, h, bd = g.X, g.yt, g.yb, g.h, g.bd + (g.P(352, X * 0.79, 0.2).c(345, X * 0.97, 282, yt, 218, yt, 0.4).c(122, yt, h + 28, X * 0.9, h + 28, X * 0.76, 1) + .c(h + 28, X * 0.6, 128, X * 0.53, 232, X * 0.53, 0).draw()) + (g.P(232, X * 0.53, 0).c(108, X * 0.53, h, X * 0.42, h, X * 0.25, 1).c(h, X * 0.06, 108, yb, 218, yb, 0.5) + .c(318, yb, 366, X * 0.06, 385, X * 0.17, 0).draw()) + g.ball(352, X * 0.79, bd * 0.85) + + +@rom("θ", l=0.82, r=0.82) +def _theta(g): + AS, o, B, h = g.AS, g.o, g.B, g.h + cy = AS / 2 - o + g.ring(215, cy, 215 - h, AS / 2 + o - B / 2, 1, 2.0); g.bar(h * 0.6, 430 - h * 0.6, cy) + + +@rom("λ", l=0.6, r=0.55) +def _lambda(g): + X, AS, B = g.X, g.AS, g.B + yb = -g.o + B / 2 + (g.P(12, AS - 38, 0.1).c(38, AS + 4, 95, AS, 122, AS - 72, 0.9).l(390, 62, 1) + .c(400, 22, 422, yb, 452, yb, 0.3).draw()) + g.P(290, X * 0.62, 0).l(12, B / 2, 0.25).draw() + + +@rom("μ", r=0.55) +def _mu(g): + X, D, B, yb, h = g.X, g.D, g.B, g.yb, g.h + x0, x1 = 0, 330 + g.P(x0, X - B / 2, 0.5).l(x0, X - 60, 1).l(x0, D + 40, 1).l(x0, D + B / 2, 0.7).draw() + (g.P(x0 + h * 0.3, X * 0.34, 0).c(x0 + 28, X * 0.06, 100, yb, 165, yb, 0.55) + .c(250, yb, x1 - 30, X * 0.12, x1 - h * 0.3, X * 0.38, 0).draw()) + (g.P(x1, X - B / 2, 0.5).l(x1, X - 60, 1).l(x1, 70, 1).c(x1, 22, x1 + 22, yb, x1 + 60, yb, 0.3) + .c(x1 + 80, yb, x1 + 93, 18, x1 + 96, 42, 0).draw()) + + +@rom("π", l=0.5, r=0.5) +def _pi(g): + X, A, B, o = g.X, g.A, g.B, g.o + yb, bb = -o + B / 2, A * 0.36 + yb2 = X - bb / 2 + g.P(-12, X * 0.76, 0.4).c(0, X * 0.92, 40, yb2, 110, yb2, 0.6).l(540, yb2, 0.3).draw({"b": bb}) + g.P(165, X - bb, 0.8).c(165, X * 0.45, 138, X * 0.18, 72, 26, 0.35).draw() + g.P(398, X - bb, 1).l(398, 78, 1).c(398, 25, 418, yb, 452, yb, 0.3).c(478, yb, 492, 20, 495, 46, 0).draw() + + +@rom("ρ", l=0.8, r=0.82) +def _rho(g): + X, D, o, B, h = g.X, g.D, g.o, g.B, g.h + cx, rx, cy, ry = 225, 225 - h, X / 2, X / 2 + o - B / 2 + g.ring(cx, cy, rx, ry); g.P(h, X * 0.48, 1).l(h, D + 40, 1).l(h, D + B / 2, 0.7).draw() + + +@rom("σ", l=0.82, r=0.5) +def _sigma(g): + X, o, B, h = g.X, g.o, g.B, g.h + cx, cy, rx, ry, bb = 212, X * 0.47, 212 - h, X * 0.47 + o - B / 2, B * 2.1 + g.ring(cx, cy, rx, ry) + g.P(cx, cy + ry + B / 2 - bb / 2, 0).l(470, cy + ry + B / 2 - bb / 2, 0).draw({"b": bb, "a": bb, "ne": 3}) + + +@rom("φ", l=0.82, r=0.82) +def _phi(g): + X, AS, D, o, B = g.X, g.AS, g.D, g.o, g.B + cx = 245 + g.ring(cx, X / 2, 245 - g.h, X / 2 + o - B / 2, 1, 2.1) + g.P(cx, D, 0.7).l(cx, D + 45, 1).l(cx, AS - 80, 1).l(cx, AS - 30, 0.7).draw() + + +@rom("ψ", l=0.6, r=0.6) +def _psi(g): + X, AS, D, B = g.X, g.AS, g.D, g.B + cx = 250 + g.P(cx, D, 0.7).l(cx, D + 45, 1).l(cx, AS - 80, 1).l(cx, AS - 30, 0.7).draw() + (g.P(18, X, 0.3).l(18, X - 50, 1).l(18, X * 0.55, 1).c(18, X * 0.12, 110, 40, cx, 40, 0.4) + .c(390, 40, 482, X * 0.12, 482, X * 0.55, 1).l(482, X - 50, 1).l(482, X, 0.3).draw()) + + +@rom("ω", l=0.75, r=0.75) +def _omega(g): + X, yb, h = g.X, g.yb, g.h + W = 600 + (g.P(118, X + 4, 0.15).c(48, X * 0.82, h, X * 0.6, h, X * 0.36, 1).c(h, X * 0.08, 92, yb, 165, yb, 0.5) + .c(238, yb, 282, X * 0.15, 300, X * 0.52, 0.15) + .c(318, X * 0.15, 362, yb, 435, yb, 0.5).c(508, yb, W - h, X * 0.08, W - h, X * 0.36, 1) + .c(W - h, X * 0.6, W - 48, X * 0.82, W - 118, X + 4, 0.15).draw()) + + +@rom("Γ", r=0.5) +def _Gamma(g): + C, B, h = g.C, g.B, g.h + g.stem(0, 0, C); g.serif(0, C, -1, "l"); g.serif(0, 0, 1, "b"); g.bar(-h, 395, C - B / 2); vbeak(g, 395 - g.S * 0.7, C, C - 128) + + +@rom("Δ", l=0.45, r=0.45) +def _Delta(g): + C, A, B, o = g.C, g.A, g.B, g.o + xl, xm, xr = 0, 290, 580 + g.P(xl + 20, B, 0).l(xm - A * 0.15, C + o - B / 2, 0).draw(); g.P(xm - A * 0.25, C + o - B / 2, 0.9).l(xr - A * 0.35, B, 1).draw() + g.bar(xl, xr, B / 2) + + +@rom("Σ", l=0.6, r=0.6) +def _Sigma(g): + C, A, B, S = g.C, g.A, g.B, g.S + W = 480 + g.bar(0, W, C - B / 2); vbeak(g, W - S * 0.7, C, C - 130); g.P(30, C - B, 1).l(255, C * 0.5, 1).draw() + g.P(255, C * 0.5, 0).l(25, B, 0).draw() + g.bar(0, W + 20, B / 2); vbeak(g, W + 20 - S * 0.7, 0, 140) + + +@rom("Ω", l=0.6, r=0.6) +def _Omega(g): + C, B, A, o = g.C, g.B, g.A, g.o + cx, cy, rx, ry, t0, t1 = 330, C * 0.56, 330 - g.h - 30, C * 0.44 + o - B / 2, 1.37 * PI, -0.37 * PI + s, e = g.pt(cx, cy, rx, ry, t0), g.pt(cx, cy, rx, ry, t1) + (g.P(s[0] + 40, B, 0.5).c(s[0] + 15, s[1] * 0.5, s[0], s[1], s[0], s[1], 0.7).arc(cx, cy, rx, ry, t0, PI, 1) + .arc(cx, cy, rx, ry, PI, 0.5 * PI, 0.6).arc(cx, cy, rx, ry, 0.5 * PI, 0, 1).arc(cx, cy, rx, ry, 0, t1, 0.7) + .c(e[0], e[1], e[0] - 15, e[1] * 0.5, e[0] - 40, B, 0.5).draw()) + g.bar(s[0] - 130, s[0] + 60, B / 2); g.bar(e[0] - 60, e[0] + 130, B / 2) + + +# ===================================================================== +# MATHS. Operators use the rule pen: a square nib, R thick. +# The math axis is at 250; big operators are centred on it. +# ===================================================================== + +AX = 250 + + +def Rw(g): + return max(g.B * 1.35, g.A * 0.44) + + +def rule(g, x0, y0, x1, y1, ne=None): + r = Rw(g) + g.P(x0, y0, 0).l(x1, y1, 0).draw({"a": r, "b": r, "ne": ne or 8, "role": "rule"}) + + +OPW = {"l": 1.4, "r": 1.4, "upright": True} + + +@rom("+", **OPW) +def _plus(g): + rule(g, 0, AX, 560, AX); rule(g, 280, AX - 280, 280, AX + 280) + + +@rom("−", **OPW) +def _minus(g): + rule(g, 0, AX, 560, AX) + + +@rom("=", **OPW) +def _equal(g): + rule(g, 0, AX + 108, 560, AX + 108); rule(g, 0, AX - 108, 560, AX - 108) + + +@rom("×", **OPW) +def _multiply(g): + d = 200 + rule(g, 280 - d, AX - d, 280 + d, AX + d, 3); rule(g, 280 - d, AX + d, 280 + d, AX - d, 3) + + +@rom("<", **OPW) +def _less(g): + r = Rw(g) + g.P(500, AX + 250, 0).l(0, AX, 0).l(500, AX - 250, 0).draw({"a": r, "b": r, "ne": 2.6, "role": "rule"}) + + +@rom(">", **OPW) +def _greater(g): + r = Rw(g) + g.P(0, AX + 250, 0).l(500, AX, 0).l(0, AX - 250, 0).draw({"a": r, "b": r, "ne": 2.6, "role": "rule"}) + + +@rom("·", l=1.2, r=1.2, upright=True) +def _periodcentered(g): + g.dot(0, AX, g.A * 0.95) + + +@rom("∑", l=0.6, r=0.6, bigop=True, upright=True) +def _summation(g): + A, B, S = g.A, g.B, g.S + top, bot, W = AX + 520, AX - 520, 820 + g.bar(0, W, top - B / 2); vbeak(g, W - S * 0.7, top, top - 160); g.P(40, top - B, 1).l(430, AX, 1).draw() + g.P(430, AX, 0).l(30, bot + B, 0).draw() + g.bar(0, W + 30, bot + B / 2); vbeak(g, W + 30 - S * 0.7, bot, bot + 170) + + +@rom("∫", l=0.6, r=0.6, bigop=True, slant=0.2, yc=AX) +def _integral(g): + bd, A = g.bd, g.A + d = max(bd, A * 0.9) * 0.92 + (g.P(352, AX + 600, 0.25).c(352, AX + 655, 315, AX + 680, 280, AX + 680, 0.35).c(232, AX + 680, 212, AX + 630, 205, AX + 520, 1) + .l(150, AX - 520, 1).c(143, AX - 630, 123, AX - 680, 75, AX - 680, 0.35).c(40, AX - 680, 3, AX - 655, 3, AX - 600, 0.25).draw()) + g.ball(352, AX + 600, d); g.ball(3, AX - 600, d) + + +@rom("∞", l=0.8, r=0.8) +def _infinity(g): + a, b = 360, 150 + cx, cy = a, AX + p = g.P(cx, cy, 1) + for i in range(1, 201): + t = i / 200 * TAU + p.l(cx + a * 0.9 * math.sin(t) / (1 + 0.18 * math.cos(2 * t)), + cy + b * math.sin(2 * t) / (1 + 0.18 * math.cos(2 * t)) * 1.05, 1) + p.draw() + + +@rom("∂", l=0.8, r=0.8) +def _partialdiff(g): + yb, bd, X, B, o = g.yb, g.bd, g.X, g.B, g.o + yt = 700 + o - B / 2 + (g.P(112, 600, 0.2).c(112, 672, 180, yt, 252, yt, 0.5).c(352, yt, 392, 600, 392, 420, 1).l(392, 260, 1) + .c(392, 80, 305, yb, 212, yb, 0.6).c(112, yb, 62, 90, 62, 195, 1).c(62, 312, 145, 382, 236, 382, 0.5) + .c(305, 382, 362, 345, 392, 288, 0.1).draw()) + g.ball(112, 600, bd * 0.9) + + +@rom("∇", l=0.5, r=0.5, upright=True) +def _nabla(g): + C, A, B = g.C, g.A, g.B + W = 600 + g.bar(0, W, C - B / 2); g.P(30, C - B, 1).l(W / 2 - A * 0.1, -g.o + B / 2, 0.9).draw() + g.P(W / 2 + A * 0.2, -g.o + B / 2, 0).l(W - 25, C - B, 0).draw() + + +def radical(g, top, bot, bar): + A, B = g.A, g.B + r = Rw(g) + mid, H = bot + (top - bot) * 0.42, top - bot + xb = 165 + H * 0.05 + xt = xb + H * 0.26 + g.P(0, mid - 12, 0).l(62, mid + 30, 0.2).draw({"a": r * 0.9, "b": r * 0.9, "ne": 3}) + g.P(62, mid + 30, 0.9).l(xb, bot + B / 2, 1).draw() + g.P(xb, bot + B / 2, 0).l(xt, top - r / 2, 0).draw() + rule(g, xt, top - r / 2, xt + bar, top - r / 2) + return xt + + +@rom("√", l=0.5, r=0.2, upright=True) +def _radical(g): + return radical(g, 770, -240, 40) + + +@rom("′", l=0.4, r=0.4) +def _minute(g): + g.P(150, 720, 1).l(40, 440, 0.1).draw() + + +# ===================================================================== +# ITALIC. A true cursive: entry hooks, exit tails, single-storey a and +# g, a descending f, round-bottomed v and w. Drawn upright; the engine +# slants the centre-lines but not the pen. +# ===================================================================== + +def entry(g, x, top): + B = g.B + return g.P(x - 72, top - 88, 0).c(x - 50, top - 36, x - 26, top - B / 2, x - 6, top - B / 2, 0.4).c(x, top - B / 2, x, top - 34, x, top - 64, 1) + + +def exit_t(p, g, x): + yb = g.yb + return p.l(x, 112, 1).c(x, 34, x + 22, yb, x + 62, yb, 0.4).c(x + 96, yb, x + 118, 30, x + 130, 76, 0) + + +def iarch(g, x0, x1, top): + B, h = g.B, g.h + yt = top + g.o - B / 2 + xm = (x0 + x1) / 2 + 20 + return (g.P(x0 + h * 0.3, top * 0.46, 0).c(x0 + 30, top * 0.84, xm - (xm - x0) * 0.6, yt, xm, yt, 0.5) + .c(xm + (x1 - xm) * 0.65, yt, x1, top * 0.86, x1, top * 0.6, 1)) + + +def ibowl(g, xr): + """The a, d, g, q bowl: open at the right, joins the stem.""" + X, yt, yb, h = g.X, g.yt, g.yb, g.h + (g.P(xr - h * 0.2, X * 0.8, 0.1).c(xr - 30, X + 4, xr - 80, yt, xr - 122, yt, 0.4).c(xr - 232, yt, h, X * 0.64, h, X * 0.36, 1) + .c(h, X * 0.1, 62, yb, 128, yb, 0.5).c(200, yb, xr - 30, X * 0.25, xr - h * 0.25, X * 0.47, 0).draw()) + + +@ita("i", l=0.7, r=0.5) +def _ii(g): + X, AS = g.X, g.AS + exit_t(entry(g, 0, X), g, 0).draw(); g.dot(18, X + (AS - X) * 0.66, dotD(g)) + + +@ita("l", l=0.7, r=0.5) +def _il(g): + exit_t(entry(g, 0, g.AS), g, 0).draw() + + +@ita("j", l=0.5, r=0.6) +def _ij(g): + X, AS, D, B, bd, o = g.X, g.AS, g.D, g.B, g.bd, g.o + yb = D - o + B / 2 + (entry(g, 0, X).l(0, D + 150, 1).c(0, D + 50, -40, yb, -105, yb, 0.5) + .c(-150, yb, -182, D + 22, -190, D + bd * 0.5 + 8, 0.25).draw()) + g.ball(-190, D + bd * 0.5 + 8); g.dot(18, X + (AS - X) * 0.66, dotD(g)) + + +@ita("n", l=0.7, r=0.5) +def _in(g): + X, B = g.X, g.B + entry(g, 0, X).l(0, B / 2, 1).draw(); exit_t(iarch(g, 0, 300, X), g, 300).draw() + + +@ita("m", l=0.7, r=0.5) +def _im(g): + X, B = g.X, g.B + entry(g, 0, X).l(0, B / 2, 1).draw(); iarch(g, 0, 270, X).l(270, B / 2, 1).draw(); exit_t(iarch(g, 270, 540, X), g, 540).draw() + + +@ita("h", l=0.7, r=0.5) +def _ih(g): + X, AS, B = g.X, g.AS, g.B + entry(g, 0, AS).l(0, B / 2, 1).draw(); exit_t(iarch(g, 0, 300, X), g, 300).draw() + + +@ita("u", l=0.7, r=0.5) +def _iu(g): + X, B, yb, h = g.X, g.B, g.yb, g.h + x1 = 300 + (entry(g, 0, X).l(0, X * 0.34, 1).c(0, X * 0.08, 60, yb, 128, yb, 0.5) + .c(200, yb, x1 - 28, X * 0.22, x1 - h * 0.25, X * 0.48, 0).draw()) + exit_t(g.P(x1, X, 0.5).l(x1, X - 44, 1), g, x1).draw() + + +@ita("a", l=0.8, r=0.5) +def _ia(g): + X = g.X + x1 = 300 + ibowl(g, x1); exit_t(g.P(x1, X, 0.5).l(x1, X - 44, 1), g, x1).draw() + + +@ita("d", l=0.8, r=0.5) +def _id(g): + AS = g.AS + x1 = 300 + ibowl(g, x1); exit_t(g.P(x1, AS, 0.5).l(x1, AS - 44, 1), g, x1).draw() + + +@ita("q", l=0.8, r=0.6) +def _iq(g): + X, D = g.X, g.D + x1 = 300 + ibowl(g, x1); g.P(x1, X, 0.5).l(x1, X - 44, 1).l(x1, D + g.B / 2, 1).draw(); g.serif(x1, D, 1, "b") + + +@ita("g", l=0.8, r=0.6) +def _ig(g): + X, D, B, bd, o = g.X, g.D, g.B, g.bd, g.o + x1, yb = 300, D - o + B / 2 + ibowl(g, x1) + (g.P(x1, X, 0.5).l(x1, X - 44, 1).l(x1, D + 160, 1).c(x1, D + 55, x1 - 48, yb, x1 - 130, yb, 0.5) + .c(x1 - 205, yb, x1 - 245, D + 40, x1 - 250, D + bd * 0.5 + 10, 0.25).draw()) + g.ball(x1 - 250, D + bd * 0.5 + 10) + + +@ita("e", l=0.8, r=0.5) +def _ie(g): + X, yt, yb, h = g.X, g.yt, g.yb, g.h + (g.P(h * 0.5, X * 0.36, 0).c(110, X * 0.4, 268, X * 0.52, 268, X * 0.77, 0.35).c(268, X * 0.95, 218, yt, 160, yt, 0.4) + .c(62, yt, h, X * 0.62, h, X * 0.36, 1).c(h, X * 0.1, 80, yb, 150, yb, 0.5).c(212, yb, 252, 30, 278, 82, 0).draw()) + + +@ita("c", l=0.8, r=0.5) +def _ic(g): + X, yt, yb, h, bd = g.X, g.yt, g.yb, g.h, g.bd + (g.P(258, X * 0.74, 0.2).c(255, X * 0.95, 212, yt, 165, yt, 0.4).c(62, yt, h, X * 0.62, h, X * 0.36, 1) + .c(h, X * 0.1, 80, yb, 150, yb, 0.5).c(212, yb, 252, 30, 278, 82, 0).draw()) + g.ball(258, X * 0.74, bd * 0.8) + + +@ita("o", l=0.8, r=0.8) +def _io(g): + X, yt, yb, h = g.X, g.yt, g.yb, g.h + cx, rx = 200, 200 - h + g.ring(cx, X / 2, rx, X / 2 + g.o - g.B / 2) + + +def it_f(g, x, xd): + X, AS, D, B, bd, o, A = g.X, g.AS, g.D, g.B, g.bd, g.o, g.A + yt2, yb, ye = AS + o - B / 2, D - o + B / 2, AS - max(bd, A * 0.6) * 0.5 - 12 + (g.P(xd, ye, 0.25).c(xd, AS - 4, x + (xd - x) * 0.7, yt2, x + 70, yt2, 0.4).c(x + 20, yt2, x, AS - 60, x, AS - 160, 1) + .l(x, D + 150, 1).c(x, D + 55, x - 35, yb, x - 90, yb, 0.4).c(x - 140, yb, x - 170, D + 40, x - 172, D + bd * 0.45 + 12, 0.25).draw()) + g.ball(xd, ye); g.ball(x - 172, D + bd * 0.45 + 12, bd * 0.85) + + +@ita("f", l=0.3, r=0.2) +def _if(g): + X, B = g.X, g.B + x = 190 + it_f(g, x, x + 150); g.bar(x - 130, x + 125, X - B / 2) + + +@ita("fi", l=0.3, r=0.5) +def _ifi(g): + X, B = g.X, g.B + x = 190 + xi = x + 250 + it_f(g, x, xi + 12); exit_t(entry(g, xi, X), g, xi).draw(); g.bar(x - 130, xi - 40, X - B / 2) + + +@ita("fl", l=0.3, r=0.5) +def _ifl(g): + X, AS, B = g.X, g.AS, g.B + x = 190 + xl = x + 262 + it_f(g, x, x + 150); exit_t(entry(g, xl, AS), g, xl).draw(); g.bar(x - 130, xl - 30, X - B / 2) + + +@ita("ff", l=0.3, r=0.2) +def _iff(g): + X, B = g.X, g.B + x = 190 + x2 = x + 215 + it_f(g, x, x + 128); it_f(g, x2, x2 + 150); g.bar(x - 130, x2 + 125, X - B / 2) + + +# italic capitals: their own forms (curled feet, entry hooks, drops) + +def ifoot(g, x): + B, o = g.B, g.o + yb = -o + B / 2 + return g.P(x - 62, 52, 0).c(x - 50, 10, x - 22, yb, x + 4, 20, 0.05) + + +def iexit(p, g, x): + B, o = g.B, g.o + yb = -o + B / 2 + return p.l(x, 78, 1).c(x, 24, x + 20, yb, x + 56, yb, 0.35).c(x + 84, yb, x + 100, 18, x + 104, 44, 0) + + +@ita("A", l=0.45, r=0.4) +def _iA(g): + C, B, o, A = g.C, g.B, g.o, g.A + ifoot(g, 0).l(300, C + o - B / 2, 0).draw() + # font: the sketch's bar starts at x 105, a few units short of the + # hairline at Display sizes (open counter). It starts at 84 here. + iexit(g.P(300 - A * 0.2, C + o - B / 2, 0.85).l(408, 120, 1), g, 412).draw(); g.bar(84, 400, C * 0.31) + + +@ita("V", l=0.45, r=0.45) +def _iV(g): + C, B, A, bd = g.C, g.B, g.A, g.bd + g.P(-58, C - 82, 0).c(-44, C - 20, -4, C - B / 2, 26, C - B / 2, 0.4).c(52, C - B / 2, 60, C - 40, 62, C - 82, 1).l(255, 24, 1).draw() + g.P(255 + A * 0.1, 24, 0).l(452, C - 130, 0.05).c(462, C - 70, 474, C - 34, 500, C - 24, 0.3).draw(); g.ball(506, C - 28, bd * 0.8) + + +@ita("W", l=0.45, r=0.45) +def _iW(g): + C, B, A, bd = g.C, g.B, g.A, g.bd + g.P(-58, C - 82, 0).c(-44, C - 20, -4, C - B / 2, 26, C - B / 2, 0.4).c(52, C - B / 2, 60, C - 40, 62, C - 82, 1).l(220, 24, 1).draw() + g.P(220 + A * 0.1, 24, 0).l(390, C - B / 2, 0).draw(); g.P(390, C - B / 2, 0.8).l(560, 24, 1).draw() + g.P(560 + A * 0.1, 24, 0).l(732, C - 130, 0.05).c(742, C - 70, 754, C - 34, 780, C - 24, 0.3).draw(); g.ball(786, C - 28, bd * 0.8) + + +@ita("M", l=0.5, r=0.4) +def _iM(g): + C, B, A = g.C, g.B, g.A + ifoot(g, 10).l(70, C - B / 2, 0).draw(); g.P(70, C - B / 2, 1).l(320, 12, 0.95).draw(); g.P(320 + A * 0.1, 12, 0).l(600, C - B / 2, 0).draw() + iexit(g.P(600, C - B / 2, 1), g, 600).draw(); g.serif(600, C, -1, "r", A, g.sl * 0.6) + + +@ita("N", l=0.45, r=0.45) +def _iN(g): + C, B, A, bd = g.C, g.B, g.A, g.bd + ifoot(g, 10).l(40, C - B / 2, 0).draw(); g.P(40, C - B / 2, 1).l(470, 12, 1).draw() + g.P(470, 12, 0).l(476, C - 110, 0).c(478, C - 50, 500, C - B / 2, 532, C - B / 2, 0.25).draw(); g.ball(540, C - 34, bd * 0.75) + + +@ita("O", l=0.8, r=0.8) +def _iO(g): + C, o, B, h = g.C, g.o, g.B, g.h + g.ring(290, C / 2, 290 - h, C / 2 + o - B / 2, 1, 2.0) + + +def round_ci(g, cx, t1, end_pr): + C, o, B, h, bd = g.C, g.o, g.B, g.h, g.bd + rx, cy, ry, t0 = cx - h, C / 2, C / 2 + o - B / 2, 0.2 * PI + s = g.pt(cx, cy, rx, ry, t0, 2.0) + (g.E(cx, cy, rx, ry, t0, 0.25, 2.0).arc(cx, cy, rx, ry, t0, 0.5 * PI, 0.6, 2.0).arc(cx, cy, rx, ry, 0.5 * PI, PI, 1, 2.0) + .arc(cx, cy, rx, ry, PI, 1.5 * PI, 0.6, 2.0).arc(cx, cy, rx, ry, 1.5 * PI, t1, end_pr, 2.0).draw()) + g.ball(s[0], s[1]) + return cx, rx + + +@ita("C", l=0.8, r=0.55) +def _iC(g): + round_ci(g, 280, 1.84 * PI, 0) + + +@ita("G", l=0.8, r=0.5) +def _iG(g): + C, B = g.C, g.B + cx, rx = round_ci(g, 285, 1.9 * PI, 0.15) + xr = cx + rx * 0.99 + iexit(g.P(xr, C * 0.42, 0.5).l(xr, C * 0.38, 1), g, xr).draw() + + +@ita("Q", l=0.8, r=0.45) +def _iQ(g): + C, o, B, h = g.C, g.o, g.B, g.h + g.ring(290, C / 2, 290 - h, C / 2 + o - B / 2, 1, 2.0) + g.P(200, -4, 0.2).c(250, -70, 360, -112, 470, -100, 0.75).c(530, -94, 566, -74, 596, -52, 0).draw() + + +@ita("r", l=0.7, r=0.4) +def _ir(g): + X, B, yt, bd, h = g.X, g.B, g.yt, g.bd, g.h + ye = X - bd * 0.45 - 6 + entry(g, 0, X).l(0, B / 2, 1).draw() + g.P(h * 0.3, X * 0.48, 0).c(32, X * 0.84, 110, yt, 170, yt, 0.4).c(212, yt, 236, X * 0.95, 236, ye, 0.2).draw(); g.ball(232, ye, bd * 0.9) + + +@ita("t", l=0.7, r=0.4) +def _it(g): + X, B = g.X, g.B + exit_t(g.P(0, X + 112, 0.45).l(0, X * 0.8, 1), g, 0).draw(); g.bar(-g.h - 62, g.h + 110, X - B / 2) + + +@ita("v", l=0.7, r=0.5) +def _iv(g): + X, yb, bd = g.X, g.yb, g.bd + x1 = 310 + (entry(g, 0, X).l(0, X * 0.4, 1).c(0, X * 0.1, 60, yb, 140, yb, 0.5).c(232, yb, x1, X * 0.32, x1, X * 0.62, 0.35) + .c(x1, X * 0.8, x1 - 14, X * 0.86, x1 - 38, X * 0.9, 0.2).draw()) + g.ball(x1 - 36, X * 0.86, bd * 0.8) + + +@ita("w", l=0.7, r=0.5) +def _iw(g): + X, yb, bd, B = g.X, g.yb, g.bd, g.B + m, x1 = 250, 520 + (entry(g, 0, X).l(0, X * 0.4, 1).c(0, X * 0.1, 52, yb, 118, yb, 0.5).c(190, yb, m - 16, X * 0.22, m - g.h * 0.25, X * 0.5, 0).draw()) + (g.P(m, X, 0.5).l(m, X - 44, 1).l(m, X * 0.4, 1).c(m, X * 0.1, m + 52, yb, m + 120, yb, 0.5) + .c(m + 212, yb, x1, X * 0.32, x1, X * 0.62, 0.35).c(x1, X * 0.8, x1 - 14, X * 0.86, x1 - 38, X * 0.9, 0.2).draw()) + g.ball(x1 - 36, X * 0.86, bd * 0.8) + + +@ita("y", l=0.7, r=0.6) +def _iy(g): + X, D, B, yb, bd, o, h = g.X, g.D, g.B, g.yb, g.bd, g.o, g.h + x1, yd = 300, D - o + B / 2 + (entry(g, 0, X).l(0, X * 0.34, 1).c(0, X * 0.08, 60, yb, 128, yb, 0.5) + .c(200, yb, x1 - 28, X * 0.22, x1 - h * 0.25, X * 0.48, 0).draw()) + (g.P(x1, X, 0.5).l(x1, X - 44, 1).l(x1, D + 160, 1).c(x1, D + 55, x1 - 48, yd, x1 - 130, yd, 0.5) + .c(x1 - 205, yd, x1 - 245, D + 40, x1 - 250, D + bd * 0.5 + 10, 0.25).draw()) + g.ball(x1 - 250, D + bd * 0.5 + 10) + + +@ita("p", l=0.7, r=0.8) +def _ip(g): + X, D = g.X, g.D + entry(g, 0, X).l(0, D + g.B / 2, 1).draw(); g.serif(0, D, 1, "b") + (iarch(g, 0, 330, X).l(330, X * 0.4, 1).c(330, X * 0.1, 270, g.yb, 190, g.yb, 0.5) + .c(120, g.yb, 40, X * 0.08, g.h * 0.3, X * 0.25, 0).draw()) + + +@ita("b", l=0.7, r=0.8) +def _ib(g): + X, AS = g.X, g.AS + (entry(g, 0, AS).l(0, X * 0.3, 1).c(0, X * 0.08, 80, g.yb, 165, g.yb, 0.5).c(270, g.yb, 330, X * 0.2, 330, X * 0.5, 1) + .c(330, X * 0.82, 280, g.yt, 215, g.yt, 0.5).c(140, g.yt, 60, X * 0.78, g.h * 0.3, X * 0.5, 0).draw()) + + +@ita("k", l=0.7, r=0.5) +def _ik(g): + X, AS, B, h = g.X, g.AS, g.B, g.h + entry(g, 0, AS).l(0, B / 2, 1).draw() + g.P(h * 0.3, X * 0.32, 0).c(80, X * 0.6, 170, X + 6, 235, X * 0.86, 0.4).c(270, X * 0.74, 200, X * 0.56, 90, X * 0.5, 0.6).draw() + exit_t(g.P(120, X * 0.52, 0.5).c(190, X * 0.45, 220, X * 0.3, 220, X * 0.2, 1), g, 220).draw() + + +@ita("s", l=0.8, r=0.8) +def _is(g): + X, B, yt, yb, h, bd = g.X, g.B, g.yt, g.yb, g.h, g.bd + W = 290 + (g.P(W - 20, X * 0.78, 0.2).c(W - 28, X * 0.95, W * 0.68, yt, W * 0.5, yt, 0.5).c(W * 0.24, yt, h + 2, X * 0.92, h + 2, X * 0.74, 1) + .c(h + 2, X * 0.5, W - h - 2, X * 0.52, W - h - 2, X * 0.27, 1).c(W - h - 2, X * 0.08, W * 0.76, yb, W * 0.5, yb, 0.5) + .c(W * 0.27, yb, 22, X * 0.06, 8, X * 0.2, 0.2).draw()) + g.ball(W - 22, X * 0.78, bd * 0.75); g.ball(10, X * 0.22, bd * 0.75) + + +@ita("x", l=0.6, r=0.6) +def _ix(g): + X, B, yb, yt = g.X, g.B, g.yb, g.yt + g.P(-40, X * 0.78, 0.2).c(-20, X + 4, 60, yt, 100, X * 0.7, 0.9).l(200, X * 0.3, 1).c(240, yb + 10, 300, yb, 330, X * 0.22, 0).draw() + g.P(-10, X * 0.2, 0).c(20, yb, 90, yb, 120, X * 0.3, 0.3).l(240, X * 0.72, 0).c(260, yt, 320, X + 4, 330, X * 0.82, 0).draw() + + +@ita("z", l=0.7, r=0.7) +def _iz(g): + X, A, B = g.X, g.A, g.B + g.P(30, X * 0.82, 0.1).c(70, X + 4, 150, X - B * 2, 330, X - B / 2, 0).draw(); g.P(330 - A * 0.4, X - B, 0.9).l(30, B, 0.9).draw() + g.P(30, B / 2, 0).c(150, B * 3, 250, -B, 340, B * 2, 0.1).draw() + + +# The kerning table of the sketch, in font units (roman; italic uses 70 %). +def sketch_kern() -> dict[str, float]: + K: dict[str, float] = {} + + def add(L, R, v): + for a in L: + for b in R: + K[a + b] = v + + add("T", "aceosru", -62); add("T", "yvw", -45); add("T", "AΔ", -55); add("T", ".,", -70); add("T", "J", -40) + add("A", "TVWY", -58); add("A", "vwy", -34); add("A", "'’", -60); add("AΔ", "CGOQ", -18) + add("VW", "aeo", -46); add("VW", ".,", -70); add("VW", "A", -55); add("Y", "aeo", -64); add("Y", ".,", -80); add("Y", "A", -60) + add("L", "TVWY", -70); add("L", "'’", -80); add("L", "y", -30); add("P", ".,", -80); add("P", "A", -40); add("F", ".,", -60); add("F", "a", -20) + add("r", ".,", -50); add("vwy", ".,", -40); add("f", "'’)?!]", 30); add("'’", "s", -25); add("'’", "A", -55) + add("k", "eo", -12); add("o", "vwxy", -10); add("vwy", "o", -10); add("c", "k", 0); add("R", "TVWY", -24) + return K + + +ROMAN = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,:;!?'’-–—/()[]&" +EXTRA = "fiflff" +GREEKSET = "αβγδεθλμπρσφψωΓΔΣΩ" +MATHSET = "+−×=<>()[]∑∫√∞∂′∇·" +SKETCH_CHARS = ROMAN + EXTRA + GREEKSET + MATHSET diff --git a/warp/pyproject.toml b/warp/pyproject.toml new file mode 100644 index 0000000..c695eac --- /dev/null +++ b/warp/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "warp-loom" +version = "0.1.0" +description = "Generator for the machine-made Warp typeface." +requires-python = ">=3.13" +license = "Apache-2.0" +dependencies = [ + "fontmake==3.12.2", + "fonttools==4.67.0", + "ufoLib2==0.19.1", + "ufo2ft==3.9.1", + "skia-pathops==0.9.2", + "numpy==2.5.3", + "pillow==13.0.0", + "brotli==1.2.1", +] + +[project.optional-dependencies] +test = ["pytest==9.1.1"] +# Only for tools/sketch_compare.py (not run in CI). +compare = ["playwright==1.63.0"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] + +[build-system] +requires = ["setuptools>=80"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +packages = ["loom"] diff --git a/warp/reference/warp-sketch-0.2.html b/warp/reference/warp-sketch-0.2.html new file mode 100644 index 0000000..30129a4 --- /dev/null +++ b/warp/reference/warp-sketch-0.2.html @@ -0,0 +1,2010 @@ + + + + + +Warp Sketch + + + + + +
    +
    +

    machine-made Warp, sketch 0.2

    + +
    +
    + +
    +
    +

    Set a line, then change the pen

    +
    +
    +
    +
    +
    +
    Style +
    +
    9.0
    +
    0°
    +
    36
    +
    88 px
    +
    +
    1.00
    +
    0.20
    +
    1.00
    +
    412
    +
    + +
    +
    +
    +
    + +
    +
    +

    From the first sketch to this one

    +
    +
    +
    +
    +
    +
    + +
    +
    +

    Centre-lines and one nib

    +
    +
    +
    +
    + +
    +
    +

    A paragraph from a paper

    +
    +
    +
    +
    + +
    +
    +

    Written, not borrowed from Latin

    +
    +
    +
    + +
    +
    +

    Five equations

    +
    +
    +
    + +
    +
    +

    Every glyph

    +
    +
    +
    + +
    + +
    + +
    +
    +

    Three plates

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +

    What this sketch shows, and what it does not yet

    +
    + + + + + +
    +
    + +
    +
    + + + + + + + diff --git a/warp/reference/warp-sketch-0.2.js b/warp/reference/warp-sketch-0.2.js new file mode 100644 index 0000000..5f3dcb4 --- /dev/null +++ b/warp/reference/warp-sketch-0.2.js @@ -0,0 +1,808 @@ +/* machine-made Warp — sketch 0.2 glyph engine (pen model). + Font units, y up, em 1000. Every glyph is a set of centre-lines ("strokes"). + Each stroke is swept by a nib: a rounded rectangle `a` long and `b` thick, turned by the pen angle. + Pressure (0..1) on each node sets the nib length between b (hairline) and A (stem), so + a stroke can swell and taper. With the nib level (angle 0) vertical strokes come out thick + and horizontal strokes thin: vertical stress. Serifs are short strokes of the serif pen plus a + bracket fillet; ball terminals are where the pen rests. Nothing is an outline drawn by hand. + The sweep is the union of convex hulls of consecutive nib positions, filled non-zero, + so there are no offset curves to self-intersect: no spikes or stars at tight corners. */ +(function(root){ +"use strict"; +const PI=Math.PI, TAU=2*PI; +const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); +const lerp=(a,b,t)=>a+(b-a)*t; +const smooth=t=>t*t*t*(t*(6*t-15)+10); // smootherstep: unhurried swells, no abrupt pressure changes + +/* ---------- parameters ---------- */ +const DEFAULTS={contrast:9, stress:0, xh:412, serif:1, bracket:0.2, ball:1, opsz:12, ital:false, stem:84}; +function params(o){ + o=Object.assign({},DEFAULTS,o||{}); + const k=clamp(Math.log(o.opsz/6)/Math.log(72/6),0,1); // 0 at 6 pt, 1 at 72 pt + const A=o.stem*(1.06-0.12*k); // stems a little lighter at display sizes + const ratio=1+(o.contrast-1)*(0.34+0.66*Math.min(1,k/0.72)); // full contrast from opsz 36 up; text sizes stay sturdy + const B=Math.max(A/ratio,5); + const S=Math.max(B*1.05, A*(0.27-0.16*k)); // serif thickness: never thinner than hairline + return { + o, k, A, B, S, ratio, + ang:o.stress*PI/180, + xh:o.xh+(1-k)*22, cap:670, asc:760, desc:-250, os:11, axis:250, + sl:A*0.74*o.serif, // serif length beyond the stem + bk:o.bracket, // bracket 0 (Didone) .. 1 (Scotch) + bd:o.ball>0? A*(0.8+0.14*(1-k))*o.ball : 0, // teardrop diameter + sb:30+(1-k)*18, // side bearing, opens up at small sizes + wd:1+(1-k)*0.04, // skeleton widens at small sizes + ital:!!o.ital, slant:o.ital?0.25:0, + key:[o.contrast,o.stress,o.xh,o.serif,o.bracket,o.ball,o.opsz,o.stem].map(v=>(+v).toFixed(3)).join(',') + }; +} + +/* ---------- nib ---------- */ +const NK=28; +const nibCache=new Map(); +function nib(a,b,ang,ne){ // superellipse nib, CCW polygon + const key=a.toFixed(2)+'|'+b.toFixed(2)+'|'+ang.toFixed(4)+'|'+ne; + let v=nibCache.get(key); if(v) return v; + v=[]; const c=Math.cos(ang), s=Math.sin(ang); + for(let i=0;i4000) nibCache.clear(); + nibCache.set(key,v); return v; +} +function hull(P){ // monotone chain, CCW + P.sort((a,b)=>a[0]-b[0]||a[1]-b[1]); + const cr=(o,a,b)=>(a[0]-o[0])*(b[1]-o[1])-(a[1]-o[1])*(b[0]-o[0]); + const lo=[],up=[]; + for(const p of P){while(lo.length>=2&&cr(lo[lo.length-2],lo[lo.length-1],p)<=0)lo.pop();lo.push(p);} + for(let i=P.length-1;i>=0;i--){const p=P[i];while(up.length>=2&&cr(up[up.length-2],up[up.length-1],p)<=0)up.pop();up.push(p);} + lo.pop();up.pop();return lo.concat(up); +} +function area(poly){let s=0;for(let i=0;inew Path(g,x,y,pr); + g.E=(cx,cy,rx,ry,t0,pr,e)=>{const q=sePt(cx,cy,rx,ry,t0,e||g.ex);return new Path(g,q[0],q[1],pr);}; + g.pt=(cx,cy,rx,ry,t,e)=>sePt(cx,cy,rx,ry,t,e||g.ex); + g.ring=(cx,cy,rx,ry,pr,e)=>g.E(cx,cy,rx,ry,0,pr==null?1:pr,e).arc(cx,cy,rx,ry,0,TAU,pr==null?1:pr,e).draw({closed:true}); + g.stem=(x,y0,y1,pr,o)=>{pr=pr==null?1:pr;return g.P(x,y0+g.B/2,pr).l(x,y1-g.B/2,pr).draw(o);}; + g.hair=(x0,y0,x1,y1,o)=>g.P(x0,y0,0).l(x1,y1,0).draw(o); + g.bar=(x0,x1,y)=>{const a=Math.min(x0,x1),b=Math.max(x0,x1);return g.P(a+g.B/2,y,0).l(b-g.B/2,y,0).draw({role:'bar'});}; // horizontal hairline, ink x0..x1 centred on y + g.dot=(x,y,d)=>{d=d==null?g.bd:d;if(d>0)g.balls.push([x,y,d/2]);}; + // a terminal: a teardrop that grows out of the stroke ending nearest to it (round if no stroke ends there) + g.ball=(x,y,d)=>{d=d==null?g.bd:d;if(d>0)g.drops.push([x,y,d/2]);}; + g.fill=(pts,role)=>g.fills.push({pts,role:role||'fill'}); + // serif on a stem of width w centred at x; y = outer edge; dir=+1 foot (serif rises from y), -1 head (hangs from y) + g.serif=(x,y,dir,side,w,len)=>{ + w=w==null?g.A:w; len=len==null?g.sl:len; if(len<=1) return; const S=g.S, hw=w/2, cy=y+dir*S/2; + const L=side==='r'?x:x-hw-len, R=side==='l'?x:x+hw+len; + if(dir<0&&side==='l'&&g.lc){ // lowercase head serif: a gentle wedge, thin at the tip + const dw=len*0.3; g.P(L+S*0.3,cy-dw,0).l(x,cy,0).draw({a:S*0.9,b:S,ne:5,role:'serif'}); + g.fill([[L+S*0.2,cy-dw-S*0.3],[x,y-S*1.1],[x,y],[L+S*0.2,cy-dw+S*0.45]],'bracket');} + else g.P(L+S/2,cy,0).l(R-S/2,cy,0).draw({a:S,b:S,ne:7,role:'serif'}); + const bh=g.bk*len*1.0, bl=g.bk*len*0.85; + if(bh>2){ + const br=(xe,sx)=>{const pts=[[xe-sx*2,y+dir*S*0.5],[xe-sx*2,y+dir*(S+bh)]];const c=[xe,y+dir*S],e=[xe+sx*bl,y+dir*S]; + const s0=[xe,y+dir*(S+bh)];for(let i=0;i<=10;i++){const t=i/10,m=1-t;pts.push([m*m*s0[0]+2*m*t*c[0]+t*t*e[0],m*m*s0[1]+2*m*t*c[1]+t*t*e[1]]);} + pts.push([xe+sx*bl,y+dir*S*0.5]);g.fill(pts,'bracket');}; + if(side!=='r') br(x-hw,-1); if(side!=='l') br(x+hw,1); + } + }; + // vertical beak serif (Modern capitals): hairline spur hanging from a horizontal arm end + g.beak=(x,y,dir,len)=>{len=len==null?g.A*1.25:len;const S=g.S*0.95;g.P(x-S/2,y,0.15).l(x-S/2,y+dir*len,0).draw({a:S*1.05,b:S,ne:4,role:'serif'});}; + return g; +} + +/* ---------- glyph tables ---------- */ +const G={}, GI={}; // roman, italic overrides +const flags=ch=>({cap:/^[A-ZΓΔΣΩ]$/.test(ch),lc:/^[a-zfiflff]$/.test(ch)}); +function def(ch,fn,o){G[ch]=Object.assign({fn},flags(ch),o||{});} +function idef(ch,fn,o){GI[ch]=Object.assign({fn},flags(ch),o||{});} + +/* ===================================================================== + ROMAN LOWERCASE. Coordinates are centre-lines. g.h = half stem. + g.yt / g.yb = centre-line of round tops / bottoms (ink reaches x-height + overshoot). + ===================================================================== */ +function arch(g,x0,x1,top){ // hairline branch out of the stem, over the shoulder, down the right stem + const {B,h}=g, yt=top+g.o-B/2, xm=(x0+x1)/2+12; + g.P(x0+h*0.3,top*0.6,0).c(x0+h*0.3+30,top*0.88,xm-(xm-x0)*0.55,yt,xm,yt,0.55) + .c(xm+(x1-xm)*0.6,yt,x1,top*0.9,x1,top*0.6,1).l(x1,B/2,1).draw(); + g.serif(x1,0,1,'b'); +} +function bowlRight(g,x0){ // b, p: bowl hanging off a left stem + const {X,o,B}=g, rx=205, cx=x0+rx, cy=X/2, ry=X/2+o-B/2, t0=0.84*PI; + g.E(cx,cy,rx,ry,t0,0).arc(cx,cy,rx,ry,t0,0.5*PI,0.6).arc(cx,cy,rx,ry,0.5*PI,0,1) + .arc(cx,cy,rx,ry,0,-0.5*PI,0.6).arc(cx,cy,rx,ry,-0.5*PI,-t0,0).draw(); +} +function bowlLeft(g,x1){ // d, q + const {X,o,B}=g, rx=205, cx=x1-rx, cy=X/2, ry=X/2+o-B/2, t0=0.16*PI; + g.E(cx,cy,rx,ry,t0,0).arc(cx,cy,rx,ry,t0,0.5*PI,0.6).arc(cx,cy,rx,ry,0.5*PI,PI,1) + .arc(cx,cy,rx,ry,PI,1.5*PI,0.6).arc(cx,cy,rx,ry,1.5*PI,2*PI-t0,0).draw(); +} +const dotD=g=>g.A*1.16; + +def('n',g=>{const {X}=g; g.stem(0,0,X); g.serif(0,X,-1,'l'); g.serif(0,0,1,'b'); arch(g,0,330,X);}); +def('m',g=>{const {X}=g; g.stem(0,0,X); g.serif(0,X,-1,'l'); g.serif(0,0,1,'b'); arch(g,0,290,X); arch(g,290,580,X);}); +def('h',g=>{const {X,AS}=g; g.stem(0,0,AS); g.serif(0,AS,-1,'l'); g.serif(0,0,1,'b'); arch(g,0,330,X);}); +def('u',g=>{const {B,X,yb,h}=g, x0=0, x1=330, xm=(x0+x1)/2-12; + g.P(x0,X-B/2,1).l(x0,X*0.36,1).c(x0,X*0.08,xm-(xm-x0)*0.6,yb,xm,yb,0.55) + .c(xm+(x1-xm)*0.55,yb,x1-h*0.3-30,X*0.12,x1-h*0.3,X*0.4,0).draw(); + g.serif(x0,X,-1,'l'); g.stem(x1,0,X); g.serif(x1,X,-1,'l'); g.serif(x1,0,1,'r');}); +def('o',g=>{const {X,o,B,h}=g; g.ring(220,X/2,220-h,X/2+o-B/2);},{l:0.82,r:0.82}); +def('c',g=>{const {X,o,B,h,bd}=g, cx=220, rx=220-h, cy=X/2, ry=X/2+o-B/2, t0=0.2*PI, s=g.pt(cx,cy,rx,ry,t0); + g.E(cx,cy,rx,ry,t0,0.3).arc(cx,cy,rx,ry,t0,0.5*PI,0.55).arc(cx,cy,rx,ry,0.5*PI,PI,1) + .arc(cx,cy,rx,ry,PI,1.5*PI,0.7).arc(cx,cy,rx,ry,1.5*PI,1.84*PI,0).draw(); + g.ball(s[0]-bd*0.2,s[1]-bd*0.12);},{l:0.82,r:0.7}); +def('e',g=>{const {X,o,B,h}=g, cx=222, rx=222-h, cy=X/2, ry=X/2+o-B/2, yc=X*0.56; + const t0=Math.asin(Math.pow((yc-cy)/ry,g.ex/2)); + g.bar(cx-rx-h*0.5,cx+rx+h*0.5,yc); + g.E(cx,cy,rx,ry,t0,0.9).arc(cx,cy,rx,ry,t0,0.5*PI,0.7).arc(cx,cy,rx,ry,0.5*PI,PI,1) + .arc(cx,cy,rx,ry,PI,1.5*PI,0.7).arc(cx,cy,rx,ry,1.5*PI,1.86*PI,0).draw();},{l:0.82,r:0.75}); +def('a',g=>{const {X,B,h,yt,yb,bd}=g, xr=320, xq=h+4, xb=h+Math.max(bd,g.A)*0.42, yB=X*0.7; + g.P(xb,yB,0.25).c(xb+6,X*0.94,xr-205,yt,xr-115,yt,0.55).c(xr-28,yt,xr,X*0.86,xr,X*0.62,1).l(xr,X*0.13,1) + .c(xr,X*0.03,xr+22,yb,xr+56,yb,0.25).c(xr+78,yb,xr+92,16,xr+96,40,0).draw(); + g.P(xr-h*0.3,X*0.57,0).c(xr-110,X*0.53,xq,X*0.45,xq,X*0.23,1).c(xq,X*0.05,xq+48,yb,xq+108,yb,0.6) + .c(xq+185,yb,xr-40,X*0.06,xr-h*0.3,X*0.22,0).draw(); + g.ball(xb,yB);},{r:0.6}); +def('b',g=>{const {AS}=g; g.stem(0,0,AS); g.serif(0,AS,-1,'l'); bowlRight(g,0);},{r:0.82}); +def('p',g=>{const {X,D}=g; g.stem(0,D,X); g.serif(0,X,-1,'l'); g.serif(0,D,1,'b'); bowlRight(g,0);},{r:0.82}); +def('d',g=>{const {AS}=g, x1=410; g.stem(x1,0,AS); g.serif(x1,AS,-1,'l'); g.serif(x1,0,1,'r'); bowlLeft(g,x1);},{l:0.82}); +def('q',g=>{const {X,D}=g, x1=410; g.stem(x1,D,X); g.serif(x1,D,1,'b'); bowlLeft(g,x1);},{l:0.82}); +def('i',g=>{const {X,AS}=g; g.stem(0,0,X); g.serif(0,X,-1,'l'); g.serif(0,0,1,'b'); g.dot(-g.A*0.04,X+(AS-X)*0.66,dotD(g));}); +def('j',g=>{const {X,AS,D,B,bd,o}=g, x0=0, yb=D-o+B/2; + g.P(x0,X-B/2,1).l(x0,D+150,1).c(x0,D+50,x0-40,yb,x0-105,yb,0.5).c(x0-150,yb,x0-182,D+22,x0-190,D+bd*0.55+8,0.25).draw(); + g.ball(x0-190,D+bd*0.55+8); g.serif(x0,X,-1,'l'); g.dot(-g.A*0.04,X+(AS-X)*0.66,dotD(g));},{l:0.5}); +def('k',g=>{const {X,AS,A,B,h}=g, x0=0, ax=x0+262; + g.stem(x0,0,AS); g.serif(x0,AS,-1,'l'); g.serif(x0,0,1,'b'); + g.P(x0+h-4,X*0.26,0).l(ax,X-B/2,0).draw(); g.serif(ax,X,-1,'b',B*1.6,g.sl*0.8); + g.P(x0+128,X*0.55,0.85).l(x0+292,B/2,1).draw(); g.serif(x0+292,0,1,'b',A*0.95);},{r:0.6}); +def('l',g=>{const {AS}=g; g.stem(0,0,AS); g.serif(0,AS,-1,'l'); g.serif(0,0,1,'b');}); +def('r',g=>{const {X,B,h,yt,bd,A}=g, x0=0, xe=x0+228, ye=X-Math.max(bd,A*0.6)*0.45-8; + g.stem(x0,0,X); g.serif(x0,X,-1,'l'); g.serif(x0,0,1,'b',A,g.sl*1.15); + g.P(x0+h*0.3,X*0.6,0).c(x0+h*0.3+28,X*0.88,x0+98,yt,x0+158,yt,0.5).c(x0+200,yt,xe,X*0.96,xe,ye,0.25).draw(); + g.ball(xe-bd*0.05,ye);},{r:0.5}); +def('s',g=>{const {X,B,yt,yb,h,S}=g, W=330; + g.P(W-14,X*0.79,0.12).c(W-28,X*0.95,W*0.68,yt,W*0.5,yt,0.5).c(W*0.24,yt,h+2,X*0.92,h+2,X*0.74,1) + .c(h+2,X*0.5,W-h-2,X*0.52,W-h-2,X*0.27,1).c(W-h-2,X*0.08,W*0.76,yb,W*0.5,yb,0.5) + .c(W*0.27,yb,30,X*0.06,12,X*0.21,0.12).draw(); + g.P(W-14,X*0.66,0).l(W-14,X*0.9,0).draw({a:S*1.4,b:S,role:'serif'}); + g.P(12,X*0.1,0).l(12,X*0.34,0).draw({a:S*1.4,b:S,role:'serif'});},{l:0.8,r:0.8}); +def('t',g=>{const {X,B,yb,h}=g, x0=0; + g.P(x0,X+118,0.45).l(x0,X*0.8,1).l(x0,X*0.25,1).c(x0,X*0.04,x0+40,yb,x0+100,yb,0.45).c(x0+150,yb,x0+186,X*0.06,x0+196,X*0.2,0).draw(); + g.bar(x0-h-82,x0+h+108,X-B/2);},{l:0.7,r:0.6}); +def('v',g=>{const {X,A,B,o}=g, xl=0, xm=225, xr=445; + g.P(xl,X-B/2,1).l(xm-A*0.2,B/2-o,0.9).draw(); g.P(xm+A*0.1,B/2-o,0).l(xr,X-B/2,0).draw(); + g.serif(xl,X,-1,'b',A); g.serif(xr,X,-1,'b',B*1.6,g.sl*0.85);},{l:0.5,r:0.5}); +def('w',g=>{const {X,A,B,o}=g, a=0, b=172, c=318, d=490, e=640; + g.P(a,X-B/2,1).l(b-A*0.2,B/2-o,0.9).draw(); g.P(b+A*0.1,B/2-o,0).l(c,X-B/2,0).draw(); + g.P(c,X-B/2,1).l(d-A*0.2,B/2-o,0.9).draw(); g.P(d+A*0.1,B/2-o,0).l(e,X-B/2,0).draw(); + g.serif(a,X,-1,'b',A); g.serif(c,X,-1,'b',A*1.1,g.sl*0.7); g.serif(e,X,-1,'b',B*1.6,g.sl*0.85);},{l:0.5,r:0.5}); +def('x',g=>{const {X,A,B}=g, xl=0, xr=410; + g.P(xl+8,X-B/2,1).l(xr-8,B/2,1).draw(); g.P(xl+12,B/2,0).l(xr-12,X-B/2,0).draw(); + g.serif(xl+8,X,-1,'b',A); g.serif(xr-8,0,1,'b',A); g.serif(xl+12,0,1,'b',B*1.6,g.sl*0.85); g.serif(xr-12,X,-1,'b',B*1.6,g.sl*0.85);},{l:0.6,r:0.6}); +def('y',g=>{const {X,A,B,D,bd,o}=g, xl=0, xm=225, xr=445; + g.P(xl,X-B/2,1).l(xm-A*0.15,B/2-o,0.85).draw(); + const k=(xr-xm)/X, xb=xm-k*(-D*0.55); + g.P(xr,X-B/2,0).l(xb,D*0.55,0.1).c(xb-k*60,D*0.55-60,xm-150,D+bd*0.55+6,xm-205,D+bd*0.55+6,0.35).draw(); + g.ball(xm-205,D+bd*0.55+6); + g.serif(xl,X,-1,'b',A); g.serif(xr,X,-1,'b',B*1.6,g.sl*0.85);},{l:0.5,r:0.5}); +def('z',g=>{const {X,A,B,S}=g, x0=0, x1=370; + g.bar(x0,x1,X-B/2); g.bar(x0,x1,B/2); + g.P(x1-A*0.5,X-B,1).l(x0+A*0.5,B,1).draw(); + g.P(x0+S*0.7,X,0).l(x0+S*0.7,X-115,0).draw({a:S*1.4,b:S,role:'serif'}); + g.P(x1-S*0.7,0,0).l(x1-S*0.7,125,0).draw({a:S*1.4,b:S,role:'serif'});},{l:0.7,r:0.7}); +def('f',g=>{const {X,AS,B,h,bd,A,o}=g, x0=0, yt2=AS+o-B/2, xe=x0+232, ye=AS-Math.max(bd,A*0.6)*0.5-12; + g.P(x0,B/2,1).l(x0,AS-175,1).c(x0,AS-45,x0+62,yt2,x0+150,yt2,0.5).c(x0+205,yt2,xe,AS-25,xe,ye,0.25).draw(); + g.ball(xe,ye); g.bar(x0-h-78,x0+h+118,X-B/2); g.serif(x0,0,1,'b',A,g.sl*1.2);},{r:-0.7}); + +/* ===================================================================== + ROMAN CAPITALS. Thick stems, hairline arms, beaks on arm ends. + ===================================================================== */ +function vbeak(g,x,y0,y1){g.P(x,y0,0).l(x,y1,0).draw({a:g.S*1.4,b:g.S,role:'serif'});} +function dBowl(g,x0,y0,y1,xr,xf){ // flat hairline top and bottom, round thick right side + const {B}=g, r=(y1-y0)/2, ym=(y0+y1)/2, a=y1-B/2, b=y0+B/2; + g.P(x0,a,0).l(xf,a,0).c(xf+(xr-xf)*0.62,a,xr,ym+r*0.55,xr,ym,1).c(xr,ym-r*0.55,xf+(xr-xf)*0.62,b,xf,b,0).l(x0,b,0).draw(); +} +def('H',g=>{const {C,B}=g, x1=470; g.stem(0,0,C); g.stem(x1,0,C); g.bar(0,x1,C*0.5); + for(const x of [0,x1]){g.serif(x,C,-1,'b'); g.serif(x,0,1,'b');}}); +def('I',g=>{const {C}=g; g.stem(0,0,C); g.serif(0,C,-1,'b'); g.serif(0,0,1,'b');}); +def('E',g=>{const {C,B,h}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'l'); + g.bar(-h,385,C-B/2); vbeak(g,385-g.S*0.7,C,C-118); + g.bar(-h,330,C*0.51); vbeak(g,330-g.S*0.7,C*0.51-60,C*0.51+60); + g.bar(-h,405,B/2); vbeak(g,405-g.S*0.7,0,128);},{r:0.6}); +def('F',g=>{const {C,B,h}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'b'); + g.bar(-h,385,C-B/2); vbeak(g,385-g.S*0.7,C,C-118); + g.bar(-h,325,C*0.5); vbeak(g,325-g.S*0.7,C*0.5-60,C*0.5+60);},{r:0.5}); +def('L',g=>{const {C,B,h}=g; g.stem(0,0,C); g.serif(0,C,-1,'b'); g.serif(0,0,1,'l'); + g.bar(-h,395,B/2); vbeak(g,395-g.S*0.7,0,135);},{r:0.6}); +def('T',g=>{const {C,B}=g, W=540; g.bar(0,W,C-B/2); vbeak(g,g.S*0.7,C,C-130); vbeak(g,W-g.S*0.7,C,C-130); + g.stem(W/2,0,C); g.serif(W/2,0,1,'b');},{l:0.5,r:0.5}); +def('D',g=>{const {C}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'l'); dBowl(g,0,0,C,500,200);},{r:0.85}); +def('B',g=>{const {C}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'l'); + dBowl(g,0,C*0.52-g.B,C,385,170); dBowl(g,0,0,C*0.52,440,190);},{r:0.8}); +def('P',g=>{const {C}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'b'); dBowl(g,0,C*0.44,C,420,190);},{r:0.7}); +function roundC(g,cx,rx,t0,t1){const {C,o,B}=g, cy=C/2, ry=C/2+o-B/2; + const p=g.E(cx,cy,rx,ry,t0,0.2).arc(cx,cy,rx,ry,t0,0.5*PI,0.6).arc(cx,cy,rx,ry,0.5*PI,PI,1).arc(cx,cy,rx,ry,PI,1.5*PI,0.6).arc(cx,cy,rx,ry,1.5*PI,t1,0.15).draw(); + return [g.pt(cx,cy,rx,ry,t0),g.pt(cx,cy,rx,ry,t1)];} +def('C',g=>{const {C,h}=g, cx=300; const [s,e]=roundC(g,cx,cx-h,0.2*PI,1.8*PI); + vbeak(g,s[0]-2,s[1]-110,s[1]+30); vbeak(g,e[0]-2,e[1]-20,e[1]+100);},{l:0.82,r:0.6}); +def('O',g=>{const {C,o,B,h}=g; g.ring(330,C/2,330-h,C/2+o-B/2);},{l:0.82,r:0.82}); +def('S',g=>{const {C,B,o,h,S}=g, W=420, yt=C+o-B/2, yb=-o+B/2; + g.P(W-16,C*0.78,0.15).c(W-30,C*0.95,W*0.68,yt,W*0.5,yt,0.5).c(W*0.22,yt,h+2,C*0.92,h+2,C*0.75,1) + .c(h+2,C*0.5,W-h-2,C*0.52,W-h-2,C*0.26,1).c(W-h-2,C*0.08,W*0.76,yb,W*0.5,yb,0.5) + .c(W*0.24,yb,26,C*0.06,12,C*0.2,0.15).draw(); + vbeak(g,W-16,C*0.68,C*0.92); vbeak(g,12,C*0.08,C*0.33);},{l:0.8,r:0.8}); +def('U',g=>{const {C,B,o,h,A}=g, x0=0, x1=480, yb=-o+B/2, xm=(x0+x1)/2; + g.P(x0,C-B/2,1).l(x0,C*0.32,1).c(x0,C*0.08,xm-150,yb,xm,yb,0.5).c(xm+150,yb,x1,C*0.08,x1,C*0.32,0).l(x1,C-B/2,0).draw(); + g.serif(x0,C,-1,'b'); g.serif(x1,C,-1,'b',B*1.6,g.sl*0.9);}); +def('V',g=>{const {C,A,B,o}=g, xl=0, xm=275, xr=550; + g.P(xl,C-B/2,1).l(xm-A*0.22,B/2-o,0.9).draw(); g.P(xm+A*0.1,B/2-o,0).l(xr,C-B/2,0).draw(); + g.serif(xl,C,-1,'b'); g.serif(xr,C,-1,'b',B*1.6);},{l:0.45,r:0.45}); +def('W',g=>{const {C,A,B,o}=g, a=0, b=200, c=395, d=600, e=800; + g.P(a,C-B/2,1).l(b-A*0.2,B/2-o,0.9).draw(); g.P(b+A*0.1,B/2-o,0).l(c,C-B/2,0).draw(); + g.P(c,C-B/2,1).l(d-A*0.2,B/2-o,0.9).draw(); g.P(d+A*0.1,B/2-o,0).l(e,C-B/2,0).draw(); + g.serif(a,C,-1,'b'); g.serif(c,C,-1,'b',A*1.1,g.sl*0.7); g.serif(e,C,-1,'b',B*1.6);},{l:0.45,r:0.45}); +def('X',g=>{const {C,A,B}=g, xl=0, xr=500; + g.P(xl+8,C-B/2,1).l(xr-8,B/2,1).draw(); g.P(xl+14,B/2,0).l(xr-14,C-B/2,0).draw(); + g.serif(xl+8,C,-1,'b'); g.serif(xr-8,0,1,'b'); g.serif(xl+14,0,1,'b',B*1.6); g.serif(xr-14,C,-1,'b',B*1.6);},{l:0.55,r:0.55}); +def('Y',g=>{const {C,A,B}=g, xl=0, xm=260, xr=520, yj=C*0.44; + g.P(xl,C-B/2,1).l(xm,yj,1).draw(); g.P(xr,C-B/2,0).l(xm+A*0.2,yj,0).draw(); g.stem(xm,0,yj+B); + g.serif(xl,C,-1,'b'); g.serif(xr,C,-1,'b',B*1.6); g.serif(xm,0,1,'b');},{l:0.45,r:0.45}); +def('Z',g=>{const {C,A,B,S}=g, x0=0, x1=470; + g.bar(x0,x1,C-B/2); g.bar(x0,x1,B/2); g.P(x1-A*0.5,C-B,1).l(x0+A*0.5,B,1).draw(); + vbeak(g,x0+S*0.7,C,C-140); vbeak(g,x1-S*0.7,0,150);},{l:0.7,r:0.7}); +def('K',g=>{const {C,A,B,h}=g, x0=0; + g.stem(x0,0,C); g.serif(x0,C,-1,'b'); g.serif(x0,0,1,'b'); + g.P(x0+h-4,C*0.3,0).l(x0+410,C-B/2,0).draw(); g.serif(x0+410,C,-1,'b',B*1.6,g.sl*0.9); + g.P(x0+180,C*0.53,0.85).l(x0+452,B/2,1).draw(); g.serif(x0+452,0,1,'b',A*0.95);},{r:0.5}); +def('M',g=>{const {C,A,B}=g, x0=0, xm=330, x1=660; + g.stem(x0,0,C,0); g.P(x0+A*0.05,C-B/2,1).l(xm-A*0.2,B/2-g.o,0.9).draw(); g.P(xm+A*0.1,B/2-g.o,0).l(x1-A*0.3,C-B/2,0).draw(); g.stem(x1,0,C); + g.serif(x0,C,-1,'l',B*1.6); g.serif(x0,0,1,'b',B*1.6); g.serif(x1,C,-1,'r'); g.serif(x1,0,1,'b');}); +def('N',g=>{const {C,A,B,o}=g, x0=0, x1=500; + g.stem(x0,0,C,0); g.P(x0+A*0.1,C-B/2,1).l(x1-A*0.15,B/2-o*2,1).draw(); g.P(x1,C-B/2,0).l(x1,-o*2,0).draw(); + g.serif(x0,C,-1,'l',B*1.6); g.serif(x0,0,1,'b',B*1.6); g.serif(x1,C,-1,'b',B*1.6);}); +def('A',g=>{const {C,A,B,o}=g, xl=0, xm=280, xr=560; + g.P(xl,B/2,0).l(xm-A*0.15,C+o-B/2,0).draw(); g.P(xm-A*0.25,C+o-B/2,0.9).l(xr,B/2,1).draw(); + g.bar(xl+95,xr-85,C*0.3); g.serif(xl,0,1,'b',B*1.6); g.serif(xr,0,1,'b');},{l:0.45,r:0.45}); + +/* ---------- 0.2: refined forms ---------- */ +def('g',g=>{const {X,B,D,o,bd,h}=g, cx=198, rx=138, cy=X*0.655, ry=X*0.345-B/2+o*0.5, yb2=D-o+B/2; + g.ring(cx,cy,rx,ry); + const e=g.pt(cx,cy,rx,ry,0.3*PI); + g.P(e[0],e[1],0).c(e[0]+26,X+2,cx+rx+18,X+16,cx+rx+66,X+12,0.12).draw(); // ear: a hairline with a small drop + g.ball(cx+rx+70,X+2,bd*0.62); + const s=g.pt(cx,cy,rx,ry,1.22*PI); + g.P(s[0],s[1],0.3).c(s[0]-34,s[1]-50,46,46,140,34,0).l(300,32,0) // link: thin, then level + .c(398,32,436,-12,436,-80,1).c(436,-178,342,yb2,228,yb2,0.5) // lower loop, right and bottom + .c(108,yb2,22,-188,22,-130,1).c(22,-82,62,-54,118,-46,0).draw();},{r:0.7}); // left side, left open +def('G',g=>{const {C,A,B,h}=g, cx=300, rx=cx-h; const [s]=roundC(g,cx,rx,0.2*PI,1.88*PI); + vbeak(g,s[0]-2,s[1]-100,s[1]+24); const xr=cx+rx*0.99, yS=C*0.44; + g.P(xr,yS-B/2,1).l(xr,C*0.12,1).l(xr,-4,0.35).draw(); // stem with a fine spur below + g.serif(xr,yS,-1,'b',A,g.sl*0.7);},{l:0.82,r:0.6}); +def('R',g=>{const {C,A,B,o}=g, yb=-o+B/2; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'b'); dBowl(g,0,C*0.47,C,405,180); + g.P(170,C*0.47+B/2,0.1).c(298,C*0.45,352,C*0.36,360,C*0.2,1).l(363,C*0.1,1) + .c(365,C*0.02,392,yb,428,yb,0.35).c(458,yb,476,16,482,44,0).draw();},{r:0.5}); // curved leg, fine exit +def('Q',g=>{const {C,o,B,h}=g, cx=325; g.ring(cx,C/2,cx-h,C/2+o-B/2); + g.P(cx-40,-o+B/2+8,0.25).c(cx+10,-62,cx+110,-122,cx+235,-120,0.8).c(cx+305,-119,cx+350,-102,cx+390,-82,0).draw();},{l:0.82,r:0.45}); +def('J',g=>{const {C,B,bd,D}=g, x0=230, yb=D*0.62; + g.P(x0,C-B/2,1).l(x0,C*0.04,1).c(x0,yb*0.55,x0-40,yb,x0-108,yb,0.5).c(x0-162,yb,x0-196,yb*0.76,x0-204,yb*0.5,0.25).draw(); + g.ball(x0-204,yb*0.5); g.serif(x0,C,-1,'b');},{l:0.6}); +def('&',g=>{const {C,B,o,h}=g, yt=C+o-B/2, yb=-o+B/2; + g.P(615,yb,0).c(540,yb,470,30,410,C*0.18,0.9).c(330,C*0.36,215,C*0.5,215,C*0.73,1) + .c(215,C*0.9,255,yt,305,yt,0.5).c(360,yt,392,C*0.92,392,C*0.82,1).c(392,C*0.67,300,C*0.6,215,C*0.52,0.3) + .c(110,C*0.44,h+4,C*0.34,h+4,C*0.22,1).c(h+4,C*0.06,120,yb,232,yb,0.5).c(362,yb,458,C*0.18,508,C*0.41,0.2).draw(); + g.bar(470,620,C*0.42);},{l:0.7,r:0.55}); + +/* ---------- ligatures ---------- */ +function fTop(g,x0,xe){const {AS,B,o,bd,A}=g, yt2=AS+o-B/2, ye=AS-Math.max(bd,A*0.6)*0.5-14; + return g.P(x0,B/2,1).l(x0,AS-178,1).c(x0,AS-45,x0+66,yt2,x0+(xe-x0)*0.62,yt2,0.5).c(x0+(xe-x0)*0.88,yt2,xe,AS-28,xe,ye,0.25);} +def('fi',g=>{const {X,AS,B,h,bd,A}=g, x0=0, xi=292, ye=AS-Math.max(bd,A*0.6)*0.5-14; + fTop(g,x0,xi).draw(); g.ball(xi,ye); g.stem(xi,0,X); g.bar(x0-h-78,xi,X-B/2); + g.serif(x0,0,1,'b',A,g.sl*1.2); g.serif(xi,0,1,'b');}); +def('fl',g=>{const {X,AS,B,o,h,A}=g, x0=0, xl=292, yt2=AS+o-B/2; + g.P(x0,B/2,1).l(x0,AS-178,1).c(x0,AS-45,x0+66,yt2,x0+160,yt2,0.5).c(x0+240,yt2,xl,AS-20,xl,AS-110,1).l(xl,B/2,1).draw(); + g.bar(x0-h-78,xl,X-B/2); g.serif(x0,0,1,'b',A,g.sl*1.2); g.serif(xl,0,1,'b');}); +def('ff',g=>{const {X,AS,B,o,h,bd,A}=g, x0=0, x2=262, yt2=AS+o-B/2, ye=AS-Math.max(bd,A*0.6)*0.5-14; + g.P(x0,B/2,1).l(x0,AS-178,1).c(x0,AS-45,x0+60,yt2,x0+140,yt2,0.5).c(x0+200,yt2,x2-h-6,AS-30,x2-h*0.4,AS-82,0.05).draw(); + fTop(g,x2,x2+232).draw(); g.ball(x2+232,ye); g.bar(x0-h-78,x2+h+118,X-B/2); + g.serif(x0,0,1,'b',A,g.sl*1.2); g.serif(x2,0,1,'b',A,g.sl*1.2);},{r:-0.7}); + +/* ---------- figures: lining, tabular ---------- */ +const FIG={adv:520}; +def('0',g=>{const {C,o,B,h}=g; g.ring(250,C/2,200-h,C/2+o-B/2,1,2.05);},FIG); +def('1',g=>{const {C,B,A}=g, x=260; g.stem(x,0,C); g.P(x-g.h+6,C-B*0.7,0.35).l(x-125,C*0.8,0).draw(); g.serif(x,0,1,'b',A,g.sl*1.35);},FIG); +def('2',g=>{const {C,B,o,bd,S}=g, yt=C+o-B/2; + g.P(95,C*0.71,0.25).c(98,C*0.93,170,yt,255,yt,0.5).c(360,yt,420,C*0.86,420,C*0.68,1).c(420,C*0.46,235,C*0.3,72,B,0.85).draw(); + g.ball(95,C*0.71); g.bar(52,448,B/2); vbeak(g,448-S*0.7,0,125);},FIG); +def('3',g=>{const {C,B,o,bd}=g, yt=C+o-B/2, yb=-o+B/2; + g.P(92,C*0.75,0.25).c(98,C*0.95,170,yt,248,yt,0.5).c(350,yt,402,C*0.88,402,C*0.76,1).c(402,C*0.62,322,C*0.545,205,C*0.545,0).draw(); + g.P(205,C*0.545,0).c(352,C*0.545,432,C*0.45,432,C*0.28,1).c(432,C*0.08,342,yb,240,yb,0.5).c(150,yb,82,C*0.06,78,C*0.21,0.25).draw(); + g.ball(92,C*0.75); g.ball(80,C*0.2);},FIG); +def('4',g=>{const {C,B,A}=g, x=345; g.P(x-A*0.2,C-B/2,0).l(38,C*0.28,0).draw(); g.stem(x,0,C); + g.bar(30,462,C*0.27); g.serif(x,0,1,'b',A,g.sl*1.1);},FIG); +def('5',g=>{const {C,B,o}=g, yb=-o+B/2; + g.bar(118,415,C-B/2); g.P(124,C-B/2,0.45).l(124,C*0.53,0.45).draw(); + g.P(124,C*0.54,0).c(170,C*0.62,215,C*0.635,260,C*0.635,0.4).c(380,C*0.635,438,C*0.5,438,C*0.32,1) + .c(438,C*0.1,345,yb,245,yb,0.5).c(150,yb,82,C*0.06,78,C*0.21,0.25).draw(); g.ball(80,C*0.2);},FIG); +function six(g,M){const {C,B,o}=g, yt=C+o-B/2, ry=C*0.32+o-B/2-6; + const T=(x,y)=>M?[500-x,C-y]:[x,y], P=(x,y,pr)=>{const q=T(x,y);return g.P(q[0],q[1],pr);}; + const p=P(405,C*0.82,0.25); const c=(a,b,c2,d,e,f,pr)=>{const q1=T(a,b),q2=T(c2,d),q3=T(e,f);p.c(q1[0],q1[1],q2[0],q2[1],q3[0],q3[1],pr);}; + c(398,C*0.96,330,yt,262,yt,0.5); c(140,yt,75,C*0.75,75,C*0.36,1); p.draw(); + const q=T(250,C*0.32-6); g.ring(q[0],q[1],175,ry); const b=T(405,C*0.82); g.ball(b[0],b[1]);} +def('6',g=>six(g,false),FIG); +def('9',g=>six(g,true),FIG); +def('7',g=>{const {C,B,A}=g; g.bar(48,452,C-B/2); vbeak(g,48+g.S*0.7,C,C-130); + g.P(452-A*0.5,C-B,0.9).c(390,C*0.6,240,C*0.38,222,B/2,1).draw();},FIG); +def('8',g=>{const {C,o,B}=g; const yj=C*0.535; g.ring(250,(yj+C+o-B/2)/2,148,(C+o-B/2-yj)/2); g.ring(250,(yj-o+B/2)/2,176,(yj+o-B/2)/2);},FIG); + +/* ---------- punctuation and delimiters ---------- */ +function paren(g,x0,w,top,bot,dir){const m=(top+bot)/2,H=top-bot, xr=x0+w*dir, B=g.B; + g.P(xr,top-B/2,0).c(xr-dir*w*0.82,top-H*0.2,x0,m+H*0.26,x0,m,1).c(x0,m-H*0.26,xr-dir*w*0.82,bot+H*0.2,xr,bot+B/2,0).draw();} +function brack(g,x,top,bot,dir){const {B}=g; g.stem(x,bot,top,1); g.bar(x-dir*g.h,x+dir*130,top-B/2); g.bar(x-dir*g.h,x+dir*130,bot+B/2); + if(dir<0){} } +def(' ',g=>{},{adv:270,empty:true}); +def('.',g=>{g.dot(0,dotD(g)/2,dotD(g));},{l:0.9,r:0.9}); +def(',',g=>{const d=dotD(g); g.ball(0,d/2,d); g.P(d*0.22,d*0.32,0.85).c(d*0.3,-d*0.3,0,-d*0.9,-d*0.45,-d*1.25,0).draw();},{l:0.9,r:0.9}); +def(':',g=>{const d=dotD(g); g.dot(0,d/2,d); g.dot(0,g.X-d/2,d);},{l:0.9,r:0.9}); +def(';',g=>{const d=dotD(g); g.dot(0,g.X-d/2,d); g.ball(0,d/2,d); g.P(d*0.22,d*0.32,0.85).c(d*0.3,-d*0.3,0,-d*0.9,-d*0.45,-d*1.25,0).draw();},{l:0.9,r:0.9}); +def('!',g=>{const d=dotD(g),{C,B}=g; g.P(0,C-B/2,1).l(0,C*0.6,0.85).l(0,d+70,0.15).draw(); g.dot(0,d/2,d);},{l:0.9,r:0.9}); +def('?',g=>{const d=dotD(g),{C,B,o}=g, yt=C+o-B/2; + g.P(48,C*0.72,0.25).c(52,C*0.94,120,yt,190,yt,0.5).c(280,yt,325,C*0.86,325,C*0.73,1).c(325,C*0.55,190,C*0.5,190,C*0.33,0.6).l(190,d+80,0.2).draw(); + g.ball(48,C*0.72); g.dot(190,d/2,d);},{l:0.8,r:0.8}); +def("'",g=>{const {C}=g; g.P(0,C-g.B/2,1).l(0,C*0.66,0.15).draw();},{l:0.9,r:0.9}); +def('’',g=>{const d=dotD(g),{C}=g; g.ball(0,C-d/2,d); g.P(d*0.22,C-d*0.68,0.85).c(d*0.3,C-d*1.3,0,C-d*1.9,-d*0.45,C-d*2.25,0).draw();},{l:0.9,r:0.9}); +def('-',g=>{const {X}=g; g.P(0,X*0.56,0).l(170,X*0.56,0).draw({b:g.A*0.5,a:g.A*0.5,ne:5});},{l:0.8,r:0.8}); +def('–',g=>{const {X}=g; g.P(0,X*0.56,0).l(500,X*0.56,0).draw({b:g.B*1.5,a:g.B*1.5,ne:5});},{l:0.4,r:0.4}); +def('—',g=>{const {X}=g; g.P(0,X*0.56,0).l(940,X*0.56,0).draw({b:g.B*1.5,a:g.B*1.5,ne:5});},{l:0.2,r:0.2}); +def('/',g=>{const {C}=g; g.P(330,C+60,0.55).l(0,-120,0.55).draw();},{l:0.5,r:0.5}); +def('(',g=>paren(g,0,150,770,-250,1),{l:0.9,r:0.6}); +def(')',g=>paren(g,150,150,770,-250,-1),{l:0.6,r:0.9}); +def('[',g=>brack(g,0,770,-250,1),{l:0.9,r:0.6}); +def(']',g=>brack(g,130,770,-250,-1),{l:0.6,r:0.9}); +function h2(g){return g.h+4;} + +/* ===================================================================== + GREEK. One continuous pen line where the hand would make one; no slab + serifs on the lowercase, entries and exits are pen pressure. + ===================================================================== */ +def('α',g=>{const {X,yt,yb,h}=g; + g.P(448,X+8,0.1).c(425,X*0.58,335,yb,215,yb,0.6).c(92,yb,h+2,X*0.24,h+2,X*0.5,1) + .c(h+2,X*0.78,100,yt,212,yt,0.6).c(318,yt,392,X*0.42,478,4,0.05).draw();},{l:0.8,r:0.5}); +def('β',g=>{const {X,AS,D,B,o,h}=g, x0=0, yt2=AS+o-B/2, yb=-o+B/2; + g.P(x0,D,0.7).l(x0,D+50,1).l(x0,AS*0.66,1).c(x0,AS-12,95,yt2,190,yt2,0.5).c(290,yt2,345,AS*0.9,345,AS*0.79,1) + .c(345,X*1.18,268,X+16,165,X+16,0).c(335,X+16,412,X*0.78,412,X*0.42,1) + .c(412,X*0.1,322,yb,215,yb,0.5).c(118,yb,x0+30,X*0.08,x0+h*0.3,X*0.22,0).draw();},{r:0.8}); +def('γ',g=>{const {X,D,B}=g; + g.P(0,X*0.72,0.15).c(10,X+4,92,X+6,128,X*0.76,0.9).l(232,-30,1).c(262,-95,262,D+70,226,D+70,0.45) + .c(190,D+70,190,-95,226,-30,0.15).draw(); + g.P(450,X-B/2,0).l(232,-30,0).draw();},{l:0.6,r:0.5}); +def('δ',g=>{const {X,AS,B,o,h,bd}=g, cx=225, cy=X*0.46, rx=225-h, ry=X*0.46+o-B/2, yt2=AS+o-B/2; + g.ring(cx,cy,rx,ry); + const e=g.pt(cx,cy,rx,ry,0.36*PI); + g.P(372,AS-bd*0.45-10,0.2).c(366,AS+2,300,yt2,232,yt2,0.45).c(150,yt2,108,AS*0.94,108,AS*0.86,0.8) + .c(108,AS*0.72,e[0]-40,X*1.04,e[0],e[1],1).draw(); + g.ball(372,AS-bd*0.45-10,bd*0.9);},{l:0.82,r:0.8}); +def('ε',g=>{const {X,yt,yb,h,bd}=g; + g.P(352,X*0.79,0.2).c(345,X*0.97,282,yt,218,yt,0.4).c(122,yt,h+28,X*0.9,h+28,X*0.76,1).c(h+28,X*0.6,128,X*0.53,232,X*0.53,0).draw(); + g.P(232,X*0.53,0).c(108,X*0.53,h,X*0.42,h,X*0.25,1).c(h,X*0.06,108,yb,218,yb,0.5).c(318,yb,366,X*0.06,385,X*0.17,0).draw(); + g.ball(352,X*0.79,bd*0.85);},{l:0.82,r:0.7}); +def('θ',g=>{const {AS,o,B,h}=g, cy=AS/2-o; g.ring(215,cy,215-h,AS/2+o-B/2,1,2.0); g.bar(h*0.6,430-h*0.6,cy);},{l:0.82,r:0.82}); +def('λ',g=>{const {X,AS,B}=g, yb=-g.o+B/2; + g.P(12,AS-38,0.1).c(38,AS+4,95,AS,122,AS-72,0.9).l(390,62,1).c(400,22,422,yb,452,yb,0.3).draw(); + g.P(290,X*0.62,0).l(12,B/2,0.25).draw();},{l:0.6,r:0.55}); +def('μ',g=>{const {X,D,B,yb,h}=g, x0=0, x1=330; + g.P(x0,X-B/2,0.5).l(x0,X-60,1).l(x0,D+40,1).l(x0,D+B/2,0.7).draw(); + g.P(x0+h*0.3,X*0.34,0).c(x0+28,X*0.06,100,yb,165,yb,0.55).c(250,yb,x1-30,X*0.12,x1-h*0.3,X*0.38,0).draw(); + g.P(x1,X-B/2,0.5).l(x1,X-60,1).l(x1,70,1).c(x1,22,x1+22,yb,x1+60,yb,0.3).c(x1+80,yb,x1+93,18,x1+96,42,0).draw();},{r:0.55}); +def('π',g=>{const {X,A,B,o}=g, yb=-o+B/2, bb=A*0.36, yb2=X-bb/2; + g.P(-12,X*0.76,0.4).c(0,X*0.92,40,yb2,110,yb2,0.6).l(540,yb2,0.3).draw({b:bb}); + g.P(165,X-bb,0.8).c(165,X*0.45,138,X*0.18,72,26,0.35).draw(); + g.P(398,X-bb,1).l(398,78,1).c(398,25,418,yb,452,yb,0.3).c(478,yb,492,20,495,46,0).draw();},{l:0.5,r:0.5}); +def('ρ',g=>{const {X,D,o,B,h}=g, cx=225, rx=225-h, cy=X/2, ry=X/2+o-B/2; + g.ring(cx,cy,rx,ry); g.P(h,X*0.48,1).l(h,D+40,1).l(h,D+B/2,0.7).draw();},{l:0.8,r:0.82}); +def('σ',g=>{const {X,o,B,h}=g, cx=212, cy=X*0.47, rx=212-h, ry=X*0.47+o-B/2, bb=B*2.1; + g.ring(cx,cy,rx,ry); g.P(cx,cy+ry+B/2-bb/2,0).l(470,cy+ry+B/2-bb/2,0).draw({b:bb,a:bb,ne:3});},{l:0.82,r:0.5}); +def('φ',g=>{const {X,AS,D,o,B}=g, cx=245; + g.ring(cx,X/2,245-g.h,X/2+o-B/2,1,2.1); + g.P(cx,D,0.7).l(cx,D+45,1).l(cx,AS-80,1).l(cx,AS-30,0.7).draw();},{l:0.82,r:0.82}); +def('ψ',g=>{const {X,AS,D,B}=g, cx=250; + g.P(cx,D,0.7).l(cx,D+45,1).l(cx,AS-80,1).l(cx,AS-30,0.7).draw(); + g.P(18,X,0.3).l(18,X-50,1).l(18,X*0.55,1).c(18,X*0.12,110,40,cx,40,0.4).c(390,40,482,X*0.12,482,X*0.55,1).l(482,X-50,1).l(482,X,0.3).draw();},{l:0.6,r:0.6}); +def('ω',g=>{const {X,yb,h}=g, W=600; + g.P(118,X+4,0.15).c(48,X*0.82,h,X*0.6,h,X*0.36,1).c(h,X*0.08,92,yb,165,yb,0.5).c(238,yb,282,X*0.15,300,X*0.52,0.15) + .c(318,X*0.15,362,yb,435,yb,0.5).c(508,yb,W-h,X*0.08,W-h,X*0.36,1).c(W-h,X*0.6,W-48,X*0.82,W-118,X+4,0.15).draw();},{l:0.75,r:0.75}); +def('Γ',g=>{const {C,B,h}=g; g.stem(0,0,C); g.serif(0,C,-1,'l'); g.serif(0,0,1,'b'); g.bar(-h,395,C-B/2); vbeak(g,395-g.S*0.7,C,C-128);},{r:0.5}); +def('Δ',g=>{const {C,A,B,o}=g, xl=0, xm=290, xr=580; + g.P(xl+20,B,0).l(xm-A*0.15,C+o-B/2,0).draw(); g.P(xm-A*0.25,C+o-B/2,0.9).l(xr-A*0.35,B,1).draw(); g.bar(xl,xr,B/2);},{l:0.45,r:0.45}); +def('Σ',g=>{const {C,A,B,S}=g, W=480; + g.bar(0,W,C-B/2); vbeak(g,W-S*0.7,C,C-130); g.P(30,C-B,1).l(255,C*0.5,1).draw(); g.P(255,C*0.5,0).l(25,B,0).draw(); + g.bar(0,W+20,B/2); vbeak(g,W+20-S*0.7,0,140);},{l:0.6,r:0.6}); +def('Ω',g=>{const {C,B,A,o}=g, cx=330, cy=C*0.56, rx=330-g.h-30, ry=C*0.44+o-B/2, t0=1.37*PI, t1=-0.37*PI; + const s=g.pt(cx,cy,rx,ry,t0), e=g.pt(cx,cy,rx,ry,t1); + g.P(s[0]+40,B,0.5).c(s[0]+15,s[1]*0.5,s[0],s[1],s[0],s[1],0.7).arc(cx,cy,rx,ry,t0,PI,1).arc(cx,cy,rx,ry,PI,0.5*PI,0.6) + .arc(cx,cy,rx,ry,0.5*PI,0,1).arc(cx,cy,rx,ry,0,t1,0.7).c(e[0],e[1],e[0]-15,e[1]*0.5,e[0]-40,B,0.5).draw(); + g.bar(s[0]-130,s[0]+60,B/2); g.bar(e[0]-60,e[0]+130,B/2);},{l:0.6,r:0.6}); + +/* ===================================================================== + MATHS. Operators use the rule pen: a square nib, R thick. + The math axis is at 250; big operators are centred on it. + ===================================================================== */ +function R(g){return Math.max(g.B*1.35,g.A*0.44);} +function rule(g,x0,y0,x1,y1,ne){const r=R(g);g.P(x0,y0,0).l(x1,y1,0).draw({a:r,b:r,ne:ne||8,role:'rule'});} +const AX=250, OPW={l:1.4,r:1.4,upright:true}; +def('+',g=>{rule(g,0,AX,560,AX); rule(g,280,AX-280,280,AX+280);},OPW); +def('−',g=>{rule(g,0,AX,560,AX);},OPW); +def('=',g=>{rule(g,0,AX+108,560,AX+108); rule(g,0,AX-108,560,AX-108);},OPW); +def('×',g=>{const d=200; rule(g,280-d,AX-d,280+d,AX+d,3); rule(g,280-d,AX+d,280+d,AX-d,3);},OPW); +def('<',g=>{const r=R(g); g.P(500,AX+250,0).l(0,AX,0).l(500,AX-250,0).draw({a:r,b:r,ne:2.6,role:'rule'});},OPW); +def('>',g=>{const r=R(g); g.P(0,AX+250,0).l(500,AX,0).l(0,AX-250,0).draw({a:r,b:r,ne:2.6,role:'rule'});},OPW); +def('·',g=>{g.dot(0,AX,g.A*0.95);},{l:1.2,r:1.2,upright:true}); +def('∑',g=>{const {A,B,S}=g, top=AX+520, bot=AX-520, W=820; + g.bar(0,W,top-B/2); vbeak(g,W-S*0.7,top,top-160); g.P(40,top-B,1).l(430,AX,1).draw(); g.P(430,AX,0).l(30,bot+B,0).draw(); + g.bar(0,W+30,bot+B/2); vbeak(g,W+30-S*0.7,bot,bot+170);},{l:0.6,r:0.6,bigop:true,upright:true}); +def('∫',g=>{const {bd,A}=g, d=Math.max(bd,A*0.9)*0.92; + g.P(352,AX+600,0.25).c(352,AX+655,315,AX+680,280,AX+680,0.35).c(232,AX+680,212,AX+630,205,AX+520,1) + .l(150,AX-520,1).c(143,AX-630,123,AX-680,75,AX-680,0.35).c(40,AX-680,3,AX-655,3,AX-600,0.25).draw(); + g.ball(352,AX+600,d); g.ball(3,AX-600,d);},{l:0.6,r:0.6,bigop:true,slant:0.2,yc:AX}); +def('∞',g=>{const a=360, b=150, cx=a, cy=AX; const p=g.P(cx,cy,1); + for(let i=1;i<=200;i++){const t=i/200*TAU; p.l(cx+a*0.9*Math.sin(t)/(1+0.18*Math.cos(2*t)), cy+b*Math.sin(2*t)/(1+0.18*Math.cos(2*t))*1.05,1);} + p.draw();},{l:0.8,r:0.8}); +def('∂',g=>{const {yb,bd,X,B,o}=g, yt=700+o-B/2; + g.P(112,600,0.2).c(112,672,180,yt,252,yt,0.5).c(352,yt,392,600,392,420,1).l(392,260,1) + .c(392,80,305,yb,212,yb,0.6).c(112,yb,62,90,62,195,1).c(62,312,145,382,236,382,0.5).c(305,382,362,345,392,288,0.1).draw(); + g.ball(112,600,bd*0.9);},{l:0.8,r:0.8}); +def('∇',g=>{const {C,A,B}=g, W=600; g.bar(0,W,C-B/2); g.P(30,C-B,1).l(W/2-A*0.1,-g.o+B/2,0.9).draw(); g.P(W/2+A*0.2,-g.o+B/2,0).l(W-25,C-B,0).draw();},{l:0.5,r:0.5,upright:true}); +def('√',g=>radical(g,770,-240,40),{l:0.5,r:0.2,upright:true}); +def('′',g=>{g.P(150,720,1).l(40,440,0.1).draw();},{l:0.4,r:0.4}); +function radical(g,top,bot,bar){const {A,B}=g, r=R(g), mid=bot+(top-bot)*0.42, H=top-bot; + const xb=165+H*0.05, xt=xb+H*0.26; + g.P(0,mid-12,0).l(62,mid+30,0.2).draw({a:r*0.9,b:r*0.9,ne:3}); + g.P(62,mid+30,0.9).l(xb,bot+B/2,1).draw(); + g.P(xb,bot+B/2,0).l(xt,top-r/2,0).draw(); + rule(g,xt,top-r/2,xt+bar,top-r/2); + return xt;} + +/* ===================================================================== + ITALIC. A true cursive: entry hooks, exit tails, single-storey a and g, + a descending f, round-bottomed v and w. Drawn upright; the engine slants + the centre-lines but not the pen, so the stress stays vertical. + ===================================================================== */ +function entry(g,x,top){const {B}=g; return g.P(x-72,top-88,0).c(x-50,top-36,x-26,top-B/2,x-6,top-B/2,0.4).c(x,top-B/2,x,top-34,x,top-64,1);} +function exitT(p,g,x){const {yb}=g; return p.l(x,112,1).c(x,34,x+22,yb,x+62,yb,0.4).c(x+96,yb,x+118,30,x+130,76,0);} +function iarch(g,x0,x1,top){const {B,h}=g, yt=top+g.o-B/2, xm=(x0+x1)/2+20; + return g.P(x0+h*0.3,top*0.46,0).c(x0+30,top*0.84,xm-(xm-x0)*0.6,yt,xm,yt,0.5).c(xm+(x1-xm)*0.65,yt,x1,top*0.86,x1,top*0.6,1);} +function ibowl(g,xr){const {X,yt,yb,h}=g; // the a, d, g, q bowl: open at the right, joins the stem + g.P(xr-h*0.2,X*0.8,0.1).c(xr-30,X+4,xr-80,yt,xr-122,yt,0.4).c(xr-232,yt,h,X*0.64,h,X*0.36,1) + .c(h,X*0.1,62,yb,128,yb,0.5).c(200,yb,xr-30,X*0.25,xr-h*0.25,X*0.47,0).draw();} +idef('i',g=>{const {X,AS}=g; exitT(entry(g,0,X),g,0).draw(); g.dot(18,X+(AS-X)*0.66,dotD(g));},{l:0.7,r:0.5}); +idef('l',g=>{const {AS}=g; exitT(entry(g,0,AS),g,0).draw();},{l:0.7,r:0.5}); +idef('j',g=>{const {X,AS,D,B,bd,o}=g, yb=D-o+B/2; + entry(g,0,X).l(0,D+150,1).c(0,D+50,-40,yb,-105,yb,0.5).c(-150,yb,-182,D+22,-190,D+bd*0.5+8,0.25).draw(); + g.ball(-190,D+bd*0.5+8); g.dot(18,X+(AS-X)*0.66,dotD(g));},{l:0.5,r:0.6}); +idef('n',g=>{const {X,B}=g; entry(g,0,X).l(0,B/2,1).draw(); exitT(iarch(g,0,300,X),g,300).draw();},{l:0.7,r:0.5}); +idef('m',g=>{const {X,B}=g; entry(g,0,X).l(0,B/2,1).draw(); iarch(g,0,270,X).l(270,B/2,1).draw(); exitT(iarch(g,270,540,X),g,540).draw();},{l:0.7,r:0.5}); +idef('h',g=>{const {X,AS,B}=g; entry(g,0,AS).l(0,B/2,1).draw(); exitT(iarch(g,0,300,X),g,300).draw();},{l:0.7,r:0.5}); +idef('u',g=>{const {X,B,yb,h}=g, x1=300; + entry(g,0,X).l(0,X*0.34,1).c(0,X*0.08,60,yb,128,yb,0.5).c(200,yb,x1-28,X*0.22,x1-h*0.25,X*0.48,0).draw(); + exitT(g.P(x1,X,0.5).l(x1,X-44,1),g,x1).draw();},{l:0.7,r:0.5}); +idef('a',g=>{const {X}=g, x1=300; ibowl(g,x1); exitT(g.P(x1,X,0.5).l(x1,X-44,1),g,x1).draw();},{l:0.8,r:0.5}); +idef('d',g=>{const {AS}=g, x1=300; ibowl(g,x1); exitT(g.P(x1,AS,0.5).l(x1,AS-44,1),g,x1).draw();},{l:0.8,r:0.5}); +idef('q',g=>{const {X,D}=g, x1=300; ibowl(g,x1); g.P(x1,X,0.5).l(x1,X-44,1).l(x1,D+g.B/2,1).draw(); g.serif(x1,D,1,'b');},{l:0.8,r:0.6}); +idef('g',g=>{const {X,D,B,bd,o}=g, x1=300, yb=D-o+B/2; ibowl(g,x1); + g.P(x1,X,0.5).l(x1,X-44,1).l(x1,D+160,1).c(x1,D+55,x1-48,yb,x1-130,yb,0.5).c(x1-205,yb,x1-245,D+40,x1-250,D+bd*0.5+10,0.25).draw(); + g.ball(x1-250,D+bd*0.5+10);},{l:0.8,r:0.6}); +idef('e',g=>{const {X,yt,yb,h}=g; + g.P(h*0.5,X*0.36,0).c(110,X*0.4,268,X*0.52,268,X*0.77,0.35).c(268,X*0.95,218,yt,160,yt,0.4) + .c(62,yt,h,X*0.62,h,X*0.36,1).c(h,X*0.1,80,yb,150,yb,0.5).c(212,yb,252,30,278,82,0).draw();},{l:0.8,r:0.5}); +idef('c',g=>{const {X,yt,yb,h,bd}=g; + g.P(258,X*0.74,0.2).c(255,X*0.95,212,yt,165,yt,0.4).c(62,yt,h,X*0.62,h,X*0.36,1).c(h,X*0.1,80,yb,150,yb,0.5).c(212,yb,252,30,278,82,0).draw(); + g.ball(258,X*0.74,bd*0.8);},{l:0.8,r:0.5}); +idef('o',g=>{const {X,yt,yb,h}=g, cx=200, rx=200-h; + g.ring(cx,X/2,rx,X/2+g.o-g.B/2);},{l:0.8,r:0.8}); +function itF(g,x,xd){const {X,AS,D,B,bd,o,A}=g, yt2=AS+o-B/2, yb=D-o+B/2, ye=AS-Math.max(bd,A*0.6)*0.5-12; + g.P(xd,ye,0.25).c(xd,AS-4,x+(xd-x)*0.7,yt2,x+70,yt2,0.4).c(x+20,yt2,x,AS-60,x,AS-160,1).l(x,D+150,1) + .c(x,D+55,x-35,yb,x-90,yb,0.4).c(x-140,yb,x-170,D+40,x-172,D+bd*0.45+12,0.25).draw(); + g.ball(xd,ye); g.ball(x-172,D+bd*0.45+12,bd*0.85);} +idef('f',g=>{const {X,B}=g, x=190; itF(g,x,x+150); g.bar(x-130,x+125,X-B/2);},{l:0.3,r:0.2}); +idef('fi',g=>{const {X,B}=g, x=190, xi=x+250; itF(g,x,xi+12); exitT(entry(g,xi,X),g,xi).draw(); g.bar(x-130,xi-40,X-B/2);},{l:0.3,r:0.5}); +idef('fl',g=>{const {X,AS,B}=g, x=190, xl=x+262; itF(g,x,x+150); exitT(entry(g,xl,AS),g,xl).draw(); g.bar(x-130,xl-30,X-B/2);},{l:0.3,r:0.5}); +idef('ff',g=>{const {X,B}=g, x=190, x2=x+215; itF(g,x,x+128); itF(g,x2,x2+150); g.bar(x-130,x2+125,X-B/2);},{l:0.3,r:0.2}); +/* italic capitals: their own forms (curled feet, entry hooks, drops), not slanted roman */ +function iFoot(g,x,y1){const {B,o}=g, yb=-o+B/2; return g.P(x-62,52,0).c(x-50,10,x-22,yb,x+4,20,0.05);} +function iExit(p,g,x){const {B,o}=g, yb=-o+B/2; return p.l(x,78,1).c(x,24,x+20,yb,x+56,yb,0.35).c(x+84,yb,x+100,18,x+104,44,0);} +idef('A',g=>{const {C,B,o,A}=g; + iFoot(g,0).l(300,C+o-B/2,0).draw(); + iExit(g.P(300-A*0.2,C+o-B/2,0.85).l(408,120,1),g,412).draw(); g.bar(105,400,C*0.31);},{l:0.45,r:0.4}); +idef('V',g=>{const {C,B,A,bd}=g; + g.P(-58,C-82,0).c(-44,C-20,-4,C-B/2,26,C-B/2,0.4).c(52,C-B/2,60,C-40,62,C-82,1).l(255,24,1).draw(); + g.P(255+A*0.1,24,0).l(452,C-130,0.05).c(462,C-70,474,C-34,500,C-24,0.3).draw(); g.ball(506,C-28,bd*0.8);},{l:0.45,r:0.45}); +idef('W',g=>{const {C,B,A,bd}=g; + g.P(-58,C-82,0).c(-44,C-20,-4,C-B/2,26,C-B/2,0.4).c(52,C-B/2,60,C-40,62,C-82,1).l(220,24,1).draw(); + g.P(220+A*0.1,24,0).l(390,C-B/2,0).draw(); g.P(390,C-B/2,0.8).l(560,24,1).draw(); + g.P(560+A*0.1,24,0).l(732,C-130,0.05).c(742,C-70,754,C-34,780,C-24,0.3).draw(); g.ball(786,C-28,bd*0.8);},{l:0.45,r:0.45}); +idef('M',g=>{const {C,B,A}=g; + iFoot(g,10).l(70,C-B/2,0).draw(); g.P(70,C-B/2,1).l(320,12,0.95).draw(); g.P(320+A*0.1,12,0).l(600,C-B/2,0).draw(); + iExit(g.P(600,C-B/2,1),g,600).draw(); g.serif(600,C,-1,'r',A,g.sl*0.6);},{l:0.5,r:0.4}); +idef('N',g=>{const {C,B,A,bd}=g; + iFoot(g,10).l(40,C-B/2,0).draw(); g.P(40,C-B/2,1).l(470,12,1).draw(); + g.P(470,12,0).l(476,C-110,0).c(478,C-50,500,C-B/2,532,C-B/2,0.25).draw(); g.ball(540,C-34,bd*0.75);},{l:0.45,r:0.45}); +idef('O',g=>{const {C,o,B,h}=g; g.ring(290,C/2,290-h,C/2+o-B/2,1,2.0);},{l:0.8,r:0.8}); +function roundCi(g,cx,t1,endPr){const {C,o,B,h,bd}=g, rx=cx-h, cy=C/2, ry=C/2+o-B/2, t0=0.2*PI, s=g.pt(cx,cy,rx,ry,t0,2.0); + g.E(cx,cy,rx,ry,t0,0.25,2.0).arc(cx,cy,rx,ry,t0,0.5*PI,0.6,2.0).arc(cx,cy,rx,ry,0.5*PI,PI,1,2.0).arc(cx,cy,rx,ry,PI,1.5*PI,0.6,2.0).arc(cx,cy,rx,ry,1.5*PI,t1,endPr,2.0).draw(); + g.ball(s[0],s[1]); return [cx,rx];} +idef('C',g=>{roundCi(g,280,1.84*PI,0);},{l:0.8,r:0.55}); +idef('G',g=>{const {C,B}=g; const [cx,rx]=roundCi(g,285,1.9*PI,0.15); const xr=cx+rx*0.99; + iExit(g.P(xr,C*0.42,0.5).l(xr,C*0.38,1),g,xr).draw();},{l:0.8,r:0.5}); +idef('Q',g=>{const {C,o,B,h}=g; g.ring(290,C/2,290-h,C/2+o-B/2,1,2.0); + g.P(200,-4,0.2).c(250,-70,360,-112,470,-100,0.75).c(530,-94,566,-74,596,-52,0).draw();},{l:0.8,r:0.45}); +idef('r',g=>{const {X,B,yt,bd,h}=g, ye=X-bd*0.45-6; + entry(g,0,X).l(0,B/2,1).draw(); + g.P(h*0.3,X*0.48,0).c(32,X*0.84,110,yt,170,yt,0.4).c(212,yt,236,X*0.95,236,ye,0.2).draw(); g.ball(232,ye,bd*0.9);},{l:0.7,r:0.4}); +idef('t',g=>{const {X,B}=g; exitT(g.P(0,X+112,0.45).l(0,X*0.8,1),g,0).draw(); g.bar(-g.h-62,g.h+110,X-B/2);},{l:0.7,r:0.4}); +idef('v',g=>{const {X,yb,bd}=g, x1=310; + entry(g,0,X).l(0,X*0.4,1).c(0,X*0.1,60,yb,140,yb,0.5).c(232,yb,x1,X*0.32,x1,X*0.62,0.35).c(x1,X*0.8,x1-14,X*0.86,x1-38,X*0.9,0.2).draw(); + g.ball(x1-36,X*0.86,bd*0.8);},{l:0.7,r:0.5}); +idef('w',g=>{const {X,yb,bd,B}=g, m=250, x1=520; + entry(g,0,X).l(0,X*0.4,1).c(0,X*0.1,52,yb,118,yb,0.5).c(190,yb,m-16,X*0.22,m-g.h*0.25,X*0.5,0).draw(); + g.P(m,X,0.5).l(m,X-44,1).l(m,X*0.4,1).c(m,X*0.1,m+52,yb,m+120,yb,0.5).c(m+212,yb,x1,X*0.32,x1,X*0.62,0.35).c(x1,X*0.8,x1-14,X*0.86,x1-38,X*0.9,0.2).draw(); + g.ball(x1-36,X*0.86,bd*0.8);},{l:0.7,r:0.5}); +idef('y',g=>{const {X,D,B,yb,bd,o,h}=g, x1=300, yd=D-o+B/2; + entry(g,0,X).l(0,X*0.34,1).c(0,X*0.08,60,yb,128,yb,0.5).c(200,yb,x1-28,X*0.22,x1-h*0.25,X*0.48,0).draw(); + g.P(x1,X,0.5).l(x1,X-44,1).l(x1,D+160,1).c(x1,D+55,x1-48,yd,x1-130,yd,0.5).c(x1-205,yd,x1-245,D+40,x1-250,D+bd*0.5+10,0.25).draw(); + g.ball(x1-250,D+bd*0.5+10);},{l:0.7,r:0.6}); +idef('p',g=>{const {X,D}=g; entry(g,0,X).l(0,D+g.B/2,1).draw(); g.serif(0,D,1,'b'); + iarch(g,0,330,X).l(330,X*0.4,1).c(330,X*0.1,270,g.yb,190,g.yb,0.5).c(120,g.yb,40,X*0.08,g.h*0.3,X*0.25,0).draw();},{l:0.7,r:0.8}); +idef('b',g=>{const {X,AS}=g; entry(g,0,AS).l(0,X*0.3,1).c(0,X*0.08,80,g.yb,165,g.yb,0.5).c(270,g.yb,330,X*0.2,330,X*0.5,1) + .c(330,X*0.82,280,g.yt,215,g.yt,0.5).c(140,g.yt,60,X*0.78,g.h*0.3,X*0.5,0).draw();},{l:0.7,r:0.8}); +idef('k',g=>{const {X,AS,B,h}=g; entry(g,0,AS).l(0,B/2,1).draw(); + g.P(h*0.3,X*0.32,0).c(80,X*0.6,170,X+6,235,X*0.86,0.4).c(270,X*0.74,200,X*0.56,90,X*0.5,0.6).draw(); + exitT(g.P(120,X*0.52,0.5).c(190,X*0.45,220,X*0.3,220,X*0.2,1),g,220).draw();},{l:0.7,r:0.5}); +idef('s',g=>{const {X,B,yt,yb,h,bd}=g, W=290; + g.P(W-20,X*0.78,0.2).c(W-28,X*0.95,W*0.68,yt,W*0.5,yt,0.5).c(W*0.24,yt,h+2,X*0.92,h+2,X*0.74,1) + .c(h+2,X*0.5,W-h-2,X*0.52,W-h-2,X*0.27,1).c(W-h-2,X*0.08,W*0.76,yb,W*0.5,yb,0.5).c(W*0.27,yb,22,X*0.06,8,X*0.2,0.2).draw(); + g.ball(W-22,X*0.78,bd*0.75); g.ball(10,X*0.22,bd*0.75);},{l:0.8,r:0.8}); +idef('x',g=>{const {X,B,yb,yt}=g; + g.P(-40,X*0.78,0.2).c(-20,X+4,60,yt,100,X*0.7,0.9).l(200,X*0.3,1).c(240,yb+10,300,yb,330,X*0.22,0).draw(); + g.P(-10,X*0.2,0).c(20,yb,90,yb,120,X*0.3,0.3).l(240,X*0.72,0).c(260,yt,320,X+4,330,X*0.82,0).draw();},{l:0.6,r:0.6}); +idef('z',g=>{const {X,A,B}=g; g.P(30,X*0.82,0.1).c(70,X+4,150,X-B*2,330,X-B/2,0).draw(); g.P(330-A*0.4,X-B,0.9).l(30,B,0.9).draw(); + g.P(30,B/2,0).c(150,B*3,250,-B,340,B*2,0.1).draw();},{l:0.7,r:0.7}); + +/* ===================================================================== + SWEEP: centre-lines + nib -> polygons + ===================================================================== */ +function nibFor(p,o,pr){ + const b=o.b!=null?o.b:p.B, a=o.a!=null?o.a:(b+(p.A-b)*clamp(pr,0,1)); + const ang=o.role==='rule'?0:p.ang+(o.turn||0); + return nib(a,b,ang,o.ne||3.2); +} +function circle(x,y,r){const v=[];for(let i=0;i<40;i++){const t=i/40*TAU;v.push([x+r*Math.cos(t),y+r*Math.sin(t)]);}return v;} +function teardrop(g,x,y,r){ // find the stroke end this terminal belongs to; grow a drop out of it + let best=null,bd=r*3.2; + for(const s of g.strokes){const P=s.pts;for(const end of [0,P.length-1]){const q=P[end],d=Math.hypot(q[0]-x,q[1]-y);if(d=0&&i+dir[x*wd+(y+dy-yc)*sl,y+dy]; + const polys=[], strokes=[]; let x0=1e9,x1=-1e9,y0=1e9,y1=-1e9; + const bb=(ux,uy)=>{if(uxx1)x1=ux;if(uyy1)y1=uy;}; + for(const s of g.strokes){ + let prev=null; const out=[]; + for(let i=0;i[q[0]+v[0],q[1]+v[1]]); + if(prev) polys.push(hull(prev.concat(cur))); else if(s.pts.length===1) polys.push(cur); + prev=cur; out.push([q[0],q[1],pr,nb]); + } + strokes.push({pts:out,nodes:s.nodes.map(n=>T(n[0],n[1])),role:s.o.role||'stroke'}); + } + const balls=[]; + for(const [x,y,r] of g.balls){const q=T(x,y);polys.push(circle(q[0],q[1],r));balls.push([q[0],q[1],r]);bb(x*wd-r,y+dy-r);bb(x*wd+r,y+dy+r);} + for(const [x,y,r] of g.drops){const t=teardrop(g,x,y,r);g.fills.push({pts:t.pts,role:t.round?'ball':'drop'});} + const fills=[]; + for(const f of g.fills){const pts=ccw(f.pts.map(([x,y])=>{bb(x*wd,y+dy);return T(x,y);}));polys.push(pts);fills.push({pts,role:f.role});} + if(x0>x1){x0=0;x1=0;y0=0;y1=0;} + let adv,dx; + if(def_.adv!=null&&!mods.prop){adv=def_.adv*wd;dx=def_.empty?0:(adv-(x1-x0))/2-x0;} + else{const L=p.sb*(def_.l==null?1:def_.l)+(mods.extra||0), Rr=p.sb*(def_.r==null?1:def_.r)+(mods.extra||0);adv=x1-x0+L+Rr;dx=L-x0;} + let path=null; + if(typeof Path2D!=='undefined'){path=new Path2D();for(const poly of polys){path.moveTo(poly[0][0],poly[0][1]);for(let i=1;i3&&ch.endsWith('.os')){ // old-style figure: the lining drawing on a shorter or shifted frame + base=ch[0]; const k=OSF[base], X=p.xh; mods={p:{cap:k==='x'?X*1.02:X*1.42},dy:k==='d'?-X*0.42:0,prop:true,wdx:0.97};} + else if(ch.startsWith('sc:')){ // small cap: the capital drawn at x-height, narrower, with heavier hairlines + base=ch.slice(3); mods={sc:true,p:{cap:p.xh*1.1,A:p.A*0.94,B:p.B*1.3,S:p.S*1.2,sl:p.sl*0.8},wdx:0.83,extra:p.sb*0.15};} + const d=(p.ital&&GI[base])||G[base]; if(!d) return null; + gd=compose(d,p,undefined,mods); gd.ch=ch; gd.cursive=!!(p.ital&&GI[base]); + if(/[\u03b1-\u03c9]/.test(base)){gd.adv+=p.sb*0.5;gd.dx+=p.sb*0.25;} + if(cache.size>2500) cache.clear(); + cache.set(key,gd); return gd; +} +function paint(ctx,gd,x,y,s){if(!gd||!gd.path)return;ctx.save();ctx.translate(x+gd.dx*s,y);ctx.scale(s,-s);ctx.fill(gd.path);ctx.restore();} +/* kerning: pairs in font units at 1000/em (roman; italic uses 70 %) */ +const KERN={}; +(function(){const add=(L,R,v)=>{for(const a of L)for(const b of R)KERN[a+b]=v;}; + add('T','aceosru',-62); add('T','yvw',-45); add('T','AΔ',-55); add('T','.,',-70); add('T','J',-40); + add('A','TVWY',-58); add('A','vwy',-34); add('A','\'’',-60); add('AΔ','CGOQ',-18); + add('VW','aeo',-46); add('VW','.,',-70); add('VW','A',-55); add('Y','aeo',-64); add('Y','.,',-80); add('Y','A',-60); + add('L','TVWY',-70); add('L','\'’',-80); add('L','y',-30); add('P','.,',-80); add('P','A',-40); add('F','.,',-60); add('F','a',-20); + add('r','.,',-50); add('vwy','.,',-40); add('f','\'’)?!]',30); add('\'’','s',-25); add('\'’','A',-55); + add('k','eo',-12); add('o','vwxy',-10); add('vwy','o',-10); add('c','k',0); add('R','TVWY',-24);})(); +function shape(text,o){ // -> glyph names: ligatures, old-style figures, small caps + let t=text; + if(o.liga!==false) t=t.replace(/ff/g,'ff').replace(/fi/g,'fi').replace(/fl/g,'fl'); + const out=[]; + for(const ch of t){ + if(o.smcp&&/[a-z]/.test(ch)) out.push('sc:'+ch.toUpperCase()); + else if(o.c2sc&&/[A-Z]/.test(ch)) out.push('sc:'+ch); + else if(o.onum!==false&&/[0-9]/.test(ch)) out.push(ch+'.os'); + else out.push(ch);} + return out; +} +function drawText(ctx,text,x,y,size,p,o){ // returns width; o: measure, track (units), liga, onum, smcp, c2sc, kern + o=o||{}; const s=size/1000; let pen=x, prev=''; + for(const name of shape(text,o)){ + const k=o.kern===false?0:(KERN[prev.slice(-1)+name[0]]||0)*(p.ital?0.7:1); pen+=k*s; + const gd=glyph(name,p); prev=name.startsWith('sc:')?'':name.replace('.os',''); + if(!gd){pen+=300*s;continue;} if(!o.measure) paint(ctx,gd,pen,y,s); pen+=(gd.adv+(o.track||0))*s;} + return pen-x; +} +function measure(text,size,p,o){return drawText(null,text,0,0,size,p,Object.assign({},o||{},{measure:true}));} + +/* ===================================================================== + MATHS SETTER: a tiny TeX subset, enough to show the glyphs in maths. + \frac \sqrt \sum \int ^ _ ' \left( \right) \mathrm \, \; \quad, Greek by name. + Scripts are set at 70 % with a slightly smaller optical size: a little sturdier, a little opener. + ===================================================================== */ +const GREEK={rho:'ρ',alpha:'α',beta:'β',gamma:'γ',delta:'δ',epsilon:'ε',varepsilon:'ε',theta:'θ',lambda:'λ',mu:'μ',pi:'π',sigma:'σ',phi:'φ',varphi:'φ',psi:'ψ',omega:'ω',Gamma:'Γ',Delta:'Δ',Sigma:'Σ',Omega:'Ω'}; +const SYM={infty:['∞','ord'],partial:['∂','ord',1],nabla:['∇','ord'],cdot:['·','bin'],times:['×','bin'],sum:['∑','op'],int:['∫','op'],prime:['′','ord'],lt:['<','rel'],gt:['>','rel']}; +function tokenize(s){const t=[];let i=0;while(i({w:0,h:0,d:0,items:[]}); +function hpack(list){let x=0,h=0,d=0;const items=[];for(const b of list){for(const it of b.items)items.push(Object.assign({},it,{x:it.x+x}));x+=b.w;h=Math.max(h,b.h);d=Math.max(d,b.d);}return {w:x,h,d,items};} +function shiftB(b,dx,dy){return {w:b.w,h:b.h+dy,d:b.d-dy,items:b.items.map(it=>Object.assign({},it,{x:it.x+dx,y:it.y+dy}))};} +function overlay(list,w){let h=0,d=0;const items=[];for(const b of list){items.push(...b.items);h=Math.max(h,b.h);d=Math.max(d,b.d);}return {w:w,h,d,items};} +function spaceB(w){return {w,h:0,d:0,items:[]};} +function mctx(base,size,level,display){ + const opsz=Math.max(6,base.opsz*[1,0.8,0.66][level]); + const o=Object.assign({},base,{opsz}); + return {base,size,level,display,p:params(Object.assign({},o,{ital:false})),pi:params(Object.assign({},o,{ital:true}))}; +} +const sub_=c=>mctx(c.base,c.size*(c.level===0?0.7:0.78),Math.min(2,c.level+1),false); +function gbox(ch,c,ital){const p=ital?c.pi:c.p, gd=glyph(ch,p), s=c.size/1000; + if(!gd) return spaceB(0.3*c.size); + return {w:gd.adv*s,h:Math.max(gd.ymax,0)*s,d:Math.max(-gd.ymin,0)*s,items:[{t:'g',gd,x:0,y:0,s}],ital:ital&&!gd.cursive?0:0};} +function customBox(fn,c,extra){const p=c.p, s=c.size/1000; const gd=compose({fn,l:0.3,r:0.3},p,extra); + return {gd,box:{w:gd.adv*s,h:Math.max(gd.ymax,0)*s,d:Math.max(-gd.ymin,0)*s,items:[{t:'g',gd,x:0,y:0,s}]}};} +function ruleU(p){return Math.max(p.B*1.35,p.A*0.44);} +function frac(num,den,c){const S=c.size, s=S/1000, Rp=ruleU(c.p)*s, ax=AX*s, gap=S*0.11; + const w=Math.max(num.w,den.w)+S*0.16; + const n=shiftB(num,(w-num.w)/2,ax+Rp/2+gap+num.d), dn=shiftB(den,(w-den.w)/2,ax-Rp/2-gap-den.h); + const b=overlay([n,dn],w); b.items.push({t:'r',x:S*0.04,y:ax-Rp/2,w:w-S*0.08,h:Rp}); + return hpack([spaceB(S*0.08),b,spaceB(S*0.08)]);} +function sqrtB(inner,c){const S=c.size, s=S/1000, p=c.p; + const top=Math.max((inner.h+S*0.14)/s+ruleU(p),760), bot=Math.min(-(inner.d+S*0.1)/s,-215); + const bar=inner.w/s+50; let xt=0; + const {gd,box}=customBox((g)=>{xt=radical(g,top,bot,bar);return xt;},c); + const ix=(xt*p.wd+gd.dx+25)*s; + return overlay([box,shiftB(inner,ix,0)],Math.max(box.w,ix+inner.w+S*0.05));} +function delimB(ch,inner,c){const S=c.size, s=S/1000; + const ext=Math.max((inner.h+S*0.12)/s-AX,AX-(-(inner.d+S*0.12)/s),520), top=AX+ext, bot=AX-ext, w=150*Math.sqrt((top-bot)/1020); + const fn=ch==='('?g=>paren(g,0,w,top,bot,1):ch===')'?g=>paren(g,w,w,top,bot,-1):ch==='['?g=>brack(g,0,top,bot,1):g=>brack(g,130,top,bot,-1); + return customBox(fn,c).box;} +function bigop(ch,c){const S=c.size, b=gbox(ch,c,false); b.op=ch; return b;} +function scripts(a,sup,sub,c){const S=c.size, b=a.box; + if(a.cls==='op'&&a.op==='∑'&&c.display){const g=S*0.12, w=Math.max(b.w,sup?sup.w:0,sub?sub.w:0), parts=[shiftB(b,(w-b.w)/2,0)]; + if(sup) parts.push(shiftB(sup,(w-sup.w)/2,b.h+g+sup.d)); if(sub) parts.push(shiftB(sub,(w-sub.w)/2,-(b.d+g+sub.h))); + return overlay(parts,w);} + let u=0,v=0,ox=0,sx=0; + if(a.op==='∫'){u=sup?b.h-sup.h*0.5:0; v=sub?b.d-sub.h*0.5:0; ox=S*0.12; sx=-S*0.12;} + else{u=sup?Math.max(S*0.4,b.h-S*0.28):0; v=sub?Math.max(S*0.16,b.d-S*0.2):0; + if(sup&&sub){const gap=(u-sup.d)-(sub.h-v); if(gap') a={cls:'rel',box:gbox(t,c,false)}; + else if(t==='('||t==='[') a={cls:'open',box:gbox(t,c,false)}; + else if(t===')'||t===']') a={cls:'close',box:gbox(t,c,false)}; + else if(t===',') a={cls:'punct',box:gbox(',',c,false)}; + else if(t==='/') a={cls:'ord',box:gbox('/',c,false)}; + else if(t==="'") a={cls:'ord',box:gbox('′',c,false)}; + else return null; + let sup=null,sub=null; + while(!bare&&pos.i TTFont: + return TTFont(FONTS / "ttf" / f"{Bd.ps_name(Bd.STYLES[key])}.ttf") + + +# ---------------------------------------------------------------- metrics + +def test_sketch_defaults(): + assert E.DEFAULTS["contrast"] == 9 + assert E.DEFAULTS["stress"] == 0 + assert E.DEFAULTS["bracket"] == 0.2 + assert E.DEFAULTS["xh"] == 412 + assert E.DEFAULTS["stem"] == 84 + p = E.params(opsz=72) + assert (p.xh, p.cap, p.asc, p.desc, p.axis, p.os) == (412, 670, 760, -250, 250, 11) + + +def test_optical_sizes(): + assert Bd.TEXT_OPSZ == 10 and Bd.DISPLAY_OPSZ == 60 + text, display = Bd.STYLES["Regular"].p(), Bd.STYLES["Display"].p() + assert text.xh > display.xh > 412 + assert text.B > display.B # sturdier hairlines at text size + assert text.sb > display.sb + + +def test_bold_is_a_true_bold(): + r, b = Bd.STYLES["Regular"].p(), Bd.STYLES["Bold"].p() + assert 1.55 <= b.A / r.A <= 1.65 + assert 1.2 <= b.B / r.B <= 1.4 + assert b.S / r.S < b.A / r.A + + +def test_vertical_metrics_in_fonts(): + for key in STYLES: + f = ttf(key) + os2, hhea = f["OS/2"], f["hhea"] + assert f["head"].unitsPerEm == 1000 + assert os2.sCapHeight == 670 + assert (os2.sTypoAscender, os2.sTypoDescender, os2.sTypoLineGap) == (900, -300, 0) + assert (hhea.ascent, hhea.descent) == (900, -300) + assert os2.fsSelection & (1 << 7) + assert os2.usWinAscent >= 900 and os2.usWinDescent >= 300 + + +# --------------------------------------------------------------- glyph set + +def required() -> set[int]: + cps = set(range(0x20, 0x7F)) + cps |= {ord(c) for c in CS.LATIN1_LETTERS} + cps |= {ord(c) for c in "ßÆæØøŒœ‘’“”‚„‹›«»…•·€©®°—– "} + cps |= {ord(c) for c in CS.GREEK} + cps |= {ord(c) for c in "+−×÷±=≠<>≤≥∑∏∫√∞∂∇′″·∘→←↔∈∉⊂⊆∪∩∀∃∅≈≡∝"} + return cps + + +def test_greek_set(): + low = [c for c in CS.GREEK if c.islower()] + cap = [c for c in CS.GREEK if c.isupper()] + assert len(cap) == 24 and len(low) == 25 and "ς" in low + + +@pytest.mark.parametrize("key", STYLES) +def test_coverage(key): + cmap = ttf(key).getBestCmap() + missing = sorted(required() - set(cmap)) + assert not missing, [hex(c) for c in missing] + + +def test_greek_capitals_drawn_or_referenced(): + drawn = set("ΓΔΘΛΞΠΣΦΨΩ") + for ch in CS.GREEK: + if not ch.isupper(): + continue + g = CS.BY_NAME[CS.glyph_name(ch)] + if ch in drawn: + assert g.key == ch and not g.comps, ch + else: + assert g.comps == [CS.glyph_name(CS.GREEK_LOOKALIKE[ch])], ch + + +def test_accented_letters_are_composites(): + for ch in CS.LATIN1_LETTERS: + g = CS.BY_NAME[CS.glyph_name(ch)] + if ch in "ÆÐרÞßæð÷øþ" or not g.comps: + continue + assert g.comp_kind == "accent" and len(g.comps) == 2 + + +def test_space_and_nbsp(): + f = ttf("Regular") + cmap = f.getBestCmap() + assert f["hmtx"][cmap[0x20]][0] == f["hmtx"][cmap[0xA0]][0] > 0 + + +# ---------------------------------------------------- port of the engine + +SN = None + + +def sketch_node(): + global SN + if SN is None: + import sys + sys.path.insert(0, str(WARP / "tools")) + import sketch_node + if sketch_node.node() is None: + pytest.skip("node is not installed") + SN = sketch_node + return SN + + +PORT_STYLES = ["Regular", "Italic", "Display", "DisplayItalic"] + + +@pytest.mark.parametrize("key", PORT_STYLES) +def test_advances_and_bounds_match_the_sketch_engine(key): + """The port gives the sketch's advances and ink bounds (to 0.01 unit; + the engine caches nibs by keys rounded to 0.01). The maths operators + that share one advance (CS.EQUAL_WIDTH_MATHS) are compared on ink only.""" + sn = sketch_node() + st = Bd.STYLES[key] + prm = {k: v for k, v in dict(st.params).items() if k in ("opsz", "ital")} + chars = CS.SKETCH_CHARS + " " + names = list(chars) + [f"{d}.os" for d in "0123456789"] + [f"sc:{c}" for c in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"] + ref = sn.glyphs(prm, names, polys=False) + p = st.p() + for nm, r in ref.items(): + base, mods = nm, {} + if nm.endswith(".os"): + base, mods = nm[0], E.osf_mods(p, nm[0]) + elif nm.startswith("sc:"): + base, mods = nm[3:], E.sc_mods(p) + d = (GI.get(base) if p.ital else None) or G[base] + sw = E.compose(d, p, mods) + adv, dx = sw.adv, sw.dx + if E.is_greek_lower(base): + adv += p.sb * 0.5 + dx += p.sb * 0.25 + for a, b in ((sw.xmin, r["xmin"]), (sw.xmax, r["xmax"]), (sw.ymin, r["ymin"]), (sw.ymax, r["ymax"])): + assert a == pytest.approx(b, abs=0.01), nm + assert adv == pytest.approx(r["adv"], abs=0.02), nm + # and the font: the advance in the compiled TTF, rounded + if len(nm) == 1 and nm not in CS.EQUAL_WIDTH_MATHS: + name = CS.glyph_name(nm) + if name in CS.BY_NAME and not CS.BY_NAME[name].comps: + assert Bd.drawn(key, name).adv == pytest.approx(r["adv"], abs=0.02), nm + + +def test_font_advances_are_the_loom_advances(): + for key in STYLES: + f = ttf(key) + for name in DRAWN[:: 7]: + assert f["hmtx"][name][0] == Bd.rnd(Bd.drawn(key, name).adv), (key, name) + + +def test_equal_width_maths(): + for key in STYLES: + f = ttf(key) + cmap = f.getBestCmap() + widths = {f["hmtx"][cmap[ord(c)]][0] for c in CS.EQUAL_WIDTH_MATHS} + assert len(widths) == 1, key + + +# --------------------------------------------------------------- outlines + +def contour_area(segs) -> float: + pts = [] + for s in segs: + if s[0] == "line": + pts.append(s[1]) + else: + pts += list(O._bez(s[1], np.linspace(0, 1, 9))[:-1]) + a = np.array(pts) + return O.signed_area(a) + + +@pytest.mark.parametrize("key", ["Regular", "BoldItalic", "Display"]) +def test_outlines_are_clean(key): + for name in DRAWN: + cs = Bd.drawn(key, name).contours + for segs in cs: + for s in segs: + assert O._seg_len(s) >= O.MIN_SEG - 0.5, (name, "short segment") + # outer contours counter-clockwise (PostScript order); the union is + # already overlap-free: removing overlaps again changes nothing + path = pathops.Path() + O.draw(cs, path.getPen(), rnd=False) + before = path.area + path.simplify(fix_winding=True) + assert path.area == pytest.approx(before, rel=0.002), (name, "self-intersection") + if cs: + biggest = max(cs, key=lambda c: abs(contour_area(c))) + assert contour_area(biggest) > 0, (name, "direction") + + +def test_points_per_glyph_are_few(): + counts = [] + for name in DRAWN: + n = sum(1 if s[0] == "line" else 3 for c in Bd.drawn("Regular", name).contours for s in c) + counts.append(n) + assert np.median(counts) < 90 + assert max(counts) < 600 + + +def test_fidelity_report(): + rep = json.loads((PROOFS / "fidelity.json").read_text()) + assert rep["pass"] is True + assert set(rep["styles"]) == set(STYLES) + for st in rep["styles"].values(): + for s, row in st["summary"].items(): + assert row["max"] <= rep["tolerance"][s] + + +# ---------------------------------------------------------------- kerning + +def test_kerning_groups_are_disjoint_and_exist(): + names = set(CS.ORDER) + for side in (K.KERN1, K.KERN2): + seen = set() + for members in side.values(): + for m in members: + assert m in names, m + assert m not in seen, f"{m} is in two groups" + seen.add(m) + for a, b in K.PAIRS: + assert a in K.KERN1 and b in K.KERN2 + + +def test_kerning_starts_from_the_sketch_table(): + from loom.sketch import sketch_kern + sk = sketch_kern() + for pair, v in (("To", -62), ("AV", -58), ("Y.", -80), ("L'", -80), ("P.", -80)): + assert sk[pair] == v + a = next(k for k, m in K.KERN1.items() if CS.glyph_name(pair[0]) in m) + b = next(k for k, m in K.KERN2.items() if CS.glyph_name(pair[1]) in m) + assert K.PAIRS[(a, b)] == v + + +def profile(key, name, side): + """x of the outer ink edge in bands of 20 units (min for left, max for right).""" + d = Bd.drawn(key, name) + out = {} + for poly in d.sweep.polys: + for x, y in poly: + b = int(y // 20) + x += d.dx + if side == "r": + out[b] = max(out.get(b, -1e9), x) + else: + out[b] = min(out.get(b, 1e9), x) + return out, d.adv + + +@pytest.mark.parametrize("key", ["Regular", "Italic", "Bold", "Display"]) +def test_no_kerned_pair_touches(key): + kern = K.kerning(Bd.STYLES[key]) + for (g1, g2), v in kern.items(): + for a in K.KERN1[g1.split(".")[-1]]: + ra, adv = profile(key, a, "r") if not CS.BY_NAME[a].comps else (None, None) + if ra is None: + continue + for b in K.KERN2[g2.split(".")[-1]]: + if CS.BY_NAME[b].comps: + continue + lb, _ = profile(key, b, "l") + gaps = [adv + v + lb[k] - ra[k] for k in ra if k in lb] + if gaps: + assert min(gaps) > 0, (key, a, b, min(gaps)) + + +# --------------------------------------------------------------- features + +def gsub_features(f: TTFont) -> set[str]: + return {r.FeatureTag for r in f["GSUB"].table.FeatureList.FeatureRecord} + + +def single_map(f: TTFont, tag: str) -> dict: + out = {} + gsub = f["GSUB"].table + for r in gsub.FeatureList.FeatureRecord: + if r.FeatureTag != tag: + continue + for li in r.Feature.LookupListIndex: + for st in gsub.LookupList.Lookup[li].SubTable: + st = getattr(st, "ExtSubTable", st) + if hasattr(st, "mapping"): + # smcp is one multiple substitution lookup (ß -> s.sc s.sc) + out.update({k: v[0] if isinstance(v, list) and len(v) == 1 else v + for k, v in st.mapping.items()}) + return out + + +@pytest.mark.parametrize("key", STYLES) +def test_features(key): + f = ttf(key) + feats = gsub_features(f) + assert {"ccmp", "liga", "smcp", "c2sc", "onum", "lnum", "pnum", "tnum"} <= feats + assert {"kern", "mark", "mkmk"} <= {r.FeatureTag for r in f["GPOS"].table.FeatureList.FeatureRecord} + assert single_map(f, "smcp")["a"] == "a.sc" + assert single_map(f, "c2sc")["A"] == "a.sc" + assert single_map(f, "smcp")["alpha"] == "alpha.sc" + assert single_map(f, "onum")["zero"] == "zero.osf" + assert single_map(f, "tnum")["zero.osf"] == "zero.tosf" + assert single_map(f, "pnum")["zero"] == "zero.lf" + lig = {} + gsub = f["GSUB"].table + for r in gsub.FeatureList.FeatureRecord: + if r.FeatureTag == "liga": + for li in r.Feature.LookupListIndex: + for st in gsub.LookupList.Lookup[li].SubTable: + lig.update(getattr(st, "ligatures", {})) + got = {tuple([first] + l.Component): l.LigGlyph for first, ls in lig.items() for l in ls} + for seq, name in ((("f", "i"), "fi"), (("f", "l"), "fl"), (("f", "f"), "f_f"), + (("f", "f", "i"), "f_f_i"), (("f", "f", "l"), "f_f_l")): + assert got.get(seq) == name + + +def test_default_figures_are_lining_tabular(): + f = ttf("Regular") + widths = {f["hmtx"][n][0] for n in CS.FIG_NAMES} + assert len(widths) == 1 + assert len({f["hmtx"][n + ".tosf"][0] for n in CS.FIG_NAMES}) == 1 + assert len({f["hmtx"][n + ".osf"][0] for n in CS.FIG_NAMES}) > 1 + + +# ------------------------------------------------------------------ names + +def test_names_fit(): + for key in STYLES: + f = ttf(key) + fam = f["name"].getDebugName(16) or f["name"].getDebugName(1) + sty = f["name"].getDebugName(17) or f["name"].getDebugName(2) + assert fam.startswith("machine-made Warp") + assert len(fam if sty == "Regular" else f"{fam} {sty}") <= 32 + for rec in f["name"].names: + if rec.nameID in (1, 2, 4, 16, 17): + assert len(rec.toUnicode()) <= 32, (key, rec.nameID, rec.toUnicode()) + assert f["name"].getDebugName(6) == Bd.ps_name(Bd.STYLES[key]) + + +def test_no_bare_warp_family(): + for key in STYLES: + f = ttf(key) + for rec in f["name"].names: + if rec.nameID in (1, 16, 4): + assert rec.toUnicode().startswith("machine-made Warp") + + +def test_italic_angle(): + for key in STYLES: + f = ttf(key) + if Bd.STYLES[key].italic: + assert f["post"].italicAngle == pytest.approx(-math.degrees(math.atan(0.25)), abs=0.01) + else: + assert f["post"].italicAngle == 0 + + +# ---------------------------------------------------------------- reports + +def test_readability_report(): + rep = json.loads((PROOFS / "readability.json").read_text()) + assert rep["gate"]["pass"] is True + assert rep["sizes_px"] == [10, 12, 14] + assert {c["style"] for c in rep["gate"]["checks"]} == {"Regular", "Italic"} + + +def test_sketch_compare_report(): + rep = json.loads((PROOFS / "sketch-compare.json").read_text()) + for key in ("Regular", "Italic", "Display", "DisplayItalic"): + assert rep["styles"][key]["mean_iou"] >= 0.9, key + + +# ------------------------------------------------------------ determinism + +def test_build_is_deterministic(): + Bd.drawn.cache_clear() + a = Bd.build_font("Display") + Bd.drawn.cache_clear() + b = Bd.build_font("Display") + for name in CS.ORDER[::5]: + ga, gb = a[name], b[name] + assert ga.width == gb.width + assert [[(p.x, p.y, p.segmentType) for p in c] for c in ga.contours] == \ + [[(p.x, p.y, p.segmentType) for p in c] for c in gb.contours] + assert a.kerning == b.kerning and a.features.text == b.features.text diff --git a/warp/tools/fidelity.py b/warp/tools/fidelity.py new file mode 100644 index 0000000..796ddf7 --- /dev/null +++ b/warp/tools/fidelity.py @@ -0,0 +1,149 @@ +"""Fidelity of the outlines: compare each font glyph with the pen sweep. + +Usage: + python tools/fidelity.py [--out ../proofs/warp/fidelity.json] [--gate] + +For every drawn glyph of every style, the tool rasterises two shapes, +independently: + +* the sweep: the engine's polygons (nib hulls, discs, teardrops, + brackets), each filled on its own and combined (their union, the + nonzero fill), with Pillow; +* the font glyph: read back from the compiled TTF (quadratic curves, + integer points, after fontmake), its counters cleared. + +Both are filled once at 2048 pixels per em (about 2 pixels per unit) +and averaged down to grey levels at 16, 32, 64 and 128 pixels per em. The difference of a glyph at one size +is the sum of the absolute grey differences divided by the ink of the +sweep. The gate checks the largest difference over all glyphs at each +size against TOLERANCE. + +Apache-2.0. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +import numpy as np +from fontTools.ttLib import TTFont +from PIL import Image, ImageDraw + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent)) + +from loom import build as Bd # noqa: E402 +from loom import charset as CS # noqa: E402 +from loom.raster import PolyPen, fill_glyph, fill_sweep # noqa: E402 + +SIZES = (16, 32, 64, 128) +# Both shapes are filled once at BASE pixels per em (about 2 pixels per +# unit) and averaged down to each size. +BASE = 2048 +# Largest allowed difference (fraction of the ink) per size in pixels per em. +TOLERANCE = {16: 0.05, 32: 0.05, 64: 0.05, 128: 0.05} +FONTS = HERE.parents[1] / "fonts" / "warp" / "ttf" + + +def box_of(polys): + xs = [x for p in polys for x, _ in p] + ys = [y for p in polys for _, y in p] + return min(xs), min(ys), max(xs), max(ys) + + +def raster(polys, box, font): + """Fill at BASE pixels per em; returns a 0..1 array.""" + x0, y0, x1, y1 = box + s = BASE / 1000.0 + block = BASE // min(SIZES) + W = int((x1 - x0) * s) + 2 * block + H = int((y1 - y0) * s) + 2 * block + W, H = W + (-W) % block, H + (-H) % block + tr = lambda p: ((p[0] - x0) * s + block, (y1 - p[1]) * s + block) # noqa: E731 + out = Image.new("L", (W, H), 0) + draw = ImageDraw.Draw(out) + if not font: + # the sweep: every polygon is inked; their union is the nonzero fill + fill_sweep(draw, polys, tr) + else: + # the font glyph has no overlaps: contours at an even nesting depth + # are ink, at an odd depth counters, drawn from the outside in. + # Pillow inks the boundary pixels of a polygon, so a counter keeps + # its boundary inked: both shapes get the same half-pixel edge. + fill_glyph(draw, polys, tr) + return np.asarray(out, dtype=float) / 255.0 + + +def down(a, ppem): + k = BASE // ppem + H, W = a.shape + return a.reshape(H // k, k, W // k, k).mean(axis=(1, 3)) + + +def compare(style_key: str, font: TTFont) -> dict: + gs = font.getGlyphSet() + out = {} + for gl in CS.GLYPHS: + if gl.comps or gl.variant == "notdef": + continue + d = Bd.drawn(style_key, gl.name) + sweep = [[(x + d.dx, y) for x, y in poly] for poly in d.sweep.polys if len(poly) >= 3] + if not sweep: + continue + pen = PolyPen(gs) + gs[gl.name].draw(pen) + if not pen.polys: + out[gl.name] = {str(s): 1.0 for s in SIZES} + continue + bx = box_of(sweep + pen.polys) + res = {} + A = raster(sweep, bx, font=False) + Bm = raster(pen.polys, bx, font=True) + for ppem in SIZES: + a, b = down(A, ppem), down(Bm, ppem) + res[str(ppem)] = round(float(np.abs(a - b).sum() / max(a.sum(), 1e-9)), 5) + out[gl.name] = res + return out + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Compare the Warp outlines with the pen sweep.") + ap.add_argument("--out", default=str(HERE.parents[1] / "proofs" / "warp" / "fidelity.json")) + ap.add_argument("--styles", default=",".join(Bd.STYLES)) + ap.add_argument("--gate", action="store_true") + args = ap.parse_args(argv) + report = {"about": "Difference between each font glyph (compiled TTF) and its pen sweep, as a fraction " + "of the ink, at several pixels per em. See warp/tools/fidelity.py.", + "sizes_ppem": list(SIZES), "base_ppem": BASE, + "tolerance": {str(k): v for k, v in TOLERANCE.items()}, "styles": {}, "pass": True} + for key in args.styles.split(","): + st = Bd.STYLES[key] + font = TTFont(FONTS / f"{Bd.ps_name(st)}.ttf") + glyphs = compare(key, font) + summary = {} + for s in SIZES: + vals = sorted(((v[str(s)], n) for n, v in glyphs.items()), reverse=True) + arr = np.array([v for v, _ in vals]) + summary[str(s)] = {"max": vals[0][0], "max_glyph": vals[0][1], "mean": round(float(arr.mean()), 5), + "p95": round(float(np.percentile(arr, 95)), 5), + "pass": vals[0][0] <= TOLERANCE[s]} + report["pass"] &= summary[str(s)]["pass"] + report["styles"][key] = {"font": f"{Bd.ps_name(st)}.ttf", "glyphs_checked": len(glyphs), + "summary": summary, "glyphs": glyphs} + print(f"{key}: {len(glyphs)} glyphs; " + "; ".join( + f"{s} ppem max {summary[str(s)]['max']:.4f} ({summary[str(s)]['max_glyph']}) mean {summary[str(s)]['mean']:.4f}" + for s in SIZES)) + out = Path(args.out) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(report, indent=1) + "\n") + print(f"Wrote {out}. {'pass' if report['pass'] else 'FAIL'}") + if args.gate and not report["pass"]: + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/warp/tools/finish.py b/warp/tools/finish.py new file mode 100644 index 0000000..320839b --- /dev/null +++ b/warp/tools/finish.py @@ -0,0 +1,78 @@ +"""Finish the compiled Warp fonts. + +Usage: + python tools/finish.py --static FONT.ttf FONT.otf ... + python tools/finish.py --woff2 FONT.ttf ... [--woff2-dir DIR] + +--static: add a STAT table that names the position of the static on the + wght, ital and opsz axes (one value per axis, as the OpenType spec + shows for static fonts). Text styles sit at opsz 10, Display at 60. +--woff2: write a WOFF2 copy. + +Apache-2.0. +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from fontTools.otlLib.builder import buildStatTable +from fontTools.ttLib import TTFont + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from loom.build import DISPLAY_OPSZ, PS_DISPLAY, TEXT_OPSZ # noqa: E402 + + +def finish_static(path: Path) -> None: + font = TTFont(path) + wght = font["OS/2"].usWeightClass + italic = bool(font["OS/2"].fsSelection & 1) + ps = font["name"].getDebugName(6) or "" + display = ps.startswith(PS_DISPLAY + "-") + w = {"value": wght, "name": "Bold" if wght == 700 else "Regular", "flags": 0x2 if wght == 400 else 0} + if wght == 400: + w["linkedValue"] = 700 + i = {"value": 1 if italic else 0, "name": "Italic" if italic else "Roman", "flags": 0 if italic else 0x2} + if not italic: + i["linkedValue"] = 1 + o = {"value": DISPLAY_OPSZ if display else TEXT_OPSZ, "name": "Display" if display else "Text", + "flags": 0 if display else 0x2} + axes = [ + {"tag": "wght", "name": "Weight", "values": [w]}, + {"tag": "opsz", "name": "Optical size", "values": [o]}, + {"tag": "ital", "name": "Italic", "values": [i]}, + ] + buildStatTable(font, axes, elidedFallbackName=2, macNames=False) + font.save(path) + print(f"Added STAT to {path}.") + + +def write_woff2(path: Path, out_dir: Path) -> None: + font = TTFont(path) + font.flavor = "woff2" + out_dir.mkdir(parents=True, exist_ok=True) + out = out_dir / (path.stem + ".woff2") + font.save(out) + print(f"Wrote {out}.") + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description="Finish the compiled Warp fonts.") + ap.add_argument("--static", nargs="*", default=[], type=Path) + ap.add_argument("--woff2", nargs="*", default=[], type=Path) + ap.add_argument("--woff2-dir", type=Path, help="Output folder for WOFF2 files.") + args = ap.parse_args(argv) + if not (args.static or args.woff2): + ap.error("Give at least one font file.") + for p in args.static: + finish_static(p) + for p in args.woff2: + write_woff2(p, args.woff2_dir or p.parent) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/warp/tools/proof.py b/warp/tools/proof.py new file mode 100644 index 0000000..6028186 --- /dev/null +++ b/warp/tools/proof.py @@ -0,0 +1,415 @@ +"""Render the Warp proof sheets into proofs/warp/. + +Usage: + python tools/proof.py [--only NAME,...] + +Sheets: + warp-sketch-vs-font-