├── .gitignore ├── LICENSE.txt ├── build.sh ├── docs ├── build │ ├── main.js │ └── main.js.map ├── css.css ├── fonts │ └── Inter-Regular.otf └── index.html ├── misc └── jsmerge.js ├── package-lock.json ├── package.json └── src ├── abs-svg-path.js ├── adaptive-bezier-curve.js ├── boot.js ├── canvas.js ├── color.js ├── feature-extract.d.ts ├── feature-extract.js ├── featureio.js ├── greiner-hormann.min.js ├── greiner-hormann.min.js.map ├── main.js ├── ml-kern.js ├── normalize-svg-path.js ├── opentype.min.js ├── opentype.min.js.map ├── raycast.js ├── simplify-path.js ├── svg-path-contours.js ├── svg.js ├── tess2.js ├── tess2.min.js ├── tesselate.js ├── tf.min.js ├── tfjs-vis.umd.min.js ├── train.js ├── vec.js └── visualize.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | /_* 3 | /docs/build-dev 4 | node_modules 5 | .DS_Store 6 | .vscode 7 | *~ 8 | *.sublime* 9 | *.build* 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Rasmus Andersson 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | cd "$(dirname "$0")" 3 | 4 | # BUILD_DIR is the intermediate product output directory. 5 | # The contents of this dir is deleted when -O is set, so make sure this is 6 | # not a directory shared with other, non-build product files. 7 | BUILD_DIR=docs/build-dev 8 | 9 | # TMP_BUILD_DIR is a build directory for temporary, intermediate products. 10 | TMP_BUILD_DIR=.build 11 | 12 | # DIST_DIR is the distribution output directory, used when -dist is set. 13 | # The contents of this dir is deleted when -dist is set, so make sure this is 14 | # not a directory shared with other, non-build product files. 15 | DIST_DIR=docs/build 16 | 17 | # INDEX_HTML_DIR is a directory of the index.html file which loads main.js. 18 | # When -w is set, this directory is served with a HTTP server on HTTP_PORT. 19 | # When -dist is set, "?v=\w" in INDEX_HTML_DIR/index.html is rewritten to 20 | # "git rev-parse HEAD". 21 | INDEX_HTML_DIR=docs 22 | 23 | # HTTP_PORT is a TCP/IP port for the web server to bind to. 24 | # Only used when -w is set. 25 | HTTP_PORT=9185 26 | 27 | OPT_HELP=false 28 | OPT_WATCH=false 29 | OPT_OPT=false 30 | OPT_DIST=false 31 | 32 | 33 | # parse args 34 | while [[ $# -gt 0 ]]; do 35 | case "$1" in 36 | -h*|--h*) 37 | OPT_HELP=true 38 | shift 39 | ;; 40 | -w*|--w*) 41 | OPT_WATCH=true 42 | shift 43 | ;; 44 | -O) 45 | OPT_OPT=true 46 | shift 47 | ;; 48 | -dist|--dist) 49 | OPT_DIST=true 50 | OPT_OPT=true 51 | shift 52 | ;; 53 | *) 54 | echo "$0: Unknown option $1" >&2 55 | OPT_HELP=true 56 | shift 57 | ;; 58 | esac 59 | done 60 | if $OPT_DIST && $OPT_WATCH; then 61 | echo "$0: options -w and -dist are not compatible." >&2 62 | OPT_HELP=true 63 | fi 64 | if $OPT_HELP; then 65 | echo "Usage: $0 [options]" 66 | echo "Options:" 67 | echo " -h Show help." 68 | echo " -O Build optimized product." 69 | echo " -w Watch source files for changes and rebuild incrementally." 70 | exit 1 71 | fi 72 | 73 | # check node_modules 74 | if ! [ -d node_modules/esbuild ]; then 75 | echo "npm install" 76 | npm install 77 | fi 78 | 79 | if $OPT_DIST; then 80 | rm -rf "$BUILD_DIR" 81 | fi 82 | 83 | mkdir -p "$BUILD_DIR" "$TMP_BUILD_DIR" 84 | BUILD_DIR_REL=$BUILD_DIR 85 | pushd "$BUILD_DIR" >/dev/null 86 | BUILD_DIR=$PWD 87 | popd >/dev/null 88 | 89 | WATCHFILE=$TMP_BUILD_DIR/build.sh.watch 90 | 91 | function fn_build_go { 92 | GO_SRCDIR=src 93 | pushd "$GO_SRCDIR" >/dev/null 94 | echo "go build $GO_SRCDIR -> $BUILD_DIR_REL/main.wasm" 95 | # tinygo build -o "$BUILD_DIR/main.wasm" -target wasm -no-debug . 96 | GOOS=js GOARCH=wasm go build -o "$BUILD_DIR/main.wasm" 97 | popd >/dev/null 98 | } 99 | 100 | function fn_build_js { 101 | if $OPT_OPT; then 102 | esbuild --platform=node --define:DEBUG=false --sourcemap --minify \ 103 | "--outfile=$TMP_BUILD_DIR/boot.js" src/boot.js 104 | else 105 | esbuild --platform=node --define:DEBUG=true --sourcemap \ 106 | "--outfile=$TMP_BUILD_DIR/boot.js" src/boot.js 107 | fi 108 | if $OPT_OPT; then 109 | esbuild --platform=node --define:DEBUG=false --sourcemap --minify --bundle \ 110 | "--external:@tensorflow/tfjs-node" \ 111 | "--outfile=$TMP_BUILD_DIR/main.js" src/main.js 112 | else 113 | esbuild --platform=node --define:DEBUG=true --sourcemap --bundle \ 114 | "--external:@tensorflow/tfjs-node" \ 115 | "--outfile=$TMP_BUILD_DIR/main.js" src/main.js 116 | fi 117 | node misc/jsmerge.js "$BUILD_DIR/main.js" \ 118 | "$TMP_BUILD_DIR/boot.js" \ 119 | "$TMP_BUILD_DIR/main.js" 120 | } 121 | 122 | function fn_watch_go { 123 | while true; do 124 | fswatch -1 -l 0.2 -r -E --exclude='.+' --include='\.go$' src >/dev/null 125 | if ! [ -f "$WATCHFILE" ] || [ "$(cat "$WATCHFILE")" != "y" ]; then break; fi 126 | set +e ; fn_build_go ; set -e 127 | done 128 | } 129 | 130 | function fn_watch_js { 131 | while true; do 132 | fswatch -1 -l 0.2 -r -E --exclude='.+' --include='\.js$' src >/dev/null 133 | if ! [ -f "$WATCHFILE" ] || [ "$(cat "$WATCHFILE")" != "y" ]; then break; fi 134 | set +e ; fn_build_js ; set -e 135 | done 136 | } 137 | 138 | # fn_build_go & 139 | fn_build_js & 140 | 141 | if $OPT_WATCH; then 142 | echo y > "$WATCHFILE" 143 | 144 | # make sure we can ctrl-c in the while loop 145 | function fn_stop { 146 | echo n > "$WATCHFILE" 147 | exit 148 | } 149 | trap fn_stop SIGINT 150 | 151 | # make sure background processes are killed when this script is stopped 152 | pids=() 153 | function fn_cleanup { 154 | set +e 155 | for pid in "${pids[@]}"; do 156 | kill $pid 2>/dev/null 157 | wait $pid 158 | kill -9 $pid 2>/dev/null 159 | echo n > "$WATCHFILE" 160 | done 161 | set -e 162 | } 163 | trap fn_cleanup EXIT 164 | 165 | # wait for initial build 166 | wait 167 | 168 | # start web server 169 | if (which serve-http >/dev/null); then 170 | serve-http -p $HTTP_PORT -quiet "$INDEX_HTML_DIR" & 171 | pids+=( $! ) 172 | echo "Web server listening at http://localhost:$HTTP_PORT/" 173 | else 174 | echo "Tip: Install serve-http to have a web server run." 175 | echo " npm install -g serve-http" 176 | fi 177 | 178 | echo "Watching source files for changes..." 179 | 180 | # fn_watch_go & 181 | # pids+=( $! ) 182 | 183 | fn_watch_js 184 | else 185 | wait 186 | 187 | if $OPT_DIST; then 188 | rm -rf "$DIST_DIR" 189 | cp -a "$BUILD_DIR" "$DIST_DIR" 190 | 191 | # patch "?v=VERSION" INDEX_HTML_DIR/index.html 192 | VERSION=$(git rev-parse HEAD) 193 | sed -E 's/\?v=[a-f0-9]+/?v='$VERSION'/g' \ 194 | "$INDEX_HTML_DIR/index.html" > "$INDEX_HTML_DIR/.index.html.tmp" 195 | mv -f "$INDEX_HTML_DIR/.index.html.tmp" "$INDEX_HTML_DIR/index.html" 196 | fi 197 | 198 | fi 199 | -------------------------------------------------------------------------------- /docs/css.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Inter'; 3 | font-weight: 100 900; 4 | font-style: normal; 5 | font-named-instance: 'Regular'; 6 | font-display: swap; 7 | src: url("https://rsms.me/inter/font-files/Inter-roman.var.woff2") format("woff2"); 8 | } 9 | @font-face { 10 | font-family: 'Inter'; 11 | font-weight: 100 900; 12 | font-style: italic; 13 | font-named-instance: 'Italic'; 14 | font-display: swap; 15 | src: url("https://rsms.me/inter/font-files/Inter-italic.var.woff2") format("woff2"); 16 | } 17 | 18 | :root { 19 | --touchLockColor: orange; 20 | --touchLockBarHeight: 48px; 21 | } 22 | 23 | body { 24 | font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, 25 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 26 | font-size: 14px; 27 | line-height: 20px; 28 | margin: 4em auto; 29 | padding: 0 2em; 30 | max-width: 800px; 31 | } 32 | 33 | :root.touchInputAvailable body { 34 | font-size:16px; 35 | line-height: 22px; 36 | margin: calc(var(--touchLockBarHeight) + 2em) auto; 37 | padding: 0 1.5em; 38 | } 39 | 40 | body.hidePointer { cursor: none; } 41 | body.hidePointer * { cursor: none; } 42 | 43 | .show-when-predicting { 44 | visibility: hidden; 45 | } 46 | body.predictive .show-when-predicting { 47 | visibility: visible; 48 | } 49 | 50 | b { 51 | font-weight: 600; 52 | } 53 | 54 | h1 { 55 | font-size:1.618rem; 56 | letter-spacing: -0.01em; 57 | font-weight:600; 58 | margin: 1em 0; 59 | } 60 | h2 { 61 | font-size:1.4eem; 62 | letter-spacing: -0.01em; 63 | font-weight:600; 64 | } 65 | h3 { 66 | font-size:1.2eem; 67 | letter-spacing: -0.01em; 68 | font-weight:600; 69 | } 70 | h1 a, h2 a, h3 a { 71 | text-decoration: none; 72 | color: inherit; 73 | } 74 | h1 a:hover, h2 a:hover, h3 a:hover { 75 | text-decoration: underline; 76 | } 77 | 78 | code { 79 | margin: 0.5em 0; 80 | display: block; 81 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 82 | } 83 | 84 | label { 85 | font-weight: 500; 86 | user-select: none; 87 | } 88 | 89 | blockquote { 90 | padding: 0.5em 1.5em; 91 | margin: 1.5em 0; 92 | margin-left: 6px; 93 | border-left: 4px solid #ccc; 94 | } 95 | -------------------------------------------------------------------------------- /docs/fonts/Inter-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsms/ml-kern/ebc2c1cb13c3a2c5873439d3467db145b2266184/docs/fonts/Inter-Regular.otf -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ml-kern 6 | 7 | 9 | 11 | 12 | 14 | 16 | 17 | 40 | 64 | 65 | 66 |
a
67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /misc/jsmerge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs') 3 | const Path = require('path') 4 | const { SourceMapConsumer, SourceMapGenerator } = require('source-map') 5 | 6 | const log = console.log.bind(console) 7 | 8 | function usage() { 9 | console.error(`usage: jsmerge.js [ ...]`) 10 | console.error(`Merges all files together with sourcemap`) 11 | process.exit(1) 12 | } 13 | 14 | async function main(argv) { 15 | if (argv.some(s => s.startsWith("-h") || s.startsWith("--h"))) { 16 | return usage() 17 | } 18 | if (argv.length < 4) { 19 | return usage() 20 | } 21 | let outfile = argv[2] 22 | let infiles = argv.slice(3) 23 | await mergeFiles(infiles, outfile, "../src") 24 | } 25 | 26 | const sourceMappingURLBuf1 = Buffer.from("//# sourceMappingURL=", "utf8") 27 | const sourceMappingURLBuf2 = Buffer.from("//#sourceMappingURL=", "utf8") 28 | 29 | 30 | async function loadSourceMap(filename, source, isSecondLevel) { 31 | let m = /(?:\n|;|^)\/\/#\s*sourceMappingURL\s*=([^\r\n]+)/g.exec(source) 32 | if (m) { 33 | let sourceMapFile = Path.resolve(Path.dirname(filename), m[1]) 34 | let rawSourceMap = JSON.parse(fs.readFileSync(sourceMapFile, "utf8")) 35 | rawSourceMap.file = filename 36 | return await new SourceMapConsumer(rawSourceMap) 37 | } else if (!isSecondLevel) { 38 | log(`no sourcemap found in ${filename}`) 39 | } // else: This is okay and will likely fail for source-level files 40 | return null 41 | } 42 | 43 | 44 | async function loadSourceMapInFile(filename) { 45 | let contents = await fs.promises.readFile(filename, "utf8") 46 | return await loadSourceMap(filename, contents, /*isSecondLevel*/true) 47 | } 48 | 49 | 50 | async function loadSourceFile(filename) { 51 | let contents = await fs.promises.readFile(filename, "utf8") 52 | let map = await loadSourceMap(filename, contents) 53 | if (map) { 54 | contents = stripSourceMappingURLComment(contents) 55 | } 56 | return { 57 | filename, 58 | contents, 59 | map, 60 | } 61 | } 62 | 63 | 64 | async function mergeFiles(infiles, outfile, sourceRoot) { 65 | let files = await Promise.all(infiles.map(loadSourceFile)) 66 | 67 | log("jsmerge", files.map(f => f.filename).join(" + "), "=>", outfile) 68 | 69 | 70 | // relative path from outfile's perspective 71 | const outdir = Path.resolve(outfile, "..") 72 | function reloutpath(abspath) { 73 | return Path.relative(outdir, abspath) 74 | } 75 | 76 | 77 | // Load source maps for second-degree source files. 78 | // This resolves source positions for original source files when intermediate build 79 | // products are used, like for instance with TypeScript. 80 | let secondDegreeMaps = new Map() 81 | let secondDegreeMapPromises = [] 82 | let sourceFilenames = new Map() 83 | let sourceFilenames2 = new Map() 84 | for (let f of files) { 85 | // note: extra ".." since esbuild does not change source paths 86 | // TODO: read sourceRoot is available (note: esbuild doesn't write one.) 87 | const dir = Path.resolve(f.filename, "..", "..") 88 | 89 | f.map.eachMapping(m => { 90 | if (m.source && !secondDegreeMaps.has(m.source)) { 91 | 92 | let absSourcePath = Path.resolve(dir, m.source) 93 | sourceFilenames.set(m.source, reloutpath(absSourcePath)) 94 | // log("* ", m.source, "=>", reloutpath(absSourcePath)) 95 | 96 | secondDegreeMaps.set(m.source, {}) 97 | secondDegreeMapPromises.push(loadSourceMapInFile(m.source).then(map2 => { 98 | secondDegreeMaps.set(m.source, map2) 99 | if (map2) { 100 | let dir2 = Path.resolve(absSourcePath, "..") 101 | map2.eachMapping(m2 => { 102 | if (!sourceFilenames2.has(m2.source)) { 103 | 104 | let absSourcePath2 = Path.resolve(dir2, m2.source) 105 | sourceFilenames2.set(m2.source, reloutpath(absSourcePath2)) 106 | // log("**", m2.source, "=>", reloutpath(absSourcePath2)) 107 | 108 | } 109 | }) 110 | } 111 | })) 112 | } 113 | }) 114 | } 115 | await Promise.all(secondDegreeMapPromises) 116 | 117 | 118 | // this will become the unified source map 119 | let outmap = new SourceMapGenerator({ file: outfile }) 120 | let lineoffs = 0 121 | let outsource = "" 122 | 123 | // copy mappings from input files to unified outmap 124 | for (let f of files) { 125 | f.map.eachMapping(m => { 126 | let mapping = { 127 | source: sourceFilenames.get(m.source) || m.source, 128 | generated: { 129 | line: m.generatedLine + lineoffs, 130 | column: m.generatedColumn, 131 | }, 132 | original: { 133 | line: m.originalLine, 134 | column: m.originalColumn, 135 | }, 136 | name: m.name, 137 | } 138 | 139 | let secondMap = secondDegreeMaps.get(m.source) 140 | if (secondMap) { 141 | // use second-degree source mapping 142 | let orig = secondMap.originalPositionFor({ 143 | line: m.originalLine, 144 | column: m.originalColumn, 145 | }) 146 | if (orig && orig.line) { 147 | mapping.original = { line: orig.line, column: orig.column } 148 | mapping.source = sourceFilenames2.get(orig.source) || orig.source 149 | mapping.name = orig.name 150 | } 151 | } 152 | 153 | outmap.addMapping(mapping) 154 | }) 155 | 156 | let contents = stringWithTrailingNewline(f.contents) 157 | lineoffs += countLines(contents) 158 | outsource += contents 159 | } 160 | 161 | let outmapfile = outfile + ".map" 162 | outsource = outsource.trimEnd("\n") + `\n//# sourceMappingURL=${Path.basename(outmapfile)}\n` 163 | 164 | return Promise.all([ 165 | fs.promises.writeFile(outmapfile, outmap.toString(), {encoding:"utf8"}), 166 | fs.promises.writeFile(outfile, outsource, {encoding:"utf8"}), 167 | ]) 168 | } 169 | 170 | 171 | function countLines(s) { 172 | let lines = 0 173 | for (let i = 0; i < s.length; i++) { 174 | if (s.charCodeAt(i) == 0xA) { 175 | lines++ 176 | } 177 | } 178 | return lines 179 | } 180 | 181 | 182 | function stringWithTrailingNewline(s) { 183 | if (s.charCodeAt(s.length-1) != 0xA) { 184 | s = s + "\n" 185 | } 186 | return s 187 | } 188 | 189 | 190 | function stripSourceMappingURLComment(s) { 191 | return s.replace(/\n\/\/#\s*sourceMappingURL\s*=[^\r\n]*/g, "\n") 192 | } 193 | 194 | 195 | main(process.argv).catch(err => { 196 | console.error(err.stack||String(err)) 197 | process.exit(1) 198 | }) 199 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ml-kern", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@tensorflow/tfjs": { 8 | "version": "1.7.4", 9 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-1.7.4.tgz", 10 | "integrity": "sha512-XWGwRQ/ECEoQacd74JY/dmbLdnMpwtq3H8tls45dQ+GJ553Advir1FDo/aQt0Yr6fTimQDeiOIG4Mcb5KduP/w==", 11 | "requires": { 12 | "@tensorflow/tfjs-converter": "1.7.4", 13 | "@tensorflow/tfjs-core": "1.7.4", 14 | "@tensorflow/tfjs-data": "1.7.4", 15 | "@tensorflow/tfjs-layers": "1.7.4" 16 | } 17 | }, 18 | "@tensorflow/tfjs-converter": { 19 | "version": "1.7.4", 20 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.7.4.tgz", 21 | "integrity": "sha512-B/Ux9I3osI0CXoESGR0Xe5C6BsEfC04+g2xn5zVaW9KEuVEnGEgnuBQxgijRFzkqTwoyLv4ptAmjyIghVARX0Q==" 22 | }, 23 | "@tensorflow/tfjs-core": { 24 | "version": "1.7.4", 25 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.7.4.tgz", 26 | "integrity": "sha512-3G4VKJ6nPs7iCt6gs3bjRj8chihKrYWenf63R0pm7D9MhlrVoX/tpN4LYVMGgBL7jHPxMLKdOkoAZJrn/J88HQ==", 27 | "requires": { 28 | "@types/offscreencanvas": "~2019.3.0", 29 | "@types/seedrandom": "2.4.27", 30 | "@types/webgl-ext": "0.0.30", 31 | "@types/webgl2": "0.0.4", 32 | "node-fetch": "~2.1.2", 33 | "seedrandom": "2.4.3" 34 | } 35 | }, 36 | "@tensorflow/tfjs-data": { 37 | "version": "1.7.4", 38 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-1.7.4.tgz", 39 | "integrity": "sha512-WFYG9wWjNDi62x6o3O20Q0XJxToCw2J4/fBEXiK/Gr0hIqVhl2oLQ1OjTWq7O08NUxM6BRzuG+ra3gWYdQUzOw==", 40 | "requires": { 41 | "@types/node-fetch": "^2.1.2", 42 | "node-fetch": "~2.1.2" 43 | } 44 | }, 45 | "@tensorflow/tfjs-layers": { 46 | "version": "1.7.4", 47 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-1.7.4.tgz", 48 | "integrity": "sha512-5/K8Z8RBfXsucL6EaSeb3/8jB/I8oPaaXkxwKVsBPQ+u6lB6LEtSKzeiFc57nDr5OMtVaUZV+pKDNEzP0RUQlg==" 49 | }, 50 | "@tensorflow/tfjs-node": { 51 | "version": "1.7.4", 52 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-node/-/tfjs-node-1.7.4.tgz", 53 | "integrity": "sha512-xcK2NMJI2eOrvDBMrT5RjJSXZPK7B/SFvoSTS+ycpoiPAooeFsDuOcj4YzsgYlSBRyVl9qKHaNn3rWhrTWwG+Q==", 54 | "requires": { 55 | "@tensorflow/tfjs": "1.7.4", 56 | "@tensorflow/tfjs-core": "1.7.4", 57 | "adm-zip": "^0.4.11", 58 | "google-protobuf": "^3.9.2", 59 | "https-proxy-agent": "^2.2.1", 60 | "node-pre-gyp": "0.14.0", 61 | "progress": "^2.0.0", 62 | "rimraf": "^2.6.2", 63 | "tar": "^4.4.6" 64 | } 65 | }, 66 | "@types/node": { 67 | "version": "13.13.4", 68 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz", 69 | "integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==" 70 | }, 71 | "@types/node-fetch": { 72 | "version": "2.5.7", 73 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz", 74 | "integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==", 75 | "requires": { 76 | "@types/node": "*", 77 | "form-data": "^3.0.0" 78 | } 79 | }, 80 | "@types/offscreencanvas": { 81 | "version": "2019.3.0", 82 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", 83 | "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==" 84 | }, 85 | "@types/seedrandom": { 86 | "version": "2.4.27", 87 | "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz", 88 | "integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE=" 89 | }, 90 | "@types/webgl-ext": { 91 | "version": "0.0.30", 92 | "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", 93 | "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==" 94 | }, 95 | "@types/webgl2": { 96 | "version": "0.0.4", 97 | "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz", 98 | "integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw==" 99 | }, 100 | "abbrev": { 101 | "version": "1.1.1", 102 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", 103 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" 104 | }, 105 | "adm-zip": { 106 | "version": "0.4.14", 107 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz", 108 | "integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g==" 109 | }, 110 | "agent-base": { 111 | "version": "4.3.0", 112 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", 113 | "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", 114 | "requires": { 115 | "es6-promisify": "^5.0.0" 116 | } 117 | }, 118 | "ansi-regex": { 119 | "version": "5.0.0", 120 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 121 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 122 | "dev": true 123 | }, 124 | "aproba": { 125 | "version": "1.2.0", 126 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 127 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 128 | }, 129 | "are-we-there-yet": { 130 | "version": "1.1.5", 131 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 132 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 133 | "requires": { 134 | "delegates": "^1.0.0", 135 | "readable-stream": "^2.0.6" 136 | } 137 | }, 138 | "asynckit": { 139 | "version": "0.4.0", 140 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 141 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 142 | }, 143 | "balanced-match": { 144 | "version": "1.0.0", 145 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 146 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 147 | }, 148 | "brace-expansion": { 149 | "version": "1.1.11", 150 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 151 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 152 | "requires": { 153 | "balanced-match": "^1.0.0", 154 | "concat-map": "0.0.1" 155 | } 156 | }, 157 | "buffer-from": { 158 | "version": "1.1.1", 159 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 160 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 161 | "dev": true 162 | }, 163 | "chownr": { 164 | "version": "1.1.4", 165 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 166 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 167 | }, 168 | "cli-progress": { 169 | "version": "3.8.1", 170 | "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.8.1.tgz", 171 | "integrity": "sha512-tzIOjWPHnXWCS6MNFOVE78mXtzBf/Z4//mfwi33eQ72cifLwXq3hsOGAukocf1Fygp7Zhylept1sUpN43qQEbg==", 172 | "dev": true, 173 | "requires": { 174 | "colors": "^1.1.2", 175 | "string-width": "^4.2.0" 176 | } 177 | }, 178 | "code-point-at": { 179 | "version": "1.1.0", 180 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 181 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 182 | }, 183 | "colors": { 184 | "version": "1.4.0", 185 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", 186 | "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", 187 | "dev": true 188 | }, 189 | "combined-stream": { 190 | "version": "1.0.8", 191 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 192 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 193 | "requires": { 194 | "delayed-stream": "~1.0.0" 195 | } 196 | }, 197 | "concat-map": { 198 | "version": "0.0.1", 199 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 200 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 201 | }, 202 | "console-control-strings": { 203 | "version": "1.1.0", 204 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 205 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 206 | }, 207 | "core-util-is": { 208 | "version": "1.0.2", 209 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 210 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 211 | }, 212 | "debug": { 213 | "version": "3.2.6", 214 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", 215 | "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", 216 | "requires": { 217 | "ms": "^2.1.1" 218 | } 219 | }, 220 | "deep-extend": { 221 | "version": "0.6.0", 222 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 223 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 224 | }, 225 | "delayed-stream": { 226 | "version": "1.0.0", 227 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 228 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 229 | }, 230 | "delegates": { 231 | "version": "1.0.0", 232 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 233 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 234 | }, 235 | "detect-libc": { 236 | "version": "1.0.3", 237 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 238 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 239 | }, 240 | "emoji-regex": { 241 | "version": "8.0.0", 242 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 243 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 244 | "dev": true 245 | }, 246 | "es6-promise": { 247 | "version": "4.2.8", 248 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 249 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 250 | }, 251 | "es6-promisify": { 252 | "version": "5.0.0", 253 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 254 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 255 | "requires": { 256 | "es6-promise": "^4.0.3" 257 | } 258 | }, 259 | "esbuild": { 260 | "version": "0.1.18", 261 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.1.18.tgz", 262 | "integrity": "sha512-NPFmm0Sp4KcgwsBIITO2Sxqvycfwad6oo0rTdX0fUq04wJhr5SushajVUWdT9E3QceVkyGlSEunskdJmrmDgAg==", 263 | "dev": true 264 | }, 265 | "form-data": { 266 | "version": "3.0.0", 267 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", 268 | "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", 269 | "requires": { 270 | "asynckit": "^0.4.0", 271 | "combined-stream": "^1.0.8", 272 | "mime-types": "^2.1.12" 273 | } 274 | }, 275 | "fs-minipass": { 276 | "version": "1.2.7", 277 | "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", 278 | "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", 279 | "requires": { 280 | "minipass": "^2.6.0" 281 | } 282 | }, 283 | "fs.realpath": { 284 | "version": "1.0.0", 285 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 286 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" 287 | }, 288 | "gauge": { 289 | "version": "2.7.4", 290 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 291 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 292 | "requires": { 293 | "aproba": "^1.0.3", 294 | "console-control-strings": "^1.0.0", 295 | "has-unicode": "^2.0.0", 296 | "object-assign": "^4.1.0", 297 | "signal-exit": "^3.0.0", 298 | "string-width": "^1.0.1", 299 | "strip-ansi": "^3.0.1", 300 | "wide-align": "^1.1.0" 301 | }, 302 | "dependencies": { 303 | "ansi-regex": { 304 | "version": "2.1.1", 305 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 306 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 307 | }, 308 | "is-fullwidth-code-point": { 309 | "version": "1.0.0", 310 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 311 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 312 | "requires": { 313 | "number-is-nan": "^1.0.0" 314 | } 315 | }, 316 | "string-width": { 317 | "version": "1.0.2", 318 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 319 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 320 | "requires": { 321 | "code-point-at": "^1.0.0", 322 | "is-fullwidth-code-point": "^1.0.0", 323 | "strip-ansi": "^3.0.0" 324 | } 325 | }, 326 | "strip-ansi": { 327 | "version": "3.0.1", 328 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 329 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 330 | "requires": { 331 | "ansi-regex": "^2.0.0" 332 | } 333 | } 334 | } 335 | }, 336 | "glob": { 337 | "version": "7.1.6", 338 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 339 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 340 | "requires": { 341 | "fs.realpath": "^1.0.0", 342 | "inflight": "^1.0.4", 343 | "inherits": "2", 344 | "minimatch": "^3.0.4", 345 | "once": "^1.3.0", 346 | "path-is-absolute": "^1.0.0" 347 | } 348 | }, 349 | "google-protobuf": { 350 | "version": "3.11.4", 351 | "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.11.4.tgz", 352 | "integrity": "sha512-lL6b04rDirurUBOgsY2+LalI6Evq8eH5TcNzi7TYQ3BsIWelT0KSOQSBsXuavEkNf+odQU6c0lgz3UsZXeNX9Q==" 353 | }, 354 | "has-unicode": { 355 | "version": "2.0.1", 356 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 357 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 358 | }, 359 | "https-proxy-agent": { 360 | "version": "2.2.4", 361 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", 362 | "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", 363 | "requires": { 364 | "agent-base": "^4.3.0", 365 | "debug": "^3.1.0" 366 | } 367 | }, 368 | "iconv-lite": { 369 | "version": "0.4.24", 370 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 371 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 372 | "requires": { 373 | "safer-buffer": ">= 2.1.2 < 3" 374 | } 375 | }, 376 | "ignore-walk": { 377 | "version": "3.0.3", 378 | "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", 379 | "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", 380 | "requires": { 381 | "minimatch": "^3.0.4" 382 | } 383 | }, 384 | "inflight": { 385 | "version": "1.0.6", 386 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 387 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 388 | "requires": { 389 | "once": "^1.3.0", 390 | "wrappy": "1" 391 | } 392 | }, 393 | "inherits": { 394 | "version": "2.0.4", 395 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 396 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 397 | }, 398 | "ini": { 399 | "version": "1.3.5", 400 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", 401 | "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" 402 | }, 403 | "is-fullwidth-code-point": { 404 | "version": "3.0.0", 405 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 406 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 407 | "dev": true 408 | }, 409 | "isarray": { 410 | "version": "1.0.0", 411 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 412 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 413 | }, 414 | "mime-db": { 415 | "version": "1.44.0", 416 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 417 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 418 | }, 419 | "mime-types": { 420 | "version": "2.1.27", 421 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 422 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 423 | "requires": { 424 | "mime-db": "1.44.0" 425 | } 426 | }, 427 | "minimatch": { 428 | "version": "3.0.4", 429 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 430 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 431 | "requires": { 432 | "brace-expansion": "^1.1.7" 433 | } 434 | }, 435 | "minimist": { 436 | "version": "1.2.5", 437 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 438 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 439 | }, 440 | "minipass": { 441 | "version": "2.9.0", 442 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", 443 | "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", 444 | "requires": { 445 | "safe-buffer": "^5.1.2", 446 | "yallist": "^3.0.0" 447 | } 448 | }, 449 | "minizlib": { 450 | "version": "1.3.3", 451 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", 452 | "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", 453 | "requires": { 454 | "minipass": "^2.9.0" 455 | } 456 | }, 457 | "mkdirp": { 458 | "version": "0.5.5", 459 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 460 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 461 | "requires": { 462 | "minimist": "^1.2.5" 463 | } 464 | }, 465 | "ms": { 466 | "version": "2.1.2", 467 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 468 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 469 | }, 470 | "needle": { 471 | "version": "2.4.1", 472 | "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.1.tgz", 473 | "integrity": "sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==", 474 | "requires": { 475 | "debug": "^3.2.6", 476 | "iconv-lite": "^0.4.4", 477 | "sax": "^1.2.4" 478 | } 479 | }, 480 | "node-fetch": { 481 | "version": "2.1.2", 482 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", 483 | "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" 484 | }, 485 | "node-pre-gyp": { 486 | "version": "0.14.0", 487 | "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", 488 | "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", 489 | "requires": { 490 | "detect-libc": "^1.0.2", 491 | "mkdirp": "^0.5.1", 492 | "needle": "^2.2.1", 493 | "nopt": "^4.0.1", 494 | "npm-packlist": "^1.1.6", 495 | "npmlog": "^4.0.2", 496 | "rc": "^1.2.7", 497 | "rimraf": "^2.6.1", 498 | "semver": "^5.3.0", 499 | "tar": "^4.4.2" 500 | } 501 | }, 502 | "nopt": { 503 | "version": "4.0.3", 504 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", 505 | "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", 506 | "requires": { 507 | "abbrev": "1", 508 | "osenv": "^0.1.4" 509 | } 510 | }, 511 | "npm-bundled": { 512 | "version": "1.1.1", 513 | "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", 514 | "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", 515 | "requires": { 516 | "npm-normalize-package-bin": "^1.0.1" 517 | } 518 | }, 519 | "npm-normalize-package-bin": { 520 | "version": "1.0.1", 521 | "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", 522 | "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" 523 | }, 524 | "npm-packlist": { 525 | "version": "1.4.8", 526 | "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", 527 | "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", 528 | "requires": { 529 | "ignore-walk": "^3.0.1", 530 | "npm-bundled": "^1.0.1", 531 | "npm-normalize-package-bin": "^1.0.1" 532 | } 533 | }, 534 | "npmlog": { 535 | "version": "4.1.2", 536 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 537 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 538 | "requires": { 539 | "are-we-there-yet": "~1.1.2", 540 | "console-control-strings": "~1.1.0", 541 | "gauge": "~2.7.3", 542 | "set-blocking": "~2.0.0" 543 | } 544 | }, 545 | "number-is-nan": { 546 | "version": "1.0.1", 547 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 548 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 549 | }, 550 | "object-assign": { 551 | "version": "4.1.1", 552 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 553 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 554 | }, 555 | "once": { 556 | "version": "1.4.0", 557 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 558 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 559 | "requires": { 560 | "wrappy": "1" 561 | } 562 | }, 563 | "opentype.js": { 564 | "version": "1.3.3", 565 | "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.3.tgz", 566 | "integrity": "sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==", 567 | "dev": true, 568 | "requires": { 569 | "string.prototype.codepointat": "^0.2.1", 570 | "tiny-inflate": "^1.0.3" 571 | } 572 | }, 573 | "os-homedir": { 574 | "version": "1.0.2", 575 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 576 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" 577 | }, 578 | "os-tmpdir": { 579 | "version": "1.0.2", 580 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 581 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 582 | }, 583 | "osenv": { 584 | "version": "0.1.5", 585 | "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", 586 | "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", 587 | "requires": { 588 | "os-homedir": "^1.0.0", 589 | "os-tmpdir": "^1.0.0" 590 | } 591 | }, 592 | "path-is-absolute": { 593 | "version": "1.0.1", 594 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 595 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 596 | }, 597 | "process-nextick-args": { 598 | "version": "2.0.1", 599 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 600 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 601 | }, 602 | "progress": { 603 | "version": "2.0.3", 604 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 605 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" 606 | }, 607 | "rc": { 608 | "version": "1.2.8", 609 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 610 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 611 | "requires": { 612 | "deep-extend": "^0.6.0", 613 | "ini": "~1.3.0", 614 | "minimist": "^1.2.0", 615 | "strip-json-comments": "~2.0.1" 616 | } 617 | }, 618 | "readable-stream": { 619 | "version": "2.3.7", 620 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 621 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 622 | "requires": { 623 | "core-util-is": "~1.0.0", 624 | "inherits": "~2.0.3", 625 | "isarray": "~1.0.0", 626 | "process-nextick-args": "~2.0.0", 627 | "safe-buffer": "~5.1.1", 628 | "string_decoder": "~1.1.1", 629 | "util-deprecate": "~1.0.1" 630 | } 631 | }, 632 | "rimraf": { 633 | "version": "2.7.1", 634 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 635 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 636 | "requires": { 637 | "glob": "^7.1.3" 638 | } 639 | }, 640 | "safe-buffer": { 641 | "version": "5.1.2", 642 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 643 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 644 | }, 645 | "safer-buffer": { 646 | "version": "2.1.2", 647 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 648 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 649 | }, 650 | "sax": { 651 | "version": "1.2.4", 652 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 653 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 654 | }, 655 | "seedrandom": { 656 | "version": "2.4.3", 657 | "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz", 658 | "integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=" 659 | }, 660 | "semver": { 661 | "version": "5.7.1", 662 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 663 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 664 | }, 665 | "set-blocking": { 666 | "version": "2.0.0", 667 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 668 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 669 | }, 670 | "signal-exit": { 671 | "version": "3.0.3", 672 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 673 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 674 | }, 675 | "source-map": { 676 | "version": "0.7.3", 677 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 678 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", 679 | "dev": true 680 | }, 681 | "source-map-support": { 682 | "version": "0.5.19", 683 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 684 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 685 | "dev": true, 686 | "requires": { 687 | "buffer-from": "^1.0.0", 688 | "source-map": "^0.6.0" 689 | }, 690 | "dependencies": { 691 | "source-map": { 692 | "version": "0.6.1", 693 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 694 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 695 | "dev": true 696 | } 697 | } 698 | }, 699 | "string-width": { 700 | "version": "4.2.0", 701 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 702 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 703 | "dev": true, 704 | "requires": { 705 | "emoji-regex": "^8.0.0", 706 | "is-fullwidth-code-point": "^3.0.0", 707 | "strip-ansi": "^6.0.0" 708 | } 709 | }, 710 | "string.prototype.codepointat": { 711 | "version": "0.2.1", 712 | "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", 713 | "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", 714 | "dev": true 715 | }, 716 | "string_decoder": { 717 | "version": "1.1.1", 718 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 719 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 720 | "requires": { 721 | "safe-buffer": "~5.1.0" 722 | } 723 | }, 724 | "strip-ansi": { 725 | "version": "6.0.0", 726 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 727 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 728 | "dev": true, 729 | "requires": { 730 | "ansi-regex": "^5.0.0" 731 | } 732 | }, 733 | "strip-json-comments": { 734 | "version": "2.0.1", 735 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 736 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 737 | }, 738 | "tar": { 739 | "version": "4.4.13", 740 | "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", 741 | "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", 742 | "requires": { 743 | "chownr": "^1.1.1", 744 | "fs-minipass": "^1.2.5", 745 | "minipass": "^2.8.6", 746 | "minizlib": "^1.2.1", 747 | "mkdirp": "^0.5.0", 748 | "safe-buffer": "^5.1.2", 749 | "yallist": "^3.0.3" 750 | } 751 | }, 752 | "tiny-inflate": { 753 | "version": "1.0.3", 754 | "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", 755 | "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", 756 | "dev": true 757 | }, 758 | "util-deprecate": { 759 | "version": "1.0.2", 760 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 761 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 762 | }, 763 | "wide-align": { 764 | "version": "1.1.3", 765 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 766 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 767 | "requires": { 768 | "string-width": "^1.0.2 || 2" 769 | }, 770 | "dependencies": { 771 | "ansi-regex": { 772 | "version": "3.0.0", 773 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 774 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 775 | }, 776 | "is-fullwidth-code-point": { 777 | "version": "2.0.0", 778 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 779 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 780 | }, 781 | "string-width": { 782 | "version": "2.1.1", 783 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 784 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 785 | "requires": { 786 | "is-fullwidth-code-point": "^2.0.0", 787 | "strip-ansi": "^4.0.0" 788 | } 789 | }, 790 | "strip-ansi": { 791 | "version": "4.0.0", 792 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 793 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 794 | "requires": { 795 | "ansi-regex": "^3.0.0" 796 | } 797 | } 798 | } 799 | }, 800 | "wrappy": { 801 | "version": "1.0.2", 802 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 803 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 804 | }, 805 | "yallist": { 806 | "version": "3.1.1", 807 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", 808 | "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" 809 | } 810 | } 811 | } 812 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ml-kern", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/main.js", 6 | "directories": { 7 | "doc": "docs" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rsms/ml-kern.git" 15 | }, 16 | "author": "rsms", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/rsms/ml-kern/issues" 20 | }, 21 | "homepage": "https://github.com/rsms/ml-kern#readme", 22 | "devDependencies": { 23 | "cli-progress": "^3.8.1", 24 | "esbuild": "^0.1.18", 25 | "opentype.js": "^1.3.3", 26 | "source-map": "^0.7.3", 27 | "source-map-support": "^0.5.19" 28 | }, 29 | "dependencies": { 30 | "@tensorflow/tfjs-node": "^1.7.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/abs-svg-path.js: -------------------------------------------------------------------------------- 1 | /** 2 | * absolutize redefines `path` with absolute coordinates 3 | * 4 | * @param {Array} path 5 | * @return {Array} 6 | */ 7 | export function absSvgPath(path){ 8 | var startX = 0 9 | var startY = 0 10 | var x = 0 11 | var y = 0 12 | 13 | return path.map(function(seg){ 14 | seg = seg.slice() 15 | var type = seg[0] 16 | var command = type.toUpperCase() 17 | 18 | // is relative 19 | if (type != command) { 20 | seg[0] = command 21 | switch (type) { 22 | case 'a': 23 | seg[6] += x 24 | seg[7] += y 25 | break 26 | case 'v': 27 | seg[1] += y 28 | break 29 | case 'h': 30 | seg[1] += x 31 | break 32 | default: 33 | for (var i = 1; i < seg.length;) { 34 | seg[i++] += x 35 | seg[i++] += y 36 | } 37 | } 38 | } 39 | 40 | // update cursor state 41 | switch (command) { 42 | case 'Z': 43 | x = startX 44 | y = startY 45 | break 46 | case 'H': 47 | x = seg[1] 48 | break 49 | case 'V': 50 | y = seg[1] 51 | break 52 | case 'M': 53 | x = startX = seg[1] 54 | y = startY = seg[2] 55 | break 56 | default: 57 | x = seg[seg.length - 2] 58 | y = seg[seg.length - 1] 59 | } 60 | 61 | return seg 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /src/adaptive-bezier-curve.js: -------------------------------------------------------------------------------- 1 | /* 2 | adaptive-bezier-curve by Matt DesLauriers 3 | 4 | Modified BSD License 5 | ==================================================== 6 | Anti-Grain Geometry - Version 2.4 7 | Copyright (C) 2002-2005 Maxim Shemanarev (McSeem) 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions 11 | are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in 18 | the documentation and/or other materials provided with the 19 | distribution. 20 | 21 | 3. The name of the author may not be used to endorse or promote 22 | products derived from this software without specific prior 23 | written permission. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 26 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, 29 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 32 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 33 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 34 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 35 | POSSIBILITY OF SUCH DAMAGE. 36 | */ 37 | 38 | function clone(point) { //TODO: use gl-vec2 for this 39 | return [point[0], point[1]] 40 | } 41 | 42 | function vec2(x, y) { 43 | return [x, y] 44 | } 45 | 46 | 47 | let opt = {} 48 | 49 | var RECURSION_LIMIT = typeof opt.recursion === 'number' ? opt.recursion : 8 50 | var FLT_EPSILON = typeof opt.epsilon === 'number' ? opt.epsilon : 1.19209290e-7 51 | var PATH_DISTANCE_EPSILON = typeof opt.pathEpsilon === 'number' ? opt.pathEpsilon : 1.0 52 | 53 | var curve_angle_tolerance_epsilon = typeof opt.angleEpsilon === 'number' ? opt.angleEpsilon : 0.01 54 | var m_angle_tolerance = opt.angleTolerance || 0 55 | var m_cusp_limit = opt.cuspLimit || 0 56 | 57 | export function adaptiveBezierCurve(start, c1, c2, end, scale, points) { 58 | if (!points) 59 | points = [] 60 | 61 | scale = typeof scale === 'number' ? scale : 1.0 62 | var distanceTolerance = PATH_DISTANCE_EPSILON / scale 63 | distanceTolerance *= distanceTolerance 64 | begin(start, c1, c2, end, points, distanceTolerance) 65 | return points 66 | } 67 | 68 | 69 | ////// Based on: 70 | ////// https://github.com/pelson/antigrain/blob/master/agg-2.4/src/agg_curves.cpp 71 | 72 | function begin(start, c1, c2, end, points, distanceTolerance) { 73 | points.push(clone(start)) 74 | var x1 = start[0], 75 | y1 = start[1], 76 | x2 = c1[0], 77 | y2 = c1[1], 78 | x3 = c2[0], 79 | y3 = c2[1], 80 | x4 = end[0], 81 | y4 = end[1] 82 | recursive(x1, y1, x2, y2, x3, y3, x4, y4, points, distanceTolerance, 0) 83 | points.push(clone(end)) 84 | } 85 | 86 | function recursive(x1, y1, x2, y2, x3, y3, x4, y4, points, distanceTolerance, level) { 87 | if(level > RECURSION_LIMIT) 88 | return 89 | 90 | var pi = Math.PI 91 | 92 | // Calculate all the mid-points of the line segments 93 | //---------------------- 94 | var x12 = (x1 + x2) / 2 95 | var y12 = (y1 + y2) / 2 96 | var x23 = (x2 + x3) / 2 97 | var y23 = (y2 + y3) / 2 98 | var x34 = (x3 + x4) / 2 99 | var y34 = (y3 + y4) / 2 100 | var x123 = (x12 + x23) / 2 101 | var y123 = (y12 + y23) / 2 102 | var x234 = (x23 + x34) / 2 103 | var y234 = (y23 + y34) / 2 104 | var x1234 = (x123 + x234) / 2 105 | var y1234 = (y123 + y234) / 2 106 | 107 | if(level > 0) { // Enforce subdivision first time 108 | // Try to approximate the full cubic curve by a single straight line 109 | //------------------ 110 | var dx = x4-x1 111 | var dy = y4-y1 112 | 113 | var d2 = Math.abs((x2 - x4) * dy - (y2 - y4) * dx) 114 | var d3 = Math.abs((x3 - x4) * dy - (y3 - y4) * dx) 115 | 116 | var da1, da2 117 | 118 | if(d2 > FLT_EPSILON && d3 > FLT_EPSILON) { 119 | // Regular care 120 | //----------------- 121 | if((d2 + d3)*(d2 + d3) <= distanceTolerance * (dx*dx + dy*dy)) { 122 | // If the curvature doesn't exceed the distanceTolerance value 123 | // we tend to finish subdivisions. 124 | //---------------------- 125 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 126 | points.push(vec2(x1234, y1234)) 127 | return 128 | } 129 | 130 | // Angle & Cusp Condition 131 | //---------------------- 132 | var a23 = Math.atan2(y3 - y2, x3 - x2) 133 | da1 = Math.abs(a23 - Math.atan2(y2 - y1, x2 - x1)) 134 | da2 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - a23) 135 | if(da1 >= pi) da1 = 2*pi - da1 136 | if(da2 >= pi) da2 = 2*pi - da2 137 | 138 | if(da1 + da2 < m_angle_tolerance) { 139 | // Finally we can stop the recursion 140 | //---------------------- 141 | points.push(vec2(x1234, y1234)) 142 | return 143 | } 144 | 145 | if(m_cusp_limit !== 0.0) { 146 | if(da1 > m_cusp_limit) { 147 | points.push(vec2(x2, y2)) 148 | return 149 | } 150 | 151 | if(da2 > m_cusp_limit) { 152 | points.push(vec2(x3, y3)) 153 | return 154 | } 155 | } 156 | } 157 | } 158 | else { 159 | if(d2 > FLT_EPSILON) { 160 | // p1,p3,p4 are collinear, p2 is considerable 161 | //---------------------- 162 | if(d2 * d2 <= distanceTolerance * (dx*dx + dy*dy)) { 163 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 164 | points.push(vec2(x1234, y1234)) 165 | return 166 | } 167 | 168 | // Angle Condition 169 | //---------------------- 170 | da1 = Math.abs(Math.atan2(y3 - y2, x3 - x2) - Math.atan2(y2 - y1, x2 - x1)) 171 | if(da1 >= pi) da1 = 2*pi - da1 172 | 173 | if(da1 < m_angle_tolerance) { 174 | points.push(vec2(x2, y2)) 175 | points.push(vec2(x3, y3)) 176 | return 177 | } 178 | 179 | if(m_cusp_limit !== 0.0) { 180 | if(da1 > m_cusp_limit) { 181 | points.push(vec2(x2, y2)) 182 | return 183 | } 184 | } 185 | } 186 | } 187 | else if(d3 > FLT_EPSILON) { 188 | // p1,p2,p4 are collinear, p3 is considerable 189 | //---------------------- 190 | if(d3 * d3 <= distanceTolerance * (dx*dx + dy*dy)) { 191 | if(m_angle_tolerance < curve_angle_tolerance_epsilon) { 192 | points.push(vec2(x1234, y1234)) 193 | return 194 | } 195 | 196 | // Angle Condition 197 | //---------------------- 198 | da1 = Math.abs(Math.atan2(y4 - y3, x4 - x3) - Math.atan2(y3 - y2, x3 - x2)) 199 | if(da1 >= pi) da1 = 2*pi - da1 200 | 201 | if(da1 < m_angle_tolerance) { 202 | points.push(vec2(x2, y2)) 203 | points.push(vec2(x3, y3)) 204 | return 205 | } 206 | 207 | if(m_cusp_limit !== 0.0) { 208 | if(da1 > m_cusp_limit) 209 | { 210 | points.push(vec2(x3, y3)) 211 | return 212 | } 213 | } 214 | } 215 | } 216 | else { 217 | // Collinear case 218 | //----------------- 219 | dx = x1234 - (x1 + x4) / 2 220 | dy = y1234 - (y1 + y4) / 2 221 | if(dx*dx + dy*dy <= distanceTolerance) { 222 | points.push(vec2(x1234, y1234)) 223 | return 224 | } 225 | } 226 | } 227 | } 228 | 229 | // Continue subdivision 230 | //---------------------- 231 | recursive(x1, y1, x12, y12, x123, y123, x1234, y1234, points, distanceTolerance, level + 1) 232 | recursive(x1234, y1234, x234, y234, x34, y34, x4, y4, points, distanceTolerance, level + 1) 233 | } 234 | -------------------------------------------------------------------------------- /src/boot.js: -------------------------------------------------------------------------------- 1 | // load source-map in nodejs 2 | try { require("source-map-support").install() } catch(_){} 3 | 4 | // patch browser to look like node so that opentype.js doesn't fail to load 5 | var global = ( 6 | typeof global != "undefined" ? global : 7 | typeof self != "undefined" ? self : 8 | typeof window != "undefined" ? window : 9 | this 10 | ) 11 | if (!global.module) { 12 | global.module = {} 13 | } 14 | -------------------------------------------------------------------------------- /src/canvas.js: -------------------------------------------------------------------------------- 1 | import { Vec2 } from "./vec" 2 | 3 | // main function is Canvas.create: 4 | // Canvas.create( 5 | // domSelector : string, 6 | // init : (c :Canvas, g :Context2D)=>void, 7 | // draw : (time :number)=>void, 8 | // ) 9 | 10 | 11 | export class Color { 12 | constructor(r,g,b,a) { 13 | this.r = r ; this.g = g ; this.b = b ; this.a = a 14 | } 15 | alpha(a) { 16 | return new Color(this.r, this.g, this.b, a) 17 | } 18 | toString() { 19 | return `rgba(${this.r*255},${this.g*255},${this.b*255},${this.a})` 20 | } 21 | } 22 | 23 | export const red = new Color(1, 0.1, 0, 1) 24 | export const orange = new Color(1, 0.5, 0, 1) 25 | export const green = new Color(0, 0.7, 0.1, 1) 26 | export const teal = new Color(0, 0.7, 0.7, 1) 27 | export const blue = new Color(0, 0.55, 1, 1) 28 | export const pink = new Color(1, 0.2, 0.7, 1) 29 | 30 | 31 | export class Canvas { 32 | constructor(domSelector) { 33 | const c = this 34 | c.canvas = document.querySelector(domSelector) 35 | if (!c.canvas) { 36 | throw new Error(`canvas with DOM selector ${JSON.stringify(domSelector)} not found`) 37 | } 38 | const g = c.g = c.canvas.getContext("2d") 39 | c._origWidth = c.canvas.width 40 | c._origHeight = c.canvas.height 41 | c.initialTransform = c.g.getTransform() 42 | c.origin = new Vec2(0,0) 43 | c.hasViewportChange = true // causes transform() to be called on first draw 44 | 45 | g.font = '11px Inter, sans-serif' 46 | 47 | c.updateSize() 48 | window.addEventListener("resize", c.updateSize.bind(c), {passive:true,capture:false}) 49 | 50 | function strokeOrFill(style, strokeWidth, f) { 51 | if (!style) { 52 | // no fill or stroke 53 | f() 54 | } else if (strokeWidth > 0) { 55 | // stroke 56 | g.lineWidth = strokeWidth 57 | g.strokeStyle = style || "black" 58 | f() 59 | g.stroke() 60 | } else { 61 | // fill 62 | g.fillStyle = style || "black" 63 | f() 64 | g.fill() 65 | } 66 | } 67 | 68 | // drawing functions 69 | g.draw = { 70 | circle(pos, radius) { 71 | g.beginPath() 72 | g.arc(pos[0], pos[1], radius || 1.5, 0, 2 * Math.PI) 73 | }, 74 | rhombus(pos, radius) { 75 | radius += 1 // to match disc 76 | g.beginPath() 77 | g.moveTo(pos[0], pos[1]-radius) 78 | g.lineTo(pos[0]+radius, pos[1]) 79 | g.lineTo(pos[0], pos[1]+radius) 80 | g.lineTo(pos[0]-radius, pos[1]) 81 | g.lineTo(pos[0], pos[1]-radius) 82 | }, 83 | line(start, end) { 84 | g.beginPath() 85 | g.moveTo(start[0], start[1]) 86 | g.lineTo(end[0], end[1]) 87 | }, 88 | plus(pos, size) { 89 | g.beginPath() 90 | g.moveTo(pos[0], pos[1] - size) 91 | g.lineTo(pos[0], pos[1] + size) 92 | g.moveTo(pos[0] - size, pos[1]) 93 | g.lineTo(pos[0] + size, pos[1]) 94 | }, 95 | bezier(start, c1, c2, end) { 96 | g.beginPath() 97 | g.moveTo(start[0], start[1]) 98 | g.bezierCurveTo(c1[0], c1[1], c2[0], c2[1], end[0], end[1]) 99 | }, 100 | triangle(a, b, c) { 101 | g.beginPath() 102 | g.moveTo(a[0], a[1]) 103 | g.lineTo(b[0], b[1]) 104 | g.lineTo(c[0], c[1]) 105 | }, 106 | arrowhead(headPos, tailPos, size, stemStrokeWidthHint) { 107 | // draws a triangle which tip is at headPos and its opposite edge is in the direction 108 | // of tailPos. stemStrokeWidthHint is a hint to offset the tip of the triangle from 109 | // the headPos. In case of drawing a an arrow, this should be the stem thickness. 110 | let angle = Math.atan2(headPos[1] - tailPos[1], headPos[0] - tailPos[0]) 111 | const marginFromTip = stemStrokeWidthHint || 0 112 | , cx = headPos[0] - Math.cos(angle)*(size - marginFromTip) 113 | , cy = headPos[1] - Math.sin(angle)*(size - marginFromTip) 114 | g.beginPath() 115 | let x = size*Math.cos(angle) + cx 116 | let y = size*Math.sin(angle) + cy 117 | g.moveTo(x, y) 118 | angle += (1/3)*(2*Math.PI) 119 | x = size*Math.cos(angle) + cx 120 | y = size*Math.sin(angle) + cy 121 | g.lineTo(x, y) 122 | angle += (1/3)*(2*Math.PI) 123 | x = size*Math.cos(angle) + cx 124 | y = size*Math.sin(angle) + cy 125 | g.lineTo(x, y) 126 | g.closePath() 127 | }, 128 | } 129 | 130 | // transform functions 131 | g.withTranslation = this.withTranslation.bind(this) 132 | g.withScale = this.withScale.bind(this) 133 | g.withTransform = this.withTransform.bind(this) 134 | 135 | // complete drawing functions with fill/stroke 136 | g.drawCircle = (pos, radius, style, strokeWidth) => { 137 | strokeOrFill(style, strokeWidth, () => g.draw.circle(pos, radius)) 138 | } 139 | g.drawRhombus = (pos, radius, style, strokeWidth) => { 140 | strokeOrFill(style, strokeWidth, () => { 141 | g.draw.rhombus(pos, radius) 142 | g.closePath() 143 | }) 144 | } 145 | g.drawLine = (start, end, style, strokeWidth) => { 146 | g.lineWidth = strokeWidth || 1 147 | g.strokeStyle = style || "black" 148 | g.draw.line(start, end) 149 | g.stroke() 150 | } 151 | g.drawPlus = (pos, size, style, strokeWidth) => { 152 | size = size || 16 153 | g.lineWidth = strokeWidth || 1 154 | g.strokeStyle = style || "black" 155 | g.draw.plus(pos, size) 156 | g.stroke() 157 | } 158 | g.drawBezier = (start, c1, c2, end, style, strokeWidth, options) => { 159 | if (options && options.handles) { 160 | let handleStyle = typeof options.handles == "boolean" ? style : options.handles 161 | g.drawDisc(c1, 2, handleStyle, 1) 162 | g.drawDisc(c2, 2, handleStyle, 1) 163 | } 164 | g.lineWidth = strokeWidth || 1 165 | g.strokeStyle = style || "black" 166 | g.draw.bezier(start, c1, c2, end) 167 | g.stroke() 168 | } 169 | g.drawOrigin = (style) => { 170 | style = style || "red" 171 | const size = 16 * g.dp 172 | const pos = [0,0] 173 | g.drawPlus(pos, size, style, g.dp) 174 | g.drawCircle(pos, g.dp*2, style) 175 | } 176 | g.drawTriangle = (a, b, c, style, strokeWidth) => { 177 | strokeOrFill(style, strokeWidth, () => { 178 | g.draw.triangle(a, b, c) 179 | g.closePath() 180 | }) 181 | } 182 | g.drawArrow = (headPos, tailPos, style, strokeWidth, arrowHeadSize) => { 183 | if (strokeWidth === undefined) { 184 | strokeWidth = g.dp 185 | } 186 | if (arrowHeadSize === undefined) { 187 | arrowHeadSize = strokeWidth * Math.PI 188 | } 189 | let angle = Math.atan2(headPos[1] - tailPos[1], headPos[0] - tailPos[0]) 190 | let cx = Math.cos(angle), cy = Math.sin(angle) 191 | // stem (offset stem head to not interfere with arrowhead point) 192 | let stemHeadPos = [ 193 | headPos[0] - cx*(arrowHeadSize - strokeWidth), 194 | headPos[1] - cy*(arrowHeadSize - strokeWidth), 195 | ] 196 | g.drawLine(stemHeadPos, tailPos, style, strokeWidth) 197 | // arrowhead 198 | const headWidthRatio = 0.6 // 1=1:1, <1 = long arrowhead, >1 = stubby arrowhead 199 | arrowHeadSize = arrowHeadSize/headWidthRatio 200 | cx = headPos[0] - cx*arrowHeadSize 201 | cy = headPos[1] - cy*arrowHeadSize 202 | g.beginPath() // tip: 203 | let x = arrowHeadSize*Math.cos(angle) + cx 204 | let y = arrowHeadSize*Math.sin(angle) + cy 205 | g.moveTo(x, y) 206 | angle += (1/3)*(2*Math.PI) 207 | x = (arrowHeadSize*headWidthRatio)*Math.cos(angle) + cx 208 | y = (arrowHeadSize*headWidthRatio)*Math.sin(angle) + cy 209 | g.lineTo(x, y) 210 | angle += (1/3)*(2*Math.PI) 211 | x = (arrowHeadSize*headWidthRatio)*Math.cos(angle) + cx 212 | y = (arrowHeadSize*headWidthRatio)*Math.sin(angle) + cy 213 | g.lineTo(x, y) 214 | g.closePath() 215 | g.fillStyle = style || "black" 216 | g.fill() 217 | } 218 | } 219 | 220 | px(v) { 221 | return Math.round(v * this.pixelScale) / this.pixelScale 222 | } 223 | 224 | setOrigin(x, y) { 225 | this.origin[0] = x 226 | this.origin[1] = y 227 | this.resetTransform() 228 | } 229 | 230 | resetTransform() { 231 | this.g.setTransform(this.initialTransform) // maybe just the identity matrix..? 232 | this.g.scale(this.pixelScale, this.pixelScale) 233 | this.g.translate(this.origin[0], this.origin[1]) 234 | this._transformChanged() 235 | this.needsDraw = true 236 | } 237 | 238 | updateSize() { 239 | const canvas = this.canvas 240 | const style = canvas.style 241 | 242 | // clear adjustments and measure size 243 | style.zoom = null 244 | style.minWidth = null 245 | style.minHeight = null 246 | canvas.width = this._origWidth 247 | canvas.height = this._origHeight 248 | let r = canvas.getBoundingClientRect() 249 | 250 | // update size 251 | this.width = Math.round(r.width) 252 | this.height = Math.round(r.height) 253 | this.g.pixelScale = this.pixelScale = window.devicePixelRatio 254 | canvas.width = Math.round(this.width * this.pixelScale) 255 | canvas.height = Math.round(this.height * this.pixelScale) 256 | style.minWidth = canvas.width + "px" 257 | style.minHeight = canvas.height + "px" 258 | style.zoom = 1/this.pixelScale 259 | 260 | // udpate transform and mark viewport changed 261 | this.resetTransform() 262 | this.hasViewportChange = true 263 | } 264 | 265 | getScale() { 266 | let tm = this.g.getTransform() 267 | return tm.d / this.pixelScale // tm.d is scaleY 268 | } 269 | 270 | _transformChanged() { 271 | let tm = this.g.getTransform() 272 | this.g.dp = 1 / (tm.d / this.pixelScale) // tm.d is scaleY 273 | } 274 | 275 | // transform is called when the viewport has changed. 276 | // Perform any coordinate space transforms here. 277 | transform() {} 278 | 279 | // draw is called whenever needsDraw is true, by the Canvas.create mechanism. 280 | // This function should be implemented by users. 281 | draw(time) {} 282 | 283 | drawIfNeeded(time) { 284 | const c = this 285 | if (c.hasViewportChange) { 286 | c.hasViewportChange = false 287 | c.transform() 288 | } 289 | if (c.needsDraw) { 290 | c.needsDraw = false 291 | c.g.clearRect(-c.origin[0], -c.origin[1], c.width, c.height) 292 | c.draw(time) 293 | } 294 | } 295 | 296 | withTranslation(x, y, f) { 297 | this.withTransform([1, 0, x, 0, 1, y /* g,h,i ignored */], f) 298 | } 299 | 300 | withScale(x, y, f) { 301 | this.withTransform([x, 0, 0, 0, y, 0 /* g,h,i ignored */], f) 302 | } 303 | 304 | withTransform(m3, f) { // m3 is a 3x3 matrix, opengl style [a,b,c,d,e,f,g,h,i] 305 | let tm = this.g.getTransform() 306 | this.g.transform(m3[0], m3[3], m3[1], m3[4], m3[2], m3[5]) 307 | this._transformChanged() 308 | try { 309 | f() 310 | } finally { 311 | this.g.setTransform(tm) 312 | this._transformChanged() 313 | } 314 | } 315 | 316 | } 317 | 318 | 319 | let canvases = new Set() 320 | 321 | Canvas.create = function(domSelector, init, draw) { 322 | const c = new Canvas(domSelector) 323 | c.draw = draw 324 | init(c, c.g) 325 | canvases.add(c) 326 | if (canvases.size == 1) { 327 | drawAll(typeof performance != "undefined" ? performance.now() : 0) 328 | } 329 | return c 330 | } 331 | 332 | Canvas.destroy = function(c) { 333 | canvases.delete(c) 334 | } 335 | 336 | function drawAll(time) { 337 | for (let c of canvases) { 338 | c.drawIfNeeded(time) 339 | } 340 | if (canvases.size > 0) { 341 | requestAnimationFrame(drawAll) 342 | } 343 | } 344 | 345 | -------------------------------------------------------------------------------- /src/color.js: -------------------------------------------------------------------------------- 1 | export class Color { 2 | constructor(r,g,b,a) { 3 | this.r = r ; this.g = g ; this.b = b ; this.a = a 4 | } 5 | alpha(a) { 6 | return new Color(this.r, this.g, this.b, a) 7 | } 8 | toString() { 9 | return `rgba(${this.r*255},${this.g*255},${this.b*255},${this.a})` 10 | } 11 | } 12 | 13 | export default { 14 | red: new Color(1, 0.1, 0, 1), 15 | orange: new Color(1, 0.5, 0, 1), 16 | green: new Color(0, 0.7, 0.1, 1), 17 | teal: new Color(0, 0.7, 0.7, 1), 18 | blue: new Color(0, 0.55, 1, 1), 19 | pink: new Color(1, 0.2, 0.7, 1), 20 | } 21 | -------------------------------------------------------------------------------- /src/feature-extract.d.ts: -------------------------------------------------------------------------------- 1 | interface FeatureData { 2 | // 3 | } 4 | -------------------------------------------------------------------------------- /src/feature-extract.js: -------------------------------------------------------------------------------- 1 | import { raycastX, PolyPointHitTester } from "./raycast" 2 | import * as opentype from "opentype.js" 3 | import { svgPathContours } from "./svg-path-contours" 4 | import { simplifyPath } from "./simplify-path" 5 | import { Vec2 } from "./vec" 6 | import { red, orange, green, teal, blue, pink } from "./canvas" 7 | 8 | // OTGlyphShapeFontSize is the origin size and relates to polygon density etc. 9 | // Changing this means also tuning svgPathContours(density) 10 | export const OTGlyphShapeFontSize = 512 11 | 12 | // FeatureRayCount controls how many ray features to compute. 13 | // A larger number means higher density. 14 | export const FeatureRayCount = 32 15 | 16 | 17 | 18 | /* 19 | interface FeatureData { 20 | // spacing is kerning & sidebearings combined; the space between two glyphs' 21 | // contour-bounding boxes. It's a percent of the font's UPM with the value range [0-1]. 22 | // This is the value that the model outpus when predicting and the value that is 23 | // used as reference example input when training the model. 24 | spacing : float 25 | 26 | // features is a set of distances measuring the space between two glyphs' silhouettes 27 | // at discreete Y-axis position. The values are normalied in UPM space; range: [0-1]. 28 | // The size of this array is exactly the constant FeatureRayCount. 29 | // Illustrated example: 30 | // 31 | // features[0] -------------------- 32 | // features[1] -------------------- 33 | // features[2] \ /------/\ 34 | // features[3] \ /------/——\ 35 | // features[4] \/------/ \ 36 | // features[5] -------------------- 37 | // features[6] -------------------- 38 | // 39 | // In this illustration the X-axis, the hyphens (---), represent the values 40 | // while the X-axis, the fN "features", represents the positions in this array. 41 | // 42 | features : Float64Array 43 | } 44 | */ 45 | 46 | 47 | export class FontFeatureExtractor { 48 | constructor() { 49 | this.font = null 50 | this.minY = 0 51 | this.maxY = 0 52 | this.fontScale = 1.0 // OTGlyphShapeFontSize / font.unitsPerEm 53 | this.enableCanvasDrawing = true 54 | this.spaceBasis = 0.0 // Constant used to compute relative spacing 55 | this.glyphs = new Map() // glyphname => OTGlyphShape 56 | } 57 | 58 | loadFont(filename) { 59 | return new Promise((resolve, reject) => { 60 | this.font = null 61 | opentype.load(filename, (err, font) => { 62 | if (err) return reject(err) 63 | this.font = font 64 | this.glyphs.clear() 65 | this.recomputeFontDependentData() 66 | resolve(font) 67 | }) 68 | }) 69 | } 70 | 71 | // if dstFloat64Array is not provided, a new Float64Array is allocated. 72 | // The array, being user provided or not, is returned as {values} 73 | computeFeaturesForGlyphPair(L, R, dstFloat64Array) { 74 | L = this.getGlyph(L) 75 | R = this.getGlyph(R) 76 | // perform raycasting to extract whistepace features 77 | if (!L.features.right) { 78 | L.computeFeatures(this.minY, this.maxY, /* includeRays */ this.enableCanvasDrawing) 79 | } 80 | if (!R.features.left) { 81 | R.computeFeatures(this.minY, this.maxY, /* includeRays */ this.enableCanvasDrawing) 82 | } 83 | if (!dstFloat64Array) { 84 | dstFloat64Array = new Float64Array(L.features.right.length + 1) 85 | } 86 | 87 | // spacing = L.RSB + R.LSB + kerning 88 | const kerning = this.font.getKerningValue(L.glyph, R.glyph) 89 | // // Normalize spacing to sum of bbox widths: 90 | // const spacing = ((L.RSB + R.LSB + kerning) * this.fontScale) / 91 | // (L.bbox.width + R.bbox.width) 92 | // Normalize spacing to average bbox width: 93 | // const spacing = ((L.RSB + R.LSB + kerning) * this.fontScale) / 94 | // ((L.bbox.width + R.bbox.width) / 2) 95 | // 96 | // Normalize spacing to spaceBasis: (see recomputeFontDependentData) 97 | const spacing = (L.RSB + R.LSB + kerning) / this.spaceBasis 98 | dstFloat64Array[0] = spacing 99 | 100 | // features = (L.features + R.features) / 2 101 | for (let i = 1; i < dstFloat64Array.length; i++) { 102 | dstFloat64Array[i] = (L.features.right[i] + R.features.left[i]) / 2 103 | } 104 | 105 | return { 106 | // feature data 107 | values: dstFloat64Array, // Float64Array of size FeatureRayCount+1 108 | // metadata (not features) 109 | L, R 110 | } 111 | } 112 | 113 | getGlyph(v) { 114 | if (typeof v == "string") { 115 | return this.getGlyphByText(v) 116 | } else if (typeof v == "number") { 117 | return this.getGlyphByIndex(v) 118 | } else if (v instanceof opentype.Glyph) { 119 | return this.getGlyphByIndex(v.index, v) 120 | } else if (v instanceof OTGlyphShape) { 121 | return v 122 | } 123 | throw new Error(`invalid argument; expected glyph, glyph index, glyph name or OTGlyphShape`) 124 | } 125 | 126 | getGlyphByText(singleCharText) { 127 | const glyphIndex = this.font.charToGlyphIndex(singleCharText) 128 | return this.getGlyphByIndex(glyphIndex) 129 | } 130 | 131 | getGlyphByName(glyphName) { 132 | const glyphIndex = this.font.nameToGlyphIndex(glyphName) 133 | return this.getGlyphByIndex(glyphIndex) 134 | } 135 | 136 | getGlyphByIndex(glyphIndex, g) { 137 | // singleCharText 138 | let shape = this.glyphs.get(glyphIndex) 139 | if (!shape) { 140 | if (!g) { 141 | g = this.font.glyphs.get(glyphIndex) 142 | } 143 | shape = new OTGlyphShape(this.font, g) 144 | this.glyphs.set(glyphIndex, shape) 145 | } 146 | return shape 147 | } 148 | 149 | 150 | recomputeFontDependentData() { 151 | // fontScale is used to translate from UPM to our local coordinate system 152 | this.fontScale = OTGlyphShapeFontSize / this.font.unitsPerEm 153 | 154 | // set spaceBasis 155 | // this.spaceBasis = this.font.charToGlyph(" ").advanceWidth // SPACE 156 | this.spaceBasis = this.font.unitsPerEm 157 | 158 | // 159 | // Compute vertical bounds 160 | // 161 | // Deciding on an approach for vertical space; the feature sample space. 162 | // 163 | // box approach 1: 164 | // compute all glyphs in the same vertical space so that we can reuse a glyph shape 165 | // in multiple pair comparisons. This is prooobably a good idea. Perhaps not. 166 | // For short glyphs this will reduce their resolution. 167 | // 168 | // Code for using the bounding box of the font: 169 | // this.minY = -this.font.tables.head.yMax * this.fontScale 170 | // this.maxY = -this.font.tables.head.yMin * this.fontScale 171 | // 172 | // Code for using the ascender & descender of the font: 173 | this.minY = -this.font.ascender * this.fontScale 174 | this.maxY = -this.font.descender * this.fontScale 175 | // 176 | // box approach 2: 177 | // Compute each pair with tightly-fitting y-axis extremes 178 | // 179 | // This is slower, since we need to compute the features for every pair, rather than 180 | // compute just once per glyph. 181 | // However, the resulting features are essentially normalized in terms of comparison 182 | // across fonts. I.e. say we sample two fonts with different ascender and descender 183 | // values. In this case then the feature data would be completely different for two 184 | // identical shapes, as seen in Figure 1. 185 | // 186 | // Figure 1: "box approach 1"; the issue with varying height: 187 | // 188 | // Font 1 Font 2 189 | // minY ————————————————————————— minY ————————————————————————— 190 | // f ---------- ---------- f ---------- ---------- 191 | // f ---------- ---------- f \ /-- ----/\ 192 | // f ---------- ---------- f \ /--- ---/——\ 193 | // f \ /-- ----/\ f \/---- --/ \ 194 | // f \ /--- ---/——\ f ---------- ---------- 195 | // f \/---- --/ \ f ---------- ---------- 196 | // f ---------- ---------- f ---------- ---------- 197 | // maxY ————————————————————————— maxY ————————————————————————— 198 | // 199 | // 200 | // Figure 2: "box approach 2"; normalizing height for each pair. 201 | // 202 | // Font 1 Font 2 203 | // minY ————————————————————————— minY ————————————————————————— 204 | // f \ /-- ----/\ f \ /-- ----/\ 205 | // f \ /--- ---/——\ f \ /--- ---/——\ 206 | // f \/---- --/ \ f \/---- --/ \ 207 | // maxY ————————————————————————— maxY ————————————————————————— 208 | // 209 | // Code for using the bounding box of the glyph pair: 210 | //this.minY = Math.min(shape1.bbox.minY, shape2.bbox.minY) 211 | //this.maxY = Math.max(shape1.bbox.maxY, shape2.bbox.maxY) 212 | } 213 | } 214 | 215 | 216 | export class PolyShape { 217 | constructor(paths, simplifyThreshold) { 218 | if (simplifyThreshold === undefined) { 219 | simplifyThreshold = 0.1 220 | } 221 | 222 | // convert curves into discrete points 223 | this.polygons = svgPathContours(paths, /*density*/0.3) 224 | 225 | // simplify polygons and convert to vertex arrays (plus compute bbox) 226 | const bbox = this.bbox = { 227 | minX: Infinity, minY: Infinity, 228 | maxX: -Infinity, maxY: -Infinity, 229 | width: 0, height: 0, 230 | } 231 | for (let i = 0; i < this.polygons.length; i++) { 232 | let points = this.polygons[i] 233 | if (simplifyThreshold > 0) { 234 | points = simplifyPath(points, simplifyThreshold) 235 | } 236 | let a = new Float64Array(points.length * 2) 237 | for (let i = 0; i < points.length; i++) { 238 | let [x, y] = points[i] 239 | a[i * 2] = x 240 | a[i * 2 + 1] = y 241 | bbox.minX = Math.min(bbox.minX, x) 242 | bbox.minY = Math.min(bbox.minY, y) 243 | bbox.maxX = Math.max(bbox.maxX, x) 244 | bbox.maxY = Math.max(bbox.maxY, y) 245 | } 246 | this.polygons[i] = a 247 | } 248 | 249 | // round bbox extremes 250 | bbox.minX = Math.floor(bbox.minX) 251 | bbox.minY = Math.floor(bbox.minY) 252 | bbox.maxX = Math.ceil(bbox.maxX) 253 | bbox.maxY = Math.ceil(bbox.maxY) 254 | 255 | // calc bbox width and height for convenience 256 | bbox.width = bbox.maxX - bbox.minX 257 | bbox.height = bbox.maxY - bbox.minY 258 | 259 | this.paddingLeft = 0 // i.e. sidebearing for fonts 260 | this.paddingRight = 0 // i.e. sidebearing for fonts 261 | 262 | // features (populated by computeFeatures) 263 | this.features = { 264 | left: null, // : Float64Array -- velocity factors (length=FeatureRayCount) 265 | right: null, // : Float64Array -- velocity factors (length=FeatureRayCount) 266 | leftRays: null, // : [Vec2,Vec2][] -- ray lines 267 | rightRays: null, // : [Vec2,Vec2][] -- ray lines 268 | } 269 | } 270 | 271 | // computeFeatures takes the shape's polygons and computes the left and right 272 | // whitespace, described by lists of distances in the input shape's coordinate system. 273 | computeFeatures(minY, maxY, includeRays) { 274 | // raycast 275 | // console.time("computeFeatures") 276 | const height = maxY - minY 277 | const yStride = Math.ceil(height/FeatureRayCount) 278 | const padding = 0 // some extra space around the bbox (for debugging) 279 | const minX = this.bbox.minX - padding 280 | const maxX = this.bbox.maxX + padding 281 | const maxDistance = maxX - minX // stop ray from traveling further 282 | const hitTesters = this.polygons.map(polygon => new PolyPointHitTester(polygon)) 283 | const UPM = this.font.unitsPerEm 284 | const fontScale = OTGlyphShapeFontSize / UPM 285 | this.features.left = new Float64Array(FeatureRayCount) 286 | this.features.right = new Float64Array(FeatureRayCount) 287 | this.features.leftRays = [] 288 | this.features.rightRays = [] 289 | for (let y = minY, rayNum = 0; y <= maxY && rayNum < FeatureRayCount; y += yStride) { 290 | let endl = raycastX(hitTesters, minX, y, 1, maxDistance) 291 | let endr = raycastX(hitTesters, maxX, y, -1, maxDistance) 292 | // Encode features as normalized x-axis "velocity": 293 | // this.features.left[rayNum] = (endl-minX) / maxDistance 294 | // this.features.right[rayNum] = (maxX-endr) / maxDistance 295 | // // Encode features as normalized height "velocity": 296 | // this.features.left[rayNum] = (endl-minX) / height 297 | // this.features.right[rayNum] = (maxX-endr) / height 298 | // Encode features as normalized UPM: 299 | this.features.left[rayNum] = (endl - minX) / fontScale / UPM 300 | this.features.right[rayNum] = (maxX - endr) / fontScale / UPM 301 | if (includeRays) { 302 | // include rays, useful for drawing 303 | this.features.leftRays.push([ new Vec2(minX,y), new Vec2(endl,y) ]) 304 | this.features.rightRays.push([ new Vec2(maxX,y), new Vec2(endr,y) ]) 305 | } 306 | rayNum++ 307 | } 308 | // console.timeEnd("computeFeatures") 309 | return this.features 310 | } 311 | 312 | get width() { 313 | return this.paddingLeft + this.bbox.width + this.paddingRight 314 | } 315 | 316 | draw(g, x, y, scale, featureSides /* "left"|"right"|(("left"|"right")[]) */, options) { 317 | options = Object.assign({ 318 | // default options 319 | valueLabels: true, // draw value labels 320 | }, options || {}) 321 | // translate in caller coordinates and scale. 322 | // adjustX adjusts the bbox _after_ scaling (with scaling applied), in shape coordinates. 323 | let adjustX = -this.bbox.minX //+ this.paddingLeft 324 | g.withTransform([scale, 0, x + (adjustX * scale), 0, scale, y], () => { 325 | 326 | if (!Array.isArray(featureSides)) { 327 | featureSides = [ featureSides ] 328 | } 329 | 330 | // // draw triangles (requires tess2 to be loaded in env) 331 | // let tesselation = tesselate(this.polygons) 332 | // drawTriangles(g, tesselation.elements, tesselation.vertices) 333 | 334 | // draw full-width box 335 | g.lineWidth = g.dp 336 | g.strokeStyle = "rgba(0,200,200,0.5)" 337 | g.strokeRect( 338 | this.bbox.minX - this.paddingLeft, 339 | this.bbox.minY, 340 | this.width, 341 | this.bbox.height, 342 | ) 343 | 344 | let polygonColors = [ red, blue, pink, orange ] 345 | 346 | // draw polygon lines 347 | for (let i = 0; i < this.polygons.length; i++) { 348 | let vertexes = this.polygons[i] 349 | let color = polygonColors[i % polygonColors.length] 350 | // log(`path ${i} colored ${color}`) 351 | g.lineWidth = g.dp 352 | g.strokeStyle = color 353 | g.beginPath() 354 | for (let j = 0; j < vertexes.length; j += 2) { 355 | let x = vertexes[j], y = vertexes[j + 1] 356 | if (j == 0) { 357 | g.moveTo(x, y) 358 | } else { 359 | g.lineTo(x, y) 360 | } 361 | } 362 | g.stroke() 363 | } 364 | 365 | // draw polygon points 366 | for (let i = 0; i < this.polygons.length; i++) { 367 | let vertexes = this.polygons[i] 368 | let color = polygonColors[i % polygonColors.length] 369 | // log(`path ${i} colored ${color}`) 370 | for (let j = 0; j < vertexes.length; j += 2) { 371 | g.drawCircle([ vertexes[j], vertexes[j + 1] ], g.dp*1.5, color) 372 | } 373 | } 374 | 375 | // draw feature rays 376 | if (g.pixelScale / g.dp < 0.8) { 377 | // don't show values if text will be too cramped 378 | options.valueLabels = false 379 | } 380 | for (let side of featureSides) { 381 | const rays = this.features[side + "Rays"] 382 | const values = this.features[side] 383 | const fontSize = 9 * g.dp 384 | const labelSpacing = side == "left" ? fontSize/-2 : fontSize/2 385 | g.fillStyle = green 386 | g.textAlign = side == "left" ? "right" : "left" 387 | g.font = `500 ${Math.round(fontSize)}px Inter, sans-serif` 388 | if (rays) for (let i = 0; i < rays.length; i++) { 389 | // draw ray 390 | let [pt1, pt2] = rays[i] 391 | g.drawArrow(pt2, pt1, green, g.dp) 392 | // draw value label 393 | if (options.valueLabels) { 394 | let value = values[i] 395 | let xspace = value < 0.015 ? labelSpacing*2 : labelSpacing 396 | let text = value == 1 ? "•" : Math.round(value*100) // % 397 | // let text = String(Math.round(value*100)/100) 398 | g.fillText(text, pt1[0] + xspace, pt1[1] + fontSize/3) 399 | } 400 | } 401 | } 402 | }) // transform 403 | } 404 | 405 | toString() { 406 | return ( 407 | this.constructor.name + 408 | `(${Math.round(this.width)}×${this.bbox.height} ${this.polygons.length} polys)` 409 | ) 410 | } 411 | } 412 | 413 | 414 | export class OTGlyphShape extends PolyShape { 415 | constructor(font, glyph, simplifyThreshold) { 416 | // get flipped glyph.path 417 | let glyphPath = glyph.getPath(0, 0, OTGlyphShapeFontSize) 418 | 419 | // convert OpenType path object to list of SVG-like path segments 420 | let paths = otPathToPaths(glyphPath) 421 | super(paths, simplifyThreshold) 422 | this.font = font 423 | this.glyph = glyph 424 | this.glyphPath = glyphPath 425 | 426 | // sidebearings, which are in UPM (unscaled) 427 | const upmbbox = glyph.getBoundingBox() 428 | this.LSB = upmbbox.x1 429 | this.RSB = glyph.advanceWidth - upmbbox.x1 - (upmbbox.x2 - upmbbox.x1) 430 | 431 | // PolyShape padding, which are in polygon coordinates (scaled) 432 | const scale = OTGlyphShapeFontSize / font.unitsPerEm 433 | this.paddingLeft = this.LSB*scale 434 | this.paddingRight = this.RSB*scale 435 | } 436 | 437 | draw(g, x, y, fontSize, featureSides) { 438 | // draw glyph shape 439 | const scale = fontSize / OTGlyphShapeFontSize 440 | // Draw actual glyph shape in addition to the polygon drawn by PolyShape.draw() 441 | // g.withScale(scale, scale, () => { 442 | // g.withTranslation(x, y, () => { 443 | // this.glyphPath.fill = "rgba(0,0,0,0.2)" // sets g.fillStyle 444 | // this.glyphPath.draw(g) 445 | // }) 446 | // }) 447 | super.draw(g, x, y, scale, featureSides) 448 | } 449 | } 450 | 451 | 452 | function otPathToPaths(otpath) { 453 | // convert from array of objects to array of arrays; 454 | // the format other functions like svgPathContours expects. 455 | return otpath.commands.map(s => { 456 | let t = s.type 457 | return ( 458 | t == "Z" ? [t] : 459 | t == "Q" ? [t, s.x1, s.y1, s.x, s.y] : 460 | t == "C" ? [t, s.x1, s.y1, s.x2, s.y2, s.x, s.y] : 461 | [t, s.x, s.y] 462 | ) 463 | }) 464 | } 465 | -------------------------------------------------------------------------------- /src/featureio.js: -------------------------------------------------------------------------------- 1 | const log = console.log.bind(console) 2 | 3 | // createWriter(filename) returns an object with two methods 4 | // for writing a file of features in a streaming manner. 5 | // 6 | // write(featureData) -- write feature data 7 | // end() -- finalize writing (closes file) 8 | export function createWriter(filename, hintTotalCount) { 9 | const fs = require("fs") 10 | const spacingArray = new DataView(new ArrayBuffer(4)) 11 | const headerBuf = Buffer.allocUnsafe(2*4) 12 | 13 | // Binary format: 14 | // file = header entry{} 15 | // header = 16 | // entry = 17 | 18 | class FeatureDataWriter { 19 | constructor(filename) { 20 | this.fd = fs.openSync(filename, "w") 21 | this.height = 0 22 | this.width = -1 23 | this.writebuf = null 24 | this.writebufOffs = 0 25 | } 26 | writeHeader(pos) { 27 | headerBuf.writeUInt32LE(this.width, 0) 28 | headerBuf.writeUInt32LE(this.height, 4) 29 | fs.writeSync(this.fd, headerBuf, 0, headerBuf.length, pos) 30 | } 31 | writebufFlush() { 32 | fs.writeSync(this.fd, this.writebuf, 0, this.writebufOffs) 33 | this.writebufOffs = 0 34 | } 35 | write(floatArray) { 36 | if (this.width == -1) { 37 | this.width = floatArray.length 38 | this.height = 0 39 | const writebufBlockSize = 4096 40 | const writebufSize = Math.ceil((this.width * 4) / writebufBlockSize) * writebufBlockSize 41 | this.writebuf = Buffer.allocUnsafe(writebufSize) 42 | this.writebufOffs = 0 43 | this.writeHeader(/*filepos*/undefined) 44 | } 45 | const widthInBytes = this.width * 4 46 | const remainingSpace = this.writebuf.length - this.writebufOffs 47 | if (remainingSpace < widthInBytes) { 48 | this.writebufFlush() 49 | } 50 | for (let i = 0; i < this.width; i++) { 51 | this.writebuf.writeInt32LE(normFloatToInt32(floatArray[i]), this.writebufOffs) 52 | this.writebufOffs += 4 53 | } 54 | // this.writebufOffs += widthInBytes 55 | this.height++ 56 | } 57 | end() { 58 | if (this.height > -1) { 59 | this.writebufFlush() 60 | this.writeHeader(/*filepos*/0) 61 | } 62 | fs.closeSync(this.fd) 63 | } 64 | } 65 | return new FeatureDataWriter(filename) 66 | } 67 | 68 | 69 | // normFloatToInt32 converts a normalized floating-point number in the range [-1-1] to an int32 70 | function normFloatToInt32(f) { 71 | return (0x7FFFFFFF * f) | 0 72 | } 73 | 74 | // int32ToNormFloat converts an int32 to a normalized floating-point number 75 | function int32ToNormFloat(i) { 76 | return i / 2147483647.0 /* 0x7FFFFFFF */ 77 | } 78 | 79 | 80 | // readSync returns a Float64Array of size width*height where 81 | // width is read from the file header and 82 | // height is min(limit, height read from file header). 83 | // 84 | // Returns an object of the following shape: 85 | // 86 | // interface FeatureDataR { 87 | // data: Float64Array // all values in row-major order 88 | // width :number // values of eah row 89 | // length :number // number of rows, i.e. "height" 90 | // row(rowIndex) :Float64Array // access subarray for row 91 | // } 92 | // 93 | // Values in the Float64Array are arranged in row-major order, meaning that all the values for 94 | // entry N is at Float64Array[N:N+width]. 95 | // Illustrated example of width=4 height=3: 96 | // 97 | // x | 0 1 2 3 98 | // y ------------ 99 | // 0 | 0 1 2 3 100 | // 1 | 4 5 6 7 101 | // 2 | 8 9 10 11 102 | // 103 | export function readSync(filename, limit) { 104 | if (!limit) { limit = Infinity } 105 | 106 | const fs = require("fs") 107 | const fd = fs.openSync(filename, "r") 108 | let buf = Buffer.allocUnsafe(8) 109 | 110 | fs.readSync(fd, buf, 0, 8) 111 | const width = buf.readUInt32LE(0) 112 | const height = Math.min(limit, buf.readUInt32LE(4)) 113 | log({width, height}) 114 | 115 | let floatArray = new Float64Array(width * height) 116 | const widthInBytes = width * 4 117 | buf = Buffer.allocUnsafe(widthInBytes) 118 | 119 | let yindex = 0 120 | for (let y = 0; y < height; y++) { 121 | let z = fs.readSync(fd, buf, 0, widthInBytes) 122 | if (z != widthInBytes) { 123 | // EOF 124 | break 125 | } 126 | const dv = new DataView(buf.buffer, buf.byteOffset, widthInBytes) 127 | for (let xindex = 0; xindex < width; xindex++) { 128 | let v = int32ToNormFloat(dv.getInt32(xindex*4, /*littleEndian*/true)) 129 | floatArray[yindex + xindex] = v 130 | } 131 | yindex += width 132 | } 133 | 134 | return { 135 | data: floatArray, 136 | width, 137 | length: height, 138 | row(rowIndex) { 139 | const start = rowIndex * width 140 | return this.data.subarray(start, start + width) 141 | }, 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/greiner-hormann.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * greiner-hormann v1.4.1 3 | * Greiner-Hormann clipping algorithm 4 | * 5 | * @author Alexander Milevski 6 | * @license MIT 7 | * @preserve 8 | */ 9 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e(t.greinerHormann={})}(this,function(t){"use strict";var e=function(t,e){1===arguments.length&&(Array.isArray(t)?(e=t[1],t=t[0]):(e=t.y,t=t.x)),this.x=t,this.y=e,this.next=null,this.prev=null,this._corresponding=null,this._distance=0,this._isEntry=!0,this._isIntersection=!1,this._visited=!1};e.createIntersection=function(t,i,s){var n=new e(t,i);return n._distance=s,n._isIntersection=!0,n._isEntry=!1,n},e.prototype.visit=function(){this._visited=!0,null===this._corresponding||this._corresponding._visited||this._corresponding.visit()},e.prototype.equals=function(t){return this.x===t.x&&this.y===t.y},e.prototype.isInside=function(t){var e=!1,i=t.first,s=i.next,n=this.x,r=this.y;do{(i.y=r||s.y=r)&&(i.x<=n||s.x<=n)&&(e^=i.x+(r-i.y)/(s.y-i.y)*(s.x-i.x)} x\n * @param {Number=} y\n */\n constructor (x, y) {\n if (arguments.length === 1) {\n // Coords\n if (Array.isArray(x)) {\n y = x[1];\n x = x[0];\n } else {\n y = x.y;\n x = x.x;\n }\n }\n\n /**\n * X coordinate\n * @type {Number}\n */\n this.x = x;\n\n /**\n * Y coordinate\n * @type {Number}\n */\n this.y = y;\n\n /**\n * Next node\n * @type {Vertex}\n */\n this.next = null;\n\n /**\n * Previous vertex\n * @type {Vertex}\n */\n this.prev = null;\n\n /**\n * Corresponding intersection in other polygon\n */\n this._corresponding = null;\n\n /**\n * Distance from previous\n */\n this._distance = 0.0;\n\n /**\n * Entry/exit point in another polygon\n * @type {Boolean}\n */\n this._isEntry = true;\n\n /**\n * Intersection vertex flag\n * @type {Boolean}\n */\n this._isIntersection = false;\n\n /**\n * Loop check\n * @type {Boolean}\n */\n this._visited = false;\n }\n\n\n /**\n * Creates intersection vertex\n * @param {Number} x\n * @param {Number} y\n * @param {Number} distance\n * @return {Vertex}\n */\n static createIntersection (x, y, distance) {\n const vertex = new Vertex(x, y);\n vertex._distance = distance;\n vertex._isIntersection = true;\n vertex._isEntry = false;\n return vertex;\n }\n\n\n /**\n * Mark as visited\n */\n visit () {\n this._visited = true;\n if (this._corresponding !== null && !this._corresponding._visited) {\n this._corresponding.visit();\n }\n }\n\n\n /**\n * Convenience\n * @param {Vertex} v\n * @return {Boolean}\n */\n equals (v) {\n return this.x === v.x && this.y === v.y;\n }\n\n\n /**\n * Check if vertex is inside a polygon by odd-even rule:\n * If the number of intersections of a ray out of the point and polygon\n * segments is odd - the point is inside.\n * @param {Polygon} poly\n * @return {Boolean}\n */\n isInside (poly) {\n let oddNodes = false;\n let vertex = poly.first;\n let next = vertex.next;\n const x = this.x;\n const y = this.y;\n\n do {\n if ((vertex.y < y && next.y >= y ||\n next.y < y && vertex.y >= y) &&\n (vertex.x <= x || next.x <= x)) {\n oddNodes ^= (vertex.x + (y - vertex.y) /\n (next.y - vertex.y) * (next.x - vertex.x) < x);\n }\n\n vertex = vertex.next;\n next = vertex.next || poly.first;\n } while (!vertex.equals(poly.first));\n\n return oddNodes;\n }\n}\n","export default class Intersection {\n\n\n /**\n * @param {Vertex} s1\n * @param {Vertex} s2\n * @param {Vertex} c1\n * @param {Vertex} c2\n */\n constructor(s1, s2, c1, c2) {\n\n /**\n * @type {Number}\n */\n this.x = 0.0;\n\n /**\n * @type {Number}\n */\n this.y = 0.0;\n\n /**\n * @type {Number}\n */\n this.toSource = 0.0;\n\n /**\n * @type {Number}\n */\n this.toClip = 0.0;\n\n const d = (c2.y - c1.y) * (s2.x - s1.x) - (c2.x - c1.x) * (s2.y - s1.y);\n\n if (d === 0) return;\n\n /**\n * @type {Number}\n */\n this.toSource = ((c2.x - c1.x) * (s1.y - c1.y) - (c2.y - c1.y) * (s1.x - c1.x)) / d;\n\n /**\n * @type {Number}\n */\n this.toClip = ((s2.x - s1.x) * (s1.y - c1.y) - (s2.y - s1.y) * (s1.x - c1.x)) / d;\n\n if (this.valid()) {\n this.x = s1.x + this.toSource * (s2.x - s1.x);\n this.y = s1.y + this.toSource * (s2.y - s1.y);\n }\n }\n\n\n /**\n * @return {Boolean}\n */\n valid () {\n return (0 < this.toSource && this.toSource < 1) && (0 < this.toClip && this.toClip < 1);\n }\n}\n","import Vertex from './vertex';\nimport Intersection from './intersection';\n\n\nexport default class Polygon {\n\n\n /**\n * Polygon representation\n * @param {Array.>} p\n * @param {Boolean=} arrayVertices\n */\n constructor (p, arrayVertices) {\n\n /**\n * @type {Vertex}\n */\n this.first = null;\n\n /**\n * @type {Number}\n */\n this.vertices = 0;\n\n /**\n * @type {Vertex}\n */\n this._lastUnprocessed = null;\n\n /**\n * Whether to handle input and output as [x,y] or {x:x,y:y}\n * @type {Boolean}\n */\n this._arrayVertices = (typeof arrayVertices === \"undefined\") ?\n Array.isArray(p[0]) :\n arrayVertices;\n\n for (let i = 0, len = p.length; i < len; i++) {\n this.addVertex(new Vertex(p[i]));\n }\n }\n\n\n /**\n * Add a vertex object to the polygon\n * (vertex is added at the 'end' of the list')\n *\n * @param vertex\n */\n addVertex (vertex) {\n if (this.first === null) {\n this.first = vertex;\n this.first.next = vertex;\n this.first.prev = vertex;\n } else {\n const next = this.first;\n const prev = next.prev;\n\n next.prev = vertex;\n vertex.next = next;\n vertex.prev = prev;\n prev.next = vertex;\n }\n this.vertices++;\n }\n\n\n /**\n * Inserts a vertex inbetween start and end\n *\n * @param {Vertex} vertex\n * @param {Vertex} start\n * @param {Vertex} end\n */\n insertVertex (vertex, start, end) {\n let prev, curr = start;\n\n while (!curr.equals(end) && curr._distance < vertex._distance) {\n curr = curr.next;\n }\n\n vertex.next = curr;\n prev = curr.prev;\n\n vertex.prev = prev;\n prev.next = vertex;\n curr.prev = vertex;\n\n this.vertices++;\n }\n\n /**\n * Get next non-intersection point\n * @param {Vertex} v\n * @return {Vertex}\n */\n getNext (v) {\n let c = v;\n while (c._isIntersection) c = c.next;\n return c;\n }\n\n\n /**\n * Unvisited intersection\n * @return {Vertex}\n */\n getFirstIntersect () {\n let v = this._firstIntersect || this.first;\n\n do {\n if (v._isIntersection && !v._visited) break;\n\n v = v.next;\n } while (!v.equals(this.first));\n\n this._firstIntersect = v;\n return v;\n }\n\n\n /**\n * Does the polygon have unvisited vertices\n * @return {Boolean} [description]\n */\n hasUnprocessed () {\n let v = this._lastUnprocessed || this.first;\n do {\n if (v._isIntersection && !v._visited) {\n this._lastUnprocessed = v;\n return true;\n }\n\n v = v.next;\n } while (!v.equals(this.first));\n\n this._lastUnprocessed = null;\n return false;\n }\n\n\n /**\n * The output depends on what you put in, arrays or objects\n * @return {Array.|Array.}\n */\n getPoints () {\n const points = [];\n let v = this.first;\n\n if (this._arrayVertices) {\n do {\n points.push([v.x, v.y]);\n v = v.next;\n } while (v !== this.first);\n } else {\n do {\n points.push({\n x: v.x,\n y: v.y\n });\n v = v.next;\n } while (v !== this.first);\n }\n\n return points;\n }\n\n /**\n * Clip polygon against another one.\n * Result depends on algorithm direction:\n *\n * Intersection: forwards forwards\n * Union: backwars backwards\n * Diff: backwards forwards\n *\n * @param {Polygon} clip\n * @param {Boolean} sourceForwards\n * @param {Boolean} clipForwards\n */\n clip (clip, sourceForwards, clipForwards) {\n let sourceVertex = this.first;\n let clipVertex = clip.first;\n let sourceInClip, clipInSource;\n\n const isUnion = !sourceForwards && !clipForwards;\n const isIntersection = sourceForwards && clipForwards;\n const isDiff = !isUnion && !isIntersection;\n\n // calculate and mark intersections\n do {\n if (!sourceVertex._isIntersection) {\n do {\n if (!clipVertex._isIntersection) {\n const i = new Intersection(\n sourceVertex,\n this.getNext(sourceVertex.next),\n clipVertex, clip.getNext(clipVertex.next)\n );\n\n if (i.valid()) {\n const sourceIntersection = Vertex.createIntersection(i.x, i.y, i.toSource);\n const clipIntersection = Vertex.createIntersection(i.x, i.y, i.toClip);\n\n sourceIntersection._corresponding = clipIntersection;\n clipIntersection._corresponding = sourceIntersection;\n\n this.insertVertex(sourceIntersection, sourceVertex, this.getNext(sourceVertex.next));\n clip.insertVertex(clipIntersection, clipVertex, clip.getNext(clipVertex.next));\n }\n }\n clipVertex = clipVertex.next;\n } while (!clipVertex.equals(clip.first));\n }\n\n sourceVertex = sourceVertex.next;\n } while (!sourceVertex.equals(this.first));\n\n // phase two - identify entry/exit points\n sourceVertex = this.first;\n clipVertex = clip.first;\n\n sourceInClip = sourceVertex.isInside(clip);\n clipInSource = clipVertex.isInside(this);\n\n sourceForwards ^= sourceInClip;\n clipForwards ^= clipInSource;\n\n do {\n if (sourceVertex._isIntersection) {\n sourceVertex._isEntry = sourceForwards;\n sourceForwards = !sourceForwards;\n }\n sourceVertex = sourceVertex.next;\n } while (!sourceVertex.equals(this.first));\n\n do {\n if (clipVertex._isIntersection) {\n clipVertex._isEntry = clipForwards;\n clipForwards = !clipForwards;\n }\n clipVertex = clipVertex.next;\n } while (!clipVertex.equals(clip.first));\n\n // phase three - construct a list of clipped polygons\n let list = [];\n\n while (this.hasUnprocessed()) {\n let current = this.getFirstIntersect();\n // keep format\n const clipped = new Polygon([], this._arrayVertices);\n\n clipped.addVertex(new Vertex(current.x, current.y));\n do {\n current.visit();\n if (current._isEntry) {\n do {\n current = current.next;\n clipped.addVertex(new Vertex(current.x, current.y));\n } while (!current._isIntersection);\n\n } else {\n do {\n current = current.prev;\n clipped.addVertex(new Vertex(current.x, current.y));\n } while (!current._isIntersection);\n }\n current = current._corresponding;\n } while (!current._visited);\n\n list.push(clipped.getPoints());\n }\n\n if (list.length === 0) {\n if (isUnion) {\n if (sourceInClip) list.push(clip.getPoints());\n else if (clipInSource) list.push(this.getPoints());\n else list.push(this.getPoints(), clip.getPoints());\n } else if (isIntersection) { // intersection\n if (sourceInClip) list.push(this.getPoints());\n else if (clipInSource) list.push(clip.getPoints());\n } else { // diff\n if (sourceInClip) list.push(clip.getPoints(), this.getPoints());\n else if (clipInSource) list.push(this.getPoints(), clip.getPoints());\n else list.push(this.getPoints());\n }\n if (list.length === 0) list = null;\n }\n\n return list;\n }\n}\n","import Polygon from './polygon';\n\n/**\n * Clip driver\n * @param {Array.>} polygonA\n * @param {Array.>} polygonB\n * @param {Boolean} sourceForwards\n * @param {Boolean} clipForwards\n * @return {Array.>}\n */\nexport default function (polygonA, polygonB, eA, eB) {\n const source = new Polygon(polygonA);\n const clip = new Polygon(polygonB);\n return source.clip(clip, eA, eB);\n}\n","import boolean from './clip';\n\n/**\n * @param {Array.|Array.} polygonA\n * @param {Array.|Array.} polygonB\n * @return {Array.>|Array.|Null}\n */\nexport function union (polygonA, polygonB) {\n return boolean(polygonA, polygonB, false, false);\n}\n\n/**\n * @param {Array.|Array.} polygonA\n * @param {Array.|Array.} polygonB\n * @return {Array.>|Array.>|Null}\n */\nexport function intersection (polygonA, polygonB) {\n return boolean(polygonA, polygonB, true, true);\n}\n\n/**\n * @param {Array.|Array.} polygonA\n * @param {Array.|Array.} polygonB\n * @return {Array.>|Array.>|Null}\n */\nexport function diff (polygonA, polygonB) {\n return boolean(polygonA, polygonB, false, true);\n}\n\nexport const clip = boolean;\n"],"names":["Vertex","x","y","arguments","length","Array","isArray","this","next","prev","_corresponding","_distance","_isEntry","_isIntersection","_visited","createIntersection","distance","vertex","visit","equals","v","isInside","poly","let","oddNodes","first","Intersection","s1","s2","c1","c2","toSource","toClip","const","d","valid","Polygon","p","arrayVertices","vertices","_lastUnprocessed","_arrayVertices","i","len","addVertex","polygonA","polygonB","eA","eB","source","clip","insertVertex","start","end","curr","getNext","c","getFirstIntersect","_firstIntersect","hasUnprocessed","getPoints","points","push","sourceForwards","clipForwards","sourceInClip","clipInSource","sourceVertex","clipVertex","isUnion","isIntersection","sourceIntersection","clipIntersection","list","current","clipped","boolean"],"mappings":";;;;;;;;iMAAe,IAAMA,EAQnB,SAAaC,EAAGC,GACW,IAArBC,UAAUC,SAERC,MAAMC,QAAQL,IAChBC,EAAID,EAAE,GACNA,EAAIA,EAAE,KAENC,EAAID,EAAEC,EACND,EAAIA,EAAEA,IAQVM,KAAKN,EAAIA,EAMTM,KAAKL,EAAIA,EAMTK,KAAKC,KAAO,KAMZD,KAAKE,KAAO,KAKZF,KAAKG,eAAiB,KAKtBH,KAAKI,UAAY,EAMjBJ,KAAKK,UAAW,EAMhBL,KAAKM,iBAAkB,EAMvBN,KAAKO,UAAW,GAWpBd,EAASe,4BAAoBd,EAAGC,EAAGc,GACjC,IAAQC,EAAS,IAAIjB,EAAOC,EAAGC,GAI/B,OAHEe,EAAON,UAAYK,EACnBC,EAAOJ,iBAAkB,EACzBI,EAAOL,UAAW,EACXK,GAOXjB,YAAEkB,iBACEX,KAAKO,UAAW,EACY,OAAxBP,KAAKG,gBAA4BH,KAAKG,eAAeI,UACrDP,KAAKG,eAAeQ,SAU5BlB,YAAEmB,gBAAQC,GACN,OAAOb,KAAKN,IAAMmB,EAAEnB,GAAKM,KAAKL,IAAMkB,EAAElB,GAW1CF,YAAEqB,kBAAUC,GACRC,IAAIC,GAAW,EACXP,EAASK,EAAKG,MACdjB,EAAOS,EAAOT,KACZP,EAAIM,KAAKN,EACTC,EAAIK,KAAKL,EAEf,IACOe,EAAOf,EAAIA,GAAKM,EAAKN,GAAOA,GAC5BM,EAAON,EAAIA,GAAKe,EAAOf,GAAKA,KAC5Be,EAAOhB,GAAKA,GAAKO,EAAKP,GAAKA,KAC9BuB,GAAaP,EAAOhB,GAAKC,EAAIe,EAAOf,IAC7BM,EAAKN,EAAIe,EAAOf,IAAMM,EAAKP,EAAIgB,EAAOhB,GAAKA,GAItDO,GADES,EAASA,EAAOT,MACFA,MAAQc,EAAKG,aACnBR,EAAOE,OAAOG,EAAKG,QAE/B,OAASD,GCzII,IAAME,EASnB,SAAYC,EAAIC,EAAIC,EAAIC,GAKtBvB,KAAKN,EAAI,EAKTM,KAAKL,EAAI,EAKTK,KAAKwB,SAAW,EAKhBxB,KAAKyB,OAAS,EAEdC,IAAMC,GAAKJ,EAAG5B,EAAI2B,EAAG3B,IAAM0B,EAAG3B,EAAI0B,EAAG1B,IAAM6B,EAAG7B,EAAI4B,EAAG5B,IAAM2B,EAAG1B,EAAIyB,EAAGzB,GAE3D,IAANgC,IAKN3B,KAAOwB,WAAaD,EAAG7B,EAAI4B,EAAG5B,IAAM0B,EAAGzB,EAAI2B,EAAG3B,IAAM4B,EAAG5B,EAAI2B,EAAG3B,IAAMyB,EAAG1B,EAAI4B,EAAG5B,IAAMiC,EAKpF3B,KAAOyB,SAAWJ,EAAG3B,EAAI0B,EAAG1B,IAAM0B,EAAGzB,EAAI2B,EAAG3B,IAAM0B,EAAG1B,EAAIyB,EAAGzB,IAAMyB,EAAG1B,EAAI4B,EAAG5B,IAAMiC,EAE5E3B,KAAK4B,UACP5B,KAAON,EAAI0B,EAAG1B,EAAIM,KAAKwB,UAAYH,EAAG3B,EAAI0B,EAAG1B,GAC7CM,KAAOL,EAAIyB,EAAGzB,EAAIK,KAAKwB,UAAYH,EAAG1B,EAAIyB,EAAGzB,MAQnDwB,YAAES,iBACI,OAAQ,EAAI5B,KAAKwB,UAAYxB,KAAKwB,SAAW,GAAO,EAAIxB,KAAKyB,QAAUzB,KAAKyB,OAAS,GCpD3F,IAAqBI,EAQnB,SAAaC,EAAGC,GAKd/B,KAAKkB,MAAQ,KAKblB,KAAKgC,SAAW,EAKhBhC,KAAKiC,iBAAmB,KAM1BjC,KAAOkC,oBAA2C,IAAlBH,EAC5BjC,MAAQC,QAAQ+B,EAAE,IAChBC,EAEJ,IAAKf,IAAImB,EAAI,EAAGC,EAAMN,EAAEjC,OAAQsC,EAAIC,EAAKD,SAClCE,UAAU,IAAI5C,EAAOqC,EAAEK,MC5BnB,WAAUG,EAAUC,EAAUC,EAAIC,GAC/Cf,IAAMgB,EAAS,IAAIb,EAAQS,GACrBK,EAAO,IAAId,EAAQU,GACzB,OAAOG,EAAOC,KAAKA,EAAMH,EAAIC,GDoC/BZ,YAAEQ,mBAAW3B,GACT,GAAmB,OAAfV,KAAKkB,MACPlB,KAAKkB,MAAaR,EAClBV,KAAKkB,MAAMjB,KAAOS,EAClBV,KAAKkB,MAAMhB,KAAOQ,MACb,CACLgB,IAAMzB,EAAOD,KAAKkB,MACZhB,EAAOD,EAAKC,KAElBD,EAAKC,KAASQ,EACdA,EAAOT,KAAOA,EACdS,EAAOR,KAAOA,EACdA,EAAKD,KAASS,EAEhBV,KAAKgC,YAWTH,YAAEe,sBAAclC,EAAQmC,EAAOC,GAG3B,IAFA9B,IAAId,EAAM6C,EAAOF,GAETE,EAAKnC,OAAOkC,IAAQC,EAAK3C,UAAYM,EAAON,WAClD2C,EAAOA,EAAK9C,KAGdS,EAAOT,KAAO8C,EACd7C,EAAc6C,EAAK7C,KAEnBQ,EAAOR,KAAOA,EACdA,EAAKD,KAASS,EACdqC,EAAK7C,KAASQ,EAEdV,KAAKgC,YAQTH,YAAEmB,iBAASnC,GAET,IADEG,IAAIiC,EAAIpC,EACDoC,EAAE3C,iBAAiB2C,EAAIA,EAAEhD,KAClC,OAASgD,GAQXpB,YAAEqB,6BACA,IAAMrC,EAAIb,KAAKmD,iBAAmBnD,KAAKkB,MAErC,EAAG,CACH,GAAML,EAAEP,kBAAoBO,EAAEN,SAAU,MAEtCM,EAAIA,EAAEZ,YACEY,EAAED,OAAOZ,KAAKkB,QAG1B,OADElB,KAAKmD,gBAAkBtC,EAChBA,GAQXgB,YAAEuB,8BACMvC,EAAIb,KAAKiC,kBAAoBjC,KAAKkB,MACtC,EAAG,CACH,GAAML,EAAEP,kBAAoBO,EAAEN,SAE5B,YADO0B,iBAAmBpB,GACjB,EAGTA,EAAIA,EAAEZ,YACEY,EAAED,OAAOZ,KAAKkB,QAG1B,OADElB,KAAKiC,iBAAmB,MACjB,GAQXJ,YAAEwB,qBACE3B,IAAM4B,EAAS,GACXzC,EAAIb,KAAKkB,MAEb,GAAIlB,KAAKkC,eACP,GACEoB,EAAOC,KAAK,CAAC1C,EAAEnB,EAAGmB,EAAElB,IACpBkB,EAAIA,EAAEZ,WACCY,IAAMb,KAAKkB,YAEpB,GACAoC,EAASC,KAAK,CACV7D,EAAGmB,EAAEnB,EACLC,EAAGkB,EAAElB,IAEPkB,EAAIA,EAAEZ,WACCY,IAAMb,KAAKkB,OAGxB,OAASoC,GAeXzB,YAAEc,cAAMA,EAAMa,EAAgBC,OAGtBC,EAAcC,EAFdC,EAAe5D,KAAKkB,MACpB2C,EAAalB,EAAKzB,MAGhB4C,GAAkBN,IAAmBC,EACrCM,EAAiBP,GAAkBC,EAIzC,EAAG,CACD,IAAKG,EAAatD,gBAChB,EAAG,CACD,IAAKuD,EAAWvD,gBAAiB,CAC/BoB,IAAMS,EAAI,IAAIhB,EACZyC,OACKZ,QAAQY,EAAa3D,MAC5B4D,EAAclB,EAAKK,QAAQa,EAAW5D,OAGtC,GAAIkC,EAAEP,QAAS,CACf,IAAQoC,EAAqBvE,EAAOe,mBAAmB2B,EAAEzC,EAAGyC,EAAExC,EAAGwC,EAAEX,UAC3DyC,EAAqBxE,EAAOe,mBAAmB2B,EAAEzC,EAAGyC,EAAExC,EAAGwC,EAAEV,QAEjEuC,EAAmB7D,eAAiB8D,EACpCA,EAAiB9D,eAAmB6D,OAE/BpB,aAAaoB,EAAoBJ,OAAmBZ,QAAQY,EAAa3D,OAC9E0C,EAAKC,aAAaqB,EAAkBJ,EAAYlB,EAAKK,QAAQa,EAAW5D,QAG5E4D,EAAaA,EAAW5D,YAChB4D,EAAWjD,OAAO+B,EAAKzB,QAGnC0C,EAAeA,EAAa3D,YACpB2D,EAAahD,OAAOZ,KAAKkB,QAGnC0C,EAAe5D,KAAKkB,MACpB2C,EAAelB,EAAKzB,MAKtBsC,GAHAE,EAAiBE,EAAa9C,SAAS6B,GAIvCc,GAHAE,EAAiBE,EAAW/C,SAASd,MAKnC,GACM4D,EAAatD,kBACfsD,EAAavD,SAAWmD,EACxBA,GAAkBA,GAEpBI,EAAeA,EAAa3D,YACpB2D,EAAahD,OAAOZ,KAAKkB,QAEnC,GACM2C,EAAWvD,kBACbuD,EAAWxD,SAAWoD,EACtBA,GAAgBA,GAElBI,EAAaA,EAAW5D,YAChB4D,EAAWjD,OAAO+B,EAAKzB,QAKjC,IAFAF,IAAIkD,EAAO,GAEJlE,KAAKoD,kBAAkB,CAC9B,IAAMe,OAAejB,oBAEbkB,EAAU,IAAIvC,EAAQ,QAASK,gBAErCkC,EAAQ/B,UAAU,IAAI5C,EAAO0E,EAAQzE,EAAGyE,EAAQxE,IAChD,EAAG,CAED,GADAwE,EAAQxD,QACJwD,EAAQ9D,SACV,GACE8D,EAAUA,EAAQlE,KAClBmE,EAAQ/B,UAAU,IAAI5C,EAAO0E,EAAQzE,EAAGyE,EAAQxE,WACxCwE,EAAQ7D,sBAGlB,GACE6D,EAAUA,EAAQjE,KAClBkE,EAAQ/B,UAAU,IAAI5C,EAAO0E,EAAQzE,EAAGyE,EAAQxE,WACxCwE,EAAQ7D,iBAEpB6D,EAAUA,EAAQhE,sBACVgE,EAAQ5D,UAEpB2D,EAAOX,KAAKa,EAAQf,aAmBtB,OAhBsB,IAAhBa,EAAKrE,SACHiE,EACEJ,EAAmBQ,EAAKX,KAAKZ,EAAKU,aAC7BM,EAAcO,EAAKX,KAAKvD,KAAKqD,aACfa,EAAKX,KAAKvD,KAAKqD,YAAaV,EAAKU,aAC/CU,EACLL,EAAmBQ,EAAKX,KAAKvD,KAAKqD,aAC7BM,GAAcO,EAAKX,KAAKZ,EAAKU,aAElCK,EAAmBQ,EAAKX,KAAKZ,EAAKU,YAAarD,KAAKqD,aAC/CM,EAAcO,EAAKX,KAAKvD,KAAKqD,YAAaV,EAAKU,aACjCa,EAAKX,KAAKvD,KAAKqD,aAEpB,IAAhBa,EAAKrE,SAAcqE,EAAO,OAGzBA,OEnQEvB,EAAO0B,UAtBb,SAAgB/B,EAAUC,GAC/B,OAAO8B,EAAQ/B,EAAUC,GAAU,GAAO,mBAQrC,SAAuBD,EAAUC,GACtC,OAAO8B,EAAQ/B,EAAUC,GAAU,GAAM,WAQpC,SAAeD,EAAUC,GAC9B,OAAO8B,EAAQ/B,EAAUC,GAAU,GAAO"} -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { FontFeatureExtractor } from "./feature-extract" 2 | import * as featureio from "./featureio" 3 | import * as visualize from "./visualize" 4 | 5 | const NODEJS = typeof process != "undefined" 6 | const WEB_BROWSER = typeof document != "undefined" 7 | 8 | const log = console.log.bind(console) 9 | 10 | 11 | function cli_usage() { 12 | const argv = process.argv.slice(1) 13 | console.error(`usage: ${argv[0]} `) 14 | process.exit(1) 15 | } 16 | 17 | async function main() { 18 | if (!WEB_BROWSER && (process.argv.length < 4 || process.argv.includes("-h"))) { 19 | cli_usage() 20 | } 21 | 22 | // XXX DEBUG 23 | if (NODEJS) { 24 | require("./train").testTrain() 25 | return 26 | } 27 | 28 | let fe = new FontFeatureExtractor() 29 | 30 | const fontFile = ( 31 | WEB_BROWSER ? "fonts/Inter-Regular.otf" : 32 | // WEB_BROWSER ? "fonts/HelveticaNeueLTStd-Roman.otf" : 33 | // WEB_BROWSER ? "fonts/Georgia.ttf" : 34 | process.argv[2] 35 | ) 36 | WEB_BROWSER && log("loading font") 37 | const font = await fe.loadFont(fontFile) 38 | 39 | const glyphPairIterator = createGlyphPairIterator(font) 40 | 41 | if (WEB_BROWSER) { 42 | visualize.createCanvas("canvas") 43 | for (let pair of glyphPairIterator) { 44 | // log("computing features of pair", pair) 45 | let data = fe.computeFeaturesForGlyphPair(pair[0], pair[1]) 46 | // log("features:", data.spacing, data.features) 47 | if (WEB_BROWSER) { 48 | visualize.setShapes(fe, data.L, data.R) 49 | // wait for a key stroke 50 | // TODO: ArrowLeft to go backwards 51 | await keyStrokeEvent("Enter", " ", "ArrowRight") 52 | // await new Promise(r => setTimeout(r, 1000)) 53 | } 54 | } 55 | } else { 56 | const outfile = process.argv[3] 57 | // const fontName = ( 58 | // nameEntryStr(font.names.fontFamily) + " " + 59 | // nameEntryStr(font.names.fontSubfamily) 60 | // ) 61 | log(`computing ${glyphPairIterator.length} pairs, writing to ${outfile}`) 62 | console.time("completed in") 63 | let w = featureio.createWriter(outfile, glyphPairIterator.length) 64 | let tmpFloat64Array = null 65 | // let limit = 2 66 | for (let pair of glyphPairIterator) { 67 | tmpFloat64Array = fe.computeFeaturesForGlyphPair(pair[0], pair[1], tmpFloat64Array).values 68 | w.write(tmpFloat64Array) 69 | // log("features:", data.spacing, data.features) 70 | // if (--limit == 0) break 71 | } 72 | w.end() 73 | console.timeEnd("completed in") 74 | // if (limit) { log("featureio.readSync =>", featureio.readSync(outfile)) } 75 | } 76 | } 77 | 78 | 79 | function nameEntryStr(name) { 80 | return (name.en || name[Object.keys(name)]).trim() 81 | } 82 | 83 | 84 | function keyStrokeEvent(...keyNames) { 85 | return new Promise(resolve => { 86 | keyNames = new Set(keyNames) 87 | function handler(ev) { 88 | // log(ev.key) 89 | if (keyNames.has(ev.key)) { 90 | document.removeEventListener("keydown", handler) 91 | resolve(ev.key) 92 | ev.stopPropagation() 93 | ev.preventDefault() 94 | } 95 | } 96 | document.addEventListener("keydown", handler) 97 | }) 98 | } 99 | 100 | 101 | function unicodeIsPrivateSpace(cp) { 102 | return cp >= 0xE000 && cp < 0xF900 103 | } 104 | 105 | 106 | function numUniquePairs(count) { 107 | // includes self+self 108 | return count*(count+1)/2 109 | } 110 | 111 | 112 | function createGlyphPairIterator(font) { 113 | // load and filter glyphs to include 114 | const glyphs = Object.values(font.glyphs.glyphs).filter(g => { 115 | // exclude glyphs like .notdef and .null 116 | if (g.name[0] == ".") { return false } 117 | 118 | // exclude glyphs without codepoint mappings (e.g. component-only glyphs) 119 | if (!g.unicode) { return false } 120 | 121 | // exclude glyphs with only private-space codepoint mapping(s) 122 | if (g.unicodes.every(unicodeIsPrivateSpace)) { return false } 123 | 124 | // exclude empty glyphs 125 | const bbox = g.path.getBoundingBox() 126 | if (bbox.x1 == 0 && bbox.x2 == 0) { return false } 127 | 128 | // include all other glyphs 129 | return true 130 | }) 131 | const nglyphs = glyphs.length 132 | 133 | // triangle visitation 134 | // 135 | // A = 1 136 | // 1 2 3 137 | // 1 1 1 138 | // 139 | // A = 2 140 | // 2 3 141 | // 2 2 142 | // 143 | // A = 3 144 | // 3 145 | // 3 146 | // 147 | // for (let i = 0; i < count; i++) { 148 | // A = glyphs[j] 149 | // for (let j = i; j < count; j++) { 150 | // B = glyphs[j] 151 | // } 152 | // } 153 | // 154 | return { 155 | length: numUniquePairs(nglyphs), 156 | [Symbol.iterator]() { 157 | // interator state 158 | let i = 0 // primary index 159 | let j = nglyphs // secondary index 160 | // result object, to avoid GC thrash. [0]=glyphs[i], [1]=glyphs[j] 161 | const res = { value: [null,null] } 162 | return { next() { 163 | if (i >= nglyphs) { 164 | return { done:true } 165 | } 166 | if (j >= nglyphs) { 167 | j = i 168 | res.value[0] = glyphs[i++] 169 | } 170 | res.value[1] = glyphs[j++] 171 | let k = `${i},${j}` 172 | return res 173 | } } 174 | } 175 | } 176 | } 177 | 178 | 179 | main().catch(err => { 180 | console.error(err.stack||String(err)) 181 | NODEJS && process.exit(1) 182 | }) 183 | -------------------------------------------------------------------------------- /src/ml-kern.js: -------------------------------------------------------------------------------- 1 | const log = console.log.bind(console) 2 | 3 | // OTGlyphShapeFontSize is the origin size and relates to polygon density etc. 4 | // Changing this means also tuning svgPathContours(density) 5 | const OTGlyphShapeFontSize = 512 6 | 7 | // FeatureRayCount controls how many ray features to compute. 8 | // A larger number means higher density. 9 | const FeatureRayCount = 32 10 | 11 | let font = null 12 | 13 | 14 | const canvas = Canvas.create("canvas", (c, g) => { 15 | g.font = '11px Inter, sans-serif' 16 | 17 | // virtual canvas size; actual size is scaled 18 | const canvasSize = new Vec2(1024,512) 19 | 20 | c.transform = () => { 21 | // const scale = c.width / canvasSize 22 | // const scale = c.height / canvasSize 23 | const scale = Math.min(c.width/canvasSize[0], c.height/canvasSize[1]) 24 | c.setOrigin( 25 | // (c.width - Math.min(c.width, c.height))/2, // left-aligned within canvasSize 26 | (c.width - canvasSize[0]*scale)/2, // centered 27 | OTGlyphShapeFontSize * scale / 1.3 28 | ) 29 | g.scale(scale, scale) 30 | } 31 | 32 | c.draw = time => { 33 | const px = c.px.bind(c) 34 | 35 | if (font) { 36 | const fontSize = canvasSize[0] * 0.35 37 | const glyphDrawScale = fontSize / OTGlyphShapeFontSize 38 | const fontScale = OTGlyphShapeFontSize / font.unitsPerEm 39 | 40 | let shape1 = OTGlyphShape.get(font, "Å") 41 | let shape2 = OTGlyphShape.get(font, "j") 42 | 43 | // Deciding on an approach for vertical space; the feature sample space. 44 | // 45 | // box approach 1: 46 | // compute all glyphs in the same vertical space so that we can reuse a glyph shape 47 | // in multiple pair comparisons. This is prooobably a good idea. Perhaps not. 48 | // For short glyphs this will reduce their resolution. 49 | // Code for using the bounding box of the font: 50 | //const minY = -font.tables.head.yMax * fontScale 51 | //const maxY = -font.tables.head.yMin * fontScale 52 | // Code for using the ascender & descender of the font: 53 | const minY = -font.ascender * fontScale 54 | const maxY = -font.descender * fontScale 55 | // 56 | // box approach 2: 57 | // Compute each pair with tightly-fitting y-axis extremes 58 | // 59 | // This is slower, since we need to compute the features for every pair, rather than 60 | // compute just once per glyph. 61 | // However, the resulting features are essentially normalized in terms of comparison 62 | // across fonts. I.e. say we sample two fonts with different ascender and descender 63 | // values. In this case then the feature data would be completely different for two 64 | // identical shapes, as seen in Figure 1. 65 | // 66 | // Figure 1: "box approach 1"; the issue with varying height: 67 | // 68 | // Font 1 Font 2 69 | // minY ————————————————————————— minY ————————————————————————— 70 | // f ---------- ---------- f ---------- ---------- 71 | // f ---------- ---------- f \ /-- ----/\ 72 | // f ---------- ---------- f \ /--- ---/——\ 73 | // f \ /-- ----/\ f \/---- --/ \ 74 | // f \ /--- ---/——\ f ---------- ---------- 75 | // f \/---- --/ \ f ---------- ---------- 76 | // f ---------- ---------- f ---------- ---------- 77 | // maxY ————————————————————————— maxY ————————————————————————— 78 | // 79 | // 80 | // Figure 2: "box approach 2"; normalizing height for each pair. 81 | // 82 | // Font 1 Font 2 83 | // minY ————————————————————————— minY ————————————————————————— 84 | // f \ /-- ----/\ f \ /-- ----/\ 85 | // f \ /--- ---/——\ f \ /--- ---/——\ 86 | // f \/---- --/ \ f \/---- --/ \ 87 | // maxY ————————————————————————— maxY ————————————————————————— 88 | // 89 | // Code for using the bounding box of the glyph pair: 90 | //const minY = Math.min(shape1.bbox.minY, shape2.bbox.minY) 91 | //const maxY = Math.max(shape1.bbox.maxY, shape2.bbox.maxY) 92 | 93 | if (shape1.features.left.length == 0 || shape2.features.left.length == 0) { 94 | // let minY = Math.min(shape1.bbox.minY, shape2.bbox.minY) 95 | // let maxY = Math.max(shape1.bbox.maxY, shape2.bbox.maxY) 96 | // perform raycasting to extract whistepace features 97 | shape1.computeFeatures(minY, maxY, /* includeRays for drawing = */ true) 98 | shape2.computeFeatures(minY, maxY, /* includeRays for drawing = */ true) 99 | log(`${shape1}.features`, shape1.features) 100 | log(`${shape2}.features`, shape2.features) 101 | } 102 | 103 | // draw: pairs 104 | let x = 0 105 | x = drawPair(x, shape1, shape2) 106 | x = drawPair(x, shape2, shape1) 107 | 108 | function drawPair(x, shape1, shape2) { 109 | // spacing = left-glyph-rsb + right-glyph-lsb + kerning 110 | const kerning = font.getKerningValue(shape1.glyph, shape2.glyph) 111 | const spacing = shape1.RSB + shape2.LSB + kerning 112 | const spacingPct = spacing / font.unitsPerEm 113 | 114 | // TODO: figure out what spacing% shuld be relative to in terms of the 115 | // result value [ML: labeled feature] Union bbox? UPM? 116 | // Needs to be a value that makes sense for all pairs of shapes. 117 | 118 | shape1.draw(g, x, 0, fontSize, "right") 119 | x += shape1.bbox.width * glyphDrawScale 120 | 121 | // spacing: visualize kerning 122 | // let spacingBetweenPairs = (spacing*fontScale) * glyphDrawScale 123 | // spacing: LSB & RSB 124 | let spacingBetweenPairs = (shape1.paddingRight + shape2.paddingLeft)*glyphDrawScale + 30 125 | let labelpos = new Vec2(x + spacingBetweenPairs/2, maxY*glyphDrawScale + 20) 126 | x += spacingBetweenPairs 127 | shape2.draw(g, x, 0, fontSize, "left") 128 | 129 | // text with kerning value 130 | const spacingText = ( 131 | Math.round(spacing*100) == 0 ? "0" : 132 | `${(spacingPct*100).toFixed(1)}% (${spacing})` 133 | ) 134 | g.fillStyle = "black" 135 | g.textAlign = "center" 136 | g.strokeStyle = "white" 137 | g.lineWidth = g.dp * 4 138 | g.font = `${Math.round(12 * g.dp)}px Inter, sans-serif` 139 | g.strokeText(spacingText, labelpos[0], labelpos[1]) 140 | g.fillText(spacingText, labelpos[0], labelpos[1]) 141 | // g.drawCircle(labelpos.addY(10), g.dp*2, "black") 142 | 143 | return x + shape2.width*glyphDrawScale + 40 144 | } 145 | 146 | 147 | // // draw: parade 148 | // let x = 0, spacing = 20 149 | // shape1.draw(g, x, 0, fontSize, "left") 150 | // x += shape1.width*glyphDrawScale + spacing 151 | 152 | // shape1.draw(g, x, 0, fontSize, "right") 153 | // x += shape1.width*glyphDrawScale + spacing 154 | 155 | // shape2.draw(g, x, 0, fontSize, "left") 156 | // x += shape2.width*glyphDrawScale + spacing 157 | 158 | // shape2.draw(g, x, 0, fontSize, "right") 159 | // x += shape2.width*glyphDrawScale + spacing 160 | } 161 | 162 | g.drawOrigin(red.alpha(0.7)) 163 | // c.needsDraw = true 164 | } // draw 165 | }) 166 | 167 | 168 | // once fonts load, redraw 169 | window.addEventListener("load", () => { 170 | canvas.needsDraw = true 171 | }) 172 | 173 | 174 | 175 | class PolyShape { 176 | constructor(paths, simplifyThreshold) { 177 | if (simplifyThreshold === undefined) { 178 | simplifyThreshold = 0.1 179 | } 180 | 181 | // convert curves into discrete points 182 | this.polygons = svgPathContours(paths, /*density*/0.3) 183 | 184 | // simplify polygons and convert to vertex arrays (plus compute bbox) 185 | const bbox = this.bbox = { 186 | minX: Infinity, minY: Infinity, 187 | maxX: -Infinity, maxY: -Infinity, 188 | width: 0, height: 0, 189 | } 190 | for (let i = 0; i < this.polygons.length; i++) { 191 | let points = this.polygons[i] 192 | if (simplifyThreshold > 0) { 193 | points = simplifyPath(points, simplifyThreshold) 194 | } 195 | let a = new Float64Array(points.length * 2) 196 | for (let i = 0; i < points.length; i++) { 197 | let [x, y] = points[i] 198 | a[i * 2] = x 199 | a[i * 2 + 1] = y 200 | bbox.minX = Math.min(bbox.minX, x) 201 | bbox.minY = Math.min(bbox.minY, y) 202 | bbox.maxX = Math.max(bbox.maxX, x) 203 | bbox.maxY = Math.max(bbox.maxY, y) 204 | } 205 | this.polygons[i] = a 206 | } 207 | 208 | // round bbox extremes 209 | bbox.minX = Math.floor(bbox.minX) 210 | bbox.minY = Math.floor(bbox.minY) 211 | bbox.maxX = Math.ceil(bbox.maxX) 212 | bbox.maxY = Math.ceil(bbox.maxY) 213 | 214 | // calc bbox width and height for convenience 215 | bbox.width = bbox.maxX - bbox.minX 216 | bbox.height = bbox.maxY - bbox.minY 217 | 218 | this.paddingLeft = 0 // i.e. sidebearing for fonts 219 | this.paddingRight = 0 // i.e. sidebearing for fonts 220 | 221 | // features (populated by computeFeatures) 222 | this.features = { 223 | left: [], // : float[] -- velocity factors 224 | right: [], // : float[] -- velocity factors 225 | leftRays: [], // : [Vec2,Vec2][] -- ray lines 226 | rightRays: [], // : [Vec2,Vec2][] -- ray lines 227 | } 228 | } 229 | 230 | // computeFeatures takes the shape's polygons and computes the left and right 231 | // whitespace, described by lists of distances in the input shape's coordinate system. 232 | computeFeatures(minY, maxY, includeRays) { 233 | // raycast 234 | // console.time("computeFeatures") 235 | const yStride = Math.ceil((maxY-minY)/FeatureRayCount) 236 | const padding = 0 // some extra space around the bbox (for debugging) 237 | const minX = this.bbox.minX - padding 238 | const maxX = this.bbox.maxX + padding 239 | const maxDistance = maxX - minX // stop ray from traveling further 240 | const hitTesters = this.polygons.map(polygon => new PolyPointHitTester(polygon)) 241 | this.features.left.length = 0 242 | this.features.right.length = 0 243 | this.features.leftRays.length = 0 244 | this.features.rightRays.length = 0 245 | for (let y = minY, rayNum = 0; y <= maxY && rayNum < FeatureRayCount; y += yStride) { 246 | let xl = minX 247 | let xr = maxX 248 | let endl = raycastX(hitTesters, xl, y, 1, maxDistance) 249 | let endr = raycastX(hitTesters, xr, y, -1, maxDistance) 250 | // Encode features as normalized x-axis "velocity": 251 | this.features.left.push((endl-xl)/maxDistance) 252 | this.features.right.push((xr-endr)/maxDistance) 253 | if (includeRays) { 254 | // include rays, useful for drawing 255 | this.features.leftRays.push([ new Vec2(xl,y), new Vec2(endl,y) ]) 256 | this.features.rightRays.push([ new Vec2(xr,y), new Vec2(endr,y) ]) 257 | } 258 | } 259 | // console.timeEnd("computeFeatures") 260 | return this.features 261 | } 262 | 263 | get width() { 264 | return this.paddingLeft + this.bbox.width + this.paddingRight 265 | } 266 | 267 | draw(g, x, y, scale, featureSides /* "left"|"right"|(("left"|"right")[]) */, options) { 268 | options = Object.assign({ 269 | // default options 270 | valueLabels: true, // draw value labels 271 | }, options || {}) 272 | // translate in caller coordinates and scale. 273 | // adjustX adjusts the bbox _after_ scaling (with scaling applied), in shape coordinates. 274 | let adjustX = -this.bbox.minX //+ this.paddingLeft 275 | g.withTransform([scale, 0, x + (adjustX * scale), 0, scale, y], () => { 276 | 277 | if (!Array.isArray(featureSides)) { 278 | featureSides = [ featureSides ] 279 | } 280 | 281 | // // draw triangles (requires tess2 to be loaded in env) 282 | // let tesselation = tesselate(this.polygons) 283 | // drawTriangles(g, tesselation.elements, tesselation.vertices) 284 | 285 | // draw full-width box 286 | g.lineWidth = g.dp 287 | g.strokeStyle = "rgba(0,200,200,0.5)" 288 | g.strokeRect( 289 | this.bbox.minX - this.paddingLeft, 290 | this.bbox.minY, 291 | this.width, 292 | this.bbox.height, 293 | ) 294 | 295 | let polygonColors = [ red, blue, pink, orange ] 296 | 297 | // draw polygon lines 298 | for (let i = 0; i < this.polygons.length; i++) { 299 | let vertexes = this.polygons[i] 300 | let color = polygonColors[i % polygonColors.length] 301 | // log(`path ${i} colored ${color}`) 302 | g.lineWidth = g.dp 303 | g.strokeStyle = color 304 | g.beginPath() 305 | for (let j = 0; j < vertexes.length; j += 2) { 306 | let x = vertexes[j], y = vertexes[j + 1] 307 | if (j == 0) { 308 | g.moveTo(x, y) 309 | } else { 310 | g.lineTo(x, y) 311 | } 312 | } 313 | g.stroke() 314 | } 315 | 316 | // draw polygon points 317 | for (let i = 0; i < this.polygons.length; i++) { 318 | let vertexes = this.polygons[i] 319 | let color = polygonColors[i % polygonColors.length] 320 | // log(`path ${i} colored ${color}`) 321 | for (let j = 0; j < vertexes.length; j += 2) { 322 | g.drawCircle([ vertexes[j], vertexes[j + 1] ], g.dp*1.5, color) 323 | } 324 | } 325 | 326 | // draw feature rays 327 | if (g.pixelScale / g.dp < 1.3) { 328 | // don't show values if text will be too cramped 329 | options.valueLabels = false 330 | } 331 | for (let side of featureSides) { 332 | const rays = this.features[side + "Rays"] 333 | const values = this.features[side] 334 | const fontSize = 9 * g.dp 335 | const labelSpacing = side == "left" ? fontSize/-2 : fontSize/2 336 | g.fillStyle = green 337 | g.textAlign = side == "left" ? "right" : "left" 338 | g.font = `500 ${Math.round(fontSize)}px Inter, sans-serif` 339 | if (rays) for (let i = 0; i < rays.length; i++) { 340 | // draw ray 341 | let [pt1, pt2] = rays[i] 342 | g.drawArrow(pt2, pt1, green, g.dp) 343 | // draw value label 344 | if (options.valueLabels) { 345 | let value = values[i] 346 | let xspace = value < 0.015 ? labelSpacing*2 : labelSpacing 347 | let text = value == 1 ? "•" : Math.round(value*100) 348 | g.fillText(text, pt1[0] + xspace, pt1[1] + fontSize/3) 349 | } 350 | } 351 | } 352 | }) // transform 353 | } 354 | 355 | toString() { 356 | return ( 357 | this.constructor.name + 358 | `(${Math.round(this.width)}×${this.bbox.height} ${this.polygons.length} polys)` 359 | ) 360 | } 361 | } 362 | 363 | 364 | class OTGlyphShape extends PolyShape { 365 | constructor(font, glyph, simplifyThreshold) { 366 | // get flipped glyph.path 367 | let glyphPath = glyph.getPath(0, 0, OTGlyphShapeFontSize) 368 | 369 | // convert OpenType path object to list of SVG-like path segments 370 | let paths = otPathToPaths(glyphPath) 371 | super(paths, simplifyThreshold) 372 | this.font = font 373 | this.glyph = glyph 374 | this.glyphPath = glyphPath 375 | 376 | // sidebearings, which are in UPM (unscaled) 377 | const upmbbox = glyph.getBoundingBox() 378 | this.LSB = upmbbox.x1 379 | this.RSB = glyph.advanceWidth - upmbbox.x1 - (upmbbox.x2 - upmbbox.x1) 380 | 381 | // PolyShape padding, which are in polygon coordinates (scaled) 382 | const scale = OTGlyphShapeFontSize / font.unitsPerEm 383 | this.paddingLeft = this.LSB*scale 384 | this.paddingRight = this.RSB*scale 385 | } 386 | 387 | draw(g, x, y, fontSize, featureSides) { 388 | // draw glyph shape 389 | const scale = fontSize / OTGlyphShapeFontSize 390 | // Draw actual glyph shape in addition to the polygon drawn by PolyShape.draw() 391 | // g.withScale(scale, scale, () => { 392 | // g.withTranslation(x, y, () => { 393 | // this.glyphPath.fill = "rgba(0,0,0,0.2)" // sets g.fillStyle 394 | // this.glyphPath.draw(g) 395 | // }) 396 | // }) 397 | super.draw(g, x, y, scale, featureSides) 398 | } 399 | } 400 | 401 | OTGlyphShape.get = (()=>{ 402 | const cache = new Map() // : Map> 403 | return (font, glyphname) => { 404 | // FIXME: glyphname is used as char; should be actual glyphname 405 | let m = cache.get(font) 406 | if (m) { 407 | let shape = m.get(glyphname) 408 | if (shape) { 409 | return shape 410 | } 411 | } else { 412 | m = new Map() 413 | cache.set(font, m) 414 | } 415 | let glyph = font.charToGlyph(glyphname) 416 | let shape = new OTGlyphShape(font, glyph) 417 | m.set(glyphname, shape) 418 | return shape 419 | } 420 | })() 421 | 422 | 423 | 424 | function raycastX(hitTesters, rayStartX, y, step, maxDistance) { 425 | let distance = 0 426 | let absstep = Math.abs(step) 427 | let x = rayStartX 428 | for (; distance < maxDistance; x += step) { 429 | // log("test", x,y) 430 | for (let hitTester of hitTesters) { 431 | if (hitTester.test(x, y)) { 432 | // log("hit", x) 433 | return x 434 | } 435 | } 436 | distance += absstep 437 | } 438 | return x 439 | } 440 | 441 | 442 | // PolyPointHitTester tests if a point is inside a polygon 443 | class PolyPointHitTester { 444 | // from http://alienryderflex.com/polygon/ 445 | constructor(polygon) { 446 | this.length = polygon.length / 2 447 | this.polygon = polygon 448 | this.constant = new Float64Array(this.length) 449 | this.multiple = new Float64Array(this.length) 450 | // precompute 451 | let j = this.length - 1; 452 | for (let i = 0; i < this.length; i++) { 453 | let x = polygon[i*2] 454 | , y = polygon[i*2 + 1] 455 | , nx = polygon[j*2] 456 | , ny = polygon[j*2 + 1] 457 | 458 | if (ny == y) { 459 | this.constant[i] = x 460 | this.multiple[i] = 0 461 | } else { 462 | this.constant[i] = ( 463 | x 464 | - (y * nx) / (ny - y) 465 | + (y * x) / (ny - y) 466 | ) 467 | this.multiple[i] = (nx - x) / (ny - y) 468 | } 469 | j = i 470 | } 471 | } 472 | 473 | 474 | test(x, y) { 475 | let oddNodes = 0, current = this.polygon[this.polygon.length-1] > y 476 | for (let i = 0; i < this.length; i++) { 477 | let previous = current 478 | current = this.polygon[i*2 + 1] > y 479 | if (current != previous) { 480 | oddNodes ^= y * this.multiple[i] + this.constant[i] < x 481 | } 482 | } 483 | return !!oddNodes 484 | } 485 | 486 | // test(x, y) { 487 | // let j = this.length - 1 488 | // let oddNodes = 0 489 | // for (let i = 0; i < this.length; i++) { 490 | // let cy = this.polygon[i*2 + 1] 491 | // let ny = this.polygon[j*2 + 1] 492 | // if (cy < y && ny >= y || ny < y && cy >= y) { 493 | // oddNodes ^= (y * this.multiple[i] + this.constant[i]) < x 494 | // } 495 | // j = i 496 | // } 497 | // return !!oddNodes 498 | // } 499 | } 500 | 501 | 502 | function otPathToPaths(otpath) { 503 | // convert from array of objects to array of arrays; 504 | // the format other functions like svgPathContours expects. 505 | return otpath.commands.map(s => { 506 | let t = s.type 507 | return ( 508 | t == "Z" ? [t] : 509 | t == "Q" ? [t, s.x1, s.y1, s.x, s.y] : 510 | t == "C" ? [t, s.x1, s.y1, s.x2, s.y2, s.x, s.y] : 511 | [t, s.x, s.y] 512 | ) 513 | }) 514 | } 515 | 516 | 517 | // opentype.load('fonts/Inter-Regular.otf', (err, _font) => { 518 | opentype.load('fonts/Georgia.ttf', (err, _font) => { 519 | log("load-font", {err, _font}) 520 | font = _font 521 | canvas.needsDraw = true 522 | }) 523 | 524 | 525 | 526 | /** 527 | * parse an svg path data string. Generates an Array 528 | * of commands where each command is an Array of the 529 | * form `[command, arg1, arg2, ...]` 530 | * 531 | * @param {String} path 532 | * @return {Array} 533 | */ 534 | function parseSvg(path) { 535 | const num = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig 536 | const parseSvgLength = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0} 537 | const segment = /([astvzqmhlc])([^astvzqmhlc]*)/ig 538 | 539 | function parseValues(args) { 540 | var numbers = args.match(num) 541 | return numbers ? numbers.map(Number) : [] 542 | } 543 | 544 | var data = [] 545 | path.replace(segment, function(_, command, args){ 546 | var type = command.toLowerCase() 547 | args = parseValues(args) 548 | 549 | // overloaded moveTo 550 | if (type == 'm' && args.length > 2) { 551 | data.push([command].concat(args.splice(0, 2))) 552 | type = 'l' 553 | command = command == 'm' ? 'l' : 'L' 554 | } 555 | 556 | while (true) { 557 | if (args.length == parseSvgLength[type]) { 558 | args.unshift(command) 559 | return data.push(args) 560 | } 561 | if (args.length < parseSvgLength[type]) throw new Error('malformed path data') 562 | data.push([command].concat(args.splice(0, parseSvgLength[type]))) 563 | } 564 | }) 565 | return data 566 | } 567 | 568 | 569 | 570 | // tesselate creates triangles from vertices 571 | function tesselate(polygons) { 572 | return Tess2.tesselate({ 573 | contours: polygons, 574 | windingRule: Tess2.WINDING_ODD, 575 | elementType: Tess2.POLYGONS, 576 | polySize: 3, 577 | vertexSize: 2 578 | }) 579 | } 580 | 581 | function drawTriangles(g, elements, vertices) { 582 | for (let i = 0; i < elements.length; i += 3) { 583 | let a = elements[i], b = elements[i+1], c = elements[i+2] 584 | g.drawTriangle( 585 | [vertices[a*2], vertices[a*2+1]], 586 | [vertices[b*2], vertices[b*2+1]], 587 | [vertices[c*2], vertices[c*2+1]], 588 | "rgba(200,10,200,0.5)", 589 | 1, 590 | ) 591 | } 592 | } 593 | 594 | -------------------------------------------------------------------------------- /src/normalize-svg-path.js: -------------------------------------------------------------------------------- 1 | 2 | const TAU = Math.PI * 2 3 | 4 | const mapToEllipse = ({ x, y }, rx, ry, cosphi, sinphi, centerx, centery) => { 5 | x *= rx 6 | y *= ry 7 | 8 | const xp = cosphi * x - sinphi * y 9 | const yp = sinphi * x + cosphi * y 10 | 11 | return { 12 | x: xp + centerx, 13 | y: yp + centery 14 | } 15 | } 16 | 17 | const approxUnitArc = (ang1, ang2) => { 18 | // If 90 degree circular arc, use a constant 19 | // as derived from http://spencermortensen.com/articles/bezier-circle 20 | const a = ang2 === 1.5707963267948966 21 | ? 0.551915024494 22 | : ang2 === -1.5707963267948966 23 | ? -0.551915024494 24 | : 4 / 3 * Math.tan(ang2 / 4) 25 | 26 | const x1 = Math.cos(ang1) 27 | const y1 = Math.sin(ang1) 28 | const x2 = Math.cos(ang1 + ang2) 29 | const y2 = Math.sin(ang1 + ang2) 30 | 31 | return [ 32 | { 33 | x: x1 - y1 * a, 34 | y: y1 + x1 * a 35 | }, 36 | { 37 | x: x2 + y2 * a, 38 | y: y2 - x2 * a 39 | }, 40 | { 41 | x: x2, 42 | y: y2 43 | } 44 | ] 45 | } 46 | 47 | const vectorAngle = (ux, uy, vx, vy) => { 48 | const sign = (ux * vy - uy * vx < 0) ? -1 : 1 49 | 50 | let dot = ux * vx + uy * vy 51 | 52 | if (dot > 1) { 53 | dot = 1 54 | } 55 | 56 | if (dot < -1) { 57 | dot = -1 58 | } 59 | 60 | return sign * Math.acos(dot) 61 | } 62 | 63 | const getArcCenter = ( 64 | px, 65 | py, 66 | cx, 67 | cy, 68 | rx, 69 | ry, 70 | largeArcFlag, 71 | sweepFlag, 72 | sinphi, 73 | cosphi, 74 | pxp, 75 | pyp 76 | ) => { 77 | const rxsq = Math.pow(rx, 2) 78 | const rysq = Math.pow(ry, 2) 79 | const pxpsq = Math.pow(pxp, 2) 80 | const pypsq = Math.pow(pyp, 2) 81 | 82 | let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq) 83 | 84 | if (radicant < 0) { 85 | radicant = 0 86 | } 87 | 88 | radicant /= (rxsq * pypsq) + (rysq * pxpsq) 89 | radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1) 90 | 91 | const centerxp = radicant * rx / ry * pyp 92 | const centeryp = radicant * -ry / rx * pxp 93 | 94 | const centerx = cosphi * centerxp - sinphi * centeryp + (px + cx) / 2 95 | const centery = sinphi * centerxp + cosphi * centeryp + (py + cy) / 2 96 | 97 | const vx1 = (pxp - centerxp) / rx 98 | const vy1 = (pyp - centeryp) / ry 99 | const vx2 = (-pxp - centerxp) / rx 100 | const vy2 = (-pyp - centeryp) / ry 101 | 102 | let ang1 = vectorAngle(1, 0, vx1, vy1) 103 | let ang2 = vectorAngle(vx1, vy1, vx2, vy2) 104 | 105 | if (sweepFlag === 0 && ang2 > 0) { 106 | ang2 -= TAU 107 | } 108 | 109 | if (sweepFlag === 1 && ang2 < 0) { 110 | ang2 += TAU 111 | } 112 | 113 | return [ centerx, centery, ang1, ang2 ] 114 | } 115 | 116 | const arcToBezier = ({ 117 | px, 118 | py, 119 | cx, 120 | cy, 121 | rx, 122 | ry, 123 | xAxisRotation = 0, 124 | largeArcFlag = 0, 125 | sweepFlag = 0 126 | }) => { 127 | const curves = [] 128 | 129 | if (rx === 0 || ry === 0) { 130 | return [] 131 | } 132 | 133 | const sinphi = Math.sin(xAxisRotation * TAU / 360) 134 | const cosphi = Math.cos(xAxisRotation * TAU / 360) 135 | 136 | const pxp = cosphi * (px - cx) / 2 + sinphi * (py - cy) / 2 137 | const pyp = -sinphi * (px - cx) / 2 + cosphi * (py - cy) / 2 138 | 139 | if (pxp === 0 && pyp === 0) { 140 | return [] 141 | } 142 | 143 | rx = Math.abs(rx) 144 | ry = Math.abs(ry) 145 | 146 | const lambda = 147 | Math.pow(pxp, 2) / Math.pow(rx, 2) + 148 | Math.pow(pyp, 2) / Math.pow(ry, 2) 149 | 150 | if (lambda > 1) { 151 | rx *= Math.sqrt(lambda) 152 | ry *= Math.sqrt(lambda) 153 | } 154 | 155 | let [ centerx, centery, ang1, ang2 ] = getArcCenter( 156 | px, 157 | py, 158 | cx, 159 | cy, 160 | rx, 161 | ry, 162 | largeArcFlag, 163 | sweepFlag, 164 | sinphi, 165 | cosphi, 166 | pxp, 167 | pyp 168 | ) 169 | 170 | // If 'ang2' == 90.0000000001, then `ratio` will evaluate to 171 | // 1.0000000001. This causes `segments` to be greater than one, which is an 172 | // unecessary split, and adds extra points to the bezier curve. To alleviate 173 | // this issue, we round to 1.0 when the ratio is close to 1.0. 174 | let ratio = Math.abs(ang2) / (TAU / 4) 175 | if (Math.abs(1.0 - ratio) < 0.0000001) { 176 | ratio = 1.0 177 | } 178 | 179 | const segments = Math.max(Math.ceil(ratio), 1) 180 | 181 | ang2 /= segments 182 | 183 | for (let i = 0; i < segments; i++) { 184 | curves.push(approxUnitArc(ang1, ang2)) 185 | ang1 += ang2 186 | } 187 | 188 | return curves.map(curve => { 189 | const { x: x1, y: y1 } = mapToEllipse(curve[ 0 ], rx, ry, cosphi, sinphi, centerx, centery) 190 | const { x: x2, y: y2 } = mapToEllipse(curve[ 1 ], rx, ry, cosphi, sinphi, centerx, centery) 191 | const { x, y } = mapToEllipse(curve[ 2 ], rx, ry, cosphi, sinphi, centerx, centery) 192 | 193 | return { x1, y1, x2, y2, x, y } 194 | }) 195 | } 196 | 197 | // ----------------------------------------------------------------------- 198 | 199 | export function normalizeSvgPath(path){ 200 | // init state 201 | var prev 202 | var result = [] 203 | var bezierX = 0 204 | var bezierY = 0 205 | var startX = 0 206 | var startY = 0 207 | var quadX = null 208 | var quadY = null 209 | var x = 0 210 | var y = 0 211 | 212 | for (var i = 0, len = path.length; i < len; i++) { 213 | var seg = path[i] 214 | var command = seg[0] 215 | 216 | switch (command) { 217 | case 'M': 218 | startX = seg[1] 219 | startY = seg[2] 220 | break 221 | case 'A': 222 | var curves = arcToBezier({ 223 | px: x, 224 | py: y, 225 | cx: seg[6], 226 | cy: seg[7], 227 | rx: seg[1], 228 | ry: seg[2], 229 | xAxisRotation: seg[3], 230 | largeArcFlag: seg[4], 231 | sweepFlag: seg[5] 232 | }) 233 | 234 | // null-curves 235 | if (!curves.length) continue 236 | 237 | for (var j = 0, c; j < curves.length; j++) { 238 | c = curves[j] 239 | seg = ['C', c.x1, c.y1, c.x2, c.y2, c.x, c.y] 240 | if (j < curves.length - 1) result.push(seg) 241 | } 242 | 243 | break 244 | case 'S': 245 | // default control point 246 | var cx = x 247 | var cy = y 248 | if (prev == 'C' || prev == 'S') { 249 | cx += cx - bezierX // reflect the previous command's control 250 | cy += cy - bezierY // point relative to the current point 251 | } 252 | seg = ['C', cx, cy, seg[1], seg[2], seg[3], seg[4]] 253 | break 254 | case 'T': 255 | if (prev == 'Q' || prev == 'T') { 256 | quadX = x * 2 - quadX // as with 'S' reflect previous control point 257 | quadY = y * 2 - quadY 258 | } else { 259 | quadX = x 260 | quadY = y 261 | } 262 | seg = quadratic(x, y, quadX, quadY, seg[1], seg[2]) 263 | break 264 | case 'Q': 265 | quadX = seg[1] 266 | quadY = seg[2] 267 | seg = quadratic(x, y, seg[1], seg[2], seg[3], seg[4]) 268 | break 269 | case 'L': 270 | seg = line(x, y, seg[1], seg[2]) 271 | break 272 | case 'H': 273 | seg = line(x, y, seg[1], y) 274 | break 275 | case 'V': 276 | seg = line(x, y, x, seg[1]) 277 | break 278 | case 'Z': 279 | seg = line(x, y, startX, startY) 280 | break 281 | } 282 | 283 | // update state 284 | prev = command 285 | x = seg[seg.length - 2] 286 | y = seg[seg.length - 1] 287 | if (seg.length > 4) { 288 | bezierX = seg[seg.length - 4] 289 | bezierY = seg[seg.length - 3] 290 | } else { 291 | bezierX = x 292 | bezierY = y 293 | } 294 | result.push(seg) 295 | } 296 | 297 | return result 298 | } 299 | 300 | function line(x1, y1, x2, y2){ 301 | return ['C', x1, y1, x2, y2, x2, y2] 302 | } 303 | 304 | function quadratic(x1, y1, cx, cy, x2, y2){ 305 | return [ 306 | 'C', 307 | x1/3 + (2/3) * cx, 308 | y1/3 + (2/3) * cy, 309 | x2/3 + (2/3) * cx, 310 | y2/3 + (2/3) * cy, 311 | x2, 312 | y2 313 | ] 314 | } 315 | 316 | -------------------------------------------------------------------------------- /src/raycast.js: -------------------------------------------------------------------------------- 1 | export function raycastX(hitTesters, rayStartX, y, step, maxDistance) { 2 | let distance = 0 3 | let absstep = Math.abs(step) 4 | let x = rayStartX 5 | for (; distance < maxDistance; x += step) { 6 | // log("test", x,y) 7 | for (let hitTester of hitTesters) { 8 | if (hitTester.test(x, y)) { 9 | // log("hit", x) 10 | return x 11 | } 12 | } 13 | distance += absstep 14 | } 15 | return x 16 | } 17 | 18 | 19 | // PolyPointHitTester tests if a point is inside a polygon 20 | export class PolyPointHitTester { 21 | // from http://alienryderflex.com/polygon/ 22 | constructor(polygon) { 23 | this.length = polygon.length / 2 24 | this.polygon = polygon 25 | this.constant = new Float64Array(this.length) 26 | this.multiple = new Float64Array(this.length) 27 | // precompute 28 | let j = this.length - 1; 29 | for (let i = 0; i < this.length; i++) { 30 | let x = polygon[i*2] 31 | , y = polygon[i*2 + 1] 32 | , nx = polygon[j*2] 33 | , ny = polygon[j*2 + 1] 34 | 35 | if (ny == y) { 36 | this.constant[i] = x 37 | this.multiple[i] = 0 38 | } else { 39 | this.constant[i] = ( 40 | x 41 | - (y * nx) / (ny - y) 42 | + (y * x) / (ny - y) 43 | ) 44 | this.multiple[i] = (nx - x) / (ny - y) 45 | } 46 | j = i 47 | } 48 | } 49 | 50 | 51 | test(x, y) { 52 | let oddNodes = 0, current = this.polygon[this.polygon.length-1] > y 53 | for (let i = 0; i < this.length; i++) { 54 | let previous = current 55 | current = this.polygon[i*2 + 1] > y 56 | if (current != previous) { 57 | oddNodes ^= y * this.multiple[i] + this.constant[i] < x 58 | } 59 | } 60 | return !!oddNodes 61 | } 62 | 63 | // test(x, y) { 64 | // let j = this.length - 1 65 | // let oddNodes = 0 66 | // for (let i = 0; i < this.length; i++) { 67 | // let cy = this.polygon[i*2 + 1] 68 | // let ny = this.polygon[j*2 + 1] 69 | // if (cy < y && ny >= y || ny < y && cy >= y) { 70 | // oddNodes ^= (y * this.multiple[i] + this.constant[i]) < x 71 | // } 72 | // j = i 73 | // } 74 | // return !!oddNodes 75 | // } 76 | } 77 | -------------------------------------------------------------------------------- /src/simplify-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Vladimir Agafonkin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are 6 | permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of 9 | conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 12 | of conditions and the following disclaimer in the documentation and/or other materials 13 | provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 17 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 18 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 19 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 20 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 22 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | */ 25 | 26 | function getSqDist(p1, p2) { 27 | var dx = p1[0] - p2[0], 28 | dy = p1[1] - p2[1]; 29 | 30 | return dx * dx + dy * dy; 31 | } 32 | 33 | // basic distance-based simplification 34 | export function simplifyRadialDist(points, tolerance) { 35 | if (points.length<=1) 36 | return points; 37 | tolerance = typeof tolerance === 'number' ? tolerance : 1; 38 | var sqTolerance = tolerance * tolerance; 39 | 40 | var prevPoint = points[0], 41 | newPoints = [prevPoint], 42 | point; 43 | 44 | for (var i = 1, len = points.length; i < len; i++) { 45 | point = points[i]; 46 | 47 | if (getSqDist(point, prevPoint) > sqTolerance) { 48 | newPoints.push(point); 49 | prevPoint = point; 50 | } 51 | } 52 | 53 | if (prevPoint !== point) newPoints.push(point); 54 | 55 | return newPoints; 56 | } 57 | 58 | // ------------------------------------------------------------------- 59 | 60 | // square distance from a point to a segment 61 | function getSqSegDist(p, p1, p2) { 62 | var x = p1[0], 63 | y = p1[1], 64 | dx = p2[0] - x, 65 | dy = p2[1] - y; 66 | 67 | if (dx !== 0 || dy !== 0) { 68 | 69 | var t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy); 70 | 71 | if (t > 1) { 72 | x = p2[0]; 73 | y = p2[1]; 74 | 75 | } else if (t > 0) { 76 | x += dx * t; 77 | y += dy * t; 78 | } 79 | } 80 | 81 | dx = p[0] - x; 82 | dy = p[1] - y; 83 | 84 | return dx * dx + dy * dy; 85 | } 86 | 87 | function simplifyDPStep(points, first, last, sqTolerance, simplified) { 88 | var maxSqDist = sqTolerance, 89 | index; 90 | 91 | for (var i = first + 1; i < last; i++) { 92 | var sqDist = getSqSegDist(points[i], points[first], points[last]); 93 | 94 | if (sqDist > maxSqDist) { 95 | index = i; 96 | maxSqDist = sqDist; 97 | } 98 | } 99 | 100 | if (maxSqDist > sqTolerance) { 101 | if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified); 102 | simplified.push(points[index]); 103 | if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified); 104 | } 105 | } 106 | 107 | // simplification using Ramer-Douglas-Peucker algorithm 108 | export function simplifyDouglasPeucker(points, tolerance) { 109 | if (points.length<=1) 110 | return points; 111 | tolerance = typeof tolerance === 'number' ? tolerance : 1; 112 | var sqTolerance = tolerance * tolerance; 113 | 114 | var last = points.length - 1; 115 | 116 | var simplified = [points[0]]; 117 | simplifyDPStep(points, 0, last, sqTolerance, simplified); 118 | simplified.push(points[last]); 119 | 120 | return simplified; 121 | } 122 | 123 | // ------------------------------------------------------------------- 124 | 125 | //simplifies using both algorithms 126 | export function simplifyPath(points, tolerance) { 127 | points = simplifyRadialDist(points, tolerance); 128 | points = simplifyDouglasPeucker(points, tolerance); 129 | return points; 130 | } 131 | -------------------------------------------------------------------------------- /src/svg-path-contours.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | Copyright (c) 2014 Matt DesLauriers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | import { normalizeSvgPath } from "./normalize-svg-path" 24 | import { absSvgPath } from "./abs-svg-path" 25 | import { adaptiveBezierCurve } from "./adaptive-bezier-curve" 26 | 27 | function vec2Copy(out, a) { 28 | out[0] = a[0] 29 | out[1] = a[1] 30 | return out 31 | } 32 | 33 | function set(out, x, y) { 34 | out[0] = x 35 | out[1] = y 36 | return out 37 | } 38 | 39 | var tmp1 = [0,0], 40 | tmp2 = [0,0], 41 | tmp3 = [0,0] 42 | 43 | function bezierTo(points, scale, start, seg) { 44 | adaptiveBezierCurve( 45 | start, 46 | set(tmp1, seg[1], seg[2]), 47 | set(tmp2, seg[3], seg[4]), 48 | set(tmp3, seg[5], seg[6]), 49 | scale, 50 | points 51 | ) 52 | } 53 | 54 | /*function pointsToFloat32Array(points) { 55 | let a = new Float32Array(points.length * 2) 56 | for (let i = 0; i < points.length; i++) { 57 | a[i] = points[i][0] 58 | a[i * 2] = points[i][1] 59 | } 60 | return a 61 | } 62 | 63 | return function contours(svg, scale) { 64 | var paths = [] 65 | var points = [] 66 | var pen = [0, 0] 67 | normalizeSvgPath(absSvgPath(svg)).forEach((segment, i, self) => { 68 | if (segment[0] === 'M') { 69 | let [, x, y] = segment 70 | pen[0] = x 71 | pen[1] = y 72 | if (points.length > 0) { 73 | paths.push(pointsToFloat32Array(points)) 74 | points.length = 0 75 | // points = [] 76 | } 77 | } else if (segment[0] === 'C') { 78 | bezierTo(points, scale, pen, segment) 79 | set(pen, segment[5], segment[6]) 80 | } else { 81 | throw new Error('illegal type in SVG: '+segment[0]) 82 | } 83 | }) 84 | if (points.length > 0) { 85 | paths.push(pointsToFloat32Array(points)) 86 | } 87 | return paths 88 | }*/ 89 | 90 | export function svgPathContours(svg, scale) { 91 | var paths = [] 92 | var points = [] 93 | var pen = [0, 0] 94 | normalizeSvgPath(absSvgPath(svg)).forEach((segment, i, self) => { 95 | if (segment[0] === 'M') { 96 | let [, x, y] = segment 97 | pen[0] = x 98 | pen[1] = y 99 | if (points.length > 0) { 100 | paths.push(points) 101 | points = [] 102 | } 103 | } else if (segment[0] === 'C') { 104 | bezierTo(points, scale, pen, segment) 105 | set(pen, segment[5], segment[6]) 106 | } else { 107 | throw new Error('illegal type in SVG: '+segment[0]) 108 | } 109 | }) 110 | if (points.length > 0) { 111 | paths.push(points) 112 | } 113 | return paths 114 | } 115 | -------------------------------------------------------------------------------- /src/svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * parse an svg path data string. Generates an Array 3 | * of commands where each command is an Array of the 4 | * form `[command, arg1, arg2, ...]` 5 | * 6 | * @param {String} path 7 | * @return {Array} 8 | */ 9 | export function parse(path) { 10 | const num = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/ig 11 | const parseSvgLength = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0} 12 | const segment = /([astvzqmhlc])([^astvzqmhlc]*)/ig 13 | 14 | function parseValues(args) { 15 | var numbers = args.match(num) 16 | return numbers ? numbers.map(Number) : [] 17 | } 18 | 19 | var data = [] 20 | path.replace(segment, function(_, command, args){ 21 | var type = command.toLowerCase() 22 | args = parseValues(args) 23 | 24 | // overloaded moveTo 25 | if (type == 'm' && args.length > 2) { 26 | data.push([command].concat(args.splice(0, 2))) 27 | type = 'l' 28 | command = command == 'm' ? 'l' : 'L' 29 | } 30 | 31 | while (true) { 32 | if (args.length == parseSvgLength[type]) { 33 | args.unshift(command) 34 | return data.push(args) 35 | } 36 | if (args.length < parseSvgLength[type]) throw new Error('malformed path data') 37 | data.push([command].concat(args.splice(0, parseSvgLength[type]))) 38 | } 39 | }) 40 | return data 41 | } 42 | -------------------------------------------------------------------------------- /src/tess2.min.js: -------------------------------------------------------------------------------- 1 | !function(E){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=E();else if("function"==typeof define&&define.amd)define([],E);else{var A;"undefined"!=typeof window?A=window:"undefined"!=typeof global?A=global:"undefined"!=typeof self&&(A=self),A.Tess2=E()}}(function(){var E,A,K;return function F(x,y,p){function l(r,G){if(!y[r]){if(!x[r]){var j=typeof require=="function"&&require;if(!G&&j)return j(r,!0);if(v)return v(r,!0);var B=new Error("Cannot find module '"+r+"'");throw B.code="MODULE_NOT_FOUND",B}var z=y[r]={exports:{}};x[r][0].call(z.exports,function(C){var D=x[r][1][C];return l(D?D:C)},z,z.exports,F,x,y,p)}return y[r].exports}for(var v=typeof require=="function"&&require,w=0;w0?d0?(b.t-c.t)*d+(b.t-a.t)*e:0},j.transEval=function(a,b,c){l(j.transLeq(a,b)&&j.transLeq(b,c));var d=b.t-a.t,e=c.t-b.t;return d+e>0?d0?(b.s-c.s)*d+(b.s-a.s)*e:0},j.vertCCW=function(a,b,c){return a.s*(b.t-c.t)+b.s*(c.t-a.t)+c.s*(a.t-b.t)>=0},j.interpolate=function(a,b,c,d){return a=a<0?0:a,c=c<0?0:c,a<=c?c==0?(b+d)/2:b+(d-b)*(a/(a+c)):d+(b-d)*(c/(a+c))},j.intersect=function(a,b,c,d,e){var f,i,g;j.vertLeq(a,b)||(g=a,a=b,b=g),j.vertLeq(c,d)||(g=c,c=d,d=g),j.vertLeq(a,c)||(g=a,a=c,c=g,g=b,b=d,d=g),j.vertLeq(c,b)?j.vertLeq(b,d)?(f=j.edgeEval(a,c,b),i=j.edgeEval(c,b,d),f+i<0&&(f=-f,i=-i),e.s=j.interpolate(f,c.s,i,b.s)):(f=j.edgeSign(a,c,b),i=-j.edgeSign(a,d,b),f+i<0&&(f=-f,i=-i),e.s=j.interpolate(f,c.s,i,d.s)):e.s=(c.s+b.s)/2,j.transLeq(a,b)||(g=a,a=b,b=g),j.transLeq(c,d)||(g=c,c=d,d=g),j.transLeq(a,c)||(g=a,a=c,c=g,g=b,b=d,d=g),j.transLeq(c,b)?j.transLeq(b,d)?(f=j.transEval(a,c,b),i=j.transEval(c,b,d),f+i<0&&(f=-f,i=-i),e.t=j.interpolate(f,c.t,i,b.t)):(f=j.transSign(a,c,b),i=-j.transSign(a,d,b),f+i<0&&(f=-f,i=-i),e.t=j.interpolate(f,c.t,i,d.t)):e.t=(c.t+b.t)/2};function B(){this.key=null,this.next=null,this.prev=null}function z(a,b){this.head=new B(),this.head.next=this.head,this.head.prev=this.head,this.frame=a,this.leq=b}z.prototype={min:function(){return this.head.next},max:function(){return this.head.prev},insert:function(a){return this.insertBefore(this.head,a)},search:function(a){var b=this.head;do b=b.next;while(b.key!==null&&!this.leq(this.frame,a,b.key));return b},insertBefore:function(a,b){do a=a.prev;while(a.key!==null&&!this.leq(this.frame,a.key,b));var c=new B();return c.key=b,c.next=a.next,a.next.prev=c,c.prev=a,a.next=c,c},delete:function(a){a.next.prev=a.prev,a.prev.next=a.next}};function C(){this.handle=null}function D(){this.key=null,this.node=null}function I(a,b){this.size=0,this.max=a,this.nodes=[],this.nodes.length=a+1;for(var c=0;cthis.size||this.leq(c[d].key,c[e].key)){b[a].handle=d,c[d].node=a;break}b[a].handle=e,c[e].node=a,a=f}},floatUp_:function(a){var b=this.nodes,c=this.handles,d,e,f;for(d=b[a].handle;;){f=a>>1,e=b[f].handle;if(f==0||this.leq(c[e].key,c[d].key)){b[a].handle=d,c[d].node=a;break}b[a].handle=e,c[e].node=a,a=f}},init:function(){for(var a=this.size;a>=1;--a)this.floatDown_(a);this.initialized=!0},min:function(){return this.handles[this.nodes[1].handle].key},isEmpty:function(){this.size===0},insert:function(a){var b,c;b=++this.size;if(b*2>this.max){this.max*=2;var d;d=this.nodes.length,this.nodes.length=this.max+1;for(var e=d;e0&&(a[1].handle=a[this.size].handle,b[a[1].handle].node=1,b[c].key=null,b[c].node=this.freeList,this.freeList=c,--this.size,this.size>0&&this.floatDown_(1)),d},delete:function(a){var b=this.nodes,c=this.handles,d;l(a>=1&&a<=this.max&&c[a].key!==null),d=c[a].node,b[d].handle=b[this.size].handle,c[b[d].handle].node=d,--this.size,d<=this.size&&(d<=1||this.leq(c[b[d>>1].handle].key,c[b[d].handle].key)?this.floatDown_(d):this.floatUp_(d)),c[a].key=null,c[a].node=this.freeList,this.freeList=a}};function H(){this.eUp=null,this.nodeUp=null,this.windingNumber=0,this.inside=!1,this.sentinel=!1,this.dirty=!1,this.fixUpperEdge=!1}var h={};h.regionBelow=function(a){return a.nodeUp.prev.key},h.regionAbove=function(a){return a.nodeUp.next.key},h.debugEvent=function(a){},h.addWinding=function(a,b){a.winding+=b.winding,a.Sym.winding+=b.Sym.winding},h.edgeLeq=function(a,b,c){var d=a.event,e,f,i=b.eUp,g=c.eUp;if(i.Dst===d)return g.Dst===d?j.vertLeq(i.Org,g.Org)?j.edgeSign(g.Dst,i.Org,g.Org)<=0:j.edgeSign(i.Dst,g.Org,i.Org)>=0:j.edgeSign(g.Dst,d,g.Org)<=0;if(g.Dst===d)return j.edgeSign(i.Dst,d,i.Org)>=0;var e=j.edgeEval(i.Dst,d,i.Org),f=j.edgeEval(g.Dst,d,g.Org);return e>=f},h.deleteRegion=function(a,b){b.fixUpperEdge&&l(b.eUp.winding===0),b.eUp.activeRegion=null,a.dict.delete(b.nodeUp)},h.fixUpperEdge=function(a,b,c){l(b.fixUpperEdge),a.mesh.delete(b.eUp),b.fixUpperEdge=!1,b.eUp=c,c.activeRegion=b},h.topLeftRegion=function(a,b){var c=b.eUp.Org,d;do b=h.regionAbove(b);while(b.eUp.Org===c);if(b.fixUpperEdge){d=a.mesh.connect(h.regionBelow(b).eUp.Sym,b.eUp.Lnext);if(d===null)return null;h.fixUpperEdge(a,b,d),b=h.regionAbove(b)}return b},h.topRightRegion=function(a){var b=a.eUp.Dst,a=null;do a=h.regionAbove(a);while(a.eUp.Dst===b);return a},h.addRegionBelow=function(a,b,c){var d=new H();return d.eUp=c,d.nodeUp=a.dict.insertBefore(b.nodeUp,d),d.fixUpperEdge=!1,d.sentinel=!1,d.dirty=!1,c.activeRegion=d,d},h.isWindingInside=function(a,b){switch(a.windingRule){case p.WINDING_ODD:return(b&1)!=0;case p.WINDING_NONZERO:return b!=0;case p.WINDING_POSITIVE:return b>0;case p.WINDING_NEGATIVE:return b<0;case p.WINDING_ABS_GEQ_TWO:return b>=2||b<=-2}return l(!1),!1},h.computeWinding=function(a,b){b.windingNumber=h.regionAbove(b).windingNumber+b.eUp.winding,b.inside=h.isWindingInside(a,b.windingNumber)},h.finishRegion=function(a,b){var c=b.eUp,d=c.Lface;d.inside=b.inside,d.anEdge=c,h.deleteRegion(a,b)},h.finishLeftRegions=function(a,b,c){for(var d,e,f=null,i=b,e=b.eUp;i!==c;){i.fixUpperEdge=!1,f=h.regionBelow(i),d=f.eUp;if(d.Org!=e.Org){if(!f.fixUpperEdge){h.finishRegion(a,i);break}d=a.mesh.connect(e.Lprev,d.Sym),h.fixUpperEdge(a,f,d)}e.Onext!==d&&(a.mesh.splice(d.Oprev,d),a.mesh.splice(e,d)),h.finishRegion(a,i),e=f.eUp,i=f}return e},h.addRightEdges=function(a,b,c,d,e,f){var i,g,k,m,q=!0;k=c;do l(j.vertLeq(k.Org,k.Dst)),h.addRegionBelow(a,b,k.Sym),k=k.Onext;while(k!==d);for(e===null&&(e=h.regionBelow(b).eUp.Rprev),g=b,m=e;;){i=h.regionBelow(g),k=i.eUp.Sym;if(k.Org!==m.Org)break;k.Onext!==m&&(a.mesh.splice(k.Oprev,k),a.mesh.splice(m.Oprev,k)),i.windingNumber=g.windingNumber-k.winding,i.inside=h.isWindingInside(a,i.windingNumber),g.dirty=!0,!q&&h.checkForRightSplice(a,g)&&(h.addWinding(k,m),h.deleteRegion(a,g),a.mesh.delete(m)),q=!1,g=i,m=k}g.dirty=!0,l(g.windingNumber-k.winding===i.windingNumber),f&&h.walkDirtyRegions(a,g)},h.spliceMergeVertices=function(a,b,c){a.mesh.splice(b,c)},h.vertexWeights=function(a,b,c){var d=j.vertL1dist(b,a),e=j.vertL1dist(c,a),f=.5*e/(d+e),i=.5*d/(d+e);a.coords[0]+=f*b.coords[0]+i*c.coords[0],a.coords[1]+=f*b.coords[1]+i*c.coords[1],a.coords[2]+=f*b.coords[2]+i*c.coords[2]},h.getIntersectData=function(a,b,c,d,e,f){b.coords[0]=b.coords[1]=b.coords[2]=0,b.idx=-1,h.vertexWeights(b,c,d),h.vertexWeights(b,e,f)},h.checkForRightSplice=function(a,b){var c=h.regionBelow(b),d=b.eUp,e=c.eUp;if(j.vertLeq(d.Org,e.Org)){if(j.edgeSign(e.Dst,d.Org,e.Org)>0)return!1;j.vertEq(d.Org,e.Org)?d.Org!==e.Org&&(a.pq.delete(d.Org.pqHandle),h.spliceMergeVertices(a,e.Oprev,d)):(a.mesh.splitEdge(e.Sym),a.mesh.splice(d,e.Oprev),b.dirty=c.dirty=!0)}else{if(j.edgeSign(d.Dst,e.Org,d.Org)<0)return!1;h.regionAbove(b).dirty=b.dirty=!0,a.mesh.splitEdge(d.Sym),a.mesh.splice(e.Oprev,d)}return!0},h.checkForLeftSplice=function(a,b){var c=h.regionBelow(b),d=b.eUp,e=c.eUp,f;l(!j.vertEq(d.Dst,e.Dst));if(j.vertLeq(d.Dst,e.Dst)){if(j.edgeSign(d.Dst,e.Dst,d.Org)<0)return!1;h.regionAbove(b).dirty=b.dirty=!0,f=a.mesh.splitEdge(d),a.mesh.splice(e.Sym,f),f.Lface.inside=b.inside}else{if(j.edgeSign(e.Dst,d.Dst,e.Org)>0)return!1;b.dirty=c.dirty=!0,f=a.mesh.splitEdge(e),a.mesh.splice(d.Lnext,e.Sym),f.Rface.inside=b.inside}return!0},h.checkForIntersect=function(a,b){var c=h.regionBelow(b),d=b.eUp,e=c.eUp,f=d.Org,i=e.Org,g=d.Dst,k=e.Dst,m,q,n=new v(),s,t;l(!j.vertEq(k,g)),l(j.edgeSign(g,a.event,f)<=0),l(j.edgeSign(k,a.event,i)>=0),l(f!==a.event&&i!==a.event),l(!b.fixUpperEdge&&!c.fixUpperEdge);if(f===i)return!1;m=Math.min(f.t,g.t),q=Math.max(i.t,k.t);if(m>q)return!1;if(j.vertLeq(f,i)){if(j.edgeSign(k,f,i)>0)return!1}else if(j.edgeSign(g,i,f)<0)return!1;return h.debugEvent(a),j.intersect(g,f,k,i,n),l(Math.min(f.t,g.t)<=n.t),l(n.t<=Math.max(i.t,k.t)),l(Math.min(k.s,g.s)<=n.s),l(n.s<=Math.max(i.s,f.s)),j.vertLeq(n,a.event)&&(n.s=a.event.s,n.t=a.event.t),s=j.vertLeq(f,i)?f:i,j.vertLeq(s,n)&&(n.s=s.s,n.t=s.t),j.vertEq(n,f)||j.vertEq(n,i)?(h.checkForRightSplice(a,b),!1):!j.vertEq(g,a.event)&&j.edgeSign(g,a.event,n)>=0||!j.vertEq(k,a.event)&&j.edgeSign(k,a.event,n)<=0?k===a.event?(a.mesh.splitEdge(d.Sym),a.mesh.splice(e.Sym,d),b=h.topLeftRegion(a,b),d=h.regionBelow(b).eUp,h.finishLeftRegions(a,h.regionBelow(b),c),h.addRightEdges(a,b,d.Oprev,d,d,!0),TRUE):g===a.event?(a.mesh.splitEdge(e.Sym),a.mesh.splice(d.Lnext,e.Oprev),c=b,b=h.topRightRegion(b),t=h.regionBelow(b).eUp.Rprev,c.eUp=e.Oprev,e=h.finishLeftRegions(a,c,null),h.addRightEdges(a,b,e.Onext,d.Rprev,t,!0),!0):(j.edgeSign(g,a.event,n)>=0&&(h.regionAbove(b).dirty=b.dirty=!0,a.mesh.splitEdge(d.Sym),d.Org.s=a.event.s,d.Org.t=a.event.t),j.edgeSign(k,a.event,n)<=0&&(b.dirty=c.dirty=!0,a.mesh.splitEdge(e.Sym),e.Org.s=a.event.s,e.Org.t=a.event.t),!1):(a.mesh.splitEdge(d.Sym),a.mesh.splitEdge(e.Sym),a.mesh.splice(e.Oprev,d),d.Org.s=n.s,d.Org.t=n.t,d.Org.pqHandle=a.pq.insert(d.Org),h.getIntersectData(a,d.Org,f,g,i,k),h.regionAbove(b).dirty=b.dirty=c.dirty=!0,!1)},h.walkDirtyRegions=function(a,b){for(var c=h.regionBelow(b),d,e;;){for(;c.dirty;)b=c,c=h.regionBelow(c);if(!b.dirty){c=b,b=h.regionAbove(b);if(b==null||!b.dirty)return}b.dirty=!1,d=b.eUp,e=c.eUp,d.Dst!==e.Dst&&(h.checkForLeftSplice(a,b)&&(c.fixUpperEdge?(h.deleteRegion(a,c),a.mesh.delete(e),c=h.regionBelow(b),e=c.eUp):b.fixUpperEdge&&(h.deleteRegion(a,b),a.mesh.delete(d),b=h.regionAbove(c),d=b.eUp)));if(d.Org!==e.Org)if(d.Dst!==e.Dst&&!b.fixUpperEdge&&!c.fixUpperEdge&&(d.Dst===a.event||e.Dst===a.event)){if(h.checkForIntersect(a,b))return}else h.checkForRightSplice(a,b);d.Org===e.Org&&d.Dst===e.Dst&&(h.addWinding(e,d),h.deleteRegion(a,b),a.mesh.delete(d),b=h.regionAbove(c))}},h.connectRightVertex=function(a,b,c){var d,e=c.Onext,f=h.regionBelow(b),i=b.eUp,g=f.eUp,k=!1;i.Dst!==g.Dst&&h.checkForIntersect(a,b),j.vertEq(i.Org,a.event)&&(a.mesh.splice(e.Oprev,i),b=h.topLeftRegion(a,b),e=h.regionBelow(b).eUp,h.finishLeftRegions(a,h.regionBelow(b),f),k=!0),j.vertEq(g.Org,a.event)&&(a.mesh.splice(c,g.Oprev),c=h.finishLeftRegions(a,f,null),k=!0);if(k){h.addRightEdges(a,b,c.Onext,e,e,!0);return}j.vertLeq(g.Org,i.Org)?d=g.Oprev:d=i,d=a.mesh.connect(c.Lprev,d),h.addRightEdges(a,b,d,d.Onext,d.Onext,!1),d.Sym.activeRegion.fixUpperEdge=!0,h.walkDirtyRegions(a,b)},h.connectLeftDegenerate=function(a,b,c){var d,e,f,i,g;d=b.eUp;if(j.vertEq(d.Org,c)){l(!1),h.spliceMergeVertices(a,d,c.anEdge);return}if(!j.vertEq(d.Dst,c)){a.mesh.splitEdge(d.Sym),b.fixUpperEdge&&(a.mesh.delete(d.Onext),b.fixUpperEdge=!1),a.mesh.splice(c.anEdge,d),h.sweepEvent(a,c);return}l(!1),b=h.topRightRegion(b),g=h.regionBelow(b),f=g.eUp.Sym,e=i=f.Onext,g.fixUpperEdge&&(l(e!==f),h.deleteRegion(a,g),a.mesh.delete(f),f=e.Oprev),a.mesh.splice(c.anEdge,f),j.edgeGoesLeft(e)||(e=null),h.addRightEdges(a,b,f.Onext,i,e,!0)},h.connectLeftVertex=function(a,b){var c,d,e,f,i,g,k=new H();k.eUp=b.anEdge.Sym,c=a.dict.search(k).key,d=h.regionBelow(c);if(!d)return;f=c.eUp,i=d.eUp;if(j.edgeSign(f.Dst,b,f.Org)===0){h.connectLeftDegenerate(a,c,b);return}e=j.vertLeq(i.Dst,f.Dst)?c:d;if(c.inside||e.fixUpperEdge){if(e===c)g=a.mesh.connect(b.anEdge.Sym,f.Lnext);else{var m=a.mesh.connect(i.Dnext,b.anEdge);g=m.Sym}e.fixUpperEdge?h.fixUpperEdge(a,e,g):h.computeWinding(a,h.addRegionBelow(a,c,g)),h.sweepEvent(a,b)}else h.addRightEdges(a,c,b.anEdge,b.anEdge,null,!0)},h.sweepEvent=function(a,b){a.event=b,h.debugEvent(a);for(var c=b.anEdge;c.activeRegion===null;){c=c.Onext;if(c==b.anEdge){h.connectLeftVertex(a,b);return}}var d=h.topLeftRegion(a,c.activeRegion);l(d!==null);var e=h.regionBelow(d),f=e.eUp,i=h.finishLeftRegions(a,e,null);i.Onext===f?h.connectRightVertex(a,d,i):h.addRightEdges(a,d,i.Onext,f,f,!0)},h.addSentinel=function(a,b,c,d){var e=new H(),f=a.mesh.makeEdge();f.Org.s=c,f.Org.t=d,f.Dst.s=b,f.Dst.t=d,a.event=f.Dst,e.eUp=f,e.windingNumber=0,e.inside=!1,e.fixUpperEdge=!1,e.sentinel=!0,e.dirty=!1,e.nodeUp=a.dict.insert(e)},h.initEdgeDict=function(a){a.dict=new z(a,h.edgeLeq);var b=a.bmax[0]-a.bmin[0],c=a.bmax[1]-a.bmin[1],d=a.bmin[0]-b,e=a.bmax[0]+b,f=a.bmin[1]-c,i=a.bmax[1]+c;h.addSentinel(a,d,e,f),h.addSentinel(a,d,e,i)},h.doneEdgeDict=function(a){for(var b,c=0;(b=a.dict.min().key)!==null;)b.sentinel||(l(b.fixUpperEdge),l(++c==1)),l(b.windingNumber==0),h.deleteRegion(a,b)},h.removeDegenerateEdges=function(a){var b,c,d,e=a.mesh.eHead;for(b=e.next;b!==e;b=c)c=b.next,d=b.Lnext,j.vertEq(b.Org,b.Dst)&&b.Lnext.Lnext!==b&&(h.spliceMergeVertices(a,d,b),a.mesh.delete(b),b=d,d=b.Lnext),d.Lnext===b&&(d!==b&&((d===c||d===c.Sym)&&(c=c.next),a.mesh.delete(d)),(b===c||b===c.Sym)&&(c=c.next),a.mesh.delete(b))},h.initPriorityQ=function(a){var b,c,d,e=0;for(d=a.mesh.vHead,c=d.next;c!==d;c=c.next)e++;for(e+=8,b=a.pq=new I(e,j.vertLeq),d=a.mesh.vHead,c=d.next;c!==d;c=c.next)c.pqHandle=b.insert(c);return c!==d?!1:(b.init(),!0)},h.donePriorityQ=function(a){a.pq=null},h.removeDegenerateFaces=function(a,b){var c,d,e;for(c=b.fHead.next;c!==b.fHead;c=d)d=c.next,e=c.anEdge,l(e.Lnext!==e),e.Lnext.Lnext===e&&(h.addWinding(e.Onext,e),a.mesh.delete(e));return!0},h.computeInterior=function(a){var b,c;h.removeDegenerateEdges(a);if(!h.initPriorityQ(a))return!1;for(h.initEdgeDict(a);(b=a.pq.extractMin())!==null;){for(;;){c=a.pq.min();if(c===null||!j.vertEq(c,b))break;c=a.pq.extractMin(),h.spliceMergeVertices(a,b.anEdge,c.anEdge)}h.sweepEvent(a,b)}return a.event=a.dict.min().key.eUp.Org,h.debugEvent(a),h.doneEdgeDict(a),h.donePriorityQ(a),h.removeDegenerateFaces(a,a.mesh)?(a.mesh.check(),!0):!1};function J(){this.mesh=null,this.normal=[0,0,0],this.sUnit=[0,0,0],this.tUnit=[0,0,0],this.bmin=[0,0],this.bmax=[0,0],this.windingRule=p.WINDING_ODD,this.dict=null,this.pq=null,this.event=null,this.vertexIndexCounter=0,this.vertices=[],this.vertexIndices=[],this.vertexCount=0,this.elements=[],this.elementCount=0}J.prototype={dot_:function(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]},normalize_:function(a){var b=a[0]*a[0]+a[1]*a[1]+a[2]*a[2];l(b>0),b=Math.sqrt(b),a[0]/=b,a[1]/=b,a[2]/=b},longAxis_:function(a){var b=0;return Math.abs(a[1])>Math.abs(a[0])&&(b=1),Math.abs(a[2])>Math.abs(a[b])&&(b=2),b},computeNormal_:function(a){var b,c,d,e,f,i,g=[0,0,0],k=[0,0,0],m=[0,0,0],q=[0,0,0],n=[0,0,0],s=[null,null,null],t=[null,null,null],u=this.mesh.vHead,o;for(b=u.next,o=0;o<3;++o)e=b.coords[o],k[o]=e,t[o]=b,g[o]=e,s[o]=b;for(b=u.next;b!==u;b=b.next)for(o=0;o<3;++o)e=b.coords[o],eg[o]&&(g[o]=e,s[o]=b);o=0,g[1]-k[1]>g[0]-k[0]&&(o=1),g[2]-k[2]>g[o]-k[o]&&(o=2);if(k[o]>=g[o]){a[0]=0,a[1]=0,a[2]=1;return}for(i=0,c=t[o],d=s[o],m[0]=c.coords[0]-d.coords[0],m[1]=c.coords[1]-d.coords[1],m[2]=c.coords[2]-d.coords[2],b=u.next;b!==u;b=b.next)q[0]=b.coords[0]-d.coords[0],q[1]=b.coords[1]-d.coords[1],q[2]=b.coords[2]-d.coords[2],n[0]=m[1]*q[2]-m[2]*q[1],n[1]=m[2]*q[0]-m[0]*q[2],n[2]=m[0]*q[1]-m[1]*q[0],f=n[0]*n[0]+n[1]*n[1]+n[2]*n[2],f>i&&(i=f,a[0]=n[0],a[1]=n[1],a[2]=n[2]);i<=0&&(a[0]=a[1]=a[2]=0,a[this.longAxis_(m)]=1)},checkOrientation_:function(){var a,b,c=this.mesh.fHead,d,e=this.mesh.vHead,f;for(a=0,b=c.next;b!==c;b=b.next){f=b.anEdge;if(f.winding<=0)continue;do a+=(f.Org.s-f.Dst.s)*(f.Org.t+f.Dst.t),f=f.Lnext;while(f!==b.anEdge)}if(a<0){for(d=e.next;d!==e;d=d.next)d.t=-d.t;this.tUnit[0]=-this.tUnit[0],this.tUnit[1]=-this.tUnit[1],this.tUnit[2]=-this.tUnit[2]}},projectPolygon_:function(){var a,b=this.mesh.vHead,c=[0,0,0],d,e,f,i,g=!1;for(c[0]=this.normal[0],c[1]=this.normal[1],c[2]=this.normal[2],c[0]===0&&c[1]===0&&c[2]===0&&(this.computeNormal_(c),g=!0),d=this.sUnit,e=this.tUnit,f=this.longAxis_(c),d[f]=0,d[(f+1)%3]=1,d[(f+2)%3]=0,e[f]=0,e[(f+1)%3]=0,e[(f+2)%3]=c[f]>0?1:-1,a=b.next;a!==b;a=a.next)a.s=this.dot_(a.coords,d),a.t=this.dot_(a.coords,e);for(g&&this.checkOrientation_(),i=!0,a=b.next;a!==b;a=a.next)i?(this.bmin[0]=this.bmax[0]=a.s,this.bmin[1]=this.bmax[1]=a.t,i=!1):(a.sthis.bmax[0]&&(this.bmax[0]=a.s),a.tthis.bmax[1]&&(this.bmax[1]=a.t))},addWinding_:function(a,b){a.winding+=b.winding,a.Sym.winding+=b.Sym.winding},tessellateMonoRegion_:function(a,b){var c,d;for(c=b.anEdge,l(c.Lnext!==c&&c.Lnext.Lnext!==c);j.vertLeq(c.Dst,c.Org);c=c.Lprev);for(;j.vertLeq(c.Org,c.Dst);c=c.Lnext);for(d=c.Lprev;c.Lnext!==d;)if(j.vertLeq(c.Dst,d.Org)){for(;d.Lnext!==c&&(j.edgeGoesLeft(d.Lnext)||j.edgeSign(d.Org,d.Dst,d.Lnext.Dst)<=0);){var e=a.connect(d.Lnext,d);d=e.Sym}d=d.Lprev}else{for(;d.Lnext!=c&&(j.edgeGoesRight(c.Lprev)||j.edgeSign(c.Dst,c.Org,c.Lprev.Org)>=0);){var e=a.connect(c,c.Lprev);c=e.Sym}c=c.Lnext}for(l(d.Lnext!==c);d.Lnext.Lnext!==c;){var e=a.connect(d.Lnext,d);d=e.Sym}return!0},tessellateInterior_:function(a){var b,c;for(b=a.fHead.next;b!==a.fHead;b=c){c=b.next;if(b.inside){if(!this.tessellateMonoRegion_(a,b))return!1}}return!0},discardExterior_:function(a){var b,c;for(b=a.fHead.next;b!==a.fHead;b=c)c=b.next,b.inside||a.zapFace(b)},setWindingNumber_:function(a,b,c){var d,e;for(d=a.eHead.next;d!==a.eHead;d=e)e=d.next,d.Rface.inside!==d.Lface.inside?d.winding=d.Lface.inside?b:-b:c?a.delete(d):d.winding=0},getNeighbourFace_:function(a){return a.Rface?a.Rface.inside?a.Rface.n:-1:-1},outputPolymesh_:function(a,b,c,d){var e,f,i,g=0,k=0,m,q,n=0,s;for(c>3&&a.mergeConvexFaces(c),e=a.vHead.next;e!==a.vHead;e=e.next)e.n=-1;for(f=a.fHead.next;f!=a.fHead;f=f.next){f.n=-1;if(!f.inside)continue;i=f.anEdge,m=0;do e=i.Org,e.n===-1&&(e.n=k,k++),m++,i=i.Lnext;while(i!==f.anEdge);l(m<=c),f.n=g,++g}for(this.elementCount=g,b==p.CONNECTED_POLYGONS&&(g*=2),this.elements=[],this.elements.length=g*c,this.vertexCount=k,this.vertices=[],this.vertices.length=k*d,this.vertexIndices=[],this.vertexIndices.length=k,e=a.vHead.next;e!==a.vHead;e=e.next)if(e.n!=-1){var t=e.n*d;this.vertices[t+0]=e.coords[0],this.vertices[t+1]=e.coords[1],d>2&&(this.vertices[t+2]=e.coords[2]),this.vertexIndices[e.n]=e.idx}var u=0;for(f=a.fHead.next;f!==a.fHead;f=f.next){if(!f.inside)continue;i=f.anEdge,m=0;do e=i.Org,this.elements[u++]=e.n,m++,i=i.Lnext;while(i!==f.anEdge);for(q=m;q2&&(this.vertices[q++]=d.Org.coords[2]),this.vertexIndices[n++]=d.Org.idx,m++,d=d.Lnext;while(d!==e);this.elements[s++]=k,this.elements[s++]=m,k+=m}},addContour:function(a,b){var c,d;for(this.mesh===null&&(this.mesh=new G()),a<2&&(a=2),a>3&&(a=3),c=null,d=0;d2?c.Org.coords[2]=b[d+2]:c.Org.coords[2]=0,c.Org.idx=this.vertexIndexCounter++,c.winding=1,c.Sym.winding=-1},tesselate:function(a,b,c,d,e){this.vertices=[],this.elements=[],this.vertexIndices=[],this.vertexIndexCounter=0,e&&(this.normal[0]=e[0],this.normal[1]=e[1],this.normal[2]=e[2]),this.windingRule=a,d<2&&(d=2),d>3&&(d=3);if(!this.mesh)return!1;this.projectPolygon_(),h.computeInterior(this);var f=this.mesh;return b==p.BOUNDARY_CONTOURS?this.setWindingNumber_(f,1,!0):this.tessellateInterior_(f),f.check(),b==p.BOUNDARY_CONTOURS?this.outputContours_(f,d):this.outputPolymesh_(f,b,c,d),!0}}},{}]},{},[1])(1)}); 2 | -------------------------------------------------------------------------------- /src/tesselate.js: -------------------------------------------------------------------------------- 1 | // tesselate creates triangles from vertices 2 | export function tesselate(polygons) { 3 | return Tess2.tesselate({ 4 | contours: polygons, 5 | windingRule: Tess2.WINDING_ODD, 6 | elementType: Tess2.POLYGONS, 7 | polySize: 3, 8 | vertexSize: 2 9 | }) 10 | } 11 | 12 | function drawTriangles(g, elements, vertices) { 13 | for (let i = 0; i < elements.length; i += 3) { 14 | let a = elements[i], b = elements[i+1], c = elements[i+2] 15 | g.drawTriangle( 16 | [vertices[a*2], vertices[a*2+1]], 17 | [vertices[b*2], vertices[b*2+1]], 18 | [vertices[c*2], vertices[c*2+1]], 19 | "rgba(200,10,200,0.5)", 20 | 1, 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/train.js: -------------------------------------------------------------------------------- 1 | // import * as tf from "@tensorflow/tfjs-node" 2 | import * as fs from "fs" 3 | import * as featureio from "./featureio" 4 | 5 | const log = console.log.bind(console) 6 | 7 | // const NUM_PITCH_CLASSES = 7 8 | // const TRAINING_DATA_LENGTH = 7000 9 | // const TEST_DATA_LENGTH = 700 10 | 11 | // const model = tf.sequential() 12 | // model.add(tf.layers.dense({units: 250, activation: 'relu', inputShape: [8]})) 13 | // model.add(tf.layers.dense({units: 175, activation: 'relu'})) 14 | // model.add(tf.layers.dense({units: 150, activation: 'relu'})) 15 | // model.add(tf.layers.dense({units: NUM_PITCH_CLASSES, activation: 'softmax'})) 16 | 17 | // model.compile({ 18 | // optimizer: tf.train.adam(), 19 | // loss: 'sparseCategoricalCrossentropy', 20 | // metrics: ['accuracy'] 21 | // }) 22 | 23 | export function testTrain() { 24 | log("testTrain") 25 | console.time("featureio.readSync") 26 | let trainingData = featureio.readSync("./georgia.bin") 27 | console.timeEnd("featureio.readSync") 28 | log({row_of_trainingData: trainingData.row(3)}) 29 | } 30 | -------------------------------------------------------------------------------- /src/vec.js: -------------------------------------------------------------------------------- 1 | export class Vec2 extends Array { 2 | add(f) { checkNumArg(f) ; return new Vec2(this[0] + f, this[1] + f) } 3 | sub(f) { checkNumArg(f) ; return new Vec2(this[0] - f, this[1] - f) } 4 | mul(f) { checkNumArg(f) ; return new Vec2(this[0] * f, this[1] * f) } 5 | div(f) { checkNumArg(f) ; return new Vec2(this[0] / f, this[1] / f) } 6 | 7 | addX(f) { checkNumArg(f) ; return new Vec2(this[0] + f, this[1]) } 8 | addY(f) { checkNumArg(f) ; return new Vec2(this[0], this[1] + f) } 9 | subX(f) { checkNumArg(f) ; return new Vec2(this[0] - f, this[1]) } 10 | subY(f) { checkNumArg(f) ; return new Vec2(this[0], this[1] - f) } 11 | mulX(f) { checkNumArg(f) ; return new Vec2(this[0] * f, this[1]) } 12 | mulY(f) { checkNumArg(f) ; return new Vec2(this[0], this[1] * f) } 13 | divX(f) { checkNumArg(f) ; return new Vec2(this[0] / f, this[1]) } 14 | divY(f) { checkNumArg(f) ; return new Vec2(this[0], this[1] / f) } 15 | 16 | add2(vec) { checkVec2Arg(vec) ; return new Vec2(this[0] + vec[0], this[1] + vec[1]) } 17 | sub2(vec) { checkVec2Arg(vec) ; return new Vec2(this[0] - vec[0], this[1] - vec[1]) } 18 | mul2(vec) { checkVec2Arg(vec) ; return new Vec2(this[0] * vec[0], this[1] * vec[1]) } 19 | div2(vec) { checkVec2Arg(vec) ; return new Vec2(this[0] / vec[0], this[1] / vec[1]) } 20 | 21 | distanceTo(v) { // euclidean distance between this and v 22 | return Math.sqrt(this.squaredDistanceTo(v)) 23 | } 24 | squaredDistanceTo(v){ 25 | let x = this[0] - v[0] , y = this[1] - v[1] 26 | return x * x + y * y 27 | } 28 | angle(v) { // angle from this to v in radians 29 | return Math.atan2(this[1] - v[1], this[0] - v[0]) // + Math.PI 30 | } 31 | magnitude() { // v^2 = x^2 + y^2 32 | return Math.sqrt(this[0] * this[0] + this[1] * this[1]) 33 | } 34 | lerp(v, t) { // LERP - Linear intERPolation between this and v. t must be in range [0-1] 35 | let a = this, ax = a[0], ay = a[1] 36 | return new Vec2(ax + t * (v[0] - ax), ay + t * (v[1] - ay)) 37 | } 38 | set(f) { checkNumArg(f) ; this[0] = f ; this[1] = f ; return this } 39 | set2(vec) { checkVec2Arg(vec) ; this[0] = vec[0] ; this[1] = vec[1] ; return this } 40 | abs() { return new Vec2(Math.abs(this[0]), Math.abs(this[1])) } 41 | clone() { return new Vec2(this) } 42 | toString() { return `(${this[0]} ${this[1]})` } 43 | } 44 | 45 | function checkNumArg(v) { 46 | if (typeof v != "number") { 47 | throw new Error("argument is not a number") 48 | } 49 | } 50 | 51 | function checkVec2Arg(v) { 52 | if (!Array.isArray(v)) { 53 | throw new Error("argument is not a Vec2 or Array") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/visualize.js: -------------------------------------------------------------------------------- 1 | import { Canvas, red, orange, green, teal, blue, pink } from "./canvas" 2 | import { Vec2 } from "./vec" 3 | import { OTGlyphShapeFontSize, FeatureRayCount } from "./feature-extract" 4 | 5 | 6 | let canvas = null 7 | let featureExtractor = null 8 | let shapes = [] 9 | 10 | 11 | export function setShapes(featureExtractor_, ...shapes_) { 12 | featureExtractor = featureExtractor_ 13 | if (!featureExtractor) { 14 | shapes = [] 15 | } else { 16 | shapes = Array.from(shapes_) 17 | } 18 | redrawCanvas() 19 | } 20 | 21 | 22 | export function redrawCanvas() { 23 | if (canvas) { 24 | canvas.needsDraw = true 25 | } 26 | } 27 | 28 | 29 | export function getCanvas() { 30 | return canvas 31 | } 32 | 33 | 34 | export function createCanvas(domElement) { 35 | if (!domElement) { 36 | domElement = document.createElement("canvas") 37 | ;(document.body || document.documentElement).appendChild(domElement) 38 | } 39 | canvas = Canvas.create(domElement, (c, g) => { 40 | g.font = '11px Inter, sans-serif' 41 | 42 | // virtual canvas size; actual size is scaled 43 | const canvasSize = new Vec2(1024,512) 44 | 45 | c.transform = () => { 46 | // const scale = c.width / canvasSize 47 | // const scale = c.height / canvasSize 48 | const scale = Math.min(c.width/canvasSize[0], c.height/canvasSize[1]) 49 | c.setOrigin( 50 | // (c.width - Math.min(c.width, c.height))/2, // left-aligned within canvasSize 51 | 40 + (c.width - canvasSize[0]*scale)/2, // centered 52 | OTGlyphShapeFontSize * scale / 1.3 53 | ) 54 | g.scale(scale, scale) 55 | } 56 | 57 | window.addEventListener("load", () => { c.needsDraw = true }) 58 | 59 | c.draw = time => { 60 | if (featureExtractor && shapes.length > 0) { 61 | const font = featureExtractor.font 62 | const fontSize = canvasSize[0] * 0.3 63 | const glyphDrawScale = fontSize / OTGlyphShapeFontSize 64 | const fontScale = OTGlyphShapeFontSize / font.unitsPerEm 65 | 66 | if (shapes.length % 2 != 0) { 67 | // draw: parade 68 | let x = 0, spacing = 40 69 | for (let shape of shapes) { 70 | shape.draw(g, x, 0, fontSize, ["left","right"]) 71 | x += shape.width * glyphDrawScale + spacing 72 | } 73 | } else { 74 | // draw: pairs 75 | const maxY = featureExtractor.maxY 76 | 77 | let x = 0 78 | for (let i = 0; i < shapes.length; i += 2) { 79 | let L = shapes[i], R = shapes[i + 1] 80 | x = drawPair(x, L, R) 81 | x = drawPair(x, R, L) 82 | } 83 | 84 | function drawPair(x, shape1, shape2) { 85 | // spacing = left-glyph-rsb + right-glyph-lsb + kerning 86 | const kerning = font.getKerningValue(shape1.glyph, shape2.glyph) 87 | const spacing = shape1.RSB + shape2.LSB + kerning 88 | // const spacingPct = spacing / font.unitsPerEm 89 | const spacingPct = spacing / featureExtractor.spaceBasis 90 | 91 | // TODO: figure out what spacing% shuld be relative to in terms of the 92 | // result value [ML: labeled feature] Union bbox? UPM? 93 | // Needs to be a value that makes sense for all pairs of shapes. 94 | 95 | shape1.draw(g, x, 0, fontSize, "right") 96 | x += shape1.bbox.width * glyphDrawScale 97 | 98 | // spacing: visualize kerning 99 | // let spacingBetweenPairs = (spacing*fontScale) * glyphDrawScale 100 | 101 | // spacing: LSB & RSB 102 | let spacingBetweenPairs = (shape1.paddingRight + shape2.paddingLeft)*glyphDrawScale + 30 103 | 104 | let labelpos = new Vec2(x + spacingBetweenPairs/2, maxY*glyphDrawScale + 20) 105 | x += spacingBetweenPairs 106 | shape2.draw(g, x, 0, fontSize, "left") 107 | 108 | // text with kerning value 109 | const spacingText = ( 110 | Math.round(spacing*100) == 0 ? "0" : 111 | `${(spacingPct*100).toFixed(1)}% (${spacing})` 112 | ) 113 | g.fillStyle = "black" 114 | g.textAlign = "center" 115 | g.strokeStyle = "white" 116 | g.lineWidth = g.dp * 4 117 | g.font = `${Math.round(12 * g.dp)}px Inter, sans-serif` 118 | g.strokeText(spacingText, labelpos[0], labelpos[1]) 119 | g.fillText(spacingText, labelpos[0], labelpos[1]) 120 | // g.drawCircle(labelpos.addY(10), g.dp*2, "black") 121 | 122 | return x + shape2.width*glyphDrawScale + 40 123 | } 124 | } 125 | 126 | } 127 | 128 | g.drawOrigin(red.alpha(0.7)) 129 | // c.needsDraw = true 130 | } // draw 131 | }) 132 | } // function createCanvas 133 | 134 | --------------------------------------------------------------------------------