├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── blob ├── logo.png ├── pull_all_modules.sh └── wiki │ ├── artifacts_dl1.png │ ├── artifacts_dl2.png │ └── artifacts_dl3.png ├── doc ├── Layers.md ├── Why.md └── article.md └── roadmap ├── .timestamp ├── Makefile ├── graph.dot └── roadmap.png /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Modular Font Editor K (MFEK) Code of Conduct 2 | 3 | The code of conduct of the MFEK project, based on the Django Project's code of conduct (by way of [the Markdown version in the highly Google-ranked repository mfl_api_docs](https://github.com/MasterFacilityList/mfl_api_docs/edit/master/docs/15_code_of_conduct.rst)), consists of the follwing six pillars: 4 | 5 | * **Be friendly and patient**. 6 | * **Be welcoming**. We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, colour, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. 7 | * **Be considerate**. Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. 8 | * **Be respectful**. Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Django community should be respectful when dealing with other members as well as with people outside the Django community. 9 | * **Be careful in the words that you choose**. We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: 10 | 11 | * Violent threats or language directed against another person. 12 | * Discriminatory jokes and language. 13 | * Posting sexually explicit or violent material. 14 | * Posting (or threatening to post) other people's personally identifying information ("doxing"). 15 | * Personal insults, especially those using racist or sexist terms. 16 | * Unwelcome sexual attention. 17 | * Advocating for, or encouraging, any of the above behavior. 18 | * Repeated harassment of others. In general, if someone asks you to stop, then stop. 19 | 20 | * **When we disagree, try to understand why**. Disagreements, both social and technical, happen all the time and Django is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we’re different. The strength of Django comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere, rather offer to help resolving issues and to help learn from mistakes. 21 | 22 | If you believe the code of conduct has been broken, you may email lead developer Fred Brennan <copypasteⒶkittens.ph> and ask for his intervention. Please put ``[MFEK CoC]`` in the subject line. Please consider asking the other party for an apology, if appropriate to your situation, before doing so—we are all human. 23 | 24 | The license of the MFEK CoC is the Creative Commons Attribution 3.0 license, [following Django's lead](https://www.djangoproject.com/conduct/). 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modular Font Editor K 2 | 3 | 4 | 5 | **Modular Font Editor K** (MFEK) is an open source modular font editor. It attempts to apply the Unix adage that each program should do one thing and do it well to a GUI font editor. 6 | 7 | MFEK is still very alpha, and many parts are missing. You can help! 8 | 9 | **[Why MFEK?](https://github.com/MFEK/docs/blob/master/doc/Why.md)** 10 | 11 | To pull all modules, why not use this script? 12 | 13 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/MFEK/docs/master/blob/pull_all_modules.sh)" 14 | 15 | To see who wrote an MFEK module, check its `AUTHORS` file. The two main authors of MFEK are Fredrick R. Brennan (@ctrlcctrlv) and Matthew Blanchard (@MatthewBlanchard). 16 | 17 | ## Current roadmap as of 2021-11-02 18 | ![Current roadmap as of 2021-11-02](https://raw.githubusercontent.com/MFEK/docs/master/roadmap/roadmap.png) 19 | ### Roadmap key 20 | * Dotted line around module name — module not started. 21 | * Dashed line around module name — module started, yet far from completion. 22 | * No line around module name — module is still far from being begun, and may indeed be unnecessary and never be begun. 23 | * A bold black arrow represents a program calling a program. 24 | * A red arrow represents a program including a library. 25 | * A black arrow represents a library including a library. 26 | * All libraries and programs are assumed to be in Rust unless noted otherwise. 27 | 28 | ## Modular programs 29 | 30 | * [`MFEKglif`](https://github.com/MFEK/glif) (.glif editor w/planned Spiro support) 31 | * [`MFEKpathops`](https://github.com/MFEK/pathops) (applies different kinds of operations to .glif paths) 32 | * [`MFEKstroke`](https://github.com/MFEK/stroke) (applies different kinds of strokes to .glif files with open contours) 33 | * [`MFEKmetadata`](https://github.com/MFEK/metadata) (UFO metadata querier) 34 | * [`MFEKabout`](https://github.com/MFEK/about) (MFEK's about screen) 35 | 36 | ### Planned 37 | 38 | * `MFEKufo` (a launcher for MFEKglif that displays all glyphs) 39 | * `MFEKdesignspace` (design space XML creator/editor) 40 | * `MFEKmetrics` (load UFO file into HarfBuzz and output typed text, edit horizontal/vertical kerning and bearings, test interpolation) 41 | * `MFEKopentype` (OpenType layout editor based on @simoncozens' ideas) 42 | * `MFEKexport` (frontend to fontmake) 43 | 44 | #### Far off 45 | 46 | * `MFEKpshints` (Add PostScript hints to glyphs and test them) 47 | * `MFEKtruetype` (basically would be an open source version of Visual TrueType (VTT)) 48 | 49 | ## Libraries 50 | 51 | * [`glifparser.rlib`](https://github.com/MFEK/glifparser.rlib) (a .glif parser) 52 | * [`integer-or-float.rlib`](https://github.com/MFEK/integer_or_float.rlib) (implements a .glif data type) 53 | * (We need this because Norad has no support for `` in `.glif` files, and due to how they went about implementing Norad, fixing that is trickier than having my own glyph parser. Furthermore, as I plan to support Spiro, B-Splines, etc., through UFO format extensions, I should have one anyway.) 54 | * [`icu-data.rlib`](https://github.com/MFEK/icu-data.rlib) (Unicode ICU data without C libicu, currently only encodings) 55 | * [`ipc.rlib`](https://github.com/MFEK/ipc.rlib) (_very_ basic inter-process communication functions) 56 | * [`math.rlib`](https://github.com/MFEK/math.rlib) (implements algorithms for MFEKstroke: Pattern-Along-Path, Variable/Constant Width Stroke, etc.) 57 | * [`spiro.rlib`](https://github.com/MFEK/spiro.rlib) (a Rust implementation of Raph Levien's [Spiro](https://github.com/raphlinus/spiro) curve type) 58 | * [`feaparser.rlib`](https://github.com/MFEK/feaparser.rlib) (an OpenType Feature File Format [`.fea`] parser) 59 | * [`glifrenderer.rlib`](https://github.com/MFEK/glifrenderer.rlib) (a Skia renderer focused on rendering font glyphs in a pleasing way) 60 | 61 | ### Planned 62 | 63 | * libskef (Port of @skef's «Expand Stroke» feature to a reusable C API. Will likely also require `SplineSet` type from FontForge.) 64 | 65 | ## Flow 66 | 67 | MFEK's inter-process communication (IPC) will be minimal. UFO is the format, and most of the time, MFEK modules are going to be starting with just what's on the disk. We can put the planned MFEK modules onto a linear spectrum between _forms_ and _canvases_. The quintessential form is MFEKdesignspace: it is purely a form. The user inputs the names of their UFO masters, their axes, instances, and rules, and out comes a rigidly hierarchical `.designspace` file for consumption by `fontmake` and MFEKinterpolate. As a form, once it's filled out, it's done. We may need to go back and add more rules or instances or what have you, but it's essentially one run and done. Meanwhile, the quintessential canvas is MFEKglif: the user draws their glyphs and can spend as long as they want doing so, and will likely have multiple MFEKglif instances running in parallel. As long as you're working on the font, MFEKglif will probably be open most of the time. 68 | 69 | MFEKstroke is an interesting example because it's in between. The user needs to fill out a form, yes, the stroke parameters — but she also needs to see what the glyph will look like with the parameters, and tweak them to her heart's content to get the best output. However, once parameters are chosen, it becomes almost pure form. It is both form and canvas, as is MFEKinterpolate. MFEKopentype and MFEKkern, meanwhile, have some form elements, but are more canvas than form. 70 | 71 | Most of our IPC can just be `system` calls (launching new processes). At our most advanced, we watch a file or directory for changes and reconcile accordingly. 72 | 73 | Let's consider we want to make a cursive font. Here is how we would proceed, according to my vision: 74 | 75 | * Run MFEKmetadata. When it gets no argument, or the argument of a non-existent directory, it assumes we want a new font. So, we fill out a form. 76 | * We then run MFEKufo and see empty squares. Clicking `A` launches MFEKglif with the command line `MFEKglif glyphs/A_.glif`. 77 | * MFEKglif calls MFEKmetadata as `MFEKmetadata metrics`. MFEKmetadata returns on stdout the em-size, ascender, descender, and x-height and cap-height if known based on the UFO metadata. MFEKglif draws guidelines. 78 | * We start drawing our `A`. We decide to make it a single stroke. We press Ctrl-Shift-E, and MFEKglif launches MFEKstroke, saves the state of the glyph in the undoes list, and begins monitoring `A_.glif` for changes. 79 | * MFEKstroke, likewise, monitors `A_.glif`. If written out, it changes its display. Perhaps auto-saving of every action can be optionally considered. How daring are we? Will our `.glif` file's `` contain undoes? Perhaps! 80 | * The user settles on a stroke and presses Stroke. MFEKstroke writes and exits. MFEKglif dutifully reads from the disk. 81 | * And so on for the basic Latin. It's come time to add an OpenType table. Launch `MFEKopentype`, which will build the font and use HarfBuzz to display it, and auto-update as the user writes their OpenType Layout code. This could be FEA, but it also could be Simon Cozens' [FEZ](https://github.com/simoncozens/fez), a higher level FEA-like syntax. MFEKopentype must be more conservative and only reload the font upon saving of any glyph, not every small action in MFEKglif. 82 | * Finally, we have something we think servicable. In MFEKufo we press Generate, which calls MFEKexport. We're not writing a TrueType generator here, it's a simple form that calls `fontmake` with appropriate arguments. 83 | 84 | Notice that while MFEK grows in size, we can offload one or more steps to FontForge/`fontmake` scripts. So even with only one or two programs, MFEK is immediately useful—we don't need the entire thing done to start using it in production. In fact, I plan to make fonts while I work on MFEK, and use less and less of FontForge over time. 85 | 86 | But our goal is not to totally abandon FontForge, or AFDKO, or fontmake. No, rather, we want a new GUI. But to build our modular font editor, we'll take the good parts out of everything. FontForge is great at dealing with legacy formats: we can imagine a MFEKconvert based on a C library sourced from FontForge code, which calls either that, fontmake, or AFDKO, based on the type of conversion requested. 87 | 88 | ## Code of Conduct (CoC) 89 | 90 | See [`CODE_OF_CONDUCT.md`](https://github.com/MFEK/docs/blob/master/CODE_OF_CONDUCT.md). The MFEK CoC there, last updated 17th November 2021, is that of the whole organization and all of the repositories and communication channels under its umbrella. 91 | 92 | ## Thanks to… 93 | 94 | * Matthew Blanchard; 95 | * Caleb Maclennan; 96 | * Dave Crossland; 97 | * Simon Cozens; 98 | * Eli Heuer; 99 | * Georg Duffner (for EB Garamond ExtraBold, used in our logo); 100 | * All organization members, module authors and contributors; 101 | * All developers of open source font-related software and fonts! 102 | -------------------------------------------------------------------------------- /blob/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MFEK/docs/07f862966db42a8a0be27c9c335a799fbb2de21c/blob/logo.png -------------------------------------------------------------------------------- /blob/pull_all_modules.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Pull all current MFEK modules! Latest version @ https://github.com/MFEK/docs/ 3 | 4 | MODULES=("glif" "stroke" "pathops" "metadata" "init" "about") 5 | 6 | for m in ${MODULES[@]}; do 7 | if [ ! -d MFEK"$m" ]; then 8 | git clone https://github.com/MFEK/"$m" MFEK"$m" 9 | else 10 | cd MFEK"$m"; 11 | git pull --ff-only 12 | cargo build 13 | cd .. 14 | fi 15 | done 16 | -------------------------------------------------------------------------------- /blob/wiki/artifacts_dl1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MFEK/docs/07f862966db42a8a0be27c9c335a799fbb2de21c/blob/wiki/artifacts_dl1.png -------------------------------------------------------------------------------- /blob/wiki/artifacts_dl2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MFEK/docs/07f862966db42a8a0be27c9c335a799fbb2de21c/blob/wiki/artifacts_dl2.png -------------------------------------------------------------------------------- /blob/wiki/artifacts_dl3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MFEK/docs/07f862966db42a8a0be27c9c335a799fbb2de21c/blob/wiki/artifacts_dl3.png -------------------------------------------------------------------------------- /doc/Layers.md: -------------------------------------------------------------------------------- 1 | # MFEKglif: towards layers and history 2 | 3 | Layers were particularly difficult to add to MFEKglif because of the way the UFO standard handles layers. However, I think I finally have an implementation model that will work, for both the definition of layers and the definition of history (undoes and redoes). 4 | 5 | ## Layers in the UFO standard 6 | 7 | In the UFO standard, each `.glif` file is a layer unto itself. The `glyphs` directory actually refers to the "Foreground" ("Fore") layer in FontForge, and the UFO standard dictates that _new_ `glyphs` directories ought to be made for each of the layers in the font according to the data in `/layercontents.plist`. For example: 8 | 9 | ```xml 10 | 11 | 13 | 14 | 15 | 16 | public.default 17 | glyphs 18 | 19 | 20 | Sketches 21 | glyphs.S_ketches 22 | 23 | 24 | public.background 25 | glyphs.public.background 26 | 27 | 28 | 29 | ``` 30 | 31 | would refer to a UFO font with three glyphs directories in the root: `glyphs`, `glyphs.S_ketches`, and `glyphs.public.background`. 32 | 33 | There are only two defined layer keys in the standard (though all beginning in `public.` are reserved): `public.default`, for `glyphs`, and `public.background`, for the Background layer found in many font editors. 34 | 35 | Layer properties are defined _inside_ the `glyphs` directories in `layerinfo.plist` files. In UFO3, only `color` is reserved. Example: 36 | 37 | ```xml 38 | 39 | 40 | 41 | 42 | color 43 | 0,1,1,0.7 44 | 45 | 46 | ``` 47 | 48 | Besides this, a `lib` `` can contain font editor-specific data. 49 | 50 | ## Layers in MFEKglif 51 | 52 | This way of doing things will not work well for MFEKglif. For one thing, `.glif` files can be unattached and not always part of a parent UFO. Therefore, we need our own layers format. Layers are how they are in the UFO standard because UFO is expected to be used to generate OpenType fonts, and in the OpenType format, each output layer is defined as a specific glyph. For example, in an emoji font with two colors, you'd have for example `Yellow`, `Black`, `White` and `Red`. Then, from `glyphs.Y_ellow`, `glyphs.B_lack`, etc., the OpenType font would actually have four glyphs for each defined glyph in the font editor. 53 | 54 | So, I think the best way to handle this is much like we've already done by having `MFEKstroke` convert a variable width stroked (VWS) path into a normal `.glif` `` of ``'s. I've come up with a format which will use the `` for holding the other layers, and then have a program which will make the actually `fontmake`-compilable UFO with `glyphs` directories for each layer. I call making the `fontmake`-compilable UFO “Reconciliaton” (see § Reconciliation). 55 | 56 | MFEKglif already can detect if it is being called with the path to a detached `.glif` file or a `.glif` file inside of a `glyphs` directory in a parent UFO. It should further make sure that it is always either being run on one of these two, and not a glyph layer created by other software, which could cause all sorts of collission issues. If the user tries to run e.g. `MFEKglif A.ufo/glyphs.public.background/A_.glif`, MFEKglif should refuse to do that and exit non-0. 57 | 58 | ## MFEKglif's format 59 | 60 | On startup, the first thing is to immediately figure out which contours are in each layer. All of the contours outside of the `` are part of the mandatory `Foreground` layer, and a glyph file cannot have zero layers. 61 | 62 | The `` section will have a new element called `` which will contain `` elements which may contain any number of ``, ``, and `` elements. 63 | 64 | The order of the `` elements is important, and defines the render Z-order. Earlier layers are rendered above later layers. 65 | 66 | ### `` attributes 67 | 68 | 1. `name` ⇒ **String**: Display name of the layer. Must be unique. 69 | 2. `outputtable` ⇒ **Boolean**: Whether this layer should be output during reconciliation. User should have control of this, default to `false`. 70 | 3. `color` _(optional)_ ⇒ **32-bit unsigned integer**: The color of the contours in the layer. One byte each for R, G, B, and A. 71 | 4. `dirname` ⇒ **fn() -> String**: Hidden from the user. Generates by concatenating the string `glyphs.MFEKglif.` plus the `name` with its capital letters followed by underscores. So, for `Sketches`, `dirname` is `glyphs.MFEKglif.S_ketches`. However, for the layer with `mandatory` set to `true` it is always `public.default`. 72 | 5. `mandatory` ⇒ **Boolean**: Only `true` for a single layer, the default (but renamable) `Foreground`. 73 | 74 | ## History 75 | 76 | The `` section will have a `` section containing a `` for each layer. `` will have a `layer` attribute equal to the `dirname`, one `` per layer. (This is for human readability, the `` elements are in the same order as the ``'s.) `` contains any number of ``'s. The first `` is the most recent revision, working backwards. `` has an attribute `redoes`, which is an unsigned integer representing the number of following `` elements which are actually forward in time, but have been undone by the user. It should usually be `0`, only appearing when the user undoes an action and then saves without doing any other action which would cause redoes to be lost. 77 | 78 | ## Reconciliation 79 | 80 | It is a fact that most of the time this system will work just fine and users will not care if their non-`Foreground` layers get written, because they are for prototyping/storage of images and not for writing into the final font. 81 | 82 | However, with the increasing importance of color fonts, it is important to have a program which can split our `.glif` format into the standard `.glif` format. This program should receive a `.ufo` directory as an argument, and not be called by MFEKglif itself, even via IPC, but in future by MFEKufo (the font overview program I envision which will pop MFEKglif instances when users open glyphs just by calling `system`). 83 | 84 | So, given a UFO directory, the reconciliation process is such: 85 | 86 | 1. Read in `/layercontents.plist`. Determine what layers are already defined for this UFO. 87 | 2. If non-`public.default` layers are defined, iterate through all of their glyphs and make sure that none of them are ones that we would split. (None of them contain `` in ``.) If they do, print a helpful message and return non-0. (The user likely got into this situation because they thought they could get around MFEKglif refusing to edit another software's layer by just moving files around, not understanding the clobbering issue.) 88 | 3. In the `glyphs` (`public.default`) directory, figure out all the unique `outputtable` `dirname`'s requested. For example, one glyph in a color (emoji) font might have a `Blue` layer, while another one might have a `Red` but no `Blue`. 89 | 4. Make sure that there is no mismatch between `outputtable` for a given `dirname`. If there is, tell the user and return non-0. (Example message: “Layer Blue in glyph ‹A› is set as "do not output", but layer Blue in glyph ‹a› is set to "output". Cannot continue, please fix the layer properties in MFEKglif.”) 90 | 5. Create empty directories for all the `dirname`'s. 91 | 6. Using the `` `color` attribute, write out a `layerinfo.plist` for the list of `dirname`'s. If there's no color set, write an empty `layerinfo.plist`. 92 | 7. Copy `contents.plist`. 93 | 8. For each `dirname`, open each glyph in `glyphs` and determine if it has the current `dirname` in its `gliflayers`. If it does, copy the contents of its `` into a new `.glif` file with those contents in the document root as normal. _If there is no `dirname` in `gliflayers`_, write an empty `.glif`. 94 | 95 | ## Wrapping up: a note on images 96 | 97 | The UFO standard [reads](https://unifiedfontobject.org/versions/ufo3/glyphs/glif/#image)… 98 | 99 | > ### image: An image reference.↩ 100 | > 101 | > This optional element represents an image element in a glyph. It may occur at most once. 102 | 103 | We're ignoring this. We'll put as many images as we please in a `.glif`. The reason it's like this is because it is actually possible to store images in an OpenType font, and again, UFO is mostly focused on creating OpenType fonts from XML data. 104 | 105 | At the time of reconciliation, we can warn the user that subsequent images will either be ignored or can cause errors in some software. But, there's no reason for this arbitrary limitation for a font editor; indeed it's useful to have multiple images in one layer at times. -------------------------------------------------------------------------------- /doc/Why.md: -------------------------------------------------------------------------------- 1 | # Why? 2 | ### Why MFEK? 3 | 4 | My [Modular Font Editor K](https://github.com/MFEK/docs) project, despite its roadmap, has mystified 5 | some. Why work on it at all, and if I insist, why not work on the Runebender project? 6 | 7 | I aim to explain myself. 8 | 9 | Many have tried to replace FontForge—all have failed. I might fail, in fact, history says I 10 | probably will. Yet, the current state of affairs is so bad I feel I must try. 11 | 12 | # Updates to the status quo since this was written 13 | (Update May 2021: _This prospectus of the project was written in September 2020. Since then some 14 | things have changed, including the degree to which we collaborate with the Runebender project; while 15 | we don't contribute to Runebender itself, we do contribute to a few of its libraries like `kurbo` 16 | and `norad`._) 17 | 18 | (Update November 2021: _Our collaboration with the Rust ecosystem mentioned earlier in the year has 19 | continued thankfully, and `norad`, `flo_curves` and other crates have even made changes with our 20 | project in mind._ ♥) 21 | 22 | # Why we need a new free software font editor 23 | Progress on FontForge has ground to a halt, and often I felt I was the only one interested in 24 | continuing to make progress. It's not hard to understand why this has happened. Maintaining 25 | FontForge is _hard_. Adding new features to it is harder. It is written in a language fewer and 26 | fewer people know and requires C skills fewer and fewer people have. 27 | 28 | Many of FontForge's problems are technical: the biggest difficulty is UI. It uses a custom toolkit, 29 | which despite earnest attempts, I highly doubt it's ever going anywhere. Getting anything done in it 30 | is very difficult and sometmimes nigh impossible. 31 | 32 | This is not where the custom code ends. It has custom code for reading and writing fonts, parsing 33 | feature file syntax, stroking paths, building OpenType layout, and even _rendering text_. The 34 | MetricsView as we call it does not show the output of HarfBuzz; indeed, by default, it doesn't even 35 | rasterize with FreeType, but uses a custom rasterizer. 36 | 37 | All of the custom undocumented code is so vexing as to lead one to despair. The man who understood 38 | it all is gone, and as reading undocumented code is so much harder than writing it, we are in a 39 | state of deadlock. 40 | 41 | Some of the other problems are social. I admit that at the beginning of my becoming more active on 42 | the FontForge bug tracker I was unaccustomed to being in such a role; as I made more and more 43 | contributions my voice suddenly became not just my opinion but authoritative. We had a contentious 44 | battle on the FontForge tracker over licensing and the meaning of the headers in most files, which 45 | alienated a contributor who had previously done a lot. Sometimes I still think they're mad at me: I 46 | won't mention who but I'm sorry to them and won't be pushing further on a relicensing, though 47 | obviously I'm not in a position to tell the owners of the repository what they ought to do. 48 | 49 | Another big problem that a new project doesn't face is dealing with legacy code. We can't just start 50 | ripping all the custom stuff out of FontForge. We will break people's workflows, build scripts, and 51 | fonts. I can't rip out the MetricsView and replace it with HarfBuzz as much as I'd like to: people 52 | probably rely on its output. So, what, we should maintain two ways of showing the user how their 53 | font will look? That will make maintainership harder, not easier. 54 | 55 | Let me give you an example: COLR/CPAL, an emoji (color) font format. I was so ready to work on this. 56 | But working on it is going to be a nightmare. ~~I'm still willing to do it for pay of course, but~~ 57 | The UI elements alone are trying. Then we get into the fact that FontForge uses its own custom 58 | rasterizer to generate glyphs in the FontView, and so that needs either a total rewrite, or a 59 | replacement with FreeType. It of course doesn't support color or layering. So am I to integrate a 60 | library, or for some reason write a color font rasterizer when many exist? And let's not even get 61 | into all the UI niggles in the CharView this will introduce; we don't use Skia but rather just call 62 | random Cairo commands. Oh, and there's also a non-Cairo version of the CharView, which must not be 63 | broken without possible conflict as if we deprecate building _sans_ Cairo this will break someone's 64 | build! 65 | 66 | # Why Runebender is not the new free software font editor we need (in my opinion) 67 | 68 | First of all, I have no problem with authors of Runebender and wish them all the success in the 69 | world. This success however has not come yet, FontForge remains dominant. At the time I founded the 70 | MFEK project, Runebender had no commits to it for two months. I was told that actually commits are 71 | happening ... to the UI layer of Runebender. Incredibly, Runebender has an entirely new UI toolkit, 72 | their version of FontForge's GDraw (hopefully without any of its flaws), they call it Druid. That's 73 | not all, they also have an entirely new path rasterizer, called Piet. 74 | 75 | In fact, Runebender is another hive of custom code everywhere, for reasons I do not find logical. 76 | [As I've written in the past, regarding 77 | Alacritty,](https://gist.github.com/ctrlcctrlv/978b3ee4f55d4b4ec415a985e01cb1c9) which was a highly 78 | controversial blog post, Rust projects overly aim towards perfect code. I reject this philosophy. 79 | 80 | [At the time, I replied to criticism of me on 81 | Reddit](https://www.reddit.com/r/rust/comments/ewgczz/rust_maintainer_perfectionism_or_the_tragedy_of/) 82 | thus: 83 | 84 | > > I don't think it's surprising that people who make hobby projects in a language that has 85 | > > correctness as a core design goal would have an attitude of wanting to assure correctness, even 86 | > > at the cost of development velocity. Not everyone wants to move fast and break things. 87 | > 88 | > Sure, that's certainly one way to look at it. It helps to remember though that correctness isn't 89 | > free, and isn't a feature of the language, (although features of the language help create more 90 | > correct programs,) but is in its higher forms, once the Rust compiler and borrow checker are happy 91 | > with the code, highly subjective. 92 | 93 | I don't think it's a big problem to use C(++) in Rust. I don't really know that I agree with the 94 | philosophy that a Rust application should be _entirely_ Rust. For me one of the benefits of Rust is 95 | that I can quickly and safely wrap popular C APIs. I benefit from the work Google puts into Chromium 96 | by using Skia. I benefit from the huge user community of Dear Imgui, including such illustrious 97 | software as SHADERed. 98 | 99 | There simply are not enough hours in the day for me to write a font editor, _and_ a GUI toolkit, 100 | _and_ a path rasterizer, and keep those things well maintained while working on my font editor. But, 101 | @raphlinus and @cymr are much better programmers than me, so maybe they can do it. Let's see, I 102 | guess. 103 | 104 | That is to say, Rust is the perfect tool for the job, it's the Rust users that are the trouble. ;-) 105 | 106 | But, more than this, I've decided that the entire way we're going about this problem is wrong. We 107 | think there should be one program called "font editor", in this case Runebender, and it should do 108 | everything that Glyphsapp does on Apple, and that's that. And so, until "font editor" is production 109 | ready, it's useless in real world fonts. So, no one uses it in their projects, and there's no 110 | _practical_ reason to continue developing it besides hobby, there's no font that you can't make 111 | today that you can make because "font editor" exists. I know of no popular open source font in which 112 | Runebender played a significant, or any, role in its development. 113 | 114 | So let's look at where we really stand in the world of open source fonts. We stand in a fragmented 115 | landscape, and people keep coming along with _massive_ projects — TruFont, Runebender — 116 | to unify this fragmented landscape, and failing. Meanwhile, as FontForge can do less and less of 117 | what we need in modern fonts (emoji fonts, `rand` feature, OpenType Variations), we get more and 118 | more fragmented, and "font editor" needs to do more and more. 119 | 120 | So, "font editor" is just becoming proprietary, Glyphsapp. _This is intolerable._ Open source fonts 121 | should not need proprietary software to build. Glyphsapp is very powerful which is why it is 122 | dangerous. And can we blame its users? I can't. 123 | 124 | I made a modern font recently, [Noto Sans Tagalog](https://github.com/ctrlcctrlv/Noto-Sans-Tagalog) 125 | v3. (Disclosure: Project financially supported by Google. No one at Google or Google itself 126 | necessarily agrees with my views on font editors, obviously.) And all the flaws of FontForge came 127 | tumbling out. Its custom auto-hinter is basically useless, only supports PostScript when industry 128 | best practice is TTF hints, so that needs to be disabled. 129 | 130 | So what did I do here? I used FontForge just to draw my glyphs. I wrote a designspace XML file. I 131 | wrote a script which called on ttfautohint to do the industry best practice hinting. I used fontmake 132 | to smush it all together. I built my modern font only in free software, a fact of which I am proud, 133 | but also sad at how hard it was. 134 | 135 | But I also see opportunity. We need a solution that uses the benefits of open source software 136 | instead of its weaknesses. So, I propose, we need a modular solution, not a monolith. We don't need 137 | "font editor". What we need are ways to test a UFO font with a nice UI, to write OpenType Layout 138 | code, to draw glyphs. We need font editor**s**, that is to say, modular interoperable programs, 139 | interoperable also with Glyphs and FontForge via the UFO format, each program doing one job, and 140 | some day, hopefully, we will have enough programs that we no longer need FontForge, but only some 141 | parts of it I and others will be splitting off into C libraries. 142 | 143 | We instead of fighting futilely the fragmentation, embrace it. We embrace that font editing is not 144 | one skill: it's many skills and tasks. There's no real reason the program that's doing the 145 | auto-hinting needs to be doing the kerning and generating the OpenType tables. The UFO paradigm is 146 | at the end of the day _correct_, we just need a full throated embrace of it. We need to stop writing 147 | "font editor" and start writing font editors. 148 | 149 | [See the roadmap.](https://github.com/mfeq/mfeq/#planned-modules) 150 | 151 | ---- 152 | 153 | Fred Brennan 154 | 155 | 3 September 2020 156 | 157 | 159 | -------------------------------------------------------------------------------- /doc/article.md: -------------------------------------------------------------------------------- 1 | ![sample](https://user-images.githubusercontent.com/310356/111370352-7f961980-866e-11eb-9efd-ea3c0a8de3da.png) 2 | 3 | https://www.youtube.com/watch?v=JDKT5HZ0qvs 4 | 5 | Check out Fredrick's youtube for a live demonstration! 6 | 7 | # Variable Width Stroking 8 | 9 | I spent a good deal of time over the last month and change trying to put together a variable width stroker. I had a few objectives in mind: 10 | 11 | * Realtime 12 | * Stable (inputs close to each other should give visually similar results) 13 | * Easy to Use 14 | 15 | The first thing I did was look for existing implementations and documentation on how others have accomplished this. I took a look through Inkscape's Powerstroke implementation, 16 | and a found an implementation in R which ended up coming in handy later, but wasn't hugely helpful as I could not make heads or tail of R. There didn't seem to be a good place that 17 | layed out the process in a language I knew or a blog post, so I'm going to walk you through it here. 18 | 19 | ## Overview 20 | 21 | Here's a really stripped down overview of the overall procedure. 22 | 23 | ```python 24 | contour = [bezier1, bezier2, ...] 25 | left_collection = [] 26 | right_collection = [] 27 | 28 | for bezier in contour 29 | # we'll discuss offset below 30 | left_bezier = offset(bezier, |t| -> normal_offset, |t| -> tangent_offset) 31 | right_bezier = offset(bezier, |t| -> normal_offset, |t| -> tangent_offset)) 32 | 33 | left_collection.push(left_bezier) 34 | right_collection.push(right_bezier) 35 | 36 | 37 | flipped_right = map(flip_bezier, right_collection).reverse() 38 | merged_collection = glyph_builder(left_collection) 39 | 40 | # we'll be talking about cap_to below 41 | merged_collection.cap_to(flipped_right.first().first_point()) 42 | merged_collection.append(flipped_right) 43 | merged_collection.cap_to(merged_collection.first().first_point()) 44 | 45 | # discussed in Mind the gap! 46 | merged_collection.fix_path() 47 | glyph = merged_collection.build() 48 | 49 | ``` 50 | 51 | ## Offsetting the curves 52 | 53 | One of the main problems you have to deal with for a good VWS implementation is offsetting your curves. For my application I wanted to be able to specify an offset at the start and end of the bezier curve. 54 | 55 | In my search I found a library called flo_curves: https://github.com/Logicalshift/flo_curves and it had an offset implementation in the language I was already using! 56 | It also provided high quality primitives for things like Points, Lines and Bezier curves. I'm going to describe the broad strokes of how the offset function we use from flo_curves works here. 57 | 58 | ![base_curve](https://user-images.githubusercontent.com/310356/111370342-7efd8300-866e-11eb-8365-c930d8bd042f.PNG) 59 | 60 | ### Splitting at features 61 | 62 | The first step of this algorithm is to split the curves at loops and cusps. This paper cited in Flo_curves source code explains how to find these features far better than I ever could: https://graphics.pixar.com/people/derose/publications/CubicClassification/paper.pdf 63 | 64 | ![split_curve](https://user-images.githubusercontent.com/310356/111370339-7e64ec80-866e-11eb-827a-239e0a57ca1d.PNG) 65 | 66 | ### Sampling the curve 67 | 68 | The next part of the algorithm is pretty easy. For each of the curves from the split you're going to to sample it an arbitrary amount of times, and then offset the samples. 69 | 70 | You're going to want to evaluate the bezier at the given time t, and calculate the normalized tangent and perpendicular directions at that point. 71 | 72 | ![sampled_curve](https://user-images.githubusercontent.com/310356/111370341-7e64ec80-866e-11eb-9fb3-aa9ba51cd0e3.PNG) 73 | 74 | Here our normal is in light blue, and tangent in light green. 75 | 76 | *BEWARE*, a very common pitfall with getting tangents and normals with bezier curves is that curves with colocated handles have strange curvatures as t approaches 0 and 1. This can be solved by simply offsetting your evaluations by f64::epsilon and -f64::epsilon when t == 0 or 1 respectively. 77 | 78 | For each sample point you're going to offset along the normal by how thick you want that side of the stroke to be, and you're going to offset along the tangent to change the 'angle of the stroke'. I'll explain how you calculate the tangent offset later in this document. 79 | 80 | ![sampled_offset_curve](https://user-images.githubusercontent.com/310356/111461613-cda13080-86f3-11eb-90cb-70bad2bf6f7e.PNG) 81 | 82 | ### Fitting a curve to the samples 83 | 84 | After you've sampled the curve and offset those samples you're going to fit a curve to those sampled points. The algorithm used in flo_curves is Philip J Schnieder's from Graphic Gems 1990. 85 | 86 | This stack overflow thread has implementations in both C and C#: https://stackoverflow.com/questions/5525665/smoothing-a-hand-drawn-curve 87 | 88 | ![fit_curve](https://user-images.githubusercontent.com/310356/111370347-7efd8300-866e-11eb-97bb-f49b6b234add.PNG) 89 | 90 | ## Mind the gaps! 91 | 92 | ![discontinuity](https://user-images.githubusercontent.com/310356/111370345-7efd8300-866e-11eb-9617-342bc4c3b0b3.PNG) 93 | 94 | The end of one bezier and the start of the next isn't always going to line up, especially at corners close to 90 degrees. We're gonna end up with a gap. You can identify these by looping through each bezier and checking if it's end point matches the next's start point. IF it's within some small amount (I used 0.001) then you merge the two. If not you generate a join. There's a few types of caps and I'm gonna go over how I solved each. 95 | 96 | ### Bevel 97 | 98 | ![bevel](https://user-images.githubusercontent.com/310356/111370343-7efd8300-866e-11eb-9449-f30656a65811.PNG) 99 | 100 | Create a line from the end of the last bazier to the beginning of the next one. This is the trivial case. 101 | 102 | ### Miter 103 | 104 | ![miter-join](https://user-images.githubusercontent.com/310356/111370350-7f961980-866e-11eb-8c8c-de9f4fd74055.PNG) 105 | 106 | For a miter join you're gonna want to shoot rays from the start/end of the lines in the direction of their tangent. You will have to flip one so they're both heading the same direction. The point at which these two rays intersect is the point of your miter. You then just line to that point and then the start of the next bezier. You'll probably want to put a reasonable limit on miter and if the intersection is X units away default to a bevel. You could also leave this limit to the user. 107 | 108 | ### Round 109 | 110 | ![round_join](https://user-images.githubusercontent.com/310356/111375167-54162d80-8674-11eb-929c-b16904649d24.PNG) 111 | 112 | This one is a bit more complicated. You can find the perfect ellipse segment that runs through both points like Inkscape's implementation, but while searching for a solution for this one I found a method that's almost as simple as a miter join and gives perfectly serviceable results. 113 | 114 | You find the intersection point just like a miter join, and then you employ this piece of psuedo-code magic: 115 | 116 | ```python 117 | 118 | #if there's no intersection radius is equal to 2/3 the disntance from from to to. 119 | radius = distance(from, to) * (2/3) 120 | 121 | #if there is an intersection then radius is equal to the minimum distance from either of our points to the intersection 122 | min(distance(from, intersection), distance(to, intersection)) 123 | 124 | angle = acos(dot(from, to)) 125 | dist_along_tangent = radius*(4/(3*(1/cos(angle/2) + 1))) 126 | arc = Bezier.from_points( 127 | from, 128 | from + from_tangent * dist_along_tangents, 129 | to + to_tangent * dist_along_tangents, 130 | to 131 | ) 132 | ``` 133 | 134 | I didn't come up with this, but I genuinely can not find the source of it. The ratio in dist_along_tangents gives really decent ellipse segments for most inputs. The only issue this method has is at extreme angles where the intersection is far into the distance. For these cases I check how far the intersection is from the midpoint of from and to. If it's distance is greater than the distance to from from to (I am so sorry) we discard it. 135 | 136 | ### Inside joins 137 | 138 | When the bezier curves overlap instead of failing to meet you're going to want to fall back to a bezel. We rely on a Skia Simplify call to remove internal geometry, but you could do so by clipping the beziers where they intersect. I think this would be a fair bit slower in some cases though as you would need to backtrack along the path looking for intersections. 139 | 140 | ## Put a cap on it 141 | 142 | ![cap](https://user-images.githubusercontent.com/310356/111393770-ca745900-868f-11eb-9276-a33c15c60e2a.PNG) 143 | 144 | We've offset all of our curves and collected them into left and right collections. The next thing we need to do is stitch them up into one closed contour. If we're stroking a closed contour we can skip this step and just output the left and right sides as seperate contours since they are already closed. 145 | 146 | If we've got an open contour closing it is actually really easy. You can literally use the exact procedure used for Round joins for Round caps. Butt and Square are trivial. 147 | 148 | ## Getting angular with the editor 149 | 150 | Finding the normal offset for a given mouse postion from a VWS control handle is easy. It's the dot product of the normal and the mouse position both in coordinates relative to the vws control point. 151 | 152 | Where things get a bit trickier is the tangent offset. You could do the same as you're doing for the normal, but you're going to end up with weird lopsided handles on your VWS ribs! 153 | 154 | ![unaligned](https://user-images.githubusercontent.com/310356/111393779-cea07680-868f-11eb-85be-39ca08445fc2.PNG) 155 | 156 | You can fix this issue by scaling the shorter side's tangent offset like this: 157 | 158 | ![aligned](https://user-images.githubusercontent.com/310356/111393781-cfd1a380-868f-11eb-99d3-f2d03b34ea31.PNG) 159 | ``` 160 | scaled_tangent_offset = short_handle/max(left_offset, right_offset) 161 | final_tangent = tangent * vws_handle.tangent_offset * scaled_tangent_offset 162 | ``` 163 | ## Offset closures 164 | 165 | I very much glossed over the contents of the closures in the outline above. Those closures take time t and return a normal or tangent offset respectively. 166 | 167 | The thing is if you just give ever yvws handle an offset and lerp between those you're gonna end up with pretty heavy discontinuities where they meet. 168 | 169 | I highly suggest you use either cosine or cubic interpolation within those functions. Cosine is easy to implement and pretty much eliminates the problem. 170 | 171 | ## Wrapping up 172 | 173 | There's more I could cover here, but I think I gave a pretty good overview on how this is accomplished. Big thanks to Logicalshift for his awesome library. Thanks to Fredrick for giving me the time to work on such a cool problem. I was bussing tables only a few months ago and he gave me a great opportunity. Thanks to coolfontguy69 for suggesting the tangent offsets. -------------------------------------------------------------------------------- /roadmap/.timestamp: -------------------------------------------------------------------------------- 1 | Sun, 25 Dec 2022 03:15:28 +0000 2 | -------------------------------------------------------------------------------- /roadmap/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: 3 | make timestamp roadmap.png 4 | 5 | ODATE := $(shell eval date -Ru) 6 | POINTSIZE := 48 7 | 8 | .PHONY: timestamp 9 | timestamp: 10 | echo "$(ODATE)" > .timestamp 11 | 12 | roadmap.png: graph.dot timestamp 13 | dot -Gdpi=144 -Tpng < graph.dot > graph.png 14 | convert graph.png -font "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" -pointsize $(POINTSIZE) -gravity SouthWest -stroke '#3c3c3c' -strokewidth 5 -annotate +5+5 "$(ODATE)" -stroke none -fill "#cfc" -annotate +5+5 "$(ODATE)" graph_date.png 15 | mv graph_date.png roadmap.png 16 | rm graph.png 17 | 18 | roadmap.svg: graph.dot 19 | dot -Tsvg < graph.dot > roadmap.svg 20 | -------------------------------------------------------------------------------- /roadmap/graph.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | graph [fontname = "Nimbus-Roman", fontsize=11]; 3 | node [fontname = "Nimbus-Roman", fontsize=11]; 4 | edge [fontname = "Nimbus-Roman", style=solid, fontsize=11]; 5 | MFEKinit[style=dashed]; 6 | ipc[style=dashed]; 7 | feaparser[style=dashed]; 8 | MFEKmetadata[style=dashed]; 9 | MFEKufo[style=dotted]; 10 | MFEKdesignspace[style=dotted]; 11 | MFEKpathops[style=dashed]; 12 | MFEKopentype[style=dotted]; 13 | MFEKmetrics[style=dotted]; 14 | MFEKexport[style=dotted]; 15 | MFEKtthint[penwidth=0]; 16 | MFEKpshint[penwidth=0]; 17 | 18 | spiroinner[label=<spiro-inner
(c2rust)>]; //rlib 19 | skef[label=<libskef.so
(C dylib)>, style=dashed]; 20 | skiacpp[label=<Skia
(C++ dylib)>]; 21 | imgui[label=<Dear Imgui
(C dylib)>]; 22 | freetype2[label=<libfreetype.so
(C dylib)>]; 23 | SDL2[label=<SDL2
(C dylib)>]; 24 | HarfBuzz[label=<libharfbuzz.so
(C dylib)>]; 25 | rafxvulkan[label=vulkan>]; //rlib 26 | vulkan[label=<libvulkan.so
(C dylib…Vulkan SDK?)>]; 27 | MFEKtthint[label=<MFEKtthint
(FOSS VTT?)>]; 28 | MFEKpshint[label=<MFEKpshint
(unnecessary?)>]; 29 | "math" -> "skia-safe"; //rlib 30 | "skia-safe" -> skiacpp; //rlib 31 | "glifparser" -> "skia-safe"; 32 | "math" -> "glifparser"; //rlib 33 | "math" -> "kurbo"; //rlib 34 | "math" -> "flo_curves"; //rlib 35 | "math" -> skef [style=dashed]; 36 | "MFEKpathops" -> "math" [color="#cc0000"]; //rlib 37 | "MFEKstroke" -> "math" [color="#cc0000"]; //rlib 38 | "MFEKglif" -> "math" [color="#cc0000"]; //rlib 39 | "MFEKglif" -> "MFEKmetadata" [penwidth=3.5]; 40 | "MFEKpathops" -> "glifparser" [color="#cc0000"]; //rlib 41 | "MFEKstroke" -> "glifparser" [color="#cc0000"]; //rlib 42 | "MFEKglif" -> "glifparser" [color="#cc0000"]; //rlib 43 | "MFEKinit" -> "glifparser" [color="#cc0000"]; //rlib 44 | "MFEKufo" -> "MFEKinit" [penwidth=3.5]; 45 | "MFEKufo" -> "MFEKglif" [penwidth=3.5]; 46 | "MFEKglif" -> "MFEKufo" [penwidth=3.5]; 47 | "MFEKglif" -> "ipc" [color="#cc0000"]; //rlib 48 | "MFEKufo" -> "ipc" [color="#cc0000"]; //rlib 49 | "MFEKufo" -> "qd-unic" [color="#cc0000"]; //rlib 50 | "MFEKmetadata" -> "qd-unic" [color="#cc0000"]; //rlib 51 | "MFEKmetadata" -> "glifparser" [color="#cc0000"]; //rlib 52 | "MFEKpathops" -> "flo_curves" [color="#cc0000"]; //rlib 53 | "glifrenderer" -> "glifparser"; //rlib 54 | "glifrenderer" -> "math"; //rlib 55 | "glifrenderer" -> "skia-safe"; //rlib 56 | "MFEKufo" -> "norad" [color="#cc0000"]; //rlib 57 | "MFEKmetadata" -> "norad" [color="#cc0000"]; //rlib 58 | "MFEKinit" -> "norad" [color="#cc0000"]; //rlib 59 | "MFEKstroke" -> "skia-safe" [color="#cc0000"]; //rlib 60 | "MFEKglif" -> "skia-safe" [color="#cc0000"]; //rlib 61 | "MFEKpathops" -> "skia-safe" [color="#cc0000"]; //rlib 62 | "MFEKpathops" -> "kurbo" [color="#cc0000"]; //rlib 63 | "imgui-skia\n-renderer" -> "skia-safe"; //rlib 64 | "MFEKglif" -> "MFEKinit" [penwidth=3.5]; 65 | "MFEKufo" -> "MFEKstroke" [penwidth=3.5]; 66 | "MFEKufo" -> "MFEKmetadata" [penwidth=3.5]; 67 | "MFEKufo" -> "MFEKpathops" [penwidth=3.5]; 68 | "MFEKglif" -> "glifrenderer" [color="#cc0000"]; //rlib 69 | "MFEKufo" -> "glifrenderer" [color="#cc0000"]; //rlib 70 | "MFEKglif" -> "skulpin" [color="#cc0000"]; //rlib 71 | "MFEKufo" -> "skulpin" [color="#cc0000"]; //rlib 72 | "imgui-skia\n-renderer" -> "imgui-rs"; //rlib 73 | "skulpin" -> "imgui-skia\n-renderer"; //rlib 74 | "skulpin" -> "skia-safe"; //rlib 75 | "skulpin" -> rafxvulkan -> vulkan; //rlib 76 | "sdl2" -> SDL2; //rlib 77 | "imgui-rs" -> imgui; //rlib 78 | "imgui-rs" -> "imgui-sdl2"; 79 | "imgui-sdl2" -> "sdl2"; 80 | imgui -> SDL2 -> vulkan; 81 | imgui -> freetype2; 82 | //"MFEKufo" -> "MFEKabout" [penwidth=3.5]; 83 | "MFEKabout";// -> "imgui-skia\n-renderer"; //rlib 84 | "glifparser" -> "spiro"; //rlib 85 | spiro; 86 | spiro -> spiroinner; //rlib 87 | "MFEKmetrics" -> "skulpin" [color="#cc0000"]; //rlib 88 | "MFEKmetrics" -> "harfbuzz" [color="#cc0000"]; //rlib 89 | "harfbuzz" -> "harfbuzz-sys" -> HarfBuzz; 90 | "MFEKmetrics" -> "MFEKmetadata" [penwidth=3.5]; 91 | "MFEKmetrics" -> "MFEKopentype" [penwidth=3.5]; 92 | "MFEKopentype" -> "MFEKmetrics" [penwidth=3.5]; 93 | "MFEKmetrics" -> "glifrenderer" [color="#cc0000"]; //rlib 94 | skiacpp -> HarfBuzz; 95 | HarfBuzz -> freetype2; //rlib 96 | "MFEKmetrics" -> "glifparser" [color="#cc0000"]; //rlib 97 | "MFEKmetrics" -> "feaparser" [color="#cc0000"]; //rlib 98 | "MFEKmetrics" -> "ipc" [color="#cc0000"]; //rlib 99 | "MFEKdesignspace" -> "MFEKufo" [penwidth=3.5]; 100 | "MFEKdesignspace" -> "MFEKabout" [penwidth=3.5]; 101 | "MFEKdesignspace" -> "MFEKinit" [penwidth=3.5]; 102 | "MFEKdesignspace" -> "MFEKmetadata" [penwidth=3.5]; 103 | "MFEKdesignspace" -> "ipc" [color="#cc0000"]; //rlib 104 | "MFEKdesignspace" -> "MFEKmetrics" [penwidth=3.5]; 105 | "MFEKdocs" [style=dashed]; 106 | "MFEKabout" -> "skulpin" [color="#cc0000"]; //rlib 107 | "MFEKopentype" -> "skulpin" [color="#cc0000"]; //rlib 108 | "MFEKopentype" -> "norad" [color="#cc0000"]; //rlib 109 | "MFEKopentype" -> "glifparser" [color="#cc0000"]; //rlib 110 | "MFEKufo" -> "MFEKexport" [penwidth=3.5]; 111 | "MFEKufo" -> "MFEKmetrics" [penwidth=3.5]; 112 | "MFEKmetrics" -> "MFEKglif" [penwidth=3.5]; 113 | "MFEKdesignspace" -> "MFEKexport" [penwidth=3.5]; 114 | "MFEKdesignspace" -> "MFEKopentype" [penwidth=3.5]; 115 | "MFEKexport" -> "glifparser" [color="#cc0000"]; //rlib 116 | "MFEKexport" -> "math" [color="#cc0000"]; //rlib 117 | "MFEKexport" -> "skulpin" [color="#cc0000"]; //rlib 118 | 119 | MFEKtthint -> "skulpin" [color="#cc0000"]; //rlib 120 | MFEKpshint -> "skulpin" [color="#cc0000"]; //rlib 121 | MFEKtthint -> "glifparser" [color="#cc0000"]; //rlib 122 | MFEKpshint -> "glifparser" [color="#cc0000"]; //rlib 123 | MFEKtthint -> "glifrenderer" [color="#cc0000"]; //rlib 124 | MFEKpshint -> "glifrenderer" [color="#cc0000"]; //rlib 125 | "MFEKglif" -> MFEKtthint [penwidth=3.5]; 126 | "MFEKglif" -> MFEKpshint [penwidth=3.5]; 127 | 128 | labelloc="t" 129 | fontsize=32 130 | label=<“but that’ll take years!”: the MFEK roadmap
©2021-3 Fredrick R. Brennan & Modular Font Editor K Authors> 131 | } 132 | -------------------------------------------------------------------------------- /roadmap/roadmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MFEK/docs/07f862966db42a8a0be27c9c335a799fbb2de21c/roadmap/roadmap.png --------------------------------------------------------------------------------