├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── banner.gif ├── banner.png └── social.png ├── demo ├── demo.css ├── demo.js ├── fredokaone.ttf └── index.html ├── deploy.sh ├── dist ├── zfont.es.js ├── zfont.es.js.map ├── zfont.js ├── zfont.js.map ├── zfont.min.js └── zfont.min.js.map ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── ZdogFont.js ├── ZdogText.js ├── ZdogTextGroup.js └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | demo/zfont.* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # Serverless directories 75 | .serverless 76 | 77 | # FuseBox cache 78 | .fusebox/ 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 james 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |


Zfont

3 | 4 |

5 | A text plugin for the Zdog 3D engine! Renders TrueType fonts via Typr.js | jaames.github.io/zfont 6 | 7 |

8 | 9 |

10 | Features | Caveats | Demo | Installation | Usage | API | Zdog.Font | Zdog.Text | Zdog.TextGroup | Todo | Building 11 |

12 | 13 |
14 | 15 | ## Features 16 | 17 | * Built on top of [Typr.js](https://github.com/photopea/Typr.js), which supports a wide range of .ttf and .otf fonts with speed and grace 18 | * Less than 14kB minified and gzipped 19 | * No need to worry about waiting for fonts to load; text automatically pops into existence once the font is ready 20 | * Includes support for multiline text 21 | * Update font, text, color, alignment, etc at any time 22 | * Bonus utilities for measuring text, waiting for font load & more! 23 | 24 | ## Caveats 25 | 26 | * You have to provide a .ttf to use yourself; it isn't possible to use system fonts 27 | * Character range is limited to whichever glyphs are supported by your chosen font, and font stacks/fallbacks aren't supported yet 28 | 29 | ## Demo 30 | 31 | A live demo can be found [here](https://jaames.github.io/zfont/), there's also some more in-depth examples on [Codepen](https://codepen.io/collection/DPKGvY/)! 32 | 33 | ## Installation 34 | 35 | ### Install with NPM 36 | 37 | ```bash 38 | $ npm install zfont --save 39 | ``` 40 | 41 | If you are using a module bundler like Webpack or Rollup, import Zfont into your project: 42 | 43 | ```javascript 44 | // Using ES6 module syntax 45 | import Zfont from 'zfont'; 46 | 47 | // Using CommonJS modules 48 | const Zfont = require('zfont'); 49 | ``` 50 | 51 | ### Using the jsDelivr CDN 52 | 53 | ```html 54 | 55 | ``` 56 | 57 | When manually including the library like this, it will be globally available on `window.Zfont` 58 | 59 | ### Download and Host Yourself 60 | 61 | **[Development version](https://raw.githubusercontent.com/jaames/zfont/master/dist/zfont.js)**
62 | Uncompressed at around 75kB, with source comments included 63 | 64 | **[Production version](https://raw.githubusercontent.com/jaames/zfont/master/dist/zfont.min.js)**
65 | Minified to 45kB 66 | 67 | Then add it to the `` of your page with a ` 74 | 75 | 76 | 77 | ``` 78 | 79 | ## Usage 80 | 81 | ### Register Plugin 82 | 83 | After both Zdog and Zfont have been imported/downloaded, we need to initialize the Zfont plugin. Once it's initialized, the `Zdog.Font`, `Zdog.Text` and `Zdog.TextGroup` classes will be available: 84 | 85 | ```js 86 | Zfont.init(Zdog); 87 | ``` 88 | 89 | ### Hello World 90 | 91 | (Pssst! If you prefer to dive in, check out the [basic demo over on Codepen](https://codepen.io/rakujira/pen/vqLBwz)) 92 | 93 | To draw some text in a Zdog scene, first we need to set up a new `Zdog.Font` object with the .ttf url for our desired font, then we can create a new `Zdog.Text` object and add it to the illustration like any other shape: 94 | 95 | ```js 96 | // Initialize Zfont 97 | Zfont.init(Zdog); 98 | 99 | 100 | // Create a Zdog illustration 101 | let illo = new Zdog.Illustration({ 102 | element: '.zdog-canvas' 103 | }); 104 | 105 | // Set up a font to use 106 | let myFont = new Zdog.Font({ 107 | src: './path/to/font.ttf' 108 | }); 109 | 110 | // Create a text object 111 | // This is just a Zdog.Shape object with a couple of extra parameters! 112 | new Zdog.Text({ 113 | addTo: illo, 114 | font: myFont, 115 | value: 'Hey, Zdog!', 116 | fontSize: 64, 117 | color: '#fff' 118 | }); 119 | 120 | // Animation loop 121 | function animate() { 122 | illo.updateRenderGraph(); 123 | requestAnimationFrame(animate); 124 | } 125 | animate(); 126 | ``` 127 | 128 | ### Multiline Text 129 | 130 | Both `Zdog.Text` and `Zdog.TextGroup` support multiline text, by inserting a newline character (`\n`) wherever you wish to add a line break: 131 | 132 | ```js 133 | new Zdog.Text({ 134 | ... 135 | value: 'The quick brown fox\njumps over the\nlazy zdog', 136 | }); 137 | ``` 138 | 139 | For better readability you may prefer to use an array of strings for the `value` option. In this case, each string in the array will be treated as a seperate line of text: 140 | 141 | ```js 142 | new Zdog.Text({ 143 | ... 144 | value: [ 145 | 'The quick brown fox' 146 | 'jumps over the', 147 | 'lazy zdog' 148 | ] 149 | }); 150 | ``` 151 | 152 | ### Waiting for Fonts to Load 153 | 154 | In most cases you don't have to worry about waiting for fonts to load, as text objects will magically pop into existence once their font is ready to use. However, the plugin also provides a `Zdog.waitForFonts()` utility function if you need to delay anything until all the fonts in your scene have finished loading. 155 | 156 | For example, let's modify the animation loop from the previous example so that it doesn't begin until the fonts are ready: 157 | 158 | ```js 159 | // Animation loop 160 | function animate() { 161 | illo.updateRenderGraph(); 162 | requestAnimationFrame(animate); 163 | } 164 | // Zdog.waitForFonts() returns a Promise which is resolved once all the fonts added to the scene so far have been loaded 165 | Zdog.waitForFonts().then(() => { 166 | // Once the fonts are done, start the animation loop 167 | animate(); 168 | }) 169 | ``` 170 | 171 | ## API 172 | 173 | ### Zdog.Font 174 | 175 | Represents a font that can be used by an instance of either [`Zdog.Text`](#zdogtext) or [`Zdog.TextGroup`](#zdogtextgroup). 176 | 177 | ```js 178 | let font = new Zdog.Font({ 179 | src: './path/to/font.ttf' 180 | }) 181 | ``` 182 | 183 | #### Options 184 | 185 | | Param | Details | Default | 186 | |:-----------|:--------|:--------| 187 | | `src` | Font URL path. This can be a `.ttf` or `.otf` font, check out the [Typr.js repo](https://github.com/photopea/Typr.js) for more details about font support | `''` | 188 | 189 | #### Methods 190 | 191 | ##### `measureText(text, fontSize)` 192 | 193 | Get the measurements for the specified string `text` when rendered at `fontSize` (measured in pixels), similar to [`Canvas​Rendering​Context2D.measure​Text()`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText). 194 | 195 | Returns an object with `width`, `height`, `descender`, `ascender`. 196 | 197 | ##### `getTextPath(text, fontSize, x=0, y=0, z=0, alignX='left', alignY='bottom')` 198 | 199 | Returns an array of [Zdog path commands](https://zzz.dog/shapes#shape-path-commands) for the specified string `text`, when rendered at `fontSize` (measured in pixels). 200 | 201 | * (`x`, `y`, `z`) is the origin point of the path 202 | * `alignX` is the horizontal text alignment (equivalent to the CSS `text-align` property); either `"left"`, `"center"` or `"right"`. 203 | * `alignY` is the vertical text alignment; either `"top"`, `"middle"` or `"bottom".` 204 | 205 | ##### `waitForLoad()` 206 | 207 | Returns a Promise which resolves once this font has finished loading. 208 | 209 | ### Zdog.Text 210 | 211 | An object used for rendering text. It inherits everything from the [`Zdog.Shape`](https://zzz.dog/api#shape) class. 212 | 213 | ```js 214 | new Zdog.Text({ 215 | addTo: illo, 216 | font: font, 217 | value: 'Hey, Zdog!', 218 | textAlign: 'center', 219 | textBaseline: 'middle', 220 | color: '#5222ee', 221 | stroke: 1, 222 | }) 223 | ``` 224 | 225 | #### Options 226 | 227 | `Zdog.Text` inherits all the options from the [`Zdog.Shape`](https://zzz.dog/api#shape) class, plus a couple of extras: 228 | 229 | | Param | Details | Default | 230 | |:-----------|:--------|:--------| 231 | | `font` | [`Zdog.Font`](#zdog-font) to use for this text. This is required. | `null` | 232 | | `value` | Text string | `''` | 233 | | `fontSize` | Text size, measured in pixels | `64` | 234 | | `textAlign`| Horizontal text alignment, equivalent to the CSS `text-align` property. This can be either `'left'`, `'center'` or `'right'` | `'left'` | 235 | | `textBaseline`| Vertical text alignment, equivalent to the HTML5 canvas' [`textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline) property. This can be either `'top'`, `'middle'` or `'bottom'` | `'bottom'` | 236 | 237 | #### Properties 238 | 239 | `Zdog.Text` inherits all the properties from the [`Zdog.Shape`](https://zzz.dog/api#shape) class, as well as some extras. All of these properties can be updated at any time and the rendered text will update automatically. 240 | 241 | ##### `font` 242 | 243 | The [`Zdog.Font`](#zdog-font) instance being used for this text. 244 | 245 | ##### `value` 246 | 247 | Text value as a string. 248 | 249 | ##### `fontSize` 250 | 251 | Font size, measured in pixels. 252 | 253 | ##### `textAlign` 254 | 255 | Horizontal text alignment, equivalent to the CSS `text-align` property. This can be either `'left'`, `'center'` or `'right'` 256 | 257 | ##### `textBaseline` 258 | 259 | Vertical text alignment, equivalent to the HTML5 canvas' [`textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline) property. This can be either `'top'`, `'middle'` or `'bottom'` 260 | 261 | ### Zdog.TextGroup 262 | 263 | This class is very similar to [`Zdog.Text`](#zdog-text), except it acts as a [`Zdog.Group`](https://zzz.dog/api#group) instead, and each text glyph is rendered as its own shape. This is helpful for more advanced use-cases where you need control over each character. 264 | 265 | ```js 266 | new Zdog.TextGroup({ 267 | addTo: illo, 268 | font: font, 269 | value: 'Hey, Zdog!', 270 | textAlign: 'center', 271 | color: '#5222ee', 272 | stroke: 2, 273 | }) 274 | ``` 275 | 276 | #### Options 277 | 278 | `Zdog.TextGroup` inherits all the options from the [`Zdog.Group`](https://zzz.dog/api#group) class, plus a few extras: 279 | 280 | | Param | Details | Default | 281 | |:-----------|:--------|:--------| 282 | | `font` | [`Zdog.Font`](#zdog-font) to use for this text. This is required. | `null` | 283 | | `value` | Text string | `''` | 284 | | `fontSize` | Text size, measured in pixels | `64` | 285 | | `textAlign`| Horizontal text alignment, equivalent to the CSS `text-align` property. This can be either `'left'`, `'center'` or `'right'` | `'left'` | 286 | | `textBaseline`| Vertical text alignment, equivalent to the HTML5 canvas' [`textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline) property. This can be either `'top'`, `'middle'` or `'bottom'` | `'bottom'` | 287 | | `color` | Text color | `#333` | 288 | | `fill` | Text fill | `false` | 289 | | `stroke` | Text stroke | `stroke` | 290 | 291 | #### Properties 292 | 293 | `Zdog.TextGroup` inherits all the properties from the [`Zdog.Group`](https://zzz.dog/api#group) class, as well as some extras. All of these properties can be updated at any time and the rendered text will update automatically. 294 | 295 | ##### `font` 296 | 297 | The [`Zdog.Font`](#zdog-font) instance being used for this text. 298 | 299 | ##### `value` 300 | 301 | Text value as a string. 302 | 303 | ##### `fontSize` 304 | 305 | Font size, measured in pixels. 306 | 307 | ##### `textAlign` 308 | 309 | Horizontal text alignment, equivalent to the CSS `text-align` property. This can be either `'left'`, `'center'` or `'right'` 310 | 311 | ##### `textBaseline` 312 | 313 | Vertical text alignment, equivalent to the HTML5 canvas' [`textBaseline`](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textBaseline) property. This can be either `'top'`, `'middle'` or `'bottom'` 314 | 315 | ##### `color` 316 | 317 | Text color, equivalent to [`Shape.color`](https://zzz.dog/api#shape-color). Setting this will update the color for all of the group's children. 318 | 319 | ##### `fill` 320 | 321 | Text fill, equivalent to [`Shape.fill`](https://zzz.dog/api#shape-fill). Setting this will update the fill for all of the group's children. 322 | 323 | ##### `stroke` 324 | 325 | Text stroke, equivalent to [`Shape.stroke`](https://zzz.dog/api#shape-stroke). Setting this will update the stroke for all of the group's children. 326 | 327 | ### Zdog.waitForFonts 328 | 329 | Returns a Promise which resolves as soon as all the fonts currently added to the scene are loaded and ready for use. 330 | 331 | ```js 332 | Zdog.waitForFonts().then(function() { 333 | // Do something once the font is ready 334 | } 335 | ``` 336 | 337 | ## Todo 338 | 339 | * Google Fonts & Typekit integration? 340 | * Support for different text directions, e.g. right-to-left 341 | * Support for fallback fonts 342 | * Support for color (SVG) fonts 343 | 344 | ## Building 345 | 346 | ### Install Dependencies with NPM 347 | 348 | ```bash 349 | $ npm install 350 | ``` 351 | 352 | ### Run Devserver 353 | 354 | ```bash 355 | $ npm start 356 | ``` 357 | 358 | ### Build production files 359 | 360 | ```bash 361 | $ npm run build 362 | ``` 363 | 364 | ---- 365 | 366 | 2019 [James Daniel](//github.com/jaames) 367 | -------------------------------------------------------------------------------- /assets/banner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaames/zfont/6981c780303ecabf3bb44473cc6277c749f43683/assets/banner.gif -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaames/zfont/6981c780303ecabf3bb44473cc6277c749f43683/assets/banner.png -------------------------------------------------------------------------------- /assets/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaames/zfont/6981c780303ecabf3bb44473cc6277c749f43683/assets/social.png -------------------------------------------------------------------------------- /demo/demo.css: -------------------------------------------------------------------------------- 1 | @import url('https://simbo.codes/css-reset-and-normalize/reset-and-normalize.min.css'); 2 | @import url('https://fonts.googleapis.com/css?family=Fredoka+One'); 3 | 4 | body { 5 | color: #000032; 6 | background: #eef; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 8 | font-size: 16px; 9 | line-height: 1.5; 10 | letter-spacing: .015em; 11 | font-smoothing: antialiased; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -webkit-text-rendering: optimizeLegibility; 15 | -moz-font-smoothing: antialiased; 16 | -moz-text-rendering: optimizeLegibility; 17 | } 18 | 19 | h1, h2, h3, h4, h5, h6, .Button { 20 | font-family: "Fredoka One", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 21 | } 22 | 23 | h1 { 24 | font-size: 3.5rem; 25 | font-weight: bold; 26 | } 27 | 28 | h2 { 29 | font-size: 1.25rem; 30 | } 31 | 32 | h3 { 33 | font-size: 1.25rem; 34 | } 35 | 36 | a { 37 | color: #645bdf; 38 | cursor: pointer; 39 | text-decoration: none; 40 | border-bottom: .08em dashed currentColor; 41 | } 42 | 43 | a:hover { 44 | color: #5222ee; 45 | } 46 | 47 | .Page { 48 | width: 100%; 49 | max-width: 728px; 50 | margin: 0 auto; 51 | padding: 3rem 12px; 52 | } 53 | 54 | .Header { 55 | text-align: center; 56 | margin: 4rem 0; 57 | } 58 | 59 | .Section { 60 | margin: 2rem 0; 61 | } 62 | 63 | .Section h3 { 64 | margin-bottom: 1em; 65 | } 66 | 67 | .Button { 68 | display: inline-block; 69 | background: #000032; 70 | box-shadow: 0 5px 0 -2px #adadc0; 71 | color: #fff; 72 | padding: 0.5em 1.5em; 73 | border-radius: 12px; 74 | margin: 0 6px; 75 | border: 0; 76 | font-size: 1.2rem; 77 | transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out; 78 | } 79 | 80 | .Button:hover { 81 | color: #6e66e2; 82 | transform: scale(1.025); 83 | box-shadow: 0 8px 0 -5px #adadc0; 84 | } 85 | 86 | .ButtonGroup { 87 | margin: 2rem 0; 88 | display: flex; 89 | justify-content: center; 90 | } 91 | 92 | .ButtonGroup .Button { 93 | /* flex: 1 */ 94 | } 95 | 96 | .FeatureList { 97 | padding: 0; 98 | list-style-type: none; 99 | } 100 | 101 | .FeatureList .FeatureItem { 102 | padding: 8px 0; 103 | } 104 | 105 | @media screen and (min-width: 700px) { 106 | .FeatureList { 107 | display: flex; 108 | margin: 0 -8px; 109 | } 110 | .FeatureList .FeatureItem { 111 | padding: 0 8px; 112 | flex: 1; 113 | } 114 | } 115 | 116 | .FeatureList .FeatureItem p { 117 | /* font-family: */ 118 | } 119 | 120 | .Canvas { 121 | width: 100%; 122 | border-radius: 12px; 123 | background: #5222ee; 124 | box-shadow: 0 5px 0 -2px #8d88d6; 125 | cursor: pointer; 126 | } 127 | 128 | .Textarea { 129 | /* font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; */ 130 | border: 2px solid #dde; 131 | box-shadow: 0 5px 0 -2px #c3c3dd; 132 | border-radius: 12px; 133 | display: block; 134 | width: 100%; 135 | margin-top: 12px; 136 | padding: 16px; 137 | resize: vertical; 138 | } 139 | 140 | .Footer { 141 | text-align: right; 142 | margin: 2rem 0; 143 | } 144 | 145 | .GithubCorner { 146 | z-index: 10; 147 | border: 0; 148 | display: block; 149 | position: fixed; 150 | top: -5px; 151 | right: -5px; 152 | background: transparent; 153 | } 154 | 155 | .GithubCorner__svg { 156 | width: 100px; 157 | height: 100px; 158 | fill: #000032; 159 | transition: transform 0.2s; 160 | } 161 | 162 | .GithubCorner:hover .GithubCorner__svg { 163 | -webkit-transform: translate(-5px,5px); 164 | transform: translate(-5px,5px); 165 | } 166 | 167 | .GithubCorner .octo-arm, 168 | .GithubCorner .octo-body { 169 | fill: #eef; 170 | } -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | // Register Zfont plugin 2 | Zfont.init(Zdog); 3 | 4 | const textarea = document.getElementById('textarea'); 5 | 6 | // create illo 7 | let illo = new Zdog.Illustration({ 8 | element: '.Canvas', 9 | dragRotate: true, 10 | resize: true, 11 | rotate: {x: -0.32, y: 0.64, z: 0} 12 | }); 13 | 14 | // create font 15 | let font = new Zdog.Font({ 16 | src: './fredokaone.ttf' 17 | }); 18 | 19 | let text = new Zdog.Text({ 20 | addTo: illo, 21 | font: font, 22 | value: textarea.value, 23 | fontSize: 48, 24 | textAlign: 'center', 25 | textBaseline: 'middle', 26 | color: '#fff', 27 | fill: true, 28 | }); 29 | 30 | // text "shadow" 31 | let shadow1 = text.copy({ 32 | addTo: illo, 33 | translate: {z: -3}, 34 | color: '#aab', 35 | }); 36 | let shadow2 = text.copy({ 37 | addTo: illo, 38 | translate: {z: -6}, 39 | color: '#aab', 40 | }); 41 | 42 | textarea.addEventListener('input', function() { 43 | text.value = textarea.value; 44 | shadow1.value = textarea.value; 45 | shadow2.value = textarea.value; 46 | }); 47 | 48 | // animation loop 49 | function animate() { 50 | illo.updateRenderGraph(); 51 | requestAnimationFrame(animate); 52 | } 53 | 54 | animate(); -------------------------------------------------------------------------------- /demo/fredokaone.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaames/zfont/6981c780303ecabf3bb44473cc6277c749f43683/demo/fredokaone.ttf -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Zfont 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |

Zfont

23 |

24 | A text plugin for the Zdog 3D engine 25 |

26 | 34 |
35 |
36 |

Features

37 | 51 |
52 |
53 |

Demo

54 | 55 | 56 |
57 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # abort on errors 4 | set -e 5 | 6 | # copy dist code into demo 7 | cp -a dist/. demo/ 8 | 9 | # navigate into the build output directory 10 | cd demo 11 | 12 | # create commit 13 | git init 14 | git add -A 15 | git commit -m 'deploy demo page' 16 | 17 | # force push commit to gh-pages branch 18 | git push -f git@github.com:jaames/zfont.git master:gh-pages 19 | 20 | # navigate to last directory 21 | cd - -------------------------------------------------------------------------------- /dist/zfont.es.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Zfont v1.2.8 3 | * Text plugin for Zdog 4 | * 2019 James Daniel 5 | * MIT Licensed 6 | * github.com/jaames/zfont 7 | */ 8 | 9 | var Typr={};Typr.parse=function(buff){var bin=Typr._bin;var data=new Uint8Array(buff);var offset=0;var sfnt_version=bin.readFixed(data,offset);offset+=4;var numTables=bin.readUshort(data,offset);offset+=2;var searchRange=bin.readUshort(data,offset);offset+=2;var entrySelector=bin.readUshort(data,offset);offset+=2;var rangeShift=bin.readUshort(data,offset);offset+=2;var tags=["cmap","head","hhea","maxp","hmtx","name","OS/2","post","loca","glyf","kern","CFF ","GPOS","GSUB","SVG "];var obj={_data:data};var tabs={};for(var i=0;i>>i&1)!=0){ num++; } }return num};Typr._lctf.readClassDef=function(data,offset){var bin=Typr._bin;var obj=[];var format=bin.readUshort(data,offset);offset+=2;if(format==1){var startGlyph=bin.readUshort(data,offset);offset+=2;var glyphCount=bin.readUshort(data,offset);offset+=2;for(var i=0;i255){ return -1; }return Typr.CFF.glyphByUnicode(cff,Typr.CFF.tableSE[charcode])};Typr.CFF.readEncoding=function(data,offset,num){var bin=Typr._bin;var array=[".notdef"];var format=data[offset];offset++;if(format==0){var nCodes=data[offset];offset++;for(var i=0;i>4,nib1=b&15;if(nib0!=15){ nibs.push(nib0); }if(nib1!=15){ nibs.push(nib1); }if(nib1==15){ break }}var s="";var chars=[0,1,2,3,4,5,6,7,8,9,".","e","e-","reserved","-","endOfNumber"];for(var i=0;i=gl.xMax||gl.yMin>=gl.yMax){ return null; }if(gl.noc>0){gl.endPts=[];for(var i=0;i>>8;format&=15;if(format==0){ offset=Typr.kern.readFormat0(data,offset,map); }else { throw "unknown kern table format: "+format }}return map};Typr.kern.parseV1=function(data,offset,length,font){var bin=Typr._bin;var version=bin.readFixed(data,offset);offset+=4;var nTables=bin.readUint(data,offset);offset+=4;var map={glyph1:[],rval:[]};for(var i=0;i>>8;format&=15;if(format==0){ offset=Typr.kern.readFormat0(data,offset,map); }else { throw "unknown kern table format: "+format }}return map};Typr.kern.readFormat0=function(data,offset,map){var bin=Typr._bin;var pleft=-1;var nPairs=bin.readUshort(data,offset);offset+=2;var searchRange=bin.readUshort(data,offset);offset+=2;var entrySelector=bin.readUshort(data,offset);offset+=2;var rangeShift=bin.readUshort(data,offset);offset+=2;for(var j=0;j=tab.map.length){ return 0; }return tab.map[code]}else if(tab.format==4){var sind=-1;for(var i=0;icode){ return 0; }var gli=0;if(tab.idRangeOffset[sind]!=0){ gli=tab.glyphIdArray[code-tab.startCount[sind]+(tab.idRangeOffset[sind]>>1)-(tab.idRangeOffset.length-sind)]; }else { gli=code+tab.idDelta[sind]; }return gli&65535}else if(tab.format==12){if(code>tab.groups[tab.groups.length-1][1]){ return 0; }for(var i=0;i-1){ Typr.U._simpleGlyph(gl,path); }else { Typr.U._compoGlyph(gl,font,path); }}};Typr.U._simpleGlyph=function(gl,p){for(var c=0;c65535){ i++; }gls.push(Typr.U.codeToGlyph(font,cc));}var gsub=font["GSUB"];if(gsub==null){ return gls; }var llist=gsub.lookupList,flist=gsub.featureList;var wsep='\n\t" ,.:;!?() ،';var R="آأؤإاةدذرزوٱٲٳٵٶٷڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙۀۃۄۅۆۇۈۉۊۋۍۏےۓەۮۯܐܕܖܗܘܙܞܨܪܬܯݍݙݚݛݫݬݱݳݴݸݹࡀࡆࡇࡉࡔࡧࡩࡪࢪࢫࢬࢮࢱࢲࢹૅેૉ૊૎૏ૐ૑૒૝ૡ૤૯஁ஃ஄அஉ஌எஏ஑னப஫஬";var L="ꡲ્૗";for(var ci=0;cirlim){ continue; }var good=true;for(var l=0;lrlim){ continue; }var good=true;for(var l=0;l>1;stack.length=0;haveWidth=true;}else if(v=="o3"||v=="o23"){var hasWidthArg;hasWidthArg=stack.length%2!==0;if(hasWidthArg&&!haveWidth){width=stack.shift()+font.Private.nominalWidthX;}nStems+=stack.length>>1;stack.length=0;haveWidth=true;}else if(v=="o4"){if(stack.length>1&&!haveWidth){width=stack.shift()+font.Private.nominalWidthX;haveWidth=true;}if(open){ Typr.U.P.closePath(p); }y+=stack.pop();Typr.U.P.moveTo(p,x,y);open=true;}else if(v=="o5"){while(stack.length>0){x+=stack.shift();y+=stack.shift();Typr.U.P.lineTo(p,x,y);}}else if(v=="o6"||v=="o7"){var count=stack.length;var isX=v=="o6";for(var j=0;jMath.abs(c4y-y)){x=c4x+stack.shift();}else {y=c4y+stack.shift();}Typr.U.P.curveTo(p,c1x,c1y,c2x,c2y,jpx,jpy);Typr.U.P.curveTo(p,c3x,c3y,c4x,c4y,x,y);}}else if(v=="o14"){if(stack.length>0&&!haveWidth){width=stack.shift()+font.nominalWidthX;haveWidth=true;}if(stack.length==4){var adx=stack.shift();var ady=stack.shift();var bchar=stack.shift();var achar=stack.shift();var bind=Typr.CFF.glyphBySE(font,bchar);var aind=Typr.CFF.glyphBySE(font,achar);Typr.U._drawCFF(font.CharStrings[bind],state,font,p);state.x=adx;state.y=ady;Typr.U._drawCFF(font.CharStrings[aind],state,font,p);}if(open){Typr.U.P.closePath(p);open=false;}}else if(v=="o19"||v=="o20"){var hasWidthArg;hasWidthArg=stack.length%2!==0;if(hasWidthArg&&!haveWidth){width=stack.shift()+font.Private.nominalWidthX;}nStems+=stack.length>>1;stack.length=0;haveWidth=true;i+=nStems+7>>3;}else if(v=="o21"){if(stack.length>2&&!haveWidth){width=stack.shift()+font.Private.nominalWidthX;haveWidth=true;}y+=stack.pop();x+=stack.pop();if(open){ Typr.U.P.closePath(p); }Typr.U.P.moveTo(p,x,y);open=true;}else if(v=="o22"){if(stack.length>1&&!haveWidth){width=stack.shift()+font.Private.nominalWidthX;haveWidth=true;}x+=stack.pop();if(open){ Typr.U.P.closePath(p); }Typr.U.P.moveTo(p,x,y);open=true;}else if(v=="o25"){while(stack.length>6){x+=stack.shift();y+=stack.shift();Typr.U.P.lineTo(p,x,y);}c1x=x+stack.shift();c1y=y+stack.shift();c2x=c1x+stack.shift();c2y=c1y+stack.shift();x=c2x+stack.shift();y=c2y+stack.shift();Typr.U.P.curveTo(p,c1x,c1y,c2x,c2y,x,y);}else if(v=="o26"){if(stack.length%2){x+=stack.shift();}while(stack.length>0){c1x=x;c1y=y+stack.shift();c2x=c1x+stack.shift();c2y=c1y+stack.shift();x=c2x;y=c2y+stack.shift();Typr.U.P.curveTo(p,c1x,c1y,c2x,c2y,x,y);}}else if(v=="o27"){if(stack.length%2){y+=stack.shift();}while(stack.length>0){c1x=x+stack.shift();c1y=y;c2x=c1x+stack.shift();c2y=c1y+stack.shift();x=c2x+stack.shift();y=c2y;Typr.U.P.curveTo(p,c1x,c1y,c2x,c2y,x,y);}}else if(v=="o10"||v=="o29"){var obj=v=="o10"?font.Private:font;if(stack.length==0){console.log("error: empty stack");}else {var ind=stack.pop();var subr=obj.Subrs[ind+obj.Bias];state.x=x;state.y=y;state.nStems=nStems;state.haveWidth=haveWidth;state.width=width;state.open=open;Typr.U._drawCFF(subr,state,font,p);x=state.x;y=state.y;nStems=state.nStems;haveWidth=state.haveWidth;width=state.width;open=state.open;}}else if(v=="o30"||v=="o31"){var count,count1=stack.length;var index=0;var alternate=v=="o31";count=count1&~2;index+=count1-count;while(index -1 && glyphId < advanceWidthTable.length) { 93 | advanceWidth += advanceWidthTable[glyphId]; 94 | } 95 | return advanceWidth; 96 | }, 0); 97 | }); 98 | var width = Math.max.apply(Math, lineWidths); 99 | var lineHeight = (0 - descender) + ascender; 100 | var height = lineHeight * lines.length; 101 | 102 | // Multiply by fontScale to convert from font units to pixels 103 | return { 104 | width: width * fontScale, 105 | height: height * fontScale, 106 | lineHeight: lineHeight * fontScale, 107 | lineWidths: lineWidths.map(function (width) { return width * fontScale; }), 108 | descender: descender * fontScale, 109 | ascender: ascender * fontScale, 110 | }; 111 | }; 112 | 113 | ZdogFont.prototype.getTextPath = function getTextPath (text, fontSize, x, y, z, alignX, alignY) { 114 | var this$1 = this; 115 | if ( fontSize === void 0 ) fontSize=64; 116 | if ( x === void 0 ) x=0; 117 | if ( y === void 0 ) y=0; 118 | if ( z === void 0 ) z=0; 119 | if ( alignX === void 0 ) alignX='left'; 120 | if ( alignY === void 0 ) alignY='bottom'; 121 | 122 | if (!this._hasLoaded) { 123 | return []; 124 | } 125 | var lines = Array.isArray(text) ? text : text.split(TEXT_NEWLINE_REGEXP); 126 | var measurements = this.measureText(text, fontSize); 127 | var lineWidths = measurements.lineWidths; 128 | var lineHeight = measurements.lineHeight; 129 | return lines.map(function (line, lineIndex) { 130 | var ref = this$1.getTextOrigin(Object.assign({}, measurements, 131 | {width: lineWidths[lineIndex]}), x, y, z, alignX, alignY); 132 | var _x = ref[0]; 133 | var _y = ref[1]; 134 | var _z = ref[2]; 135 | y += lineHeight; 136 | var glyphs = typr_js.U.stringToGlyphs(this$1.font, line); 137 | var path = typr_js.U.glyphsToPath(this$1.font, glyphs); 138 | return this$1._convertPathCommands(path, fontSize, _x, _y, z); 139 | }).flat(); 140 | }; 141 | 142 | ZdogFont.prototype.getTextGlyphs = function getTextGlyphs (text, fontSize, x, y, z, alignX, alignY) { 143 | var this$1 = this; 144 | if ( fontSize === void 0 ) fontSize=64; 145 | if ( x === void 0 ) x=0; 146 | if ( y === void 0 ) y=0; 147 | if ( z === void 0 ) z=0; 148 | if ( alignX === void 0 ) alignX='left'; 149 | if ( alignY === void 0 ) alignY='bottom'; 150 | 151 | if (!this._hasLoaded) { 152 | return []; 153 | } 154 | var measurements = this.measureText(text, fontSize); 155 | var advanceWidthTable = this.font.hmtx.aWidth; 156 | var fontScale = this.getFontScale(fontSize); 157 | var lineWidths = measurements.lineWidths; 158 | var lineHeight = measurements.lineHeight; 159 | var lines = Array.isArray(text) ? text : text.split(TEXT_NEWLINE_REGEXP); 160 | return lines.map(function (line, lineIndex) { 161 | var glyphs = typr_js.U.stringToGlyphs(this$1.font, line); 162 | var ref = this$1.getTextOrigin(Object.assign({}, measurements, 163 | {width: lineWidths[lineIndex]}), x, y, z, alignX, alignY); 164 | var _x = ref[0]; 165 | var _y = ref[1]; 166 | var _z = ref[2]; 167 | y += lineHeight; 168 | return glyphs.filter(function (glyph) { return glyph !== -1; }).map(function (glyphId) { 169 | var path = typr_js.U.glyphToPath(this$1.font, glyphId); 170 | var shape = { 171 | translate: {x:_x, y: _y, z:_z}, 172 | path: this$1._convertPathCommands(path, fontSize, 0, 0, 0) 173 | }; 174 | _x += advanceWidthTable[glyphId] * fontScale; 175 | return shape; 176 | }); 177 | }).flat(); 178 | }; 179 | 180 | ZdogFont.prototype.getTextOrigin = function getTextOrigin (measuement, x, y, z, alignX, alignY) { 181 | if ( x === void 0 ) x=0; 182 | if ( y === void 0 ) y=0; 183 | if ( z === void 0 ) z=0; 184 | if ( alignX === void 0 ) alignX='left'; 185 | if ( alignY === void 0 ) alignY='bottom'; 186 | 187 | var width = measuement.width; 188 | var height = measuement.height; 189 | var lineHeight = measuement.lineHeight; 190 | switch (alignX) { 191 | case 'right': 192 | x -= width; 193 | break; 194 | case 'center': 195 | x -= width / 2; 196 | break; 197 | } 198 | switch (alignY) { 199 | case 'middle': 200 | y -= (height / 2)- lineHeight; 201 | break; 202 | case 'bottom': 203 | default: 204 | y -= height - lineHeight; 205 | break; 206 | } 207 | return [x, y, z]; 208 | }; 209 | 210 | // Convert Typr.js path commands to Zdog commands 211 | // Also apply font size scaling and coordinate adjustment 212 | // https://github.com/photopea/Typr.js 213 | // https://zzz.dog/shapes#shape-path-commands 214 | ZdogFont.prototype._convertPathCommands = function _convertPathCommands (path, fontSize, x, y, z) { 215 | if ( x === void 0 ) x=0; 216 | if ( y === void 0 ) y=0; 217 | if ( z === void 0 ) z=0; 218 | 219 | var yDir = -1; 220 | var xDir = 1; 221 | var fontScale = this.getFontScale(fontSize); 222 | var commands = path.cmds; 223 | // Apply font scale to all coords 224 | var coords = path.crds.map(function (coord) { return coord * fontScale; }); 225 | // Convert coords to Zdog commands 226 | var startCoord = null; 227 | var coordOffset = 0; 228 | return commands.map(function (cmd) { 229 | var result = null; 230 | if (!startCoord) { 231 | startCoord = {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z: z}; 232 | } 233 | switch (cmd) { 234 | case 'M': // moveTo command 235 | result = { 236 | move: {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z: z} 237 | }; 238 | coordOffset += 2; 239 | return result; 240 | case 'L': // lineTo command 241 | result = { 242 | line: {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z: z} 243 | }; 244 | coordOffset += 2; 245 | return result; 246 | case 'C': // curveTo command 247 | result = { 248 | bezier: [ 249 | {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z: z}, 250 | {x: x + coords[coordOffset + 2] * xDir, y: y + coords[coordOffset + 3] * yDir, z: z}, 251 | {x: x + coords[coordOffset + 4] * xDir, y: y + coords[coordOffset + 5] * yDir, z: z} ] 252 | }; 253 | coordOffset += 6; 254 | return result; 255 | case 'Q': // arcTo command 256 | result = { 257 | arc: [ 258 | {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z: z}, 259 | {x: x + coords[coordOffset + 2] * xDir, y: y + coords[coordOffset + 3] * yDir, z: z} ] 260 | }; 261 | coordOffset += 4; 262 | return result; 263 | case 'Z': // close path 264 | if (startCoord) { 265 | result = { 266 | line: startCoord 267 | }; 268 | startCoord = null; 269 | } 270 | return result; 271 | // unhandled type 272 | // currently, #rrggbb and X types (used in multicolor fonts) aren't supported 273 | default: 274 | return result; 275 | } 276 | }).filter(function (cmd) { return cmd !== null; }); // filter out null commands 277 | }; 278 | 279 | ZdogFont.prototype._fetchFontResource = function _fetchFontResource (source) { 280 | return new Promise(function (resolve, reject) { 281 | var request = new XMLHttpRequest(); 282 | // Fetch as an arrayBuffer for Typr.parse 283 | request.responseType = 'arraybuffer'; 284 | request.open('GET', source, true); 285 | request.onreadystatechange = function (e) { 286 | if (request.readyState === 4) { 287 | if (request.status >= 200 && request.status < 300) { 288 | resolve(request.response); 289 | } else { 290 | reject(("HTTP error " + (request.status) + ": " + (request.statusText))); 291 | } 292 | } 293 | }; 294 | request.send(null); 295 | }); 296 | }; 297 | 298 | Zdog.Font = ZdogFont; 299 | return Zdog; 300 | } 301 | 302 | function objectWithoutProperties (obj, exclude) { var target = {}; for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k) && exclude.indexOf(k) === -1) target[k] = obj[k]; return target; } 303 | function registerTextClass(Zdog) { 304 | 305 | // Zdog.Text class 306 | var ZdogText = /*@__PURE__*/(function (superclass) { 307 | function ZdogText(props) { 308 | // Set missing props to default values 309 | props = Zdog.extend({ 310 | font: null, 311 | value: '', 312 | fontSize: 64, 313 | textAlign: 'left', 314 | textBaseline: 'bottom', 315 | }, props); 316 | // Split props 317 | var font = props.font; 318 | var value = props.value; 319 | var fontSize = props.fontSize; 320 | var textAlign = props.textAlign; 321 | var textBaseline = props.textBaseline; 322 | var rest = objectWithoutProperties( props, ["font", "value", "fontSize", "textAlign", "textBaseline"] ); 323 | var shapeProps = rest; 324 | // Create shape object 325 | superclass.call(this, Object.assign({}, shapeProps, 326 | {closed: true, 327 | visible: false, // hide until font is loaded 328 | path: [{}]})); 329 | this._font = null; 330 | this._value = value; 331 | this._fontSize = fontSize; 332 | this._textAlign = textAlign; 333 | this._textBaseline = textBaseline; 334 | this.font = font; 335 | } 336 | 337 | if ( superclass ) ZdogText.__proto__ = superclass; 338 | ZdogText.prototype = Object.create( superclass && superclass.prototype ); 339 | ZdogText.prototype.constructor = ZdogText; 340 | 341 | var prototypeAccessors = { font: { configurable: true },value: { configurable: true },fontSize: { configurable: true },textAlign: { configurable: true },textBaseline: { configurable: true } }; 342 | 343 | ZdogText.prototype.updateText = function updateText () { 344 | var path = this.font.getTextPath(this.value, this.fontSize, 0, 0, 0, this.textAlign, this.textBaseline); 345 | if (path.length == 0) { // zdog doesn't know what to do with empty path arrays 346 | this.path = [{}]; 347 | this.visible = false; 348 | } else { 349 | this.path = path; 350 | this.visible = true; 351 | } 352 | this.updatePath(); 353 | }; 354 | 355 | prototypeAccessors.font.set = function (newFont) { 356 | var this$1 = this; 357 | 358 | this._font = newFont; 359 | this.font.waitForLoad().then(function () { 360 | this$1.updateText(); 361 | this$1.visible = true; 362 | // Find root Zdog.Illustration instance 363 | var root = this$1.addTo; 364 | while (root.addTo !== undefined) { 365 | root = root.addTo; 366 | } 367 | // Update render graph 368 | if (root && typeof root.updateRenderGraph === 'function') { 369 | root.updateRenderGraph(); 370 | } 371 | }); 372 | }; 373 | 374 | prototypeAccessors.font.get = function () { 375 | return this._font; 376 | }; 377 | 378 | prototypeAccessors.value.set = function (newValue) { 379 | this._value = newValue; 380 | this.updateText(); 381 | }; 382 | 383 | prototypeAccessors.value.get = function () { 384 | return this._value; 385 | }; 386 | 387 | prototypeAccessors.fontSize.set = function (newSize) { 388 | this._fontSize = newSize; 389 | this.updateText(); 390 | }; 391 | 392 | prototypeAccessors.fontSize.get = function () { 393 | return this._fontSize; 394 | }; 395 | 396 | prototypeAccessors.textAlign.set = function (newValue) { 397 | this._textAlign = newValue; 398 | this.updateText(); 399 | }; 400 | 401 | prototypeAccessors.textAlign.get = function () { 402 | return this._textAlign; 403 | }; 404 | 405 | prototypeAccessors.textBaseline.set = function (newValue) { 406 | this._textBaseline = newValue; 407 | this.updateText(); 408 | }; 409 | 410 | prototypeAccessors.textBaseline.get = function () { 411 | return this._textBaseline; 412 | }; 413 | 414 | Object.defineProperties( ZdogText.prototype, prototypeAccessors ); 415 | 416 | return ZdogText; 417 | }(Zdog.Shape)); 418 | 419 | ZdogText.optionKeys = ZdogText.optionKeys.concat(['font', 'fontSize', 'value', 'textAlign', 'textBaseline']); 420 | Zdog.Text = ZdogText; 421 | return Zdog; 422 | } 423 | 424 | function objectWithoutProperties$1 (obj, exclude) { var target = {}; for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k) && exclude.indexOf(k) === -1) target[k] = obj[k]; return target; } 425 | function registerTextGroupClass(Zdog) { 426 | 427 | // Zdog.TextGroup class 428 | var ZdogTextGroup = /*@__PURE__*/(function (superclass) { 429 | function ZdogTextGroup(props) { 430 | // Set missing props to default values 431 | props = Zdog.extend({ 432 | font: null, 433 | value: '', 434 | fontSize: 64, 435 | textAlign: 'left', 436 | textBaseline: 'bottom', 437 | color: '#333', 438 | fill: false, 439 | stroke: 1, 440 | }, props); 441 | // Split props 442 | var font = props.font; 443 | var value = props.value; 444 | var fontSize = props.fontSize; 445 | var textAlign = props.textAlign; 446 | var textBaseline = props.textBaseline; 447 | var color = props.color; 448 | var fill = props.fill; 449 | var stroke = props.stroke; 450 | var rest = objectWithoutProperties$1( props, ["font", "value", "fontSize", "textAlign", "textBaseline", "color", "fill", "stroke"] ); 451 | var groupProps = rest; 452 | // Create group object 453 | superclass.call(this, Object.assign({}, groupProps, 454 | {visible: false})); 455 | this._font = null; 456 | this._value = value; 457 | this._fontSize = fontSize; 458 | this._textAlign = textAlign; 459 | this._textBaseline = textBaseline; 460 | this._color = color; 461 | this._fill = fill; 462 | this._stroke = stroke; 463 | this.font = font; 464 | } 465 | 466 | if ( superclass ) ZdogTextGroup.__proto__ = superclass; 467 | ZdogTextGroup.prototype = Object.create( superclass && superclass.prototype ); 468 | ZdogTextGroup.prototype.constructor = ZdogTextGroup; 469 | 470 | var prototypeAccessors = { font: { configurable: true },value: { configurable: true },fontSize: { configurable: true },textAlign: { configurable: true },textBaseline: { configurable: true },color: { configurable: true },fill: { configurable: true },stroke: { configurable: true } }; 471 | 472 | ZdogTextGroup.prototype.updateText = function updateText () { 473 | var this$1 = this; 474 | 475 | // Remove old children 476 | while (this.children.length > 0) { 477 | this.removeChild(this.children[0]); 478 | } 479 | // Get text paths for each glyph 480 | var glyphs = this.font.getTextGlyphs(this.value, this.fontSize, 0, 0, 0, this.textAlign, this.textBaseline); 481 | // Convert glyphs to new shapes 482 | glyphs.filter(function (shape) { return shape.path.length > 0; }).forEach(function (shape) { 483 | this$1.addChild(new Zdog.Shape({ 484 | translate: shape.translate, 485 | path: shape.path, 486 | color: this$1.color, 487 | fill: this$1.fill, 488 | stroke: this$1.stroke, 489 | closed: true, 490 | })); 491 | }); 492 | this.updateFlatGraph(); 493 | }; 494 | 495 | prototypeAccessors.font.set = function (newFont) { 496 | var this$1 = this; 497 | 498 | this._font = newFont; 499 | this._font.waitForLoad().then(function () { 500 | this$1.updateText(); 501 | this$1.visible = true; 502 | // Find root Zdog.Illustration instance 503 | var root = this$1.addTo; 504 | while (root.addTo !== undefined) { 505 | root = root.addTo; 506 | } 507 | // Update render graph 508 | if (root && typeof root.updateRenderGraph === 'function') { 509 | root.updateRenderGraph(); 510 | } 511 | }); 512 | }; 513 | 514 | prototypeAccessors.font.get = function () { 515 | return this._font; 516 | }; 517 | 518 | prototypeAccessors.value.set = function (newValue) { 519 | this._value = newValue; 520 | this.updateText(); 521 | }; 522 | 523 | prototypeAccessors.value.get = function () { 524 | return this._value; 525 | }; 526 | 527 | prototypeAccessors.fontSize.set = function (newSize) { 528 | this._fontSize = newSize; 529 | this.updateText(); 530 | }; 531 | 532 | prototypeAccessors.fontSize.get = function () { 533 | return this._fontSize; 534 | }; 535 | 536 | prototypeAccessors.textAlign.set = function (newValue) { 537 | this._textAlign = newValue; 538 | this.updateText(); 539 | }; 540 | 541 | prototypeAccessors.textAlign.get = function () { 542 | return this._textAlign; 543 | }; 544 | 545 | prototypeAccessors.textBaseline.set = function (newValue) { 546 | this._textBaseline = newValue; 547 | this.updateText(); 548 | }; 549 | 550 | prototypeAccessors.textBaseline.get = function () { 551 | return this._textBaseline; 552 | }; 553 | 554 | prototypeAccessors.color.set = function (newColor) { 555 | this._color = newColor; 556 | this.children.forEach(function (child) { return child.color = newColor; }); 557 | }; 558 | 559 | prototypeAccessors.color.get = function () { 560 | return this._color; 561 | }; 562 | 563 | prototypeAccessors.fill.set = function (newFill) { 564 | this._fill = newFill; 565 | this.children.forEach(function (child) { return child.fill = newFill; }); 566 | }; 567 | 568 | prototypeAccessors.fill.get = function () { 569 | return this._fill; 570 | }; 571 | 572 | prototypeAccessors.stroke.set = function (newStroke) { 573 | this._stroke = newStroke; 574 | this.children.forEach(function (child) { return child.stroke = newStroke; }); 575 | }; 576 | 577 | prototypeAccessors.stroke.get = function () { 578 | return this._stroke; 579 | }; 580 | 581 | Object.defineProperties( ZdogTextGroup.prototype, prototypeAccessors ); 582 | 583 | return ZdogTextGroup; 584 | }(Zdog.Group)); 585 | 586 | ZdogTextGroup.optionKeys = ZdogTextGroup.optionKeys.concat(['color', 'fill', 'stroke', 'font', 'fontSize', 'value', 'textAlign', 'textBaseline']); 587 | Zdog.TextGroup = ZdogTextGroup; 588 | return Zdog; 589 | } 590 | 591 | var index = { 592 | init: function init(Zdog) { 593 | // Global font list to keep track of all fonts 594 | Zdog.FontList = []; 595 | 596 | // Helper to wait for all fonts to load 597 | Zdog.waitForFonts = function() { 598 | return Promise.all(Zdog.FontList.map(function (font) { return font.waitForLoad(); })); 599 | }; 600 | 601 | // Register Zfont classes onto the Zdog object 602 | registerFontClass(Zdog); 603 | registerTextClass(Zdog); 604 | registerTextGroupClass(Zdog); 605 | 606 | return Zdog; 607 | }, 608 | version: "1.2.8", 609 | }; 610 | 611 | export default index; 612 | //# sourceMappingURL=zfont.es.js.map 613 | -------------------------------------------------------------------------------- /dist/zfont.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Zfont v1.2.8 3 | * Text plugin for Zdog 4 | * 2019 James Daniel 5 | * MIT Licensed 6 | * github.com/jaames/zfont 7 | */ 8 | !function(r,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(r=r||self).Zfont=t()}(this,function(){"use strict";var z={parse:function(r){var t=z._bin,e=new Uint8Array(r),a=0,n=(t.readFixed(e,a),t.readUshort(e,a+=4));t.readUshort(e,a+=2),t.readUshort(e,a+=2),t.readUshort(e,a+=2);a+=2;for(var o=["cmap","head","hhea","maxp","hmtx","name","OS/2","post","loca","glyf","kern","CFF ","GPOS","GSUB","SVG "],i={_data:e},s={},h=0;h>>e&1)&&t++;return t},z._lctf.readClassDef=function(r,t){var e=z._bin,a=[],n=e.readUshort(r,t);if(t+=2,1==n){var o=e.readUshort(r,t),i=e.readUshort(r,t+=2);t+=2;for(var s=0;s>4,d=15&d;if(15!=c&&u.push(c),15!=d&&u.push(d),15==d)break}for(var p="",v=[0,1,2,3,4,5,6,7,8,9,".","e","e-","reserved","-","endOfNumber"],g=0;g=o.xMax||o.yMin>=o.yMax)return null;if(0>>8;if(0!=(f&=15))throw"unknown kern table format: "+f;t=z.kern.readFormat0(r,t,s)}return s},z.kern.parseV1=function(r,t,e,a){var n=z._bin,o=(n.readFixed(r,t),n.readUint(r,t+=4));t+=4;for(var i={glyph1:[],rval:[]},s=0;s>>8;if(0!=(h&=15))throw"unknown kern table format: "+h;t=z.kern.readFormat0(r,t,i)}return i},z.kern.readFormat0=function(r,t,e){var a=z._bin,n=-1,o=a.readUshort(r,t);a.readUshort(r,t+=2),a.readUshort(r,t+=2),a.readUshort(r,t+=2);t+=2;for(var i=0;i=a.map.length?0:a.map[t];if(4==a.format){for(var n=-1,o=0;ot)return 0;return 65535&(0!=a.idRangeOffset[n]?a.glyphIdArray[t-a.startCount[n]+(a.idRangeOffset[n]>>1)-(a.idRangeOffset.length-n)]:t+a.idDelta[n])}if(12!=a.format)throw"unknown cmap table format "+a.format;if(t>a.groups[a.groups.length-1][1])return 0;for(o=0;om)){for(T=!0,w=0;w>1,i=!(n.length=0);else if("o3"==P||"o23"==P)n.length%2!=0&&!i&&(s=n.shift()+e.Private.nominalWidthX),o+=n.length>>1,i=!(n.length=0);else if("o4"==P)1Math.abs(S-u)?l=m+n.shift():u=S+n.shift(),z.U.P.curveTo(a,d,c,p,v,b,_),z.U.P.curveTo(a,g,U,m,S,l,u));else if("o14"==P)0>1,i=!(n.length=0),f+=o+7>>3;else if("o21"==P)2= 1.2.0", 500 | "ws": "^7.4.3" 501 | } 502 | }, 503 | "livereload-js": { 504 | "version": "3.3.2", 505 | "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", 506 | "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", 507 | "dev": true 508 | }, 509 | "lodash.merge": { 510 | "version": "4.6.2", 511 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 512 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 513 | "dev": true 514 | }, 515 | "magic-string": { 516 | "version": "0.25.7", 517 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", 518 | "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", 519 | "dev": true, 520 | "requires": { 521 | "sourcemap-codec": "^1.4.4" 522 | } 523 | }, 524 | "merge-stream": { 525 | "version": "2.0.0", 526 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 527 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 528 | "dev": true 529 | }, 530 | "mime": { 531 | "version": "2.5.2", 532 | "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", 533 | "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", 534 | "dev": true 535 | }, 536 | "minimist": { 537 | "version": "1.2.5", 538 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 539 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 540 | "dev": true 541 | }, 542 | "normalize-path": { 543 | "version": "3.0.0", 544 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 545 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 546 | "dev": true 547 | }, 548 | "opener": { 549 | "version": "1.5.2", 550 | "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", 551 | "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", 552 | "dev": true 553 | }, 554 | "opts": { 555 | "version": "2.0.2", 556 | "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", 557 | "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", 558 | "dev": true 559 | }, 560 | "os-homedir": { 561 | "version": "2.0.0", 562 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-2.0.0.tgz", 563 | "integrity": "sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q==", 564 | "dev": true 565 | }, 566 | "path-parse": { 567 | "version": "1.0.6", 568 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 569 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 570 | "dev": true 571 | }, 572 | "picomatch": { 573 | "version": "2.2.2", 574 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", 575 | "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", 576 | "dev": true 577 | }, 578 | "pify": { 579 | "version": "4.0.1", 580 | "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", 581 | "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", 582 | "dev": true 583 | }, 584 | "readdirp": { 585 | "version": "3.5.0", 586 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", 587 | "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", 588 | "dev": true, 589 | "requires": { 590 | "picomatch": "^2.2.1" 591 | } 592 | }, 593 | "regenerate": { 594 | "version": "1.4.2", 595 | "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", 596 | "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", 597 | "dev": true 598 | }, 599 | "regenerate-unicode-properties": { 600 | "version": "8.2.0", 601 | "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", 602 | "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", 603 | "dev": true, 604 | "requires": { 605 | "regenerate": "^1.4.0" 606 | } 607 | }, 608 | "regexpu-core": { 609 | "version": "4.7.1", 610 | "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", 611 | "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", 612 | "dev": true, 613 | "requires": { 614 | "regenerate": "^1.4.0", 615 | "regenerate-unicode-properties": "^8.2.0", 616 | "regjsgen": "^0.5.1", 617 | "regjsparser": "^0.6.4", 618 | "unicode-match-property-ecmascript": "^1.0.4", 619 | "unicode-match-property-value-ecmascript": "^1.2.0" 620 | } 621 | }, 622 | "regjsgen": { 623 | "version": "0.5.2", 624 | "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", 625 | "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", 626 | "dev": true 627 | }, 628 | "regjsparser": { 629 | "version": "0.6.9", 630 | "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", 631 | "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", 632 | "dev": true, 633 | "requires": { 634 | "jsesc": "~0.5.0" 635 | } 636 | }, 637 | "resolve": { 638 | "version": "1.20.0", 639 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", 640 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", 641 | "dev": true, 642 | "requires": { 643 | "is-core-module": "^2.2.0", 644 | "path-parse": "^1.0.6" 645 | } 646 | }, 647 | "rollup": { 648 | "version": "1.32.1", 649 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.32.1.tgz", 650 | "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", 651 | "dev": true, 652 | "requires": { 653 | "@types/estree": "*", 654 | "@types/node": "*", 655 | "acorn": "^7.1.0" 656 | } 657 | }, 658 | "rollup-plugin-alias": { 659 | "version": "1.5.2", 660 | "resolved": "https://registry.npmjs.org/rollup-plugin-alias/-/rollup-plugin-alias-1.5.2.tgz", 661 | "integrity": "sha512-ODeZXhTxpD48sfcYLAFc1BGrsXKDj7o1CSNH3uYbdK3o0NxyMmaQPTNgW+ko+am92DLC8QSTe4kyxTuEkI5S5w==", 662 | "dev": true, 663 | "requires": { 664 | "slash": "^3.0.0" 665 | } 666 | }, 667 | "rollup-plugin-buble": { 668 | "version": "0.19.8", 669 | "resolved": "https://registry.npmjs.org/rollup-plugin-buble/-/rollup-plugin-buble-0.19.8.tgz", 670 | "integrity": "sha512-8J4zPk2DQdk3rxeZvxgzhHh/rm5nJkjwgcsUYisCQg1QbT5yagW+hehYEW7ZNns/NVbDCTv4JQ7h4fC8qKGOKw==", 671 | "dev": true, 672 | "requires": { 673 | "buble": "^0.19.8", 674 | "rollup-pluginutils": "^2.3.3" 675 | } 676 | }, 677 | "rollup-plugin-commonjs": { 678 | "version": "10.1.0", 679 | "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", 680 | "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", 681 | "dev": true, 682 | "requires": { 683 | "estree-walker": "^0.6.1", 684 | "is-reference": "^1.1.2", 685 | "magic-string": "^0.25.2", 686 | "resolve": "^1.11.0", 687 | "rollup-pluginutils": "^2.8.1" 688 | } 689 | }, 690 | "rollup-plugin-filesize": { 691 | "version": "6.2.1", 692 | "resolved": "https://registry.npmjs.org/rollup-plugin-filesize/-/rollup-plugin-filesize-6.2.1.tgz", 693 | "integrity": "sha512-JQ2+NMoka81lCR2caGWyngqMKpvJCl7EkFYU7A+T0dA7U1Aml13FW5Ky0aiZIeU3/13cjsKQLRr35SQVmk6i/A==", 694 | "dev": true, 695 | "requires": { 696 | "boxen": "^4.1.0", 697 | "brotli-size": "4.0.0", 698 | "colors": "^1.3.3", 699 | "filesize": "^4.1.2", 700 | "gzip-size": "^5.1.1", 701 | "lodash.merge": "^4.6.2", 702 | "terser": "^4.1.3" 703 | } 704 | }, 705 | "rollup-plugin-livereload": { 706 | "version": "1.3.0", 707 | "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-1.3.0.tgz", 708 | "integrity": "sha512-abyqXaB21+nFHo+vJULBqfzNx6zXABC19UyvqgDfdoxR/8pFAd041GO+GIUe8ZYC2DbuMUmioh1Lvbk14YLZgw==", 709 | "dev": true, 710 | "requires": { 711 | "livereload": "^0.9.1" 712 | } 713 | }, 714 | "rollup-plugin-node-resolve": { 715 | "version": "5.2.0", 716 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", 717 | "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", 718 | "dev": true, 719 | "requires": { 720 | "@types/resolve": "0.0.8", 721 | "builtin-modules": "^3.1.0", 722 | "is-module": "^1.0.0", 723 | "resolve": "^1.11.1", 724 | "rollup-pluginutils": "^2.8.1" 725 | } 726 | }, 727 | "rollup-plugin-replace": { 728 | "version": "2.2.0", 729 | "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", 730 | "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", 731 | "dev": true, 732 | "requires": { 733 | "magic-string": "^0.25.2", 734 | "rollup-pluginutils": "^2.6.0" 735 | } 736 | }, 737 | "rollup-plugin-serve": { 738 | "version": "1.1.0", 739 | "resolved": "https://registry.npmjs.org/rollup-plugin-serve/-/rollup-plugin-serve-1.1.0.tgz", 740 | "integrity": "sha512-pYkSsuA0/psKqhhictkJw1c2klya5b+LlCvipWqI9OE1aG2M97mRumZCbBlry5CMEOzYBBgSDgd1694sNbmyIw==", 741 | "dev": true, 742 | "requires": { 743 | "mime": ">=2.4.6", 744 | "opener": "1" 745 | } 746 | }, 747 | "rollup-plugin-uglify": { 748 | "version": "6.0.4", 749 | "resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.4.tgz", 750 | "integrity": "sha512-ddgqkH02klveu34TF0JqygPwZnsbhHVI6t8+hGTcYHngPkQb5MIHI0XiztXIN/d6V9j+efwHAqEL7LspSxQXGw==", 751 | "dev": true, 752 | "requires": { 753 | "@babel/code-frame": "^7.0.0", 754 | "jest-worker": "^24.0.0", 755 | "serialize-javascript": "^2.1.2", 756 | "uglify-js": "^3.4.9" 757 | } 758 | }, 759 | "rollup-pluginutils": { 760 | "version": "2.8.2", 761 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 762 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 763 | "dev": true, 764 | "requires": { 765 | "estree-walker": "^0.6.1" 766 | } 767 | }, 768 | "serialize-javascript": { 769 | "version": "2.1.2", 770 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", 771 | "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", 772 | "dev": true 773 | }, 774 | "slash": { 775 | "version": "3.0.0", 776 | "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", 777 | "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", 778 | "dev": true 779 | }, 780 | "source-map": { 781 | "version": "0.6.1", 782 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 783 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 784 | "dev": true 785 | }, 786 | "source-map-support": { 787 | "version": "0.5.19", 788 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 789 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 790 | "dev": true, 791 | "requires": { 792 | "buffer-from": "^1.0.0", 793 | "source-map": "^0.6.0" 794 | } 795 | }, 796 | "sourcemap-codec": { 797 | "version": "1.4.8", 798 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 799 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 800 | "dev": true 801 | }, 802 | "string-width": { 803 | "version": "4.2.2", 804 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", 805 | "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", 806 | "dev": true, 807 | "requires": { 808 | "emoji-regex": "^8.0.0", 809 | "is-fullwidth-code-point": "^3.0.0", 810 | "strip-ansi": "^6.0.0" 811 | }, 812 | "dependencies": { 813 | "ansi-regex": { 814 | "version": "5.0.0", 815 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 816 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 817 | "dev": true 818 | }, 819 | "emoji-regex": { 820 | "version": "8.0.0", 821 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 822 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 823 | "dev": true 824 | }, 825 | "is-fullwidth-code-point": { 826 | "version": "3.0.0", 827 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 828 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 829 | "dev": true 830 | }, 831 | "strip-ansi": { 832 | "version": "6.0.0", 833 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 834 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 835 | "dev": true, 836 | "requires": { 837 | "ansi-regex": "^5.0.0" 838 | } 839 | } 840 | } 841 | }, 842 | "strip-ansi": { 843 | "version": "5.2.0", 844 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 845 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 846 | "dev": true, 847 | "requires": { 848 | "ansi-regex": "^4.1.0" 849 | } 850 | }, 851 | "supports-color": { 852 | "version": "5.5.0", 853 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 854 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 855 | "dev": true, 856 | "requires": { 857 | "has-flag": "^3.0.0" 858 | } 859 | }, 860 | "term-size": { 861 | "version": "2.2.1", 862 | "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", 863 | "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", 864 | "dev": true 865 | }, 866 | "terser": { 867 | "version": "4.8.0", 868 | "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", 869 | "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", 870 | "dev": true, 871 | "requires": { 872 | "commander": "^2.20.0", 873 | "source-map": "~0.6.1", 874 | "source-map-support": "~0.5.12" 875 | } 876 | }, 877 | "to-regex-range": { 878 | "version": "5.0.1", 879 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 880 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 881 | "dev": true, 882 | "requires": { 883 | "is-number": "^7.0.0" 884 | } 885 | }, 886 | "type-fest": { 887 | "version": "0.8.1", 888 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 889 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", 890 | "dev": true 891 | }, 892 | "typr.js": { 893 | "version": "1.0.0", 894 | "resolved": "https://registry.npmjs.org/typr.js/-/typr.js-1.0.0.tgz", 895 | "integrity": "sha512-1fcCBnRerF4hLBrNVZyf6OwpWummpKomQ7N7OuWf3qzi4+XNSxtOnn3zvQlSwpV5SW/b0umULr5hQwOUNkZZUg==" 896 | }, 897 | "uglify-js": { 898 | "version": "3.13.3", 899 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.3.tgz", 900 | "integrity": "sha512-otIc7O9LyxpUcQoXzj2hL4LPWKklO6LJWoJUzNa8A17Xgi4fOeDC8FBDOLHnC/Slo1CQgsZMcM6as0M76BZaig==", 901 | "dev": true 902 | }, 903 | "unicode-canonical-property-names-ecmascript": { 904 | "version": "1.0.4", 905 | "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", 906 | "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", 907 | "dev": true 908 | }, 909 | "unicode-match-property-ecmascript": { 910 | "version": "1.0.4", 911 | "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", 912 | "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", 913 | "dev": true, 914 | "requires": { 915 | "unicode-canonical-property-names-ecmascript": "^1.0.4", 916 | "unicode-property-aliases-ecmascript": "^1.0.4" 917 | } 918 | }, 919 | "unicode-match-property-value-ecmascript": { 920 | "version": "1.2.0", 921 | "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", 922 | "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", 923 | "dev": true 924 | }, 925 | "unicode-property-aliases-ecmascript": { 926 | "version": "1.1.0", 927 | "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", 928 | "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", 929 | "dev": true 930 | }, 931 | "widest-line": { 932 | "version": "3.1.0", 933 | "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", 934 | "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", 935 | "dev": true, 936 | "requires": { 937 | "string-width": "^4.0.0" 938 | } 939 | }, 940 | "ws": { 941 | "version": "7.4.6", 942 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", 943 | "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", 944 | "dev": true 945 | } 946 | } 947 | } 948 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zfont", 3 | "version": "1.2.8", 4 | "description": "Text plugin for Zdog", 5 | "module": "dist/zfont.es.js", 6 | "main": "dist/zfont.js", 7 | "files": [ 8 | "dist/zfont.min.js", 9 | "dist/zfont.js", 10 | "dist/zfont.es.js" 11 | ], 12 | "scripts": { 13 | "start": "rollup -c --watch --environment DEV_SERVER,BUILD:development", 14 | "dev": "rollup -c --environment BUILD:development", 15 | "build": "npm run dev && npm run build:es && npm run build:min", 16 | "build:min": "rollup -c --environment BUILD:production", 17 | "build:es": "rollup -c --environment ES_MODULE,BUILD:production" 18 | }, 19 | "dependencies": { 20 | "typr.js": "^1.0.0" 21 | }, 22 | "devDependencies": { 23 | "rollup": "^1.13.0", 24 | "rollup-plugin-alias": "^1.5.1", 25 | "rollup-plugin-buble": "^0.19.6", 26 | "rollup-plugin-commonjs": "^10.0.0", 27 | "rollup-plugin-filesize": "^6.1.0", 28 | "rollup-plugin-livereload": "^1.0.4", 29 | "rollup-plugin-node-resolve": "^5.0.1", 30 | "rollup-plugin-replace": "^2.2.0", 31 | "rollup-plugin-serve": "^1.0.1", 32 | "rollup-plugin-uglify": "^6.0.2" 33 | }, 34 | "keywords": [ 35 | "zdog", 36 | "3d", 37 | "font", 38 | "text", 39 | "truetype", 40 | "ttf" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/jaames/zfont.git" 45 | }, 46 | "author": "James Daniel ", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/jaames/zfont/issues" 50 | }, 51 | "homepage": "https://github.com/jaames/zfont" 52 | } 53 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { version, description } from './package.json'; 2 | import filesize from 'rollup-plugin-filesize'; 3 | import buble from 'rollup-plugin-buble'; 4 | import alias from 'rollup-plugin-alias'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | import nodeResolve from 'rollup-plugin-node-resolve'; 7 | import replace from 'rollup-plugin-replace'; 8 | import { uglify } from 'rollup-plugin-uglify'; 9 | import serve from 'rollup-plugin-serve'; 10 | import livereload from 'rollup-plugin-livereload'; 11 | 12 | const build = process.env.BUILD || "development"; 13 | const devserver = process.env.DEV_SERVER || false; 14 | const esmodule = process.env.ES_MODULE || false; 15 | const prod = build === "production"; 16 | 17 | const banner = `/*! 18 | * Zfont v${version} 19 | * ${description} 20 | * 2019 James Daniel 21 | * MIT Licensed 22 | * github.com/jaames/zfont 23 | */ 24 | ` 25 | 26 | module.exports = { 27 | input: 'src/index.js', 28 | output: [ 29 | esmodule ? { 30 | file: 'dist/zfont.es.js', 31 | format: 'es', 32 | name: 'Zfont', 33 | banner: banner, 34 | sourcemap: true, 35 | sourcemapFile: 'dist/zfont.es.map' 36 | } : { 37 | file: prod ? 'dist/zfont.min.js' : 'dist/zfont.js', 38 | format: 'umd', 39 | name: 'Zfont', 40 | banner: banner, 41 | sourcemap: true, 42 | sourcemapFile: prod ? 'dist/zfont.min.js.map' : 'dist/zfont.js.map' 43 | } 44 | ].filter(Boolean), 45 | plugins: [ 46 | alias({ 47 | resolve: ['.jsx', '.js'] 48 | }), 49 | replace({ 50 | VERSION: JSON.stringify(version), 51 | PROD: prod ? 'true' : 'false', 52 | DEV_SERVER: devserver ? 'true' : 'false' 53 | }), 54 | buble({ 55 | jsx: 'h', 56 | objectAssign: 'Object.assign', 57 | transforms: { 58 | } 59 | }), 60 | nodeResolve(), 61 | commonjs(), 62 | !prod && devserver ? serve({ 63 | contentBase: ['dist', 'demo'] 64 | }) : false, 65 | !prod && devserver ? livereload() : false, 66 | // show filesize stats when building dist files 67 | !devserver ? filesize() : false, 68 | // only minify if we're producing a non-es production build 69 | prod && !esmodule ? uglify({ 70 | output: { 71 | comments: function(node, comment) { 72 | if (comment.type === 'comment2') { 73 | // preserve banner comment 74 | return /\!/i.test(comment.value); 75 | } 76 | return false; 77 | } 78 | } 79 | }) : false, 80 | ].filter(Boolean) 81 | }; -------------------------------------------------------------------------------- /src/ZdogFont.js: -------------------------------------------------------------------------------- 1 | import Typr from 'typr.js'; 2 | 3 | const TEXT_NEWLINE_REGEXP = /\r?\n/; 4 | 5 | export function registerFontClass(Zdog) { 6 | // Zdog.Font class 7 | class ZdogFont { 8 | constructor(props) { 9 | // Set missing props to default values 10 | props = Zdog.extend({ 11 | src: '', 12 | }, props); 13 | this.src = props.src; 14 | this.font = null; 15 | this._hasLoaded = false; 16 | this._loadCallbacks = []; 17 | // Add this font instance to the internal font list 18 | Zdog.FontList.push(this); 19 | // Begin loading font file 20 | this._fetchFontResource(this.src) 21 | .then(buffer => { 22 | const font = Typr.parse(buffer); 23 | // check font fields to see if the font was parsed correctly 24 | if ((!font.head) || (!font.hmtx) || (!font.hhea) || (!font.glyf)) { 25 | // get a list of missing font fields (only checks for ones that zfont uses) 26 | const missingFields = ['head', 'hmtx', 'hhea', 'glyf'].filter(field => !font[field]); 27 | throw new Error(`Typr.js could not parse this font (unable to find ${ missingFields.join(', ') })`); 28 | } 29 | return font; 30 | }) 31 | .then(font => { 32 | this.font = font; 33 | this._hasLoaded = true; 34 | this._loadCallbacks.forEach(callback => callback()); 35 | }) 36 | .catch(err => { 37 | throw new Error(`Unable to load font from ${this.src}:\n${err}`); 38 | }) 39 | } 40 | 41 | waitForLoad() { 42 | return new Promise((resolve, reject) => { 43 | // If the font is loaded, we can resolve right away 44 | if (this._hasLoaded && this._hasLoaded) { 45 | resolve(); 46 | } 47 | // Otherwise, wait for it to load 48 | else { 49 | this._loadCallbacks.push(resolve); 50 | } 51 | }); 52 | } 53 | 54 | getFontScale(fontSize) { 55 | if (!this._hasLoaded) { 56 | return null; 57 | } else { 58 | return 1 / this.font.head.unitsPerEm * fontSize; 59 | } 60 | } 61 | 62 | measureText(text, fontSize=64) { 63 | if (!this._hasLoaded) { 64 | return null; 65 | } 66 | const lines = Array.isArray(text) ? text : text.split(TEXT_NEWLINE_REGEXP); 67 | const font = this.font; 68 | const advanceWidthTable = font.hmtx.aWidth; 69 | const fontScale = this.getFontScale(fontSize); 70 | const descender = font.hhea.descender; 71 | const ascender = font.hhea.ascender; 72 | const lineGap = font.hhea.lineGap; 73 | const lineWidths = lines.map(line => { 74 | const glyphs = Typr.U.stringToGlyphs(this.font, line); 75 | return glyphs.reduce((advanceWidth, glyphId) => { 76 | // stringToGlyphs returns an array on glyph IDs that is the same length as the text string 77 | // an ID can sometimes be -1 in cases where multiple characters are merged into a single ligature 78 | if (glyphId > -1 && glyphId < advanceWidthTable.length) { 79 | advanceWidth += advanceWidthTable[glyphId]; 80 | } 81 | return advanceWidth; 82 | }, 0); 83 | }); 84 | const width = Math.max(...lineWidths); 85 | const lineHeight = (0 - descender) + ascender; 86 | const height = lineHeight * lines.length; 87 | 88 | // Multiply by fontScale to convert from font units to pixels 89 | return { 90 | width: width * fontScale, 91 | height: height * fontScale, 92 | lineHeight: lineHeight * fontScale, 93 | lineWidths: lineWidths.map(width => width * fontScale), 94 | descender: descender * fontScale, 95 | ascender: ascender * fontScale, 96 | }; 97 | } 98 | 99 | getTextPath(text, fontSize=64, x=0, y=0, z=0, alignX='left', alignY='bottom') { 100 | if (!this._hasLoaded) { 101 | return []; 102 | } 103 | const lines = Array.isArray(text) ? text : text.split(TEXT_NEWLINE_REGEXP); 104 | const measurements = this.measureText(text, fontSize); 105 | const lineWidths = measurements.lineWidths; 106 | const lineHeight = measurements.lineHeight; 107 | return lines.map((line, lineIndex) => { 108 | const [_x, _y, _z] = this.getTextOrigin({ 109 | ...measurements, 110 | width: lineWidths[lineIndex], 111 | }, x, y, z, alignX, alignY); 112 | y += lineHeight; 113 | const glyphs = Typr.U.stringToGlyphs(this.font, line); 114 | const path = Typr.U.glyphsToPath(this.font, glyphs); 115 | return this._convertPathCommands(path, fontSize, _x, _y, z); 116 | }).flat(); 117 | } 118 | 119 | getTextGlyphs(text, fontSize=64, x=0, y=0, z=0, alignX='left', alignY='bottom') { 120 | if (!this._hasLoaded) { 121 | return []; 122 | } 123 | const measurements = this.measureText(text, fontSize); 124 | const advanceWidthTable = this.font.hmtx.aWidth; 125 | const fontScale = this.getFontScale(fontSize); 126 | const lineWidths = measurements.lineWidths; 127 | const lineHeight = measurements.lineHeight; 128 | const lines = Array.isArray(text) ? text : text.split(TEXT_NEWLINE_REGEXP); 129 | return lines.map((line, lineIndex) => { 130 | const glyphs = Typr.U.stringToGlyphs(this.font, line); 131 | let [_x, _y, _z] = this.getTextOrigin({ 132 | ...measurements, 133 | width: lineWidths[lineIndex] 134 | }, x, y, z, alignX, alignY); 135 | y += lineHeight; 136 | return glyphs.filter(glyph => glyph !== -1).map(glyphId => { 137 | const path = Typr.U.glyphToPath(this.font, glyphId); 138 | const shape = { 139 | translate: {x:_x, y: _y, z:_z}, 140 | path: this._convertPathCommands(path, fontSize, 0, 0, 0) 141 | }; 142 | _x += advanceWidthTable[glyphId] * fontScale; 143 | return shape; 144 | }); 145 | }).flat(); 146 | } 147 | 148 | getTextOrigin(measuement, x=0, y=0, z=0, alignX='left', alignY='bottom') { 149 | let { width, height, lineHeight } = measuement; 150 | switch (alignX) { 151 | case 'right': 152 | x -= width; 153 | break; 154 | case 'center': 155 | x -= width / 2; 156 | break; 157 | default: 158 | break; 159 | } 160 | switch (alignY) { 161 | case 'middle': 162 | y -= (height / 2) - lineHeight; 163 | break; 164 | case 'bottom': 165 | default: 166 | y -= height - lineHeight; 167 | break; 168 | } 169 | return [x, y, z]; 170 | } 171 | 172 | // Convert Typr.js path commands to Zdog commands 173 | // Also apply font size scaling and coordinate adjustment 174 | // https://github.com/photopea/Typr.js 175 | // https://zzz.dog/shapes#shape-path-commands 176 | _convertPathCommands(path, fontSize, x=0, y=0, z=0) { 177 | const yDir = -1; 178 | const xDir = 1; 179 | const fontScale = this.getFontScale(fontSize); 180 | const commands = path.cmds; 181 | // Apply font scale to all coords 182 | const coords = path.crds.map(coord => coord * fontScale); 183 | // Convert coords to Zdog commands 184 | let startCoord = null; 185 | let coordOffset = 0; 186 | return commands.map((cmd) => { 187 | let result = null; 188 | if (!startCoord) { 189 | startCoord = {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z}; 190 | } 191 | switch (cmd) { 192 | case 'M': // moveTo command 193 | result = { 194 | move: {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z} 195 | }; 196 | coordOffset += 2; 197 | return result; 198 | case 'L': // lineTo command 199 | result = { 200 | line: {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z} 201 | }; 202 | coordOffset += 2; 203 | return result; 204 | case 'C': // curveTo command 205 | result = { 206 | bezier: [ 207 | {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z}, 208 | {x: x + coords[coordOffset + 2] * xDir, y: y + coords[coordOffset + 3] * yDir, z}, 209 | {x: x + coords[coordOffset + 4] * xDir, y: y + coords[coordOffset + 5] * yDir, z}, 210 | ] 211 | }; 212 | coordOffset += 6; 213 | return result; 214 | case 'Q': // arcTo command 215 | result = { 216 | arc: [ 217 | {x: x + coords[coordOffset] * xDir, y: y + coords[coordOffset + 1] * yDir, z}, 218 | {x: x + coords[coordOffset + 2] * xDir, y: y + coords[coordOffset + 3] * yDir, z}, 219 | ] 220 | }; 221 | coordOffset += 4; 222 | return result; 223 | case 'Z': // close path 224 | if (startCoord) { 225 | result = { 226 | line: startCoord 227 | }; 228 | startCoord = null; 229 | } 230 | return result; 231 | // unhandled type 232 | // currently, #rrggbb and X types (used in multicolor fonts) aren't supported 233 | default: 234 | return result; 235 | } 236 | }).filter(cmd => cmd !== null); // filter out null commands 237 | } 238 | 239 | _fetchFontResource(source) { 240 | return new Promise((resolve, reject) => { 241 | const request = new XMLHttpRequest(); 242 | // Fetch as an arrayBuffer for Typr.parse 243 | request.responseType = 'arraybuffer'; 244 | request.open('GET', source, true); 245 | request.onreadystatechange = e => { 246 | if (request.readyState === 4) { 247 | if (request.status >= 200 && request.status < 300) { 248 | resolve(request.response); 249 | } else { 250 | reject(`HTTP error ${request.status}: ${request.statusText}`); 251 | } 252 | } 253 | }; 254 | request.send(null); 255 | }); 256 | } 257 | } 258 | 259 | Zdog.Font = ZdogFont; 260 | return Zdog; 261 | } -------------------------------------------------------------------------------- /src/ZdogText.js: -------------------------------------------------------------------------------- 1 | export function registerTextClass(Zdog) { 2 | 3 | // Zdog.Text class 4 | class ZdogText extends Zdog.Shape { 5 | constructor(props) { 6 | // Set missing props to default values 7 | props = Zdog.extend({ 8 | font: null, 9 | value: '', 10 | fontSize: 64, 11 | textAlign: 'left', 12 | textBaseline: 'bottom', 13 | }, props); 14 | // Split props 15 | const { 16 | font, 17 | value, 18 | fontSize, 19 | textAlign, 20 | textBaseline, 21 | ...shapeProps 22 | } = props; 23 | // Create shape object 24 | super({ 25 | ...shapeProps, 26 | closed: true, 27 | visible: false, // hide until font is loaded 28 | path: [{}] 29 | }); 30 | this._font = null; 31 | this._value = value; 32 | this._fontSize = fontSize; 33 | this._textAlign = textAlign; 34 | this._textBaseline = textBaseline; 35 | this.font = font; 36 | } 37 | 38 | updateText() { 39 | let path = this.font.getTextPath(this.value, this.fontSize, 0, 0, 0, this.textAlign, this.textBaseline); 40 | if (path.length == 0) { // zdog doesn't know what to do with empty path arrays 41 | this.path = [{}]; 42 | this.visible = false; 43 | } else { 44 | this.path = path; 45 | this.visible = true; 46 | } 47 | this.updatePath(); 48 | } 49 | 50 | set font(newFont) { 51 | this._font = newFont; 52 | this.font.waitForLoad().then(() => { 53 | this.updateText(); 54 | this.visible = true; 55 | // Find root Zdog.Illustration instance 56 | let root = this.addTo; 57 | while (root.addTo !== undefined) { 58 | root = root.addTo; 59 | } 60 | // Update render graph 61 | if (root && typeof root.updateRenderGraph === 'function') { 62 | root.updateRenderGraph(); 63 | } 64 | }); 65 | } 66 | 67 | get font() { 68 | return this._font; 69 | } 70 | 71 | set value(newValue) { 72 | this._value = newValue; 73 | this.updateText(); 74 | } 75 | 76 | get value() { 77 | return this._value; 78 | } 79 | 80 | set fontSize(newSize) { 81 | this._fontSize = newSize; 82 | this.updateText(); 83 | } 84 | 85 | get fontSize() { 86 | return this._fontSize; 87 | } 88 | 89 | set textAlign(newValue) { 90 | this._textAlign = newValue; 91 | this.updateText(); 92 | } 93 | 94 | get textAlign() { 95 | return this._textAlign; 96 | } 97 | 98 | set textBaseline(newValue) { 99 | this._textBaseline = newValue; 100 | this.updateText(); 101 | } 102 | 103 | get textBaseline() { 104 | return this._textBaseline; 105 | } 106 | } 107 | 108 | ZdogText.optionKeys = ZdogText.optionKeys.concat(['font', 'fontSize', 'value', 'textAlign', 'textBaseline']); 109 | Zdog.Text = ZdogText; 110 | return Zdog; 111 | } -------------------------------------------------------------------------------- /src/ZdogTextGroup.js: -------------------------------------------------------------------------------- 1 | export function registerTextGroupClass(Zdog) { 2 | 3 | // Zdog.TextGroup class 4 | class ZdogTextGroup extends Zdog.Group { 5 | 6 | constructor(props) { 7 | // Set missing props to default values 8 | props = Zdog.extend({ 9 | font: null, 10 | value: '', 11 | fontSize: 64, 12 | textAlign: 'left', 13 | textBaseline: 'bottom', 14 | color: '#333', 15 | fill: false, 16 | stroke: 1, 17 | }, props); 18 | // Split props 19 | const { 20 | font, 21 | value, 22 | fontSize, 23 | textAlign, 24 | textBaseline, 25 | color, 26 | fill, 27 | stroke, 28 | ...groupProps 29 | } = props; 30 | // Create group object 31 | super({ 32 | ...groupProps, 33 | visible: false, // hide until font is loaded 34 | }); 35 | this._font = null; 36 | this._value = value; 37 | this._fontSize = fontSize; 38 | this._textAlign = textAlign; 39 | this._textBaseline = textBaseline; 40 | this._color = color; 41 | this._fill = fill; 42 | this._stroke = stroke; 43 | this.font = font; 44 | } 45 | 46 | updateText() { 47 | // Remove old children 48 | while (this.children.length > 0) { 49 | this.removeChild(this.children[0]); 50 | } 51 | // Get text paths for each glyph 52 | const glyphs = this.font.getTextGlyphs(this.value, this.fontSize, 0, 0, 0, this.textAlign, this.textBaseline); 53 | // Convert glyphs to new shapes 54 | glyphs.filter(shape => shape.path.length > 0).forEach(shape => { 55 | this.addChild(new Zdog.Shape({ 56 | translate: shape.translate, 57 | path: shape.path, 58 | color: this.color, 59 | fill: this.fill, 60 | stroke: this.stroke, 61 | closed: true, 62 | })); 63 | }); 64 | this.updateFlatGraph(); 65 | } 66 | 67 | set font(newFont) { 68 | this._font = newFont; 69 | this._font.waitForLoad().then(() => { 70 | this.updateText(); 71 | this.visible = true; 72 | // Find root Zdog.Illustration instance 73 | let root = this.addTo; 74 | while (root.addTo !== undefined) { 75 | root = root.addTo; 76 | } 77 | // Update render graph 78 | if (root && typeof root.updateRenderGraph === 'function') { 79 | root.updateRenderGraph(); 80 | } 81 | }); 82 | } 83 | 84 | get font() { 85 | return this._font; 86 | } 87 | 88 | set value(newValue) { 89 | this._value = newValue; 90 | this.updateText(); 91 | } 92 | 93 | get value() { 94 | return this._value; 95 | } 96 | 97 | set fontSize(newSize) { 98 | this._fontSize = newSize; 99 | this.updateText(); 100 | } 101 | 102 | get fontSize() { 103 | return this._fontSize; 104 | } 105 | 106 | set textAlign(newValue) { 107 | this._textAlign = newValue; 108 | this.updateText(); 109 | } 110 | 111 | get textAlign() { 112 | return this._textAlign; 113 | } 114 | 115 | set textBaseline(newValue) { 116 | this._textBaseline = newValue; 117 | this.updateText(); 118 | } 119 | 120 | get textBaseline() { 121 | return this._textBaseline; 122 | } 123 | 124 | set color(newColor) { 125 | this._color = newColor; 126 | this.children.forEach(child => child.color = newColor); 127 | } 128 | 129 | get color() { 130 | return this._color; 131 | } 132 | 133 | set fill(newFill) { 134 | this._fill = newFill; 135 | this.children.forEach(child => child.fill = newFill); 136 | } 137 | 138 | get fill() { 139 | return this._fill; 140 | } 141 | 142 | set stroke(newStroke) { 143 | this._stroke = newStroke; 144 | this.children.forEach(child => child.stroke = newStroke); 145 | } 146 | 147 | get stroke() { 148 | return this._stroke; 149 | } 150 | } 151 | 152 | ZdogTextGroup.optionKeys = ZdogTextGroup.optionKeys.concat(['color', 'fill', 'stroke', 'font', 'fontSize', 'value', 'textAlign', 'textBaseline']); 153 | Zdog.TextGroup = ZdogTextGroup; 154 | return Zdog; 155 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { registerFontClass } from './ZdogFont'; 2 | import { registerTextClass } from './ZdogText'; 3 | import { registerTextGroupClass } from './ZdogTextGroup'; 4 | 5 | export default { 6 | init(Zdog) { 7 | // Global font list to keep track of all fonts 8 | Zdog.FontList = []; 9 | 10 | // Helper to wait for all fonts to load 11 | Zdog.waitForFonts = function() { 12 | return Promise.all(Zdog.FontList.map(font => font.waitForLoad())); 13 | }; 14 | 15 | // Register Zfont classes onto the Zdog object 16 | registerFontClass(Zdog); 17 | registerTextClass(Zdog); 18 | registerTextGroupClass(Zdog); 19 | 20 | return Zdog; 21 | }, 22 | version: VERSION, 23 | } 24 | 25 | // add dev server flag to the window object 26 | // this will be stripped in production builds 27 | if (DEV_SERVER) { 28 | window.isDevServer = true; 29 | } --------------------------------------------------------------------------------