Favivon - Correção

This commit is contained in:
2026-05-30 19:59:39 -03:00
parent 76ddaa815d
commit d7dfd221f0
32859 changed files with 5459654 additions and 404 deletions
+649
View File
@@ -0,0 +1,649 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.52.4](https://github.com/protectwise/troika/compare/v0.52.3...v0.52.4) (2025-04-02)
### Bug Fixes
* error in Three r175 with custom(Depth|Distance)Material not having setters ([78e00b5](https://github.com/protectwise/troika/commit/78e00b512eb9618623d248070ba75ea0c55803ee)), closes [#357](https://github.com/protectwise/troika/issues/357)
## [0.52.3](https://github.com/protectwise/troika/compare/v0.52.2...v0.52.3) (2024-12-19)
### Bug Fixes
* **troika-three-text:** Fix error in Safari in BatchedText ([2260ba0](https://github.com/protectwise/troika/commit/2260ba0c92b582928402c57d721bfe8b3fa41855))
## [0.52.2](https://github.com/protectwise/troika/compare/v0.52.1...v0.52.2) (2024-11-21)
### Bug Fixes
* **BatchedText:** Fix removing text instances ([8bf4bca](https://github.com/protectwise/troika/commit/8bf4bca24e0aa05c681f183edfaaca7620e2eefd))
* **BatchedText:** Fix texture memory leak ([908fe31](https://github.com/protectwise/troika/commit/908fe314233db7201aa72710b9ec2697158fee79))
## [0.52.1](https://github.com/protectwise/troika/compare/v0.52.0...v0.52.1) (2024-11-21)
### Bug Fixes
* **BatchedText:** Fix removing text instances via .remove() ([d99549f](https://github.com/protectwise/troika/commit/d99549fa39ce05d04f1d3636e0ef0a1e954341ce))
# [0.52.0](https://github.com/protectwise/troika/compare/v0.51.1...v0.52.0) (2024-11-11)
### Features
* **BatchedText:** Support remaining visual properties ([60c5e93](https://github.com/protectwise/troika/commit/60c5e93ae5068f153240c5cfa1a109c01e4a4f51))
## [0.51.1](https://github.com/protectwise/troika/compare/v0.51.0...v0.51.1) (2024-11-11)
**Note:** Version bump only for package troika-three-text
# [0.51.0](https://github.com/protectwise/troika/compare/v0.50.3...v0.51.0) (2024-11-10)
### Bug Fixes
* **BatchedText:** Fix texel read for taller data texture ([57f3b0b](https://github.com/protectwise/troika/commit/57f3b0bfa6f13d7311d51e1cb4c3b0e1caf17458))
* **troika-three-text:** Fix error when first character is whitespace [#313](https://github.com/protectwise/troika/issues/313) ([f71679e](https://github.com/protectwise/troika/commit/f71679e97a3850057ff85054d10326c8100c5328))
### Features
* **BatchedText:** support fillOpacity, and color now sets the `diffuse` uniform ([8f83424](https://github.com/protectwise/troika/commit/8f8342447f3fe4a31420cf46fdb191ddc0afb929))
* **troika-three-text:** Add config to disable web worker ([86023ec](https://github.com/protectwise/troika/commit/86023ecd1ce00cebc3094088f935de5c129d5201)), closes [#337](https://github.com/protectwise/troika/issues/337)
## [0.50.3](https://github.com/protectwise/troika/compare/v0.50.2...v0.50.3) (2024-11-01)
### Bug Fixes
* ensure high precision sampling of batched text data texture ([f8bd307](https://github.com/protectwise/troika/commit/f8bd3076a08136ad8be29eb7ea0292bbf80ceab9))
## [0.50.2](https://github.com/protectwise/troika/compare/v0.50.1...v0.50.2) (2024-10-21)
### Bug Fixes
* **troika-three-text:** BatchedText: increase number of batch members ([449957d](https://github.com/protectwise/troika/commit/449957dfed3adbcb64cd71d181a82ce4ff8ed822))
## [0.50.1](https://github.com/protectwise/troika/compare/v0.50.0...v0.50.1) (2024-10-12)
### Bug Fixes
* **troika-three-text:** fix error when using BatchedText material color ([7f8c845](https://github.com/protectwise/troika/commit/7f8c845d02496c076bfcce7413bfd98a7c993d04))
* **troika-three-text:** remove DoubleSide double draw hack in favor of forceSinglePass flag ([7818d05](https://github.com/protectwise/troika/commit/7818d05745f5ca44530aae774df13deafb35588a))
# [0.50.0](https://github.com/protectwise/troika/compare/v0.49.1...v0.50.0) (2024-10-11)
### Features
* **troika-three-text:** allow overriding text material derivation ([9a48e0c](https://github.com/protectwise/troika/commit/9a48e0c5d0c3b632359fc56ced67b1466efb65cc))
* **troika-three-text:** ALPHA: BatchedText for batching many Text instances in a single draw call ([79c9c50](https://github.com/protectwise/troika/commit/79c9c50d0f8fec1e1d64e316d47fbb71fd164857))
## [0.49.1](https://github.com/protectwise/troika/compare/v0.49.0...v0.49.1) (2024-04-09)
### Bug Fixes
* **troika-three-text:** Fix anchorY bottom-baseline. Closes [#309](https://github.com/protectwise/troika/issues/309) ([3457b03](https://github.com/protectwise/troika/commit/3457b03af5220886b4fd0534a1d44bdc5f837851))
# [0.49.0](https://github.com/protectwise/troika/compare/v0.48.1...v0.49.0) (2023-10-08)
### Bug Fixes
* support opentype ccmp substitutions (glyph compositions) ([1e358b2](https://github.com/protectwise/troika/commit/1e358b23971914cd1a4d55e936dfe035dced57c1))
### Features
* add support for positioning of diacritic marks (e.g. Thai) ([301c34c](https://github.com/protectwise/troika/commit/301c34c3341b83eb95afc308bd796bcccb2f5ccd))
## [0.48.1](https://github.com/protectwise/troika/compare/v0.48.0...v0.48.1) (2023-10-02)
### Bug Fixes
* improve font resolution in CJK ([8d9ac64](https://github.com/protectwise/troika/commit/8d9ac641215c198cdaf1f55185814e061f8d0d6e))
* korean/japanese fonts now resolve correctly ([207a5e0](https://github.com/protectwise/troika/commit/207a5e03d8881ce26e71827b77baea6c2e39aa04))
# [0.48.0](https://github.com/protectwise/troika/compare/v0.47.2...v0.48.0) (2023-09-09)
### Features
* **troika-three-text:** add fallback font loading for full Unicode support ([#279](https://github.com/protectwise/troika/issues/279)) ([6fb8061](https://github.com/protectwise/troika/commit/6fb806145f79d7a36e79a35a006b4601535b3827)), closes [#13](https://github.com/protectwise/troika/issues/13) [#65](https://github.com/protectwise/troika/issues/65)
## [0.47.2](https://github.com/protectwise/troika/compare/v0.47.1...v0.47.2) (2023-05-15)
**Note:** Version bump only for package troika-three-text
## [0.47.1](https://github.com/protectwise/troika/compare/v0.47.0...v0.47.1) (2022-12-15)
### Bug Fixes
* **troika-three-text:** update Typr.ts to get kerning fix ([#228](https://github.com/protectwise/troika/issues/228)) ([a45db5d](https://github.com/protectwise/troika/commit/a45db5ddddcba35c9358c3014aff79fd80b30ae1))
# [0.47.0](https://github.com/protectwise/troika/compare/v0.46.3...v0.47.0) (2022-12-15)
### Bug Fixes
* **troika-three-text:** include lineGap value from the font in 'normal' line-height calculation ([c278118](https://github.com/protectwise/troika/commit/c2781182ad897a444f9502031a9626b703e85050))
* **troika-three-text:** remove IIFEs for tree-shaking ([#224](https://github.com/protectwise/troika/issues/224)) ([2e688f0](https://github.com/protectwise/troika/commit/2e688f0248296fee46e16f58a5de7b963a4cb73b))
* **troika-three-text:** update Typr to fix doubled kerning in some fonts ([#196](https://github.com/protectwise/troika/issues/196)) ([1fab9a2](https://github.com/protectwise/troika/commit/1fab9a2ce739a1f26c36b737a499075a439f8980))
* **troika-three-text:** use sTypoAscender/Descender from OS/2 font table if available ([f5c244c](https://github.com/protectwise/troika/commit/f5c244c92f9ede541b1206744aa1c7e1ff6711fc))
* **troika-three-text:** work around Safari<15 bug using SDF canvas as a texture ([#199](https://github.com/protectwise/troika/issues/199)) ([fca9aae](https://github.com/protectwise/troika/commit/fca9aae20e7b67cbd7ac3669dd91257ec84f1997))
### Features
* raise three min version to r125 and fix BufferGeometry references ([#225](https://github.com/protectwise/troika/issues/225)) ([f2ef803](https://github.com/protectwise/troika/commit/f2ef803db7ab3d9d03de2719a2781c1c3f5122cf))
* **troika-three-text:** add 'top-cap' and 'top-ex' as keywords for anchorY ([#193](https://github.com/protectwise/troika/issues/193)) ([c6a10ae](https://github.com/protectwise/troika/commit/c6a10ae30837d26505d5614b8e15ab49f2ad4625))
* **troika-three-text:** include capHeight and xHeight font metrics in textRenderInfo ([3459fd5](https://github.com/protectwise/troika/commit/3459fd540925d7aebad48c70c28e1e0a9b4195f0))
* **troika-three-text:** remove deprecated totalBounds/totalBlockSize properties from textRenderInfo ([2b87308](https://github.com/protectwise/troika/commit/2b87308f8fd02552fb86fdcc81a8e949de8e5989))
* remove custom Thenable polyfill in favor of native promises ([7af402e](https://github.com/protectwise/troika/commit/7af402e254675ca2fc182467a65d2d4f860845e4))
* **troika-three-text:** remove long-deprecated `anchor` array property ([752e302](https://github.com/protectwise/troika/commit/752e3025cec4176aadaab2a3ca475b3fd49d572e))
## [0.46.3](https://github.com/protectwise/troika/compare/v0.46.2...v0.46.3) (2022-03-11)
### Bug Fixes
* **troika-three-text:** update Typr to fix doubled kerning in some fonts ([#196](https://github.com/protectwise/troika/issues/196)) ([080119a](https://github.com/protectwise/troika/commit/080119a10797024f3bc82cb37f614c1c90027be6))
## [0.46.2](https://github.com/protectwise/troika/compare/v0.46.1...v0.46.2) (2022-03-06)
### Bug Fixes
* **troika-three-text:** fix SDF texture resizing in Three r136+ ([0fab679](https://github.com/protectwise/troika/commit/0fab679b0d547a67fff10cb0a84c17e856cf4349))
## [0.46.1](https://github.com/protectwise/troika/compare/v0.46.0...v0.46.1) (2022-03-05)
### Bug Fixes
* remove console.log ([3518689](https://github.com/protectwise/troika/commit/3518689f97f7d02236248cc547b703cdbc97a1e2))
# [0.46.0](https://github.com/protectwise/troika/compare/v0.45.0...v0.46.0) (2022-03-05)
### Features
* **troika-three-text:** add a `gpuAccelerateSDF` property for opting out of webgl sdf generation ([d436ffd](https://github.com/protectwise/troika/commit/d436ffd5063747e2e4d453240b702b174f91268d))
* **troika-three-text:** integrate webgl-sdf-generator for GPU-accelerated SDF generation ([b5c9138](https://github.com/protectwise/troika/commit/b5c913882daf480d18def77927e0dc70add082df))
# [0.45.0](https://github.com/protectwise/troika/compare/v0.44.0...v0.45.0) (2022-01-02)
### Features
* **troika-three-text:** SDFs for all fonts are now stored in the same texture ([7e871f7](https://github.com/protectwise/troika/commit/7e871f77c17dbbb68ca5c2240f569d4b102031f0))
### Performance Improvements
* **troika-three-text:** avoid extra draw call on double sided materials as of Three r130 ([6222ef3](https://github.com/protectwise/troika/commit/6222ef300ac364dcb0bb099a7469622e9287651e))
* **troika-three-text:** make the glyphColors buffer transferable ([c8c92fa](https://github.com/protectwise/troika/commit/c8c92faa70a8ad0fa08cadf38f8e4d4d2e933c10))
# [0.44.0](https://github.com/protectwise/troika/compare/v0.43.1-alpha.0...v0.44.0) (2021-11-14)
### Bug Fixes
* **troika-three-text:** fill in missing caret positions when the final glyph is a ligature - [#165](https://github.com/protectwise/troika/issues/165) ([ad2eda6](https://github.com/protectwise/troika/commit/ad2eda657a65fc01511daf52c1cfb7ccac2a03b0))
## [0.43.1-alpha.0](https://github.com/protectwise/troika/compare/v0.43.0...v0.43.1-alpha.0) (2021-10-24)
### Bug Fixes
* **troika-three-text:** fix font parsing failures in iOS Safari ([a542b42](https://github.com/protectwise/troika/commit/a542b42f0cc198fe2e9d46e4840fb8984279034b))
# [0.43.0](https://github.com/protectwise/troika/compare/v0.42.0...v0.43.0) (2021-09-20)
### Features
* **troika-three-text:** allow line wrapping after some common non-whitespace chars like hyphens ([1b20e34](https://github.com/protectwise/troika/commit/1b20e34e13fd6de7aa8257b8e50f0db671e4c964)), closes [#136](https://github.com/protectwise/troika/issues/136)
### Performance Improvements
* **troika-three-text:** parallelize SDF generation with multiple worker threads ([c2bf886](https://github.com/protectwise/troika/commit/c2bf886f280ca1c587bc3ae80a41d30ce8cb6dce))
# [0.42.0](https://github.com/protectwise/troika/compare/v0.41.2...v0.42.0) (2021-05-17)
### Bug Fixes
* add three to peerDependencies in all leaf packages ([0a11ab6](https://github.com/protectwise/troika/commit/0a11ab6ddff13b3ebd0f1f2463e0cfed17b3f5fa))
### Features
* open up 'three' peer dependency to include future versions ([d4a5b23](https://github.com/protectwise/troika/commit/d4a5b2376fffb3681750761f757b684ab798315a))
## [0.41.2](https://github.com/protectwise/troika/compare/v0.41.1...v0.41.2) (2021-05-05)
### Bug Fixes
* update bidi-js for its ES5 build files ([49ce5f2](https://github.com/protectwise/troika/commit/49ce5f2244e8a108f1c07fe32b9e8ce77f4afebb))
## [0.41.1](https://github.com/protectwise/troika/compare/v0.41.0...v0.41.1) (2021-04-26)
### Bug Fixes
* update bidi-js for fix removing type:module from package.json ([394c371](https://github.com/protectwise/troika/commit/394c37117042c28f6245aa3d1aa9a180ff8250bf))
# [0.41.0](https://github.com/protectwise/troika/compare/v0.40.0...v0.41.0) (2021-04-19)
### Bug Fixes
* **troika-three-text:** fix Arabic word position letter forms ([480ee97](https://github.com/protectwise/troika/commit/480ee97426c219195d80733b04092018f9bbca86))
* **troika-three-text:** formatting characters no longer produce visible glyphs ([c0d28e8](https://github.com/protectwise/troika/commit/c0d28e8c05a2482c06f67a6d6fe9ea45fff39cd4))
* **troika-three-text:** more correct impl for character joining types ([2ce519a](https://github.com/protectwise/troika/commit/2ce519aa9b8f502f3f4af5cdd5447456958d036b))
* **troika-three-text:** prevent mutation of input to worldPositionToTextCoords method ([d487b8a](https://github.com/protectwise/troika/commit/d487b8aeaf3ca6831192587ee4d4c1bee978f90f))
### Features
* **troika-three-text:** add full bidi text support ([3fde850](https://github.com/protectwise/troika/commit/3fde850d28524393538e2bac8920f7a4ee0e1fb4))
* **troika-three-text:** simple bidi layout support, using explicit LRO/RLO/PDF chars only ([d511655](https://github.com/protectwise/troika/commit/d511655926f53262abb4b6c990c5102180f23f64))
* **troika-three-text:** very basic support for right-to-left text layout ([ce887be](https://github.com/protectwise/troika/commit/ce887beed7976ec23fa0590d5199457182c6e6bf))
### Performance Improvements
* prune some unused functions out of the Typr build ([26e669f](https://github.com/protectwise/troika/commit/26e669f5382fd8a160d1f9814e4329620ae2879b))
# [0.40.0](https://github.com/protectwise/troika/compare/v0.39.2...v0.40.0) (2021-02-28)
### Bug Fixes
* **troika-three-text:** fix boundingBox, boundingSphere, and raycasting with curveRadius ([7cc7c82](https://github.com/protectwise/troika/commit/7cc7c821eca8f7ae63170d9a484e806bc8814a94)), closes [#103](https://github.com/protectwise/troika/issues/103)
## [0.39.2](https://github.com/protectwise/troika/compare/v0.39.1...v0.39.2) (2021-02-18)
### Bug Fixes
* **troika-three-text:** fix shader error in WebGL1 ([cdbc7dc](https://github.com/protectwise/troika/commit/cdbc7dc0cac980a0317219a4736cb48ae4bc18eb)), closes [#108](https://github.com/protectwise/troika/issues/108)
## [0.39.1](https://github.com/protectwise/troika/compare/v0.39.0...v0.39.1) (2021-02-17)
### Bug Fixes
* **troika-three-text:** selection rects no longer clip off trailing whitespace ([158305c](https://github.com/protectwise/troika/commit/158305c9f3f83aa3729b2d32c1ae2d9112540348)), closes [#78](https://github.com/protectwise/troika/issues/78)
# [0.39.0](https://github.com/protectwise/troika/compare/v0.38.1...v0.39.0) (2021-02-15)
### Features
* **troika-three-text:** add curveRadius for applying cylindrical curvature ([6fdfbbf](https://github.com/protectwise/troika/commit/6fdfbbfcc0cdae0143555c9cb6569ba9e70150c5))
* **troika-three-text:** export a function for debugging SDF textures ([3fb0c23](https://github.com/protectwise/troika/commit/3fb0c23bae22b3812839c0639f8278d68120fc8c))
* **troika-three-text:** pack SDFs using all 4 color channels, to increase max glyphs in a texture ([d236caf](https://github.com/protectwise/troika/commit/d236caf9526b5b05bb14980f54f3d73a207ed874))
## [0.38.1](https://github.com/protectwise/troika/compare/v0.38.0...v0.38.1) (2021-02-03)
### Bug Fixes
* **troika-three-text:** prevent copy() from sharing geometry between instances ([8c3ba2d](https://github.com/protectwise/troika/commit/8c3ba2d8f610c045dadee17a6221ea61ab8d26d4))
# [0.38.0](https://github.com/protectwise/troika/compare/v0.37.0...v0.38.0) (2021-01-24)
### Bug Fixes
* **troika-three-text:** allow negative percentages for outlineOffsetX/Y ([3a274f0](https://github.com/protectwise/troika/commit/3a274f070b30e5312e2f546f66db2ab9352962ca)), closes [#100](https://github.com/protectwise/troika/issues/100)
# [0.37.0](https://github.com/protectwise/troika/compare/v0.36.1...v0.37.0) (2021-01-18)
### Features
* **troika-three-text:** added inner stroke and outline blur capabilities ([e004b9d](https://github.com/protectwise/troika/commit/e004b9d2f7e2ef9e841e61156b68958076533a62))
### Performance Improvements
* **troika-three-text:** swap tiny-inflate to fflate for minor speed boost on woff fonts ([2ae29fa](https://github.com/protectwise/troika/commit/2ae29faffcec2302453ce9dabac633ade8181127))
## [0.36.1](https://github.com/protectwise/troika/compare/v0.36.0...v0.36.1) (2020-12-16)
### Bug Fixes
* **troika-three-text:** soften Typr.ts console warnings to debug level ([50d951f](https://github.com/protectwise/troika/commit/50d951fd06108194ef0a485af1fcce58c2710cde))
# [0.36.0](https://github.com/protectwise/troika/compare/v0.35.0...v0.36.0) (2020-12-04)
### Bug Fixes
* **troika-three-text:** fix wrong caret position for collapsed ligature characters ([f220035](https://github.com/protectwise/troika/commit/f220035430787b3d178ad8cfe4b067fe9793de97))
### Features
* **troika-three-text:** fix kerning by updating from Typr.js to Typr.ts ([43144cf](https://github.com/protectwise/troika/commit/43144cfbb8f553d552a5bef179a7e5cfc8179fe3)), closes [#70](https://github.com/protectwise/troika/issues/70)
# [0.35.0](https://github.com/protectwise/troika/compare/v0.34.2...v0.35.0) (2020-11-16)
**Note:** Version bump only for package troika-three-text
## [0.34.2](https://github.com/protectwise/troika/compare/v0.34.1...v0.34.2) (2020-11-09)
### Bug Fixes
* **troika-three-text:** dispose the outline material when the base material is disposed ([68bd2c8](https://github.com/protectwise/troika/commit/68bd2c867f9ccbb53a41b2a3c3aedcf886354d38))
* **troika-three-text:** fix error when disposing the base material with outlines enabled ([73a51f5](https://github.com/protectwise/troika/commit/73a51f5ef87676727667becc0e6bbc6495bff751))
## [0.34.1](https://github.com/protectwise/troika/compare/v0.34.0...v0.34.1) (2020-10-20)
**Note:** Version bump only for package troika-three-text
# [0.34.0](https://github.com/protectwise/troika/compare/v0.33.1...v0.34.0) (2020-10-19)
### Bug Fixes
* **troika-three-text:** clipRect is no longer clamped to the text block's bounds ([15edbd9](https://github.com/protectwise/troika/commit/15edbd95c0ec525c4a268ff3781e6e516981da02))
* **troika-three-text:** fix text baseline being positioned too low ([596d8ca](https://github.com/protectwise/troika/commit/596d8ca1e6ba35f9e68bcbda74329823a3b1b1ad))
### Features
* **troika-three-text:** expose blockBounds and visibleBounds in textRenderInfo ([f3340ec](https://github.com/protectwise/troika/commit/f3340ec1efac6a6b00f596d9ef898ed7c2a6568a))
* **troika-three-text:** text outline and better antialiasing at small sizes ([3836809](https://github.com/protectwise/troika/commit/3836809cc919b57b5eb357e66e35a15903bd54f7))
### Performance Improvements
* micro-optimization of sdf texture insertion loop ([995c2a6](https://github.com/protectwise/troika/commit/995c2a6652181f26677b8da4207f18c32455e59c))
## [0.33.1](https://github.com/protectwise/troika/compare/v0.33.0...v0.33.1) (2020-10-02)
**Note:** Version bump only for package troika-three-text
# [0.33.0](https://github.com/protectwise/troika/compare/v0.32.0...v0.33.0) (2020-10-02)
### Bug Fixes
* add "sideEffects":false to package.json files to assist treeshaking ([61109b2](https://github.com/protectwise/troika/commit/61109b2e3d21dc794ef66b3f28cf63bbdd34150e))
* add PURE annotations to make troika-three-text treeshakeable ([8e76b5c](https://github.com/protectwise/troika/commit/8e76b5c31a3cbda86595654ba9d66d8d635e44a1))
* remove redundant "browser" and defunct "jsnext:main" fields from package.json files ([0abec40](https://github.com/protectwise/troika/commit/0abec40e3af06d3ae4d990bf198d871b46730f1f))
* **troika-three-text:** make `color` prop only apply to that instance when sharing a base material ([da0f995](https://github.com/protectwise/troika/commit/da0f995be3b7594bafc6f24dd6981ee787ff4ee1))
### Features
* **troika-three-text:** modifications to the base material are now picked up automatically ([fc81d3a](https://github.com/protectwise/troika/commit/fc81d3a13ef84a8358bfbdcac066cb13a161c7f6))
# [0.32.0](https://github.com/protectwise/troika/compare/v0.31.0...v0.32.0) (2020-09-16)
### Bug Fixes
* mutate boundingBox and set depth to 0 ([1f9b6be](https://github.com/protectwise/troika/commit/1f9b6bef083c26c9de9ac0ce169544ed3f99cf89))
### Features
* added boundingBox calculation ([140e9e8](https://github.com/protectwise/troika/commit/140e9e8bf2865c54f21877ca03834bbde4e9ab52))
# [0.31.0](https://github.com/protectwise/troika/compare/v0.30.2...v0.31.0) (2020-08-11)
**Note:** Version bump only for package troika-three-text
## [0.30.2](https://github.com/protectwise/troika/compare/v0.30.1...v0.30.2) (2020-07-22)
### Bug Fixes
* **troika-three-text:** prevent unbound buffer errors when disposing a GlyphsGeometry ([e860eac](https://github.com/protectwise/troika/commit/e860eacd04404a328cc758af9103f5d2f55201ba)), closes [#69](https://github.com/protectwise/troika/issues/69) [react-spring/drei#62](https://github.com/react-spring/drei/issues/62)
## [0.30.1](https://github.com/protectwise/troika/compare/v0.30.0...v0.30.1) (2020-07-19)
### Bug Fixes
* **troika-three-text:** fix changing text length in ThreeJS r117+ ([a7ef945](https://github.com/protectwise/troika/commit/a7ef945119649b4c3b451783000dd5c40ad3f3ba)), closes [#69](https://github.com/protectwise/troika/issues/69)
# [0.30.0](https://github.com/protectwise/troika/compare/v0.29.0...v0.30.0) (2020-07-16)
### Features
* **troika-three-text:** add support for textIndent ([b689c0c](https://github.com/protectwise/troika/commit/b689c0c1b1d9de437eeea9390cfcf9be6c10eae9))
# [0.29.0](https://github.com/protectwise/troika/compare/v0.28.1...v0.29.0) (2020-07-06)
### Features
* **troika-three-text:** promote standalone text to a new `troika-three-text` package ([995f2eb](https://github.com/protectwise/troika/commit/995f2eb7202789a83671878209c65d240082ade7)), closes [#47](https://github.com/protectwise/troika/issues/47)
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+439
View File
@@ -0,0 +1,439 @@
# Troika Text for Three.js
The `troika-three-text` package provides high quality text rendering in [Three.js](https://threejs.org) scenes, using signed distance fields (SDF) and antialiasing using standard derivatives.
Rather than relying on pre-generated SDF textures, this parses font files (.ttf, .otf, .woff) directly using [Typr](https://github.com/fredli74/Typr.ts), and generates the SDF atlas for glyphs on-the-fly as they are used. It also handles proper kerning, ligature glyph substitution, right-to-left/bidirectional layout, joined scripts like Arabic, and will automatically load fallback fonts for full unicode coverage. All font parsing, SDF generation, and glyph layout is performed in a web worker to prevent frame drops.
Once the SDFs are generated, it assembles a geometry that positions all the glyphs, and _patches_ any Three.js Material with the proper shader code for rendering the SDFs. This means you can still benefit from all the features of Three.js's built-in materials like lighting, physically-based rendering, shadows, and fog.
## Demos
* [With the Troika scene management framework](https://troika-examples.netlify.com/#text)
* [With react-three-fiber](https://codesandbox.io/embed/troika-3d-text-via-react-three-fiber-ntfx2?fontsize=14)
* [With a video texture](https://bfplr.csb.app/)
* [With the Material Icons font](https://codesandbox.io/s/material-icons-in-troika-three-text-t2mu7?file=/src/index.js)
* [With in-place editing](https://stackblitz.com/edit/vitejs-vite-xq3mug?file=src%2Fmain.js)
## With Other Frameworks
* [In the `drei` utilities for react-three-fiber](https://github.com/pmndrs/drei#text)
* [As an A-Frame component](https://github.com/lojjic/aframe-troika-text)
* [As a Web Component in three-elements](https://www.npmjs.com/package/@three-elements/text)
## Screenshots
![Text Rendering](../../docs/troika-three-text/images/screenshot1.png)
![Zoomed-in](../../docs/troika-three-text/images/screenshot2.png)
![Font with ligatures](../../docs/troika-three-text/images/screenshot3.png)
![Text with a texture](../../docs/troika-three-text/images/screenshot4.png)
![Unicode coverage](../../docs/troika-three-text/images/screenshot5.png)
## Installation
Get it from [NPM](https://www.npmjs.com/package/troika-three-text):
```sh
npm install troika-three-text
```
You will also need to install a compatible version of [Three.js](https://threejs.org); see the [notes on Three.js versions in the Getting Started docs](../../docs/getting-started/setup.md#threejs) for details.
```sh
npm install three
```
## Usage
```js
import {Text} from 'troika-three-text'
```
You can then use the `Text` class like any other Three.js mesh:
```js
// Create:
const myText = new Text()
myScene.add(myText)
// Set properties to configure:
myText.text = 'Hello world!'
myText.fontSize = 0.2
myText.position.z = -2
myText.color = 0x9966FF
// Update the rendering:
myText.sync()
```
It's a good idea to call the `.sync()` method after changing any properties that would affect the text's layout. If you don't, it will be called automatically on the next render frame, but calling it yourself can get the result sooner.
When you're done with the `Text` instance, be sure to call `dispose` on it to prevent a memory leak:
```js
myScene.remove(myText)
myText.dispose()
```
## Supported properties
Instances of `Text` support the following configuration properties:
### `text`
The string of text to be rendered. Newlines and repeating whitespace characters are honored.
Default: _none_
### `anchorX`
Defines the horizontal position in the text block that should line up with the local origin. Can be specified as a numeric `x` position in local units, a string percentage of the total text block width e.g. `'25%'`, or one of the following keyword strings: `'left'`, `'center'`, or `'right'`.
Default: `0`
### `anchorY`
Defines the vertical position in the text block that should line up with the local origin. Can be specified as a numeric `y` position in local units (note: down is negative y), a string percentage of the total text block height e.g. `'25%'`, or one of the following keyword strings: `'top'`, `'top-baseline'`, `'top-cap'`, `'top-ex'`, `'middle'`, `'bottom-baseline'`, or `'bottom'`.
Default: `0`
### `clipRect`
If specified, defines the `[minX, minY, maxX, maxY]` of a rectangle outside of which all pixels will be discarded. This can be used for example to clip overflowing text when `whiteSpace='nowrap'`.
Default: _none_
### `color`
This is a shortcut for setting the `color` of the text's `material`. You can use this if you don't want to specify a whole custom `material` and just want to change its color.
Use the `material` property if you want to control aspects of the material other than its color.
Default: _none_ - uses the color of the `material`
### `curveRadius`
Defines a cylindrical radius along which the text's plane will be curved. Positive numbers put the cylinder's centerline (oriented vertically) that distance in front of the text, for a concave curvature, while negative numbers put it behind the text for a convex curvature. The centerline will be aligned with the text's local origin; you can use `anchorX` to offset it.
Since each glyph is by default rendered with a simple quad, each glyph remains a flat plane internally. You can use [`glyphGeometryDetail`](#glyphgeometrydetail) to add more vertices for curvature inside glyphs.
Default: `0`
### `depthOffset`
This is a shortcut for setting the material's [`polygonOffset` and related properties](https://threejs.org/docs/#api/en/materials/Material.polygonOffset), which can be useful in preventing z-fighting when this text is laid on top of another plane in the scene. Positive numbers are further from the camera, negatives closer.
Be aware that while this can help with z-fighting, it does not affect the rendering order; if the text renders before the content behind it, you may see antialiasing pixels that appear too dark or light. You may need to also change the text mesh's `renderOrder`, or set its `z` position a fraction closer to the camera, to ensure the text renders after background objects.
Default: `0`
### `direction`
Sets the base direction for the text. The default value of "auto" will choose a direction based on the text's content according to the bidi spec. A value of "ltr" or "rtl" will force the direction.
Default: `'auto'`
### `fillOpacity`
Controls the opacity of just the glyph's fill area, separate from any configured `strokeOpacity`, `outlineOpacity`, and the material's `opacity`. A `fillOpacity` of `0` will make the fill invisible, leaving just the stroke and/or outline.
Default: `1`
### `font`
The URL of a custom font file to be used. Supported font formats are:
* .ttf
* .otf
* .woff (.woff2 is _not_ supported)
Default: The *Roboto* font loaded from Google Fonts CDN
### `fontSize`
The em-height at which to render the font, in local world units.
Default: `0.1`
### `fontStyle`
Either `"italic"` or `"normal"`. Currently only used to select the preferred style for the [fallback Unicode fonts](#unicode-fallback-fonts).
Default: `"normal"`
### `fontWeight`
A numeric font weight, `"normal"`, or `"bold"`. Currently only used to select the preferred weight for the [fallback Unicode fonts](#unicode-fallback-fonts).
Default: `"normal"`
### `glyphGeometryDetail`
The number of vertical/horizontal segments that make up each glyph's rectangular plane. This can be increased to provide more geometrical detail for custom vertex shader effects, for example.
Default: `1`
### `gpuAccelerateSDF`
When `true`, the SDF generation process will be GPU-accelerated with WebGL when possible, making it much faster especially for complex glyphs, and falling back to a JavaScript version executed in web workers when support isn't available. It should automatically detect support, but it's still somewhat experimental, so you can set it to `false` to force it to use the JS version if you encounter issues with it.
Default: `true`
### `letterSpacing`
Sets a uniform adjustment to spacing between letters after kerning is applied, in local world units. Positive numbers increase spacing and negative numbers decrease it.
Default: `0`
### `lineHeight`
Sets the height of each line of text. Can either be `'normal'` which chooses a reasonable height based on the chosen font's ascender/descender metrics, or a number that is interpreted as a multiple of the `fontSize`.
Default: `'normal'`
### `material`
Defines a Three.js Material _instance_ to be used as a base when rendering the text. This material will be automatically replaced with a new material derived from it, that adds shader code to decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing.
By default it will derive from a simple white `MeshBasicMaterial, but you can use any of the other mesh materials to gain other features like lighting, texture maps, etc.
Also see the `color` shortcut property.
Note that because your material instance is _replaced_ by a derived material instance, any changes you make to your original material will _not_ be reflected in the derived version. If you need to modify properties of the material afterward, be sure you get a new reference to the derived version:
```js
// Bad:
text.material = myOrigMaterial
myOrigMaterial.opacity = 0.5
// Good:
text.material = myOrigMaterial
text.material.opacity = 0.5
```
Default: a `MeshBasicMaterial` instance
### `maxWidth`
The maximum width of the text block, above which text may start wrapping according to the `whiteSpace` and `overflowWrap` properties.
Default: `Infinity`, meaning text will never wrap
### `outlineBlur`
Specifies a blur radius applied to the outer edge of the text's `outlineWidth`. If the `outlineWidth` is zero, the blur will be applied at the glyph edge, like CSS's `text-shadow` blur radius. A blur plus a nonzero `outlineWidth` can give a solid outline with a fuzzy outer edge.
The blur radius can be specified as either an absolute number in local units, or as a percentage string e.g. `"12%"` which is treated as a percentage of the `fontSize`.
Default: `0`
### `outlineColor`
The color to use for the text outline when `outlineWidth`, `outlineBlur`, and/or `outlineOffsetX/Y` are set. Accepts a ThreeJS `Color` object, or a number/string accepted by `Color#set`.
Default: black
### `outlineOffsetX`, `outlineOffsetY`
These define a horizontal and vertical offset of the text outline. Using an offset with `outlineWidth: 0` creates a drop-shadow effect like CSS's `text-shadow`; also see `outlineBlur`.
The offsets can be specified as either an absolute number in local units, or as a percentage string e.g. `"12%"` which is treated as a percentage of the `fontSize`.
Default: `0`
### `outlineOpacity`
Sets the opacity of a configured text outline, in the range `0` to `1`.
Default: `1`
### `outlineWidth`
The width of an outline/halo to be drawn around each text glyph using the `outlineColor` and `outlineOpacity`. This can help improve readability when the text is displayed against a background of low or varying contrast.
The width can be specified as either an absolute number in local units, or as a percentage string e.g. `"10%"` which is interpreted as a percentage of the `fontSize`.
Default: `0`
### `overflowWrap`
Defines how text wraps if the `whiteSpace` property is `'normal'`. Can be either `'normal'` to break at whitespace characters, or `'break-word'` to allow breaking within words.
Default: `'normal'`
### `sdfGlyphSize`
Allows overriding the default size of each glyph's SDF (signed distance field) used when rendering this text instance. This must be a power-of-two number. Larger sizes can improve the quality of glyph rendering by increasing the sharpness of corners and preventing loss of very thin lines, at the expense of increased memory footprint and longer SDF generation time.
Default: `64`
### `strokeColor`
The color of the text stroke, when `strokeWidth` is nonzero. Accepts a ThreeJS `Color` object, or a number/string accepted by `Color#set`.
Default: grey
### `strokeOpacity`
The opacity of the text stroke, when `strokeWidth` is nonzero. Accepts a number from `0` to `1`.
Default: `1`
### `strokeWidth`
Sets the width of a stroke drawn inside the edge of each text glyph, using the `strokeColor` and `strokeOpacity`.
The width can be specified as either an absolute number in local units, or as a percentage string e.g. `"10%"` which is interpreted as a percentage of the `fontSize`.
Default: `0`
### `textAlign`
The horizontal alignment of each line of text within the overall text bounding box. Can be one of `'left'`, `'right'`, `'center'`, or `'justify'`.
Default: `'left'`
### `textIndent`
An indentation applied to the first character of each _hard_ newline. Behaves like CSS `text-indent`.
Default: `0`
### `unicodeFontsUrl`
A custom hosting location for [unicode-font-resolver](https://github.com/lojjic/unicode-font-resolver/) data index and font files, if you want to self-host them rather than using the default CDN. See [Unicode Fallback Fonts](#unicode-fallback-fonts) below for details.
### `whiteSpace`
Defines whether text should wrap when a line reaches the `maxWidth`. Can be either `'normal'`, to allow wrapping according to the `overflowWrap` property, or `'nowrap'` to prevent wrapping.
Note that `'normal'` in this context _does_ honor newline characters to manually break lines, making it behave more like `'pre-wrap'` does in CSS.
Default: `'normal'`
## Unicode Fallback Fonts
When a character is not covered by the configured `font` file, [unicode-font-resolver](https://github.com/lojjic/unicode-font-resolver/) will be used to query for an appropriate fallback font. All Unicode character ranges supported by the [Google Noto Fonts](https://fonts.google.com/noto) are covered this way.
By default, the unicode-font-resolver data index and font files are loaded from the [jsDelivr CDN](https://www.jsdelivr.com/). If you wish to self-host those files you can do so; however be aware the full set of data and fonts is nearly 300MB.
To self-host the files:
- Go to the [unicode-font-resolver Github tags page](https://github.com/lojjic/unicode-font-resolver/tags)
- Find the tag matching the version of `@unicode-font-resolver/client` declared in `troika-three-text/package.json`'s `devDependencies`.
- Download the source code .zip or .tar.gz for that release and unpack it.
- Copy the contents of the `packages/data/` directory to your server where you want to host it.
- Configure troika-three-text to load from that server URL:
- Per Text instance:
```js
text.unicodeFontsURL = 'https://my.host/unicode-fonts-data'
```
- Globally:
```js
import {configureTextBuilder} from 'troika-three-text'
configureTextBuilder({
unicodeFontsURL: 'https://my.host/unicode-fonts-data'
})
```
## Handling Asynchronous Updates
Since the text processing occurs in a web worker, it is by definition asynchronous. This means that you can't rely on the text being visible or having a complete geometry immediately. If you need to do things like access the geometry's `boundingSphere` or the `textRenderInfo`, you will have to listen for completion. You can do this two ways:
1. Pass a callback function when you call the `sync` method:
```js
myText.sync(() => {
// code to execute after sync completes...
})
```
This is best when you want to only react to _that specific_ sync call. Keep in mind that the callback will not execute if the text is already fully synced.
2. Add a listener for the `synccomplete` event:
```js
myText.addEventListener('synccomplete', () => {
// code to execute after sync completes...
})
```
This will fire after _every_ sync, no matter who invoked it. This is best if you need to react to all syncs, for example to trigger a manual canvas render.
You can also listen for the `syncstart` event if you need to react to the initiation of a sync call, e.g. to set some sort of "waiting" state while the text is being processed.
### Disabling web worker
Some environments don't allow evaluating JS strings for security reasons, like those with a restrictive [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), which blocks how troika-three-text creates its web worker. Or you may want to avoid using a worker for some other reason. You can force it to run on the main thread instead (although still asynchronously) by configuring it prior to creating any Text instances:
```js
import {configureTextBuilder} from 'troika-three-text'
configureTextBuilder({
useWorker: false
})
```
## Preloading
To avoid long pauses when first displaying a piece of text in your scene, you can preload fonts and optionally pre-generate the SDF textures for particular glyphs up front:
```js
import {preloadFont} from 'troika-three-text'
myApp.showLoadingScreen()
preloadFont(
{
font: 'path/to/myfontfile.woff',
characters: 'abcdefghijklmnopqrstuvwxyz'
},
() => {
myApp.showScene()
}
)
```
The arguments are:
- `options`
- `options.font` - The URL of the font file to preload. If `null` is passed, this will preload the default font.
- `options.characters` - A string or array of string character sequences for which to pre-generate glyph SDF textures. Note that this _will_ honor ligature substitution, so you may need to specify ligature sequences in addition to their individual characters to get all possible glyphs, e.g. `["t", "h", "th"]` to get the "t" and "h" glyphs plus the "th" glyph.
- `options.sdfGlyphSize` - The size at which to prerender the SDFs for the `characters` glyphs. See the `sdfGlyphSize` config property on `Text` for details about SDF sizes. If not specified, will use the default SDF size.
- `callback` - A function that will be called when the preloading is complete.
## Postprocessing
It is possible to use `Text` within scenes that utilize the [postprocessing](https://github.com/vanruesc/postprocessing) library for applying image effects. However, you must enable a special mode in that library that allows `Text`'s custom material to be honored. Just do the following once somewhere in your code:
```js
import { OverrideMaterialManager } from 'postprocessing'
OverrideMaterialManager.workaroundEnabled = true
```
## Carets and Selection Ranges
In addition to rendering text, it is possible to access positioning information for caret placement and selection ranges. To access that info, use the `getCaretAtPoint` and `getSelectionRects` utility functions. Both of these functions take a `textRenderInfo` object as input, which you can get from the `Text` object's `textRenderInfo` property after sync has completed. See "Handling Asynchronous Updates" above for how to react to sync completion events.
### `getCaretAtPoint(textRenderInfo, x, y)`
This returns the caret position nearest to a given x/y position in the local text plane. This is useful for placing an editing caret based on a click or ther raycasted event. The return value is an object with the following properties:
- `x` - x position of the caret
- `y` - y position of the caret's bottom
- `height` - height of the caret, based on the current fontSize and lineHeight
- `charIndex` - the index in the original input string of this caret's target character. The caret will be for the position _before_ that character. For the final caret position, this will be equal to the string length. For ligature glyphs, this will be for the first character in the ligature sequence.
### `getSelectionRects(textRenderInfo, start, end)`
This returns a list of rectangles covering all the characters within a given character range. This is useful for highlighting a selection range. The return value is an array of objects, each with `{left, top, right, bottom}` properties in the local text plane.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+194
View File
@@ -0,0 +1,194 @@
/*
Custom build of Typr.ts (https://github.com/fredli74/Typr.ts) for use in Troika text rendering.
Original MIT license applies: https://github.com/fredli74/Typr.ts/blob/master/LICENSE
Custom bundle of woff2otf (https://github.com/arty-name/woff2otf) with fflate
(https://github.com/101arrowz/fflate) for use in Troika text rendering.
Original licenses apply:
- fflate: https://github.com/101arrowz/fflate/blob/master/LICENSE (MIT)
- woff2otf.js: https://github.com/arty-name/woff2otf/blob/master/woff2otf.js (Apache2)
Custom bundle of @unicode-font-resolver/client v1.0.2 (https://github.com/lojjic/unicode-font-resolver)
for use in Troika text rendering.
Original MIT license applies
*/
'use strict';(function(J,z){"object"===typeof exports&&"undefined"!==typeof module?z(exports,require("three"),require("troika-worker-utils"),require("webgl-sdf-generator"),require("bidi-js"),require("troika-three-utils")):"function"===typeof define&&define.amd?define("exports three troika-worker-utils webgl-sdf-generator bidi-js troika-three-utils".split(" "),z):(J="undefined"!==typeof globalThis?globalThis:J||self,z(J.troika_three_text={},J.THREE,J.troika_worker_utils,J.webgl_sdf_generator,J.bidi_js,
J.troika_three_utils))})(this,function(J,z,ba,R,X,ja){function xa(e){return e&&"object"===typeof e&&"default"in e?e:{"default":e}}function Va(e,f,h,l,b,a,c,d,g,k,q=!0){return q?Wa(e,f,h,l,b,a,c,d,g,k).then(null,q=>{ya||(console.warn("WebGL SDF generation failed, falling back to JS",q),ya=!0);return za(e,f,h,l,b,a,c,d,g,k)}):za(e,f,h,l,b,a,c,d,g,k)}function Aa(){let e=S();for(;ka.length&&5>S()-e;)ka.shift()();pa=ka.length?setTimeout(Aa,0):0}function za(e,f,h,l,b,a,c,d,g,k){let q="TroikaTextSDFGenerator_JS_"+
Xa++%4,m=Ba[q];m||(m=Ba[q]={workerModule:ba.defineWorkerModule({name:q,workerId:q,dependencies:[Ca["default"],S],init(a,b){let c=a().javascript.generate;return function(...a){let d=b();return{textureData:c(...a),timing:b()-d}}},getTransferables(a){return[a.textureData.buffer]}}),requests:0,idleTimer:null});m.requests++;clearTimeout(m.idleTimer);return m.workerModule(e,f,h,l,b,a).then(({textureData:a,timing:b})=>{let h=S(),n=new Uint8Array(4*a.length);for(let b=0;b<a.length;b++)n[4*b+k]=a[b];la.webglUtils.renderImageData(c,
n,d,g,e,f,1<<3-k);b+=S()-h;0===--m.requests&&(m.idleTimer=setTimeout(()=>{ba.terminateWorker(q)},2E3));return{timing:b}})}function Y(){return(self.performance||Date).now()}function qa(e,f){Da=!0;e=Ea({},e);let h=Y();var {defaultFontURL:l}=Q,b=[];l&&b.push({label:"default",src:Fa(l)});e.font&&b.push({label:"user",src:Fa(e.font)});e.font=b;e.text=""+e.text;e.sdfGlyphSize=e.sdfGlyphSize||Q.sdfGlyphSize;e.unicodeFontsURL=e.unicodeFontsURL||Q.unicodeFontsURL;if(null!=e.colorRanges){l={};for(var a in e.colorRanges)e.colorRanges.hasOwnProperty(a)&&
(b=e.colorRanges[a],"number"!==typeof b&&(b=Ya.set(b).getHex()),l[a]=b);e.colorRanges=l}Object.freeze(e);let {textureWidth:c,sdfExponent:d}=Q,{sdfGlyphSize:g}=e,k=c/g*4,q=ma[g];q||(a=document.createElement("canvas"),a.width=c,a.height=256*g/k,q=ma[g]={glyphCount:0,sdfGlyphSize:g,sdfCanvas:a,sdfTexture:new z.Texture(a,void 0,void 0,void 0,z.LinearFilter,z.LinearFilter),contextLost:!1,glyphsByFont:new Map},q.sdfTexture.generateMipmaps=!1,Za(q));let {sdfTexture:m,sdfCanvas:n}=q;(Q.useWorker?Ga:$a)(e).then(a=>
{let {glyphIds:b,glyphFontIndices:l,fontData:y,glyphPositions:t,fontSize:v,timings:w}=a,u=[],G=new Float32Array(4*b.length),L=0,C=0;var A=Y();let H=y.map(a=>{let b=q.glyphsByFont.get(a.src);b||q.glyphsByFont.set(a.src,b=new Map);return b});b.forEach((c,d)=>{var f=l[d];let {src:k,unitsPerEm:e}=y[f];var m=H[f].get(c);if(!m){let {path:b,pathBounds:d}=a.glyphData[k][c];m=Math.max(d[2]-d[0],d[3]-d[1])/g*(Q.sdfMargin*g+.5);var h=q.glyphCount++;H[f].set(c,m={path:b,atlasIndex:h,sdfViewBox:[d[0]-m,d[1]-m,
d[2]+m,d[3]+m]});u.push(m)}({sdfViewBox:c}=m);f=t[C++];h=t[C++];let n=v/e;G[L++]=f+c[0]*n;G[L++]=h+c[1]*n;G[L++]=f+c[2]*n;G[L++]=h+c[3]*n;b[d]=m.atlasIndex});w.quads=(w.quads||0)+(Y()-A);let B=Y();w.sdf={};A=n.height;let Z=Math.pow(2,Math.ceil(Math.log2(Math.ceil(q.glyphCount/k)*g)));Z>A&&(console.info(`Increasing SDF texture size ${A}->${Z}`),ab(n,c,Z),m.dispose());Promise.all(u.map(a=>Ha(a,q,e.gpuAccelerateSDF).then(({timing:b})=>{w.sdf[a.atlasIndex]=b}))).then(()=>{u.length&&!q.contextLost&&(Ia(q),
m.needsUpdate=!0);w.sdfTotal=Y()-B;w.total=Y()-h;f(Object.freeze({parameters:e,sdfTexture:m,sdfGlyphSize:g,sdfExponent:d,glyphBounds:G,glyphAtlasIndices:b,glyphColors:a.glyphColors,caretPositions:a.caretPositions,chunkedBounds:a.chunkedBounds,ascender:a.ascender,descender:a.descender,lineHeight:a.lineHeight,capHeight:a.capHeight,xHeight:a.xHeight,topBaseline:a.topBaseline,blockBounds:a.blockBounds,visibleBounds:a.visibleBounds,timings:a.timings}))})});Promise.resolve().then(()=>{q.contextLost||n._warm||
(la.webgl.isSupported(n),n._warm=!0)})}function Ha({path:e,atlasIndex:f,sdfViewBox:h},{sdfGlyphSize:l,sdfCanvas:b,contextLost:a},c){if(a)return Promise.resolve({timing:-1});let {textureWidth:d,sdfExponent:g}=Q;a=Math.floor(f/4);return Va(l,l,e,h,Math.max(h[2]-h[0],h[3]-h[1]),g,b,a%(d/l)*l,Math.floor(a/(d/l))*l,f%4,c)}function Za(e){let f=e.sdfCanvas;f.addEventListener("webglcontextlost",f=>{console.log("Context Lost",f);f.preventDefault();e.contextLost=!0});f.addEventListener("webglcontextrestored",
f=>{console.log("Context Restored",f);e.contextLost=!1;let h=[];e.glyphsByFont.forEach(b=>{b.forEach(a=>{h.push(Ha(a,e,!0))})});Promise.all(h).then(()=>{Ia(e);e.sdfTexture.needsUpdate=!0})})}function Ea(e,f){for(let h in f)f.hasOwnProperty(h)&&(e[h]=f[h]);return e}function Fa(e){na||(na="undefined"===typeof document?{}:document.createElement("a"));na.href=e;return na.href}function Ia(e){if("function"!==typeof createImageBitmap){console.info("Safari<15: applying SDF canvas workaround");let {sdfCanvas:f,
sdfTexture:h}=e,{width:l,height:b}=f;e=e.sdfCanvas.getContext("webgl");let a=h.image.data;a&&a.length===l*b*4||(a=new Uint8Array(l*b*4),h.image={width:l,height:b,data:a},h.flipY=!1,h.isDataTexture=!0);e.readPixels(0,0,l,b,e.RGBA,e.UNSIGNED_BYTE,a)}}function bb(e){let f=Ja[e];f||(f=Ja[e]=(new z.PlaneGeometry(1,1,e,e)).translate(.5,.5,0));return f}function ra(e){e=ja.createDerivedMaterial(e,{chained:!0,extensions:{derivatives:!0},uniforms:{uTroikaSDFTexture:{value:null},uTroikaSDFTextureSize:{value:new z.Vector2},
uTroikaSDFGlyphSize:{value:0},uTroikaSDFExponent:{value:0},uTroikaTotalBounds:{value:new z.Vector4(0,0,0,0)},uTroikaClipRect:{value:new z.Vector4(0,0,0,0)},uTroikaEdgeOffset:{value:0},uTroikaFillOpacity:{value:1},uTroikaPositionOffset:{value:new z.Vector2},uTroikaCurveRadius:{value:0},uTroikaBlurRadius:{value:0},uTroikaStrokeWidth:{value:0},uTroikaStrokeColor:{value:new z.Color},uTroikaStrokeOpacity:{value:1},uTroikaOrient:{value:new z.Matrix3},uTroikaUseGlyphColors:{value:!0},uTroikaSDFDebug:{value:!1}},
vertexDefs:"\nuniform vec2 uTroikaSDFTextureSize;\nuniform float uTroikaSDFGlyphSize;\nuniform vec4 uTroikaTotalBounds;\nuniform vec4 uTroikaClipRect;\nuniform mat3 uTroikaOrient;\nuniform bool uTroikaUseGlyphColors;\nuniform float uTroikaEdgeOffset;\nuniform float uTroikaBlurRadius;\nuniform vec2 uTroikaPositionOffset;\nuniform float uTroikaCurveRadius;\nattribute vec4 aTroikaGlyphBounds;\nattribute float aTroikaGlyphIndex;\nattribute vec3 aTroikaGlyphColor;\nvarying vec2 vTroikaGlyphUV;\nvarying vec4 vTroikaTextureUVBounds;\nvarying float vTroikaTextureChannel;\nvarying vec3 vTroikaGlyphColor;\nvarying vec2 vTroikaGlyphDimensions;\n",
vertexTransform:"\nvec4 bounds = aTroikaGlyphBounds;\nbounds.xz += uTroikaPositionOffset.x;\nbounds.yw -= uTroikaPositionOffset.y;\n\nvec4 outlineBounds = vec4(\n bounds.xy - uTroikaEdgeOffset - uTroikaBlurRadius,\n bounds.zw + uTroikaEdgeOffset + uTroikaBlurRadius\n);\nvec4 clippedBounds = vec4(\n clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw),\n clamp(outlineBounds.zw, uTroikaClipRect.xy, uTroikaClipRect.zw)\n);\n\nvec2 clippedXY = (mix(clippedBounds.xy, clippedBounds.zw, position.xy) - bounds.xy) / (bounds.zw - bounds.xy);\n\nposition.xy = mix(bounds.xy, bounds.zw, clippedXY);\n\nuv = (position.xy - uTroikaTotalBounds.xy) / (uTroikaTotalBounds.zw - uTroikaTotalBounds.xy);\n\nfloat rad = uTroikaCurveRadius;\nif (rad != 0.0) {\n float angle = position.x / rad;\n position.xz = vec2(sin(angle) * rad, rad - cos(angle) * rad);\n normal.xz = vec2(sin(angle), cos(angle));\n}\n \nposition = uTroikaOrient * position;\nnormal = uTroikaOrient * normal;\n\nvTroikaGlyphUV = clippedXY.xy;\nvTroikaGlyphDimensions = vec2(bounds[2] - bounds[0], bounds[3] - bounds[1]);\n\n\nfloat txCols = uTroikaSDFTextureSize.x / uTroikaSDFGlyphSize;\nvec2 txUvPerSquare = uTroikaSDFGlyphSize / uTroikaSDFTextureSize;\nvec2 txStartUV = txUvPerSquare * vec2(\n mod(floor(aTroikaGlyphIndex / 4.0), txCols),\n floor(floor(aTroikaGlyphIndex / 4.0) / txCols)\n);\nvTroikaTextureUVBounds = vec4(txStartUV, vec2(txStartUV) + txUvPerSquare);\nvTroikaTextureChannel = mod(aTroikaGlyphIndex, 4.0);\n",
fragmentDefs:"\nuniform sampler2D uTroikaSDFTexture;\nuniform vec2 uTroikaSDFTextureSize;\nuniform float uTroikaSDFGlyphSize;\nuniform float uTroikaSDFExponent;\nuniform float uTroikaEdgeOffset;\nuniform float uTroikaFillOpacity;\nuniform float uTroikaBlurRadius;\nuniform vec3 uTroikaStrokeColor;\nuniform float uTroikaStrokeWidth;\nuniform float uTroikaStrokeOpacity;\nuniform bool uTroikaSDFDebug;\nvarying vec2 vTroikaGlyphUV;\nvarying vec4 vTroikaTextureUVBounds;\nvarying float vTroikaTextureChannel;\nvarying vec2 vTroikaGlyphDimensions;\n\nfloat troikaSdfValueToSignedDistance(float alpha) {\n // Inverse of exponential encoding in webgl-sdf-generator\n \n float maxDimension = max(vTroikaGlyphDimensions.x, vTroikaGlyphDimensions.y);\n float absDist = (1.0 - pow(2.0 * (alpha > 0.5 ? 1.0 - alpha : alpha), 1.0 / uTroikaSDFExponent)) * maxDimension;\n float signedDist = absDist * (alpha > 0.5 ? -1.0 : 1.0);\n return signedDist;\n}\n\nfloat troikaGlyphUvToSdfValue(vec2 glyphUV) {\n vec2 textureUV = mix(vTroikaTextureUVBounds.xy, vTroikaTextureUVBounds.zw, glyphUV);\n vec4 rgba = texture2D(uTroikaSDFTexture, textureUV);\n float ch = floor(vTroikaTextureChannel + 0.5); //NOTE: can't use round() in WebGL1\n return ch == 0.0 ? rgba.r : ch == 1.0 ? rgba.g : ch == 2.0 ? rgba.b : rgba.a;\n}\n\nfloat troikaGlyphUvToDistance(vec2 uv) {\n return troikaSdfValueToSignedDistance(troikaGlyphUvToSdfValue(uv));\n}\n\nfloat troikaGetAADist() {\n \n #if defined(GL_OES_standard_derivatives) || __VERSION__ >= 300\n return length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.5;\n #else\n return vTroikaGlyphDimensions.x / 64.0;\n #endif\n}\n\nfloat troikaGetFragDistValue() {\n vec2 clampedGlyphUV = clamp(vTroikaGlyphUV, 0.5 / uTroikaSDFGlyphSize, 1.0 - 0.5 / uTroikaSDFGlyphSize);\n float distance = troikaGlyphUvToDistance(clampedGlyphUV);\n \n // Extrapolate distance when outside bounds:\n distance += clampedGlyphUV == vTroikaGlyphUV ? 0.0 : \n length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions);\n\n \n\n return distance;\n}\n\nfloat troikaGetEdgeAlpha(float distance, float distanceOffset, float aaDist) {\n #if defined(IS_DEPTH_MATERIAL) || defined(IS_DISTANCE_MATERIAL)\n float alpha = step(-distanceOffset, -distance);\n #else\n\n float alpha = smoothstep(\n distanceOffset + aaDist,\n distanceOffset - aaDist,\n distance\n );\n #endif\n\n return alpha;\n}\n",
fragmentColorTransform:"\nfloat aaDist = troikaGetAADist();\nfloat fragDistance = troikaGetFragDistValue();\nfloat edgeAlpha = uTroikaSDFDebug ?\n troikaGlyphUvToSdfValue(vTroikaGlyphUV) :\n troikaGetEdgeAlpha(fragDistance, uTroikaEdgeOffset, max(aaDist, uTroikaBlurRadius));\n\n#if !defined(IS_DEPTH_MATERIAL) && !defined(IS_DISTANCE_MATERIAL)\nvec4 fillRGBA = gl_FragColor;\nfillRGBA.a *= uTroikaFillOpacity;\nvec4 strokeRGBA = uTroikaStrokeWidth == 0.0 ? fillRGBA : vec4(uTroikaStrokeColor, uTroikaStrokeOpacity);\nif (fillRGBA.a == 0.0) fillRGBA.rgb = strokeRGBA.rgb;\ngl_FragColor = mix(fillRGBA, strokeRGBA, smoothstep(\n -uTroikaStrokeWidth - aaDist,\n -uTroikaStrokeWidth + aaDist,\n fragDistance\n));\ngl_FragColor.a *= edgeAlpha;\n#endif\n\nif (edgeAlpha == 0.0) {\n discard;\n}\n",
customRewriter({vertexShader:f,fragmentShader:e}){let h=/\buniform\s+vec3\s+diffuse\b/;h.test(e)&&(e=e.replace(h,"varying vec3 vTroikaGlyphColor").replace(/\bdiffuse\b/g,"vTroikaGlyphColor"),h.test(f)||(f=f.replace(ja.voidMainRegExp,"uniform vec3 diffuse;\n$&\nvTroikaGlyphColor = uTroikaUseGlyphColors ? aTroikaGlyphColor / 255.0 : diffuse;\n")));return{vertexShader:f,fragmentShader:e}}});e.transparent=!0;e.forceSinglePass=!0;Object.defineProperties(e,{isTroikaTextMaterial:{value:!0},shadowSide:{get(){return this.side},
set(){}}});return e}function Ka(e){return Array.isArray(e)?e[0]:e}function sa(e,f){let h=new e.constructor(f);h.set(e.subarray(0,f));return h}function cb(e){let f=ja.createDerivedMaterial(e,{chained:!0,uniforms:{uTroikaMatricesTextureSize:{value:new z.Vector2},uTroikaMatricesTexture:{value:null}},vertexDefs:`
uniform highp sampler2D ${"uTroikaMatricesTexture"};
uniform vec2 ${"uTroikaMatricesTextureSize"};
attribute float ${"aTroikaTextBatchMemberIndex"};
vec4 troikaBatchTexel(float offset) {
offset += ${"aTroikaTextBatchMemberIndex"} * ${(32).toFixed(1)} / 4.0;
float w = ${"uTroikaMatricesTextureSize"}.x;
vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${"uTroikaMatricesTextureSize"};
return texture2D(${"uTroikaMatricesTexture"}, uv);
}
`,vertexTransform:"\n mat4 matrix = mat4(\n troikaBatchTexel(0.0),\n troikaBatchTexel(1.0),\n troikaBatchTexel(2.0),\n troikaBatchTexel(3.0)\n );\n position.xyz = (matrix * vec4(position, 1.0)).xyz;\n "});f=ra(f);f=ja.createDerivedMaterial(f,{chained:!0,uniforms:{uTroikaIsOutline:{value:!1}},customRewriter(f){"uTroikaTotalBounds uTroikaClipRect uTroikaPositionOffset uTroikaEdgeOffset uTroikaBlurRadius uTroikaStrokeWidth uTroikaStrokeColor uTroikaStrokeOpacity uTroikaFillOpacity uTroikaCurveRadius diffuse".split(" ").forEach(e=>
{f=db(f,e)});return f},vertexDefs:"\n uniform bool uTroikaIsOutline;\n vec3 troikaFloatToColor(float v) {\n return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;\n }\n ",vertexTransform:"\n uTroikaTotalBounds = troikaBatchTexel(4.0);\n uTroikaClipRect = troikaBatchTexel(5.0);\n \n vec4 data = troikaBatchTexel(6.0);\n diffuse = troikaFloatToColor(data.x);\n uTroikaFillOpacity = data.y;\n uTroikaCurveRadius = data.z;\n \n data = troikaBatchTexel(7.0);\n if (uTroikaIsOutline) {\n if (data == vec4(0.0)) { // degenerate if zero outline\n position = vec3(0.0);\n } else {\n uTroikaPositionOffset = data.xy;\n uTroikaEdgeOffset = data.z;\n uTroikaBlurRadius = data.w;\n }\n } else {\n uTroikaStrokeWidth = data.x;\n uTroikaStrokeColor = troikaFloatToColor(data.y);\n uTroikaStrokeOpacity = data.z;\n }\n "});
f.setMatrixTexture=e=>{f.uniforms.uTroikaMatricesTexture.value=e;f.uniforms.uTroikaMatricesTextureSize.value.set(e.image.width,e.image.height)};return f}function db({vertexShader:e,fragmentShader:f},h,l=h){h=new RegExp(`uniform\\s+(bool|float|vec[234]|mat[34])\\s+${h}\\b`);let b,a=!1;f=f.replace(h,(c,f)=>{a=!0;return`varying ${b=f} ${l}`});let c=!1;e=e.replace(h,(d,f)=>{c=!0;return`${a?"varying":""} ${b=f} ${l}`});c||(e=`${a?"varying":""} ${b} ${l};\n${e}`);return{vertexShader:e,fragmentShader:f}}
function eb(e){let f=La.get(e);if(!f){f=[];let {caretPositions:h}=e,l,b=(a,b,g,k)=>{(!l||g<(l.top+l.bottom)/2)&&f.push(l={bottom:b,top:g,carets:[]});g>l.top&&(l.top=g);b<l.bottom&&(l.bottom=b);l.carets.push({x:a,y:b,height:g-b,charIndex:k})},a=0;for(;a<h.length;a+=4)b(h[a],h[a+2],h[a+3],a/4);b(h[a-3],h[a-2],h[a-1],a/4)}La.set(e,f);return f}var Ca=xa(R);X=xa(X);R=ba.defineWorkerModule({name:"Typr Font Parser",dependencies:[function(){return"undefined"==typeof window&&(self.window=self),function(e){var f=
{parse:function(b){var a=f._bin;b=new Uint8Array(b);if("ttcf"==a.readASCII(b,0,4)){var c=4;a.readUshort(b,c);c+=2;a.readUshort(b,c);c+=2;var d=a.readUint(b,c);c+=4;for(var g=[],k=0;k<d;k++){var q=a.readUint(b,c);c+=4;g.push(f._readFont(b,q))}return g}return[f._readFont(b,0)]},_readFont:function(b,a){var c=f._bin,d=a;c.readFixed(b,a);a+=4;var g=c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;var k="cmap;head;hhea;maxp;hmtx;name;OS/2;post;loca;glyf;kern;CFF ;GDEF;GPOS;GSUB;SVG ".split(";");
d={_data:b,_offset:d};for(var q={},e=0;e<g;e++){var n=c.readASCII(b,a,4);a+=4;c.readUint(b,a);a+=4;var h=c.readUint(b,a);a+=4;var l=c.readUint(b,a);a+=4;q[n]={offset:h,length:l}}for(e=0;e<k.length;e++)a=k[e],q[a]&&(d[a.trim()]=f[a.trim()].parse(b,q[a].offset,q[a].length,d));return d},_tabOffset:function(b,a,c){var d=f._bin,g=d.readUshort(b,c+4);c+=12;for(var k=0;k<g;k++){var q=d.readASCII(b,c,4);c+=4;d.readUint(b,c);c+=4;var e=d.readUint(b,c);if(c+=4,d.readUint(b,c),c+=4,q==a)return e}return 0}};
f._bin={readFixed:function(b,a){return(b[a]<<8|b[a+1])+(b[a+2]<<8|b[a+3])/65540},readF2dot14:function(b,a){return f._bin.readShort(b,a)/16384},readInt:function(b,a){return f._bin._view(b).getInt32(a)},readInt8:function(b,a){return f._bin._view(b).getInt8(a)},readShort:function(b,a){return f._bin._view(b).getInt16(a)},readUshort:function(b,a){return f._bin._view(b).getUint16(a)},readUshorts:function(b,a,c){for(var d=[],g=0;g<c;g++)d.push(f._bin.readUshort(b,a+2*g));return d},readUint:function(b,a){return f._bin._view(b).getUint32(a)},
readUint64:function(b,a){return 4294967296*f._bin.readUint(b,a)+f._bin.readUint(b,a+4)},readASCII:function(b,a,c){for(var d="",f=0;f<c;f++)d+=String.fromCharCode(b[a+f]);return d},readUnicode:function(b,a,c){for(var d="",f=0;f<c;f++){var k=b[a++]<<8|b[a++];d+=String.fromCharCode(k)}return d},_tdec:"undefined"!=typeof window&&window.TextDecoder?new window.TextDecoder:null,readUTF8:function(b,a,c){var d=f._bin._tdec;return d&&0==a&&c==b.length?d.decode(b):f._bin.readASCII(b,a,c)},readBytes:function(b,
a,c){for(var d=[],f=0;f<c;f++)d.push(b[a+f]);return d},readASCIIArray:function(b,a,c){for(var d=[],f=0;f<c;f++)d.push(String.fromCharCode(b[a+f]));return d},_view:function(b){return b._dataView||(b._dataView=b.buffer?new DataView(b.buffer,b.byteOffset,b.byteLength):new DataView((new Uint8Array(b)).buffer))}};f._lctf={};f._lctf.parse=function(b,a,c,d,g){var k=f._bin;c={};d=a;k.readFixed(b,a);a+=4;var q=k.readUshort(b,a);a+=2;var e=k.readUshort(b,a);a=k.readUshort(b,a+2);return c.scriptList=f._lctf.readScriptList(b,
d+q),c.featureList=f._lctf.readFeatureList(b,d+e),c.lookupList=f._lctf.readLookupList(b,d+a,g),c};f._lctf.readLookupList=function(b,a,c){var d=f._bin,g=a,k=[],q=d.readUshort(b,a);a+=2;for(var e=0;e<q;e++){var n=d.readUshort(b,a);a+=2;n=f._lctf.readLookupTable(b,g+n,c);k.push(n)}return k};f._lctf.readLookupTable=function(b,a,c){var d=f._bin,g=a,k={tabs:[]};k.ltype=d.readUshort(b,a);a+=2;k.flag=d.readUshort(b,a);a+=2;var q=d.readUshort(b,a);a+=2;for(var e=k.ltype,n=0;n<q;n++){var h=d.readUshort(b,a);
a+=2;h=c(b,e,g+h,k);k.tabs.push(h)}return k};f._lctf.numOfOnes=function(b){for(var a=0,c=0;32>c;c++)0!=(b>>>c&1)&&a++;return a};f._lctf.readClassDef=function(b,a){var c=f._bin,d=[],g=c.readUshort(b,a);if(a+=2,1==g){var k=c.readUshort(b,a);a+=2;var q=c.readUshort(b,a);a+=2;for(var e=0;e<q;e++)d.push(k+e),d.push(k+e),d.push(c.readUshort(b,a)),a+=2}if(2==g)for(g=c.readUshort(b,a),a+=2,e=0;e<g;e++)d.push(c.readUshort(b,a)),a+=2,d.push(c.readUshort(b,a)),a+=2,d.push(c.readUshort(b,a)),a+=2;return d};f._lctf.getInterval=
function(b,a){for(var c=0;c<b.length;c+=3){var d=b[c],f=b[c+1];if(b[c+2],d<=a&&a<=f)return c}return-1};f._lctf.readCoverage=function(b,a){var c=f._bin,d={};d.fmt=c.readUshort(b,a);a+=2;var g=c.readUshort(b,a);return a+=2,1==d.fmt&&(d.tab=c.readUshorts(b,a,g)),2==d.fmt&&(d.tab=c.readUshorts(b,a,3*g)),d};f._lctf.coverageIndex=function(b,a){var c=b.tab;return 1==b.fmt?c.indexOf(a):2==b.fmt&&(b=f._lctf.getInterval(c,a),-1!=b)?c[b+2]+(a-c[b]):-1};f._lctf.readFeatureList=function(b,a){var c=f._bin,d=a,
g=[],k=c.readUshort(b,a);a+=2;for(var q=0;q<k;q++){var e=c.readASCII(b,a,4);a+=4;var n=c.readUshort(b,a);a+=2;n=f._lctf.readFeatureTable(b,d+n);n.tag=e.trim();g.push(n)}return g};f._lctf.readFeatureTable=function(b,a){var c=f._bin,d=a,g={},k=c.readUshort(b,a);a+=2;0<k&&(g.featureParams=d+k);d=c.readUshort(b,a);a+=2;g.tab=[];for(k=0;k<d;k++)g.tab.push(c.readUshort(b,a+2*k));return g};f._lctf.readScriptList=function(b,a){var c=f._bin,d=a,g={},k=c.readUshort(b,a);a+=2;for(var q=0;q<k;q++){var e=c.readASCII(b,
a,4);a+=4;var n=c.readUshort(b,a);a+=2;g[e.trim()]=f._lctf.readScriptTable(b,d+n)}return g};f._lctf.readScriptTable=function(b,a){var c=f._bin,d=a,g={},k=c.readUshort(b,a);a+=2;0<k&&(g.default=f._lctf.readLangSysTable(b,d+k));k=c.readUshort(b,a);a+=2;for(var e=0;e<k;e++){var m=c.readASCII(b,a,4);a+=4;var n=c.readUshort(b,a);a+=2;g[m.trim()]=f._lctf.readLangSysTable(b,d+n)}return g};f._lctf.readLangSysTable=function(b,a){var c=f._bin,d={};c.readUshort(b,a);a+=2;d.reqFeature=c.readUshort(b,a);a+=2;
var g=c.readUshort(b,a);return a+=2,d.features=c.readUshorts(b,a,g),d};f.CFF={};f.CFF.parse=function(b,a,c){var d=f._bin;(b=new Uint8Array(b.buffer,a,c))[a=0];b[++a];b[++a];b[++a];a++;var g=[];a=f.CFF.readIndex(b,a,g);var k=[];for(c=0;c<g.length-1;c++)k.push(d.readASCII(b,a+g[c],g[c+1]-g[c]));a+=g[g.length-1];g=[];a=f.CFF.readIndex(b,a,g);k=[];for(c=0;c<g.length-1;c++)k.push(f.CFF.readDict(b,a+g[c],a+g[c+1]));a+=g[g.length-1];g=k[0];var e=[];a=f.CFF.readIndex(b,a,e);k=[];for(c=0;c<e.length-1;c++)k.push(d.readASCII(b,
a+e[c],e[c+1]-e[c]));if(a+=e[e.length-1],f.CFF.readSubrs(b,a,g),g.CharStrings){a=g.CharStrings;e=[];a=f.CFF.readIndex(b,a,e);var m=[];for(c=0;c<e.length-1;c++)m.push(d.readBytes(b,a+e[c],e[c+1]-e[c]));g.CharStrings=m}if(g.ROS){a=g.FDArray;e=[];a=f.CFF.readIndex(b,a,e);g.FDArray=[];for(c=0;c<e.length-1;c++)m=f.CFF.readDict(b,a+e[c],a+e[c+1]),f.CFF._readFDict(b,m,k),g.FDArray.push(m);a=g.FDSelect;g.FDSelect=[];c=b[a];if(a++,3!=c)throw c;e=d.readUshort(b,a);a+=2;for(c=0;c<e+1;c++)g.FDSelect.push(d.readUshort(b,
a),b[a+2]),a+=3}return g.Encoding&&(g.Encoding=f.CFF.readEncoding(b,g.Encoding,g.CharStrings.length)),g.charset&&(g.charset=f.CFF.readCharset(b,g.charset,g.CharStrings.length)),f.CFF._readFDict(b,g,k),g};f.CFF._readFDict=function(b,a,c){var d,g;for(g in a.Private&&(d=a.Private[1],a.Private=f.CFF.readDict(b,d,d+a.Private[0]),a.Private.Subrs&&f.CFF.readSubrs(b,d+a.Private.Subrs,a.Private)),a)-1!="FamilyName FontName FullName Notice version Copyright".split(" ").indexOf(g)&&(a[g]=c[a[g]-426+35])};f.CFF.readSubrs=
function(b,a,c){var d=f._bin,g=[];a=f.CFF.readIndex(b,a,g);var k=g.length;c.Bias=1240>k?107:33900>k?1131:32768;c.Subrs=[];for(k=0;k<g.length-1;k++)c.Subrs.push(d.readBytes(b,a+g[k],g[k+1]-g[k]))};f.CFF.tableSE=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,
80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,0,111,112,113,114,0,115,116,117,118,119,120,121,122,0,123,0,124,125,126,127,128,129,130,131,0,132,133,0,134,135,136,137,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,138,0,139,0,0,0,0,140,141,142,143,0,0,0,0,0,144,0,0,0,145,0,0,146,147,148,149,0,0,0,0];f.CFF.glyphByUnicode=function(b,a){for(var c=0;c<b.charset.length;c++)if(b.charset[c]==a)return c;
return-1};f.CFF.glyphBySE=function(b,a){return 0>a||255<a?-1:f.CFF.glyphByUnicode(b,f.CFF.tableSE[a])};f.CFF.readEncoding=function(b,a,c){f._bin;c=[".notdef"];var d=b[a];if(a++,0!=d)throw"error: unknown encoding format: "+d;d=b[a];a++;for(var g=0;g<d;g++)c.push(b[a+g]);return c};f.CFF.readCharset=function(b,a,c){var d=f._bin,g=[".notdef"],k=b[a];if(a++,0==k)for(var e=0;e<c;e++){var m=d.readUshort(b,a);a+=2;g.push(m)}else{if(1!=k&&2!=k)throw"error: format: "+k;for(;g.length<c;){m=d.readUshort(b,a);
a+=2;var n=0;1==k?(n=b[a],a++):(n=d.readUshort(b,a),a+=2);for(e=0;e<=n;e++)g.push(m),m++}}return g};f.CFF.readIndex=function(b,a,c){var d=f._bin,g=d.readUshort(b,a)+1,k=b[a+=2];if(a++,1==k)for(var e=0;e<g;e++)c.push(b[a+e]);else if(2==k)for(e=0;e<g;e++)c.push(d.readUshort(b,a+2*e));else if(3==k)for(e=0;e<g;e++)c.push(16777215&d.readUint(b,a+3*e-1));else if(1!=g)throw"unsupported offset size: "+k+", count: "+g;return a+g*k-1};f.CFF.getCharString=function(b,a,c){var d=f._bin,g=b[a],k=b[a+1];b[a+2];
b[a+3];b[a+4];var e=1,m=null,n=null;20>=g&&(m=g,e=1);12==g&&(m=100*g+k,e=2);21<=g&&27>=g&&(m=g,e=1);28==g&&(n=d.readShort(b,a+1),e=3);29<=g&&31>=g&&(m=g,e=1);32<=g&&246>=g&&(n=g-139,e=1);247<=g&&250>=g&&(n=256*(g-247)+k+108,e=2);251<=g&&254>=g&&(n=256*-(g-251)-k-108,e=2);255==g&&(n=d.readInt(b,a+1)/65535,e=5);c.val=null!=n?n:"o"+m;c.size=e};f.CFF.readCharString=function(b,a,c){c=a+c;for(var d=f._bin,g=[];a<c;){var k=b[a],e=b[a+1];b[a+2];b[a+3];b[a+4];var m=1,n=null,h=null;20>=k&&(n=k,m=1);12==k&&
(n=100*k+e,m=2);19!=k&&20!=k||(n=k,m=2);21<=k&&27>=k&&(n=k,m=1);28==k&&(h=d.readShort(b,a+1),m=3);29<=k&&31>=k&&(n=k,m=1);32<=k&&246>=k&&(h=k-139,m=1);247<=k&&250>=k&&(h=256*(k-247)+e+108,m=2);251<=k&&254>=k&&(h=256*-(k-251)-e-108,m=2);255==k&&(h=d.readInt(b,a+1)/65535,m=5);g.push(null!=h?h:"o"+n);a+=m}return g};f.CFF.readDict=function(b,a,c){for(var d=f._bin,g={},k=[];a<c;){var e=b[a],m=b[a+1];b[a+2];b[a+3];b[a+4];var h=1,l=null,x=null;if(28==e&&(x=d.readShort(b,a+1),h=3),29==e&&(x=d.readInt(b,a+
1),h=5),32<=e&&246>=e&&(x=e-139,h=1),247<=e&&250>=e&&(x=256*(e-247)+m+108,h=2),251<=e&&254>=e&&(x=256*-(e-251)-m-108,h=2),255==e)throw d.readInt(b,a+1),"unknown number";if(30==e){x=[];for(h=1;;){var p=b[a+h];h++;var r=p>>4;p&=15;if(15!=r&&x.push(r),15!=p&&x.push(p),15==p)break}r="";p=[0,1,2,3,4,5,6,7,8,9,".","e","e-","reserved","-","endOfNumber"];for(var t=0;t<x.length;t++)r+=p[x[t]];x=parseFloat(r)}21>=e&&(l="version Notice FullName FamilyName Weight FontBBox BlueValues OtherBlues FamilyBlues FamilyOtherBlues StdHW StdVW escape UniqueID XUID charset Encoding CharStrings Private Subrs defaultWidthX nominalWidthX".split(" ")[e],
h=1,12==e)&&(l=["Copyright","isFixedPitch","ItalicAngle","UnderlinePosition","UnderlineThickness","PaintType","CharstringType","FontMatrix","StrokeWidth","BlueScale","BlueShift","BlueFuzz","StemSnapH","StemSnapV","ForceBold",0,0,"LanguageGroup","ExpansionFactor","initialRandomSeed","SyntheticBase","PostScript","BaseFontName","BaseFontBlend",0,0,0,0,0,0,"ROS","CIDFontVersion","CIDFontRevision","CIDFontType","CIDCount","UIDBase","FDArray","FDSelect","FontName"][m],h=2);null!=l?(g[l]=1==k.length?k[0]:
k,k=[]):k.push(x);a+=h}return g};f.cmap={};f.cmap.parse=function(b,a,c){b=new Uint8Array(b.buffer,a,c);a=0;c=f._bin;var d={};c.readUshort(b,a);a+=2;var g=c.readUshort(b,a);a+=2;var k=[];d.tables=[];for(var e=0;e<g;e++){var m=c.readUshort(b,a);a+=2;var h=c.readUshort(b,a);a+=2;var l=c.readUint(b,a);a+=4;var x="p"+m+"e"+h,p=k.indexOf(l);if(-1==p){var r;p=d.tables.length;k.push(l);var t=c.readUshort(b,l);0==t?r=f.cmap.parse0(b,l):4==t?r=f.cmap.parse4(b,l):6==t?r=f.cmap.parse6(b,l):12==t?r=f.cmap.parse12(b,
l):console.debug("unknown format: "+t,m,h,l);d.tables.push(r)}if(null!=d[x])throw"multiple tables for one platform+encoding";d[x]=p}return d};f.cmap.parse0=function(b,a){var c=f._bin,d={};d.format=c.readUshort(b,a);a+=2;var g=c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;d.map=[];for(c=0;c<g-6;c++)d.map.push(b[a+c]);return d};f.cmap.parse4=function(b,a){var c=f._bin,d=a,g={};g.format=c.readUshort(b,a);a+=2;var k=c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;var e=c.readUshort(b,a);a+=2;e/=2;g.searchRange=
c.readUshort(b,a);a+=2;g.entrySelector=c.readUshort(b,a);a+=2;g.rangeShift=c.readUshort(b,a);a+=2;g.endCount=c.readUshorts(b,a,e);a=a+2*e+2;g.startCount=c.readUshorts(b,a,e);a+=2*e;g.idDelta=[];for(var m=0;m<e;m++)g.idDelta.push(c.readShort(b,a)),a+=2;g.idRangeOffset=c.readUshorts(b,a,e);a+=2*e;for(g.glyphIdArray=[];a<d+k;)g.glyphIdArray.push(c.readUshort(b,a)),a+=2;return g};f.cmap.parse6=function(b,a){var c=f._bin,d={};d.format=c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=
2;d.firstCode=c.readUshort(b,a);a+=2;var g=c.readUshort(b,a);a+=2;d.glyphIdArray=[];for(var k=0;k<g;k++)d.glyphIdArray.push(c.readUshort(b,a)),a+=2;return d};f.cmap.parse12=function(b,a){var c=f._bin,d={};d.format=c.readUshort(b,a);a=a+2+2;c.readUint(b,a);a+=4;c.readUint(b,a);a+=4;var g=c.readUint(b,a);a+=4;d.groups=[];for(var k=0;k<g;k++){var e=a+12*k,m=c.readUint(b,e+0),h=c.readUint(b,e+4);e=c.readUint(b,e+8);d.groups.push([m,h,e])}return d};f.glyf={};f.glyf.parse=function(b,a,c,d){b=[];for(a=0;a<
d.maxp.numGlyphs;a++)b.push(null);return b};f.glyf._parseGlyf=function(b,a){var c=f._bin,d=b._data,g=f._tabOffset(d,"glyf",b._offset)+b.loca[a];if(b.loca[a]==b.loca[a+1])return null;b={};if(b.noc=c.readShort(d,g),g+=2,b.xMin=c.readShort(d,g),g+=2,b.yMin=c.readShort(d,g),g+=2,b.xMax=c.readShort(d,g),g+=2,b.yMax=c.readShort(d,g),g+=2,b.xMin>=b.xMax||b.yMin>=b.yMax)return null;if(0<b.noc){b.endPts=[];for(a=0;a<b.noc;a++)b.endPts.push(c.readUshort(d,g)),g+=2;a=c.readUshort(d,g);if(g+=2,d.length-g<a)return null;
b.instructions=c.readBytes(d,g,a);g+=a;var k=b.endPts[b.noc-1]+1;b.flags=[];for(a=0;a<k;a++){var e=d[g];if(g++,b.flags.push(e),0!=(8&e)){var m=d[g];g++;for(var h=0;h<m;h++)b.flags.push(e),a++}}b.xs=[];for(a=0;a<k;a++)e=0!=(2&b.flags[a]),m=0!=(16&b.flags[a]),e?(b.xs.push(m?d[g]:-d[g]),g++):m?b.xs.push(0):(b.xs.push(c.readShort(d,g)),g+=2);b.ys=[];for(a=0;a<k;a++)e=0!=(4&b.flags[a]),m=0!=(32&b.flags[a]),e?(b.ys.push(m?d[g]:-d[g]),g++):m?b.ys.push(0):(b.ys.push(c.readShort(d,g)),g+=2);for(a=g=d=0;a<
k;a++)d+=b.xs[a],g+=b.ys[a],b.xs[a]=d,b.ys[a]=g}else{b.parts=[];do a=c.readUshort(d,g),g+=2,k={m:{a:1,b:0,c:0,d:1,tx:0,ty:0},p1:-1,p2:-1},(b.parts.push(k),k.glyphIndex=c.readUshort(d,g),g+=2,1&a)?(e=c.readShort(d,g),g+=2,m=c.readShort(d,g),g+=2):(e=c.readInt8(d,g),g++,m=c.readInt8(d,g),g++),2&a?(k.m.tx=e,k.m.ty=m):(k.p1=e,k.p2=m),8&a?(k.m.a=k.m.d=c.readF2dot14(d,g),g+=2):64&a?(k.m.a=c.readF2dot14(d,g),g+=2,k.m.d=c.readF2dot14(d,g),g+=2):128&a&&(k.m.a=c.readF2dot14(d,g),g+=2,k.m.b=c.readF2dot14(d,
g),g+=2,k.m.c=c.readF2dot14(d,g),g+=2,k.m.d=c.readF2dot14(d,g),g+=2);while(32&a);if(256&a)for(c=c.readUshort(d,g),g+=2,b.instr=[],a=0;a<c;a++)b.instr.push(d[g]),g++}return b};f.GDEF={};f.GDEF.parse=function(b,a,c,d){c=f._bin.readUshort(b,a+4);return{glyphClassDef:0===c?null:f._lctf.readClassDef(b,a+c)}};f.GPOS={};f.GPOS.parse=function(b,a,c,d){return f._lctf.parse(b,a,c,d,f.GPOS.subt)};f.GPOS.subt=function(b,a,c,d){var g=f._bin,k=c,e={};if(e.fmt=g.readUshort(b,c),c+=2,1==a||2==a||3==a||7==a||8==a&&
2>=e.fmt){var m=g.readUshort(b,c);c+=2;e.coverage=f._lctf.readCoverage(b,m+k)}if(1==a&&1==e.fmt)d=g.readUshort(b,c),0!=d&&(e.pos=f.GPOS.readValueRecord(b,c+2,d));else if(2==a&&1<=e.fmt&&2>=e.fmt){d=g.readUshort(b,c);c+=2;a=g.readUshort(b,c);c+=2;m=f._lctf.numOfOnes(d);var h=f._lctf.numOfOnes(a);if(1==e.fmt){e.pairsets=[];var l=g.readUshort(b,c);c+=2;for(var x=0;x<l;x++){var p=k+g.readUshort(b,c);c+=2;var r=g.readUshort(b,p);p+=2;for(var t=[],v=0;v<r;v++){var w=g.readUshort(b,p);p+=2;0!=d&&(u=f.GPOS.readValueRecord(b,
p,d),p+=2*m);0!=a&&(G=f.GPOS.readValueRecord(b,p,a),p+=2*h);t.push({gid2:w,val1:u,val2:G})}e.pairsets.push(t)}}if(2==e.fmt)for(u=g.readUshort(b,c),c+=2,G=g.readUshort(b,c),c+=2,l=g.readUshort(b,c),c+=2,g=g.readUshort(b,c),c+=2,e.classDef1=f._lctf.readClassDef(b,k+u),e.classDef2=f._lctf.readClassDef(b,k+G),e.matrix=[],x=0;x<l;x++){k=[];for(v=0;v<g;v++){var u=null,G=null;0!=d&&(u=f.GPOS.readValueRecord(b,c,d),c+=2*m);0!=a&&(G=f.GPOS.readValueRecord(b,c,a),c+=2*h);k.push({val1:u,val2:G})}e.matrix.push(k)}}else if(4==
a&&1==e.fmt)e.markCoverage=f._lctf.readCoverage(b,g.readUshort(b,c)+k),e.baseCoverage=f._lctf.readCoverage(b,g.readUshort(b,c+2)+k),e.markClassCount=g.readUshort(b,c+4),e.markArray=f.GPOS.readMarkArray(b,g.readUshort(b,c+6)+k),e.baseArray=f.GPOS.readBaseArray(b,g.readUshort(b,c+8)+k,e.markClassCount);else if(6==a&&1==e.fmt)e.mark1Coverage=f._lctf.readCoverage(b,g.readUshort(b,c)+k),e.mark2Coverage=f._lctf.readCoverage(b,g.readUshort(b,c+2)+k),e.markClassCount=g.readUshort(b,c+4),e.mark1Array=f.GPOS.readMarkArray(b,
g.readUshort(b,c+6)+k),e.mark2Array=f.GPOS.readBaseArray(b,g.readUshort(b,c+8)+k,e.markClassCount);else{if(9==a&&1==e.fmt){e=g.readUshort(b,c);c=g.readUint(b,c+2);if(9==d.ltype)d.ltype=e;else if(d.ltype!=e)throw"invalid extension substitution";return f.GPOS.subt(b,d.ltype,k+c)}console.debug("unsupported GPOS table LookupType",a,"format",e.fmt)}return e};f.GPOS.readValueRecord=function(b,a,c){var d=f._bin,e=[];return e.push(1&c?d.readShort(b,a):0),a+=1&c?2:0,e.push(2&c?d.readShort(b,a):0),a+=2&c?2:
0,e.push(4&c?d.readShort(b,a):0),a+=4&c?2:0,e.push(8&c?d.readShort(b,a):0),e};f.GPOS.readBaseArray=function(b,a,c){var d=f._bin,e=[],k=a,h=d.readUshort(b,a);a+=2;for(var m=0;m<h;m++){for(var n=[],l=0;l<c;l++)n.push(f.GPOS.readAnchorRecord(b,k+d.readUshort(b,a))),a+=2;e.push(n)}return e};f.GPOS.readMarkArray=function(b,a){var c=f._bin,d=[],e=a,k=c.readUshort(b,a);a+=2;for(var h=0;h<k;h++){var m=f.GPOS.readAnchorRecord(b,c.readUshort(b,a+2)+e);m.markClass=c.readUshort(b,a);d.push(m);a+=4}return d};
f.GPOS.readAnchorRecord=function(b,a){var c=f._bin,d={};return d.fmt=c.readUshort(b,a),d.x=c.readShort(b,a+2),d.y=c.readShort(b,a+4),d};f.GSUB={};f.GSUB.parse=function(b,a,c,d){return f._lctf.parse(b,a,c,d,f.GSUB.subt)};f.GSUB.subt=function(b,a,c,d){var e=f._bin,k=c,h={};if(h.fmt=e.readUshort(b,c),c+=2,1!=a&&2!=a&&4!=a&&5!=a&&6!=a)return null;if(1==a||2==a||4==a||5==a&&2>=h.fmt||6==a&&2>=h.fmt){var m=e.readUshort(b,c);c+=2;h.coverage=f._lctf.readCoverage(b,k+m)}if(1==a&&1<=h.fmt&&2>=h.fmt)1==h.fmt?
h.delta=e.readShort(b,c):2==h.fmt&&(a=e.readUshort(b,c),h.newg=e.readUshorts(b,c+2,a));else if(2==a&&1==h.fmt)for(a=e.readUshort(b,c),c+=2,h.seqs=[],d=0;d<a;d++){m=e.readUshort(b,c)+k;c+=2;var n=e.readUshort(b,m);h.seqs.push(e.readUshorts(b,m+2,n))}else if(4==a)for(h.vals=[],a=e.readUshort(b,c),c+=2,d=0;d<a;d++)m=e.readUshort(b,c),c+=2,h.vals.push(f.GSUB.readLigatureSet(b,k+m));else if(5==a&&2==h.fmt){if(2==h.fmt)for(d=e.readUshort(b,c),c+=2,h.cDef=f._lctf.readClassDef(b,k+d),h.scset=[],a=e.readUshort(b,
c),c+=2,d=0;d<a;d++)m=e.readUshort(b,c),c+=2,h.scset.push(0==m?null:f.GSUB.readSubClassSet(b,k+m))}else if(6==a&&3==h.fmt){if(3==h.fmt){for(d=0;3>d;d++){a=e.readUshort(b,c);c+=2;m=[];for(n=0;n<a;n++)m.push(f._lctf.readCoverage(b,k+e.readUshort(b,c+2*n)));c+=2*a;0==d&&(h.backCvg=m);1==d&&(h.inptCvg=m);2==d&&(h.ahedCvg=m)}a=e.readUshort(b,c);h.lookupRec=f.GSUB.readSubstLookupRecords(b,c+2,a)}}else{if(7==a&&1==h.fmt){h=e.readUshort(b,c);c=e.readUint(b,c+2);if(9==d.ltype)d.ltype=h;else if(d.ltype!=h)throw"invalid extension substitution";
return f.GSUB.subt(b,d.ltype,k+c)}console.debug("unsupported GSUB table LookupType",a,"format",h.fmt)}return h};f.GSUB.readSubClassSet=function(b,a){var c=f._bin.readUshort,d=a,e=[],k=c(b,a);a+=2;for(var h=0;h<k;h++){var m=c(b,a);a+=2;e.push(f.GSUB.readSubClassRule(b,d+m))}return e};f.GSUB.readSubClassRule=function(b,a){var c=f._bin.readUshort,d={},e=c(b,a),k=c(b,a+=2);a+=2;d.input=[];for(var h=0;h<e-1;h++)d.input.push(c(b,a)),a+=2;return d.substLookupRecords=f.GSUB.readSubstLookupRecords(b,a,k),
d};f.GSUB.readSubstLookupRecords=function(b,a,c){for(var d=f._bin.readUshort,e=[],k=0;k<c;k++)e.push(d(b,a),d(b,a+2)),a+=4;return e};f.GSUB.readChainSubClassSet=function(b,a){var c=f._bin,d=a,e=[],k=c.readUshort(b,a);a+=2;for(var h=0;h<k;h++){var m=c.readUshort(b,a);a+=2;e.push(f.GSUB.readChainSubClassRule(b,d+m))}return e};f.GSUB.readChainSubClassRule=function(b,a){for(var c=f._bin,d={},e=["backtrack","input","lookahead"],k=0;k<e.length;k++){var h=c.readUshort(b,a);a+=2;1==k&&h--;d[e[k]]=c.readUshorts(b,
a,h);a+=2*d[e[k]].length}h=c.readUshort(b,a);return a+=2,d.subst=c.readUshorts(b,a,2*h),d};f.GSUB.readLigatureSet=function(b,a){var c=f._bin,d=a,e=[],k=c.readUshort(b,a);a+=2;for(var h=0;h<k;h++){var m=c.readUshort(b,a);a+=2;e.push(f.GSUB.readLigature(b,d+m))}return e};f.GSUB.readLigature=function(b,a){var c=f._bin,d={chain:[]};d.nglyph=c.readUshort(b,a);a+=2;var e=c.readUshort(b,a);a+=2;for(var k=0;k<e-1;k++)d.chain.push(c.readUshort(b,a)),a+=2;return d};f.head={};f.head.parse=function(b,a,c){c=
f._bin;var d={};return c.readFixed(b,a),a+=4,d.fontRevision=c.readFixed(b,a),a+=4,c.readUint(b,a),a+=4,c.readUint(b,a),a+=4,d.flags=c.readUshort(b,a),a+=2,d.unitsPerEm=c.readUshort(b,a),a+=2,d.created=c.readUint64(b,a),a+=8,d.modified=c.readUint64(b,a),a+=8,d.xMin=c.readShort(b,a),a+=2,d.yMin=c.readShort(b,a),a+=2,d.xMax=c.readShort(b,a),a+=2,d.yMax=c.readShort(b,a),a+=2,d.macStyle=c.readUshort(b,a),a+=2,d.lowestRecPPEM=c.readUshort(b,a),a+=2,d.fontDirectionHint=c.readShort(b,a),a+=2,d.indexToLocFormat=
c.readShort(b,a),a+=2,d.glyphDataFormat=c.readShort(b,a),d};f.hhea={};f.hhea.parse=function(b,a,c){c=f._bin;var d={};return c.readFixed(b,a),a+=4,d.ascender=c.readShort(b,a),a+=2,d.descender=c.readShort(b,a),a+=2,d.lineGap=c.readShort(b,a),a+=2,d.advanceWidthMax=c.readUshort(b,a),a+=2,d.minLeftSideBearing=c.readShort(b,a),a+=2,d.minRightSideBearing=c.readShort(b,a),a+=2,d.xMaxExtent=c.readShort(b,a),a+=2,d.caretSlopeRise=c.readShort(b,a),a+=2,d.caretSlopeRun=c.readShort(b,a),a+=2,d.caretOffset=c.readShort(b,
a),a+=2,a+=8,d.metricDataFormat=c.readShort(b,a),a+=2,d.numberOfHMetrics=c.readUshort(b,a),d};f.hmtx={};f.hmtx.parse=function(b,a,c,d){c=f._bin;for(var e={aWidth:[],lsBearing:[]},k=0,h=0,m=0;m<d.maxp.numGlyphs;m++)m<d.hhea.numberOfHMetrics&&(k=c.readUshort(b,a),a+=2,h=c.readShort(b,a),a+=2),e.aWidth.push(k),e.lsBearing.push(h);return e};f.kern={};f.kern.parse=function(b,a,c,d){var e=f._bin,k=e.readUshort(b,a);if(a+=2,1==k)return f.kern.parseV1(b,a-2,c,d);c=e.readUshort(b,a);a+=2;d={glyph1:[],rval:[]};
for(k=0;k<c;k++){a+=2;e.readUshort(b,a);a+=2;var h=e.readUshort(b,a);a+=2;h>>>=8;if(0!=(h&=15))throw"unknown kern table format: "+h;a=f.kern.readFormat0(b,a,d)}return d};f.kern.parseV1=function(b,a,c,d){c=f._bin;c.readFixed(b,a);a+=4;d=c.readUint(b,a);a+=4;for(var e={glyph1:[],rval:[]},k=0;k<d;k++){c.readUint(b,a);a+=4;var h=c.readUshort(b,a);a+=2;c.readUshort(b,a);a+=2;h>>>=8;if(0!=(h&=15))throw"unknown kern table format: "+h;a=f.kern.readFormat0(b,a,e)}return e};f.kern.readFormat0=function(b,a,
c){var d=f._bin,e=-1,k=d.readUshort(b,a);a+=2;d.readUshort(b,a);a+=2;d.readUshort(b,a);a+=2;d.readUshort(b,a);a+=2;for(var h=0;h<k;h++){var m=d.readUshort(b,a);a+=2;var n=d.readUshort(b,a);a+=2;var l=d.readShort(b,a);a+=2;m!=e&&(c.glyph1.push(m),c.rval.push({glyph2:[],vals:[]}));e=c.rval[c.rval.length-1];e.glyph2.push(n);e.vals.push(l);e=m}return a};f.loca={};f.loca.parse=function(b,a,c,d){c=f._bin;var e=[],k=d.head.indexToLocFormat;d=d.maxp.numGlyphs+1;if(0==k)for(var h=0;h<d;h++)e.push(c.readUshort(b,
a+(h<<1))<<1);if(1==k)for(h=0;h<d;h++)e.push(c.readUint(b,a+(h<<2)));return e};f.maxp={};f.maxp.parse=function(b,a,c){c=f._bin;var d={},e=c.readUint(b,a);return a+=4,d.numGlyphs=c.readUshort(b,a),a+=2,65536==e&&(d.maxPoints=c.readUshort(b,a),a+=2,d.maxContours=c.readUshort(b,a),a+=2,d.maxCompositePoints=c.readUshort(b,a),a+=2,d.maxCompositeContours=c.readUshort(b,a),a+=2,d.maxZones=c.readUshort(b,a),a+=2,d.maxTwilightPoints=c.readUshort(b,a),a+=2,d.maxStorage=c.readUshort(b,a),a+=2,d.maxFunctionDefs=
c.readUshort(b,a),a+=2,d.maxInstructionDefs=c.readUshort(b,a),a+=2,d.maxStackElements=c.readUshort(b,a),a+=2,d.maxSizeOfInstructions=c.readUshort(b,a),a+=2,d.maxComponentElements=c.readUshort(b,a),a+=2,d.maxComponentDepth=c.readUshort(b,a)),d};f.name={};f.name.parse=function(b,a,c){c=f._bin;var d={};c.readUshort(b,a);a+=2;var e=c.readUshort(b,a);a+=2;c.readUshort(b,a);for(var k,h="copyright fontFamily fontSubfamily ID fullName version postScriptName trademark manufacturer designer description urlVendor urlDesigner licence licenceURL --- typoFamilyName typoSubfamilyName compatibleFull sampleText postScriptCID wwsFamilyName wwsSubfamilyName lightPalette darkPalette".split(" "),
m=a+=2,n=0;n<e;n++){var l=c.readUshort(b,a);a+=2;var x=c.readUshort(b,a);a+=2;var p=c.readUshort(b,a);a+=2;var r=c.readUshort(b,a);a+=2;var t=c.readUshort(b,a);a+=2;var v=c.readUshort(b,a);a+=2;var w=h[r];v=m+12*e+v;if(0==l)t=c.readUnicode(b,v,t/2);else if(3==l&&0==x)t=c.readUnicode(b,v,t/2);else if(0==x)t=c.readASCII(b,v,t);else if(1==x)t=c.readUnicode(b,v,t/2);else if(3==x)t=c.readUnicode(b,v,t/2);else{if(1!=l)throw"unknown encoding "+x+", platformID: "+l;t=c.readASCII(b,v,t);console.debug("reading unknown MAC encoding "+
x+" as ASCII")}l="p"+l+","+p.toString(16);null==d[l]&&(d[l]={});d[l][void 0!==w?w:r]=t;d[l]._lang=p}for(var u in d)if(null!=d[u].postScriptName&&1033==d[u]._lang)return d[u];for(u in d)if(null!=d[u].postScriptName&&0==d[u]._lang)return d[u];for(u in d)if(null!=d[u].postScriptName&&3084==d[u]._lang)return d[u];for(u in d)if(null!=d[u].postScriptName)return d[u];for(u in d){k=u;break}return console.debug("returning name table with languageID "+d[k]._lang),d[k]};f["OS/2"]={};f["OS/2"].parse=function(b,
a,c){c=f._bin.readUshort(b,a);a+=2;var d={};if(0==c)f["OS/2"].version0(b,a,d);else if(1==c)f["OS/2"].version1(b,a,d);else if(2==c||3==c||4==c)f["OS/2"].version2(b,a,d);else{if(5!=c)throw"unknown OS/2 table version: "+c;f["OS/2"].version5(b,a,d)}return d};f["OS/2"].version0=function(b,a,c){var d=f._bin;return c.xAvgCharWidth=d.readShort(b,a),a+=2,c.usWeightClass=d.readUshort(b,a),a+=2,c.usWidthClass=d.readUshort(b,a),a+=2,c.fsType=d.readUshort(b,a),a+=2,c.ySubscriptXSize=d.readShort(b,a),a+=2,c.ySubscriptYSize=
d.readShort(b,a),a+=2,c.ySubscriptXOffset=d.readShort(b,a),a+=2,c.ySubscriptYOffset=d.readShort(b,a),a+=2,c.ySuperscriptXSize=d.readShort(b,a),a+=2,c.ySuperscriptYSize=d.readShort(b,a),a+=2,c.ySuperscriptXOffset=d.readShort(b,a),a+=2,c.ySuperscriptYOffset=d.readShort(b,a),a+=2,c.yStrikeoutSize=d.readShort(b,a),a+=2,c.yStrikeoutPosition=d.readShort(b,a),a+=2,c.sFamilyClass=d.readShort(b,a),a+=2,c.panose=d.readBytes(b,a,10),a+=10,c.ulUnicodeRange1=d.readUint(b,a),a+=4,c.ulUnicodeRange2=d.readUint(b,
a),a+=4,c.ulUnicodeRange3=d.readUint(b,a),a+=4,c.ulUnicodeRange4=d.readUint(b,a),a+=4,c.achVendID=[d.readInt8(b,a),d.readInt8(b,a+1),d.readInt8(b,a+2),d.readInt8(b,a+3)],a+=4,c.fsSelection=d.readUshort(b,a),a+=2,c.usFirstCharIndex=d.readUshort(b,a),a+=2,c.usLastCharIndex=d.readUshort(b,a),a+=2,c.sTypoAscender=d.readShort(b,a),a+=2,c.sTypoDescender=d.readShort(b,a),a+=2,c.sTypoLineGap=d.readShort(b,a),a+=2,c.usWinAscent=d.readUshort(b,a),a+=2,c.usWinDescent=d.readUshort(b,a),a+2};f["OS/2"].version1=
function(b,a,c){var d=f._bin;return a=f["OS/2"].version0(b,a,c),c.ulCodePageRange1=d.readUint(b,a),a+=4,c.ulCodePageRange2=d.readUint(b,a),a+4};f["OS/2"].version2=function(b,a,c){var d=f._bin;return a=f["OS/2"].version1(b,a,c),c.sxHeight=d.readShort(b,a),a+=2,c.sCapHeight=d.readShort(b,a),a+=2,c.usDefault=d.readUshort(b,a),a+=2,c.usBreak=d.readUshort(b,a),a+=2,c.usMaxContext=d.readUshort(b,a),a+2};f["OS/2"].version5=function(b,a,c){var d=f._bin;return a=f["OS/2"].version2(b,a,c),c.usLowerOpticalPointSize=
d.readUshort(b,a),a+=2,c.usUpperOpticalPointSize=d.readUshort(b,a),a+2};f.post={};f.post.parse=function(b,a,c){c=f._bin;var d={};return d.version=c.readFixed(b,a),a+=4,d.italicAngle=c.readFixed(b,a),a+=4,d.underlinePosition=c.readShort(b,a),a+=2,d.underlineThickness=c.readShort(b,a),d};null==f&&(f={});null==f.U&&(f.U={});f.U.codeToGlyph=function(b,a){b=b.cmap;var c=-1;if(null!=b.p0e4?c=b.p0e4:null!=b.p3e1?c=b.p3e1:null!=b.p1e0?c=b.p1e0:null!=b.p0e3&&(c=b.p0e3),-1==c)throw"no familiar platform and encoding!";
b=b.tables[c];if(0==b.format)return a>=b.map.length?0:b.map[a];if(4==b.format){var d=-1;for(c=0;c<b.endCount.length;c++)if(a<=b.endCount[c]){d=c;break}return-1==d||b.startCount[d]>a?0:65535&(0!=b.idRangeOffset[d]?b.glyphIdArray[a-b.startCount[d]+(b.idRangeOffset[d]>>1)-(b.idRangeOffset.length-d)]:a+b.idDelta[d])}if(12==b.format){if(a>b.groups[b.groups.length-1][1])return 0;for(c=0;c<b.groups.length;c++)if(d=b.groups[c],d[0]<=a&&a<=d[1])return d[2]+(a-d[0]);return 0}throw"unknown cmap table format "+
b.format;};f.U.glyphToPath=function(b,a){var c={cmds:[],crds:[]};if(b.SVG&&b.SVG.entries[a]){var d=b.SVG.entries[a];return null==d?c:("string"==typeof d&&(d=f.SVG.toPath(d),b.SVG.entries[a]=d),d)}if(b.CFF){d={x:0,y:0,stack:[],nStems:0,haveWidth:!1,width:b.CFF.Private?b.CFF.Private.defaultWidthX:0,open:!1};var e=b.CFF,k=b.CFF.Private;if(e.ROS){for(k=0;e.FDSelect[k+2]<=a;)k+=2;k=e.FDArray[e.FDSelect[k+1]].Private}f.U._drawCFF(b.CFF.CharStrings[a],d,e,k,c)}else b.glyf&&f.U._drawGlyf(a,b,c);return c};
f.U._drawGlyf=function(b,a,c){var d=a.glyf[b];null==d&&(d=a.glyf[b]=f.glyf._parseGlyf(a,b));null!=d&&(-1<d.noc?f.U._simpleGlyph(d,c):f.U._compoGlyph(d,a,c))};f.U._simpleGlyph=function(b,a){for(var c=0;c<b.noc;c++){for(var d=0==c?0:b.endPts[c-1]+1,e=b.endPts[c],k=d;k<=e;k++){var h=k==d?e:k-1,m=k==e?d:k+1,n=1&b.flags[k],l=1&b.flags[h],x=1&b.flags[m],p=b.xs[k],r=b.ys[k];if(k==d)if(n){if(!l){f.U.P.moveTo(a,p,r);continue}f.U.P.moveTo(a,b.xs[h],b.ys[h])}else l?f.U.P.moveTo(a,b.xs[h],b.ys[h]):f.U.P.moveTo(a,
(b.xs[h]+p)/2,(b.ys[h]+r)/2);n?l&&f.U.P.lineTo(a,p,r):x?f.U.P.qcurveTo(a,p,r,b.xs[m],b.ys[m]):f.U.P.qcurveTo(a,p,r,(p+b.xs[m])/2,(r+b.ys[m])/2)}f.U.P.closePath(a)}};f.U._compoGlyph=function(b,a,c){for(var d=0;d<b.parts.length;d++){var e={cmds:[],crds:[]},k=b.parts[d];f.U._drawGlyf(k.glyphIndex,a,e);k=k.m;for(var h=0;h<e.crds.length;h+=2){var m=e.crds[h],l=e.crds[h+1];c.crds.push(m*k.a+l*k.b+k.tx);c.crds.push(m*k.c+l*k.d+k.ty)}for(h=0;h<e.cmds.length;h++)c.cmds.push(e.cmds[h])}};f.U._getGlyphClass=
function(b,a){b=f._lctf.getInterval(a,b);return-1==b?0:a[b+2]};f.U._applySubs=function(b,a,c,d){for(var e=b.length-a-1,k=0;k<c.tabs.length;k++)if(null!=c.tabs[k]){var h,m=c.tabs[k];if(!m.coverage||-1!=(h=f._lctf.coverageIndex(m.coverage,b[a])))if(1==c.ltype)b[a],1==m.fmt?b[a]+=m.delta:b[a]=m.newg[h];else if(4==c.ltype)for(var l=m.vals[h],y=0;y<l.length;y++){var x=l[y];m=x.chain.length;if(!(m>e)){for(var p=!0,r=0,t=0;t<m;t++){for(;-1==b[a+r+(1+t)];)r++;x.chain[t]!=b[a+r+(1+t)]&&(p=!1)}if(p){b[a]=x.nglyph;
for(t=0;t<m+r;t++)b[a+t+1]=-1;break}}}else if(5==c.ltype&&2==m.fmt)for(l=f._lctf.getInterval(m.cDef,b[a]),r=m.scset[m.cDef[l+2]],x=0;x<r.length;x++){y=r[x];var v=y.input;if(!(v.length>e)){p=!0;for(t=0;t<v.length;t++){var w=f._lctf.getInterval(m.cDef,b[a+1+t]);if(-1==l&&m.cDef[w+2]!=v[t]){p=!1;break}}if(p)for(p=y.substLookupRecords,y=0;y<p.length;y+=2)p[y],p[y+1]}}else if(6==c.ltype&&3==m.fmt&&f.U._glsCovered(b,m.backCvg,a-m.backCvg.length)&&f.U._glsCovered(b,m.inptCvg,a)&&f.U._glsCovered(b,m.ahedCvg,
a+m.inptCvg.length))for(p=m.lookupRec,x=0;x<p.length;x+=2)l=p[x],f.U._applySubs(b,a+l,d[p[x+1]],d)}};f.U._glsCovered=function(b,a,c){for(var d=0;d<a.length;d++)if(-1==f._lctf.coverageIndex(a[d],b[c+d]))return!1;return!0};f.U.glyphsToPath=function(b,a,c){for(var d={cmds:[],crds:[]},e=0,k=0;k<a.length;k++){var h=a[k];if(-1!=h){for(var m=k<a.length-1&&-1!=a[k+1]?a[k+1]:0,l=f.U.glyphToPath(b,h),y=0;y<l.crds.length;y+=2)d.crds.push(l.crds[y]+e),d.crds.push(l.crds[y+1]);c&&d.cmds.push(c);for(y=0;y<l.cmds.length;y++)d.cmds.push(l.cmds[y]);
c&&d.cmds.push("X");e+=b.hmtx.aWidth[h];k<a.length-1&&(e+=f.U.getPairAdjustment(b,h,m))}}return d};f.U.P={};f.U.P.moveTo=function(b,a,c){b.cmds.push("M");b.crds.push(a,c)};f.U.P.lineTo=function(b,a,c){b.cmds.push("L");b.crds.push(a,c)};f.U.P.curveTo=function(b,a,c,e,f,k,h){b.cmds.push("C");b.crds.push(a,c,e,f,k,h)};f.U.P.qcurveTo=function(b,a,c,e,f){b.cmds.push("Q");b.crds.push(a,c,e,f)};f.U.P.closePath=function(b){b.cmds.push("Z")};f.U._drawCFF=function(b,a,c,e,g){for(var d=a.stack,h=a.nStems,m=
a.haveWidth,l=a.width,y=a.open,x=0,p=a.x,r=a.y,t=0,v=0,w=0,u=0,G=0,L=0,C=0,A=0,H=0,B=0,Z={val:0,size:0};x<b.length;){f.CFF.getCharString(b,x,Z);var D=Z.val;if(x+=Z.size,"o1"==D||"o18"==D)0!=d.length%2&&!m&&(l=d.shift()+e.nominalWidthX),h+=d.length>>1,d.length=0,m=!0;else if("o3"==D||"o23"==D)0!=d.length%2&&!m&&(l=d.shift()+e.nominalWidthX),h+=d.length>>1,d.length=0,m=!0;else if("o4"==D)1<d.length&&!m&&(l=d.shift()+e.nominalWidthX,m=!0),y&&f.U.P.closePath(g),r+=d.pop(),f.U.P.moveTo(g,p,r),y=!0;else if("o5"==
D)for(;0<d.length;)p+=d.shift(),r+=d.shift(),f.U.P.lineTo(g,p,r);else if("o6"==D||"o7"==D){var I=d.length,K="o6"==D;for(D=0;D<I;D++){var F=d.shift();K?p+=F:r+=F;K=!K;f.U.P.lineTo(g,p,r)}}else if("o8"==D||"o24"==D){I=d.length;for(K=0;K+6<=I;)t=p+d.shift(),v=r+d.shift(),w=t+d.shift(),u=v+d.shift(),p=w+d.shift(),r=u+d.shift(),f.U.P.curveTo(g,t,v,w,u,p,r),K+=6;"o24"==D&&(p+=d.shift(),r+=d.shift(),f.U.P.lineTo(g,p,r))}else{if("o11"==D)break;if("o1234"==D||"o1235"==D||"o1236"==D||"o1237"==D)"o1234"==D&&
(v=r,w=(t=p+d.shift())+d.shift(),B=u=v+d.shift(),L=u,A=r,p=(C=(G=(H=w+d.shift())+d.shift())+d.shift())+d.shift(),f.U.P.curveTo(g,t,v,w,u,H,B),f.U.P.curveTo(g,G,L,C,A,p,r)),"o1235"==D&&(t=p+d.shift(),v=r+d.shift(),w=t+d.shift(),u=v+d.shift(),H=w+d.shift(),B=u+d.shift(),G=H+d.shift(),L=B+d.shift(),C=G+d.shift(),A=L+d.shift(),p=C+d.shift(),r=A+d.shift(),d.shift(),f.U.P.curveTo(g,t,v,w,u,H,B),f.U.P.curveTo(g,G,L,C,A,p,r)),"o1236"==D&&(t=p+d.shift(),v=r+d.shift(),w=t+d.shift(),B=u=v+d.shift(),L=u,C=(G=
(H=w+d.shift())+d.shift())+d.shift(),A=L+d.shift(),p=C+d.shift(),f.U.P.curveTo(g,t,v,w,u,H,B),f.U.P.curveTo(g,G,L,C,A,p,r)),"o1237"==D&&(t=p+d.shift(),v=r+d.shift(),w=t+d.shift(),u=v+d.shift(),H=w+d.shift(),B=u+d.shift(),G=H+d.shift(),L=B+d.shift(),C=G+d.shift(),A=L+d.shift(),Math.abs(C-p)>Math.abs(A-r)?p=C+d.shift():r=A+d.shift(),f.U.P.curveTo(g,t,v,w,u,H,B),f.U.P.curveTo(g,G,L,C,A,p,r));else if("o14"==D){if(0<d.length&&!m&&(l=d.shift()+c.nominalWidthX,m=!0),4==d.length)I=d.shift(),K=d.shift(),F=
d.shift(),D=d.shift(),F=f.CFF.glyphBySE(c,F),D=f.CFF.glyphBySE(c,D),f.U._drawCFF(c.CharStrings[F],a,c,e,g),a.x=I,a.y=K,f.U._drawCFF(c.CharStrings[D],a,c,e,g);y&&(f.U.P.closePath(g),y=!1)}else if("o19"==D||"o20"==D)0!=d.length%2&&!m&&(l=d.shift()+e.nominalWidthX),h+=d.length>>1,d.length=0,m=!0,x+=h+7>>3;else if("o21"==D)2<d.length&&!m&&(l=d.shift()+e.nominalWidthX,m=!0),r+=d.pop(),p+=d.pop(),y&&f.U.P.closePath(g),f.U.P.moveTo(g,p,r),y=!0;else if("o22"==D)1<d.length&&!m&&(l=d.shift()+e.nominalWidthX,
m=!0),p+=d.pop(),y&&f.U.P.closePath(g),f.U.P.moveTo(g,p,r),y=!0;else if("o25"==D){for(;6<d.length;)p+=d.shift(),r+=d.shift(),f.U.P.lineTo(g,p,r);t=p+d.shift();v=r+d.shift();w=t+d.shift();u=v+d.shift();p=w+d.shift();r=u+d.shift();f.U.P.curveTo(g,t,v,w,u,p,r)}else if("o26"==D)for(d.length%2&&(p+=d.shift());0<d.length;)t=p,v=r+d.shift(),p=w=t+d.shift(),r=(u=v+d.shift())+d.shift(),f.U.P.curveTo(g,t,v,w,u,p,r);else if("o27"==D)for(d.length%2&&(r+=d.shift());0<d.length;)v=r,w=(t=p+d.shift())+d.shift(),
u=v+d.shift(),p=w+d.shift(),r=u,f.U.P.curveTo(g,t,v,w,u,p,r);else if("o10"==D||"o29"==D)I="o10"==D?e:c,0==d.length?console.debug("error: empty stack"):(K=d.pop(),I=I.Subrs[K+I.Bias],a.x=p,a.y=r,a.nStems=h,a.haveWidth=m,a.width=l,a.open=y,f.U._drawCFF(I,a,c,e,g),p=a.x,r=a.y,h=a.nStems,m=a.haveWidth,l=a.width,y=a.open);else if("o30"==D||"o31"==D)for(I=d.length,D=(K=0,"o31"==D),K+=I-(I&=-3);K<I;)D?(v=r,w=(t=p+d.shift())+d.shift(),r=(u=v+d.shift())+d.shift(),5==I-K?(p=w+d.shift(),K++):p=w,D=!1):(t=p,
v=r+d.shift(),w=t+d.shift(),u=v+d.shift(),p=w+d.shift(),5==I-K?(r=u+d.shift(),K++):r=u,D=!0),f.U.P.curveTo(g,t,v,w,u,p,r),K+=4;else{if("o"==(D+"").charAt(0))throw console.debug("Unknown operation: "+D,b),D;d.push(D)}}}a.x=p;a.y=r;a.nStems=h;a.haveWidth=m;a.width=l;a.open=y};var h=f,l={Typr:h};return e.Typr=h,e.default=l,Object.defineProperty(e,"__esModule",{value:!0}),e}({}).Typr},function(){return function(e){var f=Uint8Array,h=Uint16Array,l=Uint32Array,b=new f([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,
3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),a=new f([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),c=new f([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),d=function(a,b){for(var c=new h(31),d=0;31>d;++d)c[d]=b+=1<<a[d-1];a=new l(c[30]);for(d=1;30>d;++d)for(b=c[d];b<c[d+1];++b)a[b]=b-c[d]<<5|d;return[c,a]},g=d(b,2),k=g[0];g=g[1];k[28]=258;g[258]=28;var q=d(a,0)[0],m=new h(32768);for(d=0;32768>d;++d)g=(43690&d)>>>1|(21845&d)<<1,g=(61680&(g=(52428&g)>>>2|(13107&g)<<2))>>>4|(3855&g)<<
4,m[d]=((65280&g)>>>8|(255&g)<<8)>>>1;var n=function(a,b,c){for(var d=a.length,e=0,f=new h(b);e<d;++e)++f[a[e]-1];var g=new h(b);for(e=0;e<b;++e)g[e]=g[e-1]+f[e-1]<<1;if(c)for(c=new h(1<<b),f=15-b,e=0;e<d;++e){if(a[e]){var k=e<<4|a[e],l=b-a[e],t=g[a[e]-1]++<<l;for(l=t|(1<<l)-1;t<=l;++t)c[m[t]>>>f]=k}}else for(c=new h(d),e=0;e<d;++e)a[e]&&(c[e]=m[g[a[e]-1]++]>>>15-a[e]);return c};g=new f(288);for(d=0;144>d;++d)g[d]=8;for(d=144;256>d;++d)g[d]=9;for(d=256;280>d;++d)g[d]=7;for(d=280;288>d;++d)g[d]=8;
var y=new f(32);for(d=0;32>d;++d)y[d]=5;var x=n(g,9,1),p=n(y,5,1),r=function(a){for(var b=a[0],c=1;c<a.length;++c)a[c]>b&&(b=a[c]);return b},t=function(a,b,c){var d=b/8|0;return(a[d]|a[d+1]<<8)>>(7&b)&c},v=function(a,b){var c=b/8|0;return(a[c]|a[c+1]<<8|a[c+2]<<16)>>(7&b)},w=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long",
"stream finishing","invalid zip data"],u=function(a,b,c){b=Error(b||w[a]);if(b.code=a,Error.captureStackTrace&&Error.captureStackTrace(b,u),!c)throw b;return b},G=function(d,e,g){var m=d.length;if(!m||g&&!g.l&&5>m)return e||new f(0);var w=!e||g,y=!g||g.i;g||(g={});e||(e=new f(3*m));var A,L=function(a){var b=e.length;a>b&&(a=new f(Math.max(2*b,a)),a.set(e),e=a)},G=g.f||0,F=g.p||0,C=g.b||0,N=g.l,O=g.d,U=g.m,M=g.n,z=8*m;do{if(!N){g.f=G=t(d,F,1);var E=t(d,F+1,3);if(F+=3,!E){F=d[(V=((A=F)/8|0)+(7&A&&1)+
4)-4]|d[V-3]<<8;E=V+F;if(E>m){y&&u(0);break}w&&L(C+F);e.set(d.subarray(V,E),C);g.b=C+=F;g.p=F=8*E;continue}if(1==E)N=x,O=p,U=9,M=5;else if(2==E){U=t(d,F,31)+257;O=t(d,F+10,15)+4;N=U+t(d,F+5,31)+1;F+=14;M=new f(N);var P=new f(19);for(E=0;E<O;++E)P[c[E]]=t(d,F+3*E,7);F+=3*O;E=r(P);O=(1<<E)-1;var ta=n(P,E,1);for(E=0;E<N;){var V;V=ta[t(d,F,O)];if(F+=15&V,16>(V>>>=4))M[E++]=V;else{var fa=P=0;for(16==V?(fa=3+t(d,F,3),F+=2,P=M[E-1]):17==V?(fa=3+t(d,F,7),F+=3):18==V&&(fa=11+t(d,F,127),F+=7);fa--;)M[E++]=
P}}N=M.subarray(0,U);E=M.subarray(U);U=r(N);M=r(E);N=n(N,U,1);O=n(E,M,1)}else u(1);if(F>z){y&&u(0);break}}w&&L(C+131072);ta=(1<<U)-1;fa=(1<<M)-1;for(var ua=F;;ua=F){E=(P=N[v(d,F)&ta])>>>4;if((F+=15&P)>z){y&&u(0);break}if(P||u(2),256>E)e[C++]=E;else{if(256==E){ua=F;N=null;break}P=E-254;if(264<E){var aa=b[E-=257];P=t(d,F,(1<<aa)-1)+k[E];F+=aa}E=O[v(d,F)&fa];aa=E>>>4;E||u(3);F+=15&E;E=q[aa];3<aa&&(aa=a[aa],E+=v(d,F)&(1<<aa)-1,F+=aa);if(F>z){y&&u(0);break}w&&L(C+131072);for(P=C+P;C<P;C+=4)e[C]=e[C-E],
e[C+1]=e[C+1-E],e[C+2]=e[C+2-E],e[C+3]=e[C+3-E];C=P}}g.l=N;g.p=ua;g.b=C;N&&(G=1,g.m=U,g.d=O,g.n=M)}while(!G);return C==e.length?e:function(a,b,c){(null==b||0>b)&&(b=0);(null==c||c>a.length)&&(c=a.length);var d=new (a instanceof h?h:a instanceof l?l:f)(c-b);return d.set(a.subarray(b,c)),d}(e,0,C)};d=new f(0);g="undefined"!=typeof TextDecoder&&new TextDecoder;try{g.decode(d,{stream:!0}),1}catch(L){}return e.convert_streams=function(a){function b(){var a=f.getUint16(g);return g+=2,a}function c(){var a=
f.getUint32(g);return g+=4,a}function d(a){u.setUint16(y,a);y+=2}function e(a){u.setUint32(y,a);y+=4}var f=new DataView(a),g=0;c();var h=c();c();var k=b();b();c();b();b();c();c();c();c();c();for(var m=0;Math.pow(2,m)<=k;)m++;m--;for(var l=16*Math.pow(2,m),t=16*k-l,n=12,p=[],v=0;v<k;v++)p.push({tag:c(),offset:c(),compLength:c(),origLength:c(),origChecksum:c()}),n+=16;var w,r=new Uint8Array(12+16*p.length+p.reduce(function(a,b){return a+b.origLength+4},0));v=r.buffer;var u=new DataView(v),y=0;return e(h),
d(k),d(l),d(m),d(t),p.forEach(function(a){e(a.tag);e(a.origChecksum);e(n);e(a.origLength);a.outOffset=n;0!=(n+=a.origLength)%4&&(n+=4-n%4)}),p.forEach(function(b){var c=a.slice(b.offset,b.offset+b.compLength);if(b.compLength!=b.origLength){var d=new Uint8Array(b.origLength);c=new Uint8Array(c,2);G(c,d)}else d=new Uint8Array(c);r.set(d,b.outOffset);d=0;0!=(n=b.outOffset+b.origLength)%4&&(d=4-n%4);r.set((new Uint8Array(d)).buffer,b.outOffset+b.origLength);w=n+d}),v.slice(0,w)},Object.defineProperty(e,
"__esModule",{value:!0}),e}({}).convert_streams},function(e,f){function h(a){if(!q){let a={R:2,L:1,D:4,C:16,U:32,T:8};q=new Map;for(let b in k){let c=0;k[b].split(",").forEach(d=>{let [e,f]=d.split("+");e=parseInt(e,36);f=f?parseInt(f,36):0;q.set(c+=e,a[b]);for(d=f;d--;)q.set(++c,a[b])})}}return q.get(a)||32}function l(a,b){let c=[];for(let d=0;d<b.length;d++){let f=b.codePointAt(d);65535<f&&d++;c.push(e.U.codeToGlyph(a,f))}if(a=a.GSUB){let {lookupList:d,featureList:f}=a,g,k=/^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/,
l=[];f.forEach(a=>{if(k.test(a.tag))for(let k=0;k<a.tab.length;k++){if(l[a.tab[k]])continue;l[a.tab[k]]=!0;let t=d[a.tab[k]],n=/^(isol|init|fina|medi)$/.test(a.tag);if(n&&!g){{var f=b;let a=new Uint8Array(f.length),c=32,d=1,e=-1;for(let b=0;b<f.length;b++){let g=f.codePointAt(b),k=h(g)|0,m=1;k&8||(c&21?k&22?(m=3,1!==d&&3!==d||a[e]++):k&33&&(2===d||4===d)&&a[e]--:c&34&&(2===d||4===d)&&a[e]--,d=a[b]=m,c=k,e=b,65535<g&&b++)}g=a}}for(f=0;f<c.length;f++)g&&n&&m[g[f]]!==a.tag||e.U._applySubs(c,f,t,d)}})}return c}
function b(b,c){function d(a){for(let b=h-1;0<=b;b--)if(-1!==c[b]&&(!a||a(c[b])))return b;return-1}function f(c){return 1===a(b,c)}function g(a,b){for(let c=0;3>c;c++)k[3*b+c]+=a[c]||0}let k=new Int16Array(3*c.length),h=0;for(;h<c.length;h++){var m=c[h];if(-1!==m){k[3*h+2]=b.hmtx.aWidth[m];var l=b.GPOS;if(l){l=l.lookupList;for(var n=0;n<l.length;n++){var q=l[n];for(var y=0;y<q.tabs.length;y++){var A=q.tabs[y];if(1===q.ltype){if(-1!==e._lctf.coverageIndex(A.coverage,m)&&A.pos){g(A.pos,h);break}}else if(2===
q.ltype){var H=null,B=d();if(-1!==B){var z=e._lctf.coverageIndex(A.coverage,c[B]);if(-1!==z){if(1===A.fmt)for(A=A.pairsets[z],z=0;z<A.length;z++)A[z].gid2===m&&(H=A[z]);else 2===A.fmt&&(H=e.U._getGlyphClass(c[B],A.classDef1),z=e.U._getGlyphClass(m,A.classDef2),H=A.matrix[H][z]);if(H){H.val1&&g(H.val1,B);H.val2&&g(H.val2,h);break}}}}else if(4===q.ltype){if(z=e._lctf.coverageIndex(A.markCoverage,m),-1!==z&&(B=d(f),H=-1===B?-1:e._lctf.coverageIndex(A.baseCoverage,c[B]),-1!==H)){q=A.markArray[z];y=A.baseArray[H][q.markClass];
k[3*h]=y.x-q.x+k[3*B]-k[3*B+2];k[3*h+1]=y.y-q.y+k[3*B+1];break}}else if(6===q.ltype&&(H=e._lctf.coverageIndex(A.mark1Coverage,m),-1!==H&&(B=d(),-1!==B&&(z=c[B],3===a(b,z)&&(z=e._lctf.coverageIndex(A.mark2Coverage,z),-1!==z))))){q=A.mark1Array[H];y=A.mark2Array[z][q.markClass];k[3*h]=y.x-q.x+k[3*B]-k[3*B+2];k[3*h+1]=y.y-q.y+k[3*B+1];break}}}}else b.kern&&!b.cff&&(l=d(),-1!==l&&(n=b.kern.glyph1.indexOf(c[l]),-1!==n&&(m=b.kern.rval[n].glyph2.indexOf(m),-1!==m&&(k[3*l+2]+=b.kern.rval[n].vals[m]))))}}return k}
function a(a,b){return(a=a.GDEF&&a.GDEF.glyphClassDef)?e.U._getGlyphClass(b,a):0}function c(...a){for(let b=0;b<a.length;b++)if("number"===typeof a[b])return a[b]}function d(a){let d=Object.create(null),f=a["OS/2"],k=a.hhea,h=a.head.unitsPerEm,m=c(f&&f.sTypoAscender,k&&k.ascender,h),n={unitsPerEm:h,ascender:m,descender:c(f&&f.sTypoDescender,k&&k.descender,0),capHeight:c(f&&f.sCapHeight,m),xHeight:c(f&&f.sxHeight,m),lineGap:c(f&&f.sTypoLineGap,k&&k.lineGap),supportsCodePoint(b){return 0<e.U.codeToGlyph(a,
b)},forEachGlyph(c,f,k,h){let m=0;const t=1/n.unitsPerEm*f,p=l(a,c);let v=0;const r=b(a,p);p.forEach((b,l)=>{if(-1!==b){var n=d[b];if(!n){const {cmds:c,crds:f}=e.U.glyphToPath(a,b);n="";var p=0;for(let a=0,b=c.length;a<b;a++){var w=g[c[a]];n+=c[a];for(var q=1;q<=w;q++)n+=(1<q?",":"")+f[p++]}let k;if(f.length){p=w=Infinity;q=k=-Infinity;for(let a=0,b=f.length;a<b;a+=2){let b=f[a],c=f[a+1];b<p&&(p=b);c<w&&(w=c);b>q&&(q=b);c>k&&(k=c)}}else p=q=w=k=0;n=d[b]={index:b,advanceWidth:a.hmtx.aWidth[b],xMin:p,
yMin:w,xMax:q,yMax:k,path:n}}h.call(null,n,m+r[3*l]*t,r[3*l+1]*t,v);m+=r[3*l+2]*t;k&&(m+=k*f)}v+=65535<c.codePointAt(v)?2:1});return m}};return n}let g={M:2,L:2,Q:4,C:6,Z:0},k={C:"18g,ca,368,1kz",D:"17k,6,2,2+4,5+c,2+6,2+1,10+1,9+f,j+11,2+1,a,2,2+1,15+2,3,j+2,6+3,2+8,2,2,2+1,w+a,4+e,3+3,2,3+2,3+5,23+w,2f+4,3,2+9,2,b,2+3,3,1k+9,6+1,3+1,2+2,2+d,30g,p+y,1,1+1g,f+x,2,sd2+1d,jf3+4,f+3,2+4,2+2,b+3,42,2,4+2,2+1,2,3,t+1,9f+w,2,el+2,2+g,d+2,2l,2+1,5,3+1,2+1,2,3,6,16wm+1v",R:"17m+3,2,2,6+3,m,15+2,2+2,h+h,13,3+8,2,2,3+1,2,p+1,x,5+4,5,a,2,2,3,u,c+2,g+1,5,2+1,4+1,5j,6+1,2,b,2+2,f,2+1,1s+2,2,3+1,7,1ez0,2,2+1,4+4,b,4,3,b,42,2+2,4,3,2+1,2,o+3,ae,ep,x,2o+2,3+1,3,5+1,6",
L:"x9u,jff,a,fd,jv",T:"4t,gj+33,7o+4,1+1,7c+18,2,2+1,2+1,2,21+a,2,1b+k,h,2u+6,3+5,3+1,2+3,y,2,v+q,2k+a,1n+8,a,p+3,2+8,2+2,2+4,18+2,3c+e,2+v,1k,2,5+7,5,4+6,b+1,u,1n,5+3,9,l+1,r,3+1,1m,5+1,5+1,3+2,4,v+1,4,c+1,1m,5+4,2+1,5,l+1,n+5,2,1n,3,2+3,9,8+1,c+1,v,1q,d,1f,4,1m+2,6+2,2+3,8+1,c+1,u,1n,3,7,6+1,l+1,t+1,1m+1,5+3,9,l+1,u,21,8+2,2,2j,3+6,d+7,2r,3+8,c+5,23+1,s,2,2,1k+d,2+4,2+1,6+a,2+z,a,2v+3,2+5,2+1,3+1,q+1,5+2,h+3,e,3+1,7,g,jk+2,qb+2,u+2,u+1,v+1,1t+1,2+6,9,3+a,a,1a+2,3c+1,z,3b+2,5+1,a,7+2,64+1,3,1n,2+6,2,2,3+7,7+9,3,1d+d,1,1+1,1s+3,1d,2+4,2,6,15+8,d+1,x+3,3+1,2+2,1l,2+1,4,2+2,1n+7,3+1,49+2,2+c,2+6,5,7,4+1,5j+1l,2+4,ek,3+1,r+4,1e+4,6+5,2p+c,1+3,1,1+2,1+b,2db+2,3y,2p+v,ff+3,30+1,n9x,1+2,2+9,x+1,29+1,7l,4,5,q+1,6,48+1,r+h,e,13+7,q+a,1b+2,1d,3+3,3+1,14,1w+5,3+1,3+1,d,9,1c,1g,2+2,3+1,6+1,2,17+1,9,6n,3,5,fn5,ki+f,h+f,5s,6y+2,ea,6b,46+4,1af+2,2+1,6+3,15+2,5,4m+1,fy+3,as+1,4a+a,4x,1j+e,1l+2,1e+3,3+1,1y+2,11+4,2+7,1r,d+1,1h+8,b+3,3,2o+2,3,2+1,7,4h,4+7,m+1,1m+1,4,12+6,4+4,5g+7,3+2,2,o,2d+5,2,5+1,2+1,6n+3,7+1,2+1,s+1,2e+7,3,2+1,2z,2,3+5,2,2u+2,3+3,2+4,78+8,2+1,75+1,2,5,41+3,3+1,5,x+9,15+5,3+3,9,a+5,3+2,1b+c,2+1,bb+6,2+5,2,2b+l,3+6,2+1,2+1,3f+5,4,2+1,2+6,2,21+1,4,2,9o+1,470+8,at4+4,1o+6,t5,1s+3,2a,f5l+1,2+3,43o+2,a+7,1+7,3+6,v+3,45+2,1j0+1i,5+1d,9,f,n+4,2+e,11t+6,2+g,3+6,2+1,2+4,7a+6,c6+3,15t+6,32+6,1,gzau,v+2n,3l+6n"},
q,m=[null,"isol","init","fina","medi"];return function(a){var b=new Uint8Array(a,0,4);b=e._bin.readASCII(b,0,4);if("wOFF"===b)a=f(a);else if("wOF2"===b)throw Error("woff2 fonts not supported");return d(e.parse(a)[0])}}],init(e,f,h){e=e();f=f();return h(e,f)}});R=ba.defineWorkerModule({name:"FontResolver",dependencies:[function(e,f){function h(a,b){let c=b=>{console.error(`Failure loading font ${a}`,b)};try{let d=new XMLHttpRequest;d.open("get",a,!0);d.responseType="arraybuffer";d.onload=function(){if(400<=
d.status)c(Error(d.statusText));else if(0<d.status)try{let c=e(d.response);c.src=a;b(c)}catch(q){c(q)}};d.onerror=c;d.send()}catch(k){c(k)}}function l(c,d){let e=b[c];e?d(e):a[c]?a[c].push(d):(a[c]=[d],h(c,d=>{d.src=c;b[c]=d;a[c].forEach(a=>a(d));delete a[c]}))}let b=Object.create(null),a=Object.create(null);return function(a,d,{lang:e,fonts:k=[],style:h="normal",weight:m="normal",unicodeFontsURL:n}={}){function c(){if(w.length){let b=w.map(b=>a.substring(b[0],b[1]+1)).join("\n");f.getFontsForString(b,
{lang:e||void 0,style:h,weight:m,dataUrl:n}).then(({fontUrls:a,chars:b})=>{let c=t.length,d=0;w.forEach(a=>{for(let e=0,f=a[1]-a[0];e<=f;e++)r[a[0]+e]=b[d++]+c;d++});let e=0;a.forEach((b,d)=>{l(b,b=>{t[d+c]=b;++e===a.length&&g()})})})}else g()}function g(){d({chars:r,fonts:t})}function p(a,b){for(let c=0;c<b.length;c++){let [d,e=d]=b[c];if(d<=a&&a<=e)return!0}return!1}let r=new Uint8Array(a.length),t=[];a.length||g();let v=new Map,w=[];"italic"!==h&&(h="normal");"number"!==typeof m&&(m="bold"===m?
700:400);k&&!Array.isArray(k)&&(k=[k]);k=k.slice().filter(a=>!a.lang||a.lang.test(e)).reverse();if(k.length){let d=0;(function C(e=0){for(let c=e,f=a.length;c<f;c++){e=a.codePointAt(c);if(1===d&&t[r[c-1]].supportsCodePoint(e)||0<c&&/\s/.test(a[c]))r[c]=r[c-1],2===d&&(w[w.length-1][1]=c);else for(let a=r[c],f=k.length;a<=f;a++)if(a===f)(2===d?w[w.length-1]:w[w.length]=[c,c])[1]=c,d=2;else{r[c]=a;let {src:f,unicodeRange:g}=k[a];if(!g||p(e,g)){let a=b[f];if(!a){l(f,()=>{C(c)});return}if(a.supportsCodePoint(e)){let b=
v.get(a);"number"!==typeof b&&(b=t.length,t.push(a),v.set(a,b));r[c]=b;d=1;break}}}65535<e&&c+1<f&&(r[c+1]=r[c],c++,2===d&&(w[w.length-1][1]=c))}c()})()}else w.push([0,a.length-1]),c()}},R,function(){return function(e){function f(a){var b=(a&m).toString(16),c=((a&m)+k-1).toString(16);return"codepoint-index/plane"+(a>>16)+"/"+b+"-"+c+".json"}function h(a,b){a&=q;return 0!=((b.codePointAt(a/6|0)||48)-48&1<<a%6)}function l(a,b){a.replace(/U\+/gi,"").replace(/^,+|,+$/g,"").split(/,+/).map(function(a){return a.split("-").map(function(a){return parseInt(a.trim(),
16)})}).forEach(function(a){var c=a[0];a=a[1];void 0===a&&(a=c);b(c,a)})}function b(a,b){l(a,function(a,c){for(;a<=c;a++)b(a)})}function a(a){var c=x.get(a);return c||(c=new g,b(a.ranges,function(a){return c.add(a)}),x.set(a,c)),c}function c(a,b,c){if(a[b])a=b;else if(a[c])a=c;else a:{for(var d in a){a=d;break a}a=void 0}return a}function d(a){return p||(p=new Set,b("9-D,20,85,A0,1680,2000-200A,2028-202F,205F,3000",function(a){p.add(a)})),p.has(a)}var g=function(){this.buckets=new Map};g.prototype.add=
function(a){var b=a>>5;this.buckets.set(b,(this.buckets.get(b)||0)|1<<(31&a))};g.prototype.has=function(a){var b=this.buckets.get(a>>5);return void 0!==b&&0!=(b&1<<(31&a))};g.prototype.serialize=function(){var a=[];return this.buckets.forEach(function(b,c){a.push((+c).toString(36)+":"+b.toString(36))}),a.join(",")};g.prototype.deserialize=function(a){var b=this;this.buckets.clear();a.split(",").forEach(function(a){a=a.split(":");b.buckets.set(parseInt(a[0],36),parseInt(a[1],36))})};var k=Math.pow(2,
8),q=k-1,m=~q,n={},y={},x=new WeakMap,p,r=new Map;return e.CodePointSet=g,e.clearCache=function(){n={};y={}},e.getFontsForString=function(b,e){function g(a){var b=r.get(a);return b||(b=fetch(t+"/"+a).then(function(a){if(!a.ok)throw Error(a.statusText);return a.json().then(function(a){if(!Array.isArray(a)||1!==a[0])throw Error("Incorrect schema version; need 1, got "+a[0]);return a[1]})}).catch(function(b){if("https://cdn.jsdelivr.net/gh/lojjic/unicode-font-resolver@v1.0.1/packages/data"!==t)return F||
(console.error('unicode-font-resolver: Failed loading from dataUrl "'+t+'", trying default CDN. '+b.message),F=!0),t="https://cdn.jsdelivr.net/gh/lojjic/unicode-font-resolver@v1.0.1/packages/data",r.delete(a),g(a);throw b;}),r.set(a,b)),b}void 0===e&&(e={});var k=e.lang;void 0===k&&(k=/\p{Script=Hangul}/u.test(b)?"ko":/\p{Script=Hiragana}|\p{Script=Katakana}/u.test(b)?"ja":"en");var m=e.category;void 0===m&&(m="sans-serif");var l=e.style;void 0===l&&(l="normal");var p=e.weight;void 0===p&&(p=400);
var t=(e.dataUrl||"https://cdn.jsdelivr.net/gh/lojjic/unicode-font-resolver@v1.0.1/packages/data").replace(/\/$/g,""),q=new Map,v=new Uint8Array(b.length),x={},z={},I=Array(b.length),K=new Map,F=!1;e=function(a){var c=b.codePointAt(a),d=f(c);I[a]=d;n[d]||K.has(d)||K.set(d,g(d).then(function(a){n[d]=a}));65535<c&&(a++,J=a)};for(var J=0;J<b.length;J++)e(J);return Promise.all(K.values()).then(function(){K.clear();for(var a=function(a){var d=b.codePointAt(a),e=null,f=n[I[a]],m=void 0,l;for(l in f){var p=
z[l];if(void 0===p&&(p=z[l]=(new RegExp(l)).test(k||"en")),p){for(var t in m=l,f[l])if(h(d,f[l][t])){e=t;break}break}}if(!e){var r;a:for(r in f)if(r!==m)for(var q in f[r])if(h(d,f[r][q])){e=q;break a}}e||(console.debug("No font coverage for U+"+d.toString(16)),e="latin");I[a]=e;y[e]||K.has(e)||K.set(e,g("font-meta/"+e+".json").then(function(a){y[e]=a}));65535<d&&(a++,c=a)},c=0;c<b.length;c++)a(c);return Promise.all(K.values())}).then(function(){for(var e,f=null,g=0;g<b.length;g++){var k=b.codePointAt(g);
if(f&&(d(k)||a(f).has(k)))v[g]=v[g-1];else{f=y[I[g]];var h=x[f.id];if(!h){var n=f.typeforms;h=c(n,m,"sans-serif");var r=c(n[h],l,"normal");n=null===(e=n[h])||void 0===e?void 0:e[r];var w=p,u=w;if(!n.includes(u)){u=1/0;for(var z=0;z<n.length;z++)Math.abs(n[z]-w)<Math.abs(u-w)&&(u=n[z])}h=x[f.id]=t+"/font-files/"+f.id+"/"+h+"."+r+"."+u+".woff"}r=q.get(h);null==r&&(r=q.size,q.set(h,r));v[g]=r}65535<k&&(g++,v[g]=v[g-1])}return{fontUrls:Array.from(q.keys()),chars:v}})},Object.defineProperty(e,"__esModule",
{value:!0}),e}({})}],init(e,f,h){return e(f,h())}});let S=()=>(self.performance||Date).now(),la=Ca["default"](),ya,ka=[],pa=0,Wa=(...e)=>new Promise((f,h)=>{ka.push(()=>{const l=S();try{la.webgl.generateIntoCanvas(...e),f({timing:S()-l})}catch(b){h(b)}});pa||(pa=setTimeout(Aa,0))}),Ba={},Xa=0,ab=la.webglUtils.resizeWebGLCanvasWithoutClearing,Q={defaultFontURL:null,unicodeFontsURL:null,sdfGlyphSize:64,sdfMargin:.0625,sdfExponent:9,textureWidth:2048,useWorker:!0},Ya=new z.Color,Da=!1,ma=Object.create(null),
na;X=ba.defineWorkerModule({name:"Typesetter",dependencies:[function(e,f){function h({text:a,lang:b,fonts:c,style:d,weight:f,preResolvedFonts:g,unicodeFontsURL:h},k){let m=({chars:a,fonts:b})=>{let c,d;const e=[];for(let f=0;f<a.length;f++)a[f]!==d?(d=a[f],e.push(c={start:f,end:f,fontObj:b[a[f]]})):c.end=f;k(e)};g?m(g):e(a,m,{lang:b,fonts:c,style:d,weight:f,unicodeFontsURL:h})}function l({text:e="",font:l,lang:q,fontSize:x=400,fontWeight:p=1,fontStyle:r="normal",letterSpacing:t=0,lineHeight:v="normal",
maxWidth:w=Infinity,direction:u,textAlign:z="left",textIndent:J=0,whiteSpace:C="normal",overflowWrap:A="normal",anchorX:H=0,anchorY:B=0,metricsOnly:Z=!1,unicodeFontsURL:D,preResolvedFonts:I=null,includeCaretPositions:K=!1,chunkedBoundsSize:F=8192,colorRanges:Q=null},N){let m=c(),n={fontLoad:0,typesetting:0};-1<e.indexOf("\r")&&(console.info("Typesetter: got text with \\r chars; normalizing to \\n"),e=e.replace(/\r\n/g,"\n").replace(/\r/g,"\n"));x=+x;t=+t;w=+w;v=v||"normal";J=+J;h({text:e,lang:q,style:r,
weight:p,fonts:"string"===typeof l?[{src:l}]:l,unicodeFontsURL:D,preResolvedFonts:I},h=>{n.fontLoad=c()-m;let l=isFinite(w),p=null,r=null,q=null,y=null,D=null,G=null,I=null,L=null,M=0,O=0,U="nowrap"!==C,S=new Map,ba=c(),R=J,X=0,T=new d,W=[T];h.forEach(a=>{let {fontObj:b}=a,{ascender:c,descender:f,unitsPerEm:h,lineGap:m,capHeight:n,xHeight:p}=b,r=S.get(b);if(!r){var q=x/h;let a="normal"===v?(c-f+m)*q:v*x,d=Math.min(a,(c-f)*q),e=(c+f)/2*q+d/2;r={index:S.size,src:b.src,fontObj:b,fontSizeMult:q,unitsPerEm:h,
ascender:c*q,descender:f*q,capHeight:n*q,xHeight:p*q,lineHeight:a,baseline:-((a-(c-f)*q)/2)-c*q,caretTop:e,caretBottom:e-d};S.set(b,r)}let {fontSizeMult:y}=r;q=e.slice(a.start,a.end+1);let u,z;b.forEachGlyph(q,x,t,(b,c,f,h)=>{c+=X;h+=a.start;u=c;z=b;let m=e.charAt(h),n=b.advanceWidth*y;var p=T.count;"isEmpty"in b||(b.isWhitespace=!!m&&/[^\S\u00A0]/.test(m),b.canBreakAfter=!!m&&k.test(m),b.isEmpty=b.xMin===b.xMax||b.yMin===b.yMax||g.test(m));b.isWhitespace||b.isEmpty||O++;if(U&&l&&!b.isWhitespace&&
c+n+R>w&&p){if(T.glyphAt(p-1).glyphObj.canBreakAfter){var q=new d;R=-c}else for(;p--;)if(0===p&&"break-word"===A){q=new d;R=-c;break}else if(T.glyphAt(p).glyphObj.canBreakAfter){q=T.splitAt(p+1);p=q.glyphAt(0).x;R-=p;for(let a=q.count;a--;)q.glyphAt(a).x-=p;break}q&&(T.isSoftWrapped=!0,T=q,W.push(T),M=w)}q=T.glyphAt(T.count);q.glyphObj=b;q.x=c+R;q.y=f;q.width=n;q.charIndex=h;q.fontData=r;"\n"===m&&(T=new d,W.push(T),R=-(c+n+t*x)+J)});X=u+z.advanceWidth*y+t*x});let ca=0;W.forEach(a=>{let b=!0;for(let d=
a.count;d--;){var c=a.glyphAt(d);b&&!c.glyphObj.isWhitespace&&(a.width=c.x+c.width,a.width>M&&(M=a.width),b=!1);let {lineHeight:e,capHeight:f,xHeight:g,baseline:h}=c.fontData;e>a.lineHeight&&(a.lineHeight=e);c=h-a.baseline;0>c&&(a.baseline+=c,a.cap+=c,a.ex+=c);a.cap=Math.max(a.cap,a.baseline+f);a.ex=Math.max(a.ex,a.baseline+g)}a.baseline-=ca;a.cap-=ca;a.ex-=ca;ca+=a.lineHeight});let ea=0,da=0;H&&("number"===typeof H?ea=-H:"string"===typeof H&&(ea=-M*("left"===H?0:"center"===H?.5:"right"===H?1:b(H))));
B&&("number"===typeof B?da=-B:"string"===typeof B&&(da="top"===B?0:"top-baseline"===B?-W[0].baseline:"top-cap"===B?-W[0].cap:"top-ex"===B?-W[0].ex:"middle"===B?ca/2:"bottom"===B?ca:"bottom-baseline"===B?-W[W.length-1].baseline:b(B)*ca));if(!Z){let b=f.getEmbeddingLevels(e,u);p=new Uint16Array(O);r=new Uint8Array(O);q=new Float32Array(2*O);y={};I=[Infinity,Infinity,-Infinity,-Infinity];L=[];K&&(G=new Float32Array(4*e.length));Q&&(D=new Uint8Array(3*O));let c=0,d=-1,g=-1,k,m;W.forEach((h,l)=>{let {count:n,
width:t}=h;if(0<n){l=0;for(var w=n;w--&&h.glyphAt(w).glyphObj.isWhitespace;)l++;var v=w=0;if("center"===z)w=(M-t)/2;else if("right"===z)w=M-t;else if("justify"===z&&h.isSoftWrapped){v=0;for(var u=n-l;u--;)h.glyphAt(u).glyphObj.isWhitespace&&v++;v=(M-t)/v}if(v||w){u=0;for(var x=0;x<n;x++){var A=h.glyphAt(x),B=A.glyphObj;A.x+=w+u;0!==v&&B.isWhitespace&&x<n-l&&(u+=v,A.width+=v)}}w=f.getReorderSegments(e,b,h.glyphAt(0).charIndex,h.glyphAt(h.count-1).charIndex);for(v=0;v<w.length;v++){let [a,b]=w[v];u=
Infinity;x=-Infinity;for(A=0;A<n;A++)if(h.glyphAt(A).charIndex>=a){for(B=A;A<n;A++){var C=h.glyphAt(A);if(C.charIndex>b)break;A<n-l&&(u=Math.min(u,C.x),x=Math.max(x,C.x+C.width))}for(;B<A;B++)C=h.glyphAt(B),C.x=x-(C.x+C.width-u);break}}let E;l=a=>E=a;for(w=0;w<n;w++){u=h.glyphAt(w);E=u.glyphObj;v=E.index;(x=b.levels[u.charIndex]&1)&&(A=f.getMirroredCharacter(e[u.charIndex]))&&u.fontData.fontObj.forEachGlyph(A,0,0,l);if(K){let {charIndex:b,fontData:c}=u;A=u.x+ea;B=u.x+u.width+ea;G[4*b]=x?B:A;G[4*b+
1]=x?A:B;G[4*b+2]=h.baseline+c.caretBottom+da;G[4*b+3]=h.baseline+c.caretTop+da;x=b-d;1<x&&a(G,d,x);d=b}if(Q)for({charIndex:x}=u;x>g;)g++,Q.hasOwnProperty(g)&&(m=Q[g]);if(!E.isWhitespace&&!E.isEmpty){x=c++;let {fontSizeMult:a,src:b,index:d}=u.fontData;A=y[b]||(y[b]={});A[v]||(A[v]={path:E.path,pathBounds:[E.xMin,E.yMin,E.xMax,E.yMax]});B=u.x+ea;C=u.y+h.baseline+da;q[2*x]=B;q[2*x+1]=C;u=B+E.xMin*a;A=C+E.yMin*a;B+=E.xMax*a;C+=E.yMax*a;u<I[0]&&(I[0]=u);A<I[1]&&(I[1]=A);B>I[2]&&(I[2]=B);C>I[3]&&(I[3]=
C);0===x%F&&(k={start:x,end:x,rect:[Infinity,Infinity,-Infinity,-Infinity]},L.push(k));k.end++;let e=k.rect;u<e[0]&&(e[0]=u);A<e[1]&&(e[1]=A);B>e[2]&&(e[2]=B);C>e[3]&&(e[3]=C);p[x]=v;r[x]=d;Q&&(v=3*x,D[v]=m>>16&255,D[v+1]=m>>8&255,D[v+2]=m&255)}}}});G&&(h=e.length-d,1<h&&a(G,d,h))}let Y=[];S.forEach(({index:a,src:b,unitsPerEm:c,ascender:d,descender:e,lineHeight:f,capHeight:g,xHeight:h})=>{Y[a]={src:b,unitsPerEm:c,ascender:d,descender:e,lineHeight:f,capHeight:g,xHeight:h}});n.typesetting=c()-ba;N({glyphIds:p,
glyphFontIndices:r,glyphPositions:q,glyphData:y,fontData:Y,caretPositions:G,glyphColors:D,chunkedBounds:L,fontSize:x,topBaseline:da+W[0].baseline,blockBounds:[ea,da-ca,ea+M,da],visibleBounds:I,timings:n})})}function b(a){a=(a=a.match(/^([\d.]+)%$/))?parseFloat(a[1]):NaN;return isNaN(a)?0:a/100}function a(a,b,c){let d=a[4*b],e=a[4*b+2],f=a[4*b+3],g=(a[4*b+1]-d)/c;for(let h=0;h<c;h++){let c=4*(b+h);a[c]=d+g*h;a[c+1]=d+g*(h+1);a[c+2]=e;a[c+3]=f}}function c(){return(self.performance||Date).now()}function d(){this.data=
[]}let g=/[\u00AD\u034F\u061C\u115F-\u1160\u17B4-\u17B5\u180B-\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8]/,k=/[^\S\u00A0]|[\-\u007C\u00AD\u2010\u2012-\u2014\u2027\u2056\u2E17\u2E40]/,q="glyphObj x y width charIndex fontData".split(" ");d.prototype={width:0,lineHeight:0,baseline:0,cap:0,ex:0,isSoftWrapped:!1,get count(){return Math.ceil(this.data.length/q.length)},glyphAt(a){let b=d.flyweight;b.data=this.data;b.index=a;return b},splitAt(a){let b=new d;
b.data=this.data.splice(a*q.length);return b}};d.flyweight=q.reduce((a,b,c,d)=>{Object.defineProperty(a,b,{get(){return this.data[this.index*q.length+c]},set(a){this.data[this.index*q.length+c]=a}});return a},{data:null,index:0});return{typeset:l,measure:function(a,b){l({...a,metricsOnly:!0},a=>{let [c,d,e,f]=a.blockBounds;b({width:e-c,height:f-d})})}}},R,X["default"]],init(e,f,h){return e(f,h())}});let Ga=ba.defineWorkerModule({name:"Typesetter",dependencies:[X],init(e){return function(f){return new Promise(h=>
{e.typeset(f,h)})}},getTransferables(e){const f=[];for(let h in e)e[h]&&e[h].buffer&&f.push(e[h].buffer);return f}}),$a=Ga.onMainThread,Ja={};class Ma extends z.InstancedBufferGeometry{constructor(){super();this.detail=1;this.curveRadius=0;this.groups=[{start:0,count:Infinity,materialIndex:0},{start:0,count:Infinity,materialIndex:1}];this.boundingSphere=new z.Sphere;this.boundingBox=new z.Box3}computeBoundingSphere(){}computeBoundingBox(){}set detail(e){if(e!==this._detail){this._detail=e;if("number"!==
typeof e||1>e)e=1;let f=bb(e);["position","normal","uv"].forEach(e=>{this.attributes[e]=f.attributes[e].clone()});this.setIndex(f.getIndex().clone())}}get detail(){return this._detail}set curveRadius(e){e!==this._curveRadius&&(this._curveRadius=e,this._updateBounds())}get curveRadius(){return this._curveRadius}updateGlyphs(e,f,h,l,b){this.updateAttributeData("aTroikaGlyphBounds",e,4);this.updateAttributeData("aTroikaGlyphIndex",f,1);this.updateAttributeData("aTroikaGlyphColor",b,3);this._blockBounds=
h;this._chunkedBounds=l;this.instanceCount=f.length;this._updateBounds()}_updateBounds(){let e=this._blockBounds;if(e){let {curveRadius:l,boundingBox:b}=this;if(l){let {PI:a,floor:c,min:d,max:g,sin:k,cos:q}=Math;var f=a/2,h=2*a;let m=Math.abs(l),n=e[0]/m,y=e[2]/m,x=c((n+f)/h)!==c((y+f)/h)?-m:d(k(n)*m,k(y)*m);f=c((n-f)/h)!==c((y-f)/h)?m:g(k(n)*m,k(y)*m);h=c((n+a)/h)!==c((y+a)/h)?2*m:g(m-q(n)*m,m-q(y)*m);b.min.set(x,e[1],0>l?-h:0);b.max.set(f,e[3],0>l?0:h)}else b.min.set(e[0],e[1],0),b.max.set(e[2],
e[3],0);b.getBoundingSphere(this.boundingSphere)}}applyClipRect(e){let f=this.getAttribute("aTroikaGlyphIndex").count,h=this._chunkedBounds;if(h)for(let l=h.length;l--;){f=h[l].end;let b=h[l].rect;if(b[1]<e.w&&b[3]>e.y&&b[0]<e.z&&b[2]>e.x)break}this.instanceCount=f}updateAttributeData(e,f,h){let l=this.getAttribute(e);f?l&&l.array.length===f.length?(l.array.set(f),l.needsUpdate=!0):(this.setAttribute(e,new z.InstancedBufferAttribute(f,h)),delete this._maxInstanceCount,this.dispose()):l&&this.deleteAttribute(e)}}
let va=new z.MeshBasicMaterial({color:16777215,side:z.DoubleSide,transparent:!0}),Na=new z.Matrix4,oa=new z.Vector3,wa=new z.Vector3,ha=[],fb=new z.Vector3,Oa=()=>{let e=new z.Mesh(new z.PlaneGeometry(1,1),va);Oa=()=>e;return e},Pa=()=>{let e=new z.Mesh(new z.PlaneGeometry(1,1,32,1),va);Pa=()=>e;return e},gb={type:"syncstart"},hb={type:"synccomplete"},Qa="font fontSize fontStyle fontWeight lang letterSpacing lineHeight maxWidth overflowWrap text direction textAlign textIndent whiteSpace anchorX anchorY colorRanges sdfGlyphSize".split(" "),
ib=Qa.concat("material","color","depthOffset","clipRect","curveRadius","orientation","glyphGeometryDetail");class ia extends z.Mesh{constructor(){super(new Ma,null);this.text="";this.curveRadius=this.anchorY=this.anchorX=0;this.direction="auto";this.unicodeFontsURL=this.font=null;this.fontSize=.1;this.fontStyle=this.fontWeight="normal";this.lang=null;this.letterSpacing=0;this.lineHeight="normal";this.maxWidth=Infinity;this.overflowWrap="normal";this.textAlign="left";this.textIndent=0;this.whiteSpace=
"normal";this.colorRanges=this.color=this.material=null;this.outlineColor=this.outlineWidth=0;this.outlineOpacity=1;this.strokeWidth=this.outlineOffsetY=this.outlineOffsetX=this.outlineBlur=0;this.strokeColor=8421504;this.fillOpacity=this.strokeOpacity=1;this.depthOffset=0;this.clipRect=null;this.orientation="+x+y";this.glyphGeometryDetail=1;this.sdfGlyphSize=null;this.gpuAccelerateSDF=!0;this.debugSDF=!1}sync(e){this._needsSync&&(this._needsSync=!1,this._isSyncing?(this._queuedSyncs||(this._queuedSyncs=
[])).push(e):(this._isSyncing=!0,this.dispatchEvent(gb),qa({text:this.text,font:this.font,lang:this.lang,fontSize:this.fontSize||.1,fontWeight:this.fontWeight||"normal",fontStyle:this.fontStyle||"normal",letterSpacing:this.letterSpacing||0,lineHeight:this.lineHeight||"normal",maxWidth:this.maxWidth,direction:this.direction||"auto",textAlign:this.textAlign,textIndent:this.textIndent,whiteSpace:this.whiteSpace,overflowWrap:this.overflowWrap,anchorX:this.anchorX,anchorY:this.anchorY,colorRanges:this.colorRanges,
includeCaretPositions:!0,sdfGlyphSize:this.sdfGlyphSize,gpuAccelerateSDF:this.gpuAccelerateSDF,unicodeFontsURL:this.unicodeFontsURL},f=>{this._isSyncing=!1;this._textRenderInfo=f;this.geometry.updateGlyphs(f.glyphBounds,f.glyphAtlasIndices,f.blockBounds,f.chunkedBounds,f.glyphColors);let h=this._queuedSyncs;h&&(this._queuedSyncs=null,this._needsSync=!0,this.sync(()=>{h.forEach(e=>e&&e())}));this.dispatchEvent(hb);e&&e()})))}onBeforeRender(e,f,h,l,b,a){this.sync();b.isTroikaTextMaterial&&this._prepareForRender(b)}dispose(){this.geometry.dispose()}get textRenderInfo(){return this._textRenderInfo||
null}createDerivedMaterial(e){return ra(e)}get material(){let e=this._derivedMaterial,f=this._baseMaterial||this._defaultMaterial||(this._defaultMaterial=va.clone());e&&e.isDerivedFrom(f)||(e=this._derivedMaterial=this.createDerivedMaterial(f),f.addEventListener("dispose",function l(){f.removeEventListener("dispose",l);e.dispose()}));if(this.hasOutline()){let f=e._outlineMtl;f||(f=e._outlineMtl=Object.create(e,{id:{value:e.id+.1}}),f.isTextOutlineMaterial=!0,f.depthWrite=!1,f.map=null,e.addEventListener("dispose",
function b(){e.removeEventListener("dispose",b);f.dispose()}));return[f,e]}return e}set material(e){e&&e.isTroikaTextMaterial?(this._derivedMaterial=e,this._baseMaterial=e.baseMaterial):this._baseMaterial=e}hasOutline(){return!!(this.outlineWidth||this.outlineBlur||this.outlineOffsetX||this.outlineOffsetY)}get glyphGeometryDetail(){return this.geometry.detail}set glyphGeometryDetail(e){this.geometry.detail=e}get curveRadius(){return this.geometry.curveRadius}set curveRadius(e){this.geometry.curveRadius=
e}get customDepthMaterial(){return Ka(this.material).getDepthMaterial()}set customDepthMaterial(e){}get customDistanceMaterial(){return Ka(this.material).getDistanceMaterial()}set customDistanceMaterial(e){}_prepareForRender(e){var f=e.isTextOutlineMaterial,h=e.uniforms,l=this.textRenderInfo;if(l){let {sdfTexture:a,blockBounds:c}=l;h.uTroikaSDFTexture.value=a;h.uTroikaSDFTextureSize.value.set(a.image.width,a.image.height);h.uTroikaSDFGlyphSize.value=l.sdfGlyphSize;h.uTroikaSDFExponent.value=l.sdfExponent;
h.uTroikaTotalBounds.value.fromArray(c);h.uTroikaUseGlyphColors.value=!f&&!!l.glyphColors;let d=l=0,e=0,k;let q=0,m=0;if(f){let {outlineWidth:a,outlineOffsetX:b,outlineOffsetY:c,outlineBlur:e,outlineOpacity:f}=this;l=this._parsePercent(a)||0;d=Math.max(0,this._parsePercent(e)||0);k=f;q=this._parsePercent(b)||0;m=this._parsePercent(c)||0}else{if(e=Math.max(0,this._parsePercent(this.strokeWidth)||0)){var b=this.strokeColor;h.uTroikaStrokeColor.value.set(null==b?8421504:b);b=this.strokeOpacity;null==
b&&(b=1)}k=this.fillOpacity}h.uTroikaEdgeOffset.value=l;h.uTroikaPositionOffset.value.set(q,m);h.uTroikaBlurRadius.value=d;h.uTroikaStrokeWidth.value=e;h.uTroikaStrokeOpacity.value=b;h.uTroikaFillOpacity.value=null==k?1:k;h.uTroikaCurveRadius.value=this.curveRadius||0;(l=this.clipRect)&&Array.isArray(l)&&4===l.length?h.uTroikaClipRect.value.fromArray(l):(l=100*(this.fontSize||.1),h.uTroikaClipRect.value.set(c[0]-l,c[1]-l,c[2]+l,c[3]+l));this.geometry.applyClipRect(h.uTroikaClipRect.value)}h.uTroikaSDFDebug.value=
!!this.debugSDF;e.polygonOffset=!!this.depthOffset;e.polygonOffsetFactor=e.polygonOffsetUnits=this.depthOffset||0;f=f?this.outlineColor||0:this.color;null==f?delete e.color:(l=e.hasOwnProperty("color")?e.color:e.color=new z.Color,(f!==l._input||"object"===typeof f)&&l.set(l._input=f));f=this.orientation||"+x+y";if(f!==e._orientation){h=h.uTroikaOrient.value;f=f.replace(/[^-+xyz]/g,"");if(l="+x+y"!==f&&f.match(/^([-+])([xyz])([-+])([xyz])$/)){let [,a,b,d,e]=l;oa.set(0,0,0)[b]="-"===a?1:-1;wa.set(0,
0,0)[e]="-"===d?-1:1;Na.lookAt(fb,oa.cross(wa),wa);h.setFromMatrix4(Na)}else h.identity();e._orientation=f}}_parsePercent(e){"string"===typeof e&&(e=(e=e.match(/^(-?[\d.]+)%$/))?parseFloat(e[1]):NaN,e=(isNaN(e)?0:e/100)*this.fontSize);return e}localPositionToTextCoords(e,f=new z.Vector2){f.copy(e);let h=this.curveRadius;h&&(f.x=Math.atan2(e.x,Math.abs(h)-Math.abs(e.z))*Math.abs(h));return f}worldPositionToTextCoords(e,f=new z.Vector2){oa.copy(e);return this.localPositionToTextCoords(this.worldToLocal(oa),
f)}raycast(e,f){let {textRenderInfo:h,curveRadius:l}=this;if(h){let b=h.blockBounds,a=l?Pa():Oa(),c=a.geometry,{position:d,uv:g}=c.attributes;for(let a=0;a<g.count;a++){let c=b[0]+g.getX(a)*(b[2]-b[0]),e=b[1]+g.getY(a)*(b[3]-b[1]),f=0;l&&(f=l-Math.cos(c/l)*l,c=Math.sin(c/l)*l);d.setXYZ(a,c,e,f)}c.boundingSphere=this.geometry.boundingSphere;c.boundingBox=this.geometry.boundingBox;a.matrixWorld=this.matrixWorld;a.material.side=this.material.side;ha.length=0;a.raycast(e,ha);for(e=0;e<ha.length;e++)ha[e].object=
this,f.push(ha[e])}}copy(e){let f=this.geometry;super.copy(e);this.geometry=f;ib.forEach(f=>{this[f]=e[f]});return this}clone(){return(new this.constructor).copy(this)}}Qa.forEach(e=>{let f="_private_"+e;Object.defineProperty(ia.prototype,e,{get(){return this[f]},set(e){e!==this[f]&&(this[f]=e,this._needsSync=!0)}})});let jb={type:"syncstart"},kb={type:"synccomplete"},Ra=new z.Box3,Sa=new z.Color;class Ta extends ia{constructor(){super();this._members=new Map;this._dataTextures={};this._onMemberSynced=
e=>{this._members.get(e.target).dirty=!0}}add(...e){for(let f=0;f<e.length;f++)e[f]instanceof ia?this.addText(e[f]):super.add(e[f]);return this}remove(...e){for(let f=0;f<e.length;f++)e[f]instanceof ia?this.removeText(e[f]):super.remove(e[f]);return this}addText(e){this._members.has(e)||(this._members.set(e,{index:-1,glyphCount:-1,dirty:!0}),e.addEventListener("synccomplete",this._onMemberSynced))}removeText(e){this._needsRepack=!0;e.removeEventListener("synccomplete",this._onMemberSynced);this._members.delete(e)}createDerivedMaterial(e){return cb(e)}updateMatrixWorld(e){super.updateMatrixWorld(e);
this.updateBounds()}updateBounds(){let e=this.geometry.boundingBox.makeEmpty();this._members.forEach((f,h)=>{h.matrixAutoUpdate&&h.updateMatrix();Ra.copy(h.geometry.boundingBox).applyMatrix4(h.matrix);e.union(Ra)});e.getBoundingSphere(this.geometry.boundingSphere)}hasOutline(){for(let e of this._members.keys())if(e.hasOutline())return!0;return!1}_prepareForRender(e){let f=e.isTextOutlineMaterial;e.uniforms.uTroikaIsOutline.value=f;let h=this._dataTextures[f?"outline":"main"],l=Math.pow(2,Math.ceil(Math.log2(32*
this._members.size)));if(!h||l!==h.image.data.length){h&&h.dispose();let a=Math.min(l/4,1024);h=this._dataTextures[f?"outline":"main"]=new z.DataTexture(new Float32Array(l),a,l/4/a,z.RGBAFormat,z.FloatType)}let b=h.image.data,a=(a,d)=>{d!==b[a]&&(b[a]=d,h.needsUpdate=!0)};this._members.forEach(({index:b},d)=>{if(-1<b){b*=32;var c=d.matrix.elements;for(let d=0;16>d;d++)a(b+d,c[d]);d._prepareForRender(e);let {uTroikaTotalBounds:g,uTroikaClipRect:h,uTroikaPositionOffset:m,uTroikaEdgeOffset:l,uTroikaBlurRadius:y,
uTroikaStrokeWidth:x,uTroikaStrokeColor:p,uTroikaStrokeOpacity:r,uTroikaFillOpacity:t,uTroikaCurveRadius:v}=e.uniforms;for(c=0;4>c;c++)a(b+16+c,g.value.getComponent(c));for(c=0;4>c;c++)a(b+20+c,h.value.getComponent(c));d=f?d.outlineColor||0:d.color;null==d&&(d=this.color);null==d&&(d=this.material.color);null==d&&(d=16777215);a(b+24,Sa.set(d).getHex());a(b+25,t.value);a(b+26,v.value);f?(a(b+28,m.value.x),a(b+29,m.value.y),a(b+30,l.value),a(b+31,y.value)):(a(b+28,x.value),a(b+29,Sa.set(p.value).getHex()),
a(b+30,r.value))}});e.setMatrixTexture(h);super._prepareForRender(e)}sync(e){let f=this._needsRepack?[]:null;this._needsRepack=!1;this._members.forEach((e,l)=>{if(e.dirty||l._needsSync)e.dirty=!1,(f||(f=[])).push(new Promise(b=>{l._needsSync?l.sync(b):b()}))});f&&(this.dispatchEvent(jb),Promise.all(f).then(()=>{let {geometry:f}=this,l=f.attributes,b=l.aTroikaTextBatchMemberIndex&&l.aTroikaTextBatchMemberIndex.array||new Uint16Array(0),a=l.aTroikaGlyphIndex&&l.aTroikaGlyphIndex.array||new Float32Array(0),
c=l.aTroikaGlyphBounds&&l.aTroikaGlyphBounds.array||new Float32Array(0),d=0;this._members.forEach((a,{textRenderInfo:b})=>{b&&(d+=b.glyphAtlasIndices.length,this._textRenderInfo=b)});d!==b.length&&(b=sa(b,d),a=sa(a,d),c=sa(c,4*d));let g=0,k=0;this._members.forEach((d,{textRenderInfo:e})=>{if(e){let f=e.glyphAtlasIndices.length;b.fill(g,k,k+f);a.set(e.glyphAtlasIndices,k,k+f);c.set(e.glyphBounds,4*k,4*(k+f));k+=f;d.index=g++}});f.updateAttributeData("aTroikaTextBatchMemberIndex",b,1);f.getAttribute("aTroikaTextBatchMemberIndex").setUsage(z.DynamicDrawUsage);
f.updateAttributeData("aTroikaGlyphIndex",a,1);f.updateAttributeData("aTroikaGlyphBounds",c,4);this.updateBounds();this.dispatchEvent(kb);e&&e()}))}copy(e){e instanceof Ta&&(super.copy(e),this._members.forEach((e,h)=>this.removeText(h)),e._members.forEach((e,h)=>this.addText(h)));return this}dispose(){super.dispose();Object.values(this._dataTextures).forEach(e=>e.dispose())}}let Ua=new WeakMap,La=new WeakMap;J.BatchedText=Ta;J.GlyphsGeometry=Ma;J.Text=ia;J.configureTextBuilder=function(e){Da?console.warn("configureTextBuilder called after first font request; will be ignored."):
Ea(Q,e)};J.createTextDerivedMaterial=ra;J.dumpSDFTextures=function(){Object.keys(ma).forEach(e=>{e=ma[e].sdfCanvas;let {width:f,height:h}=e;console.log("%c.",`
background: url(${e.toDataURL()});
background-size: ${f}px ${h}px;
color: transparent;
font-size: 0;
line-height: ${h}px;
padding-left: ${f}px;
`)})};J.fontResolverWorkerModule=R;J.getCaretAtPoint=function(e,f,h){let l=null,b=null;eb(e).forEach(a=>{if(!b||Math.abs(h-(a.top+a.bottom)/2)<Math.abs(h-(b.top+b.bottom)/2))b=a});b.carets.forEach(a=>{if(!l||Math.abs(f-a.x)<Math.abs(f-l.x))l=a});return l};J.getSelectionRects=function(e,f,h){var l;if(e){if((l=Ua.get(e))&&l.start===f&&l.end===h)return l.rects;var {caretPositions:b}=e;h<f&&(l=f,f=h,h=l);f=Math.max(f,0);h=Math.min(h,b.length+1);l=[];var a=null;for(var c=f;c<h;c++){var d=b[4*c],g=
b[4*c+1];let e=Math.min(d,g);d=Math.max(d,g);g=b[4*c+2];let f=b[4*c+3];if(!a||g!==a.bottom||f!==a.top||e>a.right||d<a.left)a={left:Infinity,right:-Infinity,bottom:g,top:f},l.push(a);a.left=Math.min(e,a.left);a.right=Math.max(d,a.right)}l.sort((a,b)=>b.bottom-a.bottom||a.left-b.left);for(b=l.length-1;0<b--;)a=l[b],c=l[b+1],a.bottom===c.bottom&&a.top===c.top&&a.left<=c.right&&a.right>=c.left&&(c.left=Math.min(c.left,a.left),c.right=Math.max(c.right,a.right),l.splice(b,1));Ua.set(e,{start:f,end:h,rects:l})}return l};
J.getTextRenderInfo=qa;J.preloadFont=function({font:e,characters:f,sdfGlyphSize:h},l){f=Array.isArray(f)?f.join("\n"):""+f;qa({font:e,sdfGlyphSize:h,text:f},l)};J.typesetterWorkerModule=X;Object.defineProperty(J,"__esModule",{value:!0})})
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env node
/* eslint-disable */
// Utility for finding the public URL for a font hosted on Google Fonts. Takes the font
// family name and returns the URL to its .woff file.
// TODO:
// - Add ability to download the file for self-hosting
// - Add options for font weights, italic, etc. and character subsets
const fetch = require('node-fetch')
const args = process.argv.slice(2)
const fontFamilyName = args[0]
if (!fontFamilyName) {
fail('Pass the name of the font family as an argument. It is case-sensitive.')
}
async function run() {
const response = await fetch(`https://fonts.googleapis.com/css2?family=${fontFamilyName}&display=swap`, {
"credentials": "omit",
"headers": {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Upgrade-Insecure-Requests": "1",
"Cache-Control": "max-age=0"
},
"method": "GET",
//"mode": "cors"
})
if (response.ok) {
const css = await response.text()
const urls = []
css.replace(/https?:[^)]*/g, (str) => {
urls.push(str)
})
if (urls.length) {
console.log(`Use this URL for the font family ${fontFamilyName}:`)
console.log(urls.join('\n'))
} else {
fail('Could not parse font URLs from Google Fonts css file.')
}
} else {
fail(`Could not find font family ${fontFamilyName}. Check its spelling and case and that it exists on Google Fonts.`)
}
}
function fail(message) {
console.error(message)
process.exit(1)
}
run()
File diff suppressed because one or more lines are too long
@@ -0,0 +1,6 @@
/*!
Custom bundle of @unicode-font-resolver/client v1.0.2 (https://github.com/lojjic/unicode-font-resolver)
for use in Troika text rendering.
Original MIT license applies
*/
export default function(){return function(t){"use strict";var n=function(){this.buckets=new Map};n.prototype.add=function(t){var n=t>>5;this.buckets.set(n,(this.buckets.get(n)||0)|1<<(31&t))},n.prototype.has=function(t){var n=this.buckets.get(t>>5);return void 0!==n&&0!=(n&1<<(31&t))},n.prototype.serialize=function(){var t=[];return this.buckets.forEach((function(n,r){t.push((+r).toString(36)+":"+n.toString(36))})),t.join(",")},n.prototype.deserialize=function(t){var n=this;this.buckets.clear(),t.split(",").forEach((function(t){var r=t.split(":");n.buckets.set(parseInt(r[0],36),parseInt(r[1],36))}))};var r=Math.pow(2,8),e=r-1,o=~e;function a(t){var n=function(t){return t&o}(t).toString(16),e=function(t){return(t&o)+r-1}(t).toString(16);return"codepoint-index/plane"+(t>>16)+"/"+n+"-"+e+".json"}function i(t,n){var r=t&e,o=n.codePointAt(r/6|0);return 0!=((o=(o||48)-48)&1<<r%6)}function u(t,n){var r;(r=t,r.replace(/U\+/gi,"").replace(/^,+|,+$/g,"").split(/,+/).map((function(t){return t.split("-").map((function(t){return parseInt(t.trim(),16)}))}))).forEach((function(t){var r=t[0],e=t[1];void 0===e&&(e=r),n(r,e)}))}function c(t,n){u(t,(function(t,r){for(var e=t;e<=r;e++)n(e)}))}var s={},f={},l=new WeakMap,v="https://cdn.jsdelivr.net/gh/lojjic/unicode-font-resolver@v1.0.1/packages/data";function d(t){var r=l.get(t);return r||(r=new n,c(t.ranges,(function(t){return r.add(t)})),l.set(t,r)),r}var h,p=new Map;function g(t,n,r){return t[n]?n:t[r]?r:function(t){for(var n in t)return n}(t)}function w(t,n){var r=n;if(!t.includes(r)){r=1/0;for(var e=0;e<t.length;e++)Math.abs(t[e]-n)<Math.abs(r-n)&&(r=t[e])}return r}function k(t){return h||(h=new Set,c("9-D,20,85,A0,1680,2000-200A,2028-202F,205F,3000",(function(t){h.add(t)}))),h.has(t)}return t.CodePointSet=n,t.clearCache=function(){s={},f={}},t.getFontsForString=function(t,n){void 0===n&&(n={});var r,e=n.lang;void 0===e&&(e=/\p{Script=Hangul}/u.test(r=t)?"ko":/\p{Script=Hiragana}|\p{Script=Katakana}/u.test(r)?"ja":"en");var o=n.category;void 0===o&&(o="sans-serif");var u=n.style;void 0===u&&(u="normal");var c=n.weight;void 0===c&&(c=400);var l=(n.dataUrl||v).replace(/\/$/g,""),h=new Map,y=new Uint8Array(t.length),b={},m={},A=new Array(t.length),S=new Map,j=!1;function M(t){var n=p.get(t);return n||(n=fetch(l+"/"+t).then((function(t){if(!t.ok)throw new Error(t.statusText);return t.json().then((function(t){if(!Array.isArray(t)||1!==t[0])throw new Error("Incorrect schema version; need 1, got "+t[0]);return t[1]}))})).catch((function(n){if(l!==v)return j||(console.error('unicode-font-resolver: Failed loading from dataUrl "'+l+'", trying default CDN. '+n.message),j=!0),l=v,p.delete(t),M(t);throw n})),p.set(t,n)),n}for(var P=function(n){var r=t.codePointAt(n),e=a(r);A[n]=e,s[e]||S.has(e)||S.set(e,M(e).then((function(t){s[e]=t}))),r>65535&&(n++,E=n)},E=0;E<t.length;E++)P(E);return Promise.all(S.values()).then((function(){S.clear();for(var n=function(n){var o=t.codePointAt(n),a=null,u=s[A[n]],c=void 0;for(var l in u){var v=m[l];if(void 0===v&&(v=m[l]=new RegExp(l).test(e||"en")),v){for(var d in c=l,u[l])if(i(o,u[l][d])){a=d;break}break}}if(!a)t:for(var h in u)if(h!==c)for(var p in u[h])if(i(o,u[h][p])){a=p;break t}a||(console.debug("No font coverage for U+"+o.toString(16)),a="latin"),A[n]=a,f[a]||S.has(a)||S.set(a,M("font-meta/"+a+".json").then((function(t){f[a]=t}))),o>65535&&(n++,r=n)},r=0;r<t.length;r++)n(r);return Promise.all(S.values())})).then((function(){for(var n,r=null,e=0;e<t.length;e++){var a=t.codePointAt(e);if(r&&(k(a)||d(r).has(a)))y[e]=y[e-1];else{r=f[A[e]];var i=b[r.id];if(!i){var s=r.typeforms,v=g(s,o,"sans-serif"),p=g(s[v],u,"normal"),m=w(null===(n=s[v])||void 0===n?void 0:n[p],c);i=b[r.id]=l+"/font-files/"+r.id+"/"+v+"."+p+"."+m+".woff"}var S=h.get(i);null==S&&(S=h.size,h.set(i,S)),y[e]=S}a>65535&&(e++,y[e]=y[e-1])}return{fontUrls:Array.from(h.keys()),chars:y}}))},Object.defineProperty(t,"__esModule",{value:!0}),t}({})}
+8
View File
@@ -0,0 +1,8 @@
/*!
Custom bundle of woff2otf (https://github.com/arty-name/woff2otf) with fflate
(https://github.com/101arrowz/fflate) for use in Troika text rendering.
Original licenses apply:
- fflate: https://github.com/101arrowz/fflate/blob/master/LICENSE (MIT)
- woff2otf.js: https://github.com/arty-name/woff2otf/blob/master/woff2otf.js (Apache2)
*/
export default function(){return function(r){"use strict";var e=Uint8Array,n=Uint16Array,t=Uint32Array,a=new e([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),i=new e([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),o=new e([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),f=function(r,e){for(var a=new n(31),i=0;i<31;++i)a[i]=e+=1<<r[i-1];var o=new t(a[30]);for(i=1;i<30;++i)for(var f=a[i];f<a[i+1];++f)o[f]=f-a[i]<<5|i;return[a,o]},u=f(a,2),v=u[0],s=u[1];v[28]=258,s[258]=28;for(var l=f(i,0)[0],c=new n(32768),g=0;g<32768;++g){var h=(43690&g)>>>1|(21845&g)<<1;h=(61680&(h=(52428&h)>>>2|(13107&h)<<2))>>>4|(3855&h)<<4,c[g]=((65280&h)>>>8|(255&h)<<8)>>>1}var w=function(r,e,t){for(var a=r.length,i=0,o=new n(e);i<a;++i)++o[r[i]-1];var f,u=new n(e);for(i=0;i<e;++i)u[i]=u[i-1]+o[i-1]<<1;if(t){f=new n(1<<e);var v=15-e;for(i=0;i<a;++i)if(r[i])for(var s=i<<4|r[i],l=e-r[i],g=u[r[i]-1]++<<l,h=g|(1<<l)-1;g<=h;++g)f[c[g]>>>v]=s}else for(f=new n(a),i=0;i<a;++i)r[i]&&(f[i]=c[u[r[i]-1]++]>>>15-r[i]);return f},d=new e(288);for(g=0;g<144;++g)d[g]=8;for(g=144;g<256;++g)d[g]=9;for(g=256;g<280;++g)d[g]=7;for(g=280;g<288;++g)d[g]=8;var m=new e(32);for(g=0;g<32;++g)m[g]=5;var b=w(d,9,1),p=w(m,5,1),y=function(r){for(var e=r[0],n=1;n<r.length;++n)r[n]>e&&(e=r[n]);return e},L=function(r,e,n){var t=e/8|0;return(r[t]|r[t+1]<<8)>>(7&e)&n},U=function(r,e){var n=e/8|0;return(r[n]|r[n+1]<<8|r[n+2]<<16)>>(7&e)},k=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],T=function(r,e,n){var t=new Error(e||k[r]);if(t.code=r,Error.captureStackTrace&&Error.captureStackTrace(t,T),!n)throw t;return t},O=function(r,f,u){var s=r.length;if(!s||u&&!u.l&&s<5)return f||new e(0);var c=!f||u,g=!u||u.i;u||(u={}),f||(f=new e(3*s));var h,d=function(r){var n=f.length;if(r>n){var t=new e(Math.max(2*n,r));t.set(f),f=t}},m=u.f||0,k=u.p||0,O=u.b||0,A=u.l,x=u.d,E=u.m,D=u.n,M=8*s;do{if(!A){u.f=m=L(r,k,1);var S=L(r,k+1,3);if(k+=3,!S){var V=r[(I=((h=k)/8|0)+(7&h&&1)+4)-4]|r[I-3]<<8,_=I+V;if(_>s){g&&T(0);break}c&&d(O+V),f.set(r.subarray(I,_),O),u.b=O+=V,u.p=k=8*_;continue}if(1==S)A=b,x=p,E=9,D=5;else if(2==S){var j=L(r,k,31)+257,z=L(r,k+10,15)+4,C=j+L(r,k+5,31)+1;k+=14;for(var F=new e(C),P=new e(19),q=0;q<z;++q)P[o[q]]=L(r,k+3*q,7);k+=3*z;var B=y(P),G=(1<<B)-1,H=w(P,B,1);for(q=0;q<C;){var I,J=H[L(r,k,G)];if(k+=15&J,(I=J>>>4)<16)F[q++]=I;else{var K=0,N=0;for(16==I?(N=3+L(r,k,3),k+=2,K=F[q-1]):17==I?(N=3+L(r,k,7),k+=3):18==I&&(N=11+L(r,k,127),k+=7);N--;)F[q++]=K}}var Q=F.subarray(0,j),R=F.subarray(j);E=y(Q),D=y(R),A=w(Q,E,1),x=w(R,D,1)}else T(1);if(k>M){g&&T(0);break}}c&&d(O+131072);for(var W=(1<<E)-1,X=(1<<D)-1,Y=k;;Y=k){var Z=(K=A[U(r,k)&W])>>>4;if((k+=15&K)>M){g&&T(0);break}if(K||T(2),Z<256)f[O++]=Z;else{if(256==Z){Y=k,A=null;break}var $=Z-254;if(Z>264){var rr=a[q=Z-257];$=L(r,k,(1<<rr)-1)+v[q],k+=rr}var er=x[U(r,k)&X],nr=er>>>4;er||T(3),k+=15&er;R=l[nr];if(nr>3){rr=i[nr];R+=U(r,k)&(1<<rr)-1,k+=rr}if(k>M){g&&T(0);break}c&&d(O+131072);for(var tr=O+$;O<tr;O+=4)f[O]=f[O-R],f[O+1]=f[O+1-R],f[O+2]=f[O+2-R],f[O+3]=f[O+3-R];O=tr}}u.l=A,u.p=Y,u.b=O,A&&(m=1,u.m=E,u.d=x,u.n=D)}while(!m);return O==f.length?f:function(r,a,i){(null==a||a<0)&&(a=0),(null==i||i>r.length)&&(i=r.length);var o=new(r instanceof n?n:r instanceof t?t:e)(i-a);return o.set(r.subarray(a,i)),o}(f,0,O)},A=new e(0);var x="undefined"!=typeof TextDecoder&&new TextDecoder;try{x.decode(A,{stream:!0}),1}catch(r){}return r.convert_streams=function(r){var e=new DataView(r),n=0;function t(){var r=e.getUint16(n);return n+=2,r}function a(){var r=e.getUint32(n);return n+=4,r}function i(r){m.setUint16(b,r),b+=2}function o(r){m.setUint32(b,r),b+=4}for(var f={signature:a(),flavor:a(),length:a(),numTables:t(),reserved:t(),totalSfntSize:a(),majorVersion:t(),minorVersion:t(),metaOffset:a(),metaLength:a(),metaOrigLength:a(),privOffset:a(),privLength:a()},u=0;Math.pow(2,u)<=f.numTables;)u++;u--;for(var v=16*Math.pow(2,u),s=16*f.numTables-v,l=12,c=[],g=0;g<f.numTables;g++)c.push({tag:a(),offset:a(),compLength:a(),origLength:a(),origChecksum:a()}),l+=16;var h,w=new Uint8Array(12+16*c.length+c.reduce((function(r,e){return r+e.origLength+4}),0)),d=w.buffer,m=new DataView(d),b=0;return o(f.flavor),i(f.numTables),i(v),i(u),i(s),c.forEach((function(r){o(r.tag),o(r.origChecksum),o(l),o(r.origLength),r.outOffset=l,(l+=r.origLength)%4!=0&&(l+=4-l%4)})),c.forEach((function(e){var n,t=r.slice(e.offset,e.offset+e.compLength);if(e.compLength!=e.origLength){var a=new Uint8Array(e.origLength);n=new Uint8Array(t,2),O(n,a)}else a=new Uint8Array(t);w.set(a,e.outOffset);var i=0;(l=e.outOffset+e.origLength)%4!=0&&(i=4-l%4),w.set(new Uint8Array(i).buffer,e.outOffset+e.origLength),h=l+i})),d.slice(0,h)},Object.defineProperty(r,"__esModule",{value:!0}),r}({}).convert_streams}
+37
View File
@@ -0,0 +1,37 @@
{
"name": "troika-three-text",
"version": "0.52.4",
"description": "SDF-based text rendering for Three.js",
"author": "Jason Johnston <jason.johnston@protectwise.com>",
"repository": {
"type": "git",
"url": "https://github.com/protectwise/troika.git",
"directory": "packages/troika-three-text"
},
"license": "MIT",
"sideEffects": false,
"main": "dist/troika-three-text.umd.js",
"module": "dist/troika-three-text.esm.js",
"module:src": "src/index.js",
"dependencies": {
"bidi-js": "^1.0.2",
"troika-three-utils": "^0.52.4",
"troika-worker-utils": "^0.52.0",
"webgl-sdf-generator": "1.1.1"
},
"peerDependencies": {
"three": ">=0.125.0"
},
"devDependencies": {
"@lojjic/typr": "0.3.8-lojjic1",
"@unicode-font-resolver/client": "1.0.2",
"fflate": "^0.7.0",
"node-fetch": "^2.6.0"
},
"scripts": {
"build-typr": "rollup -c rollup.config.build-typr.js",
"build-woff2otf": "rollup -c rollup.config.build-woff2otf.js",
"build-unicode-font-resolver": "rollup -c rollup.config.build-unicode-font-resolver.js"
},
"gitHead": "728a1780127df0c2509e967b03f17ddc46bbf5d2"
}
+88
View File
@@ -0,0 +1,88 @@
// This build file creates a static version of the Typr.ts library used for
// processing fonts. It's isolated within a "factory" function wrapper so it can
// easily be marshalled into a web worker.
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'
const {LERNA_ROOT_PATH} = process.env
if (!LERNA_ROOT_PATH) {
throw new Error("Please execute `npm run build-typr` from the repository root.")
}
const OUTPUT_TEMPLATE = `
/*!
Custom build of Typr.ts (https://github.com/fredli74/Typr.ts) for use in Troika text rendering.
Original MIT license applies: https://github.com/fredli74/Typr.ts/blob/master/LICENSE
*/
export default function() {
// Trick it into being able to run in a web worker
if (typeof window === 'undefined') {
self.window = self
}
$$CONTENT$$
return TyprExports.Typr
}
`
const [banner, footer] = OUTPUT_TEMPLATE.split('$$CONTENT$$')
export default {
input: LERNA_ROOT_PATH + '/node_modules/@lojjic/typr/dist/Typr.js',
plugins: [
nodeResolve(),
commonjs(),
{
name: 'custom',
transform(source, id) {
// Quiet the console.warn statements added in the Typr.ts fork
source = source.replace(/console\.warn/g, 'console.debug')
// Replace some unneeded property assignments with unused stub vars so they get pruned out
// SVG fonts are unsupported by browsers anyway
removeMethodAssignment('Typr.SVG')
removeMethodAssignment('Typr.SVG.parse')
removeMethodAssignment('Typr.SVG.toPath')
removeMethodAssignment('Typr.SVG._toPath')
removeMethodAssignment('Typr.SVG._tokens')
removeMethodAssignment('Typr.SVG._toksToPath')
removeMethodAssignment('Typr.SVG._reps')
removeMethodAssignment('Typr.U.pathToSVG')
removeMethodAssignment('Typr.U.pathToContext')
// We provide our own implementation for stringToGlyphs that fixes joining-type shaping
// and improves performance, so don't need the original
removeMethodAssignment('Typr.U.stringToGlyphs')
removeMethodAssignment('Typr.U._getWPfeature')
// Same for position adjustments to support mark anchoring
removeMethodAssignment('Typr.U.getPairAdjustment')
return source
function removeMethodAssignment(name) {
const re = new RegExp(`(${name}\\s*=)`)
source = source.replace(re, `var prunable${Math.round(Math.random()*1e12)}=`)
}
}
},
terser({
ecma: 5
})
],
output: {
format: 'iife',
exports: 'named',
name: 'TyprExports',
file: 'libs/typr.factory.js',
banner,
footer
}
}
@@ -0,0 +1,51 @@
import nodeResolve from 'rollup-plugin-node-resolve'
import { terser } from 'rollup-plugin-terser'
import { version } from '@unicode-font-resolver/client/package.json'
import buble from "rollup-plugin-buble";
const {LERNA_ROOT_PATH} = process.env
if (!LERNA_ROOT_PATH) {
throw new Error("Please execute `npm run build-unicode-font-resolver` from the repository root.")
}
const OUTPUT_TEMPLATE = `
/*!
Custom bundle of @unicode-font-resolver/client v${version} (https://github.com/lojjic/unicode-font-resolver)
for use in Troika text rendering.
Original MIT license applies
*/
export default function() {
$$CONTENT$$
return client
}
`
const [banner, footer] = OUTPUT_TEMPLATE.split('$$CONTENT$$')
export default {
input: LERNA_ROOT_PATH + '/node_modules/@unicode-font-resolver/client/dist/client.esm.js',
// input: LERNA_ROOT_PATH + '/../unicode-font-resolver/packages/client/dist/client.esm.js',
plugins: [
nodeResolve(),
buble({
transforms: {
unicodeRegExp: false
}
}),
terser({
ecma: 5
})
],
output: {
format: 'iife',
name: 'client',
file: 'libs/unicode-font-resolver-client.factory.js',
banner,
footer
}
}
+53
View File
@@ -0,0 +1,53 @@
// This build file assembles a function for converting WOFF fonts to OTF fonts, so they
// can be parsed by Typr. It's isolated within a "factory" function wrapper so it can
// easily be marshalled into a web worker.
import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'
const {LERNA_ROOT_PATH} = process.env
if (!LERNA_ROOT_PATH) {
throw new Error("Please execute `npm run build-woff2otf` from the repository root.")
}
const OUTPUT_TEMPLATE = `
/*!
Custom bundle of woff2otf (https://github.com/arty-name/woff2otf) with fflate
(https://github.com/101arrowz/fflate) for use in Troika text rendering.
Original licenses apply:
- fflate: https://github.com/101arrowz/fflate/blob/master/LICENSE (MIT)
- woff2otf.js: https://github.com/arty-name/woff2otf/blob/master/woff2otf.js (Apache2)
*/
export default function() {
$$CONTENT$$
return woff2otf.convert_streams
}
`
const [banner, footer] = OUTPUT_TEMPLATE.split('$$CONTENT$$')
export default {
// Starting from the src modules rather than dist as that gives us more fine-grained control
input: 'src/woff2otf.js',
plugins: [
nodeResolve(),
commonjs(),
terser({
ecma: 5
})
],
output: {
format: 'iife',
name: 'woff2otf',
file: 'libs/woff2otf.factory.js',
banner,
footer
}
}
+491
View File
@@ -0,0 +1,491 @@
import { Text } from "./Text.js";
import { DataTexture, FloatType, RGBAFormat, Vector2, Box3, Color, DynamicDrawUsage } from "three";
import { glyphBoundsAttrName, glyphIndexAttrName } from "./GlyphsGeometry";
import { createDerivedMaterial } from "troika-three-utils";
import { createTextDerivedMaterial } from "./TextDerivedMaterial";
const syncStartEvent = { type: "syncstart" };
const syncCompleteEvent = { type: "synccomplete" };
const memberIndexAttrName = "aTroikaTextBatchMemberIndex";
/*
Data texture packing strategy:
# Common:
0-15: matrix
16-19: uTroikaTotalBounds
20-23: uTroikaClipRect
24: diffuse (color/outlineColor)
25: uTroikaFillOpacity (fillOpacity/outlineOpacity)
26: uTroikaCurveRadius
27: <blank>
# Main:
28: uTroikaStrokeWidth
29: uTroikaStrokeColor
30: uTroikaStrokeOpacity
# Outline:
28-29: uTroikaPositionOffset
30: uTroikaEdgeOffset
31: uTroikaBlurRadius
*/
const floatsPerMember = 32;
const tempBox3 = new Box3();
const tempColor = new Color();
/**
* @experimental
*
* A specialized `Text` implementation that accepts any number of `Text` children
* and automatically batches them together to render in a single draw call.
*
* The `material` of each child `Text` will be ignored, and the `material` of the
* `BatchedText` will be used for all of them instead.
*
* NOTE: This only works in WebGL2 or where the OES_texture_float extension is available.
*/
export class BatchedText extends Text {
constructor () {
super();
/**
* @typedef {Object} PackingInfo
* @property {number} index - the packing order index when last packed, or -1
* @property {boolean} dirty - whether it has synced since last pack
*/
/**
* @type {Map<Text, PackingInfo>}
*/
this._members = new Map();
this._dataTextures = {}
this._onMemberSynced = (e) => {
this._members.get(e.target).dirty = true;
};
}
/**
* @override
* Batch any Text objects added as children
*/
add (...objects) {
for (let i = 0; i < objects.length; i++) {
if (objects[i] instanceof Text) {
this.addText(objects[i]);
} else {
super.add(objects[i]);
}
}
return this;
}
/**
* @override
*/
remove (...objects) {
for (let i = 0; i < objects.length; i++) {
if (objects[i] instanceof Text) {
this.removeText(objects[i]);
} else {
super.remove(objects[i]);
}
}
return this;
}
/**
* @param {Text} text
*/
addText (text) {
if (!this._members.has(text)) {
this._members.set(text, {
index: -1,
glyphCount: -1,
dirty: true
});
text.addEventListener("synccomplete", this._onMemberSynced);
}
}
/**
* @param {Text} text
*/
removeText (text) {
this._needsRepack = true
text.removeEventListener("synccomplete", this._onMemberSynced);
this._members.delete(text);
}
/**
* Use the custom derivation with extra batching logic
*/
createDerivedMaterial (baseMaterial) {
return createBatchedTextMaterial(baseMaterial);
}
updateMatrixWorld (force) {
super.updateMatrixWorld(force);
this.updateBounds();
}
/**
* Update the batched geometry bounds to hold all members
*/
updateBounds () {
// Update member local matrices and the overall bounds
const bbox = this.geometry.boundingBox.makeEmpty();
this._members.forEach((_, text) => {
if (text.matrixAutoUpdate) text.updateMatrix(); // ignore world matrix
tempBox3.copy(text.geometry.boundingBox).applyMatrix4(text.matrix);
bbox.union(tempBox3);
});
bbox.getBoundingSphere(this.geometry.boundingSphere);
}
/** @override */
hasOutline() {
// Iterator.some() not supported in Safari
for (let member of this._members.keys()) {
if (member.hasOutline()) return true;
}
return false;
}
/**
* @override
* Copy member matrices and uniform values into the data texture
*/
_prepareForRender (material) {
const isOutline = material.isTextOutlineMaterial
material.uniforms.uTroikaIsOutline.value = isOutline
// Resize the texture to fit in powers of 2
let texture = this._dataTextures[isOutline ? 'outline' : 'main'];
const dataLength = Math.pow(2, Math.ceil(Math.log2(this._members.size * floatsPerMember)));
if (!texture || dataLength !== texture.image.data.length) {
// console.log(`resizing: ${dataLength}`);
if (texture) texture.dispose();
const width = Math.min(dataLength / 4, 1024);
texture = this._dataTextures[isOutline ? 'outline' : 'main'] = new DataTexture(
new Float32Array(dataLength),
width,
dataLength / 4 / width,
RGBAFormat,
FloatType
);
}
const texData = texture.image.data;
const setTexData = (index, value) => {
if (value !== texData[index]) {
texData[index] = value;
texture.needsUpdate = true;
}
}
this._members.forEach(({ index, dirty }, text) => {
if (index > -1) {
const startIndex = index * floatsPerMember
// Matrix
const matrix = text.matrix.elements;
for (let i = 0; i < 16; i++) {
setTexData(startIndex + i, matrix[i])
}
// Let the member populate the uniforms, since that does all the appropriate
// logic and handling of defaults, and we'll just grab the results from there
text._prepareForRender(material)
const {
uTroikaTotalBounds,
uTroikaClipRect,
uTroikaPositionOffset,
uTroikaEdgeOffset,
uTroikaBlurRadius,
uTroikaStrokeWidth,
uTroikaStrokeColor,
uTroikaStrokeOpacity,
uTroikaFillOpacity,
uTroikaCurveRadius,
} = material.uniforms;
// Total bounds for uv
for (let i = 0; i < 4; i++) {
setTexData(startIndex + 16 + i, uTroikaTotalBounds.value.getComponent(i));
}
// Clip rect
for (let i = 0; i < 4; i++) {
setTexData(startIndex + 20 + i, uTroikaClipRect.value.getComponent(i));
}
// Color
let color = isOutline ? (text.outlineColor || 0) : text.color;
if (color == null) color = this.color;
if (color == null) color = this.material.color;
if (color == null) color = 0xffffff;
setTexData(startIndex + 24, tempColor.set(color).getHex());
// Fill opacity / outline opacity
setTexData(startIndex + 25, uTroikaFillOpacity.value)
// Curve radius
setTexData(startIndex + 26, uTroikaCurveRadius.value)
if (isOutline) {
// Outline properties
setTexData(startIndex + 28, uTroikaPositionOffset.value.x);
setTexData(startIndex + 29, uTroikaPositionOffset.value.y);
setTexData(startIndex + 30, uTroikaEdgeOffset.value);
setTexData(startIndex + 31, uTroikaBlurRadius.value);
} else {
// Stroke properties
setTexData(startIndex + 28, uTroikaStrokeWidth.value);
setTexData(startIndex + 29, tempColor.set(uTroikaStrokeColor.value).getHex());
setTexData(startIndex + 30, uTroikaStrokeOpacity.value);
}
}
});
material.setMatrixTexture(texture);
// For the non-member-specific uniforms:
super._prepareForRender(material);
}
sync (callback) {
// TODO: skip members updating their geometries, just use textRenderInfo directly
// Trigger sync on all members that need it
let syncPromises = this._needsRepack ? [] : null;
this._needsRepack = false;
this._members.forEach((packingInfo, text) => {
if (packingInfo.dirty || text._needsSync) {
packingInfo.dirty = false;
(syncPromises || (syncPromises = [])).push(new Promise(resolve => {
if (text._needsSync) {
text.sync(resolve);
} else {
resolve();
}
}));
}
});
// If any needed syncing, wait for them and then repack the batched geometry
if (syncPromises) {
this.dispatchEvent(syncStartEvent);
Promise.all(syncPromises).then(() => {
const { geometry } = this;
const batchedAttributes = geometry.attributes;
let memberIndexes = batchedAttributes[memberIndexAttrName] && batchedAttributes[memberIndexAttrName].array || new Uint16Array(0);
let batchedGlyphIndexes = batchedAttributes[glyphIndexAttrName] && batchedAttributes[glyphIndexAttrName].array || new Float32Array(0);
let batchedGlyphBounds = batchedAttributes[glyphBoundsAttrName] && batchedAttributes[glyphBoundsAttrName].array || new Float32Array(0);
// Initial pass to collect total glyph count and resize the arrays if needed
let totalGlyphCount = 0;
this._members.forEach((packingInfo, { textRenderInfo }) => {
if (textRenderInfo) {
totalGlyphCount += textRenderInfo.glyphAtlasIndices.length;
this._textRenderInfo = textRenderInfo; // TODO - need this, but be smarter
}
});
if (totalGlyphCount !== memberIndexes.length) {
memberIndexes = cloneAndResize(memberIndexes, totalGlyphCount);
batchedGlyphIndexes = cloneAndResize(batchedGlyphIndexes, totalGlyphCount);
batchedGlyphBounds = cloneAndResize(batchedGlyphBounds, totalGlyphCount * 4);
}
// Populate batch arrays
let memberIndex = 0;
let glyphIndex = 0;
this._members.forEach((packingInfo, { textRenderInfo }) => {
if (textRenderInfo) {
const glyphCount = textRenderInfo.glyphAtlasIndices.length;
memberIndexes.fill(memberIndex, glyphIndex, glyphIndex + glyphCount);
// TODO can skip these for members that are not dirty or shifting overall position:
batchedGlyphIndexes.set(textRenderInfo.glyphAtlasIndices, glyphIndex, glyphIndex + glyphCount);
batchedGlyphBounds.set(textRenderInfo.glyphBounds, glyphIndex * 4, (glyphIndex + glyphCount) * 4);
glyphIndex += glyphCount;
packingInfo.index = memberIndex++;
}
});
// Update the geometry attributes
geometry.updateAttributeData(memberIndexAttrName, memberIndexes, 1);
geometry.getAttribute(memberIndexAttrName).setUsage(DynamicDrawUsage);
geometry.updateAttributeData(glyphIndexAttrName, batchedGlyphIndexes, 1);
geometry.updateAttributeData(glyphBoundsAttrName, batchedGlyphBounds, 4);
this.updateBounds();
this.dispatchEvent(syncCompleteEvent);
if (callback) {
callback();
}
});
}
}
copy (source) {
if (source instanceof BatchedText) {
super.copy(source);
this._members.forEach((_, text) => this.removeText(text));
source._members.forEach((_, text) => this.addText(text));
}
return this;
}
dispose () {
super.dispose();
Object.values(this._dataTextures).forEach(tex => tex.dispose())
}
}
function cloneAndResize (source, newLength) {
const copy = new source.constructor(newLength);
copy.set(source.subarray(0, newLength));
return copy;
}
function createBatchedTextMaterial (baseMaterial) {
const texUniformName = "uTroikaMatricesTexture";
const texSizeUniformName = "uTroikaMatricesTextureSize";
// Due to how vertexTransform gets injected, the matrix transforms must happen
// in the base material of TextDerivedMaterial, but other transforms to its
// shader must come after, so we sandwich it between two derivations.
// Transform the vertex position
let batchMaterial = createDerivedMaterial(baseMaterial, {
chained: true,
uniforms: {
[texSizeUniformName]: { value: new Vector2() },
[texUniformName]: { value: null }
},
// language=GLSL
vertexDefs: `
uniform highp sampler2D ${texUniformName};
uniform vec2 ${texSizeUniformName};
attribute float ${memberIndexAttrName};
vec4 troikaBatchTexel(float offset) {
offset += ${memberIndexAttrName} * ${floatsPerMember.toFixed(1)} / 4.0;
float w = ${texSizeUniformName}.x;
vec2 uv = (vec2(mod(offset, w), floor(offset / w)) + 0.5) / ${texSizeUniformName};
return texture2D(${texUniformName}, uv);
}
`,
// language=GLSL prefix="void main() {" suffix="}"
vertexTransform: `
mat4 matrix = mat4(
troikaBatchTexel(0.0),
troikaBatchTexel(1.0),
troikaBatchTexel(2.0),
troikaBatchTexel(3.0)
);
position.xyz = (matrix * vec4(position, 1.0)).xyz;
`,
});
// Add the text shaders
batchMaterial = createTextDerivedMaterial(batchMaterial);
// Now make other changes to the derived text shader code
batchMaterial = createDerivedMaterial(batchMaterial, {
chained: true,
uniforms: {
uTroikaIsOutline: {value: false},
},
customRewriter(shaders) {
// Convert some text shader uniforms to varyings
const varyingUniforms = [
'uTroikaTotalBounds',
'uTroikaClipRect',
'uTroikaPositionOffset',
'uTroikaEdgeOffset',
'uTroikaBlurRadius',
'uTroikaStrokeWidth',
'uTroikaStrokeColor',
'uTroikaStrokeOpacity',
'uTroikaFillOpacity',
'uTroikaCurveRadius',
'diffuse'
]
varyingUniforms.forEach(uniformName => {
shaders = uniformToVarying(shaders, uniformName)
})
return shaders
},
// language=GLSL
vertexDefs: `
uniform bool uTroikaIsOutline;
vec3 troikaFloatToColor(float v) {
return mod(floor(vec3(v / 65536.0, v / 256.0, v)), 256.0) / 256.0;
}
`,
// language=GLSL prefix="void main() {" suffix="}"
vertexTransform: `
uTroikaTotalBounds = troikaBatchTexel(4.0);
uTroikaClipRect = troikaBatchTexel(5.0);
vec4 data = troikaBatchTexel(6.0);
diffuse = troikaFloatToColor(data.x);
uTroikaFillOpacity = data.y;
uTroikaCurveRadius = data.z;
data = troikaBatchTexel(7.0);
if (uTroikaIsOutline) {
if (data == vec4(0.0)) { // degenerate if zero outline
position = vec3(0.0);
} else {
uTroikaPositionOffset = data.xy;
uTroikaEdgeOffset = data.z;
uTroikaBlurRadius = data.w;
}
} else {
uTroikaStrokeWidth = data.x;
uTroikaStrokeColor = troikaFloatToColor(data.y);
uTroikaStrokeOpacity = data.z;
}
`,
});
batchMaterial.setMatrixTexture = (texture) => {
batchMaterial.uniforms[texUniformName].value = texture;
batchMaterial.uniforms[texSizeUniformName].value.set(texture.image.width, texture.image.height);
};
return batchMaterial;
}
/**
* Turn a uniform into a varying/writeable value.
* - If the uniform was used in the fragment shader, it will become a varying in both shaders.
* - If the uniform was only used in the vertex shader, it will become a writeable var.
*/
export function uniformToVarying({vertexShader, fragmentShader}, uniformName, varyingName = uniformName) {
const uniformRE = new RegExp(`uniform\\s+(bool|float|vec[234]|mat[34])\\s+${uniformName}\\b`)
let type
let hadFragmentUniform = false
fragmentShader = fragmentShader.replace(uniformRE, ($0, $1) => {
hadFragmentUniform = true
return `varying ${type = $1} ${varyingName}`
})
let hadVertexUniform = false
vertexShader = vertexShader.replace(uniformRE, (_, $1) => {
hadVertexUniform = true
return `${hadFragmentUniform ? 'varying' : ''} ${type = $1} ${varyingName}`
})
if (!hadVertexUniform) {
vertexShader = `${hadFragmentUniform ? 'varying' : ''} ${type} ${varyingName};\n${vertexShader}`
}
return {vertexShader, fragmentShader}
}
+432
View File
@@ -0,0 +1,432 @@
/**
* A factory wrapper parsing a font file using Typr.
* Also adds support for WOFF files (not WOFF2).
*/
import typrFactory from '../libs/typr.factory.js'
import woff2otfFactory from '../libs/woff2otf.factory.js'
import { defineWorkerModule } from 'troika-worker-utils'
/**
* @typedef ParsedFont
* @property {number} ascender
* @property {number} descender
* @property {number} xHeight
* @property {(number) => boolean} supportsCodePoint
* @property {(text:string, fontSize:number, letterSpacing:number, callback) => number} forEachGlyph
* @property {number} lineGap
* @property {number} capHeight
* @property {number} unitsPerEm
*/
/**
* @typedef {(buffer: ArrayBuffer) => ParsedFont} FontParser
*/
/**
* @returns {FontParser}
*/
function parserFactory(Typr, woff2otf) {
const cmdArgLengths = {
M: 2,
L: 2,
Q: 4,
C: 6,
Z: 0
}
// {joinType: "skip+step,..."}
const joiningTypeRawData = {"C":"18g,ca,368,1kz","D":"17k,6,2,2+4,5+c,2+6,2+1,10+1,9+f,j+11,2+1,a,2,2+1,15+2,3,j+2,6+3,2+8,2,2,2+1,w+a,4+e,3+3,2,3+2,3+5,23+w,2f+4,3,2+9,2,b,2+3,3,1k+9,6+1,3+1,2+2,2+d,30g,p+y,1,1+1g,f+x,2,sd2+1d,jf3+4,f+3,2+4,2+2,b+3,42,2,4+2,2+1,2,3,t+1,9f+w,2,el+2,2+g,d+2,2l,2+1,5,3+1,2+1,2,3,6,16wm+1v","R":"17m+3,2,2,6+3,m,15+2,2+2,h+h,13,3+8,2,2,3+1,2,p+1,x,5+4,5,a,2,2,3,u,c+2,g+1,5,2+1,4+1,5j,6+1,2,b,2+2,f,2+1,1s+2,2,3+1,7,1ez0,2,2+1,4+4,b,4,3,b,42,2+2,4,3,2+1,2,o+3,ae,ep,x,2o+2,3+1,3,5+1,6","L":"x9u,jff,a,fd,jv","T":"4t,gj+33,7o+4,1+1,7c+18,2,2+1,2+1,2,21+a,2,1b+k,h,2u+6,3+5,3+1,2+3,y,2,v+q,2k+a,1n+8,a,p+3,2+8,2+2,2+4,18+2,3c+e,2+v,1k,2,5+7,5,4+6,b+1,u,1n,5+3,9,l+1,r,3+1,1m,5+1,5+1,3+2,4,v+1,4,c+1,1m,5+4,2+1,5,l+1,n+5,2,1n,3,2+3,9,8+1,c+1,v,1q,d,1f,4,1m+2,6+2,2+3,8+1,c+1,u,1n,3,7,6+1,l+1,t+1,1m+1,5+3,9,l+1,u,21,8+2,2,2j,3+6,d+7,2r,3+8,c+5,23+1,s,2,2,1k+d,2+4,2+1,6+a,2+z,a,2v+3,2+5,2+1,3+1,q+1,5+2,h+3,e,3+1,7,g,jk+2,qb+2,u+2,u+1,v+1,1t+1,2+6,9,3+a,a,1a+2,3c+1,z,3b+2,5+1,a,7+2,64+1,3,1n,2+6,2,2,3+7,7+9,3,1d+d,1,1+1,1s+3,1d,2+4,2,6,15+8,d+1,x+3,3+1,2+2,1l,2+1,4,2+2,1n+7,3+1,49+2,2+c,2+6,5,7,4+1,5j+1l,2+4,ek,3+1,r+4,1e+4,6+5,2p+c,1+3,1,1+2,1+b,2db+2,3y,2p+v,ff+3,30+1,n9x,1+2,2+9,x+1,29+1,7l,4,5,q+1,6,48+1,r+h,e,13+7,q+a,1b+2,1d,3+3,3+1,14,1w+5,3+1,3+1,d,9,1c,1g,2+2,3+1,6+1,2,17+1,9,6n,3,5,fn5,ki+f,h+f,5s,6y+2,ea,6b,46+4,1af+2,2+1,6+3,15+2,5,4m+1,fy+3,as+1,4a+a,4x,1j+e,1l+2,1e+3,3+1,1y+2,11+4,2+7,1r,d+1,1h+8,b+3,3,2o+2,3,2+1,7,4h,4+7,m+1,1m+1,4,12+6,4+4,5g+7,3+2,2,o,2d+5,2,5+1,2+1,6n+3,7+1,2+1,s+1,2e+7,3,2+1,2z,2,3+5,2,2u+2,3+3,2+4,78+8,2+1,75+1,2,5,41+3,3+1,5,x+9,15+5,3+3,9,a+5,3+2,1b+c,2+1,bb+6,2+5,2,2b+l,3+6,2+1,2+1,3f+5,4,2+1,2+6,2,21+1,4,2,9o+1,470+8,at4+4,1o+6,t5,1s+3,2a,f5l+1,2+3,43o+2,a+7,1+7,3+6,v+3,45+2,1j0+1i,5+1d,9,f,n+4,2+e,11t+6,2+g,3+6,2+1,2+4,7a+6,c6+3,15t+6,32+6,1,gzau,v+2n,3l+6n"}
const JT_LEFT = 1, //indicates that a character joins with the subsequent character, but does not join with the preceding character.
JT_RIGHT = 2, //indicates that a character joins with the preceding character, but does not join with the subsequent character.
JT_DUAL = 4, //indicates that a character joins with the preceding character and joins with the subsequent character.
JT_TRANSPARENT = 8, //indicates that the character does not join with adjacent characters and that the character must be skipped over when the shaping engine is evaluating the joining positions in a sequence of characters. When a JT_TRANSPARENT character is encountered in a sequence, the JOINING_TYPE of the preceding character passes through. Diacritical marks are frequently assigned this value.
JT_JOIN_CAUSING = 16, //indicates that the character forces the use of joining forms with the preceding and subsequent characters. Kashidas and the Zero Width Joiner (U+200D) are both JOIN_CAUSING characters.
JT_NON_JOINING = 32 //indicates that a character does not join with the preceding or with the subsequent character.,
let joiningTypeMap
function getCharJoiningType(ch) {
if (!joiningTypeMap) {
const m = {
R: JT_RIGHT,
L: JT_LEFT,
D: JT_DUAL,
C: JT_JOIN_CAUSING,
U: JT_NON_JOINING,
T: JT_TRANSPARENT
}
joiningTypeMap = new Map()
for (let type in joiningTypeRawData) {
let lastCode = 0
joiningTypeRawData[type].split(',').forEach(range => {
let [skip, step] = range.split('+')
skip = parseInt(skip,36)
step = step ? parseInt(step, 36) : 0
joiningTypeMap.set(lastCode += skip, m[type])
for (let i = step; i--;) {
joiningTypeMap.set(++lastCode, m[type])
}
})
}
}
return joiningTypeMap.get(ch) || JT_NON_JOINING
}
const ISOL = 1, INIT = 2, FINA = 3, MEDI = 4
const formsToFeatures = [null, 'isol', 'init', 'fina', 'medi']
function detectJoiningForms(str) {
// This implements the algorithm described here:
// https://github.com/n8willis/opentype-shaping-documents/blob/master/opentype-shaping-arabic-general.md
const joiningForms = new Uint8Array(str.length)
let prevJoiningType = JT_NON_JOINING
let prevForm = ISOL
let prevIndex = -1
for (let i = 0; i < str.length; i++) {
const code = str.codePointAt(i)
let joiningType = getCharJoiningType(code) | 0
let form = ISOL
if (joiningType & JT_TRANSPARENT) {
continue
}
if (prevJoiningType & (JT_LEFT | JT_DUAL | JT_JOIN_CAUSING)) {
if (joiningType & (JT_RIGHT | JT_DUAL | JT_JOIN_CAUSING)) {
form = FINA
// isol->init, fina->medi
if (prevForm === ISOL || prevForm === FINA) {
joiningForms[prevIndex]++
}
}
else if (joiningType & (JT_LEFT | JT_NON_JOINING)) {
// medi->fina, init->isol
if (prevForm === INIT || prevForm === MEDI) {
joiningForms[prevIndex]--
}
}
}
else if (prevJoiningType & (JT_RIGHT | JT_NON_JOINING)) {
// medi->fina, init->isol
if (prevForm === INIT || prevForm === MEDI) {
joiningForms[prevIndex]--
}
}
prevForm = joiningForms[i] = form
prevJoiningType = joiningType
prevIndex = i
if (code > 0xffff) i++
}
// console.log(str.split('').map(ch => ch.codePointAt(0).toString(16)))
// console.log(str.split('').map(ch => getCharJoiningType(ch.codePointAt(0))))
// console.log(Array.from(joiningForms).map(f => formsToFeatures[f] || 'none'))
return joiningForms
}
function stringToGlyphs (font, str) {
const glyphIds = []
for (let i = 0; i < str.length; i++) {
const cc = str.codePointAt(i)
if (cc > 0xffff) i++
glyphIds.push(Typr.U.codeToGlyph(font, cc))
}
const gsub = font['GSUB']
if (gsub) {
const {lookupList, featureList} = gsub
let joiningForms
const supportedFeatures = /^(rlig|liga|mset|isol|init|fina|medi|half|pres|blws|ccmp)$/
const usedLookups = []
featureList.forEach(feature => {
if (supportedFeatures.test(feature.tag)) {
for (let ti = 0; ti < feature.tab.length; ti++) {
if (usedLookups[feature.tab[ti]]) continue
usedLookups[feature.tab[ti]] = true
const tab = lookupList[feature.tab[ti]]
const isJoiningFeature = /^(isol|init|fina|medi)$/.test(feature.tag)
if (isJoiningFeature && !joiningForms) { //lazy
joiningForms = detectJoiningForms(str)
}
for (let ci = 0; ci < glyphIds.length; ci++) {
if (!joiningForms || !isJoiningFeature || formsToFeatures[joiningForms[ci]] === feature.tag) {
Typr.U._applySubs(glyphIds, ci, tab, lookupList)
}
}
}
}
})
}
return glyphIds
}
// Calculate advances and x/y offsets for each glyph, e.g. kerning and mark
// attachments. This is a more complete version of Typr.U.getPairAdjustment
// and should become an upstream replacement eventually.
function calcGlyphPositions(font, glyphIds) {
const positions = new Int16Array(glyphIds.length * 3); // [offsetX, offsetY, advanceX, ...]
let glyphIndex = 0;
for (; glyphIndex < glyphIds.length; glyphIndex++) {
const glyphId = glyphIds[glyphIndex]
if (glyphId === -1) continue;
positions[glyphIndex * 3 + 2] = font.hmtx.aWidth[glyphId]; // populate advanceX in...advance.
const gpos = font.GPOS;
if (gpos) {
const llist = gpos.lookupList;
for (let i = 0; i < llist.length; i++) {
const lookup = llist[i];
for (let j = 0; j < lookup.tabs.length; j++) {
const tab = lookup.tabs[j];
// Single char placement
if (lookup.ltype === 1) {
const ind = Typr._lctf.coverageIndex(tab.coverage, glyphId);
if (ind !== -1 && tab.pos) {
applyValueRecord(tab.pos, glyphIndex)
break
}
}
// Pairs (kerning)
else if (lookup.ltype === 2) {
let adj = null;
let prevGlyphIndex = getPrevGlyphIndex()
if (prevGlyphIndex !== -1) {
const coverageIndex = Typr._lctf.coverageIndex(tab.coverage, glyphIds[prevGlyphIndex]);
if (coverageIndex !== -1) {
if (tab.fmt === 1) {
const right = tab.pairsets[coverageIndex];
for (let k = 0; k < right.length; k++) {
if (right[k].gid2 === glyphId) adj = right[k];
}
} else if (tab.fmt === 2) {
const c1 = Typr.U._getGlyphClass(glyphIds[prevGlyphIndex], tab.classDef1);
const c2 = Typr.U._getGlyphClass(glyphId, tab.classDef2);
adj = tab.matrix[c1][c2];
}
if (adj) {
if (adj.val1) applyValueRecord(adj.val1, prevGlyphIndex)
if (adj.val2) applyValueRecord(adj.val2, glyphIndex)
break
}
}
}
}
// Mark to base
else if (lookup.ltype === 4) {
const markArrIndex = Typr._lctf.coverageIndex(tab.markCoverage, glyphId);
if (markArrIndex !== -1) {
const baseGlyphIndex = getPrevGlyphIndex(isBaseGlyph);
const baseArrIndex = baseGlyphIndex === -1 ? -1 : Typr._lctf.coverageIndex(tab.baseCoverage, glyphIds[baseGlyphIndex])
if (baseArrIndex !== -1) {
const markRecord = tab.markArray[markArrIndex];
const baseAnchor = tab.baseArray[baseArrIndex][markRecord.markClass];
positions[glyphIndex * 3] = baseAnchor.x - markRecord.x + positions[baseGlyphIndex * 3] - positions[baseGlyphIndex * 3 + 2]
positions[glyphIndex * 3 + 1] = baseAnchor.y - markRecord.y + positions[baseGlyphIndex * 3 + 1];
break;
}
}
}
// Mark to mark
else if (lookup.ltype === 6) {
const mark1ArrIndex = Typr._lctf.coverageIndex(tab.mark1Coverage, glyphId);
if (mark1ArrIndex !== -1) {
const prevGlyphIndex = getPrevGlyphIndex();
if (prevGlyphIndex !== -1) {
const prevGlyphId = glyphIds[prevGlyphIndex]
if (getGlyphClass(font, prevGlyphId) === 3) { // only check mark glyphs
const mark2ArrIndex = Typr._lctf.coverageIndex(tab.mark2Coverage, prevGlyphId)
if (mark2ArrIndex !== -1) {
const mark1Record = tab.mark1Array[mark1ArrIndex];
const mark2Anchor = tab.mark2Array[mark2ArrIndex][mark1Record.markClass];
positions[glyphIndex * 3] = mark2Anchor.x - mark1Record.x + positions[prevGlyphIndex * 3] - positions[prevGlyphIndex * 3 + 2];
positions[glyphIndex * 3 + 1] = mark2Anchor.y - mark1Record.y + positions[prevGlyphIndex * 3 + 1];
break;
}
}
}
}
}
}
}
}
// Check kern table if no GPOS
else if (font.kern && !font.cff) {
const prevGlyphIndex = getPrevGlyphIndex();
if (prevGlyphIndex !== -1) {
const ind1 = font.kern.glyph1.indexOf(glyphIds[prevGlyphIndex]);
if (ind1 !== -1) {
const ind2 = font.kern.rval[ind1].glyph2.indexOf(glyphId);
if (ind2 !== -1) {
positions[prevGlyphIndex * 3 + 2] += font.kern.rval[ind1].vals[ind2];
}
}
}
}
}
return positions;
function getPrevGlyphIndex(filter) {
for (let i = glyphIndex - 1; i >=0; i--) {
if (glyphIds[i] !== -1 && (!filter || filter(glyphIds[i]))) {
return i
}
}
return -1;
}
function isBaseGlyph(glyphId) {
return getGlyphClass(font, glyphId) === 1;
}
function applyValueRecord(source, gi) {
for (let i = 0; i < 3; i++) {
positions[gi * 3 + i] += source[i] || 0
}
}
}
function getGlyphClass(font, glyphId) {
const classDef = font.GDEF && font.GDEF.glyphClassDef
return classDef ? Typr.U._getGlyphClass(glyphId, classDef) : 0;
}
function firstNum(...args) {
for (let i = 0; i < args.length; i++) {
if (typeof args[i] === 'number') {
return args[i]
}
}
}
/**
* @returns ParsedFont
*/
function wrapFontObj(typrFont) {
const glyphMap = Object.create(null)
const os2 = typrFont['OS/2']
const hhea = typrFont.hhea
const unitsPerEm = typrFont.head.unitsPerEm
const ascender = firstNum(os2 && os2.sTypoAscender, hhea && hhea.ascender, unitsPerEm)
/** @type ParsedFont */
const fontObj = {
unitsPerEm,
ascender,
descender: firstNum(os2 && os2.sTypoDescender, hhea && hhea.descender, 0),
capHeight: firstNum(os2 && os2.sCapHeight, ascender),
xHeight: firstNum(os2 && os2.sxHeight, ascender),
lineGap: firstNum(os2 && os2.sTypoLineGap, hhea && hhea.lineGap),
supportsCodePoint(code) {
return Typr.U.codeToGlyph(typrFont, code) > 0
},
forEachGlyph(text, fontSize, letterSpacing, callback) {
let penX = 0
const fontScale = 1 / fontObj.unitsPerEm * fontSize
const glyphIds = stringToGlyphs(typrFont, text)
let charIndex = 0
const positions = calcGlyphPositions(typrFont, glyphIds)
glyphIds.forEach((glyphId, i) => {
// Typr returns a glyph index per string codepoint, with -1s in place of those that
// were omitted due to ligature substitution. So we can track original index in the
// string via simple increment, and skip everything else when seeing a -1.
if (glyphId !== -1) {
let glyphObj = glyphMap[glyphId]
if (!glyphObj) {
const {cmds, crds} = Typr.U.glyphToPath(typrFont, glyphId)
// Build path string
let path = ''
let crdsIdx = 0
for (let i = 0, len = cmds.length; i < len; i++) {
const numArgs = cmdArgLengths[cmds[i]]
path += cmds[i]
for (let j = 1; j <= numArgs; j++) {
path += (j > 1 ? ',' : '') + crds[crdsIdx++]
}
}
// Find extents - Glyf gives this in metadata but not CFF, and Typr doesn't
// normalize the two, so it's simplest just to iterate ourselves.
let xMin, yMin, xMax, yMax
if (crds.length) {
xMin = yMin = Infinity
xMax = yMax = -Infinity
for (let i = 0, len = crds.length; i < len; i += 2) {
let x = crds[i]
let y = crds[i + 1]
if (x < xMin) xMin = x
if (y < yMin) yMin = y
if (x > xMax) xMax = x
if (y > yMax) yMax = y
}
} else {
xMin = xMax = yMin = yMax = 0
}
glyphObj = glyphMap[glyphId] = {
index: glyphId,
advanceWidth: typrFont.hmtx.aWidth[glyphId],
xMin,
yMin,
xMax,
yMax,
path,
}
}
callback.call(
null,
glyphObj,
penX + positions[i * 3] * fontScale,
positions[i * 3 + 1] * fontScale,
charIndex
)
penX += positions[i * 3 + 2] * fontScale
if (letterSpacing) {
penX += letterSpacing * fontSize
}
}
charIndex += (text.codePointAt(charIndex) > 0xffff ? 2 : 1)
})
return penX
}
}
return fontObj
}
/**
* @type FontParser
*/
return function parse(buffer) {
// Look to see if we have a WOFF file and convert it if so:
const peek = new Uint8Array(buffer, 0, 4)
const tag = Typr._bin.readASCII(peek, 0, 4)
if (tag === 'wOFF') {
buffer = woff2otf(buffer)
} else if (tag === 'wOF2') {
throw new Error('woff2 fonts not supported')
}
return wrapFontObj(Typr.parse(buffer)[0])
}
}
const workerModule = /*#__PURE__*/defineWorkerModule({
name: 'Typr Font Parser',
dependencies: [typrFactory, woff2otfFactory, parserFactory],
init(typrFactory, woff2otfFactory, parserFactory) {
const Typr = typrFactory()
const woff2otf = woff2otfFactory()
return parserFactory(Typr, woff2otf)
}
})
export default workerModule
+276
View File
@@ -0,0 +1,276 @@
import fontParser from "./FontParser.js";
import unicodeFontResolverClientFactory from "../libs/unicode-font-resolver-client.factory.js";
import { defineWorkerModule } from "troika-worker-utils";
/**
* @typedef {string | {src:string, label?:string, unicodeRange?:string, lang?:string}} UserFont
*/
/**
* @typedef {ClientOptions} FontResolverOptions
* @property {Array<UserFont>|UserFont} [fonts]
* @property {'normal'|'italic'} [style]
* @property {'normal'|'bold'|number} [style]
* @property {string} [unicodeFontsURL]
*/
/**
* @typedef {Object} FontResolverResult
* @property {Uint8Array} chars
* @property {Array<ParsedFont & {src:string}>} fonts
*/
/**
* @typedef {function} FontResolver
* @param {string} text
* @param {(FontResolverResult) => void} callback
* @param {FontResolverOptions} [options]
*/
/**
* Factory for the FontResolver function.
* @param {FontParser} fontParser
* @param {{getFontsForString: function, CodePointSet: function}} unicodeFontResolverClient
* @return {FontResolver}
*/
export function createFontResolver(fontParser, unicodeFontResolverClient) {
/**
* @type {Record<string, ParsedFont>}
*/
const parsedFonts = Object.create(null)
/**
* @type {Record<string, Array<(ParsedFont) => void>>}
*/
const loadingFonts = Object.create(null)
/**
* Load a given font url
*/
function doLoadFont(url, callback) {
const onError = err => {
console.error(`Failure loading font ${url}`, err)
}
try {
const request = new XMLHttpRequest()
request.open('get', url, true)
request.responseType = 'arraybuffer'
request.onload = function () {
if (request.status >= 400) {
onError(new Error(request.statusText))
}
else if (request.status > 0) {
try {
const fontObj = fontParser(request.response)
fontObj.src = url;
callback(fontObj)
} catch (e) {
onError(e)
}
}
}
request.onerror = onError
request.send()
} catch(err) {
onError(err)
}
}
/**
* Load a given font url if needed, invoking a callback when it's loaded. If already
* loaded, the callback will be called synchronously.
* @param {string} fontUrl
* @param {(font: ParsedFont) => void} callback
*/
function loadFont(fontUrl, callback) {
let font = parsedFonts[fontUrl]
if (font) {
callback(font)
} else if (loadingFonts[fontUrl]) {
loadingFonts[fontUrl].push(callback)
} else {
loadingFonts[fontUrl] = [callback]
doLoadFont(fontUrl, fontObj => {
fontObj.src = fontUrl
parsedFonts[fontUrl] = fontObj
loadingFonts[fontUrl].forEach(cb => cb(fontObj))
delete loadingFonts[fontUrl];
})
}
}
/**
* For a given string of text, determine which fonts are required to fully render it and
* ensure those fonts are loaded.
*/
return function (text, callback, {
lang,
fonts: userFonts = [],
style = 'normal',
weight = 'normal',
unicodeFontsURL
} = {}) {
const charResolutions = new Uint8Array(text.length);
const fontResolutions = [];
if (!text.length) {
allDone()
}
const fontIndices = new Map();
const fallbackRanges = [] // [[start, end], ...]
if (style !== 'italic') style = 'normal'
if (typeof weight !== 'number') {
weight = weight === 'bold' ? 700 : 400
}
if (userFonts && !Array.isArray(userFonts)) {
userFonts = [userFonts]
}
userFonts = userFonts.slice()
// filter by language
.filter(def => !def.lang || def.lang.test(lang))
// switch order for easier iteration
.reverse()
if (userFonts.length) {
const UNKNOWN = 0
const RESOLVED = 1
const NEEDS_FALLBACK = 2
let prevCharResult = UNKNOWN
;(function resolveUserFonts (startIndex = 0) {
for (let i = startIndex, iLen = text.length; i < iLen; i++) {
const codePoint = text.codePointAt(i)
// Carry previous character's result forward if:
// - it resolved to a font that also covers this character
// - this character is whitespace
if (
(prevCharResult === RESOLVED && fontResolutions[charResolutions[i - 1]].supportsCodePoint(codePoint)) ||
(i > 0 && /\s/.test(text[i]))
) {
charResolutions[i] = charResolutions[i - 1]
if (prevCharResult === NEEDS_FALLBACK) {
fallbackRanges[fallbackRanges.length - 1][1] = i
}
} else {
for (let j = charResolutions[i], jLen = userFonts.length; j <= jLen; j++) {
if (j === jLen) {
// none of the user fonts matched; needs fallback
const range = prevCharResult === NEEDS_FALLBACK ?
fallbackRanges[fallbackRanges.length - 1] :
(fallbackRanges[fallbackRanges.length] = [i, i])
range[1] = i;
prevCharResult = NEEDS_FALLBACK;
} else {
charResolutions[i] = j;
const { src, unicodeRange } = userFonts[j];
// filter by optional explicit unicode ranges
if (!unicodeRange || isCodeInRanges(codePoint, unicodeRange)) {
const fontObj = parsedFonts[src];
// font not yet loaded, load it and resume
if (!fontObj) {
loadFont(src, () => {
resolveUserFonts(i);
});
return;
}
// if the font actually contains a glyph for this char, lock it in
if (fontObj.supportsCodePoint(codePoint)) {
let fontIndex = fontIndices.get(fontObj);
if (typeof fontIndex !== 'number') {
fontIndex = fontResolutions.length;
fontResolutions.push(fontObj);
fontIndices.set(fontObj, fontIndex);
}
charResolutions[i] = fontIndex;
prevCharResult = RESOLVED;
break;
}
}
}
}
}
if (codePoint > 0xffff && i + 1 < iLen) {
charResolutions[i + 1] = charResolutions[i]
i++
if (prevCharResult === NEEDS_FALLBACK) {
fallbackRanges[fallbackRanges.length - 1][1] = i
}
}
}
resolveFallbacks();
})();
} else {
fallbackRanges.push([0, text.length - 1])
resolveFallbacks();
}
function resolveFallbacks() {
if (fallbackRanges.length) {
// Combine all fallback substrings into a single string for querying
const fallbackString = fallbackRanges.map(range => text.substring(range[0], range[1] + 1)).join('\n')
unicodeFontResolverClient.getFontsForString(fallbackString, {
lang: lang || undefined,
style,
weight,
dataUrl: unicodeFontsURL
}).then(({fontUrls, chars}) => {
// Extract results and put them back in the main array
const fontIndexOffset = fontResolutions.length
let charIdx = 0;
fallbackRanges.forEach(range => {
for (let i = 0, endIdx = range[1] - range[0]; i <= endIdx; i++) {
charResolutions[range[0] + i] = chars[charIdx++] + fontIndexOffset
}
charIdx++ //skip segment separator
})
// Load and parse the fallback fonts - avoiding Promise here to prevent polyfills in the worker
let loadedCount = 0;
fontUrls.forEach((url, i) => {
loadFont(url, fontObj => {
fontResolutions[i + fontIndexOffset] = fontObj
if (++loadedCount === fontUrls.length) {
allDone();
}
})
})
});
} else {
allDone();
}
}
function allDone() {
callback({
chars: charResolutions,
fonts: fontResolutions
})
}
function isCodeInRanges(code, ranges) {
// todo optimize search - CodePointSet from unicode-font-resolver?
for (let k = 0; k < ranges.length; k++) {
const [start, end = start] = ranges[k]
if (start <= code && code <= end) {
return true
}
}
return false
}
}
}
export const fontResolverWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'FontResolver',
dependencies: [
createFontResolver,
fontParser,
unicodeFontResolverClientFactory,
],
init(createFontResolver, fontParser, unicodeFontResolverClientFactory) {
return createFontResolver(fontParser, unicodeFontResolverClientFactory());
}
})
+218
View File
@@ -0,0 +1,218 @@
import {
PlaneGeometry,
InstancedBufferGeometry,
InstancedBufferAttribute,
Sphere,
Box3,
} from 'three'
const templateGeometries = {}
function getTemplateGeometry(detail) {
let geom = templateGeometries[detail]
if (!geom) {
geom = templateGeometries[detail] = new PlaneGeometry(1, 1, detail, detail).translate(0.5, 0.5, 0)
}
return geom
}
const glyphBoundsAttrName = 'aTroikaGlyphBounds'
const glyphIndexAttrName = 'aTroikaGlyphIndex'
const glyphColorAttrName = 'aTroikaGlyphColor'
/**
@class GlyphsGeometry
A specialized Geometry for rendering a set of text glyphs. Uses InstancedBufferGeometry to
render the glyphs using GPU instancing of a single quad, rather than constructing a whole
geometry with vertices, for much smaller attribute arraybuffers according to this math:
Where N = number of glyphs...
Instanced:
- position: 4 * 3
- index: 2 * 3
- normal: 4 * 3
- uv: 4 * 2
- glyph x/y bounds: N * 4
- glyph indices: N * 1
= 5N + 38
Non-instanced:
- position: N * 4 * 3
- index: N * 2 * 3
- normal: N * 4 * 3
- uv: N * 4 * 2
- glyph indices: N * 1
= 39N
A downside of this is the rare-but-possible lack of the instanced arrays extension,
which we could potentially work around with a fallback non-instanced implementation.
*/
class GlyphsGeometry extends InstancedBufferGeometry {
constructor() {
super()
this.detail = 1
this.curveRadius = 0
// Define groups for rendering text outline as a separate pass; these will only
// be used when the `material` getter returns an array, i.e. outlineWidth > 0.
this.groups = [
{start: 0, count: Infinity, materialIndex: 0},
{start: 0, count: Infinity, materialIndex: 1}
]
// Preallocate empty bounding objects
this.boundingSphere = new Sphere()
this.boundingBox = new Box3()
}
computeBoundingSphere () {
// No-op; we'll sync the boundingSphere proactively when needed.
}
computeBoundingBox() {
// No-op; we'll sync the boundingBox proactively when needed.
}
set detail(detail) {
if (detail !== this._detail) {
this._detail = detail
if (typeof detail !== 'number' || detail < 1) {
detail = 1
}
let tpl = getTemplateGeometry(detail)
;['position', 'normal', 'uv'].forEach(attr => {
this.attributes[attr] = tpl.attributes[attr].clone()
})
this.setIndex(tpl.getIndex().clone())
}
}
get detail() {
return this._detail
}
set curveRadius(r) {
if (r !== this._curveRadius) {
this._curveRadius = r
this._updateBounds()
}
}
get curveRadius() {
return this._curveRadius
}
/**
* Update the geometry for a new set of glyphs.
* @param {Float32Array} glyphBounds - An array holding the planar bounds for all glyphs
* to be rendered, 4 entries for each glyph: x1,x2,y1,y1
* @param {Float32Array} glyphAtlasIndices - An array holding the index of each glyph within
* the SDF atlas texture.
* @param {Array} blockBounds - An array holding the [minX, minY, maxX, maxY] across all glyphs
* @param {Array} [chunkedBounds] - An array of objects describing bounds for each chunk of N
* consecutive glyphs: `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`. This can be
* used with `applyClipRect` to choose an optimized `instanceCount`.
* @param {Uint8Array} [glyphColors] - An array holding r,g,b values for each glyph.
*/
updateGlyphs(glyphBounds, glyphAtlasIndices, blockBounds, chunkedBounds, glyphColors) {
// Update the instance attributes
this.updateAttributeData(glyphBoundsAttrName, glyphBounds, 4)
this.updateAttributeData(glyphIndexAttrName, glyphAtlasIndices, 1)
this.updateAttributeData(glyphColorAttrName, glyphColors, 3)
this._blockBounds = blockBounds
this._chunkedBounds = chunkedBounds
this.instanceCount = glyphAtlasIndices.length
this._updateBounds()
}
_updateBounds() {
const bounds = this._blockBounds
if (bounds) {
const { curveRadius, boundingBox: bbox } = this
if (curveRadius) {
const { PI, floor, min, max, sin, cos } = Math
const halfPi = PI / 2
const twoPi = PI * 2
const absR = Math.abs(curveRadius)
const leftAngle = bounds[0] / absR
const rightAngle = bounds[2] / absR
const minX = floor((leftAngle + halfPi) / twoPi) !== floor((rightAngle + halfPi) / twoPi)
? -absR : min(sin(leftAngle) * absR, sin(rightAngle) * absR)
const maxX = floor((leftAngle - halfPi) / twoPi) !== floor((rightAngle - halfPi) / twoPi)
? absR : max(sin(leftAngle) * absR, sin(rightAngle) * absR)
const maxZ = floor((leftAngle + PI) / twoPi) !== floor((rightAngle + PI) / twoPi)
? absR * 2 : max(absR - cos(leftAngle) * absR, absR - cos(rightAngle) * absR)
bbox.min.set(minX, bounds[1], curveRadius < 0 ? -maxZ : 0)
bbox.max.set(maxX, bounds[3], curveRadius < 0 ? 0 : maxZ)
} else {
bbox.min.set(bounds[0], bounds[1], 0)
bbox.max.set(bounds[2], bounds[3], 0)
}
bbox.getBoundingSphere(this.boundingSphere)
}
}
/**
* Given a clipping rect, and the chunkedBounds from the last updateGlyphs call, choose the lowest
* `instanceCount` that will show all glyphs within the clipped view. This is an optimization
* for long blocks of text that are clipped, to skip vertex shader evaluation for glyphs that would
* be clipped anyway.
*
* Note that since `drawElementsInstanced[ANGLE]` only accepts an instance count and not a starting
* offset, this optimization becomes less effective as the clipRect moves closer to the end of the
* text block. We could fix that by switching from instancing to a full geometry with a drawRange,
* but at the expense of much larger attribute buffers (see classdoc above.)
*
* @param {Vector4} clipRect
*/
applyClipRect(clipRect) {
let count = this.getAttribute(glyphIndexAttrName).count
let chunks = this._chunkedBounds
if (chunks) {
for (let i = chunks.length; i--;) {
count = chunks[i].end
let rect = chunks[i].rect
// note: both rects are l-b-r-t
if (rect[1] < clipRect.w && rect[3] > clipRect.y && rect[0] < clipRect.z && rect[2] > clipRect.x) {
break
}
}
}
this.instanceCount = count
}
/**
* Utility for updating instance attributes with automatic resizing
*/
updateAttributeData(attrName, newArray, itemSize) {
const attr = this.getAttribute(attrName)
if (newArray) {
// If length isn't changing, just update the attribute's array data
if (attr && attr.array.length === newArray.length) {
attr.array.set(newArray)
attr.needsUpdate = true
} else {
this.setAttribute(attrName, new InstancedBufferAttribute(newArray, itemSize))
// If the new attribute has a different size, we also have to (as of r117) manually clear the
// internal cached max instance count. See https://github.com/mrdoob/three.js/issues/19706
// It's unclear if this is a threejs bug or a truly unsupported scenario; discussion in
// that ticket is ambiguous as to whether replacing a BufferAttribute with one of a
// different size is supported, but https://github.com/mrdoob/three.js/pull/17418 strongly
// implies it should be supported. It's possible we need to
delete this._maxInstanceCount //for r117+, could be fragile
this.dispose() //for r118+, more robust feeling, but more heavy-handed than I'd like
}
} else if (attr) {
this.deleteAttribute(attrName)
}
}
}
export {
GlyphsGeometry,
glyphBoundsAttrName,
glyphColorAttrName,
glyphIndexAttrName,
}
+137
View File
@@ -0,0 +1,137 @@
import { defineWorkerModule, terminateWorker } from 'troika-worker-utils'
import createSDFGenerator from 'webgl-sdf-generator'
const now = () => (self.performance || Date).now()
const mainThreadGenerator = /*#__PURE__*/ createSDFGenerator()
let warned
/**
* Generate an SDF texture image for a single glyph path, placing the result into a webgl canvas at a
* given location and channel. Utilizes the webgl-sdf-generator external package for GPU-accelerated SDF
* generation when supported.
*/
export function generateSDF(width, height, path, viewBox, distance, exponent, canvas, x, y, channel, useWebGL = true) {
// Allow opt-out
if (!useWebGL) {
return generateSDF_JS_Worker(width, height, path, viewBox, distance, exponent, canvas, x, y, channel)
}
// Attempt GPU-accelerated generation first
return generateSDF_GL(width, height, path, viewBox, distance, exponent, canvas, x, y, channel).then(
null,
err => {
// WebGL failed either due to a hard error or unexpected results; fall back to JS in workers
if (!warned) {
console.warn(`WebGL SDF generation failed, falling back to JS`, err)
warned = true
}
return generateSDF_JS_Worker(width, height, path, viewBox, distance, exponent, canvas, x, y, channel)
}
)
}
const queue = []
const chunkTimeBudget = 5 // ms
let timer = 0
function nextChunk() {
const start = now()
while (queue.length && now() - start < chunkTimeBudget) {
queue.shift()()
}
timer = queue.length ? setTimeout(nextChunk, 0) : 0
}
/**
* WebGL-based implementation executed on the main thread. Requests are executed in time-bounded
* macrotask chunks to allow render frames to execute in between.
*/
const generateSDF_GL = (...args) => {
return new Promise((resolve, reject) => {
queue.push(() => {
const start = now()
try {
mainThreadGenerator.webgl.generateIntoCanvas(...args)
resolve({ timing: now() - start })
} catch (err) {
reject(err)
}
})
if (!timer) {
timer = setTimeout(nextChunk, 0)
}
})
}
const threadCount = 4 // how many workers to spawn
const idleTimeout = 2000 // workers will be terminated after being idle this many milliseconds
const threads = {}
let callNum = 0
/**
* Fallback JS-based implementation, fanned out to a number of worker threads for parallelism
*/
function generateSDF_JS_Worker(width, height, path, viewBox, distance, exponent, canvas, x, y, channel) {
const workerId = 'TroikaTextSDFGenerator_JS_' + ((callNum++) % threadCount)
let thread = threads[workerId]
if (!thread) {
thread = threads[workerId] = {
workerModule: defineWorkerModule({
name: workerId,
workerId,
dependencies: [
createSDFGenerator,
now
],
init(_createSDFGenerator, now) {
const generate = _createSDFGenerator().javascript.generate
return function (...args) {
const start = now()
const textureData = generate(...args)
return {
textureData,
timing: now() - start
}
}
},
getTransferables(result) {
return [result.textureData.buffer]
}
}),
requests: 0,
idleTimer: null
}
}
thread.requests++
clearTimeout(thread.idleTimer)
return thread.workerModule(width, height, path, viewBox, distance, exponent)
.then(({ textureData, timing }) => {
// copy result data into the canvas
const start = now()
// expand single-channel data into rgba
const imageData = new Uint8Array(textureData.length * 4)
for (let i = 0; i < textureData.length; i++) {
imageData[i * 4 + channel] = textureData[i]
}
mainThreadGenerator.webglUtils.renderImageData(canvas, imageData, x, y, width, height, 1 << (3 - channel))
timing += now() - start
// clean up workers after a while
if (--thread.requests === 0) {
thread.idleTimer = setTimeout(() => { terminateWorker(workerId) }, idleTimeout)
}
return { timing }
})
}
export function warmUpSDFCanvas(canvas) {
if (!canvas._warm) {
mainThreadGenerator.webgl.isSupported(canvas)
canvas._warm = true
}
}
export const resizeWebGLCanvasWithoutClearing = mainThreadGenerator.webglUtils.resizeWebGLCanvasWithoutClearing
+800
View File
@@ -0,0 +1,800 @@
import {
Color,
DoubleSide,
Matrix4,
Mesh,
MeshBasicMaterial,
PlaneGeometry,
Vector3,
Vector2,
} from 'three'
import { GlyphsGeometry } from './GlyphsGeometry.js'
import { createTextDerivedMaterial } from './TextDerivedMaterial.js'
import { getTextRenderInfo } from './TextBuilder.js'
const defaultMaterial = /*#__PURE__*/ new MeshBasicMaterial({
color: 0xffffff,
side: DoubleSide,
transparent: true
})
const defaultStrokeColor = 0x808080
const tempMat4 = /*#__PURE__*/ new Matrix4()
const tempVec3a = /*#__PURE__*/ new Vector3()
const tempVec3b = /*#__PURE__*/ new Vector3()
const tempArray = []
const origin = /*#__PURE__*/ new Vector3()
const defaultOrient = '+x+y'
function first(o) {
return Array.isArray(o) ? o[0] : o
}
let getFlatRaycastMesh = () => {
const mesh = new Mesh(
new PlaneGeometry(1, 1),
defaultMaterial
)
getFlatRaycastMesh = () => mesh
return mesh
}
let getCurvedRaycastMesh = () => {
const mesh = new Mesh(
new PlaneGeometry(1, 1, 32, 1),
defaultMaterial
)
getCurvedRaycastMesh = () => mesh
return mesh
}
const syncStartEvent = { type: 'syncstart' }
const syncCompleteEvent = { type: 'synccomplete' }
const SYNCABLE_PROPS = [
'font',
'fontSize',
'fontStyle',
'fontWeight',
'lang',
'letterSpacing',
'lineHeight',
'maxWidth',
'overflowWrap',
'text',
'direction',
'textAlign',
'textIndent',
'whiteSpace',
'anchorX',
'anchorY',
'colorRanges',
'sdfGlyphSize'
]
const COPYABLE_PROPS = SYNCABLE_PROPS.concat(
'material',
'color',
'depthOffset',
'clipRect',
'curveRadius',
'orientation',
'glyphGeometryDetail'
)
/**
* @class Text
*
* A ThreeJS Mesh that renders a string of text on a plane in 3D space using signed distance
* fields (SDF).
*/
class Text extends Mesh {
constructor() {
const geometry = new GlyphsGeometry()
super(geometry, null)
// === Text layout properties: === //
/**
* @member {string} text
* The string of text to be rendered.
*/
this.text = ''
/**
* @member {number|string} anchorX
* Defines the horizontal position in the text block that should line up with the local origin.
* Can be specified as a numeric x position in local units, a string percentage of the total
* text block width e.g. `'25%'`, or one of the following keyword strings: 'left', 'center',
* or 'right'.
*/
this.anchorX = 0
/**
* @member {number|string} anchorY
* Defines the vertical position in the text block that should line up with the local origin.
* Can be specified as a numeric y position in local units (note: down is negative y), a string
* percentage of the total text block height e.g. `'25%'`, or one of the following keyword strings:
* 'top', 'top-baseline', 'top-cap', 'top-ex', 'middle', 'bottom-baseline', or 'bottom'.
*/
this.anchorY = 0
/**
* @member {number} curveRadius
* Defines a cylindrical radius along which the text's plane will be curved. Positive numbers put
* the cylinder's centerline (oriented vertically) that distance in front of the text, for a concave
* curvature, while negative numbers put it behind the text for a convex curvature. The centerline
* will be aligned with the text's local origin; you can use `anchorX` to offset it.
*
* Since each glyph is by default rendered with a simple quad, each glyph remains a flat plane
* internally. You can use `glyphGeometryDetail` to add more vertices for curvature inside glyphs.
*/
this.curveRadius = 0
/**
* @member {string} direction
* Sets the base direction for the text. The default value of "auto" will choose a direction based
* on the text's content according to the bidi spec. A value of "ltr" or "rtl" will force the direction.
*/
this.direction = 'auto'
/**
* @member {string|null} font
* URL of a custom font to be used. Font files can be in .ttf, .otf, or .woff (not .woff2) formats.
* Defaults to Noto Sans.
*/
this.font = null //will use default from TextBuilder
this.unicodeFontsURL = null //defaults to CDN
/**
* @member {number} fontSize
* The size at which to render the font in local units; corresponds to the em-box height
* of the chosen `font`.
*/
this.fontSize = 0.1
/**
* @member {number|'normal'|'bold'}
* The weight of the font. Currently only used for fallback Noto fonts.
*/
this.fontWeight = 'normal'
/**
* @member {'normal'|'italic'}
* The style of the font. Currently only used for fallback Noto fonts.
*/
this.fontStyle = 'normal'
/**
* @member {string|null} lang
* The language code of this text; can be used for explicitly selecting certain CJK fonts.
*/
this.lang = null;
/**
* @member {number} letterSpacing
* Sets a uniform adjustment to spacing between letters after kerning is applied. Positive
* numbers increase spacing and negative numbers decrease it.
*/
this.letterSpacing = 0
/**
* @member {number|string} lineHeight
* Sets the height of each line of text, as a multiple of the `fontSize`. Defaults to 'normal'
* which chooses a reasonable height based on the chosen font's ascender/descender metrics.
*/
this.lineHeight = 'normal'
/**
* @member {number} maxWidth
* The maximum width of the text block, above which text may start wrapping according to the
* `whiteSpace` and `overflowWrap` properties.
*/
this.maxWidth = Infinity
/**
* @member {string} overflowWrap
* Defines how text wraps if the `whiteSpace` property is `normal`. Can be either `'normal'`
* to break at whitespace characters, or `'break-word'` to allow breaking within words.
* Defaults to `'normal'`.
*/
this.overflowWrap = 'normal'
/**
* @member {string} textAlign
* The horizontal alignment of each line of text within the overall text bounding box.
*/
this.textAlign = 'left'
/**
* @member {number} textIndent
* Indentation for the first character of a line; see CSS `text-indent`.
*/
this.textIndent = 0
/**
* @member {string} whiteSpace
* Defines whether text should wrap when a line reaches the `maxWidth`. Can
* be either `'normal'` (the default), to allow wrapping according to the `overflowWrap` property,
* or `'nowrap'` to prevent wrapping. Note that `'normal'` here honors newline characters to
* manually break lines, making it behave more like `'pre-wrap'` does in CSS.
*/
this.whiteSpace = 'normal'
// === Presentation properties: === //
/**
* @member {THREE.Material} material
* Defines a _base_ material to be used when rendering the text. This material will be
* automatically replaced with a material derived from it, that adds shader code to
* decrease the alpha for each fragment (pixel) outside the text glyphs, with antialiasing.
* By default it will derive from a simple white MeshBasicMaterial, but you can use any
* of the other mesh materials to gain other features like lighting, texture maps, etc.
*
* Also see the `color` shortcut property.
*/
this.material = null
/**
* @member {string|number|THREE.Color} color
* This is a shortcut for setting the `color` of the text's material. You can use this
* if you don't want to specify a whole custom `material`. Also, if you do use a custom
* `material`, this color will only be used for this particuar Text instance, even if
* that same material instance is shared across multiple Text objects.
*/
this.color = null
/**
* @member {object|null} colorRanges
* WARNING: This API is experimental and may change.
* This allows more fine-grained control of colors for individual or ranges of characters,
* taking precedence over the material's `color`. Its format is an Object whose keys each
* define a starting character index for a range, and whose values are the color for each
* range. The color value can be a numeric hex color value, a `THREE.Color` object, or
* any of the strings accepted by `THREE.Color`.
*/
this.colorRanges = null
/**
* @member {number|string} outlineWidth
* WARNING: This API is experimental and may change.
* The width of an outline/halo to be drawn around each text glyph using the `outlineColor` and `outlineOpacity`.
* Can be specified as either an absolute number in local units, or as a percentage string e.g.
* `"12%"` which is treated as a percentage of the `fontSize`. Defaults to `0`, which means
* no outline will be drawn unless an `outlineOffsetX/Y` or `outlineBlur` is set.
*/
this.outlineWidth = 0
/**
* @member {string|number|THREE.Color} outlineColor
* WARNING: This API is experimental and may change.
* The color of the text outline, if `outlineWidth`/`outlineBlur`/`outlineOffsetX/Y` are set.
* Defaults to black.
*/
this.outlineColor = 0x000000
/**
* @member {number} outlineOpacity
* WARNING: This API is experimental and may change.
* The opacity of the outline, if `outlineWidth`/`outlineBlur`/`outlineOffsetX/Y` are set.
* Defaults to `1`.
*/
this.outlineOpacity = 1
/**
* @member {number|string} outlineBlur
* WARNING: This API is experimental and may change.
* A blur radius applied to the outer edge of the text's outline. If the `outlineWidth` is
* zero, the blur will be applied at the glyph edge, like CSS's `text-shadow` blur radius.
* Can be specified as either an absolute number in local units, or as a percentage string e.g.
* `"12%"` which is treated as a percentage of the `fontSize`. Defaults to `0`.
*/
this.outlineBlur = 0
/**
* @member {number|string} outlineOffsetX
* WARNING: This API is experimental and may change.
* A horizontal offset for the text outline.
* Can be specified as either an absolute number in local units, or as a percentage string e.g. `"12%"`
* which is treated as a percentage of the `fontSize`. Defaults to `0`.
*/
this.outlineOffsetX = 0
/**
* @member {number|string} outlineOffsetY
* WARNING: This API is experimental and may change.
* A vertical offset for the text outline.
* Can be specified as either an absolute number in local units, or as a percentage string e.g. `"12%"`
* which is treated as a percentage of the `fontSize`. Defaults to `0`.
*/
this.outlineOffsetY = 0
/**
* @member {number|string} strokeWidth
* WARNING: This API is experimental and may change.
* The width of an inner stroke drawn inside each text glyph using the `strokeColor` and `strokeOpacity`.
* Can be specified as either an absolute number in local units, or as a percentage string e.g. `"12%"`
* which is treated as a percentage of the `fontSize`. Defaults to `0`.
*/
this.strokeWidth = 0
/**
* @member {string|number|THREE.Color} strokeColor
* WARNING: This API is experimental and may change.
* The color of the text stroke, if `strokeWidth` is greater than zero. Defaults to gray.
*/
this.strokeColor = defaultStrokeColor
/**
* @member {number} strokeOpacity
* WARNING: This API is experimental and may change.
* The opacity of the stroke, if `strokeWidth` is greater than zero. Defaults to `1`.
*/
this.strokeOpacity = 1
/**
* @member {number} fillOpacity
* WARNING: This API is experimental and may change.
* The opacity of the glyph's fill from 0 to 1. This behaves like the material's `opacity` but allows
* giving the fill a different opacity than the `strokeOpacity`. A fillOpacity of `0` makes the
* interior of the glyph invisible, leaving just the `strokeWidth`. Defaults to `1`.
*/
this.fillOpacity = 1
/**
* @member {number} depthOffset
* This is a shortcut for setting the material's `polygonOffset` and related properties,
* which can be useful in preventing z-fighting when this text is laid on top of another
* plane in the scene. Positive numbers are further from the camera, negatives closer.
*/
this.depthOffset = 0
/**
* @member {Array<number>} clipRect
* If specified, defines a `[minX, minY, maxX, maxY]` of a rectangle outside of which all
* pixels will be discarded. This can be used for example to clip overflowing text when
* `whiteSpace='nowrap'`.
*/
this.clipRect = null
/**
* @member {string} orientation
* Defines the axis plane on which the text should be laid out when the mesh has no extra
* rotation transform. It is specified as a string with two axes: the horizontal axis with
* positive pointing right, and the vertical axis with positive pointing up. By default this
* is '+x+y', meaning the text sits on the xy plane with the text's top toward positive y
* and facing positive z. A value of '+x-z' would place it on the xz plane with the text's
* top toward negative z and facing positive y.
*/
this.orientation = defaultOrient
/**
* @member {number} glyphGeometryDetail
* Controls number of vertical/horizontal segments that make up each glyph's rectangular
* plane. Defaults to 1. This can be increased to provide more geometrical detail for custom
* vertex shader effects, for example.
*/
this.glyphGeometryDetail = 1
/**
* @member {number|null} sdfGlyphSize
* The size of each glyph's SDF (signed distance field) used for rendering. This must be a
* power-of-two number. Defaults to 64 which is generally a good balance of size and quality
* for most fonts. Larger sizes can improve the quality of glyph rendering by increasing
* the sharpness of corners and preventing loss of very thin lines, at the expense of
* increased memory footprint and longer SDF generation time.
*/
this.sdfGlyphSize = null
/**
* @member {boolean} gpuAccelerateSDF
* When `true`, the SDF generation process will be GPU-accelerated with WebGL when possible,
* making it much faster especially for complex glyphs, and falling back to a JavaScript version
* executed in web workers when support isn't available. It should automatically detect support,
* but it's still somewhat experimental, so you can set it to `false` to force it to use the JS
* version if you encounter issues with it.
*/
this.gpuAccelerateSDF = true
this.debugSDF = false
}
/**
* Updates the text rendering according to the current text-related configuration properties.
* This is an async process, so you can pass in a callback function to be executed when it
* finishes.
* @param {function} [callback]
*/
sync(callback) {
if (this._needsSync) {
this._needsSync = false
// If there's another sync still in progress, queue
if (this._isSyncing) {
(this._queuedSyncs || (this._queuedSyncs = [])).push(callback)
} else {
this._isSyncing = true
this.dispatchEvent(syncStartEvent)
getTextRenderInfo({
text: this.text,
font: this.font,
lang: this.lang,
fontSize: this.fontSize || 0.1,
fontWeight: this.fontWeight || 'normal',
fontStyle: this.fontStyle || 'normal',
letterSpacing: this.letterSpacing || 0,
lineHeight: this.lineHeight || 'normal',
maxWidth: this.maxWidth,
direction: this.direction || 'auto',
textAlign: this.textAlign,
textIndent: this.textIndent,
whiteSpace: this.whiteSpace,
overflowWrap: this.overflowWrap,
anchorX: this.anchorX,
anchorY: this.anchorY,
colorRanges: this.colorRanges,
includeCaretPositions: true, //TODO parameterize
sdfGlyphSize: this.sdfGlyphSize,
gpuAccelerateSDF: this.gpuAccelerateSDF,
unicodeFontsURL: this.unicodeFontsURL,
}, textRenderInfo => {
this._isSyncing = false
// Save result for later use in onBeforeRender
this._textRenderInfo = textRenderInfo
// Update the geometry attributes
this.geometry.updateGlyphs(
textRenderInfo.glyphBounds,
textRenderInfo.glyphAtlasIndices,
textRenderInfo.blockBounds,
textRenderInfo.chunkedBounds,
textRenderInfo.glyphColors
)
// If we had extra sync requests queued up, kick it off
const queued = this._queuedSyncs
if (queued) {
this._queuedSyncs = null
this._needsSync = true
this.sync(() => {
queued.forEach(fn => fn && fn())
})
}
this.dispatchEvent(syncCompleteEvent)
if (callback) {
callback()
}
})
}
}
}
/**
* Initiate a sync if needed - note it won't complete until next frame at the
* earliest so if possible it's a good idea to call sync() manually as soon as
* all the properties have been set.
* @override
*/
onBeforeRender(renderer, scene, camera, geometry, material, group) {
this.sync()
// This may not always be a text material, e.g. if there's a scene.overrideMaterial present
if (material.isTroikaTextMaterial) {
this._prepareForRender(material)
}
}
/**
* Shortcut to dispose the geometry specific to this instance.
* Note: we don't also dispose the derived material here because if anything else is
* sharing the same base material it will result in a pause next frame as the program
* is recompiled. Instead users can dispose the base material manually, like normal,
* and we'll also dispose the derived material at that time.
*/
dispose() {
this.geometry.dispose()
}
/**
* @property {TroikaTextRenderInfo|null} textRenderInfo
* @readonly
* The current processed rendering data for this TextMesh, returned by the TextBuilder after
* a `sync()` call. This will be `null` initially, and may be stale for a short period until
* the asynchrous `sync()` process completes.
*/
get textRenderInfo() {
return this._textRenderInfo || null
}
/**
* Create the text derived material from the base material. Can be overridden to use a custom
* derived material.
*/
createDerivedMaterial(baseMaterial) {
return createTextDerivedMaterial(baseMaterial)
}
// Handler for automatically wrapping the base material with our upgrades. We do the wrapping
// lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.
get material() {
let derivedMaterial = this._derivedMaterial
const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultMaterial.clone())
if (!derivedMaterial || !derivedMaterial.isDerivedFrom(baseMaterial)) {
derivedMaterial = this._derivedMaterial = this.createDerivedMaterial(baseMaterial)
// dispose the derived material when its base material is disposed:
baseMaterial.addEventListener('dispose', function onDispose() {
baseMaterial.removeEventListener('dispose', onDispose)
derivedMaterial.dispose()
})
}
// If text outline is configured, render it as a preliminary draw using Three's multi-material
// feature (see GlyphsGeometry which sets up `groups` for this purpose) Doing it with multi
// materials ensures the layers are always rendered consecutively in a consistent order.
// Each layer will trigger onBeforeRender with the appropriate material.
if (this.hasOutline()) {
let outlineMaterial = derivedMaterial._outlineMtl
if (!outlineMaterial) {
outlineMaterial = derivedMaterial._outlineMtl = Object.create(derivedMaterial, {
id: {value: derivedMaterial.id + 0.1}
})
outlineMaterial.isTextOutlineMaterial = true
outlineMaterial.depthWrite = false
outlineMaterial.map = null //???
derivedMaterial.addEventListener('dispose', function onDispose() {
derivedMaterial.removeEventListener('dispose', onDispose)
outlineMaterial.dispose()
})
}
return [
outlineMaterial,
derivedMaterial
]
} else {
return derivedMaterial
}
}
set material(baseMaterial) {
if (baseMaterial && baseMaterial.isTroikaTextMaterial) { //prevent double-derivation
this._derivedMaterial = baseMaterial
this._baseMaterial = baseMaterial.baseMaterial
} else {
this._baseMaterial = baseMaterial
}
}
hasOutline() {
return !!(this.outlineWidth || this.outlineBlur || this.outlineOffsetX || this.outlineOffsetY)
}
get glyphGeometryDetail() {
return this.geometry.detail
}
set glyphGeometryDetail(detail) {
this.geometry.detail = detail
}
get curveRadius() {
return this.geometry.curveRadius
}
set curveRadius(r) {
this.geometry.curveRadius = r
}
// Create and update material for shadows upon request:
get customDepthMaterial() {
return first(this.material).getDepthMaterial()
}
set customDepthMaterial(m) {
// future: let the user override with their own?
}
get customDistanceMaterial() {
return first(this.material).getDistanceMaterial()
}
set customDistanceMaterial(m) {
// future: let the user override with their own?
}
_prepareForRender(material) {
const isOutline = material.isTextOutlineMaterial
const uniforms = material.uniforms
const textInfo = this.textRenderInfo
if (textInfo) {
const {sdfTexture, blockBounds} = textInfo
uniforms.uTroikaSDFTexture.value = sdfTexture
uniforms.uTroikaSDFTextureSize.value.set(sdfTexture.image.width, sdfTexture.image.height)
uniforms.uTroikaSDFGlyphSize.value = textInfo.sdfGlyphSize
uniforms.uTroikaSDFExponent.value = textInfo.sdfExponent
uniforms.uTroikaTotalBounds.value.fromArray(blockBounds)
uniforms.uTroikaUseGlyphColors.value = !isOutline && !!textInfo.glyphColors
let distanceOffset = 0
let blurRadius = 0
let strokeWidth = 0
let fillOpacity
let strokeOpacity
let strokeColor
let offsetX = 0
let offsetY = 0
if (isOutline) {
let {outlineWidth, outlineOffsetX, outlineOffsetY, outlineBlur, outlineOpacity} = this
distanceOffset = this._parsePercent(outlineWidth) || 0
blurRadius = Math.max(0, this._parsePercent(outlineBlur) || 0)
fillOpacity = outlineOpacity
offsetX = this._parsePercent(outlineOffsetX) || 0
offsetY = this._parsePercent(outlineOffsetY) || 0
} else {
strokeWidth = Math.max(0, this._parsePercent(this.strokeWidth) || 0)
if (strokeWidth) {
strokeColor = this.strokeColor
uniforms.uTroikaStrokeColor.value.set(strokeColor == null ? defaultStrokeColor : strokeColor)
strokeOpacity = this.strokeOpacity
if (strokeOpacity == null) strokeOpacity = 1
}
fillOpacity = this.fillOpacity
}
uniforms.uTroikaEdgeOffset.value = distanceOffset
uniforms.uTroikaPositionOffset.value.set(offsetX, offsetY)
uniforms.uTroikaBlurRadius.value = blurRadius
uniforms.uTroikaStrokeWidth.value = strokeWidth
uniforms.uTroikaStrokeOpacity.value = strokeOpacity
uniforms.uTroikaFillOpacity.value = fillOpacity == null ? 1 : fillOpacity
uniforms.uTroikaCurveRadius.value = this.curveRadius || 0
let clipRect = this.clipRect
if (clipRect && Array.isArray(clipRect) && clipRect.length === 4) {
uniforms.uTroikaClipRect.value.fromArray(clipRect)
} else {
// no clipping - choose a finite rect that shouldn't ever be reached by overflowing glyphs or outlines
const pad = (this.fontSize || 0.1) * 100
uniforms.uTroikaClipRect.value.set(
blockBounds[0] - pad,
blockBounds[1] - pad,
blockBounds[2] + pad,
blockBounds[3] + pad
)
}
this.geometry.applyClipRect(uniforms.uTroikaClipRect.value)
}
uniforms.uTroikaSDFDebug.value = !!this.debugSDF
material.polygonOffset = !!this.depthOffset
material.polygonOffsetFactor = material.polygonOffsetUnits = this.depthOffset || 0
// Shortcut for setting material color via `color` prop on the mesh; this is
// applied only to the derived material to avoid mutating a shared base material.
const color = isOutline ? (this.outlineColor || 0) : this.color
if (color == null) {
delete material.color //inherit from base
} else {
const colorObj = material.hasOwnProperty('color') ? material.color : (material.color = new Color())
if (color !== colorObj._input || typeof color === 'object') {
colorObj.set(colorObj._input = color)
}
}
// base orientation
let orient = this.orientation || defaultOrient
if (orient !== material._orientation) {
let rotMat = uniforms.uTroikaOrient.value
orient = orient.replace(/[^-+xyz]/g, '')
let match = orient !== defaultOrient && orient.match(/^([-+])([xyz])([-+])([xyz])$/)
if (match) {
let [, hSign, hAxis, vSign, vAxis] = match
tempVec3a.set(0, 0, 0)[hAxis] = hSign === '-' ? 1 : -1
tempVec3b.set(0, 0, 0)[vAxis] = vSign === '-' ? -1 : 1
tempMat4.lookAt(origin, tempVec3a.cross(tempVec3b), tempVec3b)
rotMat.setFromMatrix4(tempMat4)
} else {
rotMat.identity()
}
material._orientation = orient
}
}
_parsePercent(value) {
if (typeof value === 'string') {
let match = value.match(/^(-?[\d.]+)%$/)
let pct = match ? parseFloat(match[1]) : NaN
value = (isNaN(pct) ? 0 : pct / 100) * this.fontSize
}
return value
}
/**
* Translate a point in local space to an x/y in the text plane.
*/
localPositionToTextCoords(position, target = new Vector2()) {
target.copy(position) //simple non-curved case is 1:1
const r = this.curveRadius
if (r) { //flatten the curve
target.x = Math.atan2(position.x, Math.abs(r) - Math.abs(position.z)) * Math.abs(r)
}
return target
}
/**
* Translate a point in world space to an x/y in the text plane.
*/
worldPositionToTextCoords(position, target = new Vector2()) {
tempVec3a.copy(position)
return this.localPositionToTextCoords(this.worldToLocal(tempVec3a), target)
}
/**
* @override Custom raycasting to test against the whole text block's max rectangular bounds
* TODO is there any reason to make this more granular, like within individual line or glyph rects?
*/
raycast(raycaster, intersects) {
const {textRenderInfo, curveRadius} = this
if (textRenderInfo) {
const bounds = textRenderInfo.blockBounds
const raycastMesh = curveRadius ? getCurvedRaycastMesh() : getFlatRaycastMesh()
const geom = raycastMesh.geometry
const {position, uv} = geom.attributes
for (let i = 0; i < uv.count; i++) {
let x = bounds[0] + (uv.getX(i) * (bounds[2] - bounds[0]))
const y = bounds[1] + (uv.getY(i) * (bounds[3] - bounds[1]))
let z = 0
if (curveRadius) {
z = curveRadius - Math.cos(x / curveRadius) * curveRadius
x = Math.sin(x / curveRadius) * curveRadius
}
position.setXYZ(i, x, y, z)
}
geom.boundingSphere = this.geometry.boundingSphere
geom.boundingBox = this.geometry.boundingBox
raycastMesh.matrixWorld = this.matrixWorld
raycastMesh.material.side = this.material.side
tempArray.length = 0
raycastMesh.raycast(raycaster, tempArray)
for (let i = 0; i < tempArray.length; i++) {
tempArray[i].object = this
intersects.push(tempArray[i])
}
}
}
copy(source) {
// Prevent copying the geometry reference so we don't end up sharing attributes between instances
const geom = this.geometry
super.copy(source)
this.geometry = geom
COPYABLE_PROPS.forEach(prop => {
this[prop] = source[prop]
})
return this
}
clone() {
return new this.constructor().copy(this)
}
}
// Create setters for properties that affect text layout:
SYNCABLE_PROPS.forEach(prop => {
const privateKey = '_private_' + prop
Object.defineProperty(Text.prototype, prop, {
get() {
return this[privateKey]
},
set(value) {
if (value !== this[privateKey]) {
this[privateKey] = value
this._needsSync = true
}
}
})
})
export {
Text
}
+505
View File
@@ -0,0 +1,505 @@
import { Color, Texture, LinearFilter } from 'three'
import { defineWorkerModule } from 'troika-worker-utils'
import { fontResolverWorkerModule } from "./FontResolver.js";
import { createTypesetter } from './Typesetter.js'
import { generateSDF, warmUpSDFCanvas, resizeWebGLCanvasWithoutClearing } from './SDFGenerator.js'
import bidiFactory from 'bidi-js'
const CONFIG = {
defaultFontURL: null,
unicodeFontsURL: null,
sdfGlyphSize: 64,
sdfMargin: 1 / 16,
sdfExponent: 9,
textureWidth: 2048,
useWorker: true,
}
const tempColor = /*#__PURE__*/new Color()
let hasRequested = false
function now() {
return (self.performance || Date).now()
}
/**
* Customizes the text builder configuration. This must be called prior to the first font processing
* request, and applies to all fonts.
*
* @param {String} config.defaultFontURL - The URL of the default font to use for text processing
* requests, in case none is specified or the specifiede font fails to load or parse.
* Defaults to "Roboto Regular" from Google Fonts.
* @param {String} config.unicodeFontsURL - A custom location for the fallback unicode-font-resolver
* data and font files, if you don't want to use the default CDN. See
* https://github.com/lojjic/unicode-font-resolver for details. It can also be
* configured per text instance, but this lets you do it once globally.
* @param {Number} config.sdfGlyphSize - The default size of each glyph's SDF (signed distance field)
* texture used for rendering. Must be a power-of-two number, and applies to all fonts,
* but note that this can also be overridden per call to `getTextRenderInfo()`.
* Larger sizes can improve the quality of glyph rendering by increasing the sharpness
* of corners and preventing loss of very thin lines, at the expense of memory. Defaults
* to 64 which is generally a good balance of size and quality.
* @param {Number} config.sdfExponent - The exponent used when encoding the SDF values. A higher exponent
* shifts the encoded 8-bit values to achieve higher precision/accuracy at texels nearer
* the glyph's path, with lower precision further away. Defaults to 9.
* @param {Number} config.sdfMargin - How much space to reserve in the SDF as margin outside the glyph's
* path, as a percentage of the SDF width. A larger margin increases the quality of
* extruded glyph outlines, but decreases the precision available for the glyph itself.
* Defaults to 1/16th of the glyph size.
* @param {Number} config.textureWidth - The width of the SDF texture; must be a power of 2. Defaults to
* 2048 which is a safe maximum texture dimension according to the stats at
* https://webglstats.com/webgl/parameter/MAX_TEXTURE_SIZE and should allow for a
* reasonably large number of glyphs (default glyph size of 64^2 and safe texture size of
* 2048^2, times 4 channels, allows for 4096 glyphs.) This can be increased if you need to
* increase the glyph size and/or have an extraordinary number of glyphs.
* @param {Boolean} config.useWorker - Whether to run typesetting in a web worker. Defaults to true.
*/
function configureTextBuilder(config) {
if (hasRequested) {
console.warn('configureTextBuilder called after first font request; will be ignored.')
} else {
assign(CONFIG, config)
}
}
/**
* Repository for all font SDF atlas textures and their glyph mappings. There is a separate atlas for
* each sdfGlyphSize. Each atlas has a single Texture that holds all glyphs for all fonts.
*
* {
* [sdfGlyphSize]: {
* glyphCount: number,
* sdfGlyphSize: number,
* sdfTexture: Texture,
* sdfCanvas: HTMLCanvasElement,
* contextLost: boolean,
* glyphsByFont: Map<fontURL, Map<glyphID, {path, atlasIndex, sdfViewBox}>>
* }
* }
*/
const atlases = Object.create(null)
/**
* @typedef {object} TroikaTextRenderInfo - Format of the result from `getTextRenderInfo`.
* @property {TypesetParams} parameters - The normalized input arguments to the render call.
* @property {Texture} sdfTexture - The SDF atlas texture.
* @property {number} sdfGlyphSize - The size of each glyph's SDF; see `configureTextBuilder`.
* @property {number} sdfExponent - The exponent used in encoding the SDF's values; see `configureTextBuilder`.
* @property {Float32Array} glyphBounds - List of [minX, minY, maxX, maxY] quad bounds for each glyph.
* @property {Float32Array} glyphAtlasIndices - List holding each glyph's index in the SDF atlas.
* @property {Uint8Array} [glyphColors] - List holding each glyph's [r, g, b] color, if `colorRanges` was supplied.
* @property {Float32Array} [caretPositions] - A list of caret positions for all characters in the string; each is
* four elements: the starting X, the ending X, the bottom Y, and the top Y for the caret.
* @property {number} [caretHeight] - An appropriate height for all selection carets.
* @property {number} ascender - The font's ascender metric.
* @property {number} descender - The font's descender metric.
* @property {number} capHeight - The font's cap height metric, based on the height of Latin capital letters.
* @property {number} xHeight - The font's x height metric, based on the height of Latin lowercase letters.
* @property {number} lineHeight - The final computed lineHeight measurement.
* @property {number} topBaseline - The y position of the top line's baseline.
* @property {Array<number>} blockBounds - The total [minX, minY, maxX, maxY] rect of the whole text block;
* this can include extra vertical space beyond the visible glyphs due to lineHeight, and is
* equivalent to the dimensions of a block-level text element in CSS.
* @property {Array<number>} visibleBounds - The total [minX, minY, maxX, maxY] rect of the whole text block;
* unlike `blockBounds` this is tightly wrapped to the visible glyph paths.
* @property {Array<object>} chunkedBounds - List of bounding rects for each consecutive set of N glyphs,
* in the format `{start:N, end:N, rect:[minX, minY, maxX, maxY]}`.
* @property {object} timings - Timing info for various parts of the rendering logic including SDF
* generation, typesetting, etc.
* @frozen
*/
/**
* @callback getTextRenderInfo~callback
* @param {TroikaTextRenderInfo} textRenderInfo
*/
/**
* Main entry point for requesting the data needed to render a text string with given font parameters.
* This is an asynchronous call, performing most of the logic in a web worker thread.
* @param {TypesetParams} args
* @param {getTextRenderInfo~callback} callback
*/
function getTextRenderInfo(args, callback) {
hasRequested = true
args = assign({}, args)
const totalStart = now()
// Convert relative URL to absolute so it can be resolved in the worker, and add fallbacks.
// In the future we'll allow args.font to be a list with unicode ranges too.
const { defaultFontURL } = CONFIG
const fonts = [];
if (defaultFontURL) {
fonts.push({label: 'default', src: toAbsoluteURL(defaultFontURL)})
}
if (args.font) {
fonts.push({label: 'user', src: toAbsoluteURL(args.font)})
}
args.font = fonts
// Normalize text to a string
args.text = '' + args.text
args.sdfGlyphSize = args.sdfGlyphSize || CONFIG.sdfGlyphSize
args.unicodeFontsURL = args.unicodeFontsURL || CONFIG.unicodeFontsURL
// Normalize colors
if (args.colorRanges != null) {
let colors = {}
for (let key in args.colorRanges) {
if (args.colorRanges.hasOwnProperty(key)) {
let val = args.colorRanges[key]
if (typeof val !== 'number') {
val = tempColor.set(val).getHex()
}
colors[key] = val
}
}
args.colorRanges = colors
}
Object.freeze(args)
// Init the atlas if needed
const {textureWidth, sdfExponent} = CONFIG
const {sdfGlyphSize} = args
const glyphsPerRow = (textureWidth / sdfGlyphSize * 4)
let atlas = atlases[sdfGlyphSize]
if (!atlas) {
const canvas = document.createElement('canvas')
canvas.width = textureWidth
canvas.height = sdfGlyphSize * 256 / glyphsPerRow // start tall enough to fit 256 glyphs
atlas = atlases[sdfGlyphSize] = {
glyphCount: 0,
sdfGlyphSize,
sdfCanvas: canvas,
sdfTexture: new Texture(
canvas,
undefined,
undefined,
undefined,
LinearFilter,
LinearFilter
),
contextLost: false,
glyphsByFont: new Map()
}
atlas.sdfTexture.generateMipmaps = false
initContextLossHandling(atlas)
}
const {sdfTexture, sdfCanvas} = atlas
// Issue request to the typesetting engine in the worker
const typeset = CONFIG.useWorker ? typesetInWorker : typesetOnMainThread
typeset(args).then(result => {
const {glyphIds, glyphFontIndices, fontData, glyphPositions, fontSize, timings} = result
const neededSDFs = []
const glyphBounds = new Float32Array(glyphIds.length * 4)
let boundsIdx = 0
let positionsIdx = 0
const quadsStart = now()
const fontGlyphMaps = fontData.map(font => {
let map = atlas.glyphsByFont.get(font.src)
if (!map) {
atlas.glyphsByFont.set(font.src, map = new Map())
}
return map
})
glyphIds.forEach((glyphId, i) => {
const fontIndex = glyphFontIndices[i]
const {src: fontSrc, unitsPerEm} = fontData[fontIndex]
let glyphInfo = fontGlyphMaps[fontIndex].get(glyphId)
// If this is a glyphId not seen before, add it to the atlas
if (!glyphInfo) {
const {path, pathBounds} = result.glyphData[fontSrc][glyphId]
// Margin around path edges in SDF, based on a percentage of the glyph's max dimension.
// Note we add an extra 0.5 px over the configured value because the outer 0.5 doesn't contain
// useful interpolated values and will be ignored anyway.
const fontUnitsMargin = Math.max(pathBounds[2] - pathBounds[0], pathBounds[3] - pathBounds[1])
/ sdfGlyphSize * (CONFIG.sdfMargin * sdfGlyphSize + 0.5)
const atlasIndex = atlas.glyphCount++
const sdfViewBox = [
pathBounds[0] - fontUnitsMargin,
pathBounds[1] - fontUnitsMargin,
pathBounds[2] + fontUnitsMargin,
pathBounds[3] + fontUnitsMargin,
]
fontGlyphMaps[fontIndex].set(glyphId, (glyphInfo = { path, atlasIndex, sdfViewBox }))
// Collect those that need SDF generation
neededSDFs.push(glyphInfo)
}
// Calculate bounds for renderable quads
// TODO can we get this back off the main thread?
const {sdfViewBox} = glyphInfo
const posX = glyphPositions[positionsIdx++]
const posY = glyphPositions[positionsIdx++]
const fontSizeMult = fontSize / unitsPerEm
glyphBounds[boundsIdx++] = posX + sdfViewBox[0] * fontSizeMult
glyphBounds[boundsIdx++] = posY + sdfViewBox[1] * fontSizeMult
glyphBounds[boundsIdx++] = posX + sdfViewBox[2] * fontSizeMult
glyphBounds[boundsIdx++] = posY + sdfViewBox[3] * fontSizeMult
// Convert glyphId to SDF index for the shader
glyphIds[i] = glyphInfo.atlasIndex
})
timings.quads = (timings.quads || 0) + (now() - quadsStart)
const sdfStart = now()
timings.sdf = {}
// Grow the texture height by power of 2 if needed
const currentHeight = sdfCanvas.height
const neededRows = Math.ceil(atlas.glyphCount / glyphsPerRow)
const neededHeight = Math.pow(2, Math.ceil(Math.log2(neededRows * sdfGlyphSize)))
if (neededHeight > currentHeight) {
// Since resizing the canvas clears its render buffer, it needs special handling to copy the old contents over
console.info(`Increasing SDF texture size ${currentHeight}->${neededHeight}`)
resizeWebGLCanvasWithoutClearing(sdfCanvas, textureWidth, neededHeight)
// As of Three r136 textures cannot be resized once they're allocated on the GPU, we must dispose to reallocate it
sdfTexture.dispose()
}
Promise.all(neededSDFs.map(glyphInfo =>
generateGlyphSDF(glyphInfo, atlas, args.gpuAccelerateSDF).then(({timing}) => {
timings.sdf[glyphInfo.atlasIndex] = timing
})
)).then(() => {
if (neededSDFs.length && !atlas.contextLost) {
safariPre15Workaround(atlas)
sdfTexture.needsUpdate = true
}
timings.sdfTotal = now() - sdfStart
timings.total = now() - totalStart
// console.log(`SDF - ${timings.sdfTotal}, Total - ${timings.total - timings.fontLoad}`)
// Invoke callback with the text layout arrays and updated texture
callback(Object.freeze({
parameters: args,
sdfTexture,
sdfGlyphSize,
sdfExponent,
glyphBounds,
glyphAtlasIndices: glyphIds,
glyphColors: result.glyphColors,
caretPositions: result.caretPositions,
chunkedBounds: result.chunkedBounds,
ascender: result.ascender,
descender: result.descender,
lineHeight: result.lineHeight,
capHeight: result.capHeight,
xHeight: result.xHeight,
topBaseline: result.topBaseline,
blockBounds: result.blockBounds,
visibleBounds: result.visibleBounds,
timings: result.timings,
}))
})
})
// While the typesetting request is being handled, go ahead and make sure the atlas canvas context is
// "warmed up"; the first request will be the longest due to shader program compilation so this gets
// a head start on that process before SDFs actually start getting processed.
Promise.resolve().then(() => {
if (!atlas.contextLost) {
warmUpSDFCanvas(sdfCanvas)
}
})
}
function generateGlyphSDF({path, atlasIndex, sdfViewBox}, {sdfGlyphSize, sdfCanvas, contextLost}, useGPU) {
if (contextLost) {
// If the context is lost there's nothing we can do, just quit silently and let it
// get regenerated when the context is restored
return Promise.resolve({timing: -1})
}
const {textureWidth, sdfExponent} = CONFIG
const maxDist = Math.max(sdfViewBox[2] - sdfViewBox[0], sdfViewBox[3] - sdfViewBox[1])
const squareIndex = Math.floor(atlasIndex / 4)
const x = squareIndex % (textureWidth / sdfGlyphSize) * sdfGlyphSize
const y = Math.floor(squareIndex / (textureWidth / sdfGlyphSize)) * sdfGlyphSize
const channel = atlasIndex % 4
return generateSDF(sdfGlyphSize, sdfGlyphSize, path, sdfViewBox, maxDist, sdfExponent, sdfCanvas, x, y, channel, useGPU)
}
function initContextLossHandling(atlas) {
const canvas = atlas.sdfCanvas
/*
// Begin context loss simulation
if (!window.WebGLDebugUtils) {
let script = document.getElementById('WebGLDebugUtilsScript')
if (!script) {
script = document.createElement('script')
script.id = 'WebGLDebugUtils'
document.head.appendChild(script)
script.src = 'https://cdn.jsdelivr.net/gh/KhronosGroup/WebGLDeveloperTools@b42e702/src/debug/webgl-debug.js'
}
script.addEventListener('load', () => {
initContextLossHandling(atlas)
})
return
}
window.WebGLDebugUtils.makeLostContextSimulatingCanvas(canvas)
canvas.loseContextInNCalls(500)
canvas.addEventListener('webglcontextrestored', (event) => {
canvas.loseContextInNCalls(5000)
})
// End context loss simulation
*/
canvas.addEventListener('webglcontextlost', (event) => {
console.log('Context Lost', event)
event.preventDefault()
atlas.contextLost = true
})
canvas.addEventListener('webglcontextrestored', (event) => {
console.log('Context Restored', event)
atlas.contextLost = false
// Regenerate all glyphs into the restored canvas:
const promises = []
atlas.glyphsByFont.forEach(glyphMap => {
glyphMap.forEach(glyph => {
promises.push(generateGlyphSDF(glyph, atlas, true))
})
})
Promise.all(promises).then(() => {
safariPre15Workaround(atlas)
atlas.sdfTexture.needsUpdate = true
})
})
}
/**
* Preload a given font and optionally pre-generate glyph SDFs for one or more character sequences.
* This can be useful to avoid long pauses when first showing text in a scene, by preloading the
* needed fonts and glyphs up front along with other assets.
*
* @param {object} options
* @param {string} options.font - URL of the font file to preload. If not given, the default font will
* be loaded.
* @param {string|string[]} options.characters - One or more character sequences for which to pre-
* generate glyph SDFs. Note that this will honor ligature substitution, so you may need
* to specify ligature sequences in addition to their individual characters to get all
* possible glyphs, e.g. `["t", "h", "th"]` to get the "t" and "h" glyphs plus the "th" ligature.
* @param {number} options.sdfGlyphSize - The size at which to prerender the SDF textures for the
* specified `characters`.
* @param {function} callback - A function that will be called when the preloading is complete.
*/
function preloadFont({font, characters, sdfGlyphSize}, callback) {
let text = Array.isArray(characters) ? characters.join('\n') : '' + characters
getTextRenderInfo({ font, sdfGlyphSize, text }, callback)
}
// Local assign impl so we don't have to import troika-core
function assign(toObj, fromObj) {
for (let key in fromObj) {
if (fromObj.hasOwnProperty(key)) {
toObj[key] = fromObj[key]
}
}
return toObj
}
// Utility for making URLs absolute
let linkEl
function toAbsoluteURL(path) {
if (!linkEl) {
linkEl = typeof document === 'undefined' ? {} : document.createElement('a')
}
linkEl.href = path
return linkEl.href
}
/**
* Safari < v15 seems unable to use the SDF webgl canvas as a texture. This applies a workaround
* where it reads the pixels out of that canvas and uploads them as a data texture instead, at
* a slight performance cost.
*/
function safariPre15Workaround(atlas) {
// Use createImageBitmap support as a proxy for Safari<15, all other mainstream browsers
// have supported it for a long while so any false positives should be minimal.
if (typeof createImageBitmap !== 'function') {
console.info('Safari<15: applying SDF canvas workaround')
const {sdfCanvas, sdfTexture} = atlas
const {width, height} = sdfCanvas
const gl = atlas.sdfCanvas.getContext('webgl')
let pixels = sdfTexture.image.data
if (!pixels || pixels.length !== width * height * 4) {
pixels = new Uint8Array(width * height * 4)
sdfTexture.image = {width, height, data: pixels}
sdfTexture.flipY = false
sdfTexture.isDataTexture = true
}
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
}
}
const typesetterWorkerModule = /*#__PURE__*/defineWorkerModule({
name: 'Typesetter',
dependencies: [
createTypesetter,
fontResolverWorkerModule,
bidiFactory,
],
init(createTypesetter, fontResolver, bidiFactory) {
return createTypesetter(fontResolver, bidiFactory())
}
})
const typesetInWorker = /*#__PURE__*/defineWorkerModule({
name: 'Typesetter',
dependencies: [
typesetterWorkerModule,
],
init(typesetter) {
return function(args) {
return new Promise(resolve => {
typesetter.typeset(args, resolve)
})
}
},
getTransferables(result) {
// Mark array buffers as transferable to avoid cloning during postMessage
const transferables = []
for (let p in result) {
if (result[p] && result[p].buffer) {
transferables.push(result[p].buffer)
}
}
return transferables
}
})
const typesetOnMainThread = typesetInWorker.onMainThread
function dumpSDFTextures() {
Object.keys(atlases).forEach(size => {
const canvas = atlases[size].sdfCanvas
const {width, height} = canvas
console.log("%c.", `
background: url(${canvas.toDataURL()});
background-size: ${width}px ${height}px;
color: transparent;
font-size: 0;
line-height: ${height}px;
padding-left: ${width}px;
`)
})
}
export {
configureTextBuilder,
getTextRenderInfo,
preloadFont,
typesetterWorkerModule,
dumpSDFTextures
}
+281
View File
@@ -0,0 +1,281 @@
import { createDerivedMaterial, voidMainRegExp } from 'troika-three-utils'
import { Color, Vector2, Vector4, Matrix3 } from 'three'
// language=GLSL
const VERTEX_DEFS = `
uniform vec2 uTroikaSDFTextureSize;
uniform float uTroikaSDFGlyphSize;
uniform vec4 uTroikaTotalBounds;
uniform vec4 uTroikaClipRect;
uniform mat3 uTroikaOrient;
uniform bool uTroikaUseGlyphColors;
uniform float uTroikaEdgeOffset;
uniform float uTroikaBlurRadius;
uniform vec2 uTroikaPositionOffset;
uniform float uTroikaCurveRadius;
attribute vec4 aTroikaGlyphBounds;
attribute float aTroikaGlyphIndex;
attribute vec3 aTroikaGlyphColor;
varying vec2 vTroikaGlyphUV;
varying vec4 vTroikaTextureUVBounds;
varying float vTroikaTextureChannel;
varying vec3 vTroikaGlyphColor;
varying vec2 vTroikaGlyphDimensions;
`
// language=GLSL prefix="void main() {" suffix="}"
const VERTEX_TRANSFORM = `
vec4 bounds = aTroikaGlyphBounds;
bounds.xz += uTroikaPositionOffset.x;
bounds.yw -= uTroikaPositionOffset.y;
vec4 outlineBounds = vec4(
bounds.xy - uTroikaEdgeOffset - uTroikaBlurRadius,
bounds.zw + uTroikaEdgeOffset + uTroikaBlurRadius
);
vec4 clippedBounds = vec4(
clamp(outlineBounds.xy, uTroikaClipRect.xy, uTroikaClipRect.zw),
clamp(outlineBounds.zw, uTroikaClipRect.xy, uTroikaClipRect.zw)
);
vec2 clippedXY = (mix(clippedBounds.xy, clippedBounds.zw, position.xy) - bounds.xy) / (bounds.zw - bounds.xy);
position.xy = mix(bounds.xy, bounds.zw, clippedXY);
uv = (position.xy - uTroikaTotalBounds.xy) / (uTroikaTotalBounds.zw - uTroikaTotalBounds.xy);
float rad = uTroikaCurveRadius;
if (rad != 0.0) {
float angle = position.x / rad;
position.xz = vec2(sin(angle) * rad, rad - cos(angle) * rad);
normal.xz = vec2(sin(angle), cos(angle));
}
position = uTroikaOrient * position;
normal = uTroikaOrient * normal;
vTroikaGlyphUV = clippedXY.xy;
vTroikaGlyphDimensions = vec2(bounds[2] - bounds[0], bounds[3] - bounds[1]);
${''/* NOTE: it seems important to calculate the glyph's bounding texture UVs here in the
vertex shader, rather than in the fragment shader, as the latter gives strange artifacts
on some glyphs (those in the leftmost texture column) on some systems. The exact reason
isn't understood but doing this here, then mix()-ing in the fragment shader, seems to work. */}
float txCols = uTroikaSDFTextureSize.x / uTroikaSDFGlyphSize;
vec2 txUvPerSquare = uTroikaSDFGlyphSize / uTroikaSDFTextureSize;
vec2 txStartUV = txUvPerSquare * vec2(
mod(floor(aTroikaGlyphIndex / 4.0), txCols),
floor(floor(aTroikaGlyphIndex / 4.0) / txCols)
);
vTroikaTextureUVBounds = vec4(txStartUV, vec2(txStartUV) + txUvPerSquare);
vTroikaTextureChannel = mod(aTroikaGlyphIndex, 4.0);
`
// language=GLSL
const FRAGMENT_DEFS = `
uniform sampler2D uTroikaSDFTexture;
uniform vec2 uTroikaSDFTextureSize;
uniform float uTroikaSDFGlyphSize;
uniform float uTroikaSDFExponent;
uniform float uTroikaEdgeOffset;
uniform float uTroikaFillOpacity;
uniform float uTroikaBlurRadius;
uniform vec3 uTroikaStrokeColor;
uniform float uTroikaStrokeWidth;
uniform float uTroikaStrokeOpacity;
uniform bool uTroikaSDFDebug;
varying vec2 vTroikaGlyphUV;
varying vec4 vTroikaTextureUVBounds;
varying float vTroikaTextureChannel;
varying vec2 vTroikaGlyphDimensions;
float troikaSdfValueToSignedDistance(float alpha) {
// Inverse of exponential encoding in webgl-sdf-generator
${''/* TODO - there's some slight inaccuracy here when dealing with interpolated alpha values; those
are linearly interpolated where the encoding is exponential. Look into improving this by rounding
to nearest 2 whole texels, decoding those exponential values, and linearly interpolating the result.
*/}
float maxDimension = max(vTroikaGlyphDimensions.x, vTroikaGlyphDimensions.y);
float absDist = (1.0 - pow(2.0 * (alpha > 0.5 ? 1.0 - alpha : alpha), 1.0 / uTroikaSDFExponent)) * maxDimension;
float signedDist = absDist * (alpha > 0.5 ? -1.0 : 1.0);
return signedDist;
}
float troikaGlyphUvToSdfValue(vec2 glyphUV) {
vec2 textureUV = mix(vTroikaTextureUVBounds.xy, vTroikaTextureUVBounds.zw, glyphUV);
vec4 rgba = texture2D(uTroikaSDFTexture, textureUV);
float ch = floor(vTroikaTextureChannel + 0.5); //NOTE: can't use round() in WebGL1
return ch == 0.0 ? rgba.r : ch == 1.0 ? rgba.g : ch == 2.0 ? rgba.b : rgba.a;
}
float troikaGlyphUvToDistance(vec2 uv) {
return troikaSdfValueToSignedDistance(troikaGlyphUvToSdfValue(uv));
}
float troikaGetAADist() {
${''/*
When the standard derivatives extension is available, we choose an antialiasing alpha threshold based
on the potential change in the SDF's alpha from this fragment to its neighbor. This strategy maximizes
readability and edge crispness at all sizes and screen resolutions.
*/}
#if defined(GL_OES_standard_derivatives) || __VERSION__ >= 300
return length(fwidth(vTroikaGlyphUV * vTroikaGlyphDimensions)) * 0.5;
#else
return vTroikaGlyphDimensions.x / 64.0;
#endif
}
float troikaGetFragDistValue() {
vec2 clampedGlyphUV = clamp(vTroikaGlyphUV, 0.5 / uTroikaSDFGlyphSize, 1.0 - 0.5 / uTroikaSDFGlyphSize);
float distance = troikaGlyphUvToDistance(clampedGlyphUV);
// Extrapolate distance when outside bounds:
distance += clampedGlyphUV == vTroikaGlyphUV ? 0.0 :
length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions);
${''/*
// TODO more refined extrapolated distance by adjusting for angle of gradient at edge...
// This has potential but currently gives very jagged extensions, maybe due to precision issues?
float uvStep = 1.0 / uTroikaSDFGlyphSize;
vec2 neighbor1UV = clampedGlyphUV + (
vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * sign(0.5 - vTroikaGlyphUV.y)) :
vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * sign(0.5 - vTroikaGlyphUV.x), 0.0) :
vec2(0.0)
);
vec2 neighbor2UV = clampedGlyphUV + (
vTroikaGlyphUV.x != clampedGlyphUV.x ? vec2(0.0, uvStep * -sign(0.5 - vTroikaGlyphUV.y)) :
vTroikaGlyphUV.y != clampedGlyphUV.y ? vec2(uvStep * -sign(0.5 - vTroikaGlyphUV.x), 0.0) :
vec2(0.0)
);
float neighbor1Distance = troikaGlyphUvToDistance(neighbor1UV);
float neighbor2Distance = troikaGlyphUvToDistance(neighbor2UV);
float distToUnclamped = length((vTroikaGlyphUV - clampedGlyphUV) * vTroikaGlyphDimensions);
float distToNeighbor = length((clampedGlyphUV - neighbor1UV) * vTroikaGlyphDimensions);
float gradientAngle1 = min(asin(abs(neighbor1Distance - distance) / distToNeighbor), PI / 2.0);
float gradientAngle2 = min(asin(abs(neighbor2Distance - distance) / distToNeighbor), PI / 2.0);
distance += (cos(gradientAngle1) + cos(gradientAngle2)) / 2.0 * distToUnclamped;
*/}
return distance;
}
float troikaGetEdgeAlpha(float distance, float distanceOffset, float aaDist) {
#if defined(IS_DEPTH_MATERIAL) || defined(IS_DISTANCE_MATERIAL)
float alpha = step(-distanceOffset, -distance);
#else
float alpha = smoothstep(
distanceOffset + aaDist,
distanceOffset - aaDist,
distance
);
#endif
return alpha;
}
`
// language=GLSL prefix="void main() {" suffix="}"
const FRAGMENT_TRANSFORM = `
float aaDist = troikaGetAADist();
float fragDistance = troikaGetFragDistValue();
float edgeAlpha = uTroikaSDFDebug ?
troikaGlyphUvToSdfValue(vTroikaGlyphUV) :
troikaGetEdgeAlpha(fragDistance, uTroikaEdgeOffset, max(aaDist, uTroikaBlurRadius));
#if !defined(IS_DEPTH_MATERIAL) && !defined(IS_DISTANCE_MATERIAL)
vec4 fillRGBA = gl_FragColor;
fillRGBA.a *= uTroikaFillOpacity;
vec4 strokeRGBA = uTroikaStrokeWidth == 0.0 ? fillRGBA : vec4(uTroikaStrokeColor, uTroikaStrokeOpacity);
if (fillRGBA.a == 0.0) fillRGBA.rgb = strokeRGBA.rgb;
gl_FragColor = mix(fillRGBA, strokeRGBA, smoothstep(
-uTroikaStrokeWidth - aaDist,
-uTroikaStrokeWidth + aaDist,
fragDistance
));
gl_FragColor.a *= edgeAlpha;
#endif
if (edgeAlpha == 0.0) {
discard;
}
`
/**
* Create a material for rendering text, derived from a baseMaterial
*/
export function createTextDerivedMaterial(baseMaterial) {
const textMaterial = createDerivedMaterial(baseMaterial, {
chained: true,
extensions: {
derivatives: true
},
uniforms: {
uTroikaSDFTexture: {value: null},
uTroikaSDFTextureSize: {value: new Vector2()},
uTroikaSDFGlyphSize: {value: 0},
uTroikaSDFExponent: {value: 0},
uTroikaTotalBounds: {value: new Vector4(0,0,0,0)},
uTroikaClipRect: {value: new Vector4(0,0,0,0)},
uTroikaEdgeOffset: {value: 0},
uTroikaFillOpacity: {value: 1},
uTroikaPositionOffset: {value: new Vector2()},
uTroikaCurveRadius: {value: 0},
uTroikaBlurRadius: {value: 0},
uTroikaStrokeWidth: {value: 0},
uTroikaStrokeColor: {value: new Color()},
uTroikaStrokeOpacity: {value: 1},
uTroikaOrient: {value: new Matrix3()},
uTroikaUseGlyphColors: {value: true},
uTroikaSDFDebug: {value: false}
},
vertexDefs: VERTEX_DEFS,
vertexTransform: VERTEX_TRANSFORM,
fragmentDefs: FRAGMENT_DEFS,
fragmentColorTransform: FRAGMENT_TRANSFORM,
customRewriter({vertexShader, fragmentShader}) {
let uDiffuseRE = /\buniform\s+vec3\s+diffuse\b/
if (uDiffuseRE.test(fragmentShader)) {
// Replace all instances of `diffuse` with our varying
fragmentShader = fragmentShader
.replace(uDiffuseRE, 'varying vec3 vTroikaGlyphColor')
.replace(/\bdiffuse\b/g, 'vTroikaGlyphColor')
// Make sure the vertex shader declares the uniform so we can grab it as a fallback
if (!uDiffuseRE.test(vertexShader)) {
vertexShader = vertexShader.replace(
voidMainRegExp,
'uniform vec3 diffuse;\n$&\nvTroikaGlyphColor = uTroikaUseGlyphColors ? aTroikaGlyphColor / 255.0 : diffuse;\n'
)
}
}
return { vertexShader, fragmentShader }
}
})
// Force transparency - TODO is this reasonable?
textMaterial.transparent = true
// Force single draw call when double-sided
textMaterial.forceSinglePass = true
Object.defineProperties(textMaterial, {
isTroikaTextMaterial: {value: true},
// WebGLShadowMap reverses the side of the shadow material by default, which fails
// for planes, so here we force the `shadowSide` to always match the main side.
shadowSide: {
get() {
return this.side
},
set() {
//no-op
}
}
})
return textMaterial
}
+729
View File
@@ -0,0 +1,729 @@
/**
* @typedef {number|'left'|'center'|'right'} AnchorXValue
*/
/**
* @typedef {number|'top'|'top-baseline'|'top-cap'|'top-ex'|'middle'|'bottom-baseline'|'bottom'} AnchorYValue
*/
/**
* @typedef {object} TypesetParams
* @property {string} text
* @property {UserFont|UserFont[]} [font]
* @property {string} [lang]
* @property {number} [sdfGlyphSize=64]
* @property {number} [fontSize=1]
* @property {number|'normal'|'bold'} [fontWeight='normal']
* @property {'normal'|'italic'} [fontStyle='normal']
* @property {number} [letterSpacing=0]
* @property {'normal'|number} [lineHeight='normal']
* @property {number} [maxWidth]
* @property {'ltr'|'rtl'} [direction='ltr']
* @property {string} [textAlign='left']
* @property {number} [textIndent=0]
* @property {'normal'|'nowrap'} [whiteSpace='normal']
* @property {'normal'|'break-word'} [overflowWrap='normal']
* @property {AnchorXValue} [anchorX=0]
* @property {AnchorYValue} [anchorY=0]
* @property {boolean} [metricsOnly=false]
* @property {string} [unicodeFontsURL]
* @property {FontResolverResult} [preResolvedFonts]
* @property {boolean} [includeCaretPositions=false]
* @property {number} [chunkedBoundsSize=8192]
* @property {{[rangeStartIndex]: number}} [colorRanges]
*/
/**
* @typedef {object} TypesetResult
* @property {Uint16Array} glyphIds id for each glyph, specific to that glyph's font
* @property {Uint8Array} glyphFontIndices index into fontData for each glyph
* @property {Float32Array} glyphPositions x,y of each glyph's origin in layout
* @property {{[font]: {[glyphId]: {path: string, pathBounds: number[]}}}} glyphData data about each glyph appearing in the text
* @property {TypesetFontData[]} fontData data about each font used in the text
* @property {Float32Array} [caretPositions] startX,endX,bottomY caret positions for each char
* @property {Uint8Array} [glyphColors] color for each glyph, if color ranges supplied
* chunkedBounds, //total rects per (n=chunkedBoundsSize) consecutive glyphs
* fontSize, //calculated em height
* topBaseline: anchorYOffset + lines[0].baseline, //y coordinate of the top line's baseline
* blockBounds: [ //bounds for the whole block of text, including vertical padding for lineHeight
* anchorXOffset,
* anchorYOffset - totalHeight,
* anchorXOffset + maxLineWidth,
* anchorYOffset
* ],
* visibleBounds, //total bounds of visible text paths, may be larger or smaller than blockBounds
* timings
*/
/**
* @typedef {object} TypesetFontData
* @property src
* @property unitsPerEm
* @property ascender
* @property descender
* @property lineHeight
* @property capHeight
* @property xHeight
*/
/**
* @typedef {function} TypesetterTypesetFunction - compute fonts and layout for some text.
* @param {TypesetParams} params
* @param {(TypesetResult) => void} callback - function called when typesetting is complete.
* If the params included `preResolvedFonts`, this will be called synchronously.
*/
/**
* @typedef {function} TypesetterMeasureFunction - compute width/height for some text.
* @param {TypesetParams} params
* @param {(width:number, height:number) => void} callback - function called when measurement is complete.
* If the params included `preResolvedFonts`, this will be called synchronously.
*/
/**
* Factory function that creates a self-contained environment for processing text typesetting requests.
*
* It is important that this function has no closure dependencies, so that it can be easily injected
* into the source for a Worker without requiring a build step or complex dependency loading. All its
* dependencies must be passed in at initialization.
*
* @param {FontResolver} resolveFonts - function to resolve a string to parsed fonts
* @param {object} bidi - the bidi.js implementation object
* @return {{typeset: TypesetterTypesetFunction, measure: TypesetterMeasureFunction}}
*/
export function createTypesetter(resolveFonts, bidi) {
const INF = Infinity
// Set of Unicode Default_Ignorable_Code_Point characters, these will not produce visible glyphs
// eslint-disable-next-line no-misleading-character-class
const DEFAULT_IGNORABLE_CHARS = /[\u00AD\u034F\u061C\u115F-\u1160\u17B4-\u17B5\u180B-\u180E\u200B-\u200F\u202A-\u202E\u2060-\u206F\u3164\uFE00-\uFE0F\uFEFF\uFFA0\uFFF0-\uFFF8]/
// This regex (instead of /\s/) allows us to select all whitespace EXCEPT for non-breaking white spaces
const lineBreakingWhiteSpace = `[^\\S\\u00A0]`
// Incomplete set of characters that allow line breaking after them
// In the future we may consider a full Unicode line breaking algorithm impl: https://www.unicode.org/reports/tr14
const BREAK_AFTER_CHARS = new RegExp(`${lineBreakingWhiteSpace}|[\\-\\u007C\\u00AD\\u2010\\u2012-\\u2014\\u2027\\u2056\\u2E17\\u2E40]`)
/**
* Load and parse all the necessary fonts to render a given string of text, then group
* them into consecutive runs of characters sharing a font.
*/
function calculateFontRuns({text, lang, fonts, style, weight, preResolvedFonts, unicodeFontsURL}, onDone) {
const onResolved = ({chars, fonts: parsedFonts}) => {
let curRun, prevVal;
const runs = []
for (let i = 0; i < chars.length; i++) {
if (chars[i] !== prevVal) {
prevVal = chars[i];
runs.push(curRun = { start: i, end: i, fontObj: parsedFonts[chars[i]]});
} else {
curRun.end = i;
}
}
onDone(runs);
}
if (preResolvedFonts) {
onResolved(preResolvedFonts)
} else {
resolveFonts(
text,
onResolved,
{ lang, fonts, style, weight, unicodeFontsURL }
)
}
}
/**
* Main entry point.
* Process a text string with given font and formatting parameters, and return all info
* necessary to render all its glyphs.
* @type TypesetterTypesetFunction
*/
function typeset(
{
text='',
font,
lang,
sdfGlyphSize=64,
fontSize=400,
fontWeight=1,
fontStyle='normal',
letterSpacing=0,
lineHeight='normal',
maxWidth=INF,
direction,
textAlign='left',
textIndent=0,
whiteSpace='normal',
overflowWrap='normal',
anchorX = 0,
anchorY = 0,
metricsOnly=false,
unicodeFontsURL,
preResolvedFonts=null,
includeCaretPositions=false,
chunkedBoundsSize=8192,
colorRanges=null
},
callback
) {
const mainStart = now()
const timings = {fontLoad: 0, typesetting: 0}
// Ensure newlines are normalized
if (text.indexOf('\r') > -1) {
console.info('Typesetter: got text with \\r chars; normalizing to \\n')
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
}
// Ensure we've got numbers not strings
fontSize = +fontSize
letterSpacing = +letterSpacing
maxWidth = +maxWidth
lineHeight = lineHeight || 'normal'
textIndent = +textIndent
calculateFontRuns({
text,
lang,
style: fontStyle,
weight: fontWeight,
fonts: typeof font === 'string' ? [{src: font}] : font,
unicodeFontsURL,
preResolvedFonts
}, runs => {
timings.fontLoad = now() - mainStart
const hasMaxWidth = isFinite(maxWidth)
let glyphIds = null
let glyphFontIndices = null
let glyphPositions = null
let glyphData = null
let glyphColors = null
let caretPositions = null
let visibleBounds = null
let chunkedBounds = null
let maxLineWidth = 0
let renderableGlyphCount = 0
let canWrap = whiteSpace !== 'nowrap'
const metricsByFont = new Map() // fontObj -> metrics
const typesetStart = now()
// Distribute glyphs into lines based on wrapping
let lineXOffset = textIndent
let prevRunEndX = 0
let currentLine = new TextLine()
const lines = [currentLine]
runs.forEach(run => {
const { fontObj } = run
const { ascender, descender, unitsPerEm, lineGap, capHeight, xHeight } = fontObj
// Calculate metrics for each font used
let fontData = metricsByFont.get(fontObj)
if (!fontData) {
// Find conversion between native font units and fontSize units
const fontSizeMult = fontSize / unitsPerEm
// Determine appropriate value for 'normal' line height based on the font's actual metrics
// This does not guarantee individual glyphs won't exceed the line height, e.g. Roboto; should we use yMin/Max instead?
const calcLineHeight = lineHeight === 'normal' ?
(ascender - descender + lineGap) * fontSizeMult : lineHeight * fontSize
// Determine line height and leading adjustments
const halfLeading = (calcLineHeight - (ascender - descender) * fontSizeMult) / 2
const caretHeight = Math.min(calcLineHeight, (ascender - descender) * fontSizeMult)
const caretTop = (ascender + descender) / 2 * fontSizeMult + caretHeight / 2
fontData = {
index: metricsByFont.size,
src: fontObj.src,
fontObj,
fontSizeMult,
unitsPerEm,
ascender: ascender * fontSizeMult,
descender: descender * fontSizeMult,
capHeight: capHeight * fontSizeMult,
xHeight: xHeight * fontSizeMult,
lineHeight: calcLineHeight,
baseline: -halfLeading - ascender * fontSizeMult, // baseline offset from top of line height
// cap: -halfLeading - capHeight * fontSizeMult, // cap from top of line height
// ex: -halfLeading - xHeight * fontSizeMult, // ex from top of line height
caretTop,
caretBottom: caretTop - caretHeight
}
metricsByFont.set(fontObj, fontData)
}
const { fontSizeMult } = fontData
const runText = text.slice(run.start, run.end + 1)
let prevGlyphX, prevGlyphObj
fontObj.forEachGlyph(runText, fontSize, letterSpacing, (glyphObj, glyphX, glyphY, charIndex) => {
glyphX += prevRunEndX
charIndex += run.start
prevGlyphX = glyphX
prevGlyphObj = glyphObj
const char = text.charAt(charIndex)
const glyphWidth = glyphObj.advanceWidth * fontSizeMult
const curLineCount = currentLine.count
let nextLine
// Calc isWhitespace and isEmpty once per glyphObj
if (!('isEmpty' in glyphObj)) {
glyphObj.isWhitespace = !!char && new RegExp(lineBreakingWhiteSpace).test(char)
glyphObj.canBreakAfter = !!char && BREAK_AFTER_CHARS.test(char)
glyphObj.isEmpty = glyphObj.xMin === glyphObj.xMax || glyphObj.yMin === glyphObj.yMax || DEFAULT_IGNORABLE_CHARS.test(char)
}
if (!glyphObj.isWhitespace && !glyphObj.isEmpty) {
renderableGlyphCount++
}
// If a non-whitespace character overflows the max width, we need to soft-wrap
if (canWrap && hasMaxWidth && !glyphObj.isWhitespace && glyphX + glyphWidth + lineXOffset > maxWidth && curLineCount) {
// If it's the first char after a whitespace, start a new line
if (currentLine.glyphAt(curLineCount - 1).glyphObj.canBreakAfter) {
nextLine = new TextLine()
lineXOffset = -glyphX
} else {
// Back up looking for a whitespace character to wrap at
for (let i = curLineCount; i--;) {
// If we got the start of the line there's no soft break point; make hard break if overflowWrap='break-word'
if (i === 0 && overflowWrap === 'break-word') {
nextLine = new TextLine()
lineXOffset = -glyphX
break
}
// Found a soft break point; move all chars since it to a new line
else if (currentLine.glyphAt(i).glyphObj.canBreakAfter) {
nextLine = currentLine.splitAt(i + 1)
const adjustX = nextLine.glyphAt(0).x
lineXOffset -= adjustX
for (let j = nextLine.count; j--;) {
nextLine.glyphAt(j).x -= adjustX
}
break
}
}
}
if (nextLine) {
currentLine.isSoftWrapped = true
currentLine = nextLine
lines.push(currentLine)
maxLineWidth = maxWidth //after soft wrapping use maxWidth as calculated width
}
}
let fly = currentLine.glyphAt(currentLine.count)
fly.glyphObj = glyphObj
fly.x = glyphX + lineXOffset
fly.y = glyphY
fly.width = glyphWidth
fly.charIndex = charIndex
fly.fontData = fontData
// Handle hard line breaks
if (char === '\n') {
currentLine = new TextLine()
lines.push(currentLine)
lineXOffset = -(glyphX + glyphWidth + (letterSpacing * fontSize)) + textIndent
}
})
// At the end of a run we must capture the x position as the starting point for the next run
prevRunEndX = prevGlyphX + prevGlyphObj.advanceWidth * fontSizeMult + letterSpacing * fontSize
})
// Calculate width/height/baseline of each line (excluding trailing whitespace) and maximum block width
let totalHeight = 0
lines.forEach(line => {
let isTrailingWhitespace = true;
for (let i = line.count; i--;) {
const glyphInfo = line.glyphAt(i)
// omit trailing whitespace from width calculation
if (isTrailingWhitespace && !glyphInfo.glyphObj.isWhitespace) {
line.width = glyphInfo.x + glyphInfo.width
if (line.width > maxLineWidth) {
maxLineWidth = line.width
}
isTrailingWhitespace = false
}
// use the tallest line height, lowest baseline, and highest cap/ex
let {lineHeight, capHeight, xHeight, baseline} = glyphInfo.fontData
if (lineHeight > line.lineHeight) line.lineHeight = lineHeight
const baselineDiff = baseline - line.baseline
if (baselineDiff < 0) { //shift all metrics down
line.baseline += baselineDiff
line.cap += baselineDiff
line.ex += baselineDiff
}
// compare cap/ex based on new lowest baseline
line.cap = Math.max(line.cap, line.baseline + capHeight)
line.ex = Math.max(line.ex, line.baseline + xHeight)
}
line.baseline -= totalHeight
line.cap -= totalHeight
line.ex -= totalHeight
totalHeight += line.lineHeight
})
// Find overall position adjustments for anchoring
let anchorXOffset = 0
let anchorYOffset = 0
if (anchorX) {
if (typeof anchorX === 'number') {
anchorXOffset = -anchorX
}
else if (typeof anchorX === 'string') {
anchorXOffset = -maxLineWidth * (
anchorX === 'left' ? 0 :
anchorX === 'center' ? 0.5 :
anchorX === 'right' ? 1 :
parsePercent(anchorX)
)
}
}
if (anchorY) {
if (typeof anchorY === 'number') {
anchorYOffset = -anchorY
}
else if (typeof anchorY === 'string') {
anchorYOffset = anchorY === 'top' ? 0 :
anchorY === 'top-baseline' ? -lines[0].baseline :
anchorY === 'top-cap' ? -lines[0].cap :
anchorY === 'top-ex' ? -lines[0].ex :
anchorY === 'middle' ? totalHeight / 2 :
anchorY === 'bottom' ? totalHeight :
anchorY === 'bottom-baseline' ? -lines[lines.length - 1].baseline :
parsePercent(anchorY) * totalHeight
}
}
if (!metricsOnly) {
// Resolve bidi levels
const bidiLevelsResult = bidi.getEmbeddingLevels(text, direction)
// Process each line, applying alignment offsets, adding each glyph to the atlas, and
// collecting all renderable glyphs into a single collection.
glyphIds = new Uint16Array(renderableGlyphCount)
glyphFontIndices = new Uint8Array(renderableGlyphCount)
glyphPositions = new Float32Array(renderableGlyphCount * 2)
glyphData = {}
visibleBounds = [INF, INF, -INF, -INF]
chunkedBounds = []
if (includeCaretPositions) {
caretPositions = new Float32Array(text.length * 4)
}
if (colorRanges) {
glyphColors = new Uint8Array(renderableGlyphCount * 3)
}
let renderableGlyphIndex = 0
let prevCharIndex = -1
let colorCharIndex = -1
let chunk
let currentColor
lines.forEach((line, lineIndex) => {
let {count:lineGlyphCount, width:lineWidth} = line
// Ignore empty lines
if (lineGlyphCount > 0) {
// Count trailing whitespaces, we want to ignore these for certain things
let trailingWhitespaceCount = 0
for (let i = lineGlyphCount; i-- && line.glyphAt(i).glyphObj.isWhitespace;) {
trailingWhitespaceCount++
}
// Apply horizontal alignment adjustments
let lineXOffset = 0
let justifyAdjust = 0
if (textAlign === 'center') {
lineXOffset = (maxLineWidth - lineWidth) / 2
} else if (textAlign === 'right') {
lineXOffset = maxLineWidth - lineWidth
} else if (textAlign === 'justify' && line.isSoftWrapped) {
// count non-trailing whitespace characters, and we'll adjust the offsets per character in the next loop
let whitespaceCount = 0
for (let i = lineGlyphCount - trailingWhitespaceCount; i--;) {
if (line.glyphAt(i).glyphObj.isWhitespace) {
whitespaceCount++
}
}
justifyAdjust = (maxLineWidth - lineWidth) / whitespaceCount
}
if (justifyAdjust || lineXOffset) {
let justifyOffset = 0
for (let i = 0; i < lineGlyphCount; i++) {
let glyphInfo = line.glyphAt(i)
const glyphObj = glyphInfo.glyphObj
glyphInfo.x += lineXOffset + justifyOffset
// Expand non-trailing whitespaces for justify alignment
if (justifyAdjust !== 0 && glyphObj.isWhitespace && i < lineGlyphCount - trailingWhitespaceCount) {
justifyOffset += justifyAdjust
glyphInfo.width += justifyAdjust
}
}
}
// Perform bidi range flipping
const flips = bidi.getReorderSegments(
text, bidiLevelsResult, line.glyphAt(0).charIndex, line.glyphAt(line.count - 1).charIndex
)
for (let fi = 0; fi < flips.length; fi++) {
const [start, end] = flips[fi]
// Map start/end string indices to indices in the line
let left = Infinity, right = -Infinity
for (let i = 0; i < lineGlyphCount; i++) {
if (line.glyphAt(i).charIndex >= start) { // gte to handle removed characters
let startInLine = i, endInLine = i
for (; endInLine < lineGlyphCount; endInLine++) {
let info = line.glyphAt(endInLine)
if (info.charIndex > end) {
break
}
if (endInLine < lineGlyphCount - trailingWhitespaceCount) { //don't include trailing ws in flip width
left = Math.min(left, info.x)
right = Math.max(right, info.x + info.width)
}
}
for (let j = startInLine; j < endInLine; j++) {
const glyphInfo = line.glyphAt(j)
glyphInfo.x = right - (glyphInfo.x + glyphInfo.width - left)
}
break
}
}
}
// Assemble final data arrays
let glyphObj
const setGlyphObj = g => glyphObj = g
for (let i = 0; i < lineGlyphCount; i++) {
const glyphInfo = line.glyphAt(i)
glyphObj = glyphInfo.glyphObj
const glyphId = glyphObj.index
// Replace mirrored characters in rtl
const rtl = bidiLevelsResult.levels[glyphInfo.charIndex] & 1 //odd level means rtl
if (rtl) {
const mirrored = bidi.getMirroredCharacter(text[glyphInfo.charIndex])
if (mirrored) {
glyphInfo.fontData.fontObj.forEachGlyph(mirrored, 0, 0, setGlyphObj)
}
}
// Add caret positions
if (includeCaretPositions) {
const {charIndex, fontData} = glyphInfo
const caretLeft = glyphInfo.x + anchorXOffset
const caretRight = glyphInfo.x + glyphInfo.width + anchorXOffset
caretPositions[charIndex * 4] = rtl ? caretRight : caretLeft //start edge x
caretPositions[charIndex * 4 + 1] = rtl ? caretLeft : caretRight //end edge x
caretPositions[charIndex * 4 + 2] = line.baseline + fontData.caretBottom + anchorYOffset //common bottom y
caretPositions[charIndex * 4 + 3] = line.baseline + fontData.caretTop + anchorYOffset //common top y
// If we skipped any chars from the previous glyph (due to ligature subs), fill in caret
// positions for those missing char indices; currently this uses a best-guess by dividing
// the ligature's width evenly. In the future we may try to use the font's LigatureCaretList
// table to get better interior caret positions.
const ligCount = charIndex - prevCharIndex
if (ligCount > 1) {
fillLigatureCaretPositions(caretPositions, prevCharIndex, ligCount)
}
prevCharIndex = charIndex
}
// Track current color range
if (colorRanges) {
const {charIndex} = glyphInfo
while(charIndex > colorCharIndex) {
colorCharIndex++
if (colorRanges.hasOwnProperty(colorCharIndex)) {
currentColor = colorRanges[colorCharIndex]
}
}
}
// Get atlas data for renderable glyphs
if (!glyphObj.isWhitespace && !glyphObj.isEmpty) {
const idx = renderableGlyphIndex++
const {fontSizeMult, src: fontSrc, index: fontIndex} = glyphInfo.fontData
// Add this glyph's path data
const fontGlyphData = glyphData[fontSrc] || (glyphData[fontSrc] = {})
if (!fontGlyphData[glyphId]) {
fontGlyphData[glyphId] = {
path: glyphObj.path,
pathBounds: [glyphObj.xMin, glyphObj.yMin, glyphObj.xMax, glyphObj.yMax]
}
}
// Determine final glyph position and add to glyphPositions array
const glyphX = glyphInfo.x + anchorXOffset
const glyphY = glyphInfo.y + line.baseline + anchorYOffset
glyphPositions[idx * 2] = glyphX
glyphPositions[idx * 2 + 1] = glyphY
// Track total visible bounds
const visX0 = glyphX + glyphObj.xMin * fontSizeMult
const visY0 = glyphY + glyphObj.yMin * fontSizeMult
const visX1 = glyphX + glyphObj.xMax * fontSizeMult
const visY1 = glyphY + glyphObj.yMax * fontSizeMult
if (visX0 < visibleBounds[0]) visibleBounds[0] = visX0
if (visY0 < visibleBounds[1]) visibleBounds[1] = visY0
if (visX1 > visibleBounds[2]) visibleBounds[2] = visX1
if (visY1 > visibleBounds[3]) visibleBounds[3] = visY1
// Track bounding rects for each chunk of N glyphs
if (idx % chunkedBoundsSize === 0) {
chunk = {start: idx, end: idx, rect: [INF, INF, -INF, -INF]}
chunkedBounds.push(chunk)
}
chunk.end++
const chunkRect = chunk.rect
if (visX0 < chunkRect[0]) chunkRect[0] = visX0
if (visY0 < chunkRect[1]) chunkRect[1] = visY0
if (visX1 > chunkRect[2]) chunkRect[2] = visX1
if (visY1 > chunkRect[3]) chunkRect[3] = visY1
// Add to glyph ids and font indices arrays
glyphIds[idx] = glyphId
glyphFontIndices[idx] = fontIndex
// Add colors
if (colorRanges) {
const start = idx * 3
glyphColors[start] = currentColor >> 16 & 255
glyphColors[start + 1] = currentColor >> 8 & 255
glyphColors[start + 2] = currentColor & 255
}
}
}
}
})
// Fill in remaining caret positions in case the final character was a ligature
if (caretPositions) {
const ligCount = text.length - prevCharIndex;
if (ligCount > 1) {
fillLigatureCaretPositions(caretPositions, prevCharIndex, ligCount)
}
}
}
// Assemble final data about each font used
const fontData = []
metricsByFont.forEach(({index, src, unitsPerEm, ascender, descender, lineHeight, capHeight, xHeight}) => {
fontData[index] = {src, unitsPerEm, ascender, descender, lineHeight, capHeight, xHeight}
})
// Timing stats
timings.typesetting = now() - typesetStart
callback({
glyphIds, //id for each glyph, specific to that glyph's font
glyphFontIndices, //index into fontData for each glyph
glyphPositions, //x,y of each glyph's origin in layout
glyphData, //dict holding data about each glyph appearing in the text
fontData, //data about each font used in the text
caretPositions, //startX,endX,bottomY caret positions for each char
// caretHeight, //height of cursor from bottom to top - todo per glyph?
glyphColors, //color for each glyph, if color ranges supplied
chunkedBounds, //total rects per (n=chunkedBoundsSize) consecutive glyphs
fontSize, //calculated em height
topBaseline: anchorYOffset + lines[0].baseline, //y coordinate of the top line's baseline
blockBounds: [ //bounds for the whole block of text, including vertical padding for lineHeight
anchorXOffset,
anchorYOffset - totalHeight,
anchorXOffset + maxLineWidth,
anchorYOffset
],
visibleBounds, //total bounds of visible text paths, may be larger or smaller than blockBounds
timings
})
})
}
/**
* For a given text string and font parameters, determine the resulting block dimensions
* after wrapping for the given maxWidth.
* @param args
* @param callback
*/
function measure(args, callback) {
typeset({...args, metricsOnly: true}, (result) => {
const [x0, y0, x1, y1] = result.blockBounds
callback({
width: x1 - x0,
height: y1 - y0
})
})
}
function parsePercent(str) {
let match = str.match(/^([\d.]+)%$/)
let pct = match ? parseFloat(match[1]) : NaN
return isNaN(pct) ? 0 : pct / 100
}
function fillLigatureCaretPositions(caretPositions, ligStartIndex, ligCount) {
const ligStartX = caretPositions[ligStartIndex * 4]
const ligEndX = caretPositions[ligStartIndex * 4 + 1]
const ligBottom = caretPositions[ligStartIndex * 4 + 2]
const ligTop = caretPositions[ligStartIndex * 4 + 3]
const guessedAdvanceX = (ligEndX - ligStartX) / ligCount
for (let i = 0; i < ligCount; i++) {
const startIndex = (ligStartIndex + i) * 4
caretPositions[startIndex] = ligStartX + guessedAdvanceX * i
caretPositions[startIndex + 1] = ligStartX + guessedAdvanceX * (i + 1)
caretPositions[startIndex + 2] = ligBottom
caretPositions[startIndex + 3] = ligTop
}
}
function now() {
return (self.performance || Date).now()
}
// Array-backed structure for a single line's glyphs data
function TextLine() {
this.data = []
}
const textLineProps = ['glyphObj', 'x', 'y', 'width', 'charIndex', 'fontData']
TextLine.prototype = {
width: 0,
lineHeight: 0,
baseline: 0,
cap: 0,
ex: 0,
isSoftWrapped: false,
get count() {
return Math.ceil(this.data.length / textLineProps.length)
},
glyphAt(i) {
let fly = TextLine.flyweight
fly.data = this.data
fly.index = i
return fly
},
splitAt(i) {
let newLine = new TextLine()
newLine.data = this.data.splice(i * textLineProps.length)
return newLine
}
}
TextLine.flyweight = textLineProps.reduce((obj, prop, i, all) => {
Object.defineProperty(obj, prop, {
get() {
return this.data[this.index * textLineProps.length + i]
},
set(val) {
this.data[this.index * textLineProps.length + i] = val
}
})
return obj
}, {data: null, index: 0})
return {
typeset,
measure,
}
}
+9
View File
@@ -0,0 +1,9 @@
// Exports for troika-three-text package:
export { configureTextBuilder, getTextRenderInfo, typesetterWorkerModule, preloadFont, dumpSDFTextures } from './TextBuilder.js'
export { fontResolverWorkerModule } from './FontResolver.js'
export { Text } from './Text.js'
export { BatchedText } from './BatchedText.js'
export { GlyphsGeometry } from './GlyphsGeometry.js'
export { createTextDerivedMaterial } from './TextDerivedMaterial.js'
export { getCaretAtPoint, getSelectionRects } from './selectionUtils.js'
+153
View File
@@ -0,0 +1,153 @@
//=== Utility functions for dealing with carets and selection ranges ===//
/**
* @typedef {object} TextCaret
* @property {number} x - x position of the caret
* @property {number} y - y position of the caret's bottom
* @property {number} height - height of the caret
* @property {number} charIndex - the index in the original input string of this caret's target
* character; the caret will be for the position _before_ that character.
*/
/**
* Given a local x/y coordinate in the text block plane, find the nearest caret position.
* @param {TroikaTextRenderInfo} textRenderInfo - a result object from TextBuilder#getTextRenderInfo
* @param {number} x
* @param {number} y
* @return {TextCaret | null}
*/
export function getCaretAtPoint(textRenderInfo, x, y) {
let closestCaret = null
const rows = groupCaretsByRow(textRenderInfo)
// Find nearest row by y first
let closestRow = null
rows.forEach(row => {
if (!closestRow || Math.abs(y - (row.top + row.bottom) / 2) < Math.abs(y - (closestRow.top + closestRow.bottom) / 2)) {
closestRow = row
}
})
// Then find closest caret by x within that row
closestRow.carets.forEach(caret => {
if (!closestCaret || Math.abs(x - caret.x) < Math.abs(x - closestCaret.x)) {
closestCaret = caret
}
})
return closestCaret
}
const _rectsCache = new WeakMap()
/**
* Given start and end character indexes, return a list of rectangles covering all the
* characters within that selection.
* @param {TroikaTextRenderInfo} textRenderInfo
* @param {number} start - index of the first char in the selection
* @param {number} end - index of the first char after the selection
* @return {Array<{left, top, right, bottom}> | null}
*/
export function getSelectionRects(textRenderInfo, start, end) {
let rects
if (textRenderInfo) {
// Check cache - textRenderInfo is frozen so it's safe to cache based on it
let prevResult = _rectsCache.get(textRenderInfo)
if (prevResult && prevResult.start === start && prevResult.end === end) {
return prevResult.rects
}
const {caretPositions} = textRenderInfo
// Normalize
if (end < start) {
const s = start
start = end
end = s
}
start = Math.max(start, 0)
end = Math.min(end, caretPositions.length + 1)
// Build list of rects, expanding the current rect for all characters in a run and starting
// a new rect whenever reaching a new line or a new bidi direction
rects = []
let currentRect = null
for (let i = start; i < end; i++) {
const x1 = caretPositions[i * 4]
const x2 = caretPositions[i * 4 + 1]
const left = Math.min(x1, x2)
const right = Math.max(x1, x2)
const bottom = caretPositions[i * 4 + 2]
const top = caretPositions[i * 4 + 3]
if (!currentRect || bottom !== currentRect.bottom || top !== currentRect.top || left > currentRect.right || right < currentRect.left) {
currentRect = {
left: Infinity,
right: -Infinity,
bottom,
top,
}
rects.push(currentRect)
}
currentRect.left = Math.min(left, currentRect.left)
currentRect.right = Math.max(right, currentRect.right)
}
// Merge any overlapping rects, e.g. those formed by adjacent bidi runs
rects.sort((a, b) => b.bottom - a.bottom || a.left - b.left)
for (let i = rects.length - 1; i-- > 0;) {
const rectA = rects[i]
const rectB = rects[i + 1]
if (rectA.bottom === rectB.bottom && rectA.top === rectB.top && rectA.left <= rectB.right && rectA.right >= rectB.left) {
rectB.left = Math.min(rectB.left, rectA.left)
rectB.right = Math.max(rectB.right, rectA.right)
rects.splice(i, 1)
}
}
_rectsCache.set(textRenderInfo, {start, end, rects})
}
return rects
}
const _caretsByRowCache = new WeakMap()
/**
* Group a set of carets by row of text, caching the result. A single row of text may contain carets of
* differing positions/heights if it has multiple fonts, and they may overlap slightly across rows, so this
* uses an assumption of "at least overlapping by half" to put them in the same row.
* @return Array<{bottom: number, top: number, carets: TextCaret[]}>
*/
function groupCaretsByRow(textRenderInfo) {
// textRenderInfo is frozen so it's safe to cache based on it
let rows = _caretsByRowCache.get(textRenderInfo)
if (!rows) {
rows = []
const {caretPositions} = textRenderInfo
let curRow
const visitCaret = (x, bottom, top, charIndex) => {
// new row if not overlapping by at least half
if (!curRow || (top < (curRow.top + curRow.bottom) / 2)) {
rows.push(curRow = {bottom, top, carets: []})
}
// expand vertical limits if necessary
if (top > curRow.top) curRow.top = top
if (bottom < curRow.bottom) curRow.bottom = bottom
curRow.carets.push({
x,
y: bottom,
height: top - bottom,
charIndex,
})
}
let i = 0
for (; i < caretPositions.length; i += 4) {
visitCaret(caretPositions[i], caretPositions[i + 2], caretPositions[i + 3], i / 4)
}
// Add one more caret after the final char
visitCaret(caretPositions[i - 3], caretPositions[i - 2], caretPositions[i - 1], i / 4)
}
_caretsByRowCache.set(textRenderInfo, rows)
return rows
}
+149
View File
@@ -0,0 +1,149 @@
/*
Copyright 2012, Steffen Hanikel (https://github.com/hanikesn)
Modified by Artemy Tregubenko, 2014 (https://github.com/arty-name/woff2otf)
Modified by Jason Johnston, 2019 (pako --> fflate)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A tool to convert a WOFF back to a TTF/OTF font file, in pure Javascript
*/
import { inflateSync } from 'fflate'
export function convert_streams(bufferIn) {
var dataViewIn = new DataView(bufferIn);
var offsetIn = 0;
function read2() {
var uint16 = dataViewIn.getUint16(offsetIn);
offsetIn += 2;
return uint16;
}
function read4() {
var uint32 = dataViewIn.getUint32(offsetIn);
offsetIn += 4;
return uint32;
}
function write2(uint16) {
dataViewOut.setUint16(offsetOut, uint16);
offsetOut += 2;
}
function write4(uint32) {
dataViewOut.setUint32(offsetOut, uint32);
offsetOut += 4;
}
var WOFFHeader = {
signature: read4(),
flavor: read4(),
length: read4(),
numTables: read2(),
reserved: read2(),
totalSfntSize: read4(),
majorVersion: read2(),
minorVersion: read2(),
metaOffset: read4(),
metaLength: read4(),
metaOrigLength: read4(),
privOffset: read4(),
privLength: read4()
};
var entrySelector = 0;
while (Math.pow(2, entrySelector) <= WOFFHeader.numTables) {
entrySelector++;
}
entrySelector--;
var searchRange = Math.pow(2, entrySelector) * 16;
var rangeShift = WOFFHeader.numTables * 16 - searchRange;
var offset = 4 + 2 + 2 + 2 + 2;
var TableDirectoryEntries = [];
for (var i = 0; i < WOFFHeader.numTables; i++) {
TableDirectoryEntries.push({
tag: read4(),
offset: read4(),
compLength: read4(),
origLength: read4(),
origChecksum: read4()
});
offset += 4 * 4;
}
var arrayOut = new Uint8Array(
4 + 2 + 2 + 2 + 2 +
TableDirectoryEntries.length * (4 + 4 + 4 + 4) +
TableDirectoryEntries.reduce(function(acc, entry) { return acc + entry.origLength + 4; }, 0)
);
var bufferOut = arrayOut.buffer;
var dataViewOut = new DataView(bufferOut);
var offsetOut = 0;
write4(WOFFHeader.flavor);
write2(WOFFHeader.numTables);
write2(searchRange);
write2(entrySelector);
write2(rangeShift);
TableDirectoryEntries.forEach(function(TableDirectoryEntry) {
write4(TableDirectoryEntry.tag);
write4(TableDirectoryEntry.origChecksum);
write4(offset);
write4(TableDirectoryEntry.origLength);
TableDirectoryEntry.outOffset = offset;
offset += TableDirectoryEntry.origLength;
if ((offset % 4) != 0) {
offset += 4 - (offset % 4)
}
});
var size;
TableDirectoryEntries.forEach(function(TableDirectoryEntry) {
var compressedData = bufferIn.slice(
TableDirectoryEntry.offset,
TableDirectoryEntry.offset + TableDirectoryEntry.compLength
);
if (TableDirectoryEntry.compLength != TableDirectoryEntry.origLength) {
var uncompressedData = new Uint8Array(TableDirectoryEntry.origLength)
inflateSync(
new Uint8Array(compressedData, 2), //skip deflate header
uncompressedData
)
} else {
uncompressedData = new Uint8Array(compressedData);
}
arrayOut.set(uncompressedData, TableDirectoryEntry.outOffset);
offset = TableDirectoryEntry.outOffset + TableDirectoryEntry.origLength;
var padding = 0;
if ((offset % 4) != 0) {
padding = 4 - (offset % 4);
}
arrayOut.set(
new Uint8Array(padding).buffer,
TableDirectoryEntry.outOffset + TableDirectoryEntry.origLength
);
size = offset + padding;
});
return bufferOut.slice(0, size);
}