├── .gitignore ├── favicon.ico ├── src ├── .DS_Store ├── components │ ├── .DS_Store │ ├── EmptyCanvas.js │ ├── Cursor.js │ ├── GlyphClear.js │ ├── HideGrid.js │ ├── PaintMode.js │ ├── TypingMode.js │ ├── CellWidth.js │ ├── CellHeight.js │ ├── FontSize.js │ ├── HistoryControls.js │ ├── ToggleMapping.js │ ├── SelectedGlyph.js │ ├── GlyphList.js │ ├── QuickChooseColor.js │ ├── UiLayer.js │ ├── KeyMappings.js │ ├── SaveAsButton.js │ ├── GlyphFontSizeModifier.js │ ├── SetSelect.js │ ├── UiGrid.js │ ├── KeyMapping.js │ ├── GridControls.js │ ├── ColorPaletteSelect.js │ ├── GlyphInfo.js │ ├── GlyphOffset.js │ ├── CanvasSizeInMillimeters.js │ ├── SelectionHighlight.js │ ├── CellBg.js │ ├── Numbers.js │ ├── Coordinates.js │ ├── Preview.js │ ├── ExportButtons.js │ ├── SaveToDropboxButton.js │ ├── CanvasSizeModification.js │ ├── ErrorBoundary.js │ ├── ColorPresetSelect.js │ ├── Grid.js │ ├── ColorPalette.js │ ├── ColorSelect.js │ ├── LayerSelect.js │ ├── LoadAndPlace.js │ ├── LoadButton.js │ ├── CanvasMouseEvents.js │ ├── ColorSliders.js │ ├── Canvas.js │ ├── Cell.js │ ├── GlyphSelect.js │ └── Settings.js ├── utils │ ├── pxToMm.js │ ├── geometry.js │ ├── detectOs.js │ ├── cellsAsSvg.js │ ├── Export.js │ ├── SaveAs.js │ ├── keyIntoUnicode.js │ ├── saveToDropbox.js │ ├── colorConversion.js │ └── colorPresets.json ├── index.js └── models │ ├── GridStore.js │ ├── KeymappingsStore.js │ └── ColorStore.js ├── fonts ├── submona.ttf ├── unscii-16.ttf ├── RayMantaC64-Regular.otf ├── Tesserae-TypeDesign.otf ├── ImagoMundiMei-Regular.otf ├── ScrollBorder-Regular.otf └── Tesserae-4x4Extended.otf ├── icons ├── og-image.jpg ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-150x150.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── site.webmanifest └── safari-pinned-tab.svg ├── webpack.config.js ├── .babelrc ├── .eslintrc.json ├── webpack.dev.js ├── LICENSE ├── README.md ├── webpack.prod.js ├── index.html ├── package.json ├── style.css └── scripts └── Typr.U.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | old -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /fonts/submona.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/submona.ttf -------------------------------------------------------------------------------- /fonts/unscii-16.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/unscii-16.ttf -------------------------------------------------------------------------------- /icons/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/og-image.jpg -------------------------------------------------------------------------------- /icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/favicon-16x16.png -------------------------------------------------------------------------------- /icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/favicon-32x32.png -------------------------------------------------------------------------------- /icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/src/components/.DS_Store -------------------------------------------------------------------------------- /src/utils/pxToMm.js: -------------------------------------------------------------------------------- 1 | export function pxToMm(val, dpi) { 2 | return ((val/0.0393701)/dpi).toFixed(2) 3 | } 4 | -------------------------------------------------------------------------------- /icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /fonts/RayMantaC64-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/RayMantaC64-Regular.otf -------------------------------------------------------------------------------- /fonts/Tesserae-TypeDesign.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/Tesserae-TypeDesign.otf -------------------------------------------------------------------------------- /fonts/ImagoMundiMei-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/ImagoMundiMei-Regular.otf -------------------------------------------------------------------------------- /fonts/ScrollBorder-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/ScrollBorder-Regular.otf -------------------------------------------------------------------------------- /fonts/Tesserae-4x4Extended.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/fonts/Tesserae-4x4Extended.otf -------------------------------------------------------------------------------- /icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlotvonen/glyph-drawing-club/HEAD/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | module.exports =(env) => { 4 | return require(`./webpack.${env}.js`) 5 | } -------------------------------------------------------------------------------- /icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000000 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/geometry.js: -------------------------------------------------------------------------------- 1 | export const getBoundingRectangle = (point1, point2) => { 2 | const start_x = Math.min(point1[1], point2[1]) 3 | const end_x = Math.max(point1[1], point2[1]) 4 | 5 | const start_y = Math.min(point1[0], point2[0]) 6 | const end_y = Math.max(point1[0], point2[0]) 7 | 8 | return [[start_y, start_x], [end_y, end_x]] 9 | } 10 | -------------------------------------------------------------------------------- /src/components/EmptyCanvas.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore.js" 3 | 4 | class EmptyClear extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | } 13 | export default EmptyClear 14 | -------------------------------------------------------------------------------- /src/components/Cursor.js: -------------------------------------------------------------------------------- 1 | import React, {useRef} from "react" 2 | import { observer } from "mobx-react" 3 | 4 | const Cursor = ({x, y, w, h}) => ( 5 |
16 | ) 17 | 18 | export default observer(Cursor) -------------------------------------------------------------------------------- /src/components/GlyphClear.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore.js" 3 | 4 | class GlyphClear extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 | ) 13 | } 14 | } 15 | export default GlyphClear 16 | -------------------------------------------------------------------------------- /src/components/HideGrid.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | 4 | class HideGrid extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {"Hide grid (h):"} 9 | 15 |
16 | ) 17 | } 18 | } 19 | export default observer(HideGrid) 20 | -------------------------------------------------------------------------------- /src/components/PaintMode.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | 4 | class PaintMode extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {"Paint mode (b):"} 9 | 15 |
16 | ) 17 | } 18 | } 19 | export default observer(PaintMode) 20 | -------------------------------------------------------------------------------- /src/components/TypingMode.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | 4 | class TypingMode extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {"Typing mode (t / ESC):"} 9 | 15 |
16 | ) 17 | } 18 | } 19 | export default observer(TypingMode) 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import Canvas from "./components/Canvas" 4 | import Settings from "./components/Settings" 5 | import Preview from "./components/Preview" 6 | import ErrorBoundary from "./components/ErrorBoundary" 7 | 8 | render( 9 |
10 | 11 | 12 | 13 | 14 | 15 |
, 16 | document.getElementById("root") 17 | ) 18 | -------------------------------------------------------------------------------- /src/components/CellWidth.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class CellWidth extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Cell width:"} 10 | 11 | 12 | {store.cellWidth} px 13 |
14 | ) 15 | } 16 | } 17 | export default observer(CellWidth) -------------------------------------------------------------------------------- /src/components/CellHeight.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class CellHeight extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Cell height:"} 10 | 11 | 12 | {store.cellHeight} px 13 |
14 | ) 15 | } 16 | } 17 | export default observer(CellHeight) -------------------------------------------------------------------------------- /src/components/FontSize.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class FontSize extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Default font size:"} 10 | 11 | 12 | {store.defaultFontSize} px 13 |
14 | ) 15 | } 16 | } 17 | export default observer(FontSize) 18 | -------------------------------------------------------------------------------- /src/components/HistoryControls.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class HistoryControls extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 12 | 15 |
16 | ) 17 | } 18 | } 19 | export default observer(HistoryControls) 20 | -------------------------------------------------------------------------------- /icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Glyph Drawing Club", 3 | "short_name": "Glyph Drawing Club", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#000000", 17 | "background_color": "#000000", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ToggleMapping.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import setstore from "../models/KeymappingsStore" 4 | 5 | class ToggleMapping extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Map keys (m):"} 10 | 16 |
17 | ) 18 | } 19 | } 20 | export default observer(ToggleMapping) 21 | -------------------------------------------------------------------------------- /src/components/SelectedGlyph.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const SelectedGlyph = props => { 4 | return ( 5 |
6 |
7 | 19 | 20 | 21 |
22 |
23 | ) 24 | } 25 | 26 | export default SelectedGlyph 27 | -------------------------------------------------------------------------------- /src/components/GlyphList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import { observable, action } from "mobx" 4 | import store from "../models/CanvasStore.js" 5 | import { FixedSizeGrid as Grid } from 'react-window'; 6 | 7 | const Cell = ({ columnIndex, rowIndex, style }) => ( 8 |
9 | Item {rowIndex},{columnIndex} 10 |
11 | ); 12 | 13 | const Example = () => ( 14 | 22 | {Cell} 23 | 24 | ) -------------------------------------------------------------------------------- /src/components/QuickChooseColor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | import ColorPalette from "./ColorPalette" 5 | 6 | @observer 7 | class QuickChooseColor extends Component { 8 | 9 | 10 | render() { 11 | if(store.toggleQuickChooseColor) { 12 | return ( 13 |
14 |
15 | 16 |
17 |
18 | ) 19 | } else { 20 | return ( 21 | <> 22 | ) 23 | } 24 | } 25 | } 26 | export default QuickChooseColor 27 | -------------------------------------------------------------------------------- /src/components/UiLayer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import SelectionHighlight from "./SelectionHighlight" 3 | import Numbers from "./Numbers" 4 | import Cursor from "./Cursor" 5 | import UiGrid from "./UiGrid" 6 | import { observer } from "mobx-react"; 7 | import store from "../models/CanvasStore" 8 | 9 | @observer 10 | class UiLayer extends React.Component { 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | } 22 | export default UiLayer 23 | -------------------------------------------------------------------------------- /icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/react", 4 | "@babel/preset-env", 5 | ], 6 | "plugins": [ 7 | ["@babel/plugin-proposal-decorators", {"legacy": true}], 8 | ["@babel/plugin-proposal-class-properties", {"loose": true}], 9 | "@babel/plugin-proposal-export-default-from", 10 | "@babel/plugin-proposal-logical-assignment-operators", 11 | ["@babel/plugin-proposal-optional-chaining", { "loose": false }], 12 | ["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }], 13 | ["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }], 14 | "@babel/plugin-proposal-do-expressions", 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/detectOs.js: -------------------------------------------------------------------------------- 1 | export const getOS = () => { 2 | var userAgent = window.navigator.userAgent, 3 | platform = window.navigator.platform, 4 | macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], 5 | windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'], 6 | iosPlatforms = ['iPhone', 'iPad', 'iPod'], 7 | os = null; 8 | 9 | if (macosPlatforms.indexOf(platform) !== -1) { 10 | os = 'OSX'; 11 | } else if (iosPlatforms.indexOf(platform) !== -1) { 12 | os = 'iOS'; 13 | } else if (windowsPlatforms.indexOf(platform) !== -1) { 14 | os = 'Windows'; 15 | } else if (/Android/.test(userAgent)) { 16 | os = 'Android'; 17 | } else if (!os && /Linux/.test(platform)) { 18 | os = 'Linux'; 19 | } 20 | return os; 21 | } -------------------------------------------------------------------------------- /src/components/KeyMappings.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import setstore from "../models/KeymappingsStore" 4 | import ToggleMapping from "./ToggleMapping" 5 | import KeyMapping from "./KeyMapping" 6 | import SetSelect from "./SetSelect" 7 | 8 | @observer 9 | class KeyMappings extends React.Component { 10 | render() { 11 | const sets = setstore.sets 12 | return ( 13 |
14 | 15 | 16 | {sets.map((set, y) => ( 17 | 18 | ))} 19 |
20 | ) 21 | } 22 | } 23 | 24 | export default KeyMappings 25 | -------------------------------------------------------------------------------- /src/components/SaveAsButton.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import { observer } from "mobx-react" 4 | import { saveAs, saveSelectionAs } from "../utils/SaveAs" 5 | 6 | class SaveAsButton extends React.Component { 7 | render() { 8 | return ( 9 |
10 | Filename: 11 | store.toggleWriting()} 17 | onBlur={() => store.toggleWriting()} 18 | /> 19 | 20 | 21 |
22 | ) 23 | } 24 | } 25 | export default observer(SaveAsButton) 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended" 9 | ], 10 | "parser": "babel-eslint", 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 2017, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | "indent": [ 23 | "error", 24 | "tab" 25 | ], 26 | "linebreak-style": [ 27 | "error", 28 | "unix" 29 | ], 30 | "quotes": [ 31 | "error", 32 | "double" 33 | ], 34 | "semi": [ 35 | "error", 36 | "never" 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /src/components/GlyphFontSizeModifier.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class GlyphFontSize extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Glyph size modifier:"} 10 | 13 | 16 | 19 | 22 | {store.glyphFontSizeModifier} px 23 |
24 | ) 25 | } 26 | } 27 | export default observer(GlyphFontSize) 28 | -------------------------------------------------------------------------------- /src/components/SetSelect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import setstore from "../models/KeymappingsStore" 4 | 5 | class SetSelect extends React.Component { 6 | render() { 7 | const sets = setstore.sets 8 | const selectSetButtons = sets.map((setNumber, x) => ( 9 | 17 | )) 18 | 19 | return ( 20 |
21 | 24 | 27 | {"Select: "} 28 | {selectSetButtons} 29 |
30 | ) 31 | } 32 | } 33 | 34 | export default observer(SetSelect) 35 | -------------------------------------------------------------------------------- /src/components/UiGrid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | 5 | @observer 6 | class UiGrid extends React.Component { 7 | render() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | export default UiGrid -------------------------------------------------------------------------------- /src/components/KeyMapping.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import SelectedGlyph from "./SelectedGlyph" 3 | import setstore from "../models/KeymappingsStore" 4 | import { observer } from "mobx-react" 5 | 6 | const KeyMapping = observer(props => { 7 | return ( 8 |
14 |
15 | {Object.entries(props.keys).map(([keyName, glyph]) => { 16 | return ( 17 |
18 | {keyName} 19 | 26 |
27 | ) 28 | })} 29 |
30 |
31 | ) 32 | }) 33 | 34 | export default KeyMapping 35 | -------------------------------------------------------------------------------- /src/components/GridControls.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import gridstore from "../models/GridStore.js" 3 | 4 | class GridControls extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 | ) 25 | } 26 | } 27 | export default GridControls 28 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'eval', 6 | mode: 'development', 7 | entry: [ 8 | './src/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.HotModuleReplacementPlugin() 17 | ], 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | module: { 22 | rules: [{ 23 | test: /\.jsx?$/, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['@babel/react', '@babel/env'] 28 | } 29 | }, 30 | include: path.join(__dirname, 'src') 31 | }] 32 | }, 33 | devServer: { 34 | publicPath: '/static/', 35 | contentBase: path.resolve(__dirname, ""), 36 | watchContentBase: true, 37 | compress: true, 38 | port: 9000 39 | }, 40 | }; -------------------------------------------------------------------------------- /src/components/ColorPaletteSelect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import colorstore from "../models/ColorStore" 4 | 5 | class ColorPaletteSelect extends React.Component { 6 | render() { 7 | const palettes = colorstore.palettes 8 | const selectPaletteButtons = palettes.map((paletteNumber, x) => ( 9 | 17 | )) 18 | 19 | return ( 20 |
21 | 24 | 27 | {"Select: "} 28 | {selectPaletteButtons} 29 |
30 | ) 31 | } 32 | } 33 | 34 | export default observer(ColorPaletteSelect) 35 | -------------------------------------------------------------------------------- /src/components/GlyphInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | const GlyphInfo = () => ( 6 |
7 | width: {store.canvas[store.selected_y][store.selected_x][1]} 8 |
height: {store.canvas[store.selected_y][store.selected_x][2]} 9 |
baseline: {store.canvas[store.selected_y][store.selected_x][3]} 10 |
offset X: {store.canvas[store.selected_y][store.selected_x][4]} 11 |
offset Y: {store.canvas[store.selected_y][store.selected_x][9]} 12 |
font size modifier: +{store.canvas[store.selected_y][store.selected_x][5]}px 13 |
rotation: {store.canvas[store.selected_y][store.selected_x][6]}° 14 |
mirrored: {store.canvas[store.selected_y][store.selected_x][7] === 1 ? "no" : "yes"} 15 |
inverted shape: {store.canvas[store.selected_y][store.selected_x][8] === false ? "no" : "yes"} 16 |
17 | ); 18 | 19 | export default observer(GlyphInfo) -------------------------------------------------------------------------------- /src/models/GridStore.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from "mobx" 2 | import store from "./CanvasStore.js" 3 | 4 | class GridStore { 5 | posOffsetX = store.cellWidth 6 | posOffsetY = store.cellHeight 7 | 8 | @observable 9 | settings = { 10 | posX: 0, 11 | posY: 0, 12 | zoom: 1, 13 | } 14 | @action 15 | moveUp = () => { 16 | this.settings.posY += this.posOffsetY 17 | } 18 | @action 19 | moveRight = () => { 20 | this.settings.posX -= this.posOffsetX 21 | } 22 | @action 23 | moveDown = () => { 24 | this.settings.posY -= this.posOffsetY 25 | } 26 | @action 27 | moveLeft = () => { 28 | this.settings.posX += this.posOffsetX 29 | } 30 | @action 31 | center = () => { 32 | this.settings.posX = 0 33 | this.settings.posY = 0 34 | this.settings.zoom = 1 35 | } 36 | @action 37 | zoomIn = () => { 38 | this.settings.zoom = this.settings.zoom * 1.05 39 | } 40 | @action 41 | zoomOut = () => { 42 | this.settings.zoom = this.settings.zoom * 0.95 43 | } 44 | } 45 | export default new GridStore() 46 | -------------------------------------------------------------------------------- /src/components/GlyphOffset.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class GlyphOffsetX extends React.Component { 6 | render() { 7 | return ( 8 |
9 | {"Glyph offset X:"} 10 | 11 | 12 | {store.glyphOffsetX} 13 |
14 | {"Glyph offset Y:"} 15 | 16 | 17 | {store.glyphOffsetY} 18 |
19 | {"Offset amount:"} 20 | store.handleChangeOffsetAmount(evt)} 25 | onFocus={() => store.toggleWriting()} 26 | onBlur={() => store.toggleWriting()} 27 | /> 28 |
29 | ) 30 | } 31 | } 32 | export default observer(GlyphOffsetX) 33 | -------------------------------------------------------------------------------- /src/components/CanvasSizeInMillimeters.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | import { pxToMm } from "../utils/pxToMm" 5 | 6 | class CanvasSizeInMillimeters extends React.Component { 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | dpi: 150 12 | } 13 | } 14 | 15 | onChange = (num, e) => { 16 | this.setState({dpi: e.target.value}); 17 | } 18 | render() { 19 | return ( 20 |
21 | DPI: store.toggleWriting()} 26 | onBlur={() => store.toggleWriting()} 27 | /> 28 |
29 | {"Canvas width: "}{pxToMm(store.cellWidth * store.canvasWidth, this.state.dpi)} {"mm"} 30 |
31 | {"Canvas height: "}{pxToMm(store.cellHeight * store.canvasHeight, this.state.dpi)} {"mm"} 32 |
33 | ) 34 | } 35 | } 36 | 37 | export default observer(CanvasSizeInMillimeters) 38 | -------------------------------------------------------------------------------- /src/components/SelectionHighlight.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import { getBoundingRectangle } from "../utils/geometry" 4 | import { observer } from "mobx-react" 5 | 6 | const SelectionHighlight = props => { 7 | if (store.selectionArea.start === null) { 8 | return null 9 | } 10 | // console.log(store.selectionArea.start) 11 | const [[start_y, start_x], [end_y, end_x]] = getBoundingRectangle( 12 | store.selectionArea.start, 13 | store.selectionArea.end || [store.selected_y, store.selected_x] 14 | ) 15 | 16 | // console.log([start_y, start_x], [end_y, end_x]) 17 | 18 | let posTopX = start_x * store.cellWidth 19 | let posTopY = start_y * store.cellHeight 20 | let width = store.cellWidth * (end_x - start_x + 1) 21 | let height = store.cellHeight * (end_y - start_y + 1) 22 | 23 | return ( 24 |
33 | ) 34 | } 35 | 36 | export default observer(SelectionHighlight) 37 | -------------------------------------------------------------------------------- /src/components/CellBg.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import { observer } from "mobx-react" 4 | import { colorBlend } from "../utils/colorConversion" 5 | import colorStore from "../models/ColorStore" 6 | 7 | const CellBg = observer(props => { 8 | const bgColor = props.cell 9 | 10 | if (bgColor == "255") { 11 | return (null) 12 | } else{ 13 | return( 14 |
21 | {rawSvgCellBg({bgColor})} 22 |
23 | ) 24 | } 25 | }) 26 | 27 | export const rawSvgCellBg = ({bgColor}) => ( 28 | 34 | 35 | 36 | 37 | ) 38 | 39 | export default CellBg -------------------------------------------------------------------------------- /src/utils/cellsAsSvg.js: -------------------------------------------------------------------------------- 1 | import store from "../models/CanvasStore" 2 | import React, { Component } from "react" 3 | import { rawSvgCell } from "../components/Cell" 4 | import { rawSvgCellBg } from "../components/CellBg" 5 | 6 | export const cellsAsSvg = () => ( 7 | <> 8 | {store.canvas.map((row, y) => ( 9 | 10 | {row.map((cell, x) => ( 11 | 12 | {rawSvgCellBg({ 13 | bgColor: cell[4], 14 | })} 15 | 16 | ))} 17 | 18 | ))} 19 | 20 | {store.canvas.map((row, y) => ( 21 | 22 | {row.map((cell, x) => ( 23 | 24 | {rawSvgCell({ 25 | layer1: cell[0], 26 | layer2: cell[1], 27 | layer3: cell[2], 28 | layer4: cell[3] 29 | })} 30 | 31 | ))} 32 | 33 | ))} 34 | 35 | 36 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Michel Weststrate 4 | Copyright (c) 2018 hlotvonen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/utils/Export.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import FileSaver from "file-saver" 4 | import store from "../models/CanvasStore" 5 | import { rawSvgCell } from "../components/Cell" 6 | import { saveSvgAsPng } from "save-svg-as-png" 7 | import { cellsAsSvg } from "./cellsAsSvg" 8 | 9 | export function exportAs(type) { 10 | //Create SVG element 11 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") 12 | svg.setAttribute("width", (store.cellWidth * store.canvasWidth)) 13 | svg.setAttribute("height", (store.cellHeight * store.canvasHeight)) 14 | svg.setAttributeNS( 15 | "http://www.w3.org/2000/xmlns/", 16 | "xmlns:xlink", 17 | "http://www.w3.org/1999/xlink" 18 | ) 19 | 20 | ReactDOM.render(React.createElement(cellsAsSvg), svg) 21 | 22 | const blob = new Blob([ 23 | `${svg.outerHTML}`, 24 | ]) 25 | 26 | if(type == "svg") { 27 | FileSaver.saveAs(blob, store.fileName + ".svg") 28 | } 29 | else if(type == "png") { 30 | saveSvgAsPng(svg, store.fileName + ".png", { 31 | backgroundColor: "white", 32 | scale: store.exportSizeMultiplier / 2 33 | }) 34 | } else { 35 | return false 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/Numbers.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | 5 | @observer 6 | class Numbers extends Component { 7 | render() { 8 | const selected_y = store.selected_y 9 | const selected_x = store.selected_x 10 | 11 | const colNums = [] 12 | for (let i = 0; i < store.canvasWidth; i++) { 13 | colNums.push( 14 |
19 | {i + 1} 20 |
21 | ) 22 | } 23 | 24 | const rowNums = [] 25 | for (let i = 0; i < store.canvasHeight; i++) { 26 | rowNums.push( 27 |
36 | {i + 1} 37 |
38 | ) 39 | } 40 | 41 | return ( 42 |
43 |
{colNums}
44 |
{rowNums}
45 |
46 | ) 47 | } 48 | } 49 | export default Numbers 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Glyph Drawing Club 2 | ===================== 3 | 4 | [GlyphDrawing.club](http://www.glyphdrawing.club/) is a new and versatile online text art and modular design editor. It's inspired by the limitations and possibilities of old school ASCII art editors, but brought to modern times; the editor is based on an adjustable grid into which typographic symbols can be inserted from any font. It's best suitable for creating modular type design, illustrations, ASCII art, concrete poetry and more. 5 | 6 | Check "Help" tab on the site for keyboard shortcuts and usage. 7 | 8 | ### Font Licences 9 | 10 | Check detailed licencing info for fonts from the [wiki](https://github.com/hlotvonen/glyph-drawing-club/wiki/Fonts) 11 | 12 | ### Run locally 13 | 14 | ``` 15 | git clone git://github.com/hlotvonen/glyph-drawing-club/ 16 | npm install 17 | npm start 18 | ``` 19 | 20 | ### About 21 | GlyphDrawing.club has been designed and developed by [@hlotvonen](http://heikkilotvonen.fi) and [@i-tu](https://github.com/i-tu) using React and MobX. For help, ideas, contributions, etc. open an issue or send an email to [hlotvonen@gmail.com](mailto:hlotvonen@gmail.com). 22 | 23 | #### Notes 24 | * This project uses Typr.js, a javascript font processor: https://github.com/photopea/Typr.js 25 | -------------------------------------------------------------------------------- /src/components/Coordinates.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore.js" 3 | import { observer } from "mobx-react" 4 | import { getBoundingRectangle } from "../utils/geometry" 5 | 6 | 7 | class Coordinates extends React.Component { 8 | 9 | selectionSize() { 10 | if (store.selectionArea.start !== null) { 11 | 12 | let selectionWidth = 0 13 | let selectionHeight = 0 14 | 15 | if (store.selectionArea.end === null) { 16 | selectionWidth = Math.abs(store.selected_x - store.selectionArea.start[1]) + 1 17 | selectionHeight = Math.abs(store.selected_y - store.selectionArea.start[0]) + 1 18 | } else { 19 | selectionWidth = Math.abs(store.selectionArea.end[1] - store.selectionArea.start[1]) + 1 20 | selectionHeight = Math.abs(store.selectionArea.end[0] - store.selectionArea.start[0]) + 1 21 | } 22 | return ( 23 |
24 | selection x:{selectionWidth} y:{selectionHeight} 25 |
26 | ) 27 | 28 | } 29 | } 30 | 31 | render() { 32 | return ( 33 |
34 | x:{+store.selected_x + 1} y:{+store.selected_y + 1} 35 |
36 | {this.selectionSize()} 37 |
38 | ) 39 | } 40 | } 41 | export default observer(Coordinates) 42 | 43 | -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | import { cellsAsSvg } from "../utils/cellsAsSvg" 5 | import ReactDOM from "react-dom" 6 | 7 | @observer 8 | class Preview extends Component { 9 | render() { 10 | if(store.togglePreview) { 11 | const transformOrigin = window.innerHeight > (store.canvasHeight * store.cellHeight) ? "center" : "top" 12 | const canvasWidth = store.canvasWidth * store.cellWidth 13 | const canvasHeight = store.canvasHeight * store.cellHeight 14 | return ( 15 |
16 |
17 | 26 | {store.togglePreview ? cellsAsSvg() : null} 27 | 28 |
29 |
30 | ) 31 | } else { 32 | return ( 33 | <> 34 | ) 35 | } 36 | } 37 | } 38 | export default Preview 39 | -------------------------------------------------------------------------------- /src/components/ExportButtons.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { exportAs } from "../utils/Export" 3 | import store from "../models/CanvasStore" 4 | import { observer } from "mobx-react" 5 | 6 | class ExportButtons extends React.Component { 7 | render() { 8 | return ( 9 |
10 |
11 | {" "} 12 | Size: 13 | store.toggleWriting()} 20 | onBlur={() => store.toggleWriting()} 21 | />{" "} 22 | ({store.cellWidth * store.canvasWidth * store.exportSizeMultiplier} 23 | px ×{" "} 24 | {store.cellHeight * store.canvasHeight * store.exportSizeMultiplier} 25 | px) 26 |
27 |
28 | 29 | 34 | Read how to clean up SVG files 35 | 36 |
37 |
38 | ) 39 | } 40 | } 41 | export default observer(ExportButtons) 42 | -------------------------------------------------------------------------------- /src/components/SaveToDropboxButton.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { saveToDropbox } from "../utils/saveToDropbox" 3 | import store from "../models/CanvasStore" 4 | 5 | class SaveToDropboxButton extends React.Component { 6 | render() { 7 | return ( 8 |
9 |
10 | Full name or nickname: 11 | store.toggleWriting()} 17 | onBlur={() => store.toggleWriting()} 18 | /> 19 |
20 |
21 | Email: 22 | store.toggleWriting()} 28 | onBlur={() => store.toggleWriting()} 29 | /> 30 |
31 |
32 | Country: 33 | store.toggleWriting()} 39 | onBlur={() => store.toggleWriting()} 40 | /> 41 |
42 | 46 |
47 |
48 | ) 49 | } 50 | } 51 | export default SaveToDropboxButton 52 | -------------------------------------------------------------------------------- /src/components/CanvasSizeModification.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | class CanvasHeight extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | {"Canvas height:"} 12 | 13 | 14 | 15 | {store.canvasHeight} {"cells"} ({store.cellHeight * store.canvasHeight} px) 16 | 17 |
18 | {"Canvas width:"} 19 | 20 | 21 | 22 | {store.canvasWidth} {"cells"} ({store.cellWidth * store.canvasWidth} px) 23 | 24 |
25 |
26 | 27 |
28 | {"At selection:"} 29 |
30 | 33 | 36 |
37 | 40 | 43 |
44 | 45 |
46 | ) 47 | } 48 | } 49 | export default observer(CanvasHeight) 50 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | devtool: '', 8 | entry: [ 9 | './src/index' 10 | ], 11 | optimization: { 12 | runtimeChunk: true, 13 | splitChunks: { 14 | cacheGroups: { 15 | commons: { 16 | test: /[\\/]node_modules[\\/]/, 17 | name: 'vendors', 18 | chunks: 'all' 19 | } 20 | } 21 | }, 22 | minimizer: [ 23 | new UglifyJSPlugin({ 24 | uglifyOptions: { 25 | beautify: false, 26 | compress: true, 27 | ecma: 6, 28 | output: { 29 | comments: false 30 | }, 31 | compress: { 32 | dead_code: true 33 | } 34 | } 35 | })] 36 | }, 37 | output: { 38 | path: path.join(__dirname, 'dist'), 39 | filename: '[name].bundle.js', 40 | publicPath: '/dist/' 41 | }, 42 | plugins: [ 43 | new webpack.optimize.OccurrenceOrderPlugin(), 44 | ], 45 | resolve: { 46 | extensions: ['.js', '.jsx'] 47 | }, 48 | performance: { 49 | hints: false 50 | }, 51 | module: { 52 | rules: [{ 53 | test: /\.jsx?$/, 54 | use: { 55 | loader: 'babel-loader', 56 | options: { 57 | presets: ['@babel/react', '@babel/env'] 58 | } 59 | }, 60 | include: path.join(__dirname, 'src') 61 | }] 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react" 2 | import store from "../models/CanvasStore" 3 | 4 | class ErrorBoundary extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | hasError: false, 9 | errorMessage: '' 10 | }; 11 | } 12 | 13 | static getDerivedStateFromError(error) { 14 | // Update state so the next render will show the fallback UI. 15 | return { hasError: true }; 16 | } 17 | 18 | componentDidCatch(error, errorInfo) { 19 | // You can also log the error to an error reporting service 20 | this.setState({ 21 | errorMessage: 'Error: ' + error + ' Error Info: ' + errorInfo 22 | }) 23 | } 24 | 25 | resetPageAndClearLocalStorage() { 26 | localStorage.clear() 27 | location.reload() 28 | } 29 | 30 | render() { 31 | if (this.state.hasError) { 32 | // You can render any custom fallback UI 33 | return ( 34 |
35 |

I'm so sorry! Something bad happened, a bug probably.

36 |

PLEASE send the following error message to hlotvonen@gmail.com and describe what you where doing when the error happened.

37 |
38 |
{this.state.errorMessage}
39 |
40 |

To continue and reset the app, click the following:

41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | return this.props.children; 48 | } 49 | } 50 | 51 | export default ErrorBoundary -------------------------------------------------------------------------------- /src/utils/SaveAs.js: -------------------------------------------------------------------------------- 1 | import FileSaver from "file-saver" 2 | import store from "../models/CanvasStore" 3 | 4 | export function saveAs() { 5 | const json = localStorage.storage 6 | const blob = new Blob([json], { type: "application/json;charset=utf-8" }) 7 | FileSaver.saveAs(blob, store.fileName + ".gdc") 8 | } 9 | 10 | export function saveSelectionAs() { 11 | 12 | const json = JSON.parse(localStorage.storage) 13 | 14 | let selectionWidth = 0 15 | let selectionHeight = 0 16 | 17 | if (store.selectionArea.start !== null) { 18 | 19 | if (store.selectionArea.end === null) { 20 | selectionWidth = Math.abs(store.selected_x - store.selectionArea.start[1]) + 1 21 | selectionHeight = Math.abs(store.selected_y - store.selectionArea.start[0]) + 1 22 | } else { 23 | selectionWidth = Math.abs(store.selectionArea.end[1] - store.selectionArea.start[1]) + 1 24 | selectionHeight = Math.abs(store.selectionArea.end[0] - store.selectionArea.start[0]) + 1 25 | } 26 | } 27 | 28 | let selectionAreaCanvas = []; 29 | 30 | const boundingRectangle = store.getSelectedArea() 31 | const [[start_y, start_x]] = boundingRectangle 32 | 33 | for (let y_i = 0; y_i < selectionHeight; y_i++) { 34 | selectionAreaCanvas[y_i] = new Array(); 35 | for (let x_i = 0; x_i < selectionWidth; x_i++) { 36 | selectionAreaCanvas[y_i].push(store.canvas[start_y + y_i][start_x + x_i]) 37 | } 38 | } 39 | 40 | json.canvasWidth = selectionWidth 41 | json.canvasHeight = selectionHeight 42 | json.canvas = selectionAreaCanvas 43 | 44 | const blob = new Blob([JSON.stringify(json)], { type: "application/json;charset=utf-8" }) 45 | FileSaver.saveAs(blob, store.fileName + ".gdc") 46 | } -------------------------------------------------------------------------------- /src/components/ColorPresetSelect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import { action } from "mobx" 4 | import colorstore from "../models/ColorStore" 5 | 6 | class ColorPaletteSelect extends React.Component { 7 | render() { 8 | return ( 9 |
10 | Replace with: 11 | 39 |
40 | ) 41 | } 42 | } 43 | 44 | export default observer(ColorPaletteSelect) 45 | -------------------------------------------------------------------------------- /src/components/Grid.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | import Cell from "./Cell" 5 | import CellBg from "./CellBg" 6 | import UiLayer from "./UiLayer" 7 | import { CanvasMouseEvents } from "./CanvasMouseEvents" 8 | import gridStore from "../models/GridStore" 9 | 10 | @observer 11 | class Grid extends Component { 12 | 13 | render() { 14 | const canvas = store.canvas 15 | 16 | const gridBg = canvas.map((row, y) => ( 17 | 18 | {row.map((col, x) => ( 19 | 25 | ))} 26 | 27 | )) 28 | 29 | const gridFg = canvas.map((row, y) => ( 30 | 31 | {row.map((col, x) => ( 32 | 38 | ))} 39 | 40 | )) 41 | 42 | return ( 43 |
54 |
55 | {gridBg} 56 |
57 |
58 | {gridFg} 59 |
60 | 61 | 62 | 63 | { 64 | !store.altDown 65 | ? 66 | : "" 67 | } 68 |
69 | ) 70 | } 71 | } 72 | export default Grid 73 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Glyph Drawing Club 6 | 10 | 11 | 12 | 17 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/ColorPalette.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { action } from "mobx" 3 | import { observer } from "mobx-react" 4 | import colorStore, { colorPreset } from "../models/ColorStore" 5 | import { colorBlend } from "../utils/colorConversion" 6 | import invert from 'invert-color' 7 | import store from "../models/CanvasStore" 8 | 9 | class ColorPalette extends Component { 10 | render() { 11 | 12 | const canvas = store.canvas 13 | let uniqueColors = new Set() 14 | const list = canvas.map((row, x) => row.map((col, y) => { 15 | {uniqueColors.add(col[0][10])} 16 | {uniqueColors.add(col[1][10])} 17 | {uniqueColors.add(col[2][10])} 18 | {uniqueColors.add(col[3][10])} 19 | {uniqueColors.add(col[4][0])} 20 | } 21 | )) 22 | 23 | const colors = colorStore.palettes[colorStore.selectedPaletteIndex] 24 | 25 | const colorPalette = colors.map((color, i) => ( 26 |
colorStore.setFgColorIndex(i)} 36 | onContextMenu={(e, index) => colorStore.setBgColorIndex(e, i)} 37 | > 38 |
39 | {store.canvas[store.selected_y][store.selected_x][0][10] == i ? "1" : ""} 40 | {store.canvas[store.selected_y][store.selected_x][1][10] == i ? "2" : ""} 41 | {store.canvas[store.selected_y][store.selected_x][2][10] == i ? "3" : ""} 42 | {store.canvas[store.selected_y][store.selected_x][3][10] == i ? "4" : ""} 43 | {store.canvas[store.selected_y][store.selected_x][4][0] == i ? "b" : ""} 44 |
45 |
46 | )) 47 | return ( 48 |
49 | {colorPalette} 50 | {list} 51 |
52 | ) 53 | } 54 | } 55 | export default observer(ColorPalette) 56 | -------------------------------------------------------------------------------- /src/components/ColorSelect.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import { action } from "mobx" 4 | import colorStore from "../models/ColorStore" 5 | import store from "../models/CanvasStore" 6 | import ColorPalette from "./ColorPalette" 7 | import ColorSliders from "./ColorSliders" 8 | import ColorPaletteSelect from "./ColorPaletteSelect" 9 | import ColorPresetSelect from "./ColorPresetSelect" 10 | 11 | 12 | @observer 13 | class ColorSelect extends Component { 14 | render() { 15 | 16 | return ( 17 |
18 |

Color tools

19 | {"Coloring brush: "} 20 | {"FG"} 21 | colorStore.coloringModeFg = !colorStore.coloringModeFg)} 25 | />{" BG"} 26 | colorStore.coloringModeBg = !colorStore.coloringModeBg)} 30 | /> 31 |
32 | 33 | {"Show only used colors:"} 34 | colorStore.showUsedColors = !colorStore.showUsedColors)} 38 | /> 39 |
40 | 41 | 42 |
43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 | 51 |
52 |

