├── .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 |
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 |
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 |
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 |
24 | {child}
25 |
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 |
{label}
36 |
37 | {options.map((o) => (
38 |
39 | ))}
40 |
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 |
46 | ),
47 | Small: ({ children }) => (
48 | {' '}{children}
49 | ),
50 | Label: ({ children, htmlFor }) => (
51 |
52 | {children}
53 |
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 |
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 | onSubstitutionSelect()}>
23 | Click here to get started
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | function Btn ({ children, isActive, onClick, className }) {
31 | return (
32 |
33 | {children}
34 |
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 |
89 | {label}
90 | {format(value)}
91 |
92 |
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 |
153 |
154 | {label}
155 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | {label}
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 |
184 |
185 | )
186 | }
187 | }
188 | }
189 |
190 | export default Text
191 |
--------------------------------------------------------------------------------
/state/actions.js:
--------------------------------------------------------------------------------
1 | import fetch from 'cross-fetch'
2 | import * as PathParser from './reducers/fonts/parsePath'
3 | import encoder from './encoders'
4 |
5 | import {
6 | FETCH_FONTS,
7 | ADD_SUBSTITUTION,
8 | SELECT_SUBSTITUTION,
9 | UPDATE_SUBSTITUTION,
10 | CANCEL_SUBSTITUTION,
11 | REMOVE_SUBSTITUTION,
12 | DOWNLOAD,
13 | SET_FONTNAME,
14 | SWAP_SUBSTITUTION,
15 | SAVE,
16 | LOAD,
17 | CLEAR
18 | } from './actionTypes'
19 |
20 | import {
21 | STATUS_OK,
22 | STATUS_ERROR
23 | } from './status'
24 | import slugify from 'slugify'
25 |
26 | /**
27 | * Fetches and parses the base font
28 | * @returns {*} dispatchable event object
29 | */
30 | export function fetchFonts () {
31 | return dispatch => {
32 | dispatch({ type: FETCH_FONTS })
33 | return fetch(BASE_LINK + '/static/AVHersheySimplexMedium.otf')
34 | .then(response => {
35 | if (response.status !== 200) {
36 | dispatch(fetchFontError())
37 | throw new Error('Non 200 response')
38 | } else {
39 | return response.arrayBuffer()
40 | }
41 | })
42 | .then(buffer => {
43 | dispatch(fetchFontResult(buffer))
44 | })
45 | }
46 | }
47 |
48 | /**
49 | * Result of fetching the font
50 | * @returns {*} dispatchable event object
51 | */
52 | export function fetchFontResult (buffer) {
53 | return {
54 | type: FETCH_FONTS,
55 | status: STATUS_OK,
56 | buffer
57 | }
58 | }
59 |
60 | /**
61 | * Result of fetching the font when an error occurs
62 | * @returns {*} dispatchable event object
63 | */
64 | export function fetchFontError () {
65 | return {
66 | type: FETCH_FONTS,
67 | status: STATUS_ERROR
68 | }
69 | }
70 |
71 | export function updateSubstitutionGlyph (meta, svg) {
72 | const parser = new PathParser(meta)
73 | const { commands, warnings } = parser.parse(svg)
74 | return {
75 | type: UPDATE_SUBSTITUTION,
76 | field: 'glyph/commands',
77 | warnings,
78 | value: parser.simplify(commands)
79 | }
80 | }
81 |
82 | export function updateSubstitutionReplace (value) {
83 | return {
84 | type: UPDATE_SUBSTITUTION,
85 | field: 'replace',
86 | value
87 | }
88 | }
89 |
90 | export function updateSubstitutionAdvanceWidth (value) {
91 | return {
92 | type: UPDATE_SUBSTITUTION,
93 | field: 'glyph/advanceWidth',
94 | value
95 | }
96 | }
97 |
98 | export function updateSubstitutionDiacritic (value) {
99 | return {
100 | type: UPDATE_SUBSTITUTION,
101 | field: 'glyph/diacritic',
102 | value
103 | }
104 | }
105 |
106 | export function removeSubstitution ({ active }) {
107 | return dispatch => {
108 | dispatch({
109 | type: REMOVE_SUBSTITUTION,
110 | substitution: active
111 | })
112 | dispatch(cancelSubstitution())
113 | }
114 | }
115 |
116 | export function cancelSubstitution () {
117 | return { type: CANCEL_SUBSTITUTION }
118 | }
119 |
120 | export function submitSubstitution ({ active, currentGlyph, currentReplace }) {
121 | return dispatch => {
122 | dispatch({
123 | type: ADD_SUBSTITUTION,
124 | substitution: {
125 | replace: [currentReplace],
126 | glyph: currentGlyph
127 | },
128 | replace: active
129 | })
130 | dispatch(cancelSubstitution())
131 | }
132 | }
133 |
134 | export function selectSubstitution (substitution) {
135 | return {
136 | type: SELECT_SUBSTITUTION,
137 | substitution: substitution || {
138 | replace: [''],
139 | glyph: {
140 | commands: []
141 | }
142 | }
143 | }
144 | }
145 |
146 | export function download () {
147 | return { type: DOWNLOAD }
148 | }
149 |
150 | export function setFontName (value) {
151 | return {
152 | type: SET_FONTNAME,
153 | value
154 | }
155 | }
156 |
157 | export function swapSubstitution (a, b) {
158 | return {
159 | type: SWAP_SUBSTITUTION,
160 | a,
161 | b
162 | }
163 | }
164 |
165 | export function save (state) {
166 | const data = encoder.MSGPACK.encode(state)
167 | const blob = new window.Blob([data])
168 | const url = window.URL.createObjectURL(blob)
169 | const container = document.createElement('a')
170 | container.setAttribute('style', 'display: none;')
171 | container.setAttribute('href', url)
172 | container.setAttribute('download', slugify(state.fonts.fontname || 'conscripter-custom-font') + '.cwk')
173 | document.body.appendChild(container)
174 | container.click()
175 | setTimeout(() => {
176 | window.URL.revokeObjectURL(url)
177 | document.body.removeChild(container)
178 | }, 1000)
179 | return {
180 | type: SAVE
181 | }
182 | }
183 |
184 | export function load () {
185 | return dispatch => {
186 | const loader = document.createElement('input')
187 | loader.setAttribute('type', 'file')
188 | loader.onchange = e => {
189 | const file = e.target.files[0]
190 | const reader = new window.FileReader()
191 | reader.onload = () => {
192 | try {
193 | let data = { substitutions: [], fontname: 'My Custom Font' }
194 | if (file.name.endsWith('.json')) {
195 | data = encoder.JSON.decode(Buffer.from(reader.result).toString('utf8'))
196 | } else {
197 | data = encoder.MSGPACK.decode(Buffer.from(reader.result))
198 | }
199 | dispatch({
200 | type: LOAD,
201 | error: null,
202 | ...data
203 | })
204 | } catch (e) {
205 | dispatch({
206 | type: LOAD,
207 | error: new Error('Unable to parse selected file: did it come from Conscripter?'),
208 | substitutions: [],
209 | fontname: 'My Custom Font'
210 | })
211 | }
212 | }
213 | reader.readAsArrayBuffer(file)
214 | }
215 | loader.click()
216 | }
217 | }
218 |
219 | export function clear () {
220 | return {
221 | type: CLEAR
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/tests/syllabry1/na.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
40 |
42 |
43 |
45 | image/svg+xml
46 |
48 |
49 |
50 |
51 |
52 |
57 |
63 |
69 |
75 |
80 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/tests/syllabry1/ta.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
40 |
42 |
43 |
45 | image/svg+xml
46 |
48 |
49 |
50 |
51 |
52 |
57 |
63 |
69 |
75 |
80 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/state/reducers/fonts/assembleDataUri.js:
--------------------------------------------------------------------------------
1 | const opentype = require('opentype.js')
2 | const slugify = require('slugify')
3 |
4 | const defaultFontName = 'conscripter-custom-font'
5 |
6 | const fontPrototype = Object.getPrototypeOf(new opentype.Font({
7 | familyName: 'test',
8 | styleName: 'test',
9 | unitsPerEm: 1000,
10 | ascender: 800,
11 | descender: -200,
12 | glyphs: {}
13 | }))
14 |
15 | const defaultGsub = {
16 | version: 1,
17 | scripts: [
18 | {
19 | tag: 'DFLT',
20 | script: {
21 | defaultLangSys: {
22 | reserved: 0,
23 | reqFeatureIndex: 65535,
24 | featureIndexes: [
25 | 0
26 | ]
27 | },
28 | langSysRecords: []
29 | }
30 | },
31 | {
32 | tag: 'latn',
33 | script: {
34 | defaultLangSys: {
35 | reserved: 0,
36 | reqFeatureIndex: 65535,
37 | featureIndexes: [
38 | 0
39 | ]
40 | },
41 | langSysRecords: []
42 | }
43 | }
44 | ],
45 | features: [
46 | {
47 | tag: 'calt',
48 | feature: {
49 | featureParams: 0,
50 | lookupListIndexes: []
51 | }
52 | }
53 | ],
54 | lookups: []
55 | }
56 |
57 | function arrayBufferToBase64 (buffer) {
58 | let binary = ''
59 | const bytes = new Uint8Array(buffer)
60 | const len = bytes.byteLength
61 | for (let i = 0; i < len; i++) {
62 | binary += String.fromCharCode(bytes[i])
63 | }
64 | return window.btoa(binary)
65 | }
66 |
67 | function assertBadInput (truthy, message) {
68 | if (!truthy) {
69 | const error = new Error(message)
70 | error.status = 400
71 | throw error
72 | }
73 | }
74 |
75 | function makeSingleSubstitutionLookup (font, character, glyph) {
76 | const leadingGlyph = font.charToGlyphIndex(character)
77 |
78 | const subtable = {
79 | substFormat: 1,
80 | coverage: {
81 | format: 1,
82 | glyphs: [leadingGlyph]
83 | },
84 | deltaGlyphId: glyph - leadingGlyph
85 | }
86 |
87 | return {
88 | lookupType: 1,
89 | lookupFlag: 0,
90 | subtables: [subtable]
91 | }
92 | }
93 |
94 | function makeMultiSubstitutionLookup (font, characters, glyph) {
95 | const components = new Array(characters.length - 1)
96 | for (let i = 1; i < characters.length; i++) {
97 | components[i - 1] = font.charToGlyphIndex(characters[i])
98 | }
99 | const leadingGlyph = font.charToGlyphIndex(characters[0])
100 |
101 | const subtable = {
102 | substFormat: 1,
103 | coverage: {
104 | format: 1,
105 | glyphs: [leadingGlyph]
106 | },
107 | ligatureSets: [[{ ligGlyph: glyph, components }]]
108 | }
109 |
110 | return {
111 | lookupType: 4,
112 | lookupFlag: 0,
113 | subtables: [subtable]
114 | }
115 | }
116 |
117 | function translate (x, y, commands) {
118 | return commands.map(cmd => ({
119 | ...cmd,
120 | x: cmd.x != null ? cmd.x + x : cmd.x,
121 | y: cmd.y != null ? cmd.y + y : cmd.y
122 | }))
123 | }
124 |
125 | function addSubstitution (font, characters, glyph) {
126 | assertBadInput(typeof (characters) === 'string', 'characters argument must be a string')
127 | assertBadInput(characters.length >= 1, 'characters argument must be a string >= 1')
128 | assertBadInput(typeof (glyph) === 'number', 'glyph argument must be a glyph code')
129 | assertBadInput(Object.getPrototypeOf(font) === fontPrototype, 'font must be an instance of opentype.Font')
130 |
131 | const gsub = font.tables.gsub = font.tables.gsub || JSON.parse(JSON.stringify(defaultGsub))
132 |
133 | if (characters.length > 1) {
134 | gsub.lookups.push(makeMultiSubstitutionLookup(font, characters, glyph))
135 | } else {
136 | gsub.lookups.push(makeSingleSubstitutionLookup(font, characters, glyph))
137 | }
138 |
139 | gsub.features[0].feature.lookupListIndexes.push(gsub.lookups.length - 1)
140 | }
141 |
142 | function addGlyph (font, { isDiacritic, advanceWidth, commands }) {
143 | const g = new opentype.Glyph({
144 | index: font.glyphs.length,
145 | name: `glyph${font.glyphs.length}`
146 | })
147 | g.advanceWidth = isDiacritic ? 0 : advanceWidth
148 | g.path = new opentype.Path({
149 | fill: 'black',
150 | stroke: null,
151 | strokeWidth: 1,
152 | unitsPerEm: font.unitsPerEm
153 | })
154 | g.path.commands = isDiacritic ? translate(-advanceWidth, 0, commands) : commands
155 | font.glyphs.push(g.index, g)
156 | return g
157 | }
158 |
159 | function applySubstitutions (font, substitutions, fontname = defaultFontName) {
160 | for (const { replace, glyph } of substitutions) {
161 | const glyphId = addGlyph(font, glyph)
162 | for (const text of replace) {
163 | addSubstitution(font, text, glyphId.index)
164 | }
165 | }
166 |
167 | font.names = {
168 | copyright: {
169 | en: 'Made by Conscripter, a tool for building conlang scripts\n\nLicence: CC0 1.0\n\nBase glyphs are derived from AVHershey Simplex, created in 2016 by Stewart C. Russel (scruss.com), which is in turn dervied from the character stroke coordinates publshed by Allen V. Hershey in "Calligraphy for Computers". Additional glyphs included through use of the app may have a different licence.'
170 | },
171 | fontFamily: {
172 | en: fontname
173 | },
174 | fontSubfamily: {
175 | en: 'Medium'
176 | },
177 | fullName: {
178 | en: fontname + 'Medium'
179 | },
180 | version: {
181 | en: 'Version 000.001'
182 | },
183 | postScriptName: {
184 | en: (fontname + 'Medium').replace(/\s/gi, '')
185 | }
186 | }
187 | }
188 |
189 | function assembleDataUri (buffer, substitutions, fontname) {
190 | const font = opentype.parse(buffer)
191 | applySubstitutions(font, substitutions, fontname)
192 |
193 | return {
194 | datauri: 'data:font/otf;base64,' + arrayBufferToBase64(font.toArrayBuffer()),
195 | meta: {
196 | unitsPerEm: font.unitsPerEm,
197 | descender: font.descender
198 | }
199 | }
200 | }
201 |
202 | function download (buffer, substitutions, fontname = defaultFontName) {
203 | const font = opentype.parse(buffer)
204 | applySubstitutions(font, substitutions, fontname)
205 | const downloadname = slugify(fontname || defaultFontName) + '.otf'
206 | font.download(downloadname)
207 | }
208 |
209 | module.exports = {
210 | applySubstitutions,
211 | assembleDataUri,
212 | download
213 | }
214 |
--------------------------------------------------------------------------------
/tests/e2e/inkscape/korean-han-combined.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
21 |
40 |
42 |
43 |
45 | image/svg+xml
46 |
48 |
49 |
50 |
51 |
52 |
57 |
62 |
69 |
74 |
75 |
76 |
--------------------------------------------------------------------------------