├── static └── .gitkeep ├── CNAME ├── vue.config.js ├── .eslintignore ├── test ├── unit │ ├── setup.js │ ├── .eslintrc │ ├── specs │ │ └── HelloWorld.spec.js │ └── jest.conf.js └── e2e │ ├── specs │ └── test.js │ ├── custom-assertions │ └── elementCount.js │ ├── nightwatch.conf.js │ └── runner.js ├── config ├── prod.env.js ├── test.env.js ├── dev.env.js └── index.js ├── img ├── vfsm-step-one.png ├── vfsm-step-six.png ├── vfsm-step-ten.png ├── vfsm-step-two.png ├── vfsm-step-eight.png ├── vfsm-step-five.png ├── vfsm-step-four.png ├── vfsm-step-nine.png ├── vfsm-step-seven.png ├── vfsm-step-three.png ├── vfsm-developing-1.png ├── vfsm-staircase-one.png ├── vfsm-staircase-two.png ├── vfsm-step-eleven.png ├── vfsm-step-fifteen.png ├── vfsm-step-fourteen.png ├── vfsm-step-sixteen.png ├── vfsm-step-thirteen.png └── vfsm-step-twelve.png ├── src ├── fonts │ ├── Dispatch-Bold.eot │ ├── Dispatch-Bold.woff │ ├── Dispatch-Light.eot │ ├── Dispatch-Bold.woff2 │ ├── Dispatch-Light.woff │ ├── Dispatch-Light.woff2 │ ├── Dispatch-Regular.eot │ ├── Dispatch-Regular.woff │ ├── DispatchMono-Bold.eot │ ├── Dispatch-Regular.woff2 │ ├── DispatchMono-Bold.woff │ ├── DispatchMono-Bold.woff2 │ ├── DispatchMono-Regular.eot │ ├── DispatchMono-Regular.woff │ ├── DispatchMono-Regular.woff2 │ ├── Dispatch-Condensed-Bold.eot │ ├── Dispatch-Condensed-Bold.woff │ ├── Dispatch-Condensed-Light.eot │ ├── Dispatch-Condensed-Bold.woff2 │ ├── Dispatch-Condensed-Light.woff │ ├── Dispatch-Condensed-Light.woff2 │ ├── Dispatch-Condensed-Regular.eot │ ├── Dispatch-Condensed-Regular.woff │ └── Dispatch-Condensed-Regular.woff2 ├── styles │ ├── highlight.scss │ ├── typography.scss │ ├── reset.scss │ └── fonts.scss ├── store │ ├── getters.js │ ├── mutations.js │ ├── scales.js │ ├── hypercube.js │ ├── autopopulate.js │ ├── tables.js │ └── index.js ├── main.js ├── components │ ├── GlyphAlternatesDisplay.vue │ ├── SubstitutionSet.vue │ ├── DimensionControl.vue │ ├── GlyphView.vue │ ├── AxisControl.vue │ ├── FontUpload.vue │ ├── SubstitutionOutput.vue │ ├── SubordinateControl.vue │ ├── SubstitutionControl.vue │ └── Visualizer.vue └── App.vue ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── index.html ├── .babelrc ├── .github └── workflows │ └── gh-pages-deploy.yml ├── .eslintrc.js ├── scripts └── gh-pages-deploy.js ├── package.json ├── ROADMAP.md ├── LICENSE.txt └── README.md /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | vfbounds.occupantfonts.com -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/_vfvisualizer/' 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js 5 | /test/unit/coverage/ 6 | -------------------------------------------------------------------------------- /test/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | Vue.config.productionTip = false 4 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /img/vfsm-step-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-one.png -------------------------------------------------------------------------------- /img/vfsm-step-six.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-six.png -------------------------------------------------------------------------------- /img/vfsm-step-ten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-ten.png -------------------------------------------------------------------------------- /img/vfsm-step-two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-two.png -------------------------------------------------------------------------------- /img/vfsm-step-eight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-eight.png -------------------------------------------------------------------------------- /img/vfsm-step-five.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-five.png -------------------------------------------------------------------------------- /img/vfsm-step-four.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-four.png -------------------------------------------------------------------------------- /img/vfsm-step-nine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-nine.png -------------------------------------------------------------------------------- /img/vfsm-step-seven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-seven.png -------------------------------------------------------------------------------- /img/vfsm-step-three.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-three.png -------------------------------------------------------------------------------- /test/unit/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | }, 5 | "globals": { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /img/vfsm-developing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-developing-1.png -------------------------------------------------------------------------------- /img/vfsm-staircase-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-staircase-one.png -------------------------------------------------------------------------------- /img/vfsm-staircase-two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-staircase-two.png -------------------------------------------------------------------------------- /img/vfsm-step-eleven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-eleven.png -------------------------------------------------------------------------------- /img/vfsm-step-fifteen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-fifteen.png -------------------------------------------------------------------------------- /img/vfsm-step-fourteen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-fourteen.png -------------------------------------------------------------------------------- /img/vfsm-step-sixteen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-sixteen.png -------------------------------------------------------------------------------- /img/vfsm-step-thirteen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-thirteen.png -------------------------------------------------------------------------------- /img/vfsm-step-twelve.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/img/vfsm-step-twelve.png -------------------------------------------------------------------------------- /src/fonts/Dispatch-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Bold.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Bold.woff -------------------------------------------------------------------------------- /src/fonts/Dispatch-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Light.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Bold.woff2 -------------------------------------------------------------------------------- /src/fonts/Dispatch-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Light.woff -------------------------------------------------------------------------------- /src/fonts/Dispatch-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Light.woff2 -------------------------------------------------------------------------------- /src/fonts/Dispatch-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Regular.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Regular.woff -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Bold.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Regular.woff2 -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Bold.woff -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Bold.woff2 -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Regular.eot -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Regular.woff -------------------------------------------------------------------------------- /src/fonts/DispatchMono-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/DispatchMono-Regular.woff2 -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Bold.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Bold.woff -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Light.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Bold.woff2 -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Light.woff -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Light.woff2 -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Regular.eot -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Regular.woff -------------------------------------------------------------------------------- /src/fonts/Dispatch-Condensed-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morisawausa/_vfvisualizer/HEAD/src/fonts/Dispatch-Condensed-Regular.woff2 -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const devEnv = require('./dev.env') 4 | 5 | module.exports = merge(devEnv, { 6 | NODE_ENV: '"testing"' 7 | }) 8 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | /test/unit/coverage/ 8 | /test/e2e/reports/ 9 | selenium-debug.log 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | "postcss-import": {}, 6 | "postcss-url": {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | "autoprefixer": {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Variable Font Visualizer | Occupant Fonts 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/styles/highlight.scss: -------------------------------------------------------------------------------- 1 | .hljs-tag { 2 | color:var(--syntax-tag-color); 3 | } 4 | 5 | .hljs-name { 6 | color:var(--syntax-name-color); 7 | } 8 | 9 | .hljs-attr { 10 | color:var(--syntax-attr-color); 11 | } 12 | 13 | .hljs-string { 14 | color:var(--syntax-value-color); 15 | } 16 | 17 | .hljs-comment { 18 | color:var(--syntax-comment-color); 19 | } 20 | -------------------------------------------------------------------------------- /test/unit/specs/HelloWorld.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import HelloWorld from '@/components/HelloWorld' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('should render correct contents', () => { 6 | const Constructor = Vue.extend(HelloWorld) 7 | const vm = new Constructor().$mount() 8 | expect(vm.$el.querySelector('.hello h1').textContent) 9 | .toEqual('Welcome to Your Vue.js App') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-vue-jsx", "transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/typography.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 12px; 3 | } 4 | 5 | h1, .heading { 6 | font-size: 1.4em; 7 | } 8 | 9 | .tiny { 10 | font-size: .8em; 11 | } 12 | 13 | .ss20 { 14 | font-feature-settings: "ss20"; 15 | } 16 | 17 | .alternate-0 { 18 | background-color: white; 19 | } 20 | 21 | .alternate-1 { 22 | background-color: lightblue; 23 | } 24 | 25 | .alternate-2 { 26 | background-color: lightgreen; 27 | } 28 | 29 | .alternate-3 { 30 | background-color: lightcyan; 31 | } 32 | 33 | .alternate-4 { 34 | background-color: lightpink; 35 | } 36 | -------------------------------------------------------------------------------- /test/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': function (browser) { 6 | // automatically uses dev Server port from /config.index.js 7 | // default: http://localhost:8080 8 | // see nightwatch.conf.js 9 | const devServer = browser.globals.devServerURL 10 | 11 | browser 12 | .url(devServer) 13 | .waitForElementVisible('#app', 5000) 14 | .assert.elementPresent('.hello') 15 | .assert.containsText('h1', 'Welcome to Your Vue.js App') 16 | .assert.elementCount('img', 1) 17 | .end() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/store/getters.js: -------------------------------------------------------------------------------- 1 | export const META = 'meta' 2 | 3 | export const INSTANCES = 'instances' 4 | 5 | export const GLYPHLIST = 'glyphlist' 6 | 7 | export const AXES = 'axes' 8 | 9 | export const ALL_SUBSTITUTIONS = 'substitutions' 10 | 11 | export const CURRENT_SUBSTITUTION = 'current_substitutions' 12 | 13 | export const CURRENT_SUBSTITUTION_INDEX = 'current_substitution_index' 14 | 15 | export const CURRENT_AXIS_SETTINGS = 'current_axis_setting' 16 | 17 | export const VALID_STYLISTIC_SETS = 'valid_sylistic_sets' 18 | 19 | export const STATE_FOR_CELL = 'state_for_cell' 20 | 21 | export const SUBSTITUTION_RECTS = 'substitution_rects' 22 | 23 | export const ASSIGNED_GLYPHS = 'assigned_glyphs'; 24 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | gh-pages-deploy: 8 | name: Deploying to gh-pages 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup Node.js for use with actions 12 | uses: actions/setup-node@v2 13 | with: 14 | version: 12.x 15 | - name: Checkout branch 16 | uses: actions/checkout@v2 17 | 18 | - name: Clean install dependencies 19 | run: npm ci 20 | 21 | - name: Run deploy script 22 | run: | 23 | git config user.name "nicschumann" && git config user.email "nic@nicschumann.co" 24 | npm run gh-pages-deploy 25 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import store from './store' 6 | const he = require('he') 7 | 8 | Vue.config.productionTip = false 9 | 10 | Vue.filter('unicode', function (glyph) { 11 | if (glyph.codePoints.length > 0) { 12 | return he.decode(`&#${glyph.codePoints[0]};`) 13 | } else { 14 | return '' 15 | } 16 | }) 17 | 18 | Vue.filter('percent', function (decimal) { 19 | return (decimal * 100) + '%' 20 | }) 21 | 22 | /* eslint-disable no-new */ 23 | new Vue({ 24 | el: '#app', 25 | store, 26 | components: { App }, 27 | template: '' 28 | }) 29 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | rootDir: path.resolve(__dirname, '../../'), 5 | moduleFileExtensions: [ 6 | 'js', 7 | 'json', 8 | 'vue' 9 | ], 10 | moduleNameMapper: { 11 | '^@/(.*)$': '/src/$1' 12 | }, 13 | transform: { 14 | '^.+\\.js$': '/node_modules/babel-jest', 15 | '.*\\.(vue)$': '/node_modules/vue-jest' 16 | }, 17 | testPathIgnorePatterns: [ 18 | '/test/e2e' 19 | ], 20 | snapshotSerializers: ['/node_modules/jest-serializer-vue'], 21 | setupFiles: ['/test/unit/setup'], 22 | mapCoverage: true, 23 | coverageDirectory: '/test/unit/coverage', 24 | collectCoverageFrom: [ 25 | 'src/**/*.{js,vue}', 26 | '!src/main.js', 27 | '!**/node_modules/**' 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function (selector, count) { 11 | this.message = 'Testing if element <' + selector + '> has count: ' + count 12 | this.expected = count 13 | this.pass = function (val) { 14 | return val === this.expected 15 | } 16 | this.value = function (res) { 17 | return res.value 18 | } 19 | this.command = function (cb) { 20 | var self = this 21 | return this.api.execute(function (selector) { 22 | return document.querySelectorAll(selector).length 23 | }, [selector], function (res) { 24 | cb.call(self, res) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint' 7 | }, 8 | env: { 9 | browser: true, 10 | }, 11 | extends: [ 12 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 13 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 14 | 'plugin:vue/essential', 15 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 16 | 'standard' 17 | ], 18 | // required to lint *.vue files 19 | plugins: [ 20 | 'vue' 21 | ], 22 | // add your custom rules here 23 | rules: { 24 | // allow async-await 25 | 'generator-star-spacing': 'off', 26 | // allow debugger during development 27 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/e2e/nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register') 2 | var config = require('../../config') 3 | 4 | // http://nightwatchjs.org/gettingstarted#settings-file 5 | module.exports = { 6 | src_folders: ['test/e2e/specs'], 7 | output_folder: 'test/e2e/reports', 8 | custom_assertions_path: ['test/e2e/custom-assertions'], 9 | 10 | selenium: { 11 | start_process: true, 12 | server_path: require('selenium-server').path, 13 | host: '127.0.0.1', 14 | port: 4444, 15 | cli_args: { 16 | 'webdriver.chrome.driver': require('chromedriver').path 17 | } 18 | }, 19 | 20 | test_settings: { 21 | default: { 22 | selenium_port: 4444, 23 | selenium_host: 'localhost', 24 | silent: true, 25 | globals: { 26 | devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port) 27 | } 28 | }, 29 | 30 | chrome: { 31 | desiredCapabilities: { 32 | browserName: 'chrome', 33 | javascriptEnabled: true, 34 | acceptSslCerts: true 35 | } 36 | }, 37 | 38 | firefox: { 39 | desiredCapabilities: { 40 | browserName: 'firefox', 41 | javascriptEnabled: true, 42 | acceptSslCerts: true 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/GlyphAlternatesDisplay.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Related to substitutions 3 | */ 4 | export const INITIALIZE = 'initialize' 5 | 6 | export const LOAD_STATE = 'load_state' 7 | 8 | export const CLEAR_STATE = 'clear_state' 9 | 10 | export const UPDATE_AXIS_VALUE = 'update_axis_value' 11 | 12 | export const ADD_NEW_SUBSTITUTION = 'new_substitution' 13 | 14 | export const DELETE_SUBSTITUTION = 'delete_substitution' 15 | 16 | export const ACTIVATE_SUBSTITUTION = 'activate_substitution' 17 | 18 | export const SET_AXIS_DIMENSION_FOR_SUBSTITUTION = 'set_axis_dimension_for_substitution' 19 | 20 | export const SET_AXIS_SUBDIVISIONS_FOR_SUBSTITUTION = 'set_axis_subdivisions_for_substitution' 21 | 22 | export const UPDATE_SEQUENCE_FOR_SUBSTITUTION = 'update_sequence' 23 | 24 | export const SET_STATE_FOR_CELL = 'set_state_for_cell' 25 | 26 | /** 27 | * Related to subordinates 28 | */ 29 | 30 | export const ADD_SUBORDINATE_TO_SUBSTITUTION = 'add_subordinate_to_substitution' 31 | 32 | export const REMOVE_SUBORDINATE_FROM_SUBSTITUTION = 'remove_subordinate_from_substitution' 33 | 34 | export const SWAP_SUBORDINATE_AND_PRIMARY = 'swap_subordinate_and_primary' 35 | 36 | export const ACTIVATE_SUBORDINATE_IN_GRID = 'activate_subordinate_in_grid' 37 | 38 | export const DEACTIVATE_SUBORDINATE_IN_GRID = 'deactivate_subordinate_in_grid' 39 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | // http://meyerweb.com/eric/tools/css/reset/ 2 | // v2.0 | 20110126 3 | // License: none (public domain) 4 | 5 | html, body, div, span, applet, object, iframe, 6 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 7 | a, abbr, acronym, address, big, cite, code, 8 | del, dfn, em, img, ins, kbd, q, s, samp, 9 | small, strike, strong, sub, sup, tt, var, 10 | b, u, i, center, 11 | dl, dt, dd, ol, ul, li, 12 | fieldset, form, label, legend, 13 | table, caption, tbody, tfoot, thead, tr, th, td, 14 | article, aside, canvas, details, embed, 15 | figure, figcaption, footer, header, hgroup, 16 | menu, nav, output, ruby, section, summary, 17 | time, mark, audio, video { 18 | margin: 0; 19 | padding: 0; 20 | border: 0; 21 | font-size: 100%; 22 | font: inherit; 23 | vertical-align: baseline; 24 | box-sizing: border-box; 25 | } 26 | // HTML5 display-role reset for older browsers 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /scripts/gh-pages-deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const branchName = require('current-git-branch'); 4 | const version = require('../package.json').version; 5 | const execa = require('execa'); 6 | 7 | const FOLDER_NAME = 'dist'; 8 | const BRANCH = branchName() || 'master'; 9 | const DOMAIN = 'vfbounds.occupantfonts.com'; 10 | 11 | process.env.GH_PAGES = true; 12 | 13 | (async function() { 14 | try { 15 | await execa('git', ['checkout', '--orphan', 'gh-pages']); 16 | console.log(`Building Application from ${BRANCH} ...`); 17 | await execa('npm', ['run', 'build']); 18 | 19 | // add a way of writing a CNAME file here with the domain. 20 | await fs.writeFile(path.join(FOLDER_NAME, 'CNAME'), DOMAIN, err => {}); 21 | 22 | await execa('git', ['--work-tree', FOLDER_NAME, 'add', '--all']); 23 | await execa('git', ['--work-tree', FOLDER_NAME, 'commit', '-m', `deploy ${version}`]); 24 | console.log('Pushing to gh-pages...'); 25 | await execa('git', ['push', 'origin', 'HEAD:gh-pages', '--force']); 26 | await execa('rm', ['-rf', FOLDER_NAME]); 27 | await execa('git', ['checkout', '-f', BRANCH]); 28 | await execa('git', ['branch', '-D', 'gh-pages']); 29 | console.log('Deployed.'); 30 | } catch (e) { 31 | console.error(e.message); 32 | process.exit(1); 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /src/store/scales.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This file defines scales to be used to organize 4 | * the X and Y dimensions for visualization. 5 | */ 6 | 7 | export function linear (divisions) { 8 | return [...Array(divisions - 1).keys()].map(x => { 9 | return (x + 1) / divisions 10 | }) 11 | } 12 | 13 | export function lucas (divisions) { 14 | return [...Array(divisions - 1).keys()].map(i => { 15 | let max = 1000 16 | let radicand = Math.pow(max, i) 17 | return Math.pow(radicand, 1 / (divisions - 1)) / 1000 18 | }) 19 | } 20 | 21 | export function impallari (divisions) { 22 | return [0.5] 23 | } 24 | 25 | /** 26 | * Axes are always interpreted in user coordinates. 27 | * So bounds should be interpreted as a percentage across user coordinates, 28 | * This mapping is implemented in the following function. 29 | */ 30 | export function percent2user (bounds, axis) { 31 | return bounds.map((bound) => {return (axis.max - axis.min) * bound + axis.min}) 32 | } 33 | 34 | /** 35 | * Once we have user coordinates, it's easy to map them to normal coordinates 36 | * according to the OpenType spec. 37 | */ 38 | export function user2norm (bounds, axis) { 39 | return bounds.map((bound) => { 40 | if (bound <= axis.default) { 41 | if (axis.default == axis.min) { return 0 } 42 | 43 | return (bound - axis.min) / (axis.default - axis.min) - 1 44 | } else { 45 | if (axis.max == axis.default) { return 1 } 46 | 47 | return (bound - axis.default) / (axis.max - axis.default) 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /test/e2e/runner.js: -------------------------------------------------------------------------------- 1 | // 1. start the dev server using production config 2 | process.env.NODE_ENV = 'testing' 3 | 4 | const webpack = require('webpack') 5 | const DevServer = require('webpack-dev-server') 6 | 7 | const webpackConfig = require('../../build/webpack.prod.conf') 8 | const devConfigPromise = require('../../build/webpack.dev.conf') 9 | 10 | let server 11 | 12 | devConfigPromise.then(devConfig => { 13 | const devServerOptions = devConfig.devServer 14 | const compiler = webpack(webpackConfig) 15 | server = new DevServer(compiler, devServerOptions) 16 | const port = devServerOptions.port 17 | const host = devServerOptions.host 18 | return server.listen(port, host) 19 | }) 20 | .then(() => { 21 | // 2. run the nightwatch test suite against it 22 | // to run in additional browsers: 23 | // 1. add an entry in test/e2e/nightwatch.conf.js under "test_settings" 24 | // 2. add it to the --env flag below 25 | // or override the environment flag, for example: `npm run e2e -- --env chrome,firefox` 26 | // For more information on Nightwatch's config file, see 27 | // http://nightwatchjs.org/guide#settings-file 28 | let opts = process.argv.slice(2) 29 | if (opts.indexOf('--config') === -1) { 30 | opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js']) 31 | } 32 | if (opts.indexOf('--env') === -1) { 33 | opts = opts.concat(['--env', 'chrome']) 34 | } 35 | 36 | const spawn = require('cross-spawn') 37 | const runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' }) 38 | 39 | runner.on('exit', function (code) { 40 | server.close() 41 | process.exit(code) 42 | }) 43 | 44 | runner.on('error', function (err) { 45 | server.close() 46 | throw err 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/SubstitutionSet.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | 40 | 81 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Dispatch Mono'; 3 | font-weight: normal; 4 | src: url('fonts/DispatchMono-Regular.woff2?#iefix') format('woff2'), 5 | url('fonts/DispatchMono-Regular.woff') format('woff'), 6 | url('fonts/DispatchMono-Regular.eot?#iefix') format('embedded-opentype'); 7 | } 8 | 9 | @font-face { 10 | font-family: 'Dispatch Mono'; 11 | font-weight: 700; 12 | src: url('fonts/DispatchMono-Bold.woff2?#iefix') format('woff2'), 13 | url('fonts/DispatchMono-Bold.woff') format('woff'), 14 | url('fonts/DispatchMono-Bold.eot?#iefix') format('embedded-opentype'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Dispatch'; 19 | font-weight: 300; 20 | src: url('fonts/Dispatch-Light.woff2?#iefix') format('woff2'), 21 | url('fonts/Dispatch-Light.woff') format('woff'), 22 | url('fonts/Dispatch-Light.eot?#iefix') format('embedded-opentype'); 23 | } 24 | 25 | @font-face { 26 | font-family: 'Dispatch'; 27 | font-weight: 400; 28 | src: url('fonts/Dispatch-Regular.woff2?#iefix') format('woff2'), 29 | url('fonts/Dispatch-Regular.woff') format('woff'), 30 | url('fonts/Dispatch-Regular.eot?#iefix') format('embedded-opentype'); 31 | } 32 | 33 | @font-face { 34 | font-family: 'Dispatch'; 35 | font-weight: 700; 36 | src: url('fonts/Dispatch-Bold.woff2?#iefix') format('woff2'), 37 | url('fonts/Dispatch-Bold.woff') format('woff'), 38 | url('fonts/Dispatch-Bold.eot?#iefix') format('embedded-opentype'); 39 | } 40 | 41 | @font-face { 42 | font-family: 'Dispatch Condensed'; 43 | font-weight: 400; 44 | src: url('fonts/Dispatch-Condensed-Regular.woff2?#iefix') format('woff2'), 45 | url('fonts/Dispatch-Condensed-Regular.woff') format('woff'), 46 | url('fonts/Dispatch-Condensed-Regular.eot?#iefix') format('embedded-opentype'); 47 | } 48 | 49 | @font-face { 50 | font-family: 'Dispatch Condensed'; 51 | font-weight: 700; 52 | src: url('fonts/Dispatch-Condensed-Bold.woff2?#iefix') format('woff2'), 53 | url('fonts/Dispatch-Condensed-Bold.woff') format('woff'), 54 | url('fonts/Dispatch-Condensed-Bold.eot?#iefix') format('embedded-opentype'); 55 | } 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/store/hypercube.js: -------------------------------------------------------------------------------- 1 | const SortedSet = require('collections/sorted-set') 2 | const ndarray = require('ndarray') 3 | const DEBUG = false 4 | 5 | export function makeRect (divisions) { 6 | if (divisions.length === 0) { 7 | return 0 8 | } else { 9 | let firstDim = divisions[0] 10 | return firstDim.map(entries => { 11 | return makeRect(divisions.slice(1)) 12 | }) 13 | } 14 | } 15 | 16 | export class Hypercube { 17 | constructor (divisions, array=null, shape=null) { 18 | this.divisions = divisions.map(division => { return new SortedSet(division) }) 19 | this.state = this.initial(array, shape) 20 | } 21 | 22 | initial (array, shape) { 23 | if (array == null || shape == null) { 24 | let dimensions = this.divisions.map(x => x.length + 1) 25 | let locations = dimensions.reduce((a, b) => a * b, 1) 26 | return ndarray(new Int32Array([...Array(locations).keys()].map(x => 0)), dimensions) 27 | } else { 28 | return ndarray(new Int32Array(array), shape) 29 | } 30 | 31 | } 32 | 33 | get (point) { 34 | let normalized = this.indices(point) 35 | return this.state.get(...normalized) 36 | } 37 | 38 | set (point, state) { 39 | let normalized = this.indices(point, DEBUG) 40 | 41 | if (DEBUG) {console.log(`h input:`, normalized)} 42 | 43 | this.state.set(...normalized, state) 44 | 45 | return this 46 | } 47 | 48 | indices (point, debug=false) { 49 | return point.map((coordinate, i) => { 50 | let index = this.divisions[i].indexOf(coordinate) 51 | 52 | if (debug) {console.log(`h normalization: passed coordinate (${i}): ${coordinate}`)} 53 | if (debug) {console.log(`h normalization: index? (${i}): ${index}`)} 54 | 55 | if (index === -1) { 56 | const guess = this.divisions[i].findLeastGreaterThanOrEqual(coordinate) 57 | 58 | if (typeof guess === 'undefined') { 59 | index = this.divisions[i].length 60 | } else { 61 | if (debug) {console.log(guess)} 62 | if (debug) {console.log(`h normalization: guessed index (${i}): ${guess.value}`)} 63 | index = this.divisions[i].indexOf(guess.value) 64 | if (debug) {console.log(`h normalization: found index (${i}): ${index}`)} 65 | } 66 | return index 67 | } else { 68 | return index + 1 69 | } 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/store/autopopulate.js: -------------------------------------------------------------------------------- 1 | import FuzzySearch from 'fuzzy-search' 2 | 3 | const SUBSTITUTION_PATTERN = '.sub_'; 4 | 5 | function getBaseGlyph(glyphname, glyphs) { 6 | for (let i = 0; i < glyphs.length; i += 1) { 7 | if (glyphs[i].name == glyphname) { 8 | return glyphs[i]; 9 | } 10 | } 11 | 12 | return -1; 13 | } 14 | 15 | export function autopopulate(font) { 16 | let glyphs = font.characterSet.map(pt => { 17 | let g = font.glyphForCodePoint(pt); 18 | return { 19 | name: g.name, 20 | codePoints: g.codePoints 21 | }; 22 | }); 23 | 24 | let searcher = new FuzzySearch(glyphs, ['name'], {caseSensitive: true}) 25 | let candidates = searcher.search(SUBSTITUTION_PATTERN); 26 | // NOTE: Probably sort the candidates for reproducability 27 | let groups = {}; 28 | 29 | candidates.forEach(glyph => { 30 | let parts = glyph.name.split(SUBSTITUTION_PATTERN) 31 | let base_glyph_name = parts[0]; 32 | /** 33 | * Getting the naming convention thing right is pretty complex. 34 | * From any given name, we need to know how to get the base name. 35 | * So we need some kind of consistent syntax for modifiers. 36 | */ 37 | let class_name = parts.slice(1).join(""); 38 | 39 | if (typeof groups[base_glyph_name] === 'undefined') { 40 | let base_glyph = getBaseGlyph(base_glyph_name, glyphs); 41 | 42 | if (base_glyph === -1) { 43 | console.log(`Autopopulator: Couldn't find a baseglyph named "${base_glyph_name}" for substitution named "${glyph.name}"`); 44 | 45 | } else { 46 | // create a new entry 47 | let entry = { 48 | classname: SUBSTITUTION_PATTERN + class_name, 49 | glyphs: [base_glyph, glyph] 50 | } 51 | 52 | groups[base_glyph_name] = entry; 53 | 54 | } 55 | 56 | } else { 57 | 58 | groups[base_glyph_name].glyphs.push(glyph); 59 | 60 | } 61 | }); 62 | 63 | let classes = {} 64 | 65 | Object.values(groups).forEach(group => { 66 | let group_key = group.classname + ':' + group.glyphs.length; 67 | 68 | if (typeof classes[group_key] === 'undefined') { 69 | 70 | classes[group_key] = { 71 | glyphs: group.glyphs, 72 | subordinates: [] 73 | }; 74 | 75 | } else { 76 | 77 | classes[group_key].subordinates.push(group.glyphs); 78 | 79 | } 80 | }) 81 | 82 | return Object.values(classes); 83 | } 84 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | dev: { 9 | 10 | // Paths 11 | assetsSubDirectory: 'static', 12 | assetsPublicPath: '/', 13 | proxyTable: {}, 14 | 15 | // Various Dev Server settings 16 | host: 'localhost', // can be overwritten by process.env.HOST 17 | port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 18 | autoOpenBrowser: false, 19 | errorOverlay: true, 20 | notifyOnErrors: true, 21 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 22 | 23 | // Use Eslint Loader? 24 | // If true, your code will be linted during bundling and 25 | // linting errors and warnings will be shown in the console. 26 | useEslint: true, 27 | // If true, eslint errors and warnings will also be shown in the error overlay 28 | // in the browser. 29 | showEslintErrorsInOverlay: false, 30 | 31 | /** 32 | * Source Maps 33 | */ 34 | 35 | // https://webpack.js.org/configuration/devtool/#development 36 | devtool: 'cheap-module-eval-source-map', 37 | 38 | // If you have problems debugging vue-files in devtools, 39 | // set this to false - it *may* help 40 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 41 | cacheBusting: true, 42 | 43 | cssSourceMap: true 44 | }, 45 | 46 | build: { 47 | // Template for index.html 48 | index: path.resolve(__dirname, '../dist/index.html'), 49 | 50 | // Paths 51 | assetsRoot: path.resolve(__dirname, '../dist'), 52 | assetsSubDirectory: 'static', 53 | assetsPublicPath: '/', 54 | 55 | /** 56 | * Source Maps 57 | */ 58 | 59 | productionSourceMap: true, 60 | // https://webpack.js.org/configuration/devtool/#production 61 | devtool: '#source-map', 62 | 63 | // Gzip off by default as many popular static hosts such as 64 | // Surge or Netlify already gzip all static assets for you. 65 | // Before setting to `true`, make sure to: 66 | // npm install --save-dev compression-webpack-plugin 67 | productionGzip: false, 68 | productionGzipExtensions: ['js', 'css'], 69 | 70 | // Run the build command with an extra argument to 71 | // View the bundle analyzer report after build finishes: 72 | // `npm run build --report` 73 | // Set to `true` or `false` to always turn it on or off 74 | bundleAnalyzerReport: process.env.npm_config_report 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "variable-font-substitution-mapper", 3 | "version": "0.9.7", 4 | "description": "Variable font visualizer for Occupant Fonts", 5 | "contributors": [ 6 | { 7 | "name": "Nic Schumann", 8 | "email": "nic@occupantfonts.com" 9 | }, 10 | { 11 | "name": "Marie Otsuka", 12 | "email": "marie@occupantfonts.com" 13 | } 14 | ], 15 | "license": "Apache-2.0", 16 | "private": true, 17 | "scripts": { 18 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 19 | "start": "npm run dev", 20 | "unit": "jest --config test/unit/jest.conf.js --coverage", 21 | "e2e": "node test/e2e/runner.js", 22 | "test": "npm run unit && npm run e2e", 23 | "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs", 24 | "build": "node build/build.js", 25 | "gh-pages-deploy": "node scripts/gh-pages-deploy.js" 26 | }, 27 | "dependencies": { 28 | "array-equal": "^1.0.0", 29 | "blob-to-buffer": "^1.2.8", 30 | "collections": "^5.1.11", 31 | "fitty": "^2.3.0", 32 | "fontkit": "^1.8.1", 33 | "fuzzy-search": "^3.2.1", 34 | "hasha": "^5.2.0", 35 | "he": "^1.2.0", 36 | "highlight.js": "^10.0.3", 37 | "js-combinatorics": "^0.5.5", 38 | "ndarray": "^1.0.19", 39 | "vue": "^2.5.2", 40 | "vuex": "^3.3.0", 41 | "xmlbuilder": "^15.1.1", 42 | "xmlbuilder2": "^2.1.2" 43 | }, 44 | "devDependencies": { 45 | "autoprefixer": "^7.1.2", 46 | "babel-core": "^6.22.1", 47 | "babel-eslint": "^8.2.1", 48 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 49 | "babel-jest": "^21.0.2", 50 | "babel-loader": "^7.1.1", 51 | "babel-plugin-dynamic-import-node": "^1.2.0", 52 | "babel-plugin-syntax-jsx": "^6.18.0", 53 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", 54 | "babel-plugin-transform-runtime": "^6.22.0", 55 | "babel-plugin-transform-vue-jsx": "^3.5.0", 56 | "babel-preset-env": "^1.3.2", 57 | "babel-preset-stage-2": "^6.22.0", 58 | "babel-register": "^6.22.0", 59 | "chalk": "^2.0.1", 60 | "chromedriver": "^2.27.2", 61 | "copy-webpack-plugin": "^4.0.1", 62 | "cross-spawn": "^5.0.1", 63 | "css-loader": "^0.28.0", 64 | "current-git-branch": "^1.1.0", 65 | "eslint": "^4.15.0", 66 | "eslint-config-standard": "^10.2.1", 67 | "eslint-friendly-formatter": "^3.0.0", 68 | "eslint-loader": "^1.7.1", 69 | "eslint-plugin-import": "^2.7.0", 70 | "eslint-plugin-node": "^5.2.0", 71 | "eslint-plugin-promise": "^3.4.0", 72 | "eslint-plugin-standard": "^3.0.1", 73 | "eslint-plugin-vue": "^4.0.0", 74 | "execa": "^4.0.2", 75 | "extract-text-webpack-plugin": "^3.0.0", 76 | "file-loader": "^1.1.4", 77 | "friendly-errors-webpack-plugin": "^1.6.1", 78 | "html-webpack-plugin": "^2.30.1", 79 | "jest": "^22.0.4", 80 | "jest-serializer-vue": "^0.3.0", 81 | "nightwatch": "^0.9.12", 82 | "node-notifier": "^8.0.1", 83 | "node-sass": "^4.14.0", 84 | "optimize-css-assets-webpack-plugin": "^3.2.0", 85 | "ora": "^1.2.0", 86 | "portfinder": "^1.0.13", 87 | "postcss-import": "^11.0.0", 88 | "postcss-loader": "^2.0.8", 89 | "postcss-url": "^7.2.1", 90 | "rimraf": "^2.6.0", 91 | "sass-loader": "^7.3.1", 92 | "selenium-server": "^3.0.1", 93 | "semver": "^5.3.0", 94 | "shelljs": "^0.7.6", 95 | "transform-loader": "^0.2.4", 96 | "uglifyjs-webpack-plugin": "^1.1.1", 97 | "url-loader": "^0.5.8", 98 | "vue-jest": "^1.0.2", 99 | "vue-loader": "^13.3.0", 100 | "vue-style-loader": "^3.0.1", 101 | "vue-template-compiler": "^2.5.2", 102 | "webpack": "^3.6.0", 103 | "webpack-bundle-analyzer": "^3.3.2", 104 | "webpack-dev-server": "^2.9.1", 105 | "webpack-merge": "^4.1.0" 106 | }, 107 | "engines": { 108 | "node": ">= 6.0.0", 109 | "npm": ">= 3.0.0" 110 | }, 111 | "browserslist": [ 112 | "> 1%", 113 | "last 2 versions", 114 | "not ie <= 8" 115 | ] 116 | } 117 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 53 | 54 | 150 | -------------------------------------------------------------------------------- /src/components/DimensionControl.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 76 | 77 | 165 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Roadmap 2 | 3 | *This document outlines our roadmap for future development.* 4 | 5 | ## Version 1.0.0 6 | 7 | | Done | Version | Category | Feature | Description | 8 | | --- | --- | --- | --- | --- | 9 | | ✅| 0.9.0 | UI | Button States | Make the Button States more legible with hover, etc. 10 | | ✅| 0.9.0 | UI | Generate Table | Make the generate table button larger and clearer as this is a significant process step. 11 | | ✅| 0.9.0 | UI | Copy Table Button | Make sure the copy table button actually copies the table... 12 | | ✅| 0.9.0 | Functionality | Delete Substitutions| Self Explanitory 13 | | ✅| 0.9.0 | Functionality | >2 Substitutions | You should be able to apply multiple substitions to the same base glyph in one design space map. 14 | | ✅| 0.9.0 | Functionality | >1 Substitution Set per Map | You should be able to assign multiple substitution pairs to the same design space substitution map. 15 | | ✅ | 0.9.0 | Functionality | Autopopulate | There should be some basic intelligence built in to the substitution system to generate substitutions based on suffixes. For example, a substitution for `{x}` to `{x}.sub_{y}` should automatically be set up in the grid when you load a font. **Note:** Naming conventions must be determined ahead of time and thought through for this feature to be anything other than annoying. 16 | | ✅ | 0.9.0 | UI | Subs | Add colored background to the glyphs in the substitution pane to help communicate where different subs apply. 17 | | ✅ | 0.9.0 | UI | Output | Add controls to output pane. Finesse styling. 18 | | ✅ | 0.9.0 | UI | Arrows | Make sure the arrows are differentiated. For example, the arrows to send glyphs to the grid could be different from the substitution order. 19 | | ✅ | 0.9.0 | UI | Borders | Make sure the borders are consistent (review AxisControl component). 20 | | ✅ | 0.9.0 | UI | Metadata | Remove Unicode Count from font metadata. Remove extra line from Font Metadata Component. 21 | | ✅ | 1.0.0 | UI | Glyph Selection | Refine the UI for the glyph selections. Make is smaller, and get rid of the dumb 'choose' thing. 22 | | ✅ | 1.0.0 | UI | Axis Location Input | Add a UI Input Field for manual input into the axis positions sliders, to make it easier to jump to a location. 23 | | ✅ | 1.0.0 | UI | Disable Active Axis Sliders | Make sure axes that are assigend to grid dimensions have their axis controls sliders disabled or grayed out. 24 | | ✅ | 1.0.0 | Functionality | Show instances | Show where instances are located in the grid as an overlay 25 | | ✅ | 1.0.0 | Functionality | No duplicates | remove assigned glyphs from the searchable glyphs list. 26 | | ✅ | 1.0.0 | Functionality | Saved States | As you work, your progress should be serialized and saved to any available persistance layer, like `LocalStorage`. When you drop a font you were previously working on into the visualizer, the same state should be loaded. 27 | | | 1.0.0 | Functionality | Error Handling | You should get a nice error message if you drag a non-variable, non-`.ttf` file into the visualizer. 28 | | | 1.0.0 | Documentation | Walkthrough | Descriptions of how to use each feature, embedded into the application as tuturial text or help screens. 29 | | | 1.0.0 | Documentation | Landing Screen | Redesign landing screen so it makes a bit more sense with the rest of the application. 30 | | | 1.0.0 | Refactor | Stylistic Sets | Remove dangling references to stylistic sets implementation (this was an implementation of subs that placed the substitutes into stylistic sets, ugh.) 31 | | | ~1.0.0~ | ~Functionality~ | ~Existing `GSUB`~ | ~Make it easier to add a generated `GSUB` table to a pre-existing GSUB table.~ 32 | 33 | ## Version 1.1.0 34 | 35 | | Done | Version | Category | Feature | Description | 36 | | --- | --- | --- | --- | --- | 37 | | | 1.1.0 | Functionality | Axis Labels | Axes should have a labelling system which shows which axis is assigned to which planar dimension, and also allows for draggable subdivisions. 38 | | | 1.1.0 | UI | Keyboard Shortcuts | Add keyboard shortcuts for new substitution, and toggle subordinates. 39 | | | 1.1.0 | Optimization | Generated Cells | Optimize generated cells so as to minimize the total number of cells. 40 | | | 1.1.0 | Functionality | Zoom the grid | Make it easy to zoom and pan on the visualizer grid for more detail (maybe). 41 | | | 1.1.0 | UI | Testing | Review app at smaller screen-heights (it breaks right now). 42 | | | 1.1.0 | UI | Color | Use the OCC color palette where relevant. This may require a version of the color pallette that works for tools, rather than brand collateral. 43 | | | 1.1.0 | UI | Dark Mode | Make an `occupantfonts.com`-esque dark mode toggle. 44 | | | 1.1.0 | UI | Drag | Click and drag to toggle state on multiple grid cells at once, like the prototype. 45 | | | 1.1.0 | Functionality | Undo button | Self Explanitory 46 | 47 | ## Version 1.2.0 48 | 49 | | Done | Version | Category | Feature | Description | 50 | | --- | --- | --- | --- | --- | 51 | | | 1.2.0 | Functionality | Re-subdivision | Allow the number of grid divisions to be changed without destroying the state of the visualizer. 52 | | | 1.2.0 | Functionality | Custom Naming Conventions | Build an interface to allow folks to set their own naming conventions for autopopulation of substitutions and subordinates. 53 | -------------------------------------------------------------------------------- /src/components/GlyphView.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 129 | 130 | 179 | -------------------------------------------------------------------------------- /src/store/tables.js: -------------------------------------------------------------------------------- 1 | const { create } = require('xmlbuilder') 2 | const PRECISION = 4 3 | 4 | /** 5 | * This function un-normalizes a variable font 6 | * normalized coordinate back into the user 7 | * coordinate system. 8 | */ 9 | const unnormalize = (value, axis) => { 10 | if (value <= 0) { 11 | return (value + 1) * (axis.default - axis.min) + axis.min 12 | } else { 13 | return value * (axis.max - axis.default) + axis.default 14 | } 15 | } 16 | 17 | /** 18 | * Generate a unique name for this rule 19 | */ 20 | const ruleName = (subst) => { 21 | let sequences = subst.glyphs.map((g, i) => { 22 | return [g].concat(subst.subordinates.map(s => s[i])) 23 | }) 24 | 25 | return sequences.map(seq => seq.map(g => g.name).join('+')).join('=>') 26 | } 27 | 28 | /** 29 | * this function generates a designspace element that's compatible 30 | * with a designspace file. 31 | * 32 | */ 33 | export function designspaceTable (axes, cells) { 34 | let root = create('rules', {headless: true}) 35 | 36 | cells.forEach(group => { 37 | let substitution = group.substitution 38 | let cells = group.cells 39 | 40 | let rule = root.ele('rule', {name: ruleName(substitution)}) 41 | 42 | cells.forEach(cell => { 43 | let conditionset = rule.ele('conditionset') 44 | 45 | cell.coordinates.user.forEach((pair, i) => { 46 | conditionset = conditionset.ele('condition', { 47 | name: axes[i].name, 48 | minimum: pair[0].toFixed(PRECISION), // TODO: Multiply Through to get User Coordinates. 49 | maximum: pair[1].toFixed(PRECISION) // TODO: Multiply Through to get User Coordinates. 50 | }).up() 51 | }) 52 | 53 | rule = conditionset.up() 54 | }) 55 | 56 | rule = rule.ele('sub', {'name': substitution.glyphs[0].name, 'with': substitution.glyphs[1].name}).up() 57 | 58 | substitution.subordinates.forEach(run => { 59 | rule = rule.ele('sub', {'name': run[0].name, 'with': run[1].name}).up() 60 | }) 61 | 62 | root = rule.up() 63 | }) 64 | 65 | return [root.end({pretty: true})] 66 | } 67 | 68 | /** 69 | * This function generates a element that's compatible with a .ttx file 70 | * 71 | */ 72 | export function ttxTable (axes, cells, options) { 73 | let script_offset = options.scripts || 0 74 | let feature_offset = options.features || 0 75 | let lookup_offset = options.lookups || 0 76 | let variations_offset = options.variations || 0 77 | 78 | 79 | let GSUB = create('GSUB', {headless: true}) 80 | GSUB.ele('Version', {value: "0x00010001"}) 81 | 82 | // STATIC: Generate Scripts List 83 | GSUB 84 | .ele('ScriptList') 85 | .ele('ScriptRecord', {index: script_offset}) 86 | .ele('ScriptTag', {value: "DFLT"}).up() 87 | .ele('Script') 88 | .ele('DefaultLangSys') 89 | .ele('ReqFeatureIndex', {value: '65535'}).up() 90 | .ele('FeatureIndex', {index: 0, value: feature_offset}).up() 91 | .up() 92 | .up() 93 | .up() 94 | .up() 95 | 96 | // STATIC: Generate Feature List 97 | GSUB 98 | .ele('FeatureList') 99 | .ele('FeatureRecord', {index: feature_offset}) 100 | .ele('FeatureTag', {value: "rvrn"}).up() 101 | .ele('Feature') 102 | .com(`LookupCount=0`) 103 | .up() 104 | .up() 105 | .up() 106 | 107 | 108 | // THIS IS FINE: Generate lookup list 109 | let lookuplist = GSUB.ele('LookupList') 110 | 111 | cells.forEach((group, i) => { 112 | let substitution = group.substitution 113 | console.log(substitution.subordinates); 114 | let lookup = lookuplist.ele('Lookup', {'index': lookup_offset + i}) 115 | 116 | lookup.ele('LookupType', {value: 1}).up() 117 | lookup.ele('LookupFlag', {value: 0}).up() 118 | 119 | let subst = lookup.ele('SingleSubst', {index: 0, Format: 2}) 120 | 121 | subst.ele('Substitution', {in: substitution.glyphs[0].name, out: substitution.glyphs[1].name}).up() 122 | 123 | substitution.subordinates.map(group => { 124 | subst.ele('Substitution', {in: group[0].name, out: group[1].name}).up() 125 | }) 126 | 127 | }); 128 | 129 | let rects = {} 130 | 131 | let getKey = (cell) => { 132 | return cell.coordinates.normal.reduce((k, pair) => `${k}:${pair.map(coordinate => coordinate.toFixed(PRECISION)).join(',')}`, '') 133 | } 134 | 135 | // Unify all of the rectangles across substitutions. 136 | // Otherwise only the first substitution applies, and the rest are dropped. 137 | // NOTE: Actually... for a given coordinate, only the first rectangle that applies 138 | // is used. This means, there cannot be overlapping feature lookups, that 139 | // look up to different substitutions. In this case, only the first substitution set will be used. 140 | 141 | cells.forEach((group, lookup_index) => { 142 | let substitution = group.substitution; 143 | let cells = group.cells; 144 | 145 | cells.forEach((cell) => { 146 | let key = getKey(cell) 147 | 148 | if (typeof rects[key] === 'undefined') { 149 | // we haven't seen this cell yet! Make a new index. 150 | rects[key] = { 151 | coordinates: cell.coordinates.normal, 152 | lookups: [lookup_index] 153 | } 154 | 155 | } else { 156 | // we've already seen it. Add this lookup to the cell. 157 | rects[key].lookups.push(lookup_index) 158 | } 159 | }) 160 | }); 161 | 162 | // Write the feature variations table 163 | let featureVariations = GSUB.ele('FeatureVariations') 164 | let global_index = 0 165 | 166 | featureVariations.ele('Version', {value: '0x00010000'}) 167 | 168 | Object.values(rects).forEach((rect, rect_index) => { 169 | let record = featureVariations.ele('FeatureVariationRecord', {index: variations_offset + rect_index}) 170 | let conditionset = record.ele('ConditionSet') 171 | 172 | rect.coordinates.forEach((pair, index) => { 173 | let table = conditionset.ele('ConditionTable', {index: index, Format: 1}) 174 | 175 | table.ele('AxisIndex', {value: index}).up() 176 | table.ele('FilterRangeMinValue', {value: pair[0].toFixed(PRECISION)}).up() 177 | table.ele('FilterRangeMaxValue', {value: pair[1].toFixed(PRECISION)}).up() 178 | }) 179 | 180 | conditionset.up() 181 | 182 | let featureSubstitution = record.ele('FeatureTableSubstitution') 183 | featureSubstitution.ele('Version', {value: '0x00010000'}).up() 184 | 185 | let features = featureSubstitution 186 | .ele('SubstitutionRecord', {index: 0}) 187 | .ele('FeatureIndex', {value: feature_offset}).up() 188 | .ele('Feature'); 189 | 190 | rect.lookups.forEach((index, j) => { 191 | features.ele('LookupListIndex', {index: j, value: lookup_offset + index}).up() 192 | }); 193 | }) 194 | 195 | return [GSUB.end({pretty:true})] 196 | 197 | } 198 | 199 | 200 | /** 201 | * This function generates glyphs 3 features that can be copy-pasted 202 | * directly into glyphs 3. 203 | * 204 | * As parameters, it takes two arrays: an array of variable axes that the font contains, 205 | * and an array of cells that should be rendered. Consult the Developing section of the 206 | * readme for more information on the shape of these parameters. 207 | * 208 | * Note that these parameters are READ ONLY. you should not mutate them in any way 209 | * in this function. 210 | * 211 | * Using this information, this function should construct an array of strings that can be 212 | * copy-pasted into a glyphs3 rlig feature. Each string in this array will be interpreted 213 | * as a line of output. These strings will be joined by newlines and displayed 214 | * in an element for the user to select. 215 | */ 216 | export function glyphs3Features(axes, cells) { 217 | console.log(axes) 218 | console.log(cells) 219 | 220 | return ['not implemented yet: line 1', 'not implemented yet: line 2'] 221 | } 222 | -------------------------------------------------------------------------------- /src/components/AxisControl.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 122 | 123 | 124 | 303 | -------------------------------------------------------------------------------- /src/components/FontUpload.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 145 | 146 | 147 | 298 | -------------------------------------------------------------------------------- /src/components/SubstitutionOutput.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 200 | 201 | 334 | -------------------------------------------------------------------------------- /src/components/SubordinateControl.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 232 | 233 | 361 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Morisawa USA Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/components/SubstitutionControl.vue: -------------------------------------------------------------------------------- 1 | 126 | 127 | 262 | 263 | 264 | 540 | -------------------------------------------------------------------------------- /src/components/Visualizer.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 393 | 394 | 607 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | const Combinatorics = require('js-combinatorics') 4 | const hasha = require('hasha'); 5 | const ndarray = require('ndarray'); 6 | 7 | const DEFAULT_MAJOR_SUBDIVISIONS = 7 8 | const DEFAULT_MINOR_SUBDIVISIONS = 2 9 | 10 | import { 11 | META, 12 | GLYPHLIST, 13 | INSTANCES, 14 | AXES, 15 | ALL_SUBSTITUTIONS, 16 | CURRENT_SUBSTITUTION, 17 | CURRENT_SUBSTITUTION_INDEX, 18 | CURRENT_AXIS_SETTINGS, 19 | VALID_STYLISTIC_SETS, 20 | STATE_FOR_CELL, 21 | SUBSTITUTION_RECTS, 22 | ASSIGNED_GLYPHS 23 | } from './getters.js' 24 | 25 | import { 26 | INITIALIZE, 27 | LOAD_STATE, 28 | CLEAR_STATE, 29 | UPDATE_AXIS_VALUE, 30 | ADD_NEW_SUBSTITUTION, 31 | DELETE_SUBSTITUTION, 32 | ACTIVATE_SUBSTITUTION, 33 | SET_AXIS_DIMENSION_FOR_SUBSTITUTION, 34 | SET_AXIS_SUBDIVISIONS_FOR_SUBSTITUTION, 35 | UPDATE_SEQUENCE_FOR_SUBSTITUTION, 36 | SET_STATE_FOR_CELL, 37 | ADD_SUBORDINATE_TO_SUBSTITUTION, 38 | REMOVE_SUBORDINATE_FROM_SUBSTITUTION, 39 | SWAP_SUBORDINATE_AND_PRIMARY, 40 | ACTIVATE_SUBORDINATE_IN_GRID, 41 | DEACTIVATE_SUBORDINATE_IN_GRID 42 | } from './mutations.js' 43 | 44 | 45 | const saveableActions = [ 46 | ADD_NEW_SUBSTITUTION, 47 | DELETE_SUBSTITUTION, 48 | 49 | ADD_SUBORDINATE_TO_SUBSTITUTION, 50 | REMOVE_SUBORDINATE_FROM_SUBSTITUTION, 51 | 52 | SET_AXIS_SUBDIVISIONS_FOR_SUBSTITUTION, 53 | SET_STATE_FOR_CELL 54 | ]; 55 | 56 | import {linear, percent2user, user2norm} from './scales.js' 57 | import {autopopulate} from './autopopulate.js'; 58 | 59 | import {Hypercube} from './hypercube.js' 60 | 61 | Vue.use(Vuex) 62 | 63 | let store = new Vuex.Store({ 64 | state: { 65 | savename: '', 66 | ui: { 67 | substitution: -1, 68 | axisValues: [] 69 | }, 70 | instances: [], 71 | meta: { 72 | fullName: '', 73 | familyName: '', 74 | subfamilyName: '', 75 | copyright: '', 76 | stylisticSets: [], 77 | version: 0, 78 | numGlyphs: 0, 79 | numAxes: 0, 80 | numInstances: 0 81 | }, 82 | axes: [], 83 | glyphs: [], 84 | substitutions: [] 85 | }, 86 | getters: { 87 | // Constant Getters 88 | [META] (state, getters) { 89 | return state.meta 90 | }, 91 | [INSTANCES] (state, getters) { 92 | return state.instances 93 | }, 94 | [GLYPHLIST] (state, getters) { 95 | return state.glyphs 96 | }, 97 | [ASSIGNED_GLYPHS] (state, getters) { 98 | let assigned_glyphs = state.substitutions.map(sub => { 99 | let master_set = sub.glyphs; 100 | let subordinates = sub.subordinates.reduce((a,b) => a.concat(b), []); 101 | return master_set.concat(subordinates); 102 | }); 103 | 104 | return assigned_glyphs.reduce((a,b) => a.concat(b), []); 105 | }, 106 | [AXES] (state, getters) { 107 | return state.axes 108 | }, 109 | [ALL_SUBSTITUTIONS] (state, getters) { 110 | return state.substitutions 111 | }, 112 | [CURRENT_SUBSTITUTION] (state, getters) { 113 | return state.substitutions[state.ui.substitution] 114 | }, 115 | [CURRENT_SUBSTITUTION_INDEX] (state, getters) { 116 | return state.ui.substitution 117 | }, 118 | [CURRENT_AXIS_SETTINGS] (state, getters) { 119 | return state.ui.axisValues 120 | }, 121 | [VALID_STYLISTIC_SETS] (state, getters) { 122 | return state.meta.stylisticSets 123 | }, 124 | // Parametrized Getters 125 | [STATE_FOR_CELL] (state, getters) { 126 | return (substitutionIndex, point) => { 127 | let substitution = state.substitutions[substitutionIndex] 128 | if (typeof substitution === 'undefined') { return 0 } 129 | 130 | return substitution.state.get(point) 131 | } 132 | }, 133 | 134 | [SUBSTITUTION_RECTS] (state, getters) { 135 | return () => { 136 | let axes = state.axes 137 | let subs = state.substitutions.map(substitution => { 138 | let hypercube = substitution.state 139 | let divisions = hypercube.divisions.map(division => division.map(i => i)) 140 | 141 | // Generate all possible indices into this hypercube. 142 | let indices = Combinatorics.cartesianProduct(...hypercube.state.shape.map(length => { 143 | return [...Array(length).keys()] 144 | })).toArray() 145 | 146 | // Generate all of the bounded regions inside the hypercube. 147 | // This gets all non-default cells. 148 | let cells = indices.map(index => { 149 | let bounds = index.map((component, i) => { 150 | let dimension = divisions[i] 151 | let lower = dimension[component - 1] || 0 152 | let upper = dimension[component] || 1 153 | return [lower, upper] 154 | }) 155 | 156 | 157 | 158 | let user_coordinates = bounds.map((pair, i) => { 159 | return percent2user(pair, axes[i]) 160 | }) 161 | 162 | let normal_coordinates = user_coordinates.map((pair, i) => { 163 | return user2norm(pair, axes[i]) 164 | }) 165 | 166 | let coordinates = { 167 | user: user_coordinates, 168 | normal: normal_coordinates 169 | } 170 | 171 | let state = hypercube.state.get(...index) 172 | 173 | if (state) { 174 | console.log(coordinates) 175 | } 176 | 177 | return { bounds, coordinates, state } 178 | }).filter(x => x.state !== 0); 179 | 180 | return substitution.glyphs.slice(1).map((target_glyph, i) => { 181 | 182 | let target_state = i + 1 183 | let cells_for_target = cells.filter(x => x.state === target_state) 184 | 185 | return { 186 | substitution: { 187 | glyphs: [substitution.glyphs[0], target_glyph], 188 | subordinates: substitution.subordinates.map(glyphset => { 189 | return [glyphset[0], glyphset[i + 1]] 190 | }) 191 | }, 192 | cells: cells_for_target 193 | } 194 | }) 195 | }); 196 | 197 | return subs.reduce((a, b) => a.concat(b), []) 198 | } 199 | } 200 | }, 201 | mutations: { 202 | /** 203 | * The initialize action is the first action on loading a new 204 | * font into the application. It extracts the relevant glyph and 205 | * variations data from the typeface, and sets it up for rendering. 206 | */ 207 | [INITIALIZE] (state, {font, hash}) { 208 | /** 209 | * To initialize, we start by mapping across the available 210 | * Unicode points on the font, and getting Glyph objects for 211 | * each one. We match that with the glyph name, which is something 212 | * We're goint to want the user to search for later. 213 | */ 214 | state.savename = hash; 215 | 216 | state.glyphs = font.characterSet.map(pt => { 217 | let g = font.glyphForCodePoint(pt) 218 | return { 219 | name: g.name, 220 | codePoints: g.codePoints 221 | } 222 | }) 223 | 224 | let axesData = [] 225 | 226 | for (var axisTag in font.variationAxes) { 227 | let axisData = font.variationAxes[axisTag] 228 | axisData.tag = axisTag 229 | axesData.push(axisData) 230 | } 231 | 232 | state.axes = axesData 233 | state.ui.axisValues = axesData.map(axis => { return axis.default }) 234 | 235 | // TODO: Add handling for variable fonts with less than 2 axes. 236 | 237 | let variationsData = [] 238 | 239 | for (var variationName in font.namedVariations) { 240 | let variationData = font.namedVariations[variationName] 241 | variationData.name = variationName 242 | variationsData.push(variationData) 243 | } 244 | 245 | state.instances = variationsData 246 | 247 | state.meta = { 248 | postscriptName: font.postscriptName, 249 | fullName: font.fullName, 250 | familyName: font.familyName, 251 | subfamilyName: font.subfamilyName, 252 | copyright: font.copyright, 253 | version: font.version, 254 | numGlyphs: font.numGlyphs, 255 | numAxes: axesData.length, 256 | numInstances: variationsData.length 257 | } 258 | 259 | state.meta.stylisticSets = ['default'].concat(font.availableFeatures.filter( 260 | feature => { 261 | const start = feature.indexOf('ss') 262 | if (start === -1) { return false } 263 | try { 264 | let index = parseInt(feature.slice(start + 2)) 265 | return index >= 20 266 | } catch (e) { 267 | return false 268 | } 269 | } 270 | )) 271 | }, 272 | /** 273 | * This action reads a state saved in localStorage into 274 | * application memory. 275 | */ 276 | [LOAD_STATE] (state, {saved}) { 277 | let substitutions = []; 278 | 279 | saved.substitutions.forEach(sub => { 280 | let newSubstitution = { 281 | // The primary run of glyphs controlling the substitution 282 | glyphs: sub.glyphs, 283 | // A set of secondary runs of glyphs, which obey the 284 | // exact same rules as the primary run. 285 | subordinates: sub.subordinates, 286 | // A set of indices into the subordinates array, 287 | // indicating which of the subordinates should be 288 | // visualized in the grid. 289 | active_subordinates: [], 290 | // A set of grid divisions, equal in length to the number of design axes, 291 | // indicating where along the axis it should be divided, 292 | // as a percentage of total length. 293 | divisions: sub.divs, 294 | // the index of the design axis (and division) assigned to the 295 | // x dimension on the visualizer. 296 | x: 0, 297 | // the undex of the design axis (and division) assigned to the 298 | // y dimension on the visualizer 299 | y: 1, 300 | // A test string to be displayed to the left of the active sequence 301 | left_sequence: '', 302 | // A test string to be displayed to the right of the active sequence 303 | right_sequence: '', 304 | // The design space map which indicates where substitutions are applied 305 | // This is a hypercube with the same number of axes as design axes, 306 | // and cells equal to the product of lengths of each division array. 307 | state: new Hypercube(sub.divs, Object.values(sub.rects), sub.shape) 308 | } 309 | 310 | substitutions.push(newSubstitution); 311 | }); 312 | 313 | state.substitutions = substitutions; 314 | state.ui.substitution = substitutions.length - 1; 315 | }, 316 | /** 317 | * This action updates a single axis value to a new value 318 | * Primarily, this action responds to the sliders in the 319 | * axis control by updating the current design-point we're 320 | * looking at. 321 | */ 322 | [UPDATE_AXIS_VALUE] (state, {index, value}) { 323 | const axisValues = state.ui.axisValues 324 | axisValues[index] = value 325 | state.ui = {...state.ui, axisValues: axisValues} 326 | }, 327 | /** 328 | * This action pushes a new substutition object onto the substitutions array 329 | * Given a glyph object 330 | */ 331 | [ADD_NEW_SUBSTITUTION] (state, {glyphs}) { 332 | const substitutions = state.substitutions 333 | const divisions = state.axes.map((axis, i) => linear((i < 2) ? DEFAULT_MAJOR_SUBDIVISIONS : DEFAULT_MINOR_SUBDIVISIONS)) 334 | const newSubstitution = { 335 | // The primary run of glyphs controlling the substitution 336 | glyphs: glyphs, 337 | // A set of secondary runs of glyphs, which obey the 338 | // exact same rules as the primary run. 339 | subordinates: [], 340 | // A set of indices into the subordinates array, 341 | // indicating which of the subordinates should be 342 | // visualized in the grid. 343 | active_subordinates: [], 344 | // A set of grid divisions, equal in length to the number of design axes, 345 | // indicating where along the axis it should be divided, 346 | // as a percentage of total length. 347 | divisions: divisions, 348 | // the index of the design axis (and division) assigned to the 349 | // x dimension on the visualizer. 350 | x: 0, 351 | // the undex of the design axis (and division) assigned to the 352 | // y dimension on the visualizer 353 | y: 1, 354 | // A test string to be displayed to the left of the active sequence 355 | left_sequence: '', 356 | // A test string to be displayed to the right of the active sequence 357 | right_sequence: '', 358 | // The design space map which indicates where substitutions are applied 359 | // This is a hypercube with the same number of axes as design axes, 360 | // and cells equal to the product of lengths of each division array. 361 | state: new Hypercube(divisions) 362 | } 363 | const newLength = substitutions.push(newSubstitution) 364 | 365 | state.substitutions = substitutions 366 | state.ui.substitution = newLength - 1 367 | }, 368 | /** 369 | * This action deletes the substitution specified by the given 370 | * index from the application. 371 | */ 372 | [DELETE_SUBSTITUTION] (state, {substitutionIndex}) { 373 | let substitutions = state.substitutions 374 | substitutions = substitutions.filter((s, i) => i !== substitutionIndex) 375 | state.substitutions = substitutions 376 | }, 377 | /** 378 | * This action changes the current substitution index to a specified value. 379 | * passing an index of -1 to this action deactivates all substitutions 380 | */ 381 | [ACTIVATE_SUBSTITUTION] (state, {index}) { 382 | state.ui.substitution = index 383 | }, 384 | /** 385 | * This action updates the dimension for a specific design axis and 386 | * specific substitition. The substition and design axis are represented 387 | * as indices into the substitions list and axis list, respectively, while 388 | * the dimension of visualization is specified by a stirng name, either "x" or "y". 389 | */ 390 | [SET_AXIS_DIMENSION_FOR_SUBSTITUTION] (state, {substitutionIndex, axisIndex, dimensionName}) { 391 | let substitutions = state.substitutions 392 | let substititionToUpdate = substitutions[substitutionIndex] 393 | 394 | substititionToUpdate[dimensionName] = axisIndex 395 | 396 | substitutions[substitutionIndex] = substititionToUpdate 397 | 398 | state.substitutions = substitutions 399 | }, 400 | 401 | /** 402 | * This action updates the subdivision structure on a given axis to 403 | * the sequence passed to the action. Subdivision structures represent a 404 | * list of locations on the line between 0 and 1, each location represents 405 | * a cut point. The end points 0 and 1 are not included. 406 | * For example, a n equally-spaced, four subdivision structure is represented 407 | * as the array [0.25, 0.50, 0.75], indicating the three locations between 0 408 | * and 1 in which to divide the plane. 409 | */ 410 | [SET_AXIS_SUBDIVISIONS_FOR_SUBSTITUTION] (state, {substitutionIndex, subdivisions, dimensionName}) { 411 | let substitutions = state.substitutions 412 | let substititionToUpdate = substitutions[substitutionIndex] 413 | let axisIndex = substititionToUpdate[dimensionName] 414 | let divisions = substititionToUpdate.divisions 415 | 416 | divisions[axisIndex] = subdivisions 417 | substititionToUpdate.divisions = [...divisions] 418 | substititionToUpdate.state = new Hypercube(substititionToUpdate.divisions) 419 | 420 | substitutions[substitutionIndex] = substititionToUpdate 421 | 422 | state.substitutions = [...substitutions] 423 | }, 424 | 425 | /** 426 | * This action updates the flanking sequences of a substitution (that is, the 427 | * designer-set glyphs that accompany the glyph being substituted for proofing purposes.) 428 | */ 429 | [UPDATE_SEQUENCE_FOR_SUBSTITUTION] (state, {substitutionIndex, leftSequence, rightSequence}) { 430 | let substitutions = state.substitutions 431 | let substitution = substitutions[substitutionIndex] 432 | 433 | substitution.left_sequence = leftSequence 434 | substitution.right_sequence = rightSequence 435 | 436 | substitutions[substitutionIndex] = substitution 437 | state.substitutions = [...substitutions] 438 | }, 439 | 440 | /** 441 | * this action updates a single cell state for a given n-d cell in the 442 | * specified substitution. 443 | */ 444 | [SET_STATE_FOR_CELL] (state, {substitutionIndex, point, cellState}) { 445 | let substitutions = state.substitutions 446 | let substitution = substitutions[substitutionIndex] 447 | 448 | substitution.state.set(point, cellState) 449 | 450 | substitutions[substitutionIndex] = substitution 451 | 452 | state.substitutions = [...substitutions] 453 | }, 454 | 455 | /** 456 | * this action adds a new glyphset to the specified substitutions 457 | * set of subordinate sequences. `glyphset.length` is assumed to 458 | * be equal to substitution.glyphs.length. (IE, all glyphsets in 459 | * a substitution must be the same length.) 460 | */ 461 | [ADD_SUBORDINATE_TO_SUBSTITUTION] (state, {substitutionIndex, glyphset}) { 462 | let substitutions = state.substitutions 463 | let substitution = substitutions[substitutionIndex] 464 | 465 | substitution.subordinates = substitution.subordinates.concat([glyphset]) 466 | 467 | substitutions[substitutionIndex] = substitution 468 | state.substitutions = [...substitutions] 469 | }, 470 | 471 | /** 472 | * this action removes the subordinate sepcified by the `subordinateIndex` 473 | * from the specified substitution's subordinates list. `subordinateIndex` is 474 | * assumed to lie in the range [0, substitution.subordinates.length - 1] 475 | */ 476 | [REMOVE_SUBORDINATE_FROM_SUBSTITUTION] (state, {substitutionIndex, subordinateIndex}) { 477 | let substitutions = state.substitutions 478 | let substitution = substitutions[substitutionIndex] 479 | 480 | substitution.subordinates = substitution.subordinates.filter((s, i) => i !== subordinateIndex) 481 | substitution.active_subordinates = substitution.active_subordinates.filter(i => i !== subordinateIndex) 482 | 483 | substitutions[substitutionIndex] = substitution 484 | state.substitutions = [...substitutions] 485 | }, 486 | 487 | /** 488 | * this action swaps the current substitutions's primary glyph sequence with 489 | * the sequence specified by `subordinateIndex`. `subordinateIndex` is 490 | * assumed to lie in the range [0, substitution.subordinates.length - 1] 491 | */ 492 | [SWAP_SUBORDINATE_AND_PRIMARY] (state, {substitutionIndex, subordinateIndex}) { 493 | let substitutions = state.substitutions 494 | let substitution = substitutions[substitutionIndex] 495 | 496 | let old_primary = [...substitution.glyphs] 497 | substitution.glyphs = [...substitution.subordinates[subordinateIndex]] 498 | substitution.subordinates[subordinateIndex] = old_primary 499 | 500 | substitutions[substitutionIndex] = substitution 501 | state.substitutions = [...substitutions] 502 | }, 503 | 504 | /** 505 | * 506 | * 507 | */ 508 | [ACTIVATE_SUBORDINATE_IN_GRID] (state, {substitutionIndex, subordinateIndex}) { 509 | let substitutions = state.substitutions 510 | let substitution = substitutions[substitutionIndex] 511 | 512 | substitution.active_subordinates = substitution.active_subordinates.concat([subordinateIndex]) 513 | 514 | substitutions[substitutionIndex] = substitution 515 | state.substitutions = [...substitutions] 516 | }, 517 | 518 | /** 519 | * 520 | * 521 | */ 522 | [DEACTIVATE_SUBORDINATE_IN_GRID] (state, {substitutionIndex, subordinateIndex}) { 523 | let substitutions = state.substitutions 524 | let substitution = substitutions[substitutionIndex] 525 | 526 | substitution.active_subordinates = substitution.active_subordinates.filter(i => i !== subordinateIndex) 527 | 528 | substitutions[substitutionIndex] = substitution 529 | state.substitutions = [...substitutions] 530 | }, 531 | }, 532 | actions: { 533 | [INITIALIZE] ({commit, state}, {font, buffer}) { 534 | let hash = hasha(buffer.toString(), {algorithm: 'md5'}); 535 | commit(INITIALIZE, {font, hash}) 536 | 537 | if ( 538 | typeof localStorage !== 'undefined' && // localStorage exists, and 539 | localStorage.getItem(hash) !== null // we've seen this file before. 540 | ) { 541 | let savedState = localStorage.getItem(hash); 542 | let parsed = JSON.parse(savedState); 543 | 544 | commit(LOAD_STATE, {saved: parsed}); 545 | 546 | } else { 547 | 548 | let sequences = autopopulate(font); 549 | sequences.forEach(substitution => { 550 | commit(ADD_NEW_SUBSTITUTION, {glyphs: substitution.glyphs}); 551 | let index = state.ui.substitution; 552 | substitution.subordinates.forEach(glyphs => { 553 | commit(ADD_SUBORDINATE_TO_SUBSTITUTION, {substitutionIndex: index, glyphset: glyphs}); 554 | }) 555 | }) 556 | 557 | } 558 | }, 559 | [UPDATE_AXIS_VALUE] ({commit}, payload) { 560 | commit(UPDATE_AXIS_VALUE, payload) 561 | }, 562 | [ADD_NEW_SUBSTITUTION] ({commit}, payload) { 563 | commit(ADD_NEW_SUBSTITUTION, payload) 564 | }, 565 | [ACTIVATE_SUBSTITUTION] ({commit}, payload) { 566 | commit(ACTIVATE_SUBSTITUTION, payload) 567 | } 568 | } 569 | }) 570 | 571 | if (typeof localStorage !== 'undefined') { 572 | store.subscribe((mutation, state) => { 573 | if (saveableActions.indexOf(mutation.type) !== -1) { 574 | 575 | let savestate = { substitutions: [] }; 576 | 577 | for (var key in state.substitutions) { 578 | savestate.substitutions.push({ 579 | glyphs: state.substitutions[key].glyphs, 580 | subordinates: state.substitutions[key].subordinates, 581 | divs: state.substitutions[key].state.divisions.map(a => a.toArray()), 582 | rects: state.substitutions[key].state.state.data, 583 | shape: state.substitutions[key].state.state.shape 584 | }); 585 | } 586 | 587 | console.log(savestate); 588 | 589 | localStorage.setItem(state.savename, JSON.stringify(savestate)); 590 | } 591 | }); 592 | } 593 | 594 | 595 | 596 | export default store; 597 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Variable Font Substitution Mapper 2 | **Current Version: 0.9.6, in development.** 3 | 4 | 🎥 Watch our 2021 Typographics TypeLab talk [here](https://vimeo.com/568482316) 5 | 6 | This tool provides a visualizer and font variation substitution manager for `.ttf` variable fonts. The tool helps with setting up glyph substitutions (also called OpenType [Required Variation Alternates](https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#tag-rvrn), or `rvrn`) that depend on the design space region. It can build substititions for many different glyphs visually. Once you're done setting up substititions, the tool will generate a `` element that you can paste into a `.designspace` file. With this updated file, you can rebuild your `.ttf` using fontmake and get a variable font with complex substitution patterns. 7 | 8 | This document walks you through how to accomplish this with our tool. 9 | 10 | ## A Note about the Project 11 | 12 | This project originated at Occupant Fonts over the spring and summer of 2020. We were stuck inside, trying to successfully build our variable font projects, and trying to get our substitions to work – all without going to crazy from the stress of COVID-19. 13 | 14 | The visualizer is a project that we built to suit our needs as a studio. It has a lot of rough edges, and is not really a "product-ready" tool. If you're interested in using, you'll need to work through a few steps to get your fonts ready for the tool, and a few steps to get a working variable font with substitutions out of it. 15 | 16 | We develop this tool when we find new needs or features for specific projects we have, but we don't develop it outside of that much. So, as new projects come up, expect the tool to evolve, but don't expect it to be actively developed on a weekly basis. 17 | 18 | If you do use the tool and find issues or have suggestions, you're welcome to open up issues on this repository! 19 | 20 | ## Terminology 21 | 22 | Substitutions, or "Required Variation Alternates," determine how glyphs should be substituted depending on coordinates in your designspace. This section intros some terminology that we use throughout the application. 23 | 24 | ### Substitutions 25 | A substitution is a group of glyphs that represent the same unicode point. When that unicode is requested, the glyph that is rendered depends on the current variation settings of the font. 26 | 27 | A substitution has a default glyph and any number of substitutes, which are swapped in at different regions in the designspace. As a table, a substitution might look something like this. 28 | 29 | | | Default | Substitute 1 | ... | Substitute N | 30 | | - | --- | --- | --- | --- | 31 | | **Glyph** | `won` | `won.sub_bar1` | ... | `won.sub_barN`| 32 | | **Region** | *Region 0* | *Region 1* | ... | *Region N* | 33 | 34 | The *Region* elements here are stand-ins for rectangular staircases in the design space. 35 | 36 | ![Image Shows: the variable font substitution manager interface showing the glyph oslash and its substitions.](./img/vfsm-staircase-one.png) 37 | 38 | *The blue "staircase" shows the "Region" where a substitution is applied to the font. Whenever the coordinate is inside this 2D region (the x dimension representing `wght` and the y dimension representing `wdth`), the barless `oslash` will be swapped for the barred `oslash.sub_bar`.* 39 | 40 | ### Substitutions with Subordinates 41 | 42 | Oftentimes, we want multiple substitutions to follow the same pattern of regions. The visualizer supports associating multiple substitutions together. We call these grouped substition patterns "subordinate patterns." 43 | 44 | | | Default | Substitute | 45 | | - | --- | --- | 46 | | **Glyph** | `Q` | `Q.sub_bar` | 47 | | **Glyph** | `Oslash` | `Oslash.sub_bar` | 48 | | **Glyph** | `oslash` | `oslash.sub_bar` | 49 | | **Glyph** | `Oslashacute` | `Oslashacute.sub_bar` | 50 | | **Glyph** | `oslashacute` | `oslashacute.sub_bar` | 51 | | **Region** | *Region 1* | *Region 2* | 52 | 53 | Just like regular substitutions, substitutions with subordinates can have any number of glyphs. The catch is, each subordinate must have **the same** number of glyphs, whatever that number is. 54 | 55 | ![Image Shows: the variable font substitution manager interface showing multiple subordinates for the oslash glyph, including oslashacute and q.](./img/vfsm-staircase-two.png) 56 | 57 | *Using subordinates, large numbers of glyphs can be grouped together and use the same substitution pattern across the designspace. We often use this for small weight adjustments or serif adjustments.* 58 | 59 | 60 | 61 | ## Glyph Naming Conventions 62 | 63 | **NOTE: These naming conventions are based on what we use at Occupant Fonts. We know that they won't work for everyone. The visualizer can be used without them, as susbtitutions and subordinates can be created in the interface.** 64 | 65 | It's possible to set up and manage all your substitutions and subordinates through the visualizer interface. However, this can be a time consuming process in itself. Because of this, the visualizer provides some naming conventions The variable font visualizer will automatically generate susbtitution and subordinate sets for you, if you name your substitution glyphs in the following parts. Each of the parts is joined together with a separator. We use a period (`.`) to start off the string, and then underscores (`_`) inside it. 66 | 67 | | | Base Glyph Name | Tag | Substitution Class | Instance (optional) | 68 | | - | - | - | - | - | 69 | | **Pattern** | `{glyph}` | `sub` | `{class}` | `{id}` 70 | | **`Q.sub_bar`** | `Q` | `sub` | `bar` | 71 | | **`Oslash.sub_bar`** | `Oslash` | `sub` | `bar` | 72 | | **`won.sub_bar_1`** | `won` | `sub` | `bar` | `1` 73 | | **`won.sub_bar_2`** | `won` | `sub` | `bar` | `2` 74 | | **Notes** | This is the name of the glyph for which this substitution applies. | This tag identifies this glyph as a substitution. All substitution glyphs have this tag, which makes them easily searchable. | The class name is an arbitrary identifier that groups substitutions that should follow the same pattern together. | The instance identifier is a unique per-base-glyph, and allows for multipls substitutions per base-glyph. If there is only one substitute for the base glyph in the class, no instance id is needed. 75 | 76 | Substitutions are grouped together as subordinates under two conditions: 77 | 78 | 1. The two substitutions have the same Substitution Class (the `bar` in the examples above), and 79 | 2. The two substitions have the same number of substitutes. 80 | 81 | If these two conditions hold, then the susbtitutions will be grouped together as subordinates. If either of them does not hold, then the substitutions will not be grouped together. 82 | 83 | As a caveat: If you use this notation, then no other glyph names in your project should contain `.sub_` (this is the string our preprocessor uses to identify substitutions) as a substring in their names. Hopefully this isn't too arduous of a restriction 😊. 84 | 85 | 86 | ## Using the tool 87 | 88 | There are a few steps that you need to take in order to use the visualizer: 89 | 90 | 1. Preparing your `.glyphs` file with private unicode assignments. 91 | 2. Installing tools (only need to do this once). 92 | 3. Generating a `.designspace` representation of your `.glyphs` file. 93 | 4. Generating a visualizer-compatible `.ttf` file. 94 | 5. Updating the `.designspace` file with the visualizer output. 95 | 6. Compiling the final, production-ready `.ttf` file. 96 | 97 | We'll go through each of these steps in detail here. 98 | 99 | **Note:** At Occupant Fonts, we use Glyphs.app for most of our drawing. These instructions are based on coming from Glyphs.app, where you're working with `.glyphs` sources, and you need to convert them to `.designspace` / `.ufo`. If you're in Robofont, you're one step ahead, and you should be skip to step 3. 100 | 101 | ### 1. Preparing your Source file 102 | 103 | In order to find the alternate glyphs that you want to use in substitutions, you need to assign them to unicode values. This lets the browser render them, and ensures that they'll be searchable by glyph name in the visualizer interface. Luckily, we can assign these glyphs to "private unicodes" that aren't assigned to key bindings by default. This means you won't be able to type them, but they'll show up in the visualizer, and the web browser will be able to render them. 104 | 105 | [Private unicodes](http://www.unicode.org/faq/private_use.html#:~:text=There%20are%20three%20ranges%20of,Private%20Use%20Area%20(PUA).) are just unicode values in the range `E000` to `FF8F`, giving us **6,400** possible substitution glyphs. If you need more than 6,400 substitution glyphs, there are more private unicode ranges further out in "unicode space" – although if you're managing thousands of substitutions, this might not be the best tool for you. 106 | 107 | You're free to assign any of these values to each of your substitution glyphs. It doesn't matter which. A simple script in Glyphs or Robofont could be used to accomplish this. 108 | 109 | Cyrus also recommends a consistent naming convention for your substitution glyphs. He uses `{glyph name}.sub_bar` for most glyphs with this kind of behavior. This also makes it easy to search for substitutions, by searching for "sub" or "bar" in the visualizer. 110 | 111 | 112 | ### 2. Installing Tools 113 | 114 | You might not have the required commands for building `.ufo` files and font binaries installed right now. You can install all the required tools (`glyphs2ufo` and `fontmake`) by installing the `fontmake` package. To do this, open your terminal and run: 115 | 116 | ```sh 117 | pip3 install fontmake 118 | ``` 119 | 120 | This will add both of these tools to your system, and make them accessible from your terminal. 121 | 122 | ### 3. Generating a `.designspace` 123 | 124 | Next, we need to generate a `.designspace` file with `.ufo` files for each master. This is pretty easy with the `glyphs2ufo` tool, a [command line tool](https://github.com/daltonmaag/glyphs2ufo) which does exactly what it sounds like. 125 | 126 | To start, you need to navigate to your project folder. To do this, find the folder in Finder. Type `cd ` (with a space after) into your terminal, and then drag the folder icon into the terminal. You should get something like this: 127 | 128 | ```sh 129 | cd path/to/my/project/folder 130 | ``` 131 | 132 | with some long filesystem path to the folder you dragged. Hit enter to go to that folder in your terminal. 133 | 134 | Now that you're in the proper folder in your terminal, we can run the `glyphs2ufo` command. Type: 135 | 136 | ```sh 137 | glyphs2ufo GlyphsFile.glyphs -m ufos 138 | ``` 139 | 140 | This will generate a set of `.ufo` files and a `.designspace` file, and put them into an new folder called `ufos` in your current directory. 141 | 142 | *Note: theoretically, it should be possible to roundtrip back to a glyphs file from your designspace file. So, after you paste the `` output from your designspace file, you might be able to bring those rules back into glyphs, and export from there. However, I have not tested this, and do not know how well this kind of roundtrip works. So, it's best to generate substitutions as the last step in your production process.* 143 | 144 | 145 | ### 4. Preparing and Cleaning your `.designspace` 146 | 147 | When your `.designspace` file comes out of `glyphs2ufo`, it's not always 100% ready for the visualizer. Depending on the settings of your Glyphs file, the `glyph2ufo` tool will attempt to remap your axis coordinates to standard coordinates specified by the OpenType spec. If you're coming from RoboFont, you probably don't need to worry about all this stuff! 148 | 149 | For example, if you have a `wght` axis ranging from `0` to `1000`, the tool will attempt to remap this axes to `[100, 900]`. 150 | 151 | This is a problem for us, since we need to specify our substitituions in terms of the coordinate system we defined (`0` to `1000`), not some other one. To make sure of this, we'll need to modify the `` element in the `.designspace` file. 152 | 153 | When we generates a three axis variable font (containing Weight masters, Width masters, and virtual X-height masters) from Glyphs, we got a `.designspace` file with the following axis tag: 154 | 155 | ```xml 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | ``` 175 | 176 | Notice that our range for the weight and width axes, both `0` to `1000`, have been replaced by the OpenType Spec recommended values. These recommended values are specified in the minimum and maximum of each `` tag, and the mapping from our range to the recommended range is implemented with `` tags. 177 | 178 | The key thing to know here is that these `` elements are applied only after all other data in the `.designspace` file has been specified. The positions of masters and instances, as well as the location of feature variations, are specified in the **original range**, `0` to `1000`, not the mapped range of `100` to `900` (in the case of weight). The visualizer needs to produce features in the original range, not the mapped range. 179 | 180 | To do this, we can just delete the `` elements and make sure the `` tags have the original ranges. If you like, you can keep the mapped table for later, and paste it back in after you've generated the sequence. To prepare for the visualizer, we updated the table above to the following one: 181 | 182 | ```xml 183 | 184 | 185 | 186 | 187 | 188 | ``` 189 | 190 | Now that the mapping is removed, the visualizer will generate features using the correct, original coordinate system for the axes. 191 | 192 | #### A Note About Virtual Masters 193 | 194 | While you're doing this, it's a good idea to check the ranges on your axis tags, too. If you're using virtual masters to implement axes in Glyphs, the export process can sometimes collapse these axes onto *no* range, which causes `.ttf` compilation to fail. 195 | 196 | When we generated an `xhgt` axis using virtual masters the result axis in the `.designspace` file was specified as going from `0` to `0`, when we wanted `0` to `100`. A quick change in the `` tag for that axis solved the problem. 197 | 198 | 199 | ### 5. Generating a `.ttf` 200 | 201 | The VF visualizer works with font binaries, so we need to generate a variable `.ttf` file. We can do this with the library [`fontmake`](https://github.com/googlefonts/fontmake) from Google. `fontmake` glues together a bunch of different utilities into a single swiss-army knife. 202 | 203 | `glyphs2ufo` generated a new folder called `ufos` for us, with all of our masters and designspace file in it. To move to that new folder, type: 204 | 205 | ```sh 206 | cd ufos 207 | ``` 208 | 209 | Now that you're in the right folder, go ahead and build a `.ttf` for the visualizer: 210 | 211 | ```sh 212 | fontmake -m DesignspaceFile.designspace -o variable --no-production-names 213 | ``` 214 | 215 | This command eats a `.designspace` file, and tries to compile it into a TrueType variable font. The `--no-production-names` flag specifies that we don't want to rename any of our glyphs to AGL-standards-compliant glyph names. This is important, otherwise the `` table the visualizer generates will have unicode identifiers, rather than names, for certain glyphs, and the `.designspace` file won't understand these. The final `.ttf` we build will have production names, don't worry. 216 | 217 | If everything works properly, this command will create a new folder called `variable_ttf`, which will contain the compiled `.ttf`. That's the file that you can drag into the visualizer. If you want to open the `ufos` folder to see the new files in Finder, you can type: 218 | 219 | ```sh 220 | open . 221 | ``` 222 | 223 | In the terminal. This should open up a finder window with the current folder. From there, you can drag the file into the visualizer: [here](https://morisawausa.github.io/_vfvisualizer). 224 | 225 | ### 6. Generating Rules 226 | 227 | #### 6.1 Making a Substitution 228 | 229 | Drag your `.ttf` onto the visualizer webpage to get started. The webpage should parse the `.ttf` and render a side panel. Once that's done, you'll see an empty visualizer, ready for you to create substitutions. (If you used our naming convention, you'll see some pre-made substitions with one of them activated). 230 | 231 | ![Image Shows: an empty UI for the substition mapper, showing the axes and ad an empty list of susbtitions.](./img/vfsm-step-one.png) 232 | 233 | To create a substitution, type in the yellow `Search Glyphs...` box to search for the glyph you want to substitute by name. We call this region of the screen the substitution manager box. 234 | 235 | ![Image Shows: the glyph search bar populated with a list of glyphs matching the query oslash. The oslash glyph is at the top, and it is highlighted.](./img/vfsm-step-two.png) 236 | 237 | Click the glyph in the scrollable dropdown, then repeat the process to find the glyph you want to swap in. 238 | 239 | ![Image Shows: the oslash glyph in the substution header box](./img/vfsm-step-three.png) 240 | ![Image Shows: the oslash glyph, followed by the oslash.sub_bar glyph in the substution header box](./img/vfsm-step-four.png) 241 | 242 | You'll see a list of the substututing glyphs at the top. Click create in the bottom right of the box to enable the substitution. 243 | 244 | ![Image Shows: The cursor over the create button, ready to create the substitution.](./img/vfsm-step-five.png) 245 | ![Image Shows: A substitution created with a grid of glyphs across a 2D space. The grid goes from condensed and light in the top left to extended and bold in the bottom right.](./img/vfsm-step-six.png) 246 | 247 | Your substutution is ready for editing. 248 | 249 | #### 6.2 Making Subordinates 250 | 251 | You may want to add more glyphs to this substitution, if you'd like them all to transform in the same way. 252 | 253 | Click header of the substitution manager box (where you see the `oslash` transitioning to the `oslash.sub_bar` in these examples) to bring up a list of subordinates in this substutution (you can click again to close it). 254 | 255 | ![Image Shows: The subordinates list, currently empty, popping up to the right of the substitution manager box.](./img/vfsm-step-seven.png) 256 | 257 | Search in the yellow search box in the pop-up to find the glyphs you want to add to the substitution. 258 | 259 | ![Image Shows: The subordinates list, currently empty, popping up to the right of the substitution manager box.](./img/vfsm-step-eight.png) 260 | ![Image Shows: The subordinates list, currently empty, popping up to the right of the substitution manager box.](./img/vfsm-step-nine.png) 261 | 262 | Repeat this for as many glyphs as you like. 263 | 264 | If you make a mistake, or want to remove subordinate, hover over it in the list. You'll see a set of three, black icons appear. 265 | 266 | ![Image Shows: The subordinates list, currently empty, popping up to the right of the substitution manager box.](./img/vfsm-step-ten.png) 267 | 268 | Clicking the backspace icon (⌫), all the way to the left, will remove the subordinate. Clicking the up-down arrow (⇅), in the middle, will swap this subordinate into the grid, so you can see it. Clicking the plus icon (+), all the way to the right, will add this glyph to the grid, next to any other glyphs in the grid. 269 | 270 | Repeat this process as many times as needed to generate all your subordinates. 271 | 272 | #### 6.3 Changing the Axes. 273 | 274 | The visualizer displays 2 axes at a time. You can assign which axes are displayed on a per-substitution basis. By default, the first two registered axes are displayed. The third one can be scrubbed across using the slider in the top left, just below the box displaying font info. 275 | 276 | To change the active axes, find the dimension you want to change (`x` or `z`), click the axis tag you want to assign. 277 | 278 | ![Image Shows: Hovering over the XHEI axis tag under the x dimension label.](./img/vfsm-step-eleven.png) 279 | ![Image Shows: A new grid, with XHEI on the x dimension and wdth on the y dimension](./img/vfsm-step-twelve.png) 280 | 281 | By default, the visualizer creates seven divisions each for the first two registered axes, and 2 each for all subsequent axes. 282 | 283 | #### 6.4 Changing the Grid Region Count 284 | 285 | To change the subdivisions along a specific axis, choose the substitution you want, assign the axis you want to change to a given dimension, click in the yellow box, and change the number to the desired number of regions. Below, I changed the number of width regions from 7 to 3. 286 | 287 | ![Image Shows: A coarser grid with 2 subdivisions along XHEI and 3 subdivisions along wdth](./img/vfsm-step-thirteen.png) 288 | 289 | ###### **⚠️⚠️ WARNING ⚠️⚠️** 290 | Changing the number of grid divisions will ERASE the substitution pattern you currently have. Be sure to set your substitution grid sizes **before** you start making patterns. This hasn't been a huge limitation for us, but fixing it is a nice-to-have. 291 | 292 | #### 6.5 Make Substitutions 293 | 294 | Making substutition patterns is clicking in the grid. 295 | 296 | ![Image Shows: A grid with some substitutions filled in.](./img/vfsm-step-fourteen.png) 297 | 298 | To remove a substituted region, just click on it again. 299 | 300 | You can also use the pill-shaped icons, in the top-left to the right of the info box, to toggle various visualizer states. Clicking the "I" button toggles instance locations in the grid, for example. 301 | 302 | 303 | ![Image Shows: A grid with some substitutions filled in.](./img/vfsm-step-fifteen.png) 304 | 305 | ### 6.6 Exporting the Rules Table 306 | 307 | Click "Generate Table" in the top right to generate the `` element for your designspace file. The table will be automatically generated from what you've drawn. You can click "Copy Table" to copy the table to your clipboard and paste it into your `.designspace` file. 308 | 309 | ![Image Shows: The Generate Table element extended, with the rules table generated in it.](./img/vfsm-step-sixteen.png) 310 | 311 | ### 7. Updating the `.designspace` 312 | 313 | Once you have a `` table generated and ready to go, you can copy and paste it into the `.designspace` file. I usually paste the `` XML element into the file near the top: right below the `` tag. 314 | 315 | ### 8. Generating the production `.ttf` 316 | 317 | Now we're ready to compile the final `.ttf` file with all of our feature variations and substitutions. WE can use `fontmake` again: 318 | 319 | ```sh 320 | fontmake -m UpdatedDesignspaceFile.designspace -o variable --production-names 321 | ``` 322 | 323 | This time, we'll have `fontmake` rename our glyphs to their production names. 324 | 325 | At this point, you should be ready to drag the file into a testing tool like FontGoggles and check to see if your feature variations and substitutions are working properly. 326 | 327 | --- 328 | 329 | ## Developing and Contributing 330 | 331 | This section is for developers interested in contributing to the VFV. 332 | 333 | ### The Development Environment 334 | 335 | To develop on this repository, you'll need to get a local development environment set up first. The javascript source code you see in this repository is compiled into a web-app by `webpack`, and it depends on a variety of node modules and packages off of `npm`. Consult our `package.json` for the full list. 336 | 337 | To start with, you'll need to have `node.js` installed. We develop with `node` version `v14.17.3`. Any version below version `v16` should work. Node versions `16` and up currently don't work because `libsass`, the C++ library we use to compile our stylesheets, does not support any version above `v15`. 338 | 339 | Once you've cloned this repository into a local directory, open the directory in your terminal and run `npm install` to download and install all of the dependencies. 340 | 341 | Once these have been installed correctly, you're ready to begin development. To start a local server, run `npm start`. If everything is installed correctly and working, you should see a terminal window that looks somehting like this. 342 | 343 | ![Image of a terminal with the development server running.](img/vfsm-developing-1.png) 344 | 345 | You should be able to browse to `http://localhost:8080` and see the application running there. 346 | 347 | ### Contributing New Code Generators 348 | 349 | The Variable Font Visualizer can target multiple output formats. Currently, it can generate code that can be pasted into a `.designspace` file, however, other formats may be possible. 350 | 351 | To contribute a code generator, your job is to take a list of variable font axes, and a list of grid cells in the design space to apply substitutions to, and from this info, compile a string that can be pasted into your target platform. 352 | 353 | All code generators live in the `src/store/tables.js` file. This file exports our `.designspace` generator, as well as a semi-working prototype `.ttx` generator (currently disable). These functions are then hooked into the view in the `src/components/SubstitutionOutput.vue` file in a fairly straightforward way. For now, this process is manual, and we can assist with it. If there's interest in a variety of code generators, we can make it more automatic in the future. 354 | 355 | For now, if you want to implement a code generator, focus on implementing a function: 356 | 357 | ```js 358 | export function myCodeGenerator(axes, cells) { 359 | return ['line1', 'line2' , ...] 360 | } 361 | ``` 362 | 363 | #### Code Generator Function: Axis Parameter 364 | 365 | The axis parameter to the code generator is a list of variable font axes on the current font. Each axis is a javascript object that looks like the following: 366 | 367 | ```js 368 | { 369 | default: Number, // the default value of the axis range 370 | max: Number, // the upper bound of the axis range 371 | min: Number, // the lower bound of the axis range 372 | name: String, // the readable name of the axis 373 | tag: String // the four letter axis tag 374 | } 375 | ``` 376 | 377 | You'll get an array of these as the first parameter to the code generator. The array will be in the order in which the axes were read out of the typeface file you dropped in. 378 | 379 | 380 | #### Code Generator Function: Cells Parameter 381 | 382 | The second parameter to the code generator is an array of substitutions to create. Each substitution is a complex object that carries all the data needed to create your output. Each substitution will look like the following: 383 | 384 | ```js 385 | { 386 | /** 387 | * The substitution key details which sets of glyphs 388 | * are being substituted. the `glyphs` key carries an array of the primary glyphs 389 | * which appears in the interface when you click the substitution. The 0th glyph in the 390 | * array will be the default form, the 1st will be the 1st substitution, and so on. 391 | * In general, there can be any number of glyphs in this array, corresponding to 392 | * the number of different forms the base glyph can take on across the designspace. 393 | */ 394 | substitution: { 395 | /* This key will be an array of length equal to the number of different 396 | * forms this glyph has across the designspace. 397 | */ 398 | glyphs: [{ 399 | name: String, // the name of the glyph as specified in the file 400 | codePoints: [Number] // the glyph's unicode assignments 401 | }, ...], 402 | 403 | /* This key will contain all other glyphs that should follow 404 | * the same substutition pattern as the glyphs in the `glyphs` key. 405 | * it will be an array of arrays of glyphs. Each array inside of this key 406 | * will have the same structure as the array in the `glyphs` key. So, if 407 | * the array in `glyphs` has two glyphs in it, then this array will be an array 408 | * of arrays, each of which contains two glyphs. 409 | */ 410 | subordinates: [ 411 | [{name: String, codePoints: [Number]}, {name: String, codePoints: [Number], ...}], 412 | [{name: String, codePoints: [Number]}, {name: String, codePoints: [Number], ...}], 413 | ... 414 | ] 415 | }, 416 | /** 417 | * The cells key is a list of all of the rectangles that you see highlighted in 418 | * the substitution grid. If there are 4 highlighted grid squares, this array will contain 419 | * four cells. 420 | */ 421 | cells: [ 422 | /** 423 | * the bounds key is an array of pairs of numbers. Each pair represents the minimum 424 | * and maximum of the rectangle along the corresponding axis in the Axis parameter. 425 | * In other words, if the first axis in your axis parameter is `wght`, then the first 426 | * element in this array is the min and max of this rectangle along the `wght` axis. 427 | * 428 | * Note: the coordinate system here is interpreted as a percentage of the total 429 | * axis length, so it ranges from 0 to 1. If you had a rectangle going from 0 to 250 430 | * along an axis with a min of 0 and max of 1000, then the bounds pair for this 431 | * rectangle and axis would be [0, 0.25] 432 | */ 433 | // [axis 0 min, axis 0 max], [axis 1 min, axis 1 max], ... 434 | bounds: [[Number, Number], [Number, Number], ...], 435 | 436 | /* the coordinates object precomputes bounds in a few other useful coordinate systems: 437 | * the user coordinate system that was read in off of the variable font file 438 | * and the normalized coordinate system used by the variable font internally, 439 | * ranging from [-1, 1], with 0 at the default value. 440 | * 441 | * Otherwise, it has the same general structure as the `bounds` key. 442 | */ 443 | coordinates: { 444 | 445 | normal: [[Number, Number], [Number, Number], ...], 446 | user: [[Number, Number], [Number, Number], ...], 447 | }, 448 | 449 | /* The state number is interpreted as an array index into the `substitution.glyphs` key. 450 | * it picks out exactly which glyph should be substituted in in this design space rectangle. 451 | * a state of 1 means that the second glyph in the glyphs array should replace 452 | * the first glyph in the array here. A state of 2 means that the 3rd glyph should replace 453 | * the first glyph. 454 | * 455 | * Note that you'll never see a state of 0. 0 correspondes to the default glyph in the 456 | * substitution, and no replacement at all. 457 | */ 458 | state: Number 459 | ] 460 | /** 461 | * NOTE(1): the rectangles are not optimized at all. For example, we don't try to unify 462 | * neighboring rectangles at the moment. So far, this hasn't been an issue for us, 463 | * but if performance becomes a noticable problem, this will change. This optimization 464 | * will be done before getting to the code generation step, so your generator shouldn't 465 | * worry about dealing with this. Just process the rectangles c: 466 | * 467 | * NOTE(2): In the UI, there is a hierarchy between the data in the glyphs key 468 | * and the subordinates key. For code generation, that hierarchy probably does 469 | * not matter. You can probably safely append the glyphs array to the subordinates 470 | * array and process the whole batch in one go. 471 | */ 472 | } 473 | ``` 474 | 475 | --- 476 | 477 | ## Resources 478 | 479 | This is just a list of links to prior work that's been done on the subject. 480 | 481 | - [Overview of Feature Variations](https://github.com/irenevl/variable-fonts-with-feature-variations). This is a super useful resource that walks through manually making feature variations with the GSUB table. Basically, it's this process that we're trying to improve a bit. 482 | --------------------------------------------------------------------------------