Create cohesive color palette

53 |
54 | colorStore.changeIntensity(e.target.value)} 62 | /> 63 | {(colorStore.cohesionIntensity * 100).toFixed(0)}% Intensity with color: 64 |
65 |
colorStore.handleChangeCohesionColor()} 72 | /> 73 |
74 | 75 | 76 |
77 | ) 78 | } 79 | } 80 | export default ColorSelect 81 | -------------------------------------------------------------------------------- /src/utils/keyIntoUnicode.js: -------------------------------------------------------------------------------- 1 | export const KEY_INTO_UNICODE = { 2 | "!": 33, 3 | "\"": 34, 4 | "#": 35, 5 | $: 36, 6 | "%": 37, 7 | "&": 38, 8 | "'": 39, 9 | "(": 40, 10 | ")": 41, 11 | "*": 42, 12 | "+": 43, 13 | ",": 44, 14 | "-": 45, 15 | ".": 46, 16 | "/": 47, 17 | "0": 48, 18 | "1": 49, 19 | "2": 50, 20 | "3": 51, 21 | "4": 52, 22 | "5": 53, 23 | "6": 54, 24 | "7": 55, 25 | "8": 56, 26 | "9": 57, 27 | ":": 58, 28 | ";": 59, 29 | "<": 60, 30 | "=": 61, 31 | ">": 62, 32 | "?": 63, 33 | "@": 64, 34 | A: 65, 35 | B: 66, 36 | C: 67, 37 | D: 68, 38 | E: 69, 39 | F: 70, 40 | G: 71, 41 | H: 72, 42 | I: 73, 43 | J: 74, 44 | K: 75, 45 | L: 76, 46 | M: 77, 47 | N: 78, 48 | O: 79, 49 | P: 80, 50 | Q: 81, 51 | R: 82, 52 | S: 83, 53 | T: 84, 54 | U: 85, 55 | V: 86, 56 | W: 87, 57 | X: 88, 58 | Y: 89, 59 | Z: 90, 60 | "[": 91, 61 | "\\": 92, 62 | "]": 93, 63 | "^": 94, 64 | _: 95, 65 | "`": 96, 66 | a: 97, 67 | b: 98, 68 | c: 99, 69 | d: 100, 70 | e: 101, 71 | f: 102, 72 | g: 103, 73 | h: 104, 74 | i: 105, 75 | j: 106, 76 | k: 107, 77 | l: 108, 78 | m: 109, 79 | n: 110, 80 | o: 111, 81 | p: 112, 82 | q: 113, 83 | r: 114, 84 | s: 115, 85 | t: 116, 86 | u: 117, 87 | v: 118, 88 | w: 119, 89 | x: 120, 90 | y: 121, 91 | z: 122, 92 | "{": 123, 93 | "|": 124, 94 | "}": 125, 95 | "¿": 191, 96 | À: 192, 97 | Á: 193, 98 | Â: 194, 99 | Ã: 195, 100 | Ä: 196, 101 | Å: 197, 102 | Æ: 198, 103 | Ç: 199, 104 | È: 200, 105 | É: 201, 106 | Ê: 202, 107 | Ë: 203, 108 | Ì: 204, 109 | Í: 205, 110 | Î: 206, 111 | Ï: 207, 112 | Ð: 208, 113 | Ñ: 209, 114 | Ò: 210, 115 | Ó: 211, 116 | Ô: 212, 117 | Õ: 213, 118 | Ö: 214, 119 | "×": 215, 120 | Ø: 216, 121 | Ù: 217, 122 | Ú: 218, 123 | Û: 219, 124 | Ü: 220, 125 | Ý: 221, 126 | Þ: 222, 127 | ß: 223, 128 | à: 224, 129 | á: 225, 130 | â: 226, 131 | ã: 227, 132 | ä: 228, 133 | å: 229, 134 | æ: 230, 135 | ç: 231, 136 | è: 232, 137 | é: 233, 138 | ê: 234, 139 | ë: 235, 140 | ì: 236, 141 | í: 237, 142 | î: 238, 143 | ï: 239, 144 | ð: 240, 145 | ñ: 241, 146 | ò: 242, 147 | ó: 243, 148 | ô: 244, 149 | õ: 245, 150 | ö: 246, 151 | "÷": 247, 152 | ø: 248, 153 | ù: 249, 154 | ú: 250, 155 | û: 251, 156 | ü: 252, 157 | ý: 253, 158 | þ: 254, 159 | ÿ: 255, 160 | " ": 32, 161 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glyph-drawing-club", 3 | "version": "2.0.0", 4 | "description": "GlyphDrawing.club is a free contemporary text art and modular design editor.", 5 | "scripts": { 6 | "start": "webpack-dev-server --hot --open --env dev", 7 | "lint": "eslint src", 8 | "prettier": "prettier --write src/**/*.js", 9 | "build": "webpack --env prod" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hlotvonen/glyph-drawing-club.git" 14 | }, 15 | "author": "Heikki Lotvonen (https://github.com/hlotvonen/glyph-drawing-club)", 16 | "license": "MIT", 17 | "homepage": "https://glyphdrawing.club/", 18 | "devDependencies": { 19 | "@babel/core": "^7.9.6", 20 | "@babel/plugin-proposal-class-properties": "^7.8.3", 21 | "@babel/plugin-proposal-decorators": "^7.8.3", 22 | "@babel/plugin-proposal-do-expressions": "^7.8.3", 23 | "@babel/plugin-proposal-export-default-from": "^7.8.3", 24 | "@babel/plugin-proposal-logical-assignment-operators": "^7.8.3", 25 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3", 26 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 27 | "@babel/plugin-proposal-pipeline-operator": "^7.8.3", 28 | "@babel/plugin-transform-runtime": "^7.9.6", 29 | "@babel/preset-env": "^7.9.6", 30 | "@babel/preset-react": "^7.9.4", 31 | "@babel/preset-stage-1": "^7.8.3", 32 | "babel-eslint": "^10.1.0", 33 | "babel-loader": "^8.1.0", 34 | "brotli-gzip-webpack-plugin": "^0.5.0", 35 | "eslint": "^5.16.0", 36 | "eslint-plugin-react": "^7.19.0", 37 | "json-loader": "^0.5.7", 38 | "mobx-react-devtools": "^6.1.1", 39 | "prettier": "^1.19.1", 40 | "uglifyjs-webpack-plugin": "^1.1.2", 41 | "webpack": "^4.43.0", 42 | "webpack-cli": "^3.3.11", 43 | "webpack-dev-server": "^3.11.0" 44 | }, 45 | "dependencies": { 46 | "dropbox": "^4.0.30", 47 | "file-saver": "^2.0.2", 48 | "invert-color": "^2.0.0", 49 | "localforage": "^1.9.0", 50 | "mobx": "^5.15.4", 51 | "mobx-react": "^5.4.4", 52 | "mobx-store": "^3.4.0", 53 | "react": "^16.13.1", 54 | "react-dom": "^16.13.1", 55 | "react-tabs": "^3.1.0", 56 | "react-window": "^1.8.5", 57 | "save-svg-as-png": "^1.4.17" 58 | }, 59 | "prettier": { 60 | "trailingComma": "es5", 61 | "tabWidth": 2, 62 | "useTabs": true, 63 | "singleQuote": false, 64 | "semi": false 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/hlotvonen/glyph-drawing-club/issues" 68 | }, 69 | "main": "webpack.config.js" 70 | } 71 | -------------------------------------------------------------------------------- /src/components/LayerSelect.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer, action } from "mobx-react" 3 | import store from "../models/CanvasStore.js" 4 | 5 | const layerGlyph = (layer) => ( 6 |
7 | 12 | 13 | 14 |
15 | ) 16 | 17 | const LayerSelect = () => ( 18 |
19 |
20 | store.layerSelect(event)} value="0" name="layerSelect" checked={store.selectedLayer === 0}/> 21 | 1 22 | {layerGlyph(0)} 23 | {'Hide'} 24 | store.hideLayer(event)} value="0" /> 25 |
26 | 27 |
28 | 29 |
30 | store.layerSelect(event)} value="1" name="layerSelect" checked={store.selectedLayer === 1}/> 31 | 2 32 | {layerGlyph(1)} 33 | {'Hide'} 34 | store.hideLayer(event)} value="1" /> 35 |
36 | 37 | 38 |
39 | 40 |
41 | store.layerSelect(event)} value="2" name="layerSelect" checked={store.selectedLayer === 2}/> 42 | 3 43 | {layerGlyph(2)} 44 | {'Hide'} 45 | store.hideLayer(event)} value="2" /> 46 |
47 | 48 | 49 |
50 | 51 |
52 | store.layerSelect(event)} value="3" name="layerSelect" checked={store.selectedLayer === 3}/> 53 | 4 54 | {layerGlyph(3)} 55 | {'Hide'} 56 | store.hideLayer(event)} value="3" /> 57 |
58 | 59 |
60 | 61 |
62 | ); 63 | 64 | export default observer(LayerSelect) -------------------------------------------------------------------------------- /src/components/LoadAndPlace.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import { action } from "mobx" 4 | 5 | class LoadAndPlace extends React.Component { 6 | state = { 7 | waitingForFileUpload: false, 8 | } 9 | 10 | static readUploadedFileAsText = inputFile => { 11 | const temporaryFileReader = new FileReader() 12 | 13 | return new Promise((resolve, reject) => { 14 | temporaryFileReader.onerror = () => { 15 | temporaryFileReader.abort() 16 | reject(new DOMException("Problem parsing input file.")) 17 | } 18 | 19 | temporaryFileReader.onload = action(() => { 20 | resolve(temporaryFileReader.result) 21 | const jsonObj = JSON.parse(temporaryFileReader.result) 22 | 23 | //check if we are dealing with an old file, display warning and don't open file 24 | if (jsonObj["canvas"][0][0].length !== 5) { 25 | if (window.confirm("Can't open old file, sorry!")) { 26 | window.location.href = "https://www.glyphdrawing.club/" 27 | } 28 | throw new Error("Can't open old file, sorry!") 29 | } 30 | 31 | let canvasHeight = jsonObj["canvasHeight"] 32 | let canvasWidth = jsonObj["canvasWidth"] 33 | 34 | for (let y_i = 0; y_i < canvasHeight; y_i++) { 35 | for (let x_i = 0; x_i < canvasWidth; x_i++) { 36 | if ( 37 | store.selected_y + y_i >= store.canvasHeight || 38 | store.selected_x + x_i >= store.canvasWidth 39 | ) { 40 | continue 41 | } 42 | store.canvas[store.selected_y + y_i][ 43 | store.selected_x + x_i 44 | ].replace(jsonObj["canvas"][y_i][x_i]) 45 | } 46 | } 47 | }) 48 | temporaryFileReader.readAsText(inputFile) 49 | }) 50 | } 51 | //WIP make this into await / async 52 | @action 53 | uploadFile = event => { 54 | event.persist() 55 | 56 | if (!event.target || !event.target.files) { 57 | return 58 | } 59 | 60 | this.setState({ waitingForFileUpload: true }) 61 | 62 | const fileList = event.target.files 63 | 64 | // Uploads will push to the file input's `.files` array. Get the last uploaded file. 65 | const latestUploadedFile = event.target.files.item(fileList.length - 1) 66 | 67 | try { 68 | const fileContents = LoadAndPlace.readUploadedFileAsText( 69 | latestUploadedFile 70 | ) 71 | this.setState({ 72 | waitingForFileUpload: false, 73 | }) 74 | } catch (e) { 75 | console.log(e) 76 | this.setState({ 77 | waitingForFileUpload: false, 78 | }) 79 | } 80 | } 81 | 82 | render() { 83 | return ( 84 |
85 | {"Place from file: "} 86 | 87 |
88 | ) 89 | } 90 | } 91 | export default LoadAndPlace 92 | -------------------------------------------------------------------------------- /src/components/LoadButton.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import { action } from "mobx" 4 | 5 | class LoadButton extends React.Component { 6 | state = { 7 | waitingForFileUpload: false, 8 | } 9 | 10 | static readUploadedFileAsText = inputFile => { 11 | const temporaryFileReader = new FileReader() 12 | 13 | return new Promise((resolve, reject) => { 14 | temporaryFileReader.onerror = () => { 15 | temporaryFileReader.abort() 16 | reject(new DOMException("Problem parsing input file.")) 17 | } 18 | 19 | temporaryFileReader.onload = action(() => { 20 | resolve(temporaryFileReader.result) 21 | const jsonObj = JSON.parse(temporaryFileReader.result) 22 | 23 | //check if we are dealing with an old file, display warning and don't open file 24 | if (jsonObj["canvas"][0][0].length !== 5) { 25 | if ( 26 | window.confirm( 27 | "Can't open a file saved with an old GDC version 1.0., sorry!" 28 | ) 29 | ) { 30 | window.location.href = "https://www.glyphdrawing.club/" 31 | } 32 | throw new Error( 33 | "Can't open a file saved with an old GDC version 1.0., sorry!" 34 | ) 35 | } 36 | 37 | store.canvasHeight = jsonObj["canvasHeight"] 38 | store.canvasWidth = jsonObj["canvasWidth"] 39 | store.cellWidth = jsonObj["cellWidth"] 40 | store.cellHeight = jsonObj["cellHeight"] 41 | store.defaultFontSize = jsonObj["defaultFontSize"] 42 | store.canvas = jsonObj["canvas"] 43 | }) 44 | temporaryFileReader.readAsText(inputFile) 45 | }) 46 | } 47 | //WIP make this into await / async 48 | @action 49 | uploadFile = event => { 50 | event.persist() 51 | 52 | if (!event.target || !event.target.files) { 53 | return 54 | } 55 | 56 | this.setState({ waitingForFileUpload: true }) 57 | 58 | //reset selected_x and y and empty selection to prevent crashes 59 | store.selected_x = 0 60 | store.selected_y = 0 61 | store.emptySelection() 62 | 63 | const fileList = event.target.files 64 | 65 | // Uploads will push to the file input's `.files` array. Get the last uploaded file. 66 | const latestUploadedFile = event.target.files.item(fileList.length - 1) 67 | 68 | try { 69 | const fileContents = LoadButton.readUploadedFileAsText(latestUploadedFile) 70 | this.setState({ 71 | waitingForFileUpload: false, 72 | }) 73 | } catch (e) { 74 | console.log(e) 75 | this.setState({ 76 | waitingForFileUpload: false, 77 | }) 78 | } 79 | } 80 | 81 | render() { 82 | return ( 83 |
84 | {"Load from file: "} 85 | 86 |
87 | ) 88 | } 89 | } 90 | export default LoadButton 91 | -------------------------------------------------------------------------------- /src/components/CanvasMouseEvents.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import store from "../models/CanvasStore" 3 | import colorstore from "../models/ColorStore" 4 | import { action, toJS } from "mobx" 5 | 6 | export const CanvasMouseEvents = () => { 7 | const [position, setPosition] = useState({ x: 0, y: 0 }) 8 | 9 | function clamp(value, min, max) { 10 | return Math.min(Math.max(value, min), max) 11 | } 12 | 13 | const makeSelection = () => { 14 | //shift + s 15 | if (store.selectionArea.start && !store.selectionArea.end) { 16 | const [selectionStart, selectionEnd] = store.getSelectedArea() 17 | store.selectionArea.start = selectionStart 18 | store.selectionArea.end = selectionEnd 19 | } else { 20 | store.selectionArea.start = [position.y, position.x] 21 | store.selectionArea.end = null 22 | } 23 | } 24 | 25 | const handleMouseMove = action(e => { 26 | e.preventDefault() 27 | setPosition({ 28 | x: clamp( 29 | Math.floor(e.nativeEvent.offsetX / store.cellWidth), 30 | 0, 31 | store.canvasWidth - 1 32 | ), 33 | y: clamp( 34 | Math.floor(e.nativeEvent.offsetY / store.cellHeight), 35 | 0, 36 | store.canvasHeight - 1 37 | ), 38 | }) 39 | 40 | store.handlePaintEvents(position.x, position.y) 41 | 42 | if (store.mouseDown === true && store.shiftDown === true) { 43 | makeSelection() 44 | } 45 | }) 46 | 47 | const handleMouseDown = action(e => { 48 | e.preventDefault() 49 | store.clickSelection(position.x, position.y) 50 | if ( 51 | store.paintMode || 52 | colorstore.coloringModeFg || 53 | colorstore.coloringModeBg 54 | ) { 55 | store.paintLayer() 56 | } 57 | }) 58 | 59 | const handleMouseOver = e => { 60 | setPosition({ 61 | x: clamp( 62 | Math.floor(e.nativeEvent.offsetX / store.cellWidth), 63 | 0, 64 | store.canvasWidth - 1 65 | ), 66 | y: clamp( 67 | Math.floor(e.nativeEvent.offsetY / store.cellHeight), 68 | 0, 69 | store.canvasHeight - 1 70 | ), 71 | }) 72 | } 73 | 74 | document.addEventListener( 75 | "mousedown", 76 | function(event) { 77 | if (event.which) store.mouseDown = true 78 | }, 79 | true 80 | ) 81 | 82 | document.addEventListener( 83 | "mouseup", 84 | function(event) { 85 | if (event.which) store.mouseDown = false 86 | }, 87 | true 88 | ) 89 | 90 | return ( 91 |
handleMouseMove(e)} 95 | onMouseOver={e => handleMouseOver(e)} 96 | onMouseDown={e => handleMouseDown(e)} 97 | /> 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/saveToDropbox.js: -------------------------------------------------------------------------------- 1 | //import domtoimage from "dom-to-image" 2 | import { saveSvgAsPng } from "save-svg-as-png" 3 | import { Dropbox } from "dropbox" 4 | import store from "../models/CanvasStore" 5 | import React from "react" 6 | import ReactDOM from "react-dom" 7 | import { rawSvgCell } from "../components/Cell" 8 | 9 | export function saveToDropbox() { 10 | let scale = "scale(" + store.exportSizeMultiplier + ")" 11 | let style = { 12 | transform: scale, 13 | "transform-origin": "top left", 14 | } 15 | 16 | const ACCESS_TOKEN = "6epNSr0tHAQAAAAAAAAdD86QBYJlisfk9uIkgYSH6FZ5fSvBoPvKFEq3vqqxR1uv" 17 | const dbx = new Dropbox({ 18 | accessToken: ACCESS_TOKEN, 19 | }) 20 | 21 | if ( 22 | store.userFullName.trim() == "" || 23 | store.userEmail.trim() == "" || 24 | store.userCountry.trim() == "" 25 | ) { 26 | document.getElementById("dropbox-response").innerText = 27 | "Please fill in your info." 28 | return 29 | } 30 | 31 | store.hideGrid = true 32 | 33 | 34 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") 35 | svg.setAttribute("width", Number(store.widthPixels)) 36 | svg.setAttribute("height", Number(store.heightPixels)) 37 | svg.setAttributeNS( 38 | "http://www.w3.org/2000/xmlns/", 39 | "xmlns:xlink", 40 | "http://www.w3.org/1999/xlink" 41 | ) 42 | 43 | const cells = ( 44 | 45 | {store.canvas.map((row, y) => ( 46 | 47 | {row.map((cell, x) => ( 48 | 49 | {rawSvgCell({ 50 | glyphPath: cell[0], 51 | svgWidth: cell[1], 52 | svgHeight: cell[2], 53 | svgBaseline: cell[3], 54 | glyphOffsetX: cell[4], 55 | glyphFontSizeModifier: cell[5], 56 | rotationAmount: cell[6], 57 | flipGlyph: cell[7], 58 | glyphInvertedShape: cell[8], 59 | glyphOffsetY: cell[9], 60 | })} 61 | 62 | ))} 63 | 64 | ))} 65 | 66 | ) 67 | 68 | ReactDOM.render(cells, svg) 69 | 70 | const blob = new Blob([ 71 | `${svg.outerHTML}`, 72 | ]) 73 | 74 | dbx 75 | .filesUpload({ 76 | path: 77 | "/" + 78 | store.fileName + 79 | " " + 80 | store.userFullName + 81 | " " + 82 | store.userEmail + 83 | " " + 84 | store.userCountry + 85 | ".svg", 86 | contents: blob, 87 | }) 88 | .then(function(response) { 89 | document.getElementById("dropbox-response").innerText = 90 | "Sent successfully! Thank you!" 91 | console.log(response) 92 | }) 93 | .catch(function(error) { 94 | document.getElementById("dropbox-response").innerText = 95 | "Sorry something went wrong! Contact hlotvonen@gmail.com" 96 | console.error(error) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /src/utils/colorConversion.js: -------------------------------------------------------------------------------- 1 | export const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => { 2 | const hex = x.toString(16) 3 | return hex.length === 1 ? '0' + hex : hex 4 | }).join('') 5 | 6 | export const hexToRgb = hex => 7 | hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i 8 | ,(m, r, g, b) => '#' + r + r + g + g + b + b) 9 | .substring(1).match(/.{2}/g) 10 | .map(x => parseInt(x, 16)) 11 | 12 | export const colorBlend = (a, b, intensity) => { 13 | intensity = typeof intensity === 'undefined' ? 1 : intensity 14 | return a.reduce(function (result, current, index) { 15 | let value = (a[index] < 128) ? (2 * b[index] * a[index] / 255) : (255 - 2 * (255 - a[index]) * (255 - b[index]) / 255) 16 | value = (value * intensity + (a[index] * (1 - intensity))) 17 | return result.concat(Math.min(Math.round(value), 255)) 18 | }, []) 19 | } 20 | 21 | /** 22 | * Takes HSL values (H between 0 and 360, S and L each between 0 and 100) and returns the corresponding RGB values (each between 0 and 255) 23 | * Based on pseudo-code in the W3 Color Model document (http://www.w3.org/TR/2011/REC-css3-color-20110607/#hsl-color) 24 | */ 25 | export const hslToRgb = (h, s, l) => { 26 | let m1, m2, m3, r, g, b; 27 | 28 | h = h / 360; 29 | s = s / 100; 30 | l = l / 100; 31 | 32 | m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; 33 | 34 | m1 = l * 2 - m2; 35 | 36 | r = hueToRgb(m1, m2, h + 1/3); 37 | g = hueToRgb(m1, m2, h); 38 | b = hueToRgb(m1, m2, h - 1/3); 39 | 40 | return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)] 41 | } 42 | 43 | export const hueToRgb = (m1, m2, h) => { 44 | if(h < 0) { 45 | h = h + 1; 46 | } else if(h > 1) { 47 | h = h - 1; 48 | } 49 | 50 | if(h*6 < 1) { 51 | return m1 + (m2 - m1) * h * 6; 52 | } else if(h*2 < 1) { 53 | return m2; 54 | } else if(h*3 < 2) { 55 | return m1 + (m2 - m1) * (2/3 - h) * 6 56 | } 57 | 58 | return m1; 59 | } 60 | 61 | /** 62 | * Takes RGB values (each between 0 and 255) and returns the corresponding HSL values (H between 0 and 360, S and L each between 0 and 100). 63 | * Based on http://stackoverflow.com/a/9493060 64 | */ 65 | export const rgbToHsl = (r, g, b) => { 66 | let max, min, h, s, l; 67 | 68 | r = r / 255; 69 | g = g / 255; 70 | b = b / 255; 71 | 72 | max = Math.max(r, g, b); 73 | min = Math.min(r, g, b); 74 | 75 | l = (min + max) / 2; 76 | 77 | let diff = max - min; 78 | 79 | if (diff == 0) { 80 | s = 0; 81 | h = 0; 82 | } else { 83 | if(l > 0.5) { 84 | s = (diff) / (2 - min - max) 85 | } else { 86 | s = diff / (max + min) 87 | } 88 | 89 | switch(max) { 90 | case r: 91 | h = (g - b) / diff + (g < b ? 6 : 0); 92 | break; 93 | case g: 94 | h = (b - r) / diff + 2; 95 | break; 96 | case b: 97 | h = (r - g) / diff + 4; 98 | break; 99 | } 100 | } 101 | 102 | return [Math.round(h * 60), Math.round(s * 100), Math.round(l * 100)]; 103 | } -------------------------------------------------------------------------------- /src/models/KeymappingsStore.js: -------------------------------------------------------------------------------- 1 | import { action, observable, computed, autorun, mobx, toJS } from "mobx" 2 | import store from "./CanvasStore" 3 | import colorstore from "./ColorStore" 4 | 5 | const EMPTY_GLYPH = ["M0 0", "1", "1", "0"] 6 | const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] 7 | 8 | let keymappingsStorage 9 | 10 | class KeymappingsStore { 11 | constructor() { 12 | //load from localstorage if it's not the first time 13 | if (localStorage.keymapFirstRun) { 14 | keymappingsStorage = JSON.parse(localStorage.keymappingsStorage) 15 | this.sets = keymappingsStorage.sets 16 | //else create empty canvas 17 | } else { 18 | this.addSet() 19 | localStorage.setItem("keymapFirstRun", false) 20 | } 21 | //set localstorage, autorun will update it every time something changes 22 | autorun(() => { 23 | const keymappingsLocalstorage = { 24 | name: "keymappings", 25 | timestamp: Math.floor(Date.now() / 1000), 26 | sets: this.sets 27 | } 28 | localStorage.setItem("keymappingsStorage", JSON.stringify(keymappingsLocalstorage)) 29 | }) 30 | } 31 | @observable 32 | toggleMapping = false 33 | @observable 34 | selectedSetIndex = 0 35 | @observable 36 | sets = [] 37 | 38 | @action 39 | handleChangeMapping = () => { 40 | this.toggleMapping = !this.toggleMapping 41 | document.getElementById("toggleMapping").checked = this.toggleMapping 42 | } 43 | 44 | @action 45 | selectSet = index => { 46 | if (index >= 0 && index < this.sets.length) { 47 | this.selectedSetIndex = index 48 | } 49 | } 50 | 51 | @action 52 | addSet = () => { 53 | const set = {} 54 | for (const keyName of KEYS) { 55 | set[keyName] = EMPTY_GLYPH 56 | } 57 | this.sets.push(set) 58 | this.selectedSetIndex = this.sets.length - 1 59 | } 60 | 61 | @action 62 | deleteSet = () => { 63 | this.sets.splice(this.selectedSetIndex, 1) 64 | this.selectedSetIndex = this.selectedSetIndex - 1 65 | } 66 | 67 | @action 68 | prevSet = () => { 69 | if (this.selectedSetIndex > 0) { 70 | this.selectedSetIndex = this.selectedSetIndex - 1 71 | } 72 | } 73 | 74 | @action 75 | nextSet = () => { 76 | if (this.selectedSetIndex < this.sets.length) { 77 | this.selectedSetIndex = this.selectedSetIndex + 1 78 | } 79 | } 80 | 81 | @action 82 | setMapping = (keyName, glyph) => { 83 | const selectedSet = this.sets[this.selectedSetIndex] 84 | selectedSet[keyName] = glyph 85 | } 86 | 87 | @action 88 | getMapping = keyName => { 89 | const selectedSet = this.sets[this.selectedSetIndex] 90 | store.canvas[store.selected_y][store.selected_x][store.selectedLayer] = [ 91 | ...this.sets[this.selectedSetIndex][keyName], 92 | store.glyphOffsetX, 93 | store.glyphFontSizeModifier, 94 | store.rotationAmount, 95 | store.flipGlyph, 96 | store.glyphInvertedShape, 97 | store.glyphOffsetY, 98 | colorstore.colorIndex 99 | ] 100 | } 101 | } 102 | 103 | export default new KeymappingsStore() 104 | -------------------------------------------------------------------------------- /src/models/ColorStore.js: -------------------------------------------------------------------------------- 1 | import { action, observable, computed, autorun, mobx, toJS } from "mobx" 2 | import store from "./CanvasStore" 3 | import { colorBlend } from "../utils/colorConversion" 4 | import colorPresets from '../utils/colorPresets.json'; 5 | 6 | let colorStorage 7 | 8 | class ColorStore { 9 | constructor() { 10 | //load from localstorage if it's not the first time 11 | if (localStorage.colorFirstRun) { 12 | colorStorage = JSON.parse(localStorage.colorStorage) 13 | this.palettes = colorStorage.palettes 14 | //else create empty canvas 15 | } else { 16 | this.addPalette() 17 | localStorage.setItem("colorFirstRun", false) 18 | } 19 | //set localstorage, autorun will update it every time something changes 20 | autorun(() => { 21 | const colorLocalstorage = { 22 | name: "Color Palettes", 23 | timestamp: Math.floor(Date.now() / 1000), 24 | palettes: this.palettes 25 | } 26 | localStorage.setItem("colorStorage", JSON.stringify(colorLocalstorage)) 27 | }) 28 | } 29 | 30 | @observable 31 | colorIndex = 0 32 | @observable 33 | bgColorIndex = 255 34 | @observable 35 | cohesionIntensity = 0 36 | @observable 37 | cohesionOverlayColor = [255, 128, 32] 38 | @observable 39 | changingCohesionColor = false 40 | @observable 41 | coloringModeFg = false 42 | @observable 43 | coloringModeBg = false 44 | @observable 45 | showUsedColors = false 46 | @observable 47 | selectedPaletteIndex = 0 48 | @observable 49 | palettes = [] 50 | @observable 51 | selectedPresetPalette = "" 52 | 53 | @action 54 | colorSelect = (i, e) => { 55 | if (!this.changingCohesionColor) { 56 | this.palettes[this.selectedPaletteIndex][this.colorIndex][i] = Number(e) 57 | if (e < 0) { 58 | this.palettes[this.selectedPaletteIndex][this.colorIndex][i] = 0 59 | } 60 | if (e > 255) { 61 | this.palettes[this.selectedPaletteIndex][this.colorIndex][i] = 255 62 | } 63 | } else { 64 | this.cohesionOverlayColor[i] = Number(e) 65 | if (e < 0) { 66 | this.cohesionOverlayColor[i] = 0 67 | } 68 | if (e > 255) { 69 | this.cohesionOverlayColor[i] = 255 70 | } 71 | } 72 | } 73 | @action 74 | setFgColorIndex = index => { 75 | this.colorIndex = index 76 | } 77 | @action 78 | setBgColorIndex = (e, index) => { 79 | this.bgColorIndex = index 80 | e.preventDefault() 81 | } 82 | @action 83 | changeIntensity = e => { 84 | this.cohesionIntensity = e 85 | } 86 | @action 87 | handleChangeCohesionColor = () => { 88 | this.changingCohesionColor = !this.changingCohesionColor 89 | } 90 | 91 | 92 | @action 93 | handlePresetSelectChange = e => { 94 | this.selectedPresetPalette = Number(e) 95 | this.palettes[this.selectedPaletteIndex] = colorPresets[Number(e)].palette 96 | } 97 | 98 | @action 99 | selectPalette = index => { 100 | if (index >= 0 && index < this.palettes.length) { 101 | this.selectedPaletteIndex = index 102 | } 103 | } 104 | @action 105 | addPalette = () => { 106 | this.palettes.push(colorPresets[0].palette) 107 | this.selectedPaletteIndex = this.palettes.length - 1 108 | } 109 | 110 | @action 111 | deletePalette = () => { 112 | if (this.selectedPaletteIndex > 0) { 113 | this.palettes.splice(this.selectedPaletteIndex, 1) 114 | this.selectedPaletteIndex -= 1 115 | } 116 | } 117 | 118 | @action 119 | prevSet = () => { 120 | if (this.selectedPaletteIndex > 0) { 121 | this.selectedPaletteIndex -= 1 122 | } 123 | } 124 | 125 | @action 126 | nextSet = () => { 127 | if (this.selectedPaletteIndex < this.palette.length) { 128 | this.selectedPaletteIndex += 1 129 | } 130 | } 131 | 132 | 133 | 134 | } 135 | 136 | export default new ColorStore() 137 | -------------------------------------------------------------------------------- /src/components/ColorSliders.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import { action } from "mobx" 4 | import colorStore from "../models/ColorStore" 5 | import store from "../models/CanvasStore" 6 | import ColorPalette from "./ColorPalette" 7 | 8 | const setGradient = (steps) => { 9 | let gradientString = "linear-gradient(to right," 10 | let stepSize = 100 / (steps.length - 1); 11 | for(var i = 0; i < steps.length; i++) { 12 | gradientString += (i > 0 ? "," : "") + steps[i] + (i * stepSize) + "%" 13 | } 14 | return ( 15 | gradientString + ")" 16 | ) 17 | } 18 | const rgb = (r, g, b) => { 19 | return "rgb(" + r + "," + g + "," + b +")"; 20 | } 21 | 22 | @observer 23 | class ColorSliders extends Component { 24 | render() { 25 | const rgbSlider = (name, index, gradient) => { 26 | return ( 27 |
28 | colorStore.colorSelect(index, e.target.value)} 36 | style={{ 37 | background: setGradient(gradient) 38 | }} 39 | /> 40 | colorStore.colorSelect(index, e.target.value)} 46 | onFocus={() => store.toggleWriting()} 47 | onBlur={() => store.toggleWriting()} 48 | /> 49 | {name} 50 |
51 | ) 52 | } 53 | return ( 54 |
55 |
56 | {rgbSlider( 57 | "R", 58 | 0, 59 | colorStore.changingCohesionColor 60 | ? [rgb(0, colorStore.cohesionOverlayColor[1], colorStore.cohesionOverlayColor[2]),rgb(255, colorStore.cohesionOverlayColor[1], colorStore.cohesionOverlayColor[2])] 61 | : [rgb(0, colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][1], colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][2]),rgb(255, colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][1], colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][2])] 62 | )} 63 | {rgbSlider( 64 | "G", 65 | 1, 66 | colorStore.changingCohesionColor 67 | ? [rgb(colorStore.cohesionOverlayColor[0], 0, colorStore.cohesionOverlayColor[2]),rgb(colorStore.cohesionOverlayColor[0], 255, colorStore.cohesionOverlayColor[2])] 68 | : [rgb(colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][0], 0, colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][2]),rgb(colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][0], 255, colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][2])] 69 | )} 70 | {rgbSlider( 71 | "B", 72 | 2, 73 | colorStore.changingCohesionColor 74 | ? [rgb(colorStore.cohesionOverlayColor[0], colorStore.cohesionOverlayColor[1], 0), rgb(colorStore.cohesionOverlayColor[0], colorStore.cohesionOverlayColor[1], 255)] 75 | : [rgb(colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][0], colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][1], 0), rgb(colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][0], colorStore.palettes[colorStore.selectedPaletteIndex][colorStore.colorIndex][1], 255)] 76 | )} 77 |
78 |
87 |
88 | ) 89 | } 90 | } 91 | export default observer(ColorSliders) 92 | -------------------------------------------------------------------------------- /src/components/Canvas.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import { action } from "mobx" 4 | import store from "../models/CanvasStore.js" 5 | import setstore from "../models/KeymappingsStore" 6 | import gridstore from "../models/GridStore.js" 7 | import Grid from "./Grid" 8 | import GridControls from "./GridControls" 9 | import Coordinates from "./Coordinates" 10 | import SelectedGlyph from "./SelectedGlyph" 11 | import QuickChooseColor from "./QuickChooseColor" 12 | 13 | class Canvas extends Component { 14 | componentDidMount() { 15 | document.addEventListener("keydown", this.handleKeyPress, false) 16 | document.addEventListener("keyup", this.handleKeyPressUp, false) 17 | } 18 | componentWillUnmount() { 19 | document.removeEventListener("keydown", this.handleKeyPress, false) 20 | document.removeEventListener("keyup", this.handleKeyPressUp, false) 21 | } 22 | 23 | @action 24 | handleKeyPress = event => { 25 | //disable shortcuts if focus is on input element 26 | if (!store.disableShortcuts && !store.typingMode) { 27 | const glyph = [ 28 | store.glyphPath, 29 | store.svgWidth, 30 | store.svgHeight, 31 | store.svgBaseline, 32 | ] 33 | 34 | const handlers = !setstore.toggleMapping 35 | ? { 36 | ArrowRight: store.goRight, 37 | ArrowLeft: store.goLeft, 38 | ArrowDown: store.goDown, 39 | ArrowUp: store.goUp, 40 | " ": store.insertEmpty, 41 | Backspace: store.backSpace, 42 | Enter: store.insert, 43 | q: store.insert, 44 | e: store.insertEmptyCell, 45 | r: store.rotateGlyphRight, 46 | f: store.handleChangeFlip, 47 | h: store.handleChangeHideGrid, 48 | i: store.handleChangeInvertColor, 49 | b: store.handleChangePaintMode, 50 | z: store.handleUndoRedo, 51 | p: store.showPreview, 52 | b: store.insertBackground, 53 | v: store.colorFg, 54 | w: store.goUp, 55 | a: store.goLeft, 56 | s: store.goDown, 57 | d: store.goRight, 58 | x: store.showQuickChooseColor, 59 | c: store.copy, 60 | o: store.handleOffsetOn, 61 | ",": () => store.selectLayer("left"), 62 | ".": () => store.selectLayer("right"), 63 | "+": gridstore.zoomIn, 64 | "-": gridstore.zoomOut, 65 | 66 | //Modifier keys: 67 | Alt: store.handleAltDown, 68 | Meta: store.handleMetaDown, 69 | Control: store.handleCtrlDown, 70 | Shift: store.handleShiftDown, 71 | 72 | //Unused keys: 73 | //GNOPWX 74 | A: store.selectAll, 75 | S: store.makeSelection, 76 | C: store.copySelection, 77 | M: store.mirrorSelection, 78 | T: store.transposeSelection, 79 | R: store.rotateSelection, 80 | Y: store.rotateIndividuallySelection, 81 | U: store.flipIndividuallySelection, 82 | F: store.flipSelection, 83 | E: store.clearArea, 84 | Q: store.fillArea, 85 | B: store.fillBackgroundArea, 86 | V: store.colorFgSelectionArea, 87 | I: store.invertColorSelection, 88 | H: store.shiftAreaLeft, 89 | J: store.shiftAreaDown, 90 | K: store.shiftAreaUp, 91 | L: store.shiftAreaRight, 92 | D: store.emptySelection, 93 | 94 | m: setstore.handleChangeMapping, 95 | //draw glyph from keymap on to canvas 96 | 1: () => setstore.getMapping("1"), 97 | 2: () => setstore.getMapping("2"), 98 | 3: () => setstore.getMapping("3"), 99 | 4: () => setstore.getMapping("4"), 100 | 5: () => setstore.getMapping("5"), 101 | 6: () => setstore.getMapping("6"), 102 | 7: () => setstore.getMapping("7"), 103 | 8: () => setstore.getMapping("8"), 104 | 9: () => setstore.getMapping("9"), 105 | 0: () => setstore.getMapping("0"), 106 | } 107 | : { 108 | //assign selected glyph to keymap 109 | 1: () => setstore.setMapping("1", glyph), 110 | 2: () => setstore.setMapping("2", glyph), 111 | 3: () => setstore.setMapping("3", glyph), 112 | 4: () => setstore.setMapping("4", glyph), 113 | 5: () => setstore.setMapping("5", glyph), 114 | 6: () => setstore.setMapping("6", glyph), 115 | 7: () => setstore.setMapping("7", glyph), 116 | 8: () => setstore.setMapping("8", glyph), 117 | 9: () => setstore.setMapping("9", glyph), 118 | 0: () => setstore.setMapping("0", glyph), 119 | m: setstore.handleChangeMapping, 120 | } 121 | 122 | const handler = handlers[event.key] 123 | 124 | if (!handler) { 125 | return 126 | } 127 | 128 | handler() 129 | event.preventDefault() 130 | } 131 | } 132 | 133 | handleKeyPressUp = event => { 134 | const handlers = { 135 | Alt: store.handleAltUp, 136 | Control: store.handleCtrlUp, 137 | Meta: store.handleMetaUp, 138 | Shift: store.handleShiftUp, 139 | p: store.hidePreview, 140 | x: store.hideQuickChooseColor, 141 | o: store.handleOffsetOff, 142 | } 143 | const handler = handlers[event.key] 144 | 145 | if (!handler) { 146 | return 147 | } 148 | 149 | handler() 150 | event.preventDefault() 151 | } 152 | 153 | render() { 154 | return ( 155 |
156 |
157 | 158 | 159 | 160 |
161 | 167 |
168 | 169 |
170 |
171 | ) 172 | } 173 | } 174 | 175 | export default observer(Canvas) 176 | -------------------------------------------------------------------------------- /src/components/Cell.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import store from "../models/CanvasStore" 3 | import colorStore from "../models/ColorStore" 4 | import { observer } from "mobx-react" 5 | import { colorBlend } from "../utils/colorConversion" 6 | 7 | const Cell = observer(props => { 8 | const [ 9 | layer1, 10 | layer2, 11 | layer3, 12 | layer4 13 | ] = props.cell 14 | 15 | if(layer1[0] == "M0 0" && layer2[0] == "M0 0" && layer3[0] == "M0 0" && layer4[0] == "M0 0" ) { 16 | return (null) 17 | } else { 18 | return ( 19 | rawSvgCell({ 20 | layer1, 21 | layer2, 22 | layer3, 23 | layer4, 24 | 25 | }, props.x, props.y) 26 | ) 27 | } 28 | }) 29 | /* 30 | [ 31 | 0: glyphPath, 32 | 1: svgWidth, 33 | 2: svgHeight, 34 | 3: svgBaseline, 35 | 4: glyphOffsetX, 36 | 5: glyphFontSizeModifier, 37 | 6: rotationAmount, 38 | 7: flipGlyph, 39 | 8: glyphInvertedShape, 40 | 9: glyphOffsetY 41 | ] 42 | */ 43 | export const rawSvgCell = ({ 44 | layer1, 45 | layer2, 46 | layer3, 47 | layer4 48 | }, x, y) => ( 49 | store.handleClickSVG(x, y)} 59 | > 60 | { 61 | layer1[0] === "M0 0" || store.hiddenLayers[0] == 0 62 | ? null 63 | : ( 64 | 92 | 98 | 99 | ) 100 | } 101 | { 102 | layer2[0] === "M0 0" || store.hiddenLayers[1] == 1 103 | ? null 104 | : ( 105 | 131 | 137 | 138 | ) 139 | } 140 | { 141 | layer3[0] === "M0 0" || store.hiddenLayers[2] == 2 142 | ? null 143 | : ( 144 | 170 | 176 | 177 | ) 178 | } 179 | { 180 | layer4[0] === "M0 0" || store.hiddenLayers[3] == 3 181 | ? null 182 | : ( 183 | 209 | 215 | 216 | ) 217 | } 218 | 219 | ) 220 | 221 | export default Cell -------------------------------------------------------------------------------- /src/components/GlyphSelect.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react" 2 | import { observer } from "mobx-react" 3 | import { observable, action } from "mobx" 4 | import store from "../models/CanvasStore.js" 5 | import { KEY_INTO_UNICODE } from "../utils/keyIntoUnicode" 6 | 7 | class GlyphSelect extends Component { 8 | pageNumber = observable.box(0) 9 | selectedFont = observable.box("Tesserae Core") 10 | 11 | constructor(props) { 12 | super(props) 13 | 14 | this.state = { 15 | off: 0, 16 | font: [], 17 | num: 100, 18 | gid: 0, 19 | uncd: null, 20 | fontfile: "", 21 | pages_total: 0, 22 | inverted: false, 23 | credits: null 24 | } 25 | } 26 | componentDidMount() { 27 | this.go() 28 | document.addEventListener("keydown", this.handleKeyPress, false) 29 | } 30 | componentWillUnmount() { 31 | document.removeEventListener("keydown", this.handleKeyPress, false) 32 | } 33 | 34 | go = () => { 35 | if (this.selectedFont == "Tesserae Core") { 36 | this.load("fonts/Tesserae-TypeDesign.otf", this.fontLoaded) 37 | this.credits = null 38 | } else if (this.selectedFont == "Tesserae Extended") { 39 | this.load("fonts/Tesserae-4x4Extended.otf", this.fontLoaded) 40 | this.credits = null 41 | } else if (this.selectedFont == "Unscii") { 42 | this.load("fonts/unscii-16.ttf", this.fontLoaded) 43 | this.credits = "http://pelulamu.net/unscii/" 44 | } else if (this.selectedFont == "Submona") { 45 | this.load("fonts/submona.ttf", this.fontLoaded) 46 | this.credits = "https://github.com/pera/submona-web-font" 47 | } else if (this.selectedFont == "RayMantaC64") { 48 | this.load("fonts/RayMantaC64-Regular.otf", this.fontLoaded) 49 | this.credits = "http://datadoor.net/" 50 | } else if (this.selectedFont == "ImagoMundiMei") { 51 | this.load("fonts/ImagoMundiMei-Regular.otf", this.fontLoaded) 52 | this.credits = "http://velvetyne.fr/imagomundimei/" 53 | } else if (this.selectedFont == "ScrollBorder") { 54 | this.load("fonts/ScrollBorder-Regular.otf", this.fontLoaded) 55 | this.credits = null 56 | } 57 | 58 | this.node = document.body 59 | this.node.addEventListener("drop", this.onDrop, false) 60 | this.node.addEventListener("dragenter", this.cancel, false) 61 | this.node.addEventListener("dragleave", this.cancel, false) 62 | this.node.addEventListener("dragover", this.cancel, false) 63 | } 64 | 65 | ShowCredits() { 66 | if (!this.credits) { 67 | return null; 68 | } 69 | return ( 70 | <> 71 | {this.selectedFont} made by: {this.credits} 72 | 73 | ); 74 | } 75 | load(path, resp) { 76 | let request = new XMLHttpRequest() 77 | request.open("GET", path, true) 78 | request.responseType = "arraybuffer" 79 | request.onload = function(e) { 80 | resp(e.target.response) 81 | } 82 | request.send() 83 | } 84 | cancel(e) { 85 | e.stopPropagation() 86 | e.preventDefault() 87 | } 88 | @action 89 | fontLoaded = resp => { 90 | this.setState({ 91 | font: Typr.parse(resp), 92 | }) 93 | this.setState({ 94 | uncd: new Array(this.state.font.maxp.numGlyphs), 95 | }) 96 | 97 | for (let i = 0; i < 100000; i++) { 98 | let gid = Typr.U.codeToGlyph(this.state.font, i) 99 | if (gid == 0) { 100 | continue 101 | } 102 | if (this.state.uncd[gid] == null) { 103 | this.state.uncd[gid] = [i] 104 | } else { 105 | this.state.uncd[gid].push(i) 106 | } 107 | } 108 | 109 | this.setState({ 110 | gid: 0, 111 | off: 0, 112 | pages_total: Math.floor(this.glyphCnt() / this.state.num), 113 | }) 114 | 115 | this.drawGlyphs() 116 | this.glyphToSVG() 117 | 118 | this.pageNumber.set(0) 119 | 120 | store.fontName = this.state.font.name.fullName 121 | } 122 | @action 123 | handleKeyPress = event => { 124 | if (store.typingMode && !store.disableShortcuts) { 125 | const handlers = { 126 | ArrowRight: store.goRight, 127 | ArrowLeft: store.goLeft, 128 | ArrowDown: store.goDown, 129 | ArrowUp: store.goUp, 130 | Backspace: store.backSpace, 131 | Escape: store.handleChangeTypingMode, 132 | Enter: store.enter, 133 | "§": store.insert, 134 | } 135 | const handler = handlers[event.key] 136 | 137 | if (event.key in handlers) { 138 | handler() 139 | } 140 | if (KEY_INTO_UNICODE[event.key] !== undefined) { 141 | let path = Typr.U.glyphToPath( 142 | this.state.font, 143 | Typr.U.codeToGlyph( 144 | this.state.font, 145 | KEY_INTO_UNICODE[event.key] 146 | ) 147 | ) 148 | let svgstring = Typr.U.pathToSVG(path) 149 | store.glyphPath = svgstring 150 | store.svgWidth = this.state.font.hhea.advanceWidthMax 151 | store.svgHeight = 152 | this.state.font.hhea.ascender + 153 | Math.abs(this.state.font.hhea.descender) 154 | store.svgBaseline = this.state.font.hhea.descender 155 | 156 | store.canvas[store.selected_y][store.selected_x][store.selectedLayer].replace(store.getSelectedGlyph()) 157 | store.selected_x += 1 158 | 159 | if (store.selected_x == store.canvasWidth) { 160 | store.selected_x = 0 161 | if (store.selected_y < store.canvasHeight - 1) { 162 | store.selected_y += 1 163 | } else if (store.selected_y == store.canvasHeight) { 164 | store.selected_y -= 1 165 | } 166 | } 167 | } 168 | event.preventDefault() 169 | } 170 | } 171 | @action 172 | glyphToSVG = () => { 173 | let path = Typr.U.glyphToPath(this.state.font, this.state.gid) 174 | let svgstring = Typr.U.pathToSVG(path) 175 | store.glyphPath = svgstring 176 | store.svgWidth = this.state.font.hhea.advanceWidthMax 177 | store.svgHeight = 178 | this.state.font.hhea.ascender + Math.abs(this.state.font.hhea.descender) 179 | store.svgBaseline = this.state.font.hhea.descender 180 | } 181 | drawGlyphs = () => { 182 | let cont = document.getElementById("glyphcont") 183 | cont.innerHTML = "" 184 | let cnv = document.createElement("canvas") 185 | cnv.width = Math.floor(this.getDPR() * 40) 186 | cnv.height = Math.floor(this.getDPR() * 50) //scaleCnv(cnv); 187 | let ctx = cnv.getContext("2d") 188 | 189 | let lim = Math.min(this.state.off + this.state.num, this.glyphCnt()) 190 | let scale = (32 * this.getDPR()) / this.state.font.head.unitsPerEm 191 | 192 | 193 | for (let i = this.state.off; i < lim; i++) { 194 | let path = Typr.U.glyphToPath(this.state.font, i) 195 | 196 | cnv.width = cnv.width 197 | ctx.translate(5 * this.getDPR(), Math.round(30 * this.getDPR())) 198 | 199 | if ( this.selectedFont == "Tesserae 4x4") { 200 | ctx.fillStyle = "#f5f5f5" 201 | ctx.fillRect( 202 | 0, 203 | Math.round(-25.7 * this.getDPR()), 204 | Math.round(25.7 * this.getDPR()), 205 | Math.round(25.7 * this.getDPR()) 206 | ) 207 | } 208 | 209 | if (this.state.inverted) { 210 | ctx.fillStyle = "black" 211 | ctx.fillRect( 212 | 0, 213 | Math.round(-25 * this.getDPR()), 214 | Math.round(25 * this.getDPR()), 215 | Math.round(25 * this.getDPR()) 216 | ) 217 | } 218 | 219 | ctx.fillStyle = "black" 220 | ctx.font = "16px monospace" 221 | ctx.fillText(i+1, 0, 16) 222 | 223 | ctx.scale(scale, -scale) 224 | Typr.U.pathToContext(path, ctx) 225 | 226 | if (this.state.inverted) { 227 | ctx.fillStyle = "#f5f5f5" 228 | } else { 229 | ctx.fillStyle = "black" 230 | } 231 | ctx.fill() 232 | 233 | let img = document.createElement("img") 234 | img.setAttribute( 235 | "style", 236 | "width:" + 237 | cnv.width / this.getDPR() + 238 | "px; height:" + 239 | cnv.height / this.getDPR() + 240 | "px" 241 | ) 242 | img.gid = i 243 | img.onclick = this.glyphClick 244 | img.src = cnv.toDataURL() 245 | cont.appendChild(img) 246 | } 247 | } 248 | 249 | onDrop = e => { 250 | this.cancel(e) 251 | let fontLoaded = this.fontLoaded 252 | let r = new FileReader() 253 | let r64 = new FileReader() 254 | let file = e.dataTransfer.files[0] 255 | r.onload = function(e) { 256 | fontLoaded(e.target.result) 257 | } 258 | r.onloadend = function(e) { 259 | r64.readAsDataURL(file) 260 | r64.onloadend = function(e) { 261 | let base64result = r64.result.split(",")[1] 262 | } 263 | } 264 | r.readAsArrayBuffer(file) 265 | } 266 | 267 | glyphClick = e => { 268 | this.setState({ gid: e.target.gid }) 269 | this.glyphToSVG() 270 | } 271 | getDPR() { 272 | return window["devicePixelRatio"] || 1 273 | } 274 | glyphCnt = () => { 275 | return this.state.font.maxp.numGlyphs 276 | } 277 | @action 278 | drawNext = () => { 279 | if (this.pageNumber.get() < this.state.pages_total) { 280 | this.updatePageNum(this.pageNumber.get() + 1) 281 | } 282 | } 283 | @action 284 | drawPrev = () => { 285 | if (this.pageNumber.get() > 0) { 286 | this.updatePageNum(this.pageNumber.get() - 1) 287 | } 288 | } 289 | @action 290 | updatePageNum = value => { 291 | this.pageNumber.set(value) 292 | 293 | if (value === "") { 294 | return 295 | } 296 | 297 | if (this.pageNumber.get() > this.state.pages_total) { 298 | this.pageNumber.set(this.state.pages_total) 299 | } 300 | if (isNaN(value)) { 301 | return 302 | } 303 | 304 | this.state.off = this.pageNumber * this.state.num 305 | this.drawGlyphs() 306 | } 307 | handleFontSelectChange = value => { 308 | this.selectedFont = value 309 | this.go() 310 | } 311 | handleChangeInvert = () => { 312 | this.state.inverted = !this.state.inverted 313 | this.drawGlyphs() 314 | } 315 | render() { 316 | return ( 317 |
318 |

