├── .gitattributes ├── tests ├── non-json.json ├── simple-saved.json ├── abugida2 │ ├── Abugida2 Test PDF.pdf │ ├── base.svg │ ├── k.svg │ ├── eu.svg │ ├── a.svg │ ├── o.svg │ ├── r.svg │ ├── p.svg │ ├── t.svg │ ├── d.svg │ └── ph.svg ├── parsePath │ ├── quadratic-bezier.svg │ ├── arc.svg │ ├── circle.svg │ ├── curve.svg │ ├── simple.svg │ ├── standing-s-curve.svg │ ├── error-no-root.svg │ ├── rectangle.svg │ ├── relative.svg │ ├── curved-s-curve.svg │ ├── warn-empty-fill.svg │ ├── warn-non-empty-stroke.svg │ ├── transform-scale.svg │ ├── transform-rotate.svg │ ├── two-parts.svg │ ├── zero-xy.svg │ ├── comments.svg │ ├── viewbox.svg │ ├── warn-once-empty-fill.svg │ ├── transform-chain.svg │ ├── opacity.svg │ ├── nested.svg │ ├── transform.svg │ └── warn-complex-fill.svg ├── syllabry1 │ ├── README.md │ ├── no.svg │ ├── na.svg │ └── ta.svg └── e2e │ ├── illustrator │ └── circle.svg │ └── inkscape │ ├── korean-gul-combined.svg │ ├── korean-u.svg │ ├── korean-n.svg │ ├── korean-g.svg │ ├── korean-l.svg │ ├── korean-a.svg │ ├── korean-h.svg │ └── korean-han-combined.svg ├── state ├── keys.js ├── reducers │ ├── list.js │ ├── clearable.js │ ├── fonts │ │ ├── index.spec.js │ │ ├── index.js │ │ └── assembleDataUri.js │ ├── substitution.spec.js │ └── substitution.js ├── status.js ├── encoders │ ├── index.js │ ├── json.js │ └── msgpack.js ├── index.js ├── store.js ├── actionTypes.js └── actions.js ├── static ├── robots.txt ├── favicon.ico ├── favicon-16.png ├── favicon-32.png ├── favicon-57.png ├── favicon-60.png ├── favicon-64.png ├── favicon-70.png ├── favicon-72.png ├── favicon-76.png ├── favicon-96.png ├── favicon-114.png ├── favicon-120.png ├── favicon-144.png ├── favicon-150.png ├── favicon-152.png ├── favicon-160.png ├── favicon-180.png ├── favicon-192.png ├── favicon-310.png ├── AVHersheySimplexMedium.otf ├── how-to │ ├── abugida2 │ │ ├── upload-d.gif │ │ ├── orthography.png │ │ └── phadkoreut.gif │ └── syllabry1 │ │ ├── kakonanotato.gif │ │ └── orthography.png └── sitemap.xml ├── components ├── images.module.scss ├── header.module.scss ├── options.module.scss ├── print.module.scss ├── links.module.scss ├── footer.module.scss ├── dropzone.module.scss ├── label.js ├── description.module.scss ├── variant.js ├── badge.js ├── badge.module.scss ├── substitution-editor.module.scss ├── images.js ├── description.js ├── header.js ├── button.module.scss ├── footer.js ├── button.js ├── preview.module.scss ├── options.js ├── glyph-preview.module.scss ├── glyph-grid.module.scss ├── links.js ├── github-corner.js ├── typography.module.scss ├── head.js ├── slider-toggle.module.scss ├── typography.js ├── text.module.scss ├── dropzone.js ├── glyph-preview.js ├── preview.js ├── substitution-editor.js ├── glyph-grid.js ├── slider.js └── text.js ├── pages ├── _app.js ├── app.scss ├── about.js ├── privacy.js ├── usage.js ├── error.module.scss ├── _error.js └── index.module.scss ├── multiple-error.js ├── tools ├── read-calt.js ├── apply-substitutions.js └── extract.js ├── SOURCES ├── .gitignore ├── .babelrc ├── .github └── ISSUE_TEMPLATE │ ├── rendering-issue.md │ ├── feature_request.md │ └── bug_report.md ├── next.config.js ├── docs ├── privacy.md ├── about.md ├── todo │ └── vowel-contextual.md └── how-to.md ├── layouts └── markdown.js ├── LICENSE ├── README.md ├── WISHLIST.md ├── theme ├── _interaction.scss └── _colors.scss ├── CHANGELOG.md ├── package.json └── icons └── conscripter.svg /.gitattributes: -------------------------------------------------------------------------------- 1 | *.otf binary -------------------------------------------------------------------------------- /tests/non-json.json: -------------------------------------------------------------------------------- 1 | { "this" is not json -------------------------------------------------------------------------------- /state/keys.js: -------------------------------------------------------------------------------- 1 | export const 2 | KEY_FONTS = 'fonts' 3 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: /404.html -------------------------------------------------------------------------------- /tests/simple-saved.json: -------------------------------------------------------------------------------- 1 | {"substitutions":[],"fontname":"My Custom Font"} -------------------------------------------------------------------------------- /state/reducers/list.js: -------------------------------------------------------------------------------- 1 | export { fonts } from './fonts' 2 | export * from './substitution' 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-16.png -------------------------------------------------------------------------------- /static/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-32.png -------------------------------------------------------------------------------- /static/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-57.png -------------------------------------------------------------------------------- /static/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-60.png -------------------------------------------------------------------------------- /static/favicon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-64.png -------------------------------------------------------------------------------- /static/favicon-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-70.png -------------------------------------------------------------------------------- /static/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-72.png -------------------------------------------------------------------------------- /static/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-76.png -------------------------------------------------------------------------------- /static/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-96.png -------------------------------------------------------------------------------- /static/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-114.png -------------------------------------------------------------------------------- /static/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-120.png -------------------------------------------------------------------------------- /static/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-144.png -------------------------------------------------------------------------------- /static/favicon-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-150.png -------------------------------------------------------------------------------- /static/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-152.png -------------------------------------------------------------------------------- /static/favicon-160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-160.png -------------------------------------------------------------------------------- /static/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-180.png -------------------------------------------------------------------------------- /static/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-192.png -------------------------------------------------------------------------------- /static/favicon-310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/favicon-310.png -------------------------------------------------------------------------------- /components/images.module.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | display: block; 3 | height: 76px; 4 | width: 76px; 5 | margin: auto; 6 | } -------------------------------------------------------------------------------- /static/AVHersheySimplexMedium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/AVHersheySimplexMedium.otf -------------------------------------------------------------------------------- /static/how-to/abugida2/upload-d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/how-to/abugida2/upload-d.gif -------------------------------------------------------------------------------- /tests/abugida2/Abugida2 Test PDF.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/tests/abugida2/Abugida2 Test PDF.pdf -------------------------------------------------------------------------------- /state/status.js: -------------------------------------------------------------------------------- 1 | export const STATUS_OK = 'ok' 2 | export const STATUS_ERROR = 'error' 3 | export const STATUS_PENDING = 'pending' 4 | -------------------------------------------------------------------------------- /static/how-to/abugida2/orthography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/how-to/abugida2/orthography.png -------------------------------------------------------------------------------- /static/how-to/abugida2/phadkoreut.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/how-to/abugida2/phadkoreut.gif -------------------------------------------------------------------------------- /static/how-to/syllabry1/kakonanotato.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/how-to/syllabry1/kakonanotato.gif -------------------------------------------------------------------------------- /static/how-to/syllabry1/orthography.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dougrich/conscripter/HEAD/static/how-to/syllabry1/orthography.png -------------------------------------------------------------------------------- /state/encoders/index.js: -------------------------------------------------------------------------------- 1 | import JSON from './json' 2 | import MSGPACK from './msgpack' 3 | 4 | export default { 5 | JSON, 6 | MSGPACK 7 | } 8 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import './app.scss' 2 | 3 | export default function App ({ Component, pageProps }) { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /state/index.js: -------------------------------------------------------------------------------- 1 | import createStore from './store' 2 | import * as actions from './actions' 3 | 4 | export { 5 | createStore, 6 | actions 7 | } 8 | -------------------------------------------------------------------------------- /tests/parsePath/quadratic-bezier.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /components/header.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | max-width: 500px; 3 | padding: 1em; 4 | margin: auto; 5 | width: 100%; 6 | box-sizing: border-box; 7 | } -------------------------------------------------------------------------------- /components/options.module.scss: -------------------------------------------------------------------------------- 1 | .label { 2 | font-size: 0.8em; 3 | } 4 | 5 | .optionslist { 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: space-between; 9 | } -------------------------------------------------------------------------------- /multiple-error.js: -------------------------------------------------------------------------------- 1 | export default class MultipleErrors extends Error { 2 | constructor (message, details) { 3 | super(message) 4 | this.details = details 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/parsePath/arc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/curve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/standing-s-curve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/error-no-root.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/rectangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/relative.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/print.module.scss: -------------------------------------------------------------------------------- 1 | .only { 2 | display: none; 3 | } 4 | 5 | @media print { 6 | .only { 7 | display: block; 8 | } 9 | 10 | .none { 11 | display: none!important; 12 | } 13 | } -------------------------------------------------------------------------------- /tests/parsePath/curved-s-curve.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tools/read-calt.js: -------------------------------------------------------------------------------- 1 | const opentype = require('opentype.js') 2 | 3 | const font = opentype.loadSync(process.argv[2]) 4 | 5 | console.log(JSON.stringify(font.names, null, ' ')) 6 | 7 | // font.download('read.otf') 8 | -------------------------------------------------------------------------------- /pages/app.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | box-sizing: border-box; 4 | margin: 0; 5 | padding: 0; 6 | font-size: 16pt; 7 | min-width: 350px; 8 | } 9 | 10 | img { 11 | max-width: 100%; 12 | } -------------------------------------------------------------------------------- /tests/parsePath/warn-empty-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/warn-non-empty-stroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/parsePath/transform-scale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /components/links.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_interaction'; 2 | 3 | .block { 4 | display: block; 5 | text-decoration: none; 6 | outline: none; 7 | display: inline-block; 8 | 9 | @extend %clickable-text; 10 | } -------------------------------------------------------------------------------- /pages/about.js: -------------------------------------------------------------------------------- 1 | import about from '../docs/about.md' 2 | import MarkdownPage from '../layouts/markdown' 3 | 4 | export default function About () { 5 | return ( 6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /tests/parsePath/transform-rotate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pages/privacy.js: -------------------------------------------------------------------------------- 1 | import terms from '../docs/privacy.md' 2 | import MarkdownPage from '../layouts/markdown' 3 | 4 | export default function Terms () { 5 | return ( 6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /pages/usage.js: -------------------------------------------------------------------------------- 1 | import howTo from '../docs/how-to.md' 2 | import MarkdownPage from '../layouts/markdown' 3 | 4 | export default function Terms () { 5 | return ( 6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /SOURCES: -------------------------------------------------------------------------------- 1 | static/AVHersheySimplexMedium.otf - licensed under CC0/WTFPL, sourced from https://github.com/scruss/AVHershey-OTF 2 | 3 | components/github-corner.js - licensed under MIT, raw corner markup is sourced from http://tholman.com/github-corners/ -------------------------------------------------------------------------------- /tests/parsePath/two-parts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/parsePath/zero-xy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | # next.js build output 15 | .next 16 | out -------------------------------------------------------------------------------- /components/footer.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | 3 | .footer { 4 | @include text-color(theme-color('alert-default')); 5 | } 6 | 7 | .container { 8 | margin: auto; 9 | max-width: 500px; 10 | padding: 1em; 11 | text-align: center; 12 | } -------------------------------------------------------------------------------- /tests/parsePath/comments.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/parsePath/viewbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/parsePath/warn-once-empty-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/parsePath/transform-chain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/parsePath/opacity.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/syllabry1/README.md: -------------------------------------------------------------------------------- 1 | # Syllabry 1 2 | 3 | Syllabry 1 is a simple syllabry, written vertically top to bottom, left to right. 4 | 5 | It can optionally be written left to right, similar to Japanese. 6 | 7 | This syllabry contains six syllables: `ka`, `ta`, `na`, `ko`, `to`, and `no`. -------------------------------------------------------------------------------- /components/dropzone.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | @import '../theme/_interaction'; 3 | 4 | .container { 5 | display: block; 6 | padding: 1em; 7 | text-align: center; 8 | 9 | @include clickable-area('btn-default'); 10 | } 11 | 12 | .input { 13 | display: none; 14 | } -------------------------------------------------------------------------------- /tests/parsePath/nested.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["next/babel"] 5 | }, 6 | "production": { 7 | "presets": ["next/babel"] 8 | }, 9 | "test": { 10 | "presets": [["next/babel", { "preset-env": { "modules": "commonjs" } }]] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /components/label.js: -------------------------------------------------------------------------------- 1 | import Typography from './typography' 2 | 3 | export default function Label (props) { 4 | const { 5 | children, 6 | htmlFor 7 | } = props 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pages/error.module.scss: -------------------------------------------------------------------------------- 1 | .error { 2 | padding-top: 2em; 3 | } 4 | 5 | .code { 6 | border-right: 1px solid black; 7 | padding: 1em; 8 | margin: 1em; 9 | font-size: 2em; 10 | } 11 | 12 | .container { 13 | display: flex; 14 | } 15 | 16 | .text { 17 | margin: 1em 0em; 18 | padding: 1em 0em; 19 | line-height: 1.2em; 20 | } -------------------------------------------------------------------------------- /state/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, combineReducers } from 'redux' 2 | import thunk from 'redux-thunk' 3 | 4 | import * as reducers from './reducers/list' 5 | 6 | export default function CreateStore () { 7 | return createStore( 8 | combineReducers(reducers), 9 | applyMiddleware(thunk) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /components/description.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | 3 | .description { 4 | padding: 0.7em 1em; 5 | z-index: 1; 6 | margin-top: 0.5em; 7 | margin-bottom: 2em; 8 | } 9 | 10 | .danger { 11 | @include text-color(theme-color('alert-danger')); 12 | } 13 | 14 | .default { 15 | @include text-color(theme-color('alert-default')); 16 | } -------------------------------------------------------------------------------- /components/variant.js: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | export const defaultVariant = 'default' 4 | 5 | export function variant (cases) { 6 | return (props) => { 7 | const { variant } = props 8 | const component = cases[variant || defaultVariant] 9 | assert(!!component, 'Undefined variant: ' + variant) 10 | return component(props) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/parsePath/transform.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /components/badge.js: -------------------------------------------------------------------------------- 1 | import css from './badge.module.scss' 2 | import * as cx from 'classnames' 3 | 4 | export default function Badge (props) { 5 | const { 6 | variant = 'default', 7 | className, 8 | children 9 | } = props 10 | return ( 11 | 12 | {children} 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/badge.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_interaction'; 2 | 3 | .badge { 4 | margin: -0.5em 0em; 5 | padding: 0.25em 0.5em; 6 | pointer-events: none; 7 | } 8 | 9 | .default { 10 | @include clickable-area('btn-default'); 11 | } 12 | 13 | .success { 14 | @include clickable-area('btn-success'); 15 | } 16 | 17 | .danger { 18 | @include clickable-area('btn-danger'); 19 | } -------------------------------------------------------------------------------- /components/substitution-editor.module.scss: -------------------------------------------------------------------------------- 1 | .preview { 2 | font-size: 5em; 3 | } 4 | 5 | .actions { 6 | padding-top:1em; 7 | display: flex; 8 | justify-content: flex-end; 9 | } 10 | 11 | .movement { 12 | margin-bottom: 1em; 13 | display: flex; 14 | justify-content: space-between; 15 | } 16 | 17 | .container { 18 | max-width: 500px; 19 | margin: auto; 20 | width: 100%; 21 | } -------------------------------------------------------------------------------- /tools/apply-substitutions.js: -------------------------------------------------------------------------------- 1 | const { applySubstitutions } = require('../state/reducers/fonts/assembleDataUri') 2 | const fs = require('fs') 3 | const opentype = require('opentype.js') 4 | 5 | const font = opentype.loadSync('./static/AVHersheySimplexMedium.otf') 6 | 7 | const subs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) 8 | 9 | applySubstitutions(font, subs) 10 | 11 | font.download('demo.otf') 12 | -------------------------------------------------------------------------------- /components/images.js: -------------------------------------------------------------------------------- 1 | import css from './images.module.scss' 2 | 3 | function Image ( 4 | src, 5 | className, 6 | alt 7 | ) { 8 | return () => ( 9 | {alt} 14 | ) 15 | } 16 | 17 | const Images = { 18 | Logo: Image('/static/favicon-144.png', css.logo, 'Conscripter Logo') 19 | } 20 | 21 | export default Images 22 | -------------------------------------------------------------------------------- /components/description.js: -------------------------------------------------------------------------------- 1 | import css from './description.module.scss' 2 | import * as cx from 'classnames' 3 | import Typography from './typography' 4 | 5 | export default function Description ({ 6 | variant, 7 | className, 8 | children 9 | }) { 10 | return ( 11 | 14 | {children} 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /tests/parsePath/warn-complex-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rendering-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rendering Issue 3 | about: Report an issue rendering either SVG or font 4 | title: '' 5 | labels: bug, rendering 6 | assignees: dougrich 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior, as well as any SVGs, output .OTFs, workspace .json, or screenshots you can provide. 15 | 16 | **Context** 17 | - browser version 18 | - operating system version 19 | - any relevant plugins 20 | -------------------------------------------------------------------------------- /components/header.js: -------------------------------------------------------------------------------- 1 | import css from './header.module.scss' 2 | import Links from './links' 3 | import Typography from './typography' 4 | 5 | export default function Header () { 6 | return ( 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /state/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const FETCH_FONTS = 'fetch_font' 2 | export const DOWNLOAD = 'download' 3 | export const SET_FONTNAME = 'set_fontname' 4 | export const ADD_SUBSTITUTION = 'add_substitution' 5 | export const SWAP_SUBSTITUTION = 'swap_substitution' 6 | export const REMOVE_SUBSTITUTION = 'remove_substitution' 7 | export const SELECT_SUBSTITUTION = 'select_substitution' 8 | export const UPDATE_SUBSTITUTION = 'update_substitution' 9 | export const CANCEL_SUBSTITUTION = 'cancel_substitution' 10 | export const SAVE = 'save' 11 | export const LOAD = 'load' 12 | export const CLEAR = 'clear' 13 | -------------------------------------------------------------------------------- /tests/e2e/illustrator/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /components/button.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_interaction'; 2 | 3 | .btn { 4 | margin: 0.2rem; 5 | padding: 0.5rem 0.8rem; 6 | width: 12rem; 7 | text-align: center; 8 | position: relative; 9 | } 10 | 11 | .default { 12 | @include clickable-area('btn-default'); 13 | } 14 | 15 | .success { 16 | @include clickable-area('btn-success'); 17 | } 18 | 19 | .danger { 20 | @include clickable-area('btn-danger'); 21 | } 22 | 23 | .action { 24 | border-radius: 100%; 25 | width: 3em; 26 | height: 3em; 27 | box-sizing: border-box; 28 | @include clickable-area('btn-default'); 29 | } 30 | 31 | .bar { 32 | display: flex; 33 | margin: -0.2rem; 34 | margin-bottom: 1em; 35 | } -------------------------------------------------------------------------------- /components/footer.js: -------------------------------------------------------------------------------- 1 | import css from './footer.module.scss' 2 | import * as cx from 'classnames' 3 | import Print from './print.module.scss' 4 | import Links from './links' 5 | import Typography from './typography' 6 | 7 | export default function Footer () { 8 | return ( 9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /state/reducers/clearable.js: -------------------------------------------------------------------------------- 1 | import { CLEAR } from '../actionTypes' 2 | 3 | const defaultOptions = { 4 | only: null 5 | } 6 | 7 | export default function clearable (defaultState, options = defaultOptions) { 8 | return reducer => { 9 | return (state = defaultState, action) => { 10 | if (action && action.type === CLEAR) { 11 | if (options.only) { 12 | const next = { ...state } 13 | for (const field of options.only) { 14 | next[field] = defaultState[field] 15 | } 16 | return next 17 | } else { 18 | return defaultState 19 | } 20 | } else { 21 | return reducer(state, action) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: dougrich 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | const ISPRODUCTION = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | cssModules: true, 8 | assetPrefix: ISPRODUCTION ? '/conscripter/' : '', 9 | cssLoaderOptions: { 10 | importLoaders: 1, 11 | localIdentName: 'c[hash:base64:10]' 12 | }, 13 | sassLoaderOptions: { 14 | includePaths: [path.resolve(__dirname, 'theme')] 15 | }, 16 | webpack: config => { 17 | config.module.rules.push({ 18 | test: /\.md$/, 19 | use: 'raw-loader' 20 | }) 21 | config.plugins.push(new webpack.DefinePlugin({ 22 | BASE_LINK: ISPRODUCTION ? '"/conscripter"' : '""' 23 | })) 24 | return config 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | import css from './button.module.scss' 2 | import * as cx from 'classnames' 3 | import Typography from './typography' 4 | 5 | export default function Button (props) { 6 | const { 7 | variant = 'default', 8 | className, 9 | children 10 | } = props 11 | 12 | let child = ( 13 | {children} 14 | ) 15 | 16 | if (variant === 'action') { 17 | child = ( 18 | {children} 19 | ) 20 | } 21 | 22 | return ( 23 | 26 | ) 27 | } 28 | 29 | export function ButtonBar ({ children }) { 30 | return ( 31 |
32 | {children} 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/preview.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | width: 100%; 3 | margin: -1em; 4 | padding: 1em; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | margin-top: 0em; 9 | transition: background-color 150ms ease-in-out, color 150ms ease-in-out; 10 | } 11 | 12 | .previewText { 13 | display: block; 14 | width: 100%!important; 15 | padding: 0.25em; 16 | box-sizing: border-box; 17 | background-color: transparent; 18 | min-height: 6em; 19 | outline: none; 20 | color: inherit; 21 | } 22 | 23 | .inverted { 24 | background-color: #222; 25 | color: white; 26 | } 27 | 28 | .formcontainer { 29 | margin-bottom: 2em; 30 | } 31 | 32 | .inlinedescription { 33 | margin-bottom: 0em!important; 34 | } 35 | 36 | @media print { 37 | .previewText { 38 | min-height: 80vh; 39 | } 40 | } -------------------------------------------------------------------------------- /docs/privacy.md: -------------------------------------------------------------------------------- 1 | ## Privacy Policy 2 | 3 | __Last Updated: April 17, 2019__ 4 | 5 | This app does not collect or store your data in anyway. No data that you send is transmitted to the server beyond the initial requests sent from your browser to load the page. All information you provide to this app remains on your computer. 6 | 7 | While editing, any information you have given to the app will be stored locally in your browser's `LocalStorage`, a cache located on your computer. This is to provide you the ability to jump in and out, accidentally close the tab, and recover from crashes. Please consult your browser provider for more information about `LocalStorage`. 8 | 9 | Clicking `Save` will download a `.json` to your computer, which contains a plain text representation of the data you have given to the app. You can then manually `Load` this data back into the app. -------------------------------------------------------------------------------- /layouts/markdown.js: -------------------------------------------------------------------------------- 1 | import Head from '../components/head' 2 | import Header from '../components/header' 3 | import Markdown from 'react-markdown' 4 | import css from '../pages/index.module.scss' 5 | import Footer from '../components/footer' 6 | import GithubCorner from '../components/github-corner' 7 | import Typography from '../components/typography' 8 | 9 | export default function MarkdownPage ({ 10 | title, 11 | source 12 | }) { 13 | return ( 14 |
15 | 16 | 17 |
18 | 19 | BASE_LINK + x} /> 20 | 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /static/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | https://dougrich.github.io/conscripter 7 | 2020-04-02 8 | weekly 9 | 0.8 10 | 11 | 12 | https://dougrich.github.io/conscripter/about 13 | 2019-04-11 14 | monthly 15 | 0.8 16 | 17 | 18 | https://dougrich.github.io/conscripter/privacy 19 | 2019-04-11 20 | yearly 21 | 0.8 22 | 23 | -------------------------------------------------------------------------------- /pages/_error.js: -------------------------------------------------------------------------------- 1 | import Head from '../components/head' 2 | import Header from '../components/header' 3 | import css from '../pages/index.module.scss' 4 | import Footer from '../components/footer' 5 | import GithubCorner from '../components/github-corner' 6 | import css2 from './error.module.scss' 7 | import * as cx from 'classnames' 8 | 9 | export default function ErrorPage () { 10 | return ( 11 |
12 | 13 | 14 |
15 |
16 |
17 |
404
18 |
Sorry, it seems you've gone a bit off track. Consider raising an issue on Github if you think this is a mistake.
19 |
20 |
21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /state/encoders/json.js: -------------------------------------------------------------------------------- 1 | export default { 2 | match: (data) => { 3 | return data.indexOf('data:text/json;charset=utf-8;base64,') === 0 || data[0] === '{' 4 | }, 5 | encode: ({ fonts: { substitutions, fontname } }) => { 6 | return 'data:text/json;charset=utf-8;base64,' + window.btoa(JSON.stringify({ version: '0.1.1', substitutions, fontname })) 7 | }, 8 | decode: (data) => { 9 | if (data[0] === '{') { 10 | const { substitutions = [], fontname = 'My Custom Font' } = JSON.parse(data) 11 | return { 12 | substitutions, 13 | fontname 14 | } 15 | } else if (data.indexOf('data:text/json;charset=utf-8;base64,') === 0) { 16 | const { substitutions = [], fontname = 'My Custom Font' } = JSON.parse(window.atob(data.slice('data:text/json;charset=utf-8;base64,'.length))) 17 | return { 18 | substitutions, 19 | fontname 20 | } 21 | } else { 22 | throw new Error('JSON Decode does not have handling for this case') 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: dougrich 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /state/reducers/fonts/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions */ 2 | 3 | import { expect } from 'chai' 4 | import { reassembleDataUri } from './index' 5 | 6 | describe('reassembleDataUri', () => { 7 | const assemble = () => ({ reassembled: true }) 8 | 9 | it('does not rebuild if no changes occur', () => { 10 | const state = { buffer: true, substitutions: [], reassembled: false } 11 | const reducer = state => state 12 | const final = reassembleDataUri(assemble)(reducer) 13 | const result = final(state, {}) 14 | expect(result.reassembled).to.be.false 15 | }) 16 | 17 | it('rebuilds if any substitution change occurs', () => { 18 | const state = { buffer: true, substitutions: [], reassembled: false } 19 | const reducer = () => ({ buffer: true, substitutions: [], reassembled: false }) 20 | const final = reassembleDataUri(assemble)(reducer) 21 | const result = final(state, {}) 22 | expect(result.reassembled).to.be.true 23 | }) 24 | }) 25 | 26 | /* eslint-enable no-unused-expressions */ 27 | -------------------------------------------------------------------------------- /components/options.js: -------------------------------------------------------------------------------- 1 | import css from './options.module.scss' 2 | 3 | import Label from './label' 4 | import { Toggle } from './slider' 5 | 6 | function Option ({ 7 | field, 8 | value, 9 | disabled, 10 | label, 11 | onChange 12 | }) { 13 | return ( 14 | 15 | ) 16 | } 17 | 18 | export default function Options ({ 19 | value, 20 | optionLabels = {}, 21 | lock = {}, 22 | onChange: propsOnChange, 23 | label 24 | }) { 25 | const options = Object.keys(value) 26 | const onChange = (field, checked) => { 27 | propsOnChange({ 28 | ...value, 29 | [field]: checked 30 | }) 31 | } 32 | 33 | return ( 34 |
35 | 36 |
37 | {options.map((o) => ( 38 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 dougrich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # conscripter 2 | 3 | Static site that generates functional open type font files for conlang scripts from SVG using substitution. 4 | 5 | Conlangs (or 'constructed languages') frequently feature in all kinds of media. Sindarin, one of the languages of elves constructed by J. R. R. Tolkien is a particularily well known example. Equally as recognizable is Tengwar - the script that features heavily in the movies. 6 | 7 | Creating a script for a conlang is a complex process, but one part which should not be that complex is actually getting a computer to display your script. Unfortunately that has been relatively technical up until now, accomplished either by using font foundry oriented tools like Font Forge or Font Lab __or__ by using _shudder_ raster images. 8 | 9 | Conscripter is a simple-to-use `calt` insertion program, specialized to the topic of making nice designer fonts for conlang scripts. 10 | 11 | # Concepts & Getting Started 12 | 13 | __TL:DR;__ 14 | 15 | 1. Break your conlang script into a series of glyphs. 16 | 2. Draw each glyph using your favorite SVG tool. 17 | 3. Upload each glyph and give it a substitution. 18 | 4. Download your font and use it in Word or Adobe. 19 | 5. Report issues here. -------------------------------------------------------------------------------- /components/glyph-preview.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | width: 100%; 4 | white-space: nowrap; 5 | margin: auto; 6 | height: 1.5em; 7 | text-align: center; 8 | max-width: 2em; 9 | overflow: hidden; 10 | } 11 | 12 | .subcontainer { 13 | position: absolute; 14 | top: 10%; 15 | left: 50%; 16 | width: 1000px; 17 | box-sizing: border-box; 18 | } 19 | 20 | .textcontainer { 21 | display: block; 22 | position: absolute; 23 | top: 10%; 24 | left: 50%; 25 | transform: translate(-50%, 0%); 26 | } 27 | 28 | .glyph { 29 | display: inline-block; 30 | overflow: visible; 31 | vertical-align: bottom; 32 | z-index: 1; 33 | flex-basis: 0; 34 | top: -0.1em; 35 | position: relative; 36 | } 37 | 38 | .rule { 39 | height: 1px; 40 | position: absolute; 41 | left: 0; 42 | right: 0; 43 | opacity: 1; 44 | } 45 | 46 | .faded { 47 | opacity: 0.3; 48 | width: 3em; 49 | display: inline-block; 50 | text-overflow: clip; 51 | white-space: nowrap; 52 | vertical-align: top; 53 | } 54 | 55 | .edges { 56 | position: absolute; 57 | top: 0; 58 | left: 0; 59 | right: 0; 60 | bottom: 0; 61 | pointer-events: none; 62 | // background: linear-gradient(to right, white 5%, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, 0) 60%, white 95%); 63 | } -------------------------------------------------------------------------------- /state/reducers/substitution.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { substitution as reducer } from './substitution' 3 | import { SELECT_SUBSTITUTION } from '../actionTypes' 4 | 5 | describe('substitution', () => { 6 | it('uses last advance width manually set if no advance width is set', () => { 7 | const state = reducer() 8 | state.lastAdvanceWidth = 592 9 | const substitution = { 10 | glyph: { 11 | stuff: '1234' 12 | }, 13 | replace: ['a'] 14 | } 15 | const updated = reducer(state, { 16 | type: SELECT_SUBSTITUTION, 17 | substitution 18 | }) 19 | expect(updated.active).to.equal(substitution) 20 | expect(updated.currentGlyph.advanceWidth).to.equal(state.lastAdvanceWidth) 21 | }) 22 | 23 | it('uses current advance width if an advance width is set', () => { 24 | const state = reducer() 25 | state.lastAdvanceWidth = 592 26 | const substitution = { 27 | glyph: { 28 | stuff: '1234', 29 | advanceWidth: 999 30 | }, 31 | replace: ['a'] 32 | } 33 | const updated = reducer(state, { 34 | type: SELECT_SUBSTITUTION, 35 | substitution 36 | }) 37 | expect(updated.active).to.equal(substitution) 38 | expect(updated.currentGlyph.advanceWidth).to.equal(substitution.glyph.advanceWidth) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /WISHLIST.md: -------------------------------------------------------------------------------- 1 | # Feature Wishlist 2 | 3 | - [ ] Negative advance width to place diacritics on the previous character 4 | - [ ] Repositioning glyphs after upload (i.e. shift up, shift down) 5 | - [ ] True contextual alternates (see [./docs/todo/vowel-contextual.md](Contextual Vowel Symbols)) 6 | - [ ] Path tracing, i.e. instead of using the fill of a path use the stroke when the fill is transparent 7 | - [ ] Better support for transforms - notably skew, rotate 8 | - [ ] Better support for curves, especially `S` curves 9 | - [ ] Support for SVG arcs. See [https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths#Arcs](Relevant MDN) 10 | - [ ] Support for symbols. 11 | - [ ] Support for clipping/masks. 12 | - [ ] Support for relative paths. 13 | - [ ] Support for cloud saving. 14 | - [ ] Support for logging in. 15 | - [ ] Support for project workspaces, keeping multiple fonts active for a single user. 16 | - [ ] Support for scanning a JPG or PNG and pulling the symbol out of it. 17 | - [ ] Proper keyboard shortcuts - specifically ctrl+z, ctrl+y, ctrl+s 18 | - [ ] Explicit support for syllabry, abugidas 19 | - [ ] UX support for case insensitivity, multiple replacements for a single glyph 20 | - [ ] Slider keyboard shortcuts 21 | - [ ] Support for quick positioning corrections 22 | - [ ] Snap points on the slider 23 | - [ ] Vertical orthographies 24 | - [ ] Right to left orthographies -------------------------------------------------------------------------------- /theme/_interaction.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | 3 | $border-thickness: 0.05em; 4 | 5 | %common-clickable { 6 | transition-property: background-color, border-color, color, transform; 7 | transition-duration: 250ms; 8 | cursor: pointer; 9 | outline: none; 10 | } 11 | 12 | @mixin section-tab { 13 | border: $border-thickness*2 solid theme-color('border'); 14 | border-bottom-color: white; 15 | position: relative; 16 | z-index: 1; 17 | } 18 | 19 | @mixin section-border { 20 | margin-top: -$border-thickness; 21 | border-top: $border-thickness solid theme-color('border'); 22 | border-bottom: $border-thickness solid theme-color('border'); 23 | } 24 | 25 | %clickable-text { 26 | @extend %common-clickable; 27 | @include colors('link', 'default'); 28 | 29 | &:hover, &:focus { 30 | text-decoration: underline; 31 | } 32 | 33 | &:focus { 34 | @include colors('link', 'focus'); 35 | } 36 | } 37 | 38 | @mixin clickable-area($color: 'btn-default') { 39 | @extend %common-clickable; 40 | @include colors($color, 'default'); 41 | &:hover, &:focus { 42 | transform: translate(0, -0.1rem); 43 | } 44 | &:active { 45 | transition-duration: 100ms; 46 | transform: translate(0, 0.1rem); 47 | } 48 | &:hover { 49 | @include colors($color, 'hover'); 50 | } 51 | &:focus { 52 | @include colors($color, 'focus'); 53 | } 54 | } -------------------------------------------------------------------------------- /components/glyph-grid.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_interaction'; 2 | 3 | .container { 4 | height: 4em; 5 | padding: 0; 6 | font: inherit; 7 | width: 4em; 8 | box-sizing: border-box; 9 | text-align: center; 10 | align-items: flex-start; 11 | display: block; 12 | @include clickable-area('btn-transparent'); 13 | } 14 | 15 | .inactive { 16 | opacity: 0.25; 17 | } 18 | 19 | .disabled { 20 | pointer-events: none; 21 | } 22 | 23 | .activecell { 24 | @include section-tab; 25 | } 26 | 27 | .label { 28 | text-align: center; 29 | } 30 | 31 | .preview { 32 | font-size: 2em; 33 | } 34 | 35 | .grid { 36 | position: relative; 37 | height: 100%; 38 | overflow: auto; 39 | min-height: 6em; 40 | } 41 | 42 | .details { 43 | display: block; 44 | padding: 1em; 45 | box-sizing: border-box; 46 | @include section-border; 47 | } 48 | 49 | .detailsspacer, .details { 50 | height: auto; 51 | } 52 | 53 | .detailsspacer { 54 | float: left; 55 | width: 100%; 56 | } 57 | 58 | .gridcell { 59 | width: 4em; 60 | display: inline-block; 61 | vertical-align: top; 62 | } 63 | 64 | .emptyset { 65 | position: absolute; 66 | top: 50%; 67 | left: 50%; 68 | transform: translate(-50%, -50%); 69 | } 70 | 71 | .textcell { 72 | @extend .gridcell; 73 | height: 4em; 74 | padding-top: 1.5em; 75 | box-sizing: border-box; 76 | } 77 | 78 | @media print { 79 | .grid { 80 | height: auto; 81 | overflow: visible; 82 | } 83 | } -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | ## What is Conscripter? 2 | 3 | Conscripter is a tool that helps _conlang scripts_ come to reality using _substitution_. 4 | 5 | __TL;DR:__ Conscripter takes SVG and puts it into a font that you can use. 6 | 7 | Conlang scripts often differ widely from the latin alphabet, and a custom script can add depth and meaning to your conlang. Ideally, a custom font makes this easy to work with. 8 | 9 | Unfortunately, creating a custom font is a significant undertaking, especially if your language does not directly map onto the latin alphabet. Consider trying to make a logogram like Egyptian Hieroglyphs, a syllabary like Japanese, or an alphasyllabary like Hangul. Not every conlang warrants a block of unicode like J R R Tolkien's Tengwar. 10 | 11 | Conscripter is intended to simplify that process. Upload your glyph as an SVG and specify what text it replaces. Download font. 12 | 13 | ### Who is this for? 14 | 15 | Anybody trying to make a font for their conlang that they can use in tools like Word, Illustrator, Photoshop, their website, or Inkscape. 16 | 17 | ### Who is this not for? 18 | 19 | Are you a hardcore typography nerd who fully understands contextual alternates, chaining, and how the substitution stuff works? 20 | 21 | This tool is probably too limited for you - you might get more mileage from something like Font Forge, an excellent tool that I used to check my work here. 22 | 23 | Additionally, I'd really appreciate any feedback you can give on the end result or pointers on how to improve this app to be more useful. -------------------------------------------------------------------------------- /components/links.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { variant, defaultVariant } from './variant' 3 | import Typography from './typography' 4 | import css from './links.module.scss' 5 | const { version } = require('../package.json') 6 | 7 | function href (route) { 8 | if (route[0] === '/') { 9 | return { 10 | href: route, 11 | as: BASE_LINK + route 12 | } 13 | } else { 14 | return { 15 | href: route 16 | } 17 | } 18 | } 19 | 20 | function AnchorLink ({ 21 | route, 22 | className, 23 | children 24 | }) { 25 | return ( 26 | {children} 27 | ) 28 | } 29 | 30 | const Home = variant({ 31 | [defaultVariant]: () => ( 32 | App 33 | ), 34 | header: () => ( 35 | 36 | ) 37 | }) 38 | 39 | const About = () => (About) 40 | const Privacy = () => (Privacy) 41 | const changelogLink = 'https://github.com/dougrich/conscripter/blob/master/CHANGELOG.md' 42 | const Changelog = variant({ 43 | [defaultVariant]: () => (Changelog), 44 | versioned: () => (New in {version}) 45 | }) 46 | 47 | const Links = { 48 | Home, 49 | About, 50 | Privacy, 51 | Changelog, 52 | Usage: () => (How to Use) 53 | } 54 | 55 | export default Links 56 | -------------------------------------------------------------------------------- /docs/todo/vowel-contextual.md: -------------------------------------------------------------------------------- 1 | ## Contextual Vowel Symbols 2 | 3 | ### Specific Case 4 | 5 | Contaxia, the example language, is an abugida where exach consonant has a symbol, and the vowel that follows it is placed either above or below the consonant symbol. 6 | 7 | For example, `ha` is written as `ħ`, and `na` is written `ꞥ`. 8 | 9 | Consonants are broken into three `classes`: 'diagonal bar', 'low bar' and 'high bar'. The class determines the position that the vowel symbol appears. 10 | 11 | ### Current way to accomplish this 12 | 13 | 1. Create the `h` symbol, devoid of any vowel 14 | 2. Create the `n` symbol, devoid of any vowel 15 | 3. Create the bar symbol for the `a` vowel, devoid of any consonant 16 | 4. Create the `ħ` symbol as a substitution for `ha` with a high specificity than the `h` or `a` substitutions 17 | 5. Create the `ꞥ` symbol as a substitution for `na` 18 | 19 | Notably this creates a bad combinatorics problem - with the english alphabet alone there would be over a hundred symbols. 20 | 21 | ### How to ideally do this 22 | 23 | 1. Create the `h` symbol, devoid of any vowel 24 | 2. Create the `n` symbol, devoid of any vowel 25 | 3. Create the bar symbol for the `a` vowel, devoid of any consonant 26 | 4. Specify that if `a` is prefixed by the 'high bar' class, it should use the `a` symbol but shifted up. 27 | 5. Specify the 'high bar' class of letters. 28 | 6. Specify that if `a` is prefixed by the 'diagonal bar' class, it should use a different symbol. 29 | 30 | This inverts the combinatorics problem - instead of a hundred symbols for the english alphabet, it would only need around 30. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All changes will be documented here, with releases being tagged + linked. 4 | 5 | ## 0.3.1 6 | 7 | - #11 - Switched to blob downloads to bypass URL limits on Firefox 8 | 9 | ## 0.3.0 10 | 11 | - #12 - local copy stashed in localstorage, updated privacy policy 12 | - #10 - advance width is preserved when uploading multiple 13 | - #11 - Added binary encoding, reducing the download size 14 | - #11 - Added snapping, reducing the download size another 20% 15 | - #13 - Added offline tool for extracting + merging fonts 16 | 17 | ## 0.2.0 18 | 19 | - Support `scale` transform option 20 | - Support quadratic curves in paths 21 | - Support for arcs in paths 22 | - Support `rotate` transform option 23 | - Fixed transformation apply order 24 | - #3 - BETA - Vertical preview 25 | - Added syllabry1 tutorial 26 | - #7 - Added explicit diacritic support 27 | - #6 - Removed preview text + lines in the grid 28 | 29 | ## 0.1.1 30 | 31 | - #2 - Form editor now correctly pushes other row entries down the grid 32 | - #4 - Right to left support in the preview 33 | - #8 - Dragging, changing the fontname + others now correctly updates the preview 34 | - #1 - S curves, both standing and from existing curves, now correctly parse 35 | 36 | ## 0.1.0 37 | 38 | Initial release. 39 | 40 | - support for single substitution 41 | - support for multi substitution 42 | - support for SVG parsing of: 43 | - absolute lines 44 | - some relative lines 45 | - curves (via interpolating the curve) 46 | - circles (via interpolating the circle) 47 | - rectangles 48 | - support for exporting custom fonts as `.otf` 49 | - basic tutorial for abugida2 -------------------------------------------------------------------------------- /pages/index.module.scss: -------------------------------------------------------------------------------- 1 | $font-size: 52pt; 2 | .example { 3 | font-size: $font-size; 4 | font-family: demofont, sans-serif; 5 | height: 12em; 6 | width: 100%; 7 | } 8 | 9 | .text { 10 | font-size: 2em; 11 | } 12 | 13 | .root { 14 | font-family: sans-serif; 15 | font-size: 16pt; 16 | margin: auto; 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | box-sizing: border-box; 21 | } 22 | 23 | .actions { 24 | display: flex; 25 | justify-content: flex-end; 26 | align-self: flex-end; 27 | padding: 0.5em; 28 | } 29 | 30 | .workspace { 31 | height: 100%; 32 | flex-grow: 1; 33 | } 34 | 35 | .panel, .textpanel { 36 | padding: 1em; 37 | max-width: 500px; 38 | margin-left: auto; 39 | margin-right: auto; 40 | display: flex; 41 | flex-direction: column; 42 | page-break-after: always; 43 | box-sizing: border-box; 44 | } 45 | 46 | .textpanel { 47 | height: 100%; 48 | width: 100%; 49 | } 50 | 51 | .container { 52 | display: flex; 53 | flex-direction: column; 54 | height: 100%; 55 | box-sizing: border-box; 56 | flex-grow: 1; 57 | } 58 | 59 | .topaction { 60 | max-width: 500px; 61 | padding: 1em; 62 | margin: auto; 63 | width: 100%; 64 | box-sizing: border-box; 65 | } 66 | 67 | .internalpanel { 68 | display: flex; 69 | flex-direction: column; 70 | height: 100%; 71 | } 72 | 73 | @media print { 74 | .panel { 75 | height: auto; 76 | } 77 | } 78 | 79 | @media screen and (min-width: 1000px) { 80 | .workspace { 81 | display: flex; 82 | } 83 | 84 | .panel { 85 | padding: 1em; 86 | width: 50%; 87 | max-width: 50%; 88 | flex-shrink: 0; 89 | box-sizing: border-box; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /state/encoders/msgpack.js: -------------------------------------------------------------------------------- 1 | import * as msgpack from 'msgpack-lite' 2 | 3 | const Glyph = { 4 | name: 'conscripterglyph' 5 | } 6 | 7 | const codec = msgpack.createCodec() 8 | 9 | codec.addExtPacker(0x20, Glyph, ({ commands, advanceWidth, isDiacritic }) => { 10 | const commandBuffer = Buffer.alloc(commands.length * 9) 11 | for (let i = 0; i < commands.length; i++) { 12 | const { type, x, y } = commands[i] 13 | commandBuffer.writeUInt8(type.charCodeAt(0), i * 9 + 0) 14 | commandBuffer.writeFloatLE(x || 0, i * 9 + 1) 15 | commandBuffer.writeFloatLE(y || 0, i * 9 + 5) 16 | } 17 | return msgpack.encode({ commandBuffer, advanceWidth, isDiacritic }) 18 | }) 19 | 20 | codec.addExtUnpacker(0x20, (data) => { 21 | const d = msgpack.decode(data) 22 | const commands = new Array(d.commandBuffer.length / 9) 23 | for (let i = 0; i < commands.length; i++) { 24 | const type = String.fromCharCode(d.commandBuffer.readUInt8(i * 9 + 0)) 25 | const x = d.commandBuffer.readFloatLE(i * 9 + 1) 26 | const y = d.commandBuffer.readFloatLE(i * 9 + 5) 27 | commands[i] = { type, x, y } 28 | } 29 | return { commands, advanceWidth: d.advanceWidth, isDiacritic: d.isDiacritic } 30 | }) 31 | 32 | export default { 33 | match: (data) => { 34 | return data[0] === 131 35 | }, 36 | encode: ({ fonts: { substitutions, fontname } }) => { 37 | substitutions = substitutions.map(s => ({ 38 | ...s, 39 | glyph: { 40 | ...s.glyph, 41 | constructor: Glyph 42 | } 43 | })) 44 | return msgpack.encode({ substitutions, fontname, version: '0.1.0' }, { codec }) 45 | }, 46 | decode: (data) => { 47 | return msgpack.decode(data, { codec }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /components/github-corner.js: -------------------------------------------------------------------------------- 1 | // sourced from http://tholman.com/github-corners/ 2 | 3 | const raw = url => `` 4 | 5 | export default function GithubCorner ({ url }) { 6 | return ( 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /tools/extract.js: -------------------------------------------------------------------------------- 1 | const opentype = require('opentype.js') 2 | const fs = require('fs') 3 | 4 | const font = opentype.loadSync(process.argv[2]) 5 | const extract = [ 6 | 7 | ] 8 | 9 | const capturedWidths = {} 10 | for (const glyph in font.glyphs.glyphs) { 11 | capturedWidths[glyph] = font.glyphs.glyphs[glyph].advanceWidth 12 | } 13 | 14 | for (let i = 0; i < font.tables.gsub.lookups.length; i++) { 15 | const lookup = font.tables.gsub.lookups[i] 16 | switch (lookup.lookupType) { 17 | case 1: { 18 | const replaceCode = lookup.subtables[0].coverage.glyphs[0] 19 | const delta = lookup.subtables[0].deltaGlyphId 20 | const original = String.fromCharCode(font.glyphs.get(replaceCode).unicode) 21 | const replacement = font.glyphs.glyphs[replaceCode + delta] 22 | const commands = replacement.path.commands 23 | extract.push({ 24 | replace: [original], 25 | glyph: { 26 | commands, 27 | advanceWidth: capturedWidths[replaceCode + delta] 28 | } 29 | }) 30 | } 31 | break 32 | case 4: { 33 | const replace = [lookup.subtables[0].coverage.glyphs[0]] 34 | const replacementCode = lookup.subtables[0].ligatureSets[0][0].ligGlyph 35 | replace.push(...lookup.subtables[0].ligatureSets[0][0].components) 36 | const original = replace 37 | .map(x => String.fromCharCode(font.glyphs.get(x).unicode)) 38 | .join('') 39 | 40 | const replacement = font.glyphs.get(replacementCode) 41 | const commands = replacement.path.commands 42 | extract.push({ 43 | replace: [original], 44 | glyph: { 45 | commands, 46 | advanceWidth: capturedWidths[replacementCode] 47 | } 48 | }) 49 | } 50 | break 51 | } 52 | } 53 | 54 | fs.writeFileSync('./extract.json', JSON.stringify(extract)) 55 | 56 | // font.download('read.otf') 57 | -------------------------------------------------------------------------------- /components/typography.module.scss: -------------------------------------------------------------------------------- 1 | %menco-light { 2 | font-family: menco, sans-serif; 3 | font-weight: 500; 4 | font-style: normal; 5 | } 6 | 7 | %menco-bold { 8 | font-family: menco,sans-serif; 9 | font-weight: 700; 10 | font-style: normal; 11 | } 12 | 13 | .basedemofont { 14 | font-family: base-demofont, sans-serif; 15 | } 16 | 17 | .demofont { 18 | font-family: demofont, sans-serif; 19 | } 20 | 21 | .display { 22 | @extend %menco-bold; 23 | text-align: center; 24 | } 25 | 26 | .links { 27 | @extend %menco-light; 28 | display: flex; 29 | justify-content: space-between; 30 | font-size: 0.9rem; 31 | letter-spacing: 0.02rem; 32 | } 33 | 34 | .btn { 35 | @extend %menco-light; 36 | font-size: 0.9rem; 37 | letter-spacing: 0.02rem; 38 | } 39 | 40 | .icon { 41 | width: 3em; 42 | pointer-events: none; 43 | text-align: center; 44 | display: block; 45 | transform: translate(-50%, -50%); 46 | position: absolute; 47 | top: 50%; 48 | left: 50%; 49 | } 50 | 51 | .copyright { 52 | @extend %menco-light; 53 | margin-bottom: 1em; 54 | font-size: 0.9rem; 55 | } 56 | 57 | .small { 58 | font-size: 0.9em; 59 | opacity: 0.6; 60 | } 61 | 62 | .label { 63 | @extend %menco-light; 64 | font-size: 0.8em; 65 | display: block; 66 | margin-bottom: 0.5em; 67 | } 68 | 69 | .sectionheader { 70 | @extend %menco-bold; 71 | font-size: 1em; 72 | } 73 | 74 | .description { 75 | @extend %menco-light; 76 | font-size: 0.8em; 77 | } 78 | 79 | .input { 80 | @extend %menco-light; 81 | font-size: 1em; 82 | } 83 | 84 | .descriptor { 85 | @extend %menco-light; 86 | text-align: center; 87 | font-size: 0.8em; 88 | opacity: 0.6; 89 | } 90 | 91 | .markdown { 92 | h2, h3, h4, h5, h6 { 93 | @extend %menco-bold; 94 | } 95 | 96 | p { 97 | @extend %menco-light; 98 | line-height: 1.5em; 99 | } 100 | } -------------------------------------------------------------------------------- /state/reducers/substitution.js: -------------------------------------------------------------------------------- 1 | import { SELECT_SUBSTITUTION, UPDATE_SUBSTITUTION, CANCEL_SUBSTITUTION } from '../actionTypes' 2 | import clearable from './clearable' 3 | 4 | const defaultState = { 5 | active: null, 6 | currentGlyph: null, 7 | currentReplace: null, 8 | warnings: null, 9 | lastAdvanceWidth: 1000 10 | } 11 | 12 | export const substitution = clearable(defaultState)((state, action) => { 13 | if (action && action.type === SELECT_SUBSTITUTION) { 14 | return { 15 | ...state, 16 | active: action.substitution, 17 | currentGlyph: { 18 | ...action.substitution.glyph, 19 | advanceWidth: action.substitution.glyph.advanceWidth || state.lastAdvanceWidth || 1000 20 | }, 21 | currentReplace: action.substitution.replace[0] 22 | } 23 | } 24 | 25 | if (action && action.type === UPDATE_SUBSTITUTION) { 26 | switch (action.field) { 27 | case 'glyph/commands': 28 | return { 29 | ...state, 30 | currentGlyph: { 31 | ...state.currentGlyph, 32 | commands: action.value 33 | }, 34 | warnings: action.warnings 35 | } 36 | case 'replace': 37 | return { 38 | ...state, 39 | currentReplace: action.value 40 | } 41 | case 'glyph/advanceWidth': 42 | return { 43 | ...state, 44 | currentGlyph: { 45 | ...state.currentGlyph, 46 | advanceWidth: action.value 47 | }, 48 | lastAdvanceWidth: action.value 49 | } 50 | case 'glyph/diacritic': 51 | return { 52 | ...state, 53 | currentGlyph: { 54 | ...state.currentGlyph, 55 | isDiacritic: action.value 56 | } 57 | } 58 | } 59 | } 60 | 61 | if (action && action.type === CANCEL_SUBSTITUTION) { 62 | return { 63 | ...defaultState, 64 | lastAdvanceWidth: state.lastAdvanceWidth 65 | } 66 | } 67 | 68 | return state 69 | }) 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dougrich/conscripter", 3 | "version": "0.3.1", 4 | "description": "Static site that generates functional open type font files for conlang scripts from SVG using substitution", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test mocha --require @babel/register \"./state/**/?(*.)spec.js\"", 8 | "dev": "next", 9 | "deploy": "npm run export && touch out/.nojekyll && echo 'Publishing...' && gh-pages -d out -t", 10 | "export": "next build && next export", 11 | "build": "next build", 12 | "start": "next start", 13 | "lint": "standard", 14 | "help": "gh-pages --help" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/dougrich/conscripter.git" 19 | }, 20 | "author": "@dougrich", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/dougrich/conscripter/issues" 24 | }, 25 | "homepage": "https://github.com/dougrich/conscripter#readme", 26 | "devDependencies": { 27 | "chai": "^4.2.0", 28 | "cross-fetch": "^3.0.5", 29 | "gh-pages": "^2.0.1", 30 | "mocha": "^6.2.1", 31 | "standard": "^16.0.3", 32 | "svg-transform-parser": "0.0.1" 33 | }, 34 | "dependencies": { 35 | "@babel/core": "^7.4.3", 36 | "@babel/register": "^7.4.0", 37 | "classnames": "^2.2.6", 38 | "msgpack-lite": "^0.1.26", 39 | "next": "^10.1.3", 40 | "node-sass": "^4.12.0", 41 | "opentype.js": "^0.11.0", 42 | "parse5": "^5.1.0", 43 | "raw-loader": "^2.0.0", 44 | "react": "^16.8.6", 45 | "react-dom": "^16.8.6", 46 | "react-markdown": "^4.0.6", 47 | "react-redux": "^6.0.1", 48 | "redux": "^4.0.1", 49 | "redux-thunk": "^2.3.0", 50 | "slugify": "^1.3.4", 51 | "svg-arc-to-cubic-bezier": "^3.1.3", 52 | "svg-path-parser": "^1.1.0" 53 | }, 54 | "standard": { 55 | "globals": [ 56 | "BASE_LINK", 57 | "React", 58 | "localStorage" 59 | ], 60 | "env": [ 61 | "mocha" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components/head.js: -------------------------------------------------------------------------------- 1 | import NativeHead from 'next/head' 2 | 3 | function favicon (v) { 4 | return BASE_LINK + `/static/favicon${v}` 5 | } 6 | 7 | export default function Head ({ 8 | title 9 | }) { 10 | return ( 11 | 12 | {title} | Conscripter 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /components/slider-toggle.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | 3 | 4 | $thumb-width: 16px; 5 | $track-width: 4px; 6 | $transition-timing: 150ms; 7 | 8 | .track { 9 | height: $track-width; 10 | width: 100%; 11 | background-color: theme-color('offwhite'); 12 | position: relative; 13 | } 14 | 15 | .runner { 16 | background-color: theme-color('primary'); 17 | position: absolute; 18 | height: 100%; 19 | transition: width $transition-timing ease-in-out; 20 | } 21 | 22 | .runneractive { 23 | transition: none; 24 | } 25 | 26 | .trackcontainer { 27 | height: $track-width; 28 | padding: #{$thumb-width / 2} 0; 29 | cursor: pointer; 30 | margin-bottom: 1em; 31 | } 32 | 33 | .thumb { 34 | width: $thumb-width; 35 | height: $thumb-width; 36 | background-color: theme-color('primary'); 37 | border-radius: 100%; 38 | position: relative; 39 | top: -$thumb-width / 2 + $track-width / 2; 40 | z-index: 0; 41 | border: 2px solid theme-color('dark-primary'); 42 | box-sizing: border-box; 43 | transition: left $transition-timing ease-in-out; 44 | 45 | &:after { 46 | content: ' '; 47 | width: $thumb-width; 48 | height: $thumb-width; 49 | position: absolute; 50 | top: 50%; 51 | left: 50%; 52 | background-color: theme-color('primary'); 53 | opacity: 0.3; 54 | border-radius: 100%; 55 | transform: translate(-50%, -50%); 56 | transition: transform $transition-timing ease-in-out; 57 | z-index: -1; 58 | } 59 | 60 | &:hover:after { 61 | transform: translate(-50%, -50%) scale(2.0); 62 | } 63 | 64 | &.thumbactive { 65 | transition: none; 66 | } 67 | 68 | &.thumbactive:after { 69 | transform: translate(-50%, -50%) scale(3.0)!important; 70 | } 71 | } 72 | 73 | .toggle { 74 | width: $thumb-width * 3; 75 | margin: 0 0.5em; 76 | } 77 | 78 | .togglecheckbox { 79 | display: none; 80 | } 81 | 82 | .togglecontainer { 83 | display: flex; 84 | transition: opacity $transition-timing ease-in-out; 85 | } 86 | 87 | .disabled { 88 | pointer-events: none; 89 | opacity: 0.25; 90 | 91 | .runner, .thumb { 92 | background-color: theme-color('dark-offwhite'); 93 | } 94 | .thumb { 95 | border: theme-color('dark-offwhite'); 96 | } 97 | } -------------------------------------------------------------------------------- /components/typography.js: -------------------------------------------------------------------------------- 1 | import css from './typography.module.scss' 2 | import Images from './images' 3 | import Print from './print.module.scss' 4 | import * as cx from 'classnames' 5 | 6 | const Typography = { 7 | Header: { 8 | Display: () => ( 9 |

10 | 11 | Conscripter 12 |

13 | ), 14 | Section: ({ children }) => ( 15 |

16 | {children} 17 |

18 | ) 19 | }, 20 | Links: ({ children }) => ( 21 |
25 | {children} 26 |
27 | ), 28 | Button: ({ children }) => ( 29 | 32 | {children} 33 | 34 | ), 35 | Icon: ({ children }) => ( 36 | 39 | {children} 40 | 41 | ), 42 | Copyright: () => ( 43 |
44 | Made by Douglas Richardson © 2020.
Fonts available through the Creative Commons CC0 License.
Code available through the MIT License. 45 |
46 | ), 47 | Small: ({ children }) => ( 48 | {' '}{children} 49 | ), 50 | Label: ({ children, htmlFor }) => ( 51 | 54 | ), 55 | Description: ({ children, className }) => ( 56 |
57 | {children} 58 |
59 | ), 60 | Markdown: (props) => { 61 | const { children, className } = props 62 | return ( 63 |
64 | {children} 65 |
66 | ) 67 | }, 68 | Descriptor: ({ className, children }) => ( 69 |
70 | {children} 71 |
72 | ), 73 | Input: css.input, 74 | Demofont: css.demofont, 75 | BaseDemofont: css.basedemofont 76 | } 77 | 78 | export default Typography 79 | -------------------------------------------------------------------------------- /components/text.module.scss: -------------------------------------------------------------------------------- 1 | @import '../theme/_colors'; 2 | 3 | .textarea { 4 | border: 0; 5 | border-top: 3px solid transparent; 6 | border-bottom: 3px solid transparent; 7 | margin-top: -3px; 8 | margin-bottom: -3px; 9 | height: calc(100% + 6px)!important; 10 | overflow: auto; 11 | 12 | &:focus { 13 | border-top: 3px solid theme-color('primary'); 14 | border-bottom: 3px solid theme-color('primary'); 15 | } 16 | } 17 | 18 | .textfield { 19 | width: 100%; 20 | display: block; 21 | border: 0; 22 | border-bottom: 3px solid transparent; 23 | margin-bottom: -3px; 24 | background: transparent; 25 | outline: none; 26 | font-size: 1.2em; 27 | &:focus { 28 | border-bottom: 3px solid theme-color('primary'); 29 | } 30 | } 31 | 32 | %common-border { 33 | border: 0; 34 | transition: border 150ms ease-in-out, padding 150ms ease-in-out; 35 | } 36 | 37 | %border-bottom { 38 | border-bottom: 1px solid theme-color('offwhite'); 39 | padding-bottom: 1px; 40 | &:hover { 41 | border-bottom-width: 2px; 42 | padding-bottom: 0px; 43 | border-bottom-color: theme-color('dark'); 44 | } 45 | } 46 | 47 | %border-top { 48 | border-top: 1px solid theme-color('offwhite'); 49 | padding-top: 1px; 50 | &:hover { 51 | border-top-width: 2px; 52 | padding-top: 0px; 53 | border-top-color: theme-color('dark'); 54 | } 55 | } 56 | 57 | .textareacontainer { 58 | height: 100%; 59 | max-height: 50vh; 60 | @extend %common-border; 61 | @extend %border-bottom; 62 | @extend %border-top; 63 | } 64 | 65 | .textfieldcontainer { 66 | @extend %common-border; 67 | @extend %border-bottom; 68 | margin-bottom: 1em; 69 | } 70 | 71 | .vcell, .vcursor { 72 | width: 1em; 73 | height: 1em; 74 | text-align: center; 75 | } 76 | 77 | @keyframes vcursorblinkanimation { 78 | to { 79 | opacity: 0; 80 | } 81 | } 82 | 83 | .vcursor { 84 | display: none; 85 | background-color: theme-color('primary'); 86 | animation: vcursorblinkanimation 0.5s steps(2, start) infinite; 87 | } 88 | 89 | .vline { 90 | width: 1em; 91 | display: inline-block; 92 | } 93 | 94 | .vcontainer { 95 | direction: rtl!important; 96 | display: flex!important; 97 | justify-content: center; 98 | overflow: auto; 99 | &:focus .vcursor { 100 | display: block; 101 | } 102 | } -------------------------------------------------------------------------------- /components/dropzone.js: -------------------------------------------------------------------------------- 1 | import css from './dropzone.module.scss' 2 | import Typography from './typography' 3 | import React from 'react' 4 | 5 | export default class DropZone extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | this.state = { 9 | warning: '', 10 | loading: false 11 | } 12 | this.fileInput = React.createRef() 13 | 14 | this.onClick = (e) => { 15 | e.stopPropagation() 16 | this.fileInput.current.click() 17 | } 18 | 19 | this.onFileAdded = (e) => { 20 | const file = e.target.files[0] 21 | e.target.value = null 22 | if (file.name.indexOf('.svg') !== file.name.length - '.svg'.length) { 23 | this.setState({ 24 | warning: `${file.name} does not appear to be an SVG file: make sure it is a plain text SVG file.` 25 | }) 26 | } else { 27 | this.setState({ 28 | warning: null, 29 | loading: true 30 | }) 31 | const filereader = new window.FileReader() 32 | filereader.onload = () => { 33 | let error 34 | try { 35 | this.props.onUpload({ 36 | filename: file.name, 37 | contents: filereader.result 38 | }) 39 | } catch (e) { 40 | console.error(e) 41 | error = e 42 | } 43 | this.setState({ 44 | warning: error ? error.message : null, 45 | loading: false 46 | }) 47 | } 48 | filereader.readAsText(file) 49 | } 50 | } 51 | } 52 | 53 | renderWarning () { 54 | const { 55 | warning 56 | } = this.state 57 | 58 | if (!warning) return null 59 | return ( 60 |
61 | {warning} 62 |
63 | ) 64 | } 65 | 66 | render () { 67 | const handleOnClick = this.onClick 68 | const handleOnFileAdded = this.onFileAdded 69 | return ( 70 |
75 | Drag file or click here to upload 76 | 83 | {this.renderWarning()} 84 |
85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /components/glyph-preview.js: -------------------------------------------------------------------------------- 1 | import css from './glyph-preview.module.scss' 2 | import * as cx from 'classnames' 3 | import Typography from './typography' 4 | 5 | export default function GlyphPreview ({ 6 | commands, 7 | advanceWidth, 8 | unitsPerEm, 9 | descender, 10 | isDiacritic, 11 | className, 12 | showContext = true 13 | }) { 14 | if (!unitsPerEm) return null 15 | advanceWidth = Math.max(advanceWidth, 100) 16 | const widthEm = advanceWidth / unitsPerEm 17 | let transform = 'translate(-50%, 0%)' 18 | if (widthEm > 1) { 19 | transform += ` scale(${(1 / widthEm).toFixed(2)})` 20 | } 21 | const path = commands.map(c => { 22 | switch (c.type) { 23 | case 'M': 24 | case 'L': 25 | return `${c.type} ${c.x} ${unitsPerEm - c.y + descender}` 26 | case 'Z': 27 | return `${c.type}` 28 | default: 29 | // this is an unrecognized path command; it happens 30 | return '' 31 | } 32 | }).join(' ') 33 | const baseline = (unitsPerEm + descender) / unitsPerEm 34 | // these two depend on the font 35 | const topline = '0.4em' 36 | const textCorrection = '0em' 37 | return ( 38 |
39 |
40 | {showContext && [ 41 |
, 42 |
43 | ]} 44 |
45 | {showContext && ( 46 | abcdef 47 | )} 48 | 49 | {isDiacritic && ( 50 | 51 | )} 52 | 53 | 54 | {showContext && ( 55 | ghijk 56 | )} 57 |
58 |
59 |
60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /tests/abugida2/base.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 68 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-gul-combined.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /components/preview.js: -------------------------------------------------------------------------------- 1 | import css from './preview.module.scss' 2 | import * as cx from 'classnames' 3 | import Print from './print.module.scss' 4 | import Text from './text' 5 | import Options from './options' 6 | import Slider from './slider' 7 | import Typography from './typography' 8 | import Badge from './badge' 9 | import Links from './links' 10 | import Description from './description' 11 | 12 | export default class Preview extends React.PureComponent { 13 | constructor (props) { 14 | super(props) 15 | this.state = { 16 | fontSize: 200, 17 | options: { 18 | italic: false, 19 | bold: false, 20 | invert: false, 21 | rtl: false, 22 | vertical: false 23 | } 24 | } 25 | 26 | this.setFontSize = (fontSize) => this.setState({ fontSize }) 27 | this.setOptions = options => this.setState({ options }) 28 | this.format = v => { 29 | return ((v / 100) * 16).toFixed(1) + ' pt' 30 | } 31 | } 32 | 33 | render () { 34 | const { 35 | defaultValue, 36 | keep 37 | } = this.props 38 | const { 39 | fontSize, 40 | options 41 | } = this.state 42 | const handleFontSizeChange = this.setFontSize 43 | const handleOptionsChange = this.setOptions 44 | const handlePreviewTextChange = this.onChange 45 | return ( 46 |
47 |
48 | 56 | BETA vertical 64 | ) 65 | }} 66 | onChange={handleOptionsChange} 67 | /> 68 | {options.vertical && ( 69 | 70 | Note that there are special steps that need to be taken to get your font vertically in your editor, and not all editors work with vertical fonts. See syllabry1 for details. 71 | 72 | )} 73 |
74 | 87 |
88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /theme/_colors.scss: -------------------------------------------------------------------------------- 1 | $color-red: #dd1c1a; 2 | $color-blue: #4392F1; 3 | $color-yellow: #F0c808; 4 | $color-black: #2b303a; 5 | $color-white: white; 6 | $color-offwhite: #e0ddcf; 7 | $color-coolgrey: #f3f3f3; 8 | 9 | $theme-colors: ( 10 | 'primary': $color-blue, 11 | 'dark-primary': darken($color-blue, 15%), 12 | 'dark': $color-black, 13 | 'offwhite': $color-offwhite, 14 | 'dark-offwhite': darken($color-offwhite, 40%), 15 | 'alert-default': $color-coolgrey, 16 | 'alert-danger': lighten($color-red, 40%), 17 | 'border': $color-offwhite, 18 | 'link': ( 19 | 'focus': ( 20 | 'bg': $color-yellow, 21 | 'color': $color-black, 22 | 'border': transparent 23 | ), 24 | 'default': ( 25 | 'bg': transparent, 26 | 'color': $color-blue, 27 | 'border': transparent 28 | ) 29 | ), 30 | 'btn-default': ( 31 | 'focus': ( 32 | 'bg': $color-yellow, 33 | 'color': $color-black, 34 | 'border': $color-black 35 | ), 36 | 'default': ( 37 | 'bg': lighten($color-black, 15%), 38 | 'color': $color-white, 39 | 'border': transparent 40 | ), 41 | 'hover': ( 42 | 'bg': $color-black, 43 | 'color': $color-white, 44 | 'border': transparent 45 | ) 46 | ), 47 | 'btn-success': ( 48 | 'focus': ( 49 | 'bg': $color-yellow, 50 | 'color': $color-black, 51 | 'border': $color-black 52 | ), 53 | 'default': ( 54 | 'bg': $color-blue, 55 | 'color': $color-white, 56 | 'border': transparent 57 | ), 58 | 'hover': ( 59 | 'bg': darken($color-blue, 15%), 60 | 'color': $color-white, 61 | 'border': transparent 62 | ) 63 | ), 64 | 'btn-danger': ( 65 | 'focus': ( 66 | 'bg': $color-yellow, 67 | 'color': $color-black, 68 | 'border': $color-black 69 | ), 70 | 'default': ( 71 | 'bg': $color-red, 72 | 'color': $color-white, 73 | 'border': transparent 74 | ), 75 | 'hover': ( 76 | 'bg': darken($color-red, 15%), 77 | 'color': $color-white, 78 | 'border': transparent 79 | ) 80 | ), 81 | 'btn-transparent': ( 82 | 'focus': ( 83 | 'bg': $color-yellow, 84 | 'color': $color-black, 85 | 'border': transparent 86 | ), 87 | 'default': ( 88 | 'bg': transparent, 89 | 'color': $color-black, 90 | 'border': transparent 91 | ), 92 | 'hover': ( 93 | 'bg': darken(white, 15%), 94 | 'color': $color-black, 95 | 'border': transparent 96 | ) 97 | ) 98 | ); 99 | 100 | @function theme-color($key: 'primary') { 101 | @return map-get($theme-colors, $key); 102 | } 103 | 104 | @function colorset($key: 'primary', $field: 'color') { 105 | @return map-get(map-get($theme-colors, $key), $field); 106 | } 107 | 108 | @mixin colors($key, $field) { 109 | $set: colorset($key, $field); 110 | 111 | background-color: map-get($set, 'bg'); 112 | color: map-get($set, 'color'); 113 | border-color: map-get($set, 'border'); 114 | } 115 | 116 | @mixin text-color($color) { 117 | background-color: $color; 118 | color: darken($color, 70%); 119 | } -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-u.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-n.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-g.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-l.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /tests/abugida2/k.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/eu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/a.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/o.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/r.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/t.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/abugida2/d.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/abugida2/ph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 43 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /tests/e2e/inkscape/korean-h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /components/substitution-editor.js: -------------------------------------------------------------------------------- 1 | import css from './substitution-editor.module.scss' 2 | import GlyphPreview from './glyph-preview' 3 | import DropZone from './dropzone' 4 | import Button, { ButtonBar } from './button' 5 | import Description from './description' 6 | import Typography from './typography' 7 | import Slider, { Toggle } from './slider' 8 | import Text from './text' 9 | 10 | function surpress (handler) { 11 | return e => { 12 | e.preventDefault() 13 | e.stopPropagation() 14 | handler(e) 15 | } 16 | } 17 | 18 | export default class SubstitutionEditor extends React.PureComponent { 19 | render () { 20 | const { 21 | active, 22 | canMoveLeft, 23 | canMoveRight, 24 | canRemove, 25 | currentGlyph, 26 | currentReplace, 27 | idx, 28 | meta, 29 | warnings, 30 | onAdvanceWidthChange, 31 | onCancel, 32 | onRemove, 33 | onReplaceChange, 34 | onSubmit, 35 | onSwap, 36 | onToggleDiacritic, 37 | onUpload 38 | } = this.props 39 | 40 | if (!active) { 41 | return null 42 | } 43 | 44 | return ( 45 |
46 |
47 | {canMoveLeft 48 | ? 49 | :
} 50 | {canMoveRight 51 | ? 52 | :
} 53 |
54 |
55 | 56 | 57 | 58 | Doesn't look like what you expected? Raise an issue on Github with your SVG to help improve this app. 59 | 60 | {warnings && warnings.length 61 | ? ( 62 | 63 |
Warning!
64 |
    65 | {warnings.map((x, i) => ( 66 |
  • {x.message}
  • 67 | ))} 68 |
69 |
70 | ) 71 | : null} 72 |
73 |
74 | 79 | 87 | May only contain letters, numbers, peroids, dashes, and underscores 88 | { 93 | return (v / meta.unitsPerEm).toFixed(3) + ' em' 94 | }} 95 | value={currentGlyph.advanceWidth} 96 | onChange={onAdvanceWidthChange} 97 | /> 98 |
99 | 100 | {canRemove && } 101 | 102 | 103 | 104 | 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /components/glyph-grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid of glyphs, displaying each with the relevant substitutions 3 | */ 4 | 5 | import css from './glyph-grid.module.scss' 6 | import GlyphPreview from './glyph-preview' 7 | import * as cx from 'classnames' 8 | import Button from './button' 9 | import Typography from './typography' 10 | import Print from './print.module.scss' 11 | 12 | export default function GlyphGrid ({ substitutions, meta, active, children, onSubstitutionSelect, onSubstitutionSwap }) { 13 | const symbols = [] 14 | const gridcells = [] 15 | let foundActive = false 16 | const hasActive = active != null 17 | 18 | if (substitutions.length === 0 && !active) { 19 | return ( 20 |
21 | 22 | 25 | 26 |
27 | ) 28 | } 29 | 30 | function Btn ({ children, isActive, onClick, className }) { 31 | return ( 32 | 35 | ) 36 | } 37 | 38 | function onDragStart (e) { 39 | const index = e.currentTarget.attributes['data-index'] 40 | if (!index) { 41 | e.preventDefault() 42 | e.stopPropagation() 43 | } else { 44 | e.dataTransfer.setData('number', parseInt(index.value)) 45 | } 46 | } 47 | 48 | function onDragOver (e) { 49 | if (e.currentTarget.attributes['data-index'].value) { 50 | e.preventDefault() 51 | } 52 | } 53 | 54 | function onDrop (e) { 55 | const index = e.currentTarget.attributes['data-index'] 56 | if (!index) { 57 | e.preventDefault() 58 | e.stopPropagation() 59 | } else { 60 | const self = parseInt(index.value) 61 | const other = e.dataTransfer.getData('number') 62 | if (self !== other) { 63 | onSubstitutionSwap(self, other) 64 | } 65 | } 66 | } 67 | 68 | for (let i = 0; i < substitutions.length; i++) { 69 | const sub = substitutions[i] 70 | const { replace, glyph } = sub 71 | const key = replace.join('/') 72 | const isActive = active === sub 73 | const button = ( 74 | onSubstitutionSelect(sub)}> 75 |
{key}
76 | 77 |
78 | ) 79 | foundActive = foundActive || isActive 80 | gridcells.push({ 81 | key, 82 | index: i, 83 | button, 84 | isActive 85 | }) 86 | } 87 | 88 | gridcells.push({ 89 | key: 'add', 90 | button: ( onSubstitutionSelect()}>+), 91 | isActive: hasActive && !foundActive 92 | }) 93 | 94 | for (const { key, index, button, isActive } of gridcells) { 95 | const className = cx(css.gridcell, { 96 | [css.inactive]: !isActive && hasActive 97 | }) 98 | symbols.push( 99 |
108 | {button} 109 |
, 110 | isActive && ( 111 |
112 |
113 | {children} 114 |
115 |
116 | ) 117 | ) 118 | } 119 | 120 | return ( 121 |
122 | 123 | High Priority 124 | 125 | {symbols} 126 | 127 | Low Priority 128 | 129 |
130 | ) 131 | } 132 | -------------------------------------------------------------------------------- /icons/conscripter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 66 | 74 | 82 | 90 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /components/slider.js: -------------------------------------------------------------------------------- 1 | import Label from './label' 2 | import Typography from './typography' 3 | import css from './slider-toggle.module.scss' 4 | import * as cx from 'classnames' 5 | 6 | const noopFormat = v => v 7 | const noop = () => {} 8 | 9 | export default class Slider extends React.PureComponent { 10 | constructor (props) { 11 | super(props) 12 | this.state = { 13 | isActive: false 14 | } 15 | 16 | this.track = React.createRef() 17 | 18 | this.setAbsolute = (e) => { 19 | const { onChange = noop } = this.props 20 | onChange(this.computeNewValue(e.clientX)) 21 | } 22 | this.onMouseMove = (e) => { 23 | e.preventDefault() 24 | const newv = this.computeNewValue(e.clientX) 25 | const { onChange = noop } = this.props 26 | if (!this.state.isActive) { return } 27 | onChange(newv) 28 | } 29 | this.onThumbDown = () => { 30 | this.setState({ isActive: true }) 31 | document.addEventListener('mousemove', this.onMouseMove) 32 | document.addEventListener('mouseup', this.onThumbUp) 33 | } 34 | this.onThumbUp = () => { 35 | this.setState({ isActive: false }) 36 | document.removeEventListener('mousemove', this.onMouseMove) 37 | document.removeEventListener('mouseup', this.onThumbUp) 38 | } 39 | } 40 | 41 | computeNewValue (clientX) { 42 | const rect = this.track.current.getBoundingClientRect() 43 | const newpct = (clientX - rect.left) / rect.width 44 | const { 45 | min = 0, 46 | max = 1, 47 | step = 1 48 | } = this.props 49 | const domainvalue = (newpct * (max - min) + min) 50 | const steppedvalue = Math.round(domainvalue / step) * step 51 | const boundvalue = Math.max(min, Math.min(max, steppedvalue)) 52 | return boundvalue 53 | } 54 | 55 | computeRunnerStyle (pct) { 56 | return { 57 | width: `${(pct * 100).toFixed(2)}%` 58 | } 59 | } 60 | 61 | computeThumbStyle (pct) { 62 | const percentage = `${(pct * 100).toFixed(2)}%` 63 | const offset = (pct * 16).toFixed(2) + 'px' 64 | return { 65 | left: `calc(${percentage} - ${offset})` 66 | } 67 | } 68 | 69 | render () { 70 | const { 71 | label, 72 | format = noopFormat, 73 | value, 74 | min = 0, 75 | max = 1, 76 | step = 1 77 | } = this.props 78 | const { 79 | isActive 80 | } = this.state 81 | 82 | const pct = (Math.round(value / step) * step - min) / (max - min) 83 | 84 | const handleClick = this.setAbsolute 85 | const handleMouseDown = this.onThumbDown 86 | return ( 87 |
88 | 92 |
96 |
97 |
101 |
106 |
107 |
108 |
109 | ) 110 | } 111 | } 112 | 113 | export class Toggle extends Slider { 114 | constructor (props) { 115 | super(props) 116 | this.defaultId = 't' + Math.random().toString(16).slice(2) 117 | this.toggle = () => { 118 | const { onChange, disabled } = this.props 119 | if (!disabled) { 120 | onChange(!this.props.value) 121 | } 122 | } 123 | } 124 | 125 | render () { 126 | const { 127 | label, 128 | id = this.defaultId, 129 | value, 130 | disabled 131 | } = this.props 132 | const pct = value ? 1 : 0 133 | const handleToggle = this.toggle 134 | return ( 135 |
136 | 137 |
141 |
142 |
146 |
151 |
152 |
153 | 156 |
157 | ) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /state/reducers/fonts/index.js: -------------------------------------------------------------------------------- 1 | import { FETCH_FONTS, ADD_SUBSTITUTION, REMOVE_SUBSTITUTION, DOWNLOAD, SET_FONTNAME, SWAP_SUBSTITUTION, LOAD } from '../../actionTypes' 2 | import { STATUS_PENDING, STATUS_OK, STATUS_ERROR } from '../../status' 3 | import { KEY_FONTS } from '../../keys' 4 | import clearable from '../clearable' 5 | import encoders from '../../encoders' 6 | 7 | import { assembleDataUri, download } from './assembleDataUri' 8 | 9 | const defaultState = { 10 | status: STATUS_PENDING, 11 | buffer: null, 12 | datauri: null, 13 | substitutions: [], 14 | fontname: 'My Custom Font' 15 | } 16 | 17 | export function reassembleDataUri (assembleDataUri) { 18 | return (reducer) => { 19 | return (state, action) => { 20 | const newstate = reducer(state, action) 21 | 22 | const needToRebuildDataUri = !!newstate && !!newstate.buffer && ( 23 | !state || 24 | state.buffer !== newstate.buffer || 25 | state.substitutions !== newstate.substitutions || 26 | state.fontname !== newstate.fontname 27 | ) 28 | 29 | if (needToRebuildDataUri) { 30 | const { buffer, substitutions, fontname } = newstate 31 | return { 32 | ...newstate, 33 | ...assembleDataUri(buffer, substitutions, fontname) 34 | } 35 | } 36 | 37 | return newstate 38 | } 39 | } 40 | } 41 | 42 | const hasLocalStorage = typeof localStorage !== 'undefined' 43 | 44 | function saveLocally (key) { 45 | return (reducer) => { 46 | if (!hasLocalStorage) { 47 | return reducer 48 | } 49 | 50 | return (state, action) => { 51 | if (!state) { 52 | const previous = localStorage.getItem(key) 53 | if (previous) { 54 | try { 55 | const { substitutions, fontname } = encoders.JSON.decode(previous) 56 | state = { 57 | ...defaultState, 58 | substitutions, 59 | fontname 60 | } 61 | } catch (err) { 62 | // bad state in local storage 63 | localStorage.clear(key) 64 | } 65 | } 66 | } 67 | const newstate = reducer(state, action) 68 | if (newstate !== state) { 69 | localStorage.setItem(key, encoders.JSON.encode({ fonts: newstate })) 70 | } 71 | return newstate 72 | } 73 | } 74 | } 75 | 76 | export const fonts = saveLocally(KEY_FONTS)(clearable(defaultState, { only: ['substitutions'] })(reassembleDataUri(assembleDataUri)((state, action) => { 77 | // loading from a saved slate 78 | if (typeof state === 'string') state = JSON.parse(state) 79 | 80 | if (action && action.type === FETCH_FONTS) { 81 | switch (action.status) { 82 | case STATUS_OK: 83 | return { 84 | ...state, 85 | status: STATUS_OK, 86 | buffer: action.buffer 87 | } 88 | case STATUS_ERROR: 89 | return { 90 | ...state, 91 | status: STATUS_ERROR, 92 | buffer: null, 93 | datauri: null 94 | } 95 | default: 96 | return { 97 | ...state, 98 | status: STATUS_PENDING, 99 | buffer: null, 100 | datauri: null 101 | } 102 | } 103 | } 104 | 105 | if (action && action.type === ADD_SUBSTITUTION) { 106 | const { substitutions } = state 107 | const idx = substitutions.indexOf(action.replace) 108 | let newsubs 109 | if (idx >= 0) { 110 | newsubs = [...substitutions.slice(0, idx), action.substitution, ...substitutions.slice(idx + 1)] 111 | } else { 112 | newsubs = [...substitutions, action.substitution] 113 | } 114 | return { 115 | ...state, 116 | substitutions: newsubs 117 | } 118 | } 119 | 120 | if (action && action.type === REMOVE_SUBSTITUTION) { 121 | const { substitutions } = state 122 | const idx = substitutions.indexOf(action.substitution) 123 | let newsubs = substitutions 124 | if (idx >= 0) { 125 | newsubs = [...substitutions.slice(0, idx), ...substitutions.slice(idx + 1)] 126 | } 127 | 128 | return { 129 | ...state, 130 | substitutions: newsubs 131 | } 132 | } 133 | 134 | if (action && action.type === DOWNLOAD) { 135 | const { buffer, substitutions, fontname } = state 136 | download(buffer, substitutions, fontname) 137 | return state 138 | } 139 | 140 | if (action && action.type === SET_FONTNAME) { 141 | return { 142 | ...state, 143 | fontname: action.value 144 | } 145 | } 146 | 147 | if (action && action.type === SWAP_SUBSTITUTION) { 148 | const { substitutions } = state 149 | const newsubs = substitutions.slice() 150 | const a = newsubs[action.a] 151 | const b = newsubs[action.b] 152 | newsubs[action.a] = b 153 | newsubs[action.b] = a 154 | return { 155 | ...state, 156 | substitutions: newsubs 157 | } 158 | } 159 | 160 | if (action && action.type === LOAD) { 161 | const { substitutions, fontname, error } = action 162 | return { 163 | ...state, 164 | error, 165 | substitutions, 166 | fontname 167 | } 168 | } 169 | 170 | return state 171 | }))) 172 | -------------------------------------------------------------------------------- /tests/syllabry1/no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 64 | 71 | 78 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/how-to.md: -------------------------------------------------------------------------------- 1 | ## SVG Creation 2 | 3 | SVGs uploaded will only use the flat, solid fill portion of the image. Strokes and gradient fills are not supported. 4 | 5 | The viewbox or document width and height will be treated as the 'main area' of the glyph. You can leave these bounds - this is useful to place diacritics with letters, as seen in the Abugida example below. 6 | 7 | ## Font Usage 8 | 9 | After downloading the font, you should be able to use it in most advanced word editing programs. WordPad or Notepad will not be able to use it. 10 | 11 | Inkscape, Adobe Illustrator, Adobe Photoshop, Adobe InDesign, and Microsoft Publisher should all support it out of the box. 12 | 13 | For Microsoft Word, you will need to manually turn on `calt` support. This is done by opening the fonts menu and checking the `calt` checkbox. 14 | 15 | ## Tutorials 16 | 17 | ### Use Case: Abugida 18 | 19 | ![Abugida2 - Phadkoreut](/static/how-to/abugida2/phadkoreut.gif) 20 | 21 | Abugidas can be tricky to do even with a full font-editing tool, so let's look at how to accomplish this. The files are located [here](https://github.com/dougrich/conscripter/tree/master/tests/abugida2). Before going any further, load [this json, abugida2.json](https://raw.githubusercontent.com/dougrich/conscripter/master/tests/abugida2/abugida2.json) into Conscripter using the Load button. This is the completed font. 22 | 23 | Abugida2's orthography is really simple. 24 | 25 | ![Abugida2 - Orthography](/static/how-to/abugida2/orthography.png) 26 | 27 | --- 28 | 29 | __First__, we need to create a consistent set of glyphs in our SVG editor of choice. 30 | 31 | I'd recommend creating a `base.svg` or similar that you can base each glyph on, that includes horizontal rules for common areas like the accent mark region and how for down you go. 32 | 33 | For each consonant, draw a path. Once the path is completed, use `Stroke to Path` or similar. In Inkscape this is in `Object > Stroke to Path`. This is important because we only use the _fill_ portion of the SVG to draw the glyph. 34 | 35 | Save each consonant into their own file, keeping a consistent positioning for each. 36 | 37 | For each vowel, we need to position the accent mark _before_ the actual glyph. This is done simply by moving the entire glyph to the left of the document. 38 | 39 | I'd recommend loading up the example files in your editor to get an idea of how the positioning and layout works, as well as what a flat fill looks like. 40 | 41 | --- 42 | 43 | __Second__, we need to add these into Conscripter. 44 | 45 | Navigate to the app. Click the "Get Started" button. 46 | 47 | Add one of the `.svg` files you created, set the 'replace' to the characters you'd like it to replace, and set the advance width. 48 | 49 | ![Abugida2 - Example uploading the d glyph](/static/how-to/abugida2/upload-d.gif) 50 | 51 | After hitting submit, you can change the version by clicking on the glyph again. You can reorder the glyphs by dragging and dropping or by opening them up. 52 | 53 | Order matters, especially in Abugida2 - try uploading the `p` glyph before the `h` glyph, and then try typing `phadkoreut`. Not how the `ph` looks like a `p` glyph and then the `h` character. The most important glyphs should come before the least important glyphs. 54 | 55 | --- 56 | 57 | __Finally__, once completed, make sure to __name your font__, and then hit `save` to download a copy of everything you've done. This is needed if you want to make changes later. Hit `download` to download the actual `.otf` onto your computer. 58 | 59 | Install the font on your computer (this is operating system dependent, but most will give you an option to install if you double click to run the `.otf`). Close all your editing programs, and re-open them - you should see your font in the list of choices available. 60 | 61 | ### Use Case: Syllabry 62 | 63 | ![Syllabry1 - kakonanotato](/static/how-to/syllabry1/kakonanotato.gif) 64 | 65 | Syllabries use a single glyph for each syllable. Let's look how to accomplish this. The files are located [here]. Before going any further, load [this json, syllabry1.json] into Conscripter using the Load button. This is the completed font. 66 | 67 | Syllabry1's orthography is really simple. 68 | 69 | ![Syllabry1 - Orthography](/static/how-to/syllabry1/orthography.png) 70 | 71 | However, it should be noted that Syllabry1 is traditionally written vertically right-to-left, similar to traditional Japanese. 72 | 73 | --- 74 | 75 | __First__, we need to create a consistent set of glyphs in our SVG. Unlike with Abugida2, these glyphs do not require any fancy positioning as these glyphs are intended to be stacked on top of each other. 76 | 77 | --- 78 | 79 | __Second__, we need to add these into Conscripter. This is much like with Abugida2, however we do not need to modify the advance width. Instead, let's make sure that the preview window contains are latest. 80 | 81 | Note that in the BETA only typing at the end of the text is supported: both inline editing and highlighting are not working yet. 82 | 83 | --- 84 | 85 | __Third__, name your font and download it. Install it on your machine. 86 | 87 | When typing with your font in an editor, it will behave much like a standard horizontal font. To use it vertically your editor needs to support vertical orientation. Illustrator, for example, does. Selecting `Text > Orientation > Vertical` will turn your text vertically and right to left. 88 | 89 | If your editor supports a different way of having vertical fonts, please [Create an issue on Github](https://github.com/dougrich/conscripter/issues/new/choose) with the name of your editor so I can investigate further. 90 | 91 | --- 92 | 93 | Made it this far? Have feedback on how to improve these tutorials? [Create an issue on Github](https://github.com/dougrich/conscripter/issues/new/choose) to help improve the app! -------------------------------------------------------------------------------- /components/text.js: -------------------------------------------------------------------------------- 1 | import css from './text.module.scss' 2 | import * as cx from 'classnames' 3 | import Label from './label' 4 | import Typography from './typography' 5 | 6 | const KEYCODE = { 7 | BACKSPACE: 8, 8 | ENTER: 13, 9 | SHIFT: 16 10 | } 11 | 12 | const Text = { 13 | Field: class extends React.PureComponent { 14 | render () { 15 | const { 16 | label, 17 | required, 18 | pattern, 19 | placeholder, 20 | value, 21 | onChange 22 | } = this.props 23 | return ( 24 |
25 | 26 | onChange(value)} 31 | placeholder={placeholder} 32 | className={cx(css.textfield, Typography.Input)} 33 | /> 34 |
35 | ) 36 | } 37 | }, 38 | Area: class extends React.PureComponent { 39 | constructor (props) { 40 | super(props) 41 | this.state = { value: props.defaultValue } 42 | this.cursorRef = React.createRef() 43 | this.onChange = ({ currentTarget: { value } }) => this.setState({ value }) 44 | this.onKeyDown = (e) => { 45 | const { value } = this.state 46 | this.cursorRef.current.scrollIntoViewIfNeeded() 47 | switch (e.keyCode) { 48 | case KEYCODE.BACKSPACE: 49 | this.setState({ value: value.slice(0, value.length - 1) }) 50 | return 51 | case KEYCODE.ENTER: 52 | this.setState({ value: value + '\n' }) 53 | return 54 | case KEYCODE.SHIFT: 55 | return 56 | } 57 | if (!e.ctrlKey && !e.altKey && e.key.length === 1) { 58 | this.setState({ value: value + e.key }) 59 | } 60 | } 61 | } 62 | 63 | renderVertical () { 64 | const { 65 | className, 66 | style, 67 | keep 68 | } = this.props 69 | 70 | const { 71 | value 72 | } = this.state 73 | const lines = [] 74 | let current = [] 75 | const newline = () => { 76 | lines.push( 77 |
78 | {current} 79 |
80 | ) 81 | current = [] 82 | } 83 | // explicit array 84 | let substituted = new Array(value.length) 85 | for (let i = 0; i < value.length; i++) { 86 | substituted[i] = value[i] 87 | } 88 | 89 | for (let i = 0; i < keep.length; i++) { 90 | const { value: subset, fix } = keep[i] 91 | for (let j = 0; j < substituted.length - subset.length + 1; j++) { 92 | let matchFound = true 93 | for (let k = 0; k < subset.length; k++) { 94 | if (substituted[j + k] !== subset[k]) { 95 | matchFound = false 96 | break 97 | } 98 | } 99 | if (!matchFound) { 100 | continue 101 | } 102 | 103 | // match found, replace 104 | substituted.splice(j, subset.length, { [fix]: subset }) 105 | } 106 | } 107 | 108 | substituted = (({ current, set }) => { 109 | if (current) set.push(current) 110 | return set 111 | })(substituted.reduce( 112 | ({ current, set }, segment) => { 113 | if (typeof segment === 'string') { 114 | if (current) set.push(current) 115 | return { current: segment, set } 116 | } else { 117 | if (segment.in) { 118 | if (current) set.push(current) 119 | return { current: segment.in, set } 120 | } else if (segment.post) { 121 | return { current: current + segment.post, set } 122 | } else { 123 | throw new Error('Unrecognized segment positioning') 124 | } 125 | } 126 | }, 127 | { current: '', set: [] } 128 | )) 129 | 130 | for (let i = 0; i < substituted.length; i++) { 131 | const char = substituted[i] 132 | if (char === '\n') { 133 | newline() 134 | continue 135 | } 136 | 137 | current.push( 138 |
139 | {char} 140 |
141 | ) 142 | } 143 | current.push( 144 |
145 | ) 146 | newline() 147 | const handleKeyDown = this.onKeyDown 148 | return ( 149 |
150 |
156 | {lines} 157 |
158 |
159 | ) 160 | } 161 | 162 | render () { 163 | const { 164 | className, 165 | style, 166 | vertical 167 | } = this.props 168 | const { 169 | value 170 | } = this.state 171 | if (vertical) { 172 | return this.renderVertical() 173 | } 174 | 175 | const handleChange = this.onChange 176 | return ( 177 |
178 |