Glyph selection

319 | Select a preset font: 320 | 332 |
333 |
334 | 337 | 340 |
341 | Select page: 342 | store.toggleWriting()} 351 | onBlur={() => store.toggleWriting()} 352 | onChange={evt => { 353 | if (evt.target.value === "") { 354 | this.pageNumber.set("") 355 | return 356 | } 357 | 358 | const parsedValue = parseInt(evt.target.value, 10) 359 | if (isNaN(parsedValue)) { 360 | this.updatePageNum(0) 361 | return 362 | } 363 | 364 | this.updatePageNum(Math.max(0, parsedValue - 1)) 365 | }} 366 | /> 367 | /{this.state.pages_total + 1} 368 | {" Invert:"} 369 | 375 |
376 |
377 | 380 | 383 |
384 |
385 | {this.ShowCredits()} 386 |
387 | ) 388 | } 389 | } 390 | export default observer(GlyphSelect) 391 | -------------------------------------------------------------------------------- /src/components/Settings.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { observer } from "mobx-react" 3 | import store from "../models/CanvasStore" 4 | import CanvasSizeModification from "./CanvasSizeModification" 5 | import CellWidth from "./CellWidth" 6 | import CellHeight from "./CellHeight" 7 | import FontSize from "./FontSize" 8 | import GlyphSelect from "./GlyphSelect" 9 | import HideGrid from "./HideGrid" 10 | import KeyMappings from "./KeyMappings" 11 | import SelectedGlyph from "./SelectedGlyph" 12 | import ExportButtons from "./ExportButtons" 13 | import SaveAsButton from "./SaveAsButton" 14 | import LoadButton from "./LoadButton" 15 | import LoadAndPlace from "./LoadAndPlace" 16 | import EmptyCanvas from "./EmptyCanvas" 17 | import SaveToDropboxButton from "./SaveToDropboxButton" 18 | import TypingMode from "./TypingMode" 19 | import PaintMode from "./PaintMode" 20 | import HistoryControls from "./HistoryControls" 21 | import GlyphOffset from "./GlyphOffset" 22 | import GlyphFontSizeModifier from "./GlyphFontSizeModifier" 23 | import GlyphClear from "./GlyphClear" 24 | import CanvasSizeInMillimeters from "./CanvasSizeInMillimeters" 25 | import LayerSelect from "./LayerSelect" 26 | import ColorSelect from "./ColorSelect" 27 | import { Tab, Tabs, TabList, TabPanel } from "react-tabs" 28 | 29 | @observer 30 | class Settings extends React.Component { 31 | render() { 32 | return ( 33 |
34 | 35 | 36 | Draw 37 | Color 38 | Settings 39 | Save 40 | Help 41 | 42 | 43 | 44 |

Glyph sets

45 | 46 | 47 |

Select Layer

48 | 49 | 50 | 51 | 52 |

Modes & Tools

53 | 54 | 58 | 62 | 66 | 67 |

Glyph fine tuning

68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 |

Canvas size

79 | 80 | 81 | 82 |

Canvas size in millimeters

83 | 84 | 85 |

Cell size

86 | 87 | 88 | 93 | 94 |
95 |
96 | 97 | 98 |

Save / Load

99 | 103 | 104 | 105 | 106 |

Export

107 | 111 |
112 | {/* 113 |

Contribute to the new issue of GDC User Guide!

114 |

115 | If you would like to contribute your artwork to the upcoming 116 | GD.C User Guide v.2.0.0. zine, use the form below to submit your work. All contributions will be included to the zine. No limits! 117 |

118 | Last day to submit: 30.2.2019. The zine will be published during the late spring (date TBA) at Kosminen gallery in Helsinki. 119 |

120 | Contributors will receive a scanned pdf of the zine sent to email and can get the physical copy for the price of postage. 121 | The zines will be sold but only to cover printing costs and/or to fund future issues. 122 |

123 |
124 | 132 |

133 |
134 | By clicking the button above, you give permission to use your artwork in the zine and social media. 135 | Only your name and country will be published. 136 |

137 | */} 138 |

License:

139 |

140 | You are free to use anything you make with GlyphDrawingClub 141 | anywhere (private or commercial), without credits or licensing 142 | info. 143 |

144 |

Community links & project development:

145 | 149 | Github 150 | 151 |
152 |
153 | 154 | Instagram for examples & development updates 155 | 156 |
157 |
158 | 159 | Glyph Drawing Club blog 160 | 161 |
162 |
163 | 164 | Glyph Drawing Club discord channel for sharing, help, inspiration, community etc! Join now :) 165 | 166 |
167 | 168 | 169 |

Basic tutorial

170 |
    171 |
  1. 172 | Select the Draw tab from the 173 | sidebar 174 |
  2. 175 |
  3. 176 | Click on any glyph under Glyph 177 | selection. 178 |
  4. 179 |
  5. 180 | Press q to draw. 181 |
  6. 182 |
  7. 183 | Move around the canvas with{" "} 184 | arrow keys. 185 |
  8. 186 |
  9. 187 | Press f to flip,{" "} 188 | r to rotate or{" "} 189 | i to invert a glyph. 190 |
  10. 191 |
192 | 193 | Glyph Drawing Club is meant to be used with the keyboard, so check 194 | out all the shortcuts below! 195 | 196 | 197 |

Complete tutorial

198 |

199 | You can find a complete tutorial & more at the{" "} 200 | 204 | Glyph Drawing Club Blog 205 | 206 |

207 | 208 |

Keyboard Shortcuts

209 |

Move

210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 |
Arrow keys or WASDMove
Alt+Arrow keysQuickly move around the canvas
222 | 223 |

Draw

224 | Hold down CTRL if you want to affect all layers 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 |
qDraw (Place selected glyph)
e or spaceDelete
rRotate
fFlip
iInvert
249 | 250 |

Make glyph sets

251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 |
mStart mapping glyph sets. Press again to end mapping.
Number keys 1–10Add selected glyph to set (when mapping is on)
Number keys 1–10Insert glyph from set (when mapping is off)
267 | 268 |

Extra

269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 |
Cmd/Ctrl+zUndo
Cmd/Ctrl+Shift+zRedo
hHide Grid
(hold) pPreview
cCopy current glyph
293 | 294 |

Area selection

295 | Hold down CTRL if you want to affect all layers 296 | 297 | 298 | 299 | 300 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 351 | 352 | 353 | 354 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 |
Shift + s 301 | Start selection area. Use arrows keys to change the 302 | selection area. Press Shift + s again to lock selection area 303 |
Shift + dDeselect area
Shift + aSelect all
Shift + cPaste selected area
Shift + mMirror selected area
Shift + fFlip selected area
Shift + qFill selected area with selected glyph
Shift + eEmpty selected area
Shift + iInvert the colors of selected area
Shift + yRotate glyphs individually in selected area
Shift + uFlip glyphs individually in selected area
Shift + r 348 | Rotate selected area. Selection area has to be square (same 349 | amount of cells width & height) 350 |
Shift + t 355 | Transpose selected area. Selection area has to be square 356 | (same amount of cells width & height) 357 |
Shift + hMove selected area left
Shift + jMove selected area down
Shift + kMove selected area up
Shift + lMove selected area right
377 | 378 |

Coloring tools

379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 |
xColor palette quick access
vColor foreground
bColor background
Shift + vColor selected area foreground
Shift + bColor selected area background
403 |
404 |
405 |
406 | ) 407 | } 408 | } 409 | 410 | export default Settings 411 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | 50 | /* ~~~~~~~~~~~~~~~~~~~~~ 51 | document lvl 52 | ~~~~~~~~~~~~~~~~~~~~~ */ 53 | html, 54 | body { 55 | height: 100vh; 56 | padding: 0; 57 | margin: 0; 58 | font-family: monospace; 59 | background: #ddd; 60 | } 61 | html { 62 | box-sizing: border-box; 63 | } 64 | *, *:before, *:after { 65 | box-sizing: inherit; 66 | } 67 | b { 68 | font-weight: bold; 69 | } 70 | em { 71 | font-style:italic; 72 | margin:10px 0; 73 | display: block; 74 | } 75 | 76 | /* ~~~~~~~~~~~~~~~~~~~~~ 77 | top wrapper 78 | ~~~~~~~~~~~~~~~~~~~~~ */ 79 | #root { 80 | height: 100%; 81 | } 82 | .flex_wrapper { 83 | height: 100%; 84 | width: 100%; 85 | display: flex; 86 | flex-direction: row; 87 | } 88 | 89 | /* ~~~~~~~~~~~~~~~~~~~~~ 90 | canvas & grid 91 | ~~~~~~~~~~~~~~~~~~~~~ */ 92 | .canvas_container { 93 | background: #ddd; 94 | display: flex; 95 | flex-direction: column; 96 | overflow: hidden; 97 | flex: 1; 98 | } 99 | .aligner { 100 | display: flex; 101 | flex: 1; 102 | flex-direction: column; 103 | align-items: center; 104 | justify-content: center; 105 | overflow: hidden; 106 | position: relative; 107 | } 108 | .grid { 109 | background: white; 110 | position: absolute; 111 | } 112 | .gridBg { 113 | background: white; 114 | } 115 | .gridBg, 116 | .gridFg { 117 | position: absolute; 118 | top:0;left:0; 119 | } 120 | .UiGrid { 121 | position: absolute; 122 | top:0; 123 | left:0; 124 | pointer-events: none; 125 | opacity: 0.5; 126 | } 127 | .UiLayer { 128 | position: absolute; 129 | top:0; 130 | left:0; 131 | pointer-events: none; 132 | } 133 | /* ~~~~~~~~~~~~~~~~~~~~~ 134 | cells 135 | ~~~~~~~~~~~~~~~~~~~~~ */ 136 | .row div { 137 | display: inline-block; 138 | vertical-align:top; 139 | overflow: visible; 140 | position: relative; 141 | } 142 | .row div svg { 143 | position: absolute; 144 | top: 0; 145 | left: 0; 146 | pointer-events: none; 147 | } 148 | .row div svg rect, 149 | .row div svg path { 150 | pointer-events: none; 151 | } 152 | /* ~~~~~~~~~~~~~~~~~~~~~ 153 | grid numbers / cursor 154 | ~~~~~~~~~~~~~~~~~~~~~ */ 155 | .rowNums div.rowNum { 156 | position: absolute; 157 | display: flex; 158 | align-items: center; 159 | justify-content: center; 160 | width: 30px; 161 | left: -30px; 162 | box-shadow: none; 163 | text-align: right; 164 | font-size: 0.6em; 165 | background: none; 166 | color: #888; 167 | } 168 | .rowNums div.rowNum.highlighted { 169 | color: red; 170 | } 171 | .colNums { 172 | width: 100%; 173 | position: relative; 174 | height: 0; 175 | } 176 | .colNum { 177 | width: auto; 178 | display: inline-flex; 179 | justify-content: center; 180 | align-items: center; 181 | font-size: 0.6em; 182 | background: none; 183 | color:#888; 184 | position: relative; 185 | height: 25px; 186 | top: -25px; 187 | } 188 | .colNum.highlighted { 189 | color: red; 190 | } 191 | /* ~~~~~~~~~~~~~~~~~~~~~ 192 | selection highlight 193 | ~~~~~~~~~~~~~~~~~~~~~ */ 194 | .selectionHighlight { 195 | top: 0; 196 | left: 0; 197 | position: absolute; 198 | pointer-events: none; 199 | background-image: linear-gradient(90deg, red 50%, transparent 50%), linear-gradient(90deg, red 50%, transparent 50%), linear-gradient(0deg, red 50%, transparent 50%), linear-gradient(0deg, red 50%, transparent 50%); 200 | background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; 201 | background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px; 202 | background-position: left top, right bottom, left bottom, right top; 203 | animation: border-dance 1s infinite linear; 204 | } 205 | @keyframes border-dance { 206 | 0% { 207 | background-position: left top, right bottom, left bottom, right top; 208 | } 209 | 100% { 210 | background-position: left 15px top, right 15px bottom , left bottom 15px , right top 15px; 211 | } 212 | } 213 | .selectionHighlight.isSquare:after { 214 | content: 's'; 215 | position: absolute; 216 | bottom:0; 217 | right:0; 218 | } 219 | 220 | /* ~~~~~~~~~~~~~~~~~~~~~ 221 | coordinates 222 | ~~~~~~~~~~~~~~~~~~~~~ */ 223 | .coordinates { 224 | position: absolute; 225 | top: 0; 226 | right: 5px; 227 | } 228 | /* ~~~~~~~~~~~~~~~~~~~~~ 229 | colors 230 | ~~~~~~~~~~~~~~~~~~~~~ */ 231 | .colorPalette { 232 | display: flex; 233 | flex-wrap: wrap; 234 | background: #eee; 235 | } 236 | .color { 237 | float:left; 238 | width:25px; 239 | height:25px; 240 | cursor:pointer; 241 | font-size:9px; 242 | } 243 | .selectedColor { 244 | box-shadow: 0 0 0 1px rgba(0,255,0,1); 245 | } 246 | .colorSliders { 247 | margin-top: 10px; 248 | display: flex; 249 | } 250 | .rgbSliders { 251 | flex: 1; 252 | } 253 | .rgbSlider input[type="range"] { 254 | -webkit-appearance: none; 255 | outline: none; 256 | border-radius:10px; 257 | } 258 | .colorPreview { 259 | flex: 1; 260 | } 261 | .intensitySlider { 262 | float: left; 263 | } 264 | 265 | /* ~~~~~~~~~~~~~~~~~~~~~ 266 | grid move / zoom 267 | ~~~~~~~~~~~~~~~~~~~~~ */ 268 | .grid_controls { 269 | position: absolute; 270 | bottom: 0; 271 | right: 5px; 272 | width: 70px; 273 | height: 100px; 274 | } 275 | .grid_controls button{ 276 | width: 20px; 277 | height: 20px; 278 | padding: 0; 279 | margin-right: 3px; 280 | vertical-align: middle; 281 | border:none; 282 | background: #eee; 283 | box-shadow: 0 0 0 1px rgba(0,0,0,1); 284 | } 285 | .grid_controls button:hover { 286 | transform: scale(1.1); 287 | } 288 | .grid_controls .zoom { 289 | display: flex; 290 | justify-content: flex-end; 291 | margin-bottom: 5px 292 | } 293 | .grid_controls .zoom button:first-child { 294 | margin-right: 4px; 295 | } 296 | .grid_controls .move_y { 297 | display: block; 298 | margin: 3px auto; 299 | width: 25px; 300 | } 301 | 302 | /* ~~~~~~~~~~~~~~~~~~~~~ 303 | hide grid checked 304 | ~~~~~~~~~~~~~~~~~~~~~ */ 305 | .canvas_container.hideGrid { 306 | background: white; 307 | } 308 | .canvas_container.hideGrid .cursor, 309 | .canvas_container.hideGrid .colNums, 310 | .canvas_container.hideGrid .rowNums, 311 | .canvas_container.hideGrid .UiGrid { 312 | display: none; 313 | } 314 | 315 | /* ~~~~~~~~~~~~~~~~~~~~~ 316 | text styles 317 | ~~~~~~~~~~~~~~~~~~~~~ */ 318 | ol.instructions { 319 | list-style: initial; 320 | list-style-type:decimal; 321 | list-style-position: inside; 322 | margin-top: 0; 323 | } 324 | ol li { 325 | margin-top: 5px; 326 | } 327 | span.hotkey { 328 | background: #eee; 329 | padding: 2px; 330 | border-radius: 5px; 331 | } 332 | #dropbox-response { 333 | color:red; 334 | } 335 | h3 { 336 | width: 100%; 337 | background: #eee; 338 | font-weight: bold; 339 | padding: 0.5em; 340 | margin: 1em 0 0.5em 0; 341 | text-transform: uppercase; 342 | letter-spacing: 1px; 343 | } 344 | h4 { 345 | font-weight: bold; 346 | margin: 1em 0 0.5em 0; 347 | } 348 | table, 349 | tr { 350 | width: 100%; 351 | } 352 | tr { 353 | border-bottom:1px dotted grey; 354 | } 355 | td { 356 | padding-bottom: 2px; 357 | } 358 | table tr td { 359 | width: 65%; 360 | } 361 | table tr td:first-child { 362 | width: 35%; 363 | } 364 | 365 | /* ~~~~~~~~~~~~~~~~~~~~~ 366 | controls 367 | ~~~~~~~~~~~~~~~~~~~~~ */ 368 | .controls_container { 369 | background: white; 370 | width: 430px; 371 | padding: 5px; 372 | overflow-y: scroll; 373 | flex: none; 374 | box-shadow: 0 0 2px 0 rgba(0,0,0,0.3); 375 | 376 | } 377 | .control_component { 378 | display:block; 379 | margin-bottom: 0.5em; 380 | } 381 | .header { 382 | width: 30%; 383 | display:inline-block; 384 | } 385 | button { 386 | margin-right: 0.5em; 387 | font-family:monospace; 388 | } 389 | .page_selection, 390 | .control_inputs { 391 | display: inline-block; 392 | } 393 | #page_select_input.input-error { 394 | color:red; 395 | } 396 | 397 | #glyphcont img:hover { 398 | box-shadow: inset 0 0 0 1px rgba(255,0,0,1); 399 | cursor: pointer; 400 | } 401 | .select_set button.active { 402 | background-color: lime; 403 | } 404 | .settingsBlock { 405 | padding-bottom: 1em; 406 | } 407 | .layerSelect { 408 | display: flex; 409 | } 410 | .layerSelect div { 411 | flex:1; 412 | } 413 | /* ~~~~~~~~~~~~~~~~~~~~~ 414 | Preview 415 | ~~~~~~~~~~~~~~~~~~~~~ */ 416 | .preview { 417 | display: block; 418 | position: absolute; 419 | top:0; 420 | left:0; 421 | width: 100vw; 422 | height: 100vh; 423 | background: black; 424 | z-index: 1000; 425 | overflow: hidden; 426 | } 427 | .preview > div { 428 | position: absolute; 429 | top: 50%; 430 | left: 50%; 431 | transform: translateX(-50%) translateY(-50%); 432 | max-width: 100%; 433 | max-height: 100%; 434 | } 435 | .preview svg { 436 | overflow: visible; 437 | } 438 | /* ~~~~~~~~~~~~~~~~~~~~~ 439 | QuickChooseColor 440 | ~~~~~~~~~~~~~~~~~~~~~ */ 441 | .quickChooseColor { 442 | position: absolute; 443 | top:0; 444 | left:0; 445 | width:100%; 446 | height:100%; 447 | z-index: 999; 448 | display: flex; 449 | justify-content: center; 450 | align-items: center; 451 | } 452 | .quickChooseColor .paletteContainer { 453 | width:400px; 454 | box-shadow: 0 0 5px 0 rgba(0,0,0,0.3); 455 | } 456 | /* ~~~~~~~~~~~~~~~~~~~~~ 457 | keymappings bar 458 | ~~~~~~~~~~~~~~~~~~~~~ */ 459 | .KeyMappingsBar { 460 | width: 100%; 461 | background: white; 462 | display: flex; 463 | box-sizing:border-box; 464 | padding: 5px; 465 | } 466 | .KeyMappingsBar .Sets { 467 | flex: 1; 468 | } 469 | .KeyMappingsContainer { 470 | display: none; 471 | } 472 | .KeyMappingsFlexer { 473 | display: flex; 474 | flex-direction: row; 475 | } 476 | .KeyMappingsContainer .KeyMappingsFlexer div:first-child { 477 | order:1; 478 | } 479 | .KeyMappingsContainer.active { 480 | display: block; 481 | } 482 | .KeyMappingsContainer .KeyMappingsFlexer > div { 483 | text-align: center; 484 | box-sizing:border-box; 485 | width: 40px; 486 | border:none; 487 | display: inline-block; 488 | position: relative; 489 | overflow: visible; 490 | } 491 | .ToggleMapping, 492 | .select_set { 493 | display: inline-block; 494 | margin-right: 4px; 495 | } 496 | 497 | /* ~~~~~~~~~~~~~~~~~~~~~ 498 | selected glyph in keymappings bar 499 | ~~~~~~~~~~~~~~~~~~~~~ */ 500 | .selectedGlyph { 501 | position: relative; 502 | } 503 | .selectedGlyph .vector { 504 | height: 30px; 505 | width: 30px; 506 | border: 1px solid black; 507 | overflow: visible; 508 | box-sizing:border-box; 509 | position: relative; 510 | display: inline-block; 511 | } 512 | .SelectedGlyphContainer { 513 | width: 185px; 514 | height: 50px; 515 | margin-top: auto; 516 | position: absolute; 517 | left:10px; 518 | bottom: 10px; 519 | } 520 | .SelectedGlyphContainer .selectedGlyph { 521 | display: inline; 522 | } 523 | .SelectedGlyphContainer .selectedGlyph .vector { 524 | width: 50px; 525 | height: 50px; 526 | background: white; 527 | } 528 | #properties { 529 | display:block; 530 | width: 100px; 531 | margin-left: 5px; 532 | position: absolute; 533 | top: 0; 534 | left: 80px; 535 | } 536 | .selectedGlyph .vector svg { 537 | height: 100%; 538 | overflow: visible; 539 | position: absolute; 540 | top:0;left:0;right:0;bottom:0; 541 | margin:auto; 542 | } 543 | 544 | /* ~~~~~~~~~~~~~~~~~~~~~ 545 | react-tabs 546 | ~~~~~~~~~~~~~~~~~~~~~ */ 547 | .react-tabs { 548 | -webkit-tap-highlight-color: transparent; 549 | } 550 | 551 | .react-tabs__tab-list { 552 | border-bottom: 1px solid #aaa; 553 | margin: 0 0 10px; 554 | padding: 0; 555 | } 556 | 557 | .react-tabs__tab { 558 | display: inline-block; 559 | border: 1px solid transparent; 560 | border-bottom: none; 561 | bottom: -1px; 562 | position: relative; 563 | list-style: none; 564 | padding: 6px 12px; 565 | cursor: pointer; 566 | } 567 | 568 | .react-tabs__tab--selected { 569 | background: #fff; 570 | border-color: #aaa; 571 | color: black; 572 | border-radius: 5px 5px 0 0; 573 | } 574 | 575 | .react-tabs__tab--disabled { 576 | color: GrayText; 577 | cursor: default; 578 | } 579 | 580 | .react-tabs__tab:focus { 581 | box-shadow: 0 0 5px hsl(208, 99%, 50%); 582 | border-color: hsl(208, 99%, 50%); 583 | outline: none; 584 | } 585 | 586 | .react-tabs__tab:focus:after { 587 | content: ""; 588 | position: absolute; 589 | height: 5px; 590 | left: -4px; 591 | right: -4px; 592 | bottom: -5px; 593 | background: #fff; 594 | } 595 | 596 | .react-tabs__tab-panel { 597 | display: none; 598 | } 599 | 600 | .react-tabs__tab-panel--selected { 601 | display: block; 602 | } -------------------------------------------------------------------------------- /scripts/Typr.U.js: -------------------------------------------------------------------------------- 1 | 2 | if(Typr ==null) Typr = {}; 3 | if(Typr.U==null) Typr.U = {}; 4 | 5 | 6 | Typr.U.codeToGlyph = function(font, code) 7 | { 8 | var cmap = font.cmap; 9 | 10 | 11 | var tind = -1; 12 | if(cmap.p0e4!=null) tind = cmap.p0e4; 13 | else if(cmap.p3e1!=null) tind = cmap.p3e1; 14 | else if(cmap.p1e0!=null) tind = cmap.p1e0; 15 | 16 | if(tind==-1) throw "no familiar platform and encoding!"; 17 | 18 | var tab = cmap.tables[tind]; 19 | 20 | if(tab.format==0) 21 | { 22 | if(code>=tab.map.length) return 0; 23 | return tab.map[code]; 24 | } 25 | else if(tab.format==4) 26 | { 27 | var sind = -1; 28 | for(var i=0; icode) return 0; 31 | 32 | var gli = 0; 33 | if(tab.idRangeOffset[sind]!=0) gli = tab.glyphIdArray[(code-tab.startCount[sind]) + (tab.idRangeOffset[sind]>>1) - (tab.idRangeOffset.length-sind)]; 34 | else gli = code + tab.idDelta[sind]; 35 | return gli & 0xFFFF; 36 | } 37 | else if(tab.format==12) 38 | { 39 | if(code>tab.groups[tab.groups.length-1][1]) return 0; 40 | for(var i=0; i-1) Typr.U._simpleGlyph(gl, path); 69 | else Typr.U._compoGlyph (gl, font, path); 70 | } 71 | } 72 | Typr.U._simpleGlyph = function(gl, p) 73 | { 74 | for(var c=0; c=g) return cd.class[i]; 138 | return 0; 139 | } 140 | 141 | Typr.U.getPairAdjustment = function(font, g1, g2) 142 | { 143 | if(font.GPOS) 144 | { 145 | var ltab = null; 146 | for(var i=0; irlim) continue; 259 | var good = true; 260 | for(var l=0; l> 1; 393 | stack.length = 0; 394 | haveWidth = true; 395 | } 396 | else if(v=="o3" || v=="o23") // vstem || vstemhm 397 | { 398 | var hasWidthArg; 399 | 400 | // The number of stem operators on the stack is always even. 401 | // If the value is uneven, that means a width is specified. 402 | hasWidthArg = stack.length % 2 !== 0; 403 | if (hasWidthArg && !haveWidth) { 404 | width = stack.shift() + font.Private.nominalWidthX; 405 | } 406 | 407 | nStems += stack.length >> 1; 408 | stack.length = 0; 409 | haveWidth = true; 410 | } 411 | else if(v=="o4") 412 | { 413 | if (stack.length > 1 && !haveWidth) { 414 | width = stack.shift() + font.Private.nominalWidthX; 415 | haveWidth = true; 416 | } 417 | if(open) Typr.U.P.closePath(p); 418 | 419 | y += stack.pop(); 420 | Typr.U.P.moveTo(p,x,y); open=true; 421 | } 422 | else if(v=="o5") 423 | { 424 | while (stack.length > 0) { 425 | x += stack.shift(); 426 | y += stack.shift(); 427 | Typr.U.P.lineTo(p, x, y); 428 | } 429 | } 430 | else if(v=="o6" || v=="o7") // hlineto || vlineto 431 | { 432 | var count = stack.length; 433 | var isX = (v == "o6"); 434 | 435 | for(var j=0; j Math.abs(c4y - y)) { 531 | x = c4x + stack.shift(); 532 | } else { 533 | y = c4y + stack.shift(); 534 | } 535 | Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); 536 | Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y); 537 | } 538 | } 539 | else if(v=="o14") 540 | { 541 | if (stack.length > 0 && !haveWidth) { 542 | width = stack.shift() + font.nominalWidthX; 543 | haveWidth = true; 544 | } 545 | if(stack.length==4) // seac = standard encoding accented character 546 | { 547 | 548 | var asb = 0; 549 | var adx = stack.shift(); 550 | var ady = stack.shift(); 551 | var bchar = stack.shift(); 552 | var achar = stack.shift(); 553 | 554 | 555 | var bind = Typr.CFF.glyphBySE(font, bchar); 556 | var aind = Typr.CFF.glyphBySE(font, achar); 557 | 558 | //console.log(bchar, bind); 559 | //console.log(achar, aind); 560 | //state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width; state.open=open; 561 | 562 | Typr.U._drawCFF(font.CharStrings[bind], state,font,p); 563 | state.x = adx; state.y = ady; 564 | Typr.U._drawCFF(font.CharStrings[aind], state,font,p); 565 | 566 | //x=state.x; y=state.y; nStems=state.nStems; haveWidth=state.haveWidth; width=state.width; open=state.open; 567 | } 568 | if(open) { Typr.U.P.closePath(p); open=false; } 569 | } 570 | else if(v=="o19" || v=="o20") 571 | { 572 | var hasWidthArg; 573 | 574 | // The number of stem operators on the stack is always even. 575 | // If the value is uneven, that means a width is specified. 576 | hasWidthArg = stack.length % 2 !== 0; 577 | if (hasWidthArg && !haveWidth) { 578 | width = stack.shift() + font.Private.nominalWidthX; 579 | } 580 | 581 | nStems += stack.length >> 1; 582 | stack.length = 0; 583 | haveWidth = true; 584 | 585 | i += (nStems + 7) >> 3; 586 | } 587 | 588 | else if(v=="o21") { 589 | if (stack.length > 2 && !haveWidth) { 590 | width = stack.shift() + font.Private.nominalWidthX; 591 | haveWidth = true; 592 | } 593 | 594 | y += stack.pop(); 595 | x += stack.pop(); 596 | 597 | if(open) Typr.U.P.closePath(p); 598 | Typr.U.P.moveTo(p,x,y); open=true; 599 | } 600 | else if(v=="o22") 601 | { 602 | if (stack.length > 1 && !haveWidth) { 603 | width = stack.shift() + font.Private.nominalWidthX; 604 | haveWidth = true; 605 | } 606 | 607 | x += stack.pop(); 608 | 609 | if(open) Typr.U.P.closePath(p); 610 | Typr.U.P.moveTo(p,x,y); open=true; 611 | } 612 | else if(v=="o25") 613 | { 614 | while (stack.length > 6) { 615 | x += stack.shift(); 616 | y += stack.shift(); 617 | Typr.U.P.lineTo(p, x, y); 618 | } 619 | 620 | c1x = x + stack.shift(); 621 | c1y = y + stack.shift(); 622 | c2x = c1x + stack.shift(); 623 | c2y = c1y + stack.shift(); 624 | x = c2x + stack.shift(); 625 | y = c2y + stack.shift(); 626 | Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); 627 | } 628 | else if(v=="o26") 629 | { 630 | if (stack.length % 2) { 631 | x += stack.shift(); 632 | } 633 | 634 | while (stack.length > 0) { 635 | c1x = x; 636 | c1y = y + stack.shift(); 637 | c2x = c1x + stack.shift(); 638 | c2y = c1y + stack.shift(); 639 | x = c2x; 640 | y = c2y + stack.shift(); 641 | Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); 642 | } 643 | 644 | } 645 | else if(v=="o27") 646 | { 647 | if (stack.length % 2) { 648 | y += stack.shift(); 649 | } 650 | 651 | while (stack.length > 0) { 652 | c1x = x + stack.shift(); 653 | c1y = y; 654 | c2x = c1x + stack.shift(); 655 | c2y = c1y + stack.shift(); 656 | x = c2x + stack.shift(); 657 | y = c2y; 658 | Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); 659 | } 660 | } 661 | else if(v=="o10" || v=="o29") // callsubr || callgsubr 662 | { 663 | var obj = (v=="o10" ? font.Private : font); 664 | if(stack.length==0) { console.log("error: empty stack"); } 665 | else { 666 | var ind = stack.pop(); 667 | var subr = obj.Subrs[ ind + obj.Bias ]; 668 | state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width; state.open=open; 669 | Typr.U._drawCFF(subr, state,font,p); 670 | x=state.x; y=state.y; nStems=state.nStems; haveWidth=state.haveWidth; width=state.width; open=state.open; 671 | } 672 | } 673 | else if(v=="o30" || v=="o31") // vhcurveto || hvcurveto 674 | { 675 | var count, count1 = stack.length; 676 | var index = 0; 677 | var alternate = v == "o31"; 678 | 679 | count = count1 & ~2; 680 | index += count1 - count; 681 | 682 | while ( index < count ) 683 | { 684 | if(alternate) 685 | { 686 | c1x = x + stack.shift(); 687 | c1y = y; 688 | c2x = c1x + stack.shift(); 689 | c2y = c1y + stack.shift(); 690 | y = c2y + stack.shift(); 691 | if(count-index == 5) { x = c2x + stack.shift(); index++; } 692 | else x = c2x; 693 | alternate = false; 694 | } 695 | else 696 | { 697 | c1x = x; 698 | c1y = y + stack.shift(); 699 | c2x = c1x + stack.shift(); 700 | c2y = c1y + stack.shift(); 701 | x = c2x + stack.shift(); 702 | if(count-index == 5) { y = c2y + stack.shift(); index++; } 703 | else y = c2y; 704 | alternate = true; 705 | } 706 | Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); 707 | index += 4; 708 | } 709 | } 710 | 711 | else if((v+"").charAt(0)=="o") { console.log("Unknown operation: "+v, cmds); throw v; } 712 | else stack.push(v); 713 | } 714 | //console.log(cmds); 715 | state.x=x; state.y=y; state.nStems=nStems; state.haveWidth=haveWidth; state.width=width; state.open=open; 716 | } -------------------------------------------------------------------------------- /src/utils/colorPresets.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "Deluxe Paint", 3 | "palette": [[0,0,0],[48,138,69],[245,245,245],[113,113,113],[243,0,0],[146,0,0],[243,211,211],[243,162,81],[243,243,211],[227,211,0],[211,243,81],[211,243,211],[0,243,0],[0,146,0],[211,243,243],[0,227,227],[170,170,170],[69,223,69],[227,227,227],[113,113,113],[227,0,0],[130,0,0],[243,178,178],[243,146,65],[243,243,178],[195,195,0],[195,243,65],[178,243,178],[0,227,0],[0,130,0],[178,243,243],[0,195,195],[101,101,101],[69,223,207],[223,223,223],[101,101,101],[227,0,0],[113,0,0],[243,146,146],[243,130,32],[243,243,146],[178,162,0],[178,243,32],[146,243,146],[0,227,0],[0,113,0],[146,243,243],[0,178,178],[223,223,223],[48,138,207],[195,195,195],[81,81,81],[211,0,0],[113,0,0],[243,113,113],[243,113,0],[243,243,113],[146,146,0],[162,243,0],[130,243,113],[0,211,0],[0,113,0],[113,243,243],[0,146,146],[207,48,69],[138,138,223],[178,178,178],[65,65,65],[195,0,0],[97,0,0],[243,81,81],[227,97,0],[243,243,81],[130,130,0],[146,227,0],[97,243,81],[0,195,0],[0,97,0],[81,243,243],[0,130,130],[223,138,69],[69,48,207],[170,170,170],[48,48,48],[178,0,0],[81,0,0],[243,65,65],[195,97,0],[243,243,65],[113,97,0],[130,195,0],[65,243,65],[0,178,0],[0,81,0],[65,243,243],[0,113,113],[207,223,69],[207,48,207],[146,146,146],[32,32,32],[178,0,0],[65,0,0],[243,32,32],[178,81,0],[243,243,32],[81,81,0],[113,178,0],[32,243,32],[0,178,0],[0,65,0],[32,243,243],[0,81,81],[138,138,48],[223,138,207],[130,130,130],[32,32,32],[162,0,0],[65,0,0],[243,0,0],[146,65,0],[243,243,0],[65,65,0],[97,146,0],[0,243,0],[0,162,0],[0,65,0],[0,243,243],[0,65,65],[81,178,243],[211,211,243],[0,0,243],[0,0,146],[243,211,243],[146,0,227],[243,211,243],[227,0,227],[243,227,211],[195,146,130],[130,65,48],[81,16,16],[243,81,81],[195,32,32],[32,195,48],[81,32,195],[65,178,243],[178,178,243],[0,0,227],[0,0,130],[227,178,243],[130,0,195],[243,178,243],[195,0,195],[243,211,211],[178,130,113],[130,48,48],[65,16,0],[243,178,130],[195,65,32],[32,195,81],[130,32,195],[32,162,243],[146,146,243],[0,0,227],[0,0,113],[211,146,243],[113,0,178],[243,146,243],[178,0,178],[243,211,195],[178,113,97],[113,48,32],[65,0,0],[243,243,130],[195,113,32],[32,195,130],[178,32,195],[0,146,243],[113,130,243],[0,0,211],[0,0,113],[211,113,243],[97,0,146],[243,113,243],[146,0,146],[227,195,178],[162,113,97],[113,48,32],[48,0,0],[130,243,130],[195,146,32],[32,195,178],[195,32,162],[0,130,227],[81,97,243],[0,0,195],[0,0,97],[195,81,243],[81,0,130],[243,81,243],[130,0,130],[227,178,162],[162,97,81],[113,32,32],[48,0,0],[130,243,243],[195,195,32],[32,162,195],[195,32,130],[0,113,195],[65,65,243],[0,0,178],[0,0,81],[178,65,243],[65,0,113],[243,65,243],[97,0,113],[211,178,146],[162,97,81],[97,32,16],[32,0,0],[130,130,243],[146,195,32],[32,113,195],[195,32,81],[0,97,178],[32,32,243],[0,0,178],[0,0,65],[178,32,243],[48,0,81],[243,32,243],[81,0,81],[211,162,146],[146,81,65],[97,16,16],[32,0,0],[178,130,243],[113,195,32],[32,81,195],[195,32,32],[0,81,146],[0,0,243],[0,0,162],[0,0,65],[162,0,243],[32,0,65],[243,0,243],[65,0,65],[195,146,130],[146,81,65],[81,16,16],[32,0,0],[243,130,243],[65,195,32],[32,32,195],[255,255,255]] 4 | }, { 5 | "name": "AtariST (Reduced To 256)", 6 | "palette": [[0,0,0],[0,0,36],[0,0,72],[0,0,109],[0,0,145],[0,0,182],[0,0,218],[0,0,255],[72,0,0],[72,0,36],[72,0,72],[72,0,109],[72,0,145],[72,0,182],[72,0,218],[72,0,255],[0,36,0],[0,36,36],[0,36,72],[0,36,109],[0,36,145],[0,36,182],[0,36,218],[0,36,255],[72,36,0],[72,36,36],[72,36,72],[72,36,109],[72,36,145],[72,36,182],[72,36,218],[72,36,255],[0,72,0],[0,72,36],[0,72,72],[0,72,109],[0,72,145],[0,72,182],[0,72,218],[0,72,255],[72,72,0],[72,72,36],[72,72,72],[72,72,109],[72,72,145],[72,72,182],[72,72,218],[72,72,255],[0,109,0],[0,109,36],[0,109,72],[0,109,109],[0,109,145],[0,109,182],[0,109,218],[0,109,255],[72,109,0],[72,109,36],[72,109,72],[72,109,109],[72,109,145],[72,109,182],[72,109,218],[72,109,255],[0,145,0],[0,145,36],[0,145,72],[0,145,109],[0,145,145],[0,145,182],[0,145,218],[0,145,255],[72,145,0],[72,145,36],[72,145,72],[72,145,109],[72,145,145],[72,145,182],[72,145,218],[72,145,255],[0,182,0],[0,182,36],[0,182,72],[0,182,109],[0,182,145],[0,182,182],[0,182,218],[0,182,255],[72,182,0],[72,182,36],[72,182,72],[72,182,109],[72,182,145],[72,182,182],[72,182,218],[72,182,255],[0,218,0],[0,218,36],[0,218,72],[0,218,109],[0,218,145],[0,218,182],[0,218,218],[0,218,255],[72,218,0],[72,218,36],[72,218,72],[72,218,109],[72,218,145],[72,218,182],[72,218,218],[72,218,255],[0,255,0],[0,255,36],[0,255,72],[0,255,109],[0,255,145],[0,255,182],[0,255,218],[0,255,255],[72,255,0],[72,255,36],[72,255,72],[72,255,109],[72,255,145],[72,255,182],[72,255,218],[72,255,255],[145,0,0],[145,0,36],[145,0,72],[145,0,109],[145,0,145],[145,0,182],[145,0,218],[145,0,255],[255,0,0],[255,0,36],[255,0,72],[255,0,109],[255,0,145],[255,0,182],[255,0,218],[255,0,255],[145,36,0],[145,36,36],[145,36,72],[145,36,109],[145,36,145],[145,36,182],[145,36,218],[145,36,255],[255,36,0],[255,36,36],[255,36,72],[255,36,109],[255,36,145],[255,36,182],[255,36,218],[255,36,255],[145,72,0],[145,72,36],[145,72,72],[145,72,109],[145,72,145],[145,72,182],[145,72,218],[145,72,255],[255,72,0],[255,72,36],[255,72,72],[255,72,109],[255,72,145],[255,72,182],[255,72,218],[255,72,255],[145,109,0],[145,109,36],[145,109,72],[145,109,109],[145,109,145],[145,109,182],[145,109,218],[145,109,255],[255,109,0],[255,109,36],[255,109,72],[255,109,109],[255,109,145],[255,109,182],[255,109,218],[255,109,255],[145,145,0],[145,145,36],[145,145,72],[145,145,109],[145,145,145],[145,145,182],[145,145,218],[145,145,255],[255,145,0],[255,145,36],[255,145,72],[255,145,109],[255,145,145],[255,145,182],[255,145,218],[255,145,255],[145,182,0],[145,182,36],[145,182,72],[145,182,109],[145,182,145],[145,182,182],[145,182,218],[145,182,255],[255,182,0],[255,182,36],[255,182,72],[255,182,109],[255,182,145],[255,182,182],[255,182,218],[255,182,255],[145,218,0],[145,218,36],[145,218,72],[145,218,109],[145,218,145],[145,218,182],[145,218,218],[145,218,255],[255,218,0],[255,218,36],[255,218,72],[255,218,109],[255,218,145],[255,218,182],[255,218,218],[255,218,255],[145,255,0],[145,255,36],[145,255,72],[145,255,109],[145,255,145],[145,255,182],[145,255,218],[145,255,255],[255,255,0],[255,255,36],[255,255,72],[255,255,109],[255,255,145],[255,255,182],[255,255,218],[255,255,255]] 7 | }, { 8 | "name": "Eclipse icon", 9 | "palette": [[0,0,0],[128,0,0],[0,128,0],[128,128,0],[0,0,128],[128,0,128],[0,128,128],[192,192,192],[192,220,192],[166,202,240],[95,63,63],[127,63,63],[159,63,63],[191,63,63],[223,63,63],[255,63,63],[63,95,63],[95,95,63],[127,95,63],[159,95,63],[191,95,63],[223,95,63],[255,95,63],[63,127,63],[95,127,63],[127,127,63],[159,127,63],[191,127,63],[223,127,63],[255,127,63],[63,159,63],[95,159,63],[127,159,63],[159,159,63],[191,159,63],[223,159,63],[255,159,63],[63,191,63],[95,191,63],[127,191,63],[159,191,63],[191,191,63],[223,191,63],[255,191,63],[63,223,63],[95,223,63],[127,223,63],[159,223,63],[191,223,63],[223,223,63],[255,223,63],[63,255,63],[95,255,63],[127,255,63],[159,255,63],[191,255,63],[223,255,63],[255,255,63],[63,63,95],[95,63,95],[127,63,95],[159,63,95],[191,63,95],[223,63,95],[255,63,95],[63,95,95],[95,95,95],[127,95,95],[159,95,95],[191,95,95],[223,95,95],[255,95,95],[63,127,95],[95,127,95],[127,127,95],[159,127,95],[191,127,95],[223,127,95],[255,127,95],[63,159,95],[95,159,95],[127,159,95],[159,159,95],[191,159,95],[223,159,95],[255,159,95],[63,191,95],[95,191,95],[127,191,95],[159,191,95],[191,191,95],[223,191,95],[255,191,95],[63,223,95],[95,223,95],[127,223,95],[159,223,95],[191,223,95],[223,223,95],[255,223,95],[63,255,95],[95,255,95],[127,255,95],[159,255,95],[191,255,95],[223,255,95],[255,255,95],[63,63,127],[95,63,127],[127,63,127],[159,63,127],[191,63,127],[223,63,127],[255,63,127],[63,95,127],[95,95,127],[127,95,127],[159,95,127],[191,95,127],[223,95,127],[255,95,127],[63,127,127],[95,127,127],[127,127,127],[159,127,127],[191,127,127],[223,127,127],[255,127,127],[63,159,127],[95,159,127],[127,159,127],[159,159,127],[191,159,127],[223,159,127],[255,159,127],[63,191,127],[95,191,127],[127,191,127],[159,191,127],[191,191,127],[223,191,127],[255,191,127],[63,223,127],[95,223,127],[127,223,127],[159,223,127],[191,223,127],[223,223,127],[255,223,127],[63,255,127],[95,255,127],[127,255,127],[159,255,127],[191,255,127],[223,255,127],[255,255,127],[63,63,159],[95,63,159],[127,63,159],[159,63,159],[191,63,159],[223,63,159],[255,63,159],[63,95,159],[95,95,159],[127,95,159],[159,95,159],[191,95,159],[223,95,159],[255,95,159],[63,127,159],[95,127,159],[127,127,159],[159,127,159],[191,127,159],[223,127,159],[255,127,159],[63,159,159],[95,159,159],[127,159,159],[159,159,159],[191,159,159],[223,159,159],[255,159,159],[63,191,159],[95,191,159],[127,191,159],[159,191,159],[191,191,159],[223,191,159],[255,191,159],[63,223,159],[95,223,159],[127,223,159],[159,223,159],[191,223,159],[223,223,159],[255,223,159],[63,255,159],[95,255,159],[127,255,159],[159,255,159],[191,255,159],[223,255,159],[255,255,159],[63,63,191],[95,63,191],[127,63,191],[159,63,191],[191,63,191],[223,63,191],[255,63,191],[63,95,191],[95,95,191],[127,95,191],[159,95,191],[191,95,191],[223,95,191],[255,95,191],[63,127,191],[95,127,191],[127,127,191],[159,127,191],[191,127,191],[223,127,191],[255,127,191],[63,159,191],[95,159,191],[127,159,191],[159,159,191],[191,159,191],[223,159,191],[255,159,191],[63,191,191],[95,191,191],[127,191,191],[159,191,191],[191,191,191],[223,191,191],[255,191,191],[63,223,191],[95,223,191],[127,223,191],[159,223,191],[191,223,191],[223,223,191],[255,251,240],[160,160,164],[128,128,128],[255,0,0],[0,255,0],[255,255,0],[0,0,255],[255,0,255],[0,255,255],[255,255,255]] 10 | }, { 11 | "name": "Fornax Void Official Palette No.1", 12 | "palette": [[122,37,255],[117,42,255],[112,47,255],[108,51,255],[103,56,255],[99,60,255],[94,65,255],[89,70,255],[85,74,255],[80,79,255],[76,83,255],[71,88,255],[66,93,255],[62,97,255],[57,102,255],[53,106,255],[47,222,235],[55,214,242],[63,206,249],[70,198,255],[78,190,255],[86,182,255],[93,174,255],[101,166,255],[109,158,255],[116,150,255],[124,142,255],[132,134,255],[139,126,255],[147,118,255],[155,110,255],[162,102,255],[0,204,204],[7,195,208],[13,186,211],[20,177,215],[26,168,218],[32,160,221],[39,151,225],[45,142,228],[52,133,232],[58,124,235],[64,116,238],[71,107,242],[77,98,245],[84,89,249],[90,80,252],[96,72,255],[240,240,240],[224,233,233],[208,226,226],[192,220,220],[176,213,213],[160,206,206],[144,200,200],[128,193,193],[112,186,186],[96,180,180],[80,173,173],[64,166,166],[48,160,160],[32,153,153],[16,146,146],[0,140,140],[255,255,255],[242,242,255],[230,230,255],[218,218,255],[206,206,255],[194,194,255],[181,181,255],[169,169,255],[157,157,255],[145,145,255],[133,133,255],[120,120,255],[108,108,255],[96,96,255],[84,84,255],[72,72,255],[243,142,255],[231,136,255],[219,130,255],[208,124,255],[196,118,255],[185,112,255],[173,106,255],[161,100,255],[150,94,255],[138,88,255],[127,82,255],[115,76,255],[103,70,255],[92,64,255],[80,58,255],[69,53,255],[255,216,255],[255,194,255],[255,172,255],[255,150,255],[255,128,255],[255,96,255],[255,64,255],[255,0,255],[224,0,224],[192,0,192],[160,0,160],[128,0,128],[96,0,96],[64,0,64],[48,0,48],[32,0,32],[216,216,255],[196,196,247],[176,176,240],[157,157,232],[137,137,225],[117,117,217],[98,98,210],[78,78,202],[58,58,195],[39,39,187],[19,19,180],[0,0,173],[0,0,145],[0,0,118],[0,0,91],[0,0,64],[255,43,255],[238,40,246],[221,38,238],[204,36,229],[187,34,221],[170,32,212],[153,30,204],[136,28,195],[119,25,187],[102,23,178],[85,21,170],[68,19,161],[51,17,153],[34,15,144],[17,13,136],[0,11,128],[0,0,0],[5,5,17],[9,9,34],[13,13,51],[18,18,68],[22,22,85],[26,26,102],[30,30,119],[35,35,136],[39,39,153],[43,43,170],[47,47,187],[52,52,204],[56,56,221],[60,60,238],[64,64,255],[0,255,255],[0,240,240],[0,225,225],[0,210,210],[0,195,195],[0,180,180],[0,165,165],[0,150,150],[0,136,136],[0,121,121],[0,106,106],[0,91,91],[0,76,76],[0,61,61],[0,46,46],[0,32,32],[255,160,255],[238,157,246],[221,155,238],[204,153,229],[187,151,221],[170,149,212],[153,147,204],[136,145,195],[119,142,187],[102,140,178],[85,138,170],[68,136,161],[51,134,153],[34,132,144],[17,130,136],[0,128,128],[0,0,0],[16,9,17],[31,18,34],[46,27,51],[61,36,68],[76,45,85],[91,54,102],[106,63,119],[122,72,136],[137,81,153],[152,90,170],[167,99,187],[182,108,204],[197,117,221],[212,126,238],[227,134,255],[255,192,255],[242,172,255],[229,153,255],[217,134,255],[204,115,255],[192,96,255],[176,48,255],[160,0,255],[128,0,232],[96,0,208],[80,0,192],[64,0,128],[48,0,96],[40,0,80],[32,0,64],[16,0,48],[166,0,255],[178,0,223],[189,0,191],[200,0,159],[211,0,127],[222,0,95],[233,0,63],[244,0,31],[255,0,0],[218,0,10],[182,0,19],[145,0,28],[109,0,37],[72,0,46],[36,0,55],[0,0,64],[227,227,255],[211,211,240],[196,196,225],[181,181,210],[166,166,195],[151,151,180],[136,136,165],[121,121,150],[105,105,136],[90,90,121],[75,75,106],[60,60,91],[45,45,76],[30,30,61],[15,15,46],[255,255,255]] 13 | }, { 14 | "name": "DOOM", 15 | "palette":[[0,0,0],[31,23,11],[23,15,7],[75,75,75],[255,255,255],[27,27,27],[19,19,19],[11,11,11],[7,7,7],[47,55,31],[35,43,15],[23,31,7],[15,23,0],[79,59,43],[71,51,35],[63,43,27],[255,183,183],[247,171,171],[243,163,163],[235,151,151],[231,143,143],[223,135,135],[219,123,123],[211,115,115],[203,107,107],[199,99,99],[191,91,91],[187,87,87],[179,79,79],[175,71,71],[167,63,63],[163,59,59],[155,51,51],[151,47,47],[143,43,43],[139,35,35],[131,31,31],[127,27,27],[119,23,23],[115,19,19],[107,15,15],[103,11,11],[95,7,7],[91,7,7],[83,7,7],[79,0,0],[71,0,0],[67,0,0],[255,235,223],[255,227,211],[255,219,199],[255,211,187],[255,207,179],[255,199,167],[255,191,155],[255,187,147],[255,179,131],[247,171,123],[239,163,115],[231,155,107],[223,147,99],[215,139,91],[207,131,83],[203,127,79],[191,123,75],[179,115,71],[171,111,67],[163,107,63],[155,99,59],[143,95,55],[135,87,51],[127,83,47],[119,79,43],[107,71,39],[95,67,35],[83,63,31],[75,55,27],[63,47,23],[51,43,19],[43,35,15],[239,239,239],[231,231,231],[223,223,223],[219,219,219],[211,211,211],[203,203,203],[199,199,199],[191,191,191],[183,183,183],[179,179,179],[171,171,171],[167,167,167],[159,159,159],[151,151,151],[147,147,147],[139,139,139],[131,131,131],[127,127,127],[119,119,119],[111,111,111],[107,107,107],[99,99,99],[91,91,91],[87,87,87],[79,79,79],[71,71,71],[67,67,67],[59,59,59],[55,55,55],[47,47,47],[39,39,39],[35,35,35],[119,255,111],[111,239,103],[103,223,95],[95,207,87],[91,191,79],[83,175,71],[75,159,63],[67,147,55],[63,131,47],[55,115,43],[47,99,35],[39,83,27],[31,67,23],[23,51,15],[19,35,11],[11,23,7],[191,167,143],[183,159,135],[175,151,127],[167,143,119],[159,135,111],[155,127,107],[147,123,99],[139,115,91],[131,107,87],[123,99,79],[119,95,75],[111,87,67],[103,83,63],[95,75,55],[87,67,51],[83,63,47],[159,131,99],[143,119,83],[131,107,75],[119,95,63],[103,83,51],[91,71,43],[79,59,35],[67,51,27],[123,127,99],[111,115,87],[103,107,79],[91,99,71],[83,87,59],[71,79,51],[63,71,43],[55,63,39],[255,255,115],[235,219,87],[215,187,67],[195,155,47],[175,123,31],[155,91,19],[135,67,7],[115,43,0],[255,255,255],[255,219,219],[255,187,187],[255,155,155],[255,123,123],[255,95,95],[255,63,63],[255,31,31],[255,0,0],[239,0,0],[227,0,0],[215,0,0],[203,0,0],[191,0,0],[179,0,0],[167,0,0],[155,0,0],[139,0,0],[127,0,0],[115,0,0],[103,0,0],[91,0,0],[79,0,0],[67,0,0],[231,231,255],[199,199,255],[171,171,255],[143,143,255],[115,115,255],[83,83,255],[55,55,255],[27,27,255],[0,0,255],[0,0,227],[0,0,203],[0,0,179],[0,0,155],[0,0,131],[0,0,107],[0,0,83],[255,255,255],[255,235,219],[255,215,187],[255,199,155],[255,179,123],[255,163,91],[255,143,59],[255,127,27],[243,115,23],[235,111,15],[223,103,15],[215,95,11],[203,87,7],[195,79,0],[183,71,0],[175,67,0],[255,255,255],[255,255,215],[255,255,179],[255,255,143],[255,255,107],[255,255,71],[255,255,35],[255,255,0],[167,63,0],[159,55,0],[147,47,0],[135,35,0],[79,59,39],[67,47,27],[55,35,19],[47,27,11],[0,0,83],[0,0,71],[0,0,59],[0,0,47],[0,0,35],[0,0,23],[0,0,11],[0,0,0],[255,159,67],[255,231,75],[255,123,255],[255,0,255],[207,0,207],[159,0,155],[111,0,107],[255,255,255]] 16 | }, { 17 | "name": "Riso Blue, Yellow, FluoroPink (Preview)", 18 | "palette": [[0,75,181],[0,70,182],[7,64,179],[13,59,173],[30,54,171],[47,40,150],[255,68,14],[255,124,0],[255,154,0],[255,182,0],[255,209,0],[223,205,0],[136,162,0],[82,131,22],[42,100,26],[0,63,31],[42,120,200],[58,119,204],[59,100,196],[68,92,192],[84,82,186],[95,51,163],[255,65,51],[255,127,49],[255,156,40],[255,183,40],[255,216,15],[204,204,36],[131,165,53],[77,132,56],[33,99,60],[0,65,63],[87,146,210],[98,141,211],[102,126,206],[108,114,199],[120,98,190],[143,61,169],[255,66,79],[255,130,84],[255,158,84],[255,187,82],[255,218,78],[195,207,92],[121,165,94],[75,133,95],[29,107,89],[0,66,87],[128,172,214],[140,165,217],[146,146,213],[152,132,208],[169,119,199],[188,66,179],[255,69,126],[255,135,113],[255,159,124],[255,190,118],[252,215,127],[185,203,122],[114,165,125],[73,137,124],[17,105,117],[0,63,106],[185,200,219],[194,191,220],[208,172,214],[220,155,213],[237,131,203],[255,63,177],[255,66,142],[255,137,154],[255,160,158],[255,187,166],[244,212,171],[184,203,173],[116,167,173],[61,136,168],[14,106,160],[0,63,139],[255,65,170],[0,63,156],[49,112,175],[89,136,178],[135,159,182],[203,186,179],[216,187,124],[147,158,125],[96,130,119],[56,102,123],[0,63,117],[0,64,94],[39,100,90],[76,121,87],[121,141,78],[186,173,65],[255,128,194],[16,61,152],[65,100,166],[101,124,174],[152,149,175],[218,169,176],[225,169,123],[158,142,125],[104,116,120],[63,95,122],[15,57,114],[9,61,92],[57,93,89],[100,114,88],[134,131,82],[198,151,68],[255,162,209],[27,53,148],[76,93,169],[115,113,170],[165,133,176],[229,144,168],[232,142,115],[165,124,120],[118,105,120],[73,86,121],[24,52,111],[24,54,86],[69,87,92],[105,101,86],[144,115,80],[215,135,72],[253,189,212],[37,49,141],[86,77,158],[122,97,167],[172,114,172],[247,124,166],[245,112,115],[178,100,115],[131,87,115],[80,71,115],[37,46,106],[34,48,84],[73,73,87],[115,90,88],[153,98,84],[220,113,73],[235,211,211],[54,35,127],[99,51,150],[138,61,156],[186,74,165],[255,71,154],[255,62,102],[193,60,107],[143,55,104],[100,51,107],[50,36,96],[46,40,80],[82,61,86],[126,63,84],[165,67,78],[230,65,70],[255,221,0],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[255,53,23],[193,51,26],[139,49,31],[96,48,35],[52,40,40],[53,40,63],[87,51,61],[123,62,55],[167,62,53],[231,66,44],[254,226,31],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[243,113,0],[184,91,17],[133,81,25],[89,70,33],[38,50,40],[35,50,66],[77,74,62],[111,85,57],[156,93,53],[224,112,44],[248,229,91],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[239,135,0],[169,116,7],[119,98,20],[77,79,31],[31,55,39],[26,56,63],[64,81,61],[103,100,57],[141,108,46],[209,132,36],[246,231,124],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[229,158,0],[159,130,0],[114,108,18],[67,87,28],[23,57,38],[18,57,64],[55,87,62],[91,107,55],[132,124,47],[199,150,30],[232,228,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[212,179,0],[148,146,0],[101,121,9],[56,96,27],[7,60,39],[0,63,63],[45,93,61],[83,119,55],[119,136,45],[189,168,25],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[0,75,181],[255,255,255]] 19 | }, { 20 | "name": "Riso Blue, Yellow, FluoroPink (Blue Plate)", 21 | "palette": [[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[255,255,255],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255]] 22 | }, { 23 | "name": "Riso Blue, Yellow, FluoroPink (FluoroPink Plate)", 24 | "palette": [[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[145,145,145],[94,94,94],[48,48,48],[0,0,0],[0,0,0],[48,48,48],[94,94,94],[145,145,145],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[48,48,48],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[145,145,145],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[198,198,198],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]] 25 | }, { 26 | "name": "Riso Blue, Yellow, FluoroPink (Yellow Plate)", 27 | "palette": [[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[255,255,255],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[198,198,198],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[145,145,145],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[94,94,94],[0,0,0],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[94,94,94],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[145,145,145],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[198,198,198],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[48,48,48],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]] 28 | }, { 29 | "name": "Riso FluoroPink & Teal (Preview)", 30 | "palette": [[0,131,138],[0,44,83],[12,46,86],[22,48,89],[34,50,93],[48,52,98],[64,55,103],[82,59,108],[100,62,114],[121,66,120],[137,69,125],[157,72,131],[176,76,137],[194,79,143],[209,82,148],[222,84,152],[13,137,144],[0,48,86],[12,50,89],[22,52,92],[34,54,96],[48,57,101],[64,61,106],[82,64,112],[101,68,118],[122,72,124],[139,75,129],[158,79,136],[178,83,142],[196,87,148],[211,90,152],[224,92,157],[25,143,149],[0,52,88],[12,54,92],[22,57,95],[34,59,99],[48,62,104],[65,66,109],[83,70,115],[101,74,121],[122,78,128],[139,82,134],[159,86,140],[179,90,146],[197,94,153],[212,97,157],[225,100,162],[38,149,155],[0,57,91],[12,59,95],[23,62,98],[34,64,102],[49,68,107],[65,72,113],[83,76,119],[102,80,125],[123,85,132],[140,89,138],[160,94,144],[179,98,151],[198,103,157],[213,106,162],[225,109,167],[54,157,163],[0,62,95],[12,65,99],[23,68,102],[35,71,106],[49,74,112],[66,79,117],[84,84,124],[103,88,130],[124,93,137],[141,98,143],[161,103,150],[181,108,157],[199,113,164],[215,117,169],[227,120,174],[72,166,171],[0,68,98],[12,71,103],[23,75,106],[35,78,111],[50,82,116],[66,87,122],[84,92,128],[104,97,136],[125,103,143],[142,108,149],[162,113,156],[183,119,163],[201,124,171],[217,128,176],[229,132,181],[92,176,180],[0,75,103],[12,78,107],[23,82,111],[35,85,115],[50,90,121],[67,95,127],[86,101,134],[105,106,142],[126,113,149],[144,119,156],[165,124,163],[185,131,171],[204,136,178],[219,141,183],[232,145,189],[113,186,190],[0,82,107],[12,86,112],[23,90,116],[36,93,120],[51,99,127],[67,104,133],[86,110,140],[106,117,148],[127,124,155],[145,130,162],[166,136,170],[187,143,178],[205,149,186],[221,154,191],[234,159,196],[136,197,200],[0,90,112],[12,94,117],[24,98,121],[36,102,126],[51,108,132],[68,114,139],[87,121,146],[107,128,154],[129,135,162],[147,142,170],[168,149,178],[189,156,186],[208,163,194],[224,169,200],[237,174,205],[155,207,209],[0,97,116],[12,101,121],[24,105,126],[36,110,131],[52,116,137],[69,122,144],[88,130,152],[108,137,160],[130,145,169],[148,153,176],[169,160,185],[190,168,193],[210,175,202],[226,181,207],[239,187,213],[177,217,219],[0,104,121],[13,109,126],[24,114,131],[37,119,136],[52,125,143],[69,132,150],[89,140,158],[109,148,167],[131,157,176],[150,165,184],[171,173,192],[192,182,201],[211,189,210],[228,196,216],[241,201,222],[199,228,229],[0,111,126],[13,117,131],[24,122,136],[37,127,141],[53,134,148],[70,141,156],[90,150,164],[110,158,173],[133,168,182],[151,176,190],[173,185,199],[194,194,208],[214,203,217],[230,209,224],[244,215,230],[219,238,239],[0,119,130],[13,124,136],[25,130,141],[37,135,146],[53,142,154],[71,150,162],[91,159,170],[111,168,180],[134,178,189],[153,188,198],[174,197,207],[196,207,216],[216,216,226],[232,223,232],[246,229,239],[236,246,246],[0,124,134],[13,130,139],[25,136,144],[38,141,150],[54,149,158],[71,158,166],[91,167,174],[112,177,184],[135,187,194],[154,196,202],[176,206,212],[197,216,222],[217,226,232],[234,233,238],[248,240,245],[250,253,253],[0,129,137],[13,135,143],[25,141,148],[38,147,154],[54,155,162],[72,164,170],[92,174,179],[113,184,189],[135,195,198],[154,205,207],[176,214,217],[198,225,227],[218,235,237],[235,243,244],[249,250,251],[226,85,153],[228,93,158],[229,101,163],[230,110,168],[232,121,175],[234,133,182],[237,146,190],[239,160,198],[242,175,207],[244,188,215],[246,203,224],[249,217,232],[251,231,241],[253,242,247],[254,252,253],[255,255,255]] 31 | }, { 32 | "name": "Riso FluoroPink & Teal (Pink plate)", 33 | "palette": [[255,255,255],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[0,0,0],[255,255,255],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[21,21,21],[255,255,255],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[36,36,36],[255,255,255],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[50,50,50],[255,255,255],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[66,66,66],[255,255,255],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[82,82,82],[255,255,255],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[99,99,99],[255,255,255],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[116,116,116],[255,255,255],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[134,134,134],[255,255,255],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[150,150,150],[255,255,255],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[168,168,168],[255,255,255],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[187,187,187],[255,255,255],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[207,207,207],[255,255,255],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[226,226,226],[255,255,255],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[246,246,246],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[255,255,255]] 34 | }, { 35 | "name": "Riso FluoroPink & Teal (Teal Plate)", 36 | "palette": [[0,0,0],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[21,21,21],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[36,36,36],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[50,50,50],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[66,66,66],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[82,82,82],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[99,99,99],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[116,116,116],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[134,134,134],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[150,150,150],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[168,168,168],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[187,187,187],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[207,207,207],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[226,226,226],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[246,246,246],[0,0,0],[21,21,21],[36,36,36],[50,50,50],[66,66,66],[82,82,82],[99,99,99],[116,116,116],[134,134,134],[150,150,150],[168,168,168],[187,187,187],[207,207,207],[226,226,226],[246,246,246],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]] 37 | }, { 38 | "name": "Liero Level Palette", 39 | "palette": [[255,0,128],[100,150,200],[150,100,200],[200,100,150],[200,150,100],[150,200,100],[100,200,150],[50,100,150],[100,50,150],[150,50,100],[150,100,50],[255,128,64],[128,128,255],[255,255,255],[255,255,255],[255,255,255],[244,244,252],[248,248,188],[216,220,136],[188,176,104],[160,132,72],[236,216,160],[196,168,124],[156,120,88],[108,76,44],[172,120,72],[124,84,48],[172,120,72],[124,84,48],[140,96,56],[156,108,64],[120,72,52],[132,92,40],[136,124,0],[149,136,0],[124,112,0],[116,100,0],[172,96,28],[160,88,24],[152,84,20],[144,80,16],[120,64,8],[136,72,12],[128,68,8],[165,0,0],[189,0,0],[217,0,0],[255,255,255],[76,76,76],[85,84,84],[92,92,92],[100,100,100],[109,108,108],[116,116,116],[125,124,124],[132,132,132],[140,140,140],[148,148,148],[156,157,156],[255,255,255],[163,0,0],[187,0,0],[215,0,0],[255,255,255],[237,216,160],[197,168,124],[157,120,88],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[96,48,0],[84,40,0],[92,44,0],[89,40,0],[255,255,255],[72,36,0],[60,28,0],[68,32,0],[64,28,0],[255,255,255],[108,80,0],[108,56,0],[164,0,0],[188,0,0],[216,0,0],[255,255,255],[252,200,200],[244,164,164],[248,92,92],[244,76,76],[244,60,60],[245,76,76],[244,92,92],[245,164,164],[253,252,252],[221,220,220],[189,188,188],[157,156,156],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[81,80,192],[104,105,248],[137,136,244],[255,255,255],[80,81,192],[104,104,249],[136,137,244],[255,255,255],[60,60,144],[80,80,192],[104,104,248],[136,136,244],[180,180,244],[255,255,255],[105,104,248],[181,180,244],[44,133,44],[60,172,61],[104,189,104],[255,255,255],[44,132,45],[61,173,60],[104,188,105],[255,255,255],[32,100,32],[45,132,44],[61,172,60],[104,188,104],[164,212,164],[255,255,255],[60,173,60],[165,212,164],[0,0,0],[72,72,72],[84,84,84],[108,108,108],[124,124,124],[144,144,144],[156,156,156],[168,168,168],[180,180,180],[188,188,188],[196,196,196],[216,216,216],[220,220,220],[252,252,252],[244,188,188],[244,124,124],[248,60,60],[252,84,84],[172,0,0],[200,0,0],[252,0,0],[248,24,4],[252,36,0],[248,52,8],[248,80,16],[252,72,0],[248,108,20],[152,60,0],[252,108,0],[88,40,0],[248,136,24],[160,80,0],[168,84,0],[168,84,0],[200,100,0],[164,148,128],[100,56,0],[180,100,0],[252,144,0],[248,164,32],[112,76,0],[208,140,0],[252,180,0],[248,192,36],[236,180,0],[124,96,0],[136,116,0],[252,216,0],[40,36,8],[248,220,40],[148,136,0],[80,76,20],[160,152,40],[200,192,48],[244,232,60],[120,116,28],[240,240,212],[252,252,244],[240,240,180],[244,244,148],[244,244,112],[244,244,80],[252,252,0],[148,176,0],[116,144,0],[88,112,0],[60,80,0],[168,240,0],[84,232,0],[40,112,40],[60,172,60],[52,152,52],[44,132,44],[84,216,84],[0,144,0],[0,224,0],[84,252,252],[104,104,136],[144,144,192],[240,240,248],[220,220,244],[208,208,244],[200,200,244],[188,188,248],[168,168,248],[84,84,252],[72,68,228],[88,60,204],[108,52,180],[124,44,156],[144,40,136],[160,32,112],[196,20,68],[216,12,44],[232,4,20],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255],[255,255,255]] 40 | }] 41 | 42 | --------------------------------------------------------------------------------