├── .nvmrc ├── docs ├── intro.png └── outro.png ├── preview ├── public │ ├── favicon.ico │ └── vercel.svg ├── styles │ └── global.css ├── pages │ ├── _app.js │ └── index.js ├── postcss.config.js ├── tailwind.config.js ├── package.json ├── .gitignore └── README.md ├── scripts ├── index.ts ├── figma.js ├── denul.ts ├── image-comparison │ ├── png.js │ ├── prep.js │ ├── dupes.js │ └── compare.js ├── toV2.js ├── phonemes.js └── build-index.ts ├── test ├── config.test.js ├── sig.test.js ├── length.test.js ├── general.test.js └── perf.test.js ├── tsconfig.json ├── .gitignore ├── src ├── index.ts ├── components │ ├── sigil-image.ts │ └── sigil.ts ├── types.ts ├── core.ts └── symbols │ └── icon.ts ├── CHANGELOG.md ├── LICENSE.txt ├── package.json ├── README.md └── CONTRIBUTING.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /docs/intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urbit/sigil-js/HEAD/docs/intro.png -------------------------------------------------------------------------------- /docs/outro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urbit/sigil-js/HEAD/docs/outro.png -------------------------------------------------------------------------------- /preview/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/urbit/sigil-js/HEAD/preview/public/favicon.ico -------------------------------------------------------------------------------- /preview/styles/global.css: -------------------------------------------------------------------------------- 1 | 2 | button { 3 | @apply bg-gray-200 py-1 px-2 rounded-xl border border-gray-300 hover:bg-gray-100 font-medium; 4 | } -------------------------------------------------------------------------------- /scripts/index.ts: -------------------------------------------------------------------------------- 1 | import buildIndex from './build-index' 2 | import denul from './denul' 3 | 4 | export { 5 | buildIndex, 6 | denul 7 | } 8 | -------------------------------------------------------------------------------- /preview/pages/_app.js: -------------------------------------------------------------------------------- 1 | import 'tailwindcss/tailwind.css' 2 | import '../styles/global.css' 3 | 4 | function MyApp({ Component, pageProps }) { 5 | return 6 | } 7 | 8 | export default MyApp 9 | -------------------------------------------------------------------------------- /preview/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | const sigil = require('../dist/index').default; 2 | 3 | const config = { 4 | point: 'zod', 5 | }; 6 | 7 | test('Uses defaults', () => { 8 | expect(() => { 9 | const s = sigil(config); 10 | }).not.toThrow(); 11 | }); 12 | -------------------------------------------------------------------------------- /test/sig.test.js: -------------------------------------------------------------------------------- 1 | const sigil = require('../dist/index').default; 2 | 3 | const config = { 4 | point: null, 5 | }; 6 | 7 | test('Accepts sig', () => { 8 | config.point = '~mitten'; 9 | expect(() => { 10 | sigil(config); 11 | }).not.toThrow(); 12 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "target": "esnext", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /preview/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /preview/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "next": "latest", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0", 12 | "urbit-ob": "^5.0.1" 13 | }, 14 | "devDependencies": { 15 | "autoprefixer": "^10.2.6", 16 | "postcss": "^8.3.5", 17 | "tailwindcss": "^2.2.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /assets/*/* 12 | /bin/*/* 13 | **/dist 14 | */bin 15 | */**/dist 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .env 29 | .env.development 30 | -------------------------------------------------------------------------------- /preview/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /lib 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import sigil from './core'; 2 | import { Sigil } from './components/sigil'; 3 | import { SigilImage } from './components/sigil-image'; 4 | 5 | 6 | if (typeof window !== 'undefined' && window.customElements) { 7 | if (typeof customElements.get('urbit-sigil') === 'undefined') { 8 | customElements.define('urbit-sigil', Sigil); 9 | } 10 | if (typeof customElements.get('urbit-sigil-image') === 'undefined') { 11 | customElements.define('urbit-sigil-image', SigilImage); 12 | } 13 | } 14 | 15 | export * from './types'; 16 | export {sigil}; 17 | export default sigil; -------------------------------------------------------------------------------- /test/length.test.js: -------------------------------------------------------------------------------- 1 | const sigil = require('../dist/index').default; 2 | 3 | const config = { 4 | point: null, 5 | }; 6 | 7 | test('throws on length 3', () => { 8 | config.point = 'rafter-fig'; 9 | expect(() => { 10 | sigil(config); 11 | }).toThrow(); 12 | }); 13 | 14 | test('throws on length 5', () => { 15 | config.point = 'rovbud-fignys-mas'; 16 | expect(() => { 17 | sigil(config); 18 | }).toThrow(); 19 | }); 20 | 21 | test('throws on length 8', () => { 22 | config.point = 'ridlur-dozzod-master-listen'; 23 | expect(() => { 24 | sigil(config); 25 | }).toThrow(); 26 | }); 27 | -------------------------------------------------------------------------------- /src/components/sigil-image.ts: -------------------------------------------------------------------------------- 1 | import {Sigil} from './sigil'; 2 | 3 | export class SigilImage extends Sigil { 4 | render() { 5 | if (!this.shadowRoot) { 6 | return; 7 | } 8 | 9 | const img = this.shadowRoot.getElementById('sigil'); 10 | if (img) { 11 | this.shadowRoot.removeChild(img); 12 | } 13 | 14 | const template = document.createElement('template'); 15 | template.innerHTML = ``; 18 | const comp = template.content.childNodes[0] as HTMLElement; 19 | comp.id = 'sigil'; 20 | 21 | this.shadowRoot.appendChild(comp); 22 | } 23 | } -------------------------------------------------------------------------------- /test/general.test.js: -------------------------------------------------------------------------------- 1 | const sigil = require('../dist/index').default; 2 | 3 | const config = { 4 | point: null, 5 | foreground: 'blue', 6 | background: 'green', 7 | }; 8 | 9 | test('Galaxy works', () => { 10 | config.point = 'zod'; 11 | expect(() => { 12 | sigil(config); 13 | }).not.toThrow(); 14 | }); 15 | 16 | test('Star works', () => { 17 | config.point = 'ridlur'; 18 | expect(() => { 19 | sigil(config); 20 | }).not.toThrow(); 21 | }); 22 | 23 | test('Planet works', () => { 24 | config.point = 'littel-ponnys'; 25 | expect(() => { 26 | sigil(config); 27 | }).not.toThrow(); 28 | }); 29 | 30 | test('Invalid name is invalidated', () => { 31 | config.point = 'and'; 32 | expect(() => { 33 | sigil(config); 34 | }).toThrow(); 35 | }); 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - 2.1.0 (Feb 21, 2024) 4 | 5 | - Adds entry point to `./src/core.ts` to export `sigil` function for non-React or non-browser-based projects. 6 | 7 | - 1.5.9 (Sep 8, 2021) 8 | 9 | - Corrects typo in package.json 10 | - Updates react dep to ^17 11 | 12 | - 1.3.7 (Nov 4, 2019) 13 | 14 | - Refactors how symbols are scaled and transformed for simplicity and accuracy. 15 | - Adds `reactImageRenderer`, which adds a base64 encoded background image to a react element, bypassing any recursive use of `react.createElement()`. 16 | - Alters the `margin` config prop to a boolean, which toggles margin-driven layout behavior 17 | - Deprecates `iconMode` 18 | 19 | - 1.3.5 (Jul 23, 2019) 20 | 21 | - Removes hard-coded inline styles and responds to #37. 22 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | type Config = { 2 | /** 3 | * the point that the sigil will represent, ie ~sampel-palnet 4 | */ 5 | point: string, 6 | 7 | /** 8 | * the background color of the rectangle behind the symbols 9 | */ 10 | background?: string, 11 | /** 12 | * the foreground color of the symbols 13 | */ 14 | foreground?: string, 15 | 16 | /** 17 | * whether to simplify the shapes inside the sigil for use as an icon. 18 | */ 19 | detail?: 'none' | 'default', 20 | /** 21 | * how much space around the sigil itself 22 | */ 23 | space?: 'none' | 'default' | 'large', 24 | 25 | /** 26 | * the size in pixels to draw the sigil, defaults to 128px 27 | */ 28 | size?: number, 29 | } 30 | 31 | type SymbolIndex = { 32 | [key: string]:string 33 | } 34 | 35 | export { 36 | Config, 37 | SymbolIndex, 38 | } -------------------------------------------------------------------------------- /test/perf.test.js: -------------------------------------------------------------------------------- 1 | const sigil = require('../dist/index').default; 2 | const oldLib = require('@tlon/sigil-js'); 3 | 4 | console.log(sigil) 5 | 6 | 7 | const config = { 8 | point: '~ridlur-figbud', 9 | }; 10 | 11 | const old_config = { 12 | patp: '~ridlur-figbud', 13 | }; 14 | 15 | 16 | test('1000 zods', async () => { 17 | expect(() => { 18 | 19 | var iterations = 10000; 20 | console.time('ten-thousand-zods'); 21 | 22 | for(var i = 0; i < iterations; i++ ){ 23 | sigil(config); 24 | }; 25 | 26 | console.timeEnd('ten-thousand-zods') 27 | 28 | var iterations = 10000; 29 | console.time('ten-thousand-zods-old'); 30 | 31 | for(var i = 0; i < iterations; i++ ){ 32 | oldLib.sigil(old_config); 33 | }; 34 | 35 | console.timeEnd('ten-thousand-zods-old') 36 | 37 | }).not.toThrow(); 38 | }); 39 | -------------------------------------------------------------------------------- /scripts/figma.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const Figma = require('figma-js') 3 | 4 | const DOC_REF = 'fkE0F4vb1Wf28MSGeFCFRar3' 5 | const PAGE = 'V1.1' 6 | const TOKEN = process.env.FIGMA_API_TOKEN 7 | 8 | const figma = Figma.Client({ personalAccessToken: TOKEN }) 9 | 10 | const countInstances = arr => arr 11 | .reduce((a, b) => Object.assign(a, {[b]: (a[b] || 0) + 1}), {}) 12 | 13 | const duplicates = dict => Object 14 | .keys(dict) 15 | .filter((a) => dict[a] > 1) 16 | 17 | figma.file(DOC_REF) 18 | .then(res => { 19 | 20 | const components = res.data.components 21 | 22 | const glyphs = Object.values(components) 23 | .filter(item => item.name.split('')[0] === '_') 24 | 25 | const justNames = glyphs 26 | .map(g => g.name) 27 | 28 | console.log('Duplicates') 29 | console.log(duplicates(countInstances(justNames))) 30 | }) 31 | -------------------------------------------------------------------------------- /preview/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Tlon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/denul.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | 4 | // Figma refuses to export a file named 'nul', because of filesystem constraints on Windows machines. This script shims this behavior. 5 | const SVG_INPUT_PATH = __dirname + '/../assets/svg/'; 6 | 7 | const numComparator = (a:number, b:number) => { 8 | if (a < b) return -1 9 | if (a > b) return 1 10 | return 0 11 | } 12 | 13 | const denul = () => { 14 | const chrono = fs 15 | .readdirSync(SVG_INPUT_PATH) 16 | .filter(f => path.extname(f).toLowerCase() === '.svg') 17 | .filter(f => f === 'nul.svg' || f === 'nul1.svg' || f === 'nul-1.svg') 18 | .map(f => ({ 19 | f: f, 20 | t: fs.statSync(SVG_INPUT_PATH + '/' + f, ).birthtimeMs, 21 | })) 22 | .sort((a, b) => numComparator(a.t, b.t)) 23 | 24 | const mostRecent = chrono[0] 25 | 26 | chrono 27 | .slice(1) 28 | .forEach(f => fs.unlinkSync(SVG_INPUT_PATH + f.f)) 29 | 30 | 31 | fs.rename( 32 | SVG_INPUT_PATH + mostRecent.f, 33 | SVG_INPUT_PATH + 'nul.svg', 34 | function(err) { 35 | if ( err ) console.log('ERROR: ' + err); 36 | } 37 | ); 38 | } 39 | 40 | export default denul 41 | -------------------------------------------------------------------------------- /scripts/image-comparison/png.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const path = require('path'); 4 | const sharp = require('sharp'); 5 | const del = require('del'); 6 | 7 | const { PATHS, EXT } = require('./constants') 8 | 9 | const INPUT_PATH = PATHS.svgSmall 10 | const OUTPUT_PATH = PATHS.pngSmall 11 | 12 | const SIZE = 64 13 | 14 | // delete existing files 15 | del.sync([OUTPUT_PATH + '/*.png', `!${OUTPUT_PATH}`]); 16 | 17 | 18 | const convertSvgFiles = (inputPath, outputPath, size) => { 19 | 20 | const promises = fs 21 | .readdirSync(INPUT_PATH) 22 | .filter(filename => path.extname(filename).toLowerCase() === EXT.svg) 23 | .map((f, idx) => { 24 | 25 | process.stdout.write(`${idx+1}`) 26 | process.stdout.write('\033[0G'); 27 | 28 | const fileName = path.basename(f, EXT.svg); 29 | return sharp(INPUT_PATH + f) 30 | .resize(size) 31 | .png() 32 | .toFile(OUTPUT_PATH + fileName + EXT.png, err => { 33 | if (err) console.log(err) 34 | }); 35 | }) 36 | 37 | Promise.all(promises).then(() => { 38 | process.stdout.write('\033[0G'); 39 | }) 40 | } 41 | 42 | 43 | convertSvgFiles(INPUT_PATH, OUTPUT_PATH, SIZE) 44 | -------------------------------------------------------------------------------- /scripts/image-comparison/prep.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const svgson = require('svgson') 3 | const path = require('path') 4 | 5 | const del = require('del'); 6 | 7 | const { PATHS, EXT } = require('./constants') 8 | 9 | // This script modifies strokeWidth of SVGs to make image testing faster and easier by enabling smaller PNGs. 10 | 11 | const INPUT_PATH = PATHS.svg 12 | const OUTPUT_PATH = PATHS.svgSmall 13 | 14 | // delete existing files 15 | del.sync([OUTPUT_PATH + '/*.svg', `!${OUTPUT_PATH}`]); 16 | 17 | // Recurses through AST 18 | const process = node => { 19 | 20 | if (node.attributes.stroke !== undefined) { 21 | node.attributes['stroke-width'] = '3px' 22 | } 23 | 24 | return { 25 | ...node, 26 | children: node.children.map(n => process(n)) 27 | } 28 | } 29 | 30 | fs.readdirSync(INPUT_PATH) 31 | .filter(f => path.extname(f).toLowerCase() === EXT.svg) 32 | .forEach(f => { 33 | // get the file contents as UTF8 34 | const content = fs.readFileSync(INPUT_PATH + f, 'utf8'); 35 | // parse the SVG string into an AST 36 | const ast = svgson.parseSync(content) 37 | // change attributes in the AST 38 | const astProcessed = process(ast) 39 | // parse the AST back into svg 40 | const result = svgson.stringify(astProcessed) 41 | // Write out new files 42 | fs.writeFile( 43 | OUTPUT_PATH + f, 44 | result, 45 | err => { if (err) console.error(err) } 46 | ) 47 | }) 48 | -------------------------------------------------------------------------------- /preview/README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Tailwind CSS Example 2 | 3 | This example shows how to use [Tailwind CSS](https://tailwindcss.com/) [(v2.2)](https://blog.tailwindcss.com/tailwindcss-2-2) with Next.js. It follows the steps outlined in the official [Tailwind docs](https://tailwindcss.com/docs/guides/nextjs). 4 | 5 | It uses the new [`Just-in-Time Mode`](https://tailwindcss.com/docs/just-in-time-mode) for Tailwind CSS. 6 | 7 | ## Preview 8 | 9 | Preview the example live on [StackBlitz](http://stackblitz.com/): 10 | 11 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-tailwindcss) 12 | 13 | ## Deploy your own 14 | 15 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): 16 | 17 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss&project-name=with-tailwindcss&repository-name=with-tailwindcss) 18 | 19 | ## How to use 20 | 21 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: 22 | 23 | ```bash 24 | npx create-next-app --example with-tailwindcss with-tailwindcss-app 25 | # or 26 | yarn create next-app --example with-tailwindcss with-tailwindcss-app 27 | ``` 28 | 29 | Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@urbit/sigil-js", 3 | "version": "2.2.0", 4 | "private": false, 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.esm.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.js", 12 | "import": "./dist/index.esm.js", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./core": { 16 | "require": "./dist/core.js", 17 | "import": "./dist/core.esm.js", 18 | "types": "./dist/core.d.ts" 19 | } 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/urbit/sigil-js" 27 | }, 28 | "devDependencies": { 29 | "@size-limit/esbuild-why": "^8.2.4", 30 | "@size-limit/preset-small-lib": "^8.2.4", 31 | "@tlon/sigil-js": "^1.4.5", 32 | "@tsconfig/recommended": "^1.0.2", 33 | "@types/invariant": "^2.2.31", 34 | "@types/lodash.memoize": "^4.1.7", 35 | "dts-cli": "^2.0.0", 36 | "husky": "^8.0.3", 37 | "typescript": "^5.0.4" 38 | }, 39 | "dependencies": { 40 | "invariant": "^2.2.4", 41 | "lodash.memoize": "^4.1.2" 42 | }, 43 | "scripts": { 44 | "test": "jest", 45 | "dev": "npm run build && cp -a ./dist/* ./preview/lib", 46 | "start": "dts watch", 47 | "build": "dts build --entry ./src/index.ts --entry ./src/core.ts", 48 | "lint": "dts lint", 49 | "lint:fix": "dts lint src --fix", 50 | "size": "size-limit", 51 | "analyze": "size-limit --why", 52 | "prepare": "husky install" 53 | }, 54 | "husky": { 55 | "hooks": { 56 | "pre-commit": "dts lint" 57 | } 58 | }, 59 | "size-limit": [ 60 | { 61 | "path": "dist/sigil-js.cjs.production.min.js", 62 | "limit": "30 KB" 63 | }, 64 | { 65 | "path": "dist/sigil-js.esm.js", 66 | "limit": "30 KB" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /scripts/image-comparison/dupes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const path = require('path'); 4 | const sharp = require('sharp'); 5 | const del = require('del') 6 | const PNG = require('pngjs').PNG 7 | const pixelmatch = require('pixelmatch') 8 | const { removeContentSync } = require('./helpers') 9 | const { PATHS, EXT } = require('./constants') 10 | 11 | const INPUT_PATH = PATHS.pngSmall 12 | const OUTPUT_PATH = PATHS.diff 13 | const MAX_SCORE = 32 14 | const SIZE = 64 15 | 16 | del.sync([OUTPUT_PATH + '/*.png', `!${OUTPUT_PATH}`]); 17 | 18 | const pngs = fs 19 | .readdirSync(INPUT_PATH) 20 | .filter(f => path.extname(f).toLowerCase() === EXT.png) 21 | 22 | var cache = [] 23 | var diff = new PNG({ width: SIZE, height: SIZE }); 24 | var aData = null 25 | var bData = null 26 | var a = null 27 | var b = null 28 | var aName = null 29 | var bName = null 30 | var timeA = null 31 | var timeB = null 32 | var numDiffPixels = null 33 | 34 | for (var i=0, n=pngs.length; i < n; ++i ) { 35 | // pngs.forEach((a, idx) => { 36 | a = pngs[i] 37 | aName = path.basename(a, EXT.png); 38 | aData = PNG.sync.read(fs.readFileSync(INPUT_PATH + a)); 39 | 40 | for (var j=0, m=pngs.length; j < m; ++j ) { 41 | b = pngs[j] 42 | bName = path.basename(b, EXT.png); 43 | 44 | // pngs.forEach(b => { 45 | if (cache.includes(`${bName}:${aName}`) === false && a !== b) { 46 | 47 | process.stdout.write('\033[0G'); 48 | process.stdout.write(`${i+1}:${j+1}`) 49 | 50 | bData = PNG.sync.read(fs.readFileSync(INPUT_PATH + b)); 51 | 52 | numDiffPixels = pixelmatch( 53 | aData.data, 54 | bData.data, 55 | diff.data, 56 | SIZE, 57 | SIZE, 58 | { threshold: 0.1 } 59 | ); 60 | 61 | if (numDiffPixels < MAX_SCORE) { 62 | process.stdout.write('\033[0G'); 63 | process.stdout.write(`O`) 64 | fs.writeFileSync( 65 | `${OUTPUT_PATH}${numDiffPixels}:${aName}:${bName}.png`, 66 | PNG.sync.write(diff) 67 | ); 68 | } 69 | 70 | cache.push(`${aName}:${bName}`) 71 | 72 | } else { 73 | process.stdout.write('\033[0G'); 74 | process.stdout.write(`N`) 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /scripts/toV2.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var { stringify } = require('svgson') 3 | 4 | 5 | const pathToIndexJson = __dirname + "/index.json" 6 | 7 | const pathToDefaultOutput = __dirname + "/../src/symbols/default.ts" 8 | const pathToIconOutput = __dirname + "/../src/symbols/icon.ts" 9 | 10 | const indexObj = JSON.parse(fs.readFileSync(pathToIndexJson)) 11 | 12 | const deepClone = obj => JSON.parse(JSON.stringify(obj)) 13 | 14 | const stringifiedDefaultIndex = Object 15 | .entries(deepClone(indexObj)) 16 | .reduce((acc, [key, val]) => { 17 | 18 | val.attributes = { 19 | transform: '@TR' 20 | } 21 | 22 | val.children.forEach((child => { 23 | if (typeof child.attributes.stroke !== 'undefined') { 24 | child.attributes['stroke-width'] = '@SW' 25 | // child.attributes['vector-effect'] = 'non-scaling-stroke' 26 | 27 | } 28 | 29 | // if (child.type === 'line') 30 | 31 | delete child.attributes.dataisgeon 32 | })) 33 | 34 | acc[key] = stringify(val) 35 | .replace(/"/g, "'"); 36 | return acc 37 | }, {}) 38 | 39 | fs.writeFileSync( 40 | pathToDefaultOutput, 41 | "const index = " + JSON.stringify(stringifiedDefaultIndex).replace(/"([^"]+)":/g, '$1:') + "; export default index" 42 | ) 43 | 44 | 45 | const stringifiedIconIndex = Object 46 | .entries(deepClone(indexObj)) 47 | .reduce((acc, [key, val]) => { 48 | 49 | val.attributes = { 50 | transform: '@TR' 51 | } 52 | 53 | val.children = val.children.filter((child => { 54 | // console.log(child.attributes) 55 | return child.attributes.dataisgeon === 'true' 56 | })) 57 | 58 | val.children.forEach((child => { 59 | if (typeof child.attributes['stroke-width'] !== 'undefined') { 60 | child.attributes['stroke-width'] = '@SW' 61 | // child.attributes['vector-effect'] = 'non-scaling-stroke' 62 | } 63 | 64 | 65 | delete child.attributes.dataisgeon 66 | })) 67 | 68 | acc[key] = stringify(val) 69 | .replace(/"/g, "'"); 70 | return acc 71 | }, {}) 72 | 73 | fs.writeFileSync( 74 | pathToIconOutput, 75 | "const index = " + JSON.stringify(stringifiedIconIndex).replace(/"([^"]+)":/g, '$1:') + "; export default index" 76 | ) -------------------------------------------------------------------------------- /src/components/sigil.ts: -------------------------------------------------------------------------------- 1 | import sigil from '../core'; 2 | import memoize from 'lodash.memoize'; 3 | 4 | const deSig = (patp: string) => patp.replace('~', ''); 5 | const memSigil = memoize(arg => sigil(arg).trim(), 6 | (props) => JSON.stringify(Object.values(props)) 7 | ); 8 | const template = document.createElement('template'); 9 | template.innerHTML = ``; 14 | 15 | export class Sigil extends HTMLElement { 16 | static get observedAttributes() { 17 | return ['point', 'size', 'foreground', 'background', 'space', 'detail']; 18 | } 19 | 20 | constructor() { 21 | super(); 22 | this.attachShadow({mode: 'open'}); 23 | this.shadowRoot?.appendChild(template.content.cloneNode(true)); 24 | } 25 | 26 | connectedCallback() { 27 | this.render(); 28 | } 29 | 30 | attributeChangedCallback() { 31 | this.render(); 32 | } 33 | 34 | getSvg() { 35 | return memSigil({ 36 | point: deSig(this.point || 'zod'), 37 | size: this.size ? parseInt(this.size) : undefined, 38 | foreground: this.foreground, 39 | background: this.background, 40 | space: this.space, 41 | detail: this.detail, 42 | }); 43 | } 44 | 45 | render() { 46 | if (!this.shadowRoot) { 47 | return; 48 | } 49 | 50 | const svg = this.shadowRoot.getElementById('sigil'); 51 | if (svg) { 52 | this.shadowRoot.removeChild(svg); 53 | } 54 | 55 | const template = document.createElement('template'); 56 | template.innerHTML = this.getSvg(); 57 | const comp = template.content.childNodes[0] as HTMLElement; 58 | comp.id = 'sigil'; 59 | this.shadowRoot.appendChild(comp); 60 | } 61 | 62 | get point() { 63 | return this.getAttribute('point'); 64 | } 65 | 66 | get size() { 67 | return this.getAttribute('size'); 68 | } 69 | 70 | get foreground() { 71 | return this.getAttribute('foreground') || undefined; 72 | } 73 | 74 | get background() { 75 | return this.getAttribute('background') || undefined; 76 | } 77 | 78 | get space() { 79 | const attr = this.getAttribute('space'); 80 | if (attr === 'none' || attr === 'default' || attr === 'large') { 81 | return attr; 82 | } 83 | 84 | return undefined 85 | } 86 | 87 | get detail() { 88 | const attr = this.getAttribute('detail'); 89 | if (attr === 'none' || attr === 'default') { 90 | return attr; 91 | } 92 | 93 | return undefined 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sigil-js 2 | 3 | [![npm (scoped)](https://img.shields.io/npm/v/@tlon/sigil-js?style=flat)](https://www.npmjs.com/package/@tlon/sigil-js) 4 | 5 | [→ Github](https://github.com/urbit/sigil-js) 6 | 7 | Urbit address space contains ~4.2 billion unique points. Each has a pronounceable, easily memorized name, something like ~ravmel-ropdyl. Sigils visualize these names – there are as many unique Sigils as there are Azimuth points. `@urbit/sigil-js` is a javascript library that converts one of these names into its corresponding Sigil. 8 | 9 | ![sigil intro image](https://github.com/urbit/sigil-js/blob/master/docs/intro.png?raw=true) 10 | 11 | ## Basic Usage 12 | 13 | 14 | ### React 15 | ```js 16 | import '@urbit/sigil-js' 17 | 18 | const config = { 19 | point: '~zod', // or 'zod' 20 | size: 348, 21 | background:'#010101', 22 | foreground:'yellow', 23 | detail:'none', 24 | space:'none', 25 | } 26 | 27 | const Sigil = ({ config }) => { 28 | return ( 29 | 30 | ) 31 | } 32 | ``` 33 | 34 | ## Install 35 | 36 | `npm install @urbit/sigil-js` 37 | 38 | ## API 39 | 40 | |Param|Explanation|Type|Optional? 41 | |-----|-----------|----|--------- 42 | |`point`|Any valid urbit point name|`string`|No, and can only accept galaxies, stars and planets. 43 | |`size`| Size of desired SVG output| `integer`| Yes, defaults to 128px 44 | |`foreground`| Foreground color| `string` | Yes, defaults to '#FFF' 45 | |`background`| Background color| `string` | Yes, defaults to '#000' 46 | |`detail`| Controls whether or not details should be rendered. Useful for small sizes below 48px | `none` or `default` | Yes, defaults to `default` 47 | |`space`| space between edge of sigil and edge of background| `none`, `large` or `default` | Yes, defaults to `default` 48 | 49 | 50 | ### Build 51 | 52 | |Commands | Description | 53 | | -------------------- | --------------------------------------------- | 54 | |`npm run build`| Build the library from source | 55 | |`npm run dev`| Build the library from source and copies build into `/preview` | 56 | 57 | 58 | ### Tests and Performance 59 | 60 | `npm run test` 61 | 62 | ### Contributing 63 | Please read [CONTRIBUTING.md](https://github.com/urbit/sigil-js/CONTRIBUTING.md) for details on the process for submitting pull requests to us. 64 | 65 | ### Authors 66 | - ~ridlur-figbud 67 | 68 | ### License 69 | This project is licensed under the MIT License - see the [LICENSE.txt](https://github.com/urbit/sigil-js/LICENSE.md) file for details 70 | 71 | ![sigil outro image](https://github.com/urbit/sigil-js/blob/master/docs/outro.png?raw=true) 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Urbit 2 | 3 | Thank you for your interest in contributing to Urbit. 4 | 5 | 6 | ## Git practice 7 | 8 | Since we use the GitHub issue tracker, it is helpful (though not 9 | required) to contribute via a GitHub pull request. If you already know 10 | what you are doing, skip down to the Style section. 11 | 12 | Start by cloning the repository on your work machine: 13 | 14 | ``` 15 | $ git clone https://github.com/urbit/sigil-js 16 | ``` 17 | 18 | And, additionally, fork the repository on GitHub by clicking the "Fork" 19 | button. Add your fork as a remote: 20 | 21 | ``` 22 | $ git remote add [username] https://github.com/[username]/sigil-js 23 | ``` 24 | 25 | and set it as the default remote to push to: 26 | 27 | ``` 28 | $ git config --local remote.pushDefault [username] 29 | ``` 30 | 31 | This is good practice for any project that uses git. You will pull 32 | upstream branches from urbit/sigil-js and push to your personal sigil-js fork 33 | by default. 34 | 35 | Next, start a new branch to do your work on. For `sigil-js`, please use the 36 | latest tagged release as your starting point. For other repositories, 37 | anywhere pointed to by `master` is alright to start from. 38 | 39 | ``` 40 | $ git checkout -b [branch name] [starting point] 41 | ``` 42 | 43 | Now you are free to do your work on this branch. When finished, you may 44 | want to clean up your commits: 45 | 46 | ``` 47 | $ git rebase -i [starting point] 48 | ``` 49 | 50 | Then you can push to your public fork with `git push` and make a pull 51 | request via the GitHub UI. 52 | 53 | After your changes are merged upstream, you can delete your branch (via 54 | github UI or `git push :[branch]` remotely, and with `git branch -d` 55 | locally). 56 | 57 | ## Style 58 | 59 | We prefer the [AirBnb JS styleguide](https://github.com/airbnb/javascript). At minimum, your code should be simple and well-organized. 60 | 61 | 62 | ## What to work on 63 | 64 | If you are not thinking of contributing with a specific goal in mind, 65 | the GitHub issue tracker is the first place you should look for ideas. 66 | Issues are tagged with a priority and a difficulty. A good place to 67 | start is on either a low-difficulty issue or a low-priority issue. 68 | Higher priority issues are likely to be assigned to someone - if this is 69 | the case, then contacting that person to coordinate before starting to 70 | work is probably a good idea. 71 | 72 | There is also a "help wanted" tag for things that we are especially 73 | eager to have outside contributions on. Check here first! 74 | 75 | ## Staying in touch 76 | 77 | Questions or other communications about contributing to Urbit can go to 78 | [support@urbit.org](mailto:support@urbit.org). 79 | -------------------------------------------------------------------------------- /scripts/phonemes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { PATHS, EXT } = require('./constants') 3 | 4 | const INPUT_PATH = PATHS.lib 5 | 6 | const _prefixes = ` 7 | dozmarbinwansamlitsighidfidlissogdirwacsabwissib\ 8 | rigsoldopmodfoglidhopdardorlorhodfolrintogsilmir\ 9 | holpaslacrovlivdalsatlibtabhanticpidtorbolfosdot\ 10 | losdilforpilramtirwintadbicdifrocwidbisdasmidlop\ 11 | rilnardapmolsanlocnovsitnidtipsicropwitnatpanmin\ 12 | ritpodmottamtolsavposnapnopsomfinfonbanmorworsip\ 13 | ronnorbotwicsocwatdolmagpicdavbidbaltimtasmallig\ 14 | sivtagpadsaldivdactansidfabtarmonranniswolmispal\ 15 | lasdismaprabtobrollatlonnodnavfignomnibpagsopral\ 16 | bilhaddocridmocpacravripfaltodtiltinhapmicfanpat\ 17 | taclabmogsimsonpinlomrictapfirhasbosbatpochactid\ 18 | havsaplindibhosdabbitbarracparloddosbortochilmac\ 19 | tomdigfilfasmithobharmighinradmashalraglagfadtop\ 20 | mophabnilnosmilfopfamdatnoldinhatnacrisfotribhoc\ 21 | nimlarfitwalrapsarnalmoslandondanladdovrivbacpol\ 22 | laptalpitnambonrostonfodponsovnocsorlavmatmipfip\ 23 | ` 24 | 25 | const _suffixes = ` 26 | zodnecbudwessevpersutletfulpensytdurwepserwylsun\ 27 | rypsyxdyrnuphebpeglupdepdysputlughecryttyvsydnex\ 28 | lunmeplutseppesdelsulpedtemledtulmetwenbynhexfeb\ 29 | pyldulhetmevruttylwydtepbesdexsefwycburderneppur\ 30 | rysrebdennutsubpetrulsynregtydsupsemwynrecmegnet\ 31 | secmulnymtevwebsummutnyxrextebfushepbenmuswyxsym\ 32 | selrucdecwexsyrwetdylmynmesdetbetbeltuxtugmyrpel\ 33 | syptermebsetdutdegtexsurfeltudnuxruxrenwytnubmed\ 34 | lytdusnebrumtynseglyxpunresredfunrevrefmectedrus\ 35 | bexlebduxrynnumpyxrygryxfeptyrtustyclegnemfermer\ 36 | tenlusnussyltecmexpubrymtucfyllepdebbermughuttun\ 37 | bylsudpemdevlurdefbusbeprunmelpexdytbyttyplevmyl\ 38 | wedducfurfexnulluclennerlexrupnedlecrydlydfenwel\ 39 | nydhusrelrudneshesfetdesretdunlernyrsebhulryllud\ 40 | remlysfynwerrycsugnysnyllyndyndemluxfedsedbecmun\ 41 | lyrtesmudnytbyrsenwegfyrmurtelreptegpecnelnevfes\ 42 | ` 43 | 44 | const suffixes = _suffixes.match(/.{1,3}/g); 45 | const prefixes = _prefixes.match(/.{1,3}/g); 46 | const all = [...suffixes, ...prefixes]; 47 | 48 | 49 | 50 | const index = fs 51 | .readFileSync(INPUT_PATH + 'index.json', 'utf8') 52 | 53 | const json = JSON.parse(index) 54 | 55 | const keys = Object.keys(json) 56 | 57 | 58 | const missingSuffixes = suffixes.reduce((acc, phoneme) => { 59 | if (keys.includes(phoneme) === true) { 60 | } else { 61 | acc = acc.concat(phoneme) 62 | } 63 | return acc 64 | }, []) 65 | 66 | const missingPrefixes = prefixes.reduce((acc, phoneme) => { 67 | if (keys.includes(phoneme) === true) { 68 | } else { 69 | acc = acc.concat(phoneme) 70 | } 71 | return acc 72 | }, []) 73 | 74 | const non = keys.reduce((acc, key) => { 75 | if (all.includes(key) === true) { 76 | } else { 77 | acc = acc.concat(key) 78 | } 79 | return acc 80 | }, []) 81 | 82 | 83 | const results = { 84 | phonemeCount: `${keys.length} / 512`, 85 | missingSuffixes: `${missingSuffixes.length}, ${missingSuffixes.toString(', ')}`, 86 | missingPrefixes: `${missingPrefixes.length}, ${missingPrefixes.toString(', ')}`, 87 | invalidStrings: `${non.length}, ${non.toString(', ')}` 88 | } 89 | 90 | console.table(results) 91 | -------------------------------------------------------------------------------- /scripts/build-index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import * as svgson from 'svgson' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import * as del from 'del' 6 | import { Ast } from '../types' 7 | 8 | // Paths relative from /dist 9 | const SVG_INPUT_PATH = __dirname + '/../assets/svg/'; 10 | const INDEX_OUTPUT_PATH = __dirname + '/../assets/'; 11 | const FG = '@FG' 12 | const BG = '@BG' 13 | 14 | const compose = (...fns: Array) => { 15 | return fns.reduce((f, g) => (...xs: any) => { 16 | const r = g(...xs); 17 | return Array.isArray(r) ? f(...r) : f(r); 18 | }); 19 | } 20 | 21 | const buildIndex = () => { 22 | // Load all SVGs, process them and add them to an index object 23 | const index = fs 24 | .readdirSync(SVG_INPUT_PATH) 25 | .filter(f => path.extname(f).toLowerCase() === '.svg') 26 | .reduce((acc, f) => { 27 | const content = fs.readFileSync(SVG_INPUT_PATH + f, 'utf8'); 28 | const k = path.basename(f, '.svg'); 29 | const ast = svgson.parseSync(content); 30 | // compose executes functions from bottom (last) to first (top) 31 | //@ts-ignore 32 | acc[k] = compose( 33 | // remove several attributes to save disk space 34 | removeType, 35 | removeValue, 36 | removeId, 37 | // assigns fill and stroke color (FG or BG) 38 | createPaintTargets, 39 | // add fill="none" to tags with no fill 40 | // ensureNoFill, 41 | // Unwraps nested groups and wraps all children in single group tage with data-geon marked if the element is a base shape 42 | normalizeGroups, 43 | // Removes the root tag, in this case we remove the tag 44 | removeOuterSVGTag, 45 | // removeClipPath, 46 | removeDefs, 47 | )(ast); 48 | return acc; 49 | }, {}); 50 | 51 | // Delete the existing index.json file 52 | del.sync(SVG_INPUT_PATH + 'index.json') 53 | 54 | // Write the index object to disk as JSON file 55 | fs.writeFile( 56 | INDEX_OUTPUT_PATH + 'index.json', 57 | JSON.stringify(index, null, 2), 58 | (err) => { 59 | if (err) console.log(err) 60 | }) 61 | } 62 | 63 | const removeOuterSVGTag = (node:Ast) => node.children[0]; 64 | 65 | const flatten = (_acc:Ast[], children:Ast[]):Ast[] => children.reduce((acc, c) => { 66 | acc = [...acc, c] 67 | return flatten(acc, c.children) 68 | }, _acc) 69 | 70 | const removeChildren = (arr:Ast[]):Ast[] => arr.map(n => { 71 | n.children = [] 72 | return n 73 | }) 74 | 75 | const removeGroups = (arr: Ast[]):Ast[] => arr.filter(n => { 76 | if (n.name === 'g') return false 77 | return true 78 | }) 79 | 80 | const normalizeGroups = (node:Ast) => { 81 | const group = { 82 | name: "g", 83 | type: "element", 84 | value: "", 85 | attributes: {}, 86 | children: removeGroups(removeChildren(flatten([], node.children))), 87 | } 88 | // Assign property to root shape 89 | group.children[0].attributes['dataisgeon'] = 'true' 90 | return group 91 | } 92 | 93 | const createPaintTargets = (node:Ast):Ast => { 94 | 95 | if (node.attributes.fill === undefined) { 96 | node.attributes.fill = 'none' 97 | } 98 | 99 | if (node.attributes.fill === 'white') { 100 | node.attributes.fill = BG 101 | } 102 | 103 | if (node.attributes.fill === 'black') { 104 | node.attributes.fill = FG 105 | } 106 | 107 | if (node.attributes.stroke === 'white') { 108 | node.attributes.stroke = BG 109 | } 110 | 111 | if (node.attributes.stroke === 'black') { 112 | node.attributes.stroke = FG 113 | } 114 | 115 | return { 116 | children: node.children.map(n => createPaintTargets(n)), 117 | ...node, 118 | } 119 | } 120 | 121 | // const ensureNoFill = (node:Ast):Ast => { 122 | // return { 123 | // attributes: { 124 | // ...node.attributes, 125 | // fill: node.attributes.fill === undefined 126 | // ? 'none' 127 | // : node.attributes.fill 128 | // }, 129 | // children: node.children.map(n => ensureNoFill(n)), 130 | // ...node 131 | // } 132 | // } 133 | 134 | const removeId = (node:Ast):Ast => { 135 | delete node.attributes.id 136 | return { 137 | ...node, 138 | children: node.children.map(c => removeId(c)) 139 | } 140 | } 141 | 142 | const removeType = (node:Ast):Ast => { 143 | delete node.type 144 | return { 145 | ...node, 146 | children: node.children.map(c => removeType(c)) 147 | } 148 | } 149 | 150 | const removeValue = (node:Ast):Ast => { 151 | delete node.value 152 | return { 153 | ...node, 154 | children: node.children.map(c => removeValue(c)) 155 | } 156 | } 157 | 158 | const removeDefs = (node:Ast):Ast => { 159 | return { 160 | children: node.children 161 | .filter(n => n.name !== 'defs') 162 | .map(n => removeDefs(n)), 163 | ...node, 164 | } 165 | } 166 | 167 | export default buildIndex 168 | -------------------------------------------------------------------------------- /scripts/image-comparison/compare.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | const path = require('path'); 4 | const sharp = require('sharp'); 5 | const PNG = require('pngjs').PNG 6 | const pixelmatch = require('pixelmatch') 7 | const del = require('del') 8 | 9 | const { PATHS, EXT } = require('./constants') 10 | const { pour, PlainSVGStringRenderer } = require('./legacy/pour') 11 | const { sigil, stringRenderer } = require('../lib/dist/index') 12 | 13 | const MIN_SCORE = 0 14 | const SIZE = 400 15 | // const INPUT_PATH_OLD = PATH. 16 | // const INPUT_PATH_NEW = 17 | 18 | del.sync([PATHS.pngComp + '/*.png', `!${PATHS.pngComp}`]); 19 | del.sync([PATHS.comp + '/*.png', `!${PATHS.comp}`]); 20 | del.sync([PATHS.allComp + '/*.png', `!${PATHS.allComp}`]); 21 | 22 | let json = { 23 | 24 | } 25 | 26 | let csv = '' 27 | 28 | 29 | const oldPromises = fs 30 | .readdirSync(PATHS.svg) 31 | .filter(filename => path.extname(filename).toLowerCase() === EXT.svg) 32 | .map(f => path.basename(f, EXT.svg)) 33 | .map(p => ({ 34 | s: pour({ 35 | patp: p, 36 | renderer: PlainSVGStringRenderer, 37 | colorway: ['white', 'black'], 38 | size: SIZE, 39 | }), 40 | p: p, 41 | })) 42 | .map(({ s, p }, idx) => { 43 | process.stdout.write(`${p}: new`) 44 | process.stdout.write('\033[0G'); 45 | return sharp(Buffer.from(s)) 46 | .resize(SIZE) 47 | .png() 48 | .toFile(PATHS.pngComp + p + '-old' + EXT.png) 49 | // .then(info => { console.log(info) }) 50 | // .catch(err => { console.error(err) }); 51 | }) 52 | 53 | // Generate PNGs with new lib 54 | const newPromises = fs 55 | .readdirSync(PATHS.svg) 56 | .filter(filename => path.extname(filename).toLowerCase() === EXT.svg) 57 | .map(f => path.basename(f, EXT.svg)) 58 | .map(p => ({ 59 | s: sigil({ 60 | patp: p, 61 | renderer: stringRenderer, 62 | color: ['white', 'black'], 63 | size: SIZE, 64 | margin: 29.5, 65 | strokeWidth: 2, 66 | }), 67 | p: p, 68 | })) 69 | .map(({ s, p }, idx) => { 70 | process.stdout.write(`${p}: new`) 71 | process.stdout.write('\033[0G'); 72 | return sharp(Buffer.from(s)) 73 | .resize(SIZE) 74 | .png() 75 | .toFile(PATHS.pngComp + p + '-new' + EXT.png) 76 | // .then(info => { console.log(info) }) 77 | // .catch(err => { console.error(err) }); 78 | }) 79 | 80 | var png = null 81 | var a = null 82 | var n = null 83 | var o = null 84 | var nData = null 85 | var oData = null 86 | var diff = new PNG({ width: SIZE, height: SIZE }); 87 | // var out = new PNG({ width: SIZE*3, height: SIZE }); 88 | var numDiffPixels = null 89 | 90 | process.stdout.write(`resolving...`) 91 | process.stdout.write('\033[0G'); 92 | 93 | Promise.all([...oldPromises, ...newPromises]) 94 | .then(() => { 95 | // filenames should be the same between old and new 96 | const compositePromises = fs 97 | .readdirSync(PATHS.svg) 98 | .filter(f => path.extname(f).toLowerCase() === EXT.svg) 99 | .map(f => path.basename(f, EXT.svg)) 100 | .map(ph => { 101 | // png = filenames[i] 102 | n = PATHS.pngComp + ph + '-new' + EXT.png 103 | o = PATHS.pngComp + ph + '-old' + EXT.png 104 | 105 | process.stdout.write(`${ph}: compare`) 106 | process.stdout.write('\033[0G'); 107 | 108 | nData = PNG.sync.read(fs.readFileSync(n)); 109 | oData = PNG.sync.read(fs.readFileSync(o)); 110 | 111 | numDiffPixels = pixelmatch( 112 | nData.data, 113 | oData.data, 114 | diff.data, 115 | SIZE, 116 | SIZE, 117 | { threshold: 0.1, includeAA: true } 118 | ); 119 | 120 | json[ph] = { 121 | phoneme: ph, 122 | diffPx: numDiffPixels, 123 | } 124 | 125 | csv = `${csv}\n${ph},${numDiffPixels}` 126 | 127 | 128 | 129 | 130 | 131 | 132 | fs.writeFileSync( 133 | `${PATHS.comp}${numDiffPixels}-${ph}-diff${EXT.png}`, 134 | PNG.sync.write(diff) 135 | ); 136 | 137 | fs.writeFileSync( 138 | `${PATHS.pngComp}${ph}-diff${EXT.png}`, 139 | PNG.sync.write(diff) 140 | ); 141 | 142 | return sharp({ 143 | create: { 144 | width: SIZE*3, 145 | height: SIZE, 146 | channels: 4, 147 | background: { r: 0, g: 0, b: 0, alpha: 1 } 148 | } 149 | }) 150 | .composite([ 151 | { input: `${PATHS.pngComp}${ph}-old${EXT.png}`, gravity: 'northeast' }, 152 | { input: `${PATHS.pngComp}${ph}-new${EXT.png}`, gravity: 'centre' }, 153 | { input: `${PATHS.pngComp}${ph}-diff${EXT.png}`, gravity: 'northwest' }, 154 | ]) 155 | // .png() 156 | .toFile(PATHS.allComp + ph + EXT.png) 157 | 158 | }) 159 | 160 | Promise.all(compositePromises).then(() => {}).catch(e => console.log(e)) 161 | 162 | fs.writeFileSync(`${PATHS.logs}log.csv`, csv) 163 | fs.writeFileSync(`${PATHS.logs}log.json`, JSON.stringify(json)) 164 | 165 | }) 166 | .catch(e => console.log(e)) 167 | 168 | 169 | 170 | // create and save svgs 171 | // create and save pngs 172 | // pngs 173 | // pngsOld 174 | 175 | // for each png in pngs, get the other png from pngsOld 176 | // then, compare each png with the other png 177 | // if the diff value is above threshold, 178 | // save the diff 179 | // append an item to a CSV log: phoneme, oldPng, newPng, diffpng, diff value 180 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import {Config, SymbolIndex} from './types'; 3 | import DEFAULT_INDEX from './symbols/default' 4 | import ICON_INDEX from './symbols/icon' 5 | 6 | // Splits a string into equal-sized substrings and returns an array of these substrings. 7 | const chunkStr = (str: string, size: number):string[] => { 8 | const regex = new RegExp(`.{1,${size}}`, 'g') 9 | const result = str.match(regex) 10 | 11 | if (result === null) { 12 | return [''] 13 | } 14 | 15 | return result 16 | } 17 | 18 | export default function sigil({ 19 | point, 20 | background = '#000', 21 | foreground = '#FFF', 22 | size = 128, 23 | space = 'default', 24 | detail = 'default', 25 | }: Config) { 26 | 27 | // Point must be defined, otherwise there is nothing to do. 28 | 29 | invariant( 30 | point !== undefined, 31 | "@tlon/sigil-js must be given a point name as input." 32 | ) 33 | 34 | // The 'none' mode visually removes all superimposed linework on top of the core shapes of the sigil. It is implemented as a separate index. 35 | 36 | let symbolsIndex:SymbolIndex 37 | if (detail === 'none') { 38 | symbolsIndex = ICON_INDEX 39 | } else { 40 | symbolsIndex = DEFAULT_INDEX 41 | } 42 | 43 | // Get phonemes as array from patp input and split into array 44 | 45 | let phonemes = chunkStr(point.replace(/[\^~-]/g, ''), 3) 46 | 47 | // Point name must be valid in several ways. 1) must be a valid @p data type. 2) Must be a planet, star or galaxy. 48 | 49 | invariant( 50 | Array.isArray(phonemes) || phonemes !== null, 51 | `@urbit/sigil-js recieved an invalid point name and cannot render a sigil. Recieved '${point}'.` 52 | ) 53 | 54 | invariant( 55 | phonemes.length === 1 || phonemes.length === 2 || phonemes.length === 4, 56 | `@urbit/sigil-js cannot render point name of length '${phonemes.length}'. Recieved "${point}". Only lengths of 1 (galaxy), 2 (star), and 4 (planet) are supported at this time.` 57 | ) 58 | 59 | // Symbols are stored in the index js files as svg strings indexed by phoneme. They need to be retrieved from the index with a little bit of string processing to fill in the templated parts of the SVG, ie color. 60 | 61 | const innerSVG = phonemes.reduce((acc, phoneme, index) => { 62 | 63 | invariant( 64 | typeof symbolsIndex[phoneme] !== 'undefined', 65 | `@tlon/sigil-js requires a valid point name. '${phoneme}' is not a valid point name phoneme.` 66 | ) 67 | 68 | const SVGSubstring = symbolsIndex[phoneme] as string 69 | 70 | // Symbols don't know where they should be on the canvas out of the index. 71 | 72 | const scale = (size / 256) 73 | const symbolTransformation = index === 0 74 | ? `scale(${scale}) translate(0,0) ` 75 | : index === 1 76 | ? `scale(${scale}) translate(128,0)` 77 | : index === 2 78 | ? `scale(${scale}) translate(0,128)` 79 | : `scale(${scale}) translate(128,128)` 80 | 81 | // Path stroke-widths should never be less than 1px wide 82 | const strokeWidth = size < 64 83 | ? (256 / size).toString() 84 | : '4' 85 | 86 | // Symbols also don't know what color they should be. Variables in symbols are denoted with an '@'. 87 | // @GF = foreground color, @BG = background color, @TR = transformation applied to each symbol and @SW = stroke-width 88 | 89 | let newSVGSubstring = SVGSubstring 90 | .replaceAll('@FG', foreground) 91 | .replaceAll('@BG', background) 92 | .replaceAll('@TR', symbolTransformation) 93 | .replaceAll('@SW', strokeWidth) 94 | 95 | acc = acc + newSVGSubstring 96 | return acc 97 | }, ''); 98 | 99 | // 'Space' is a number in pixels which determines the interior space between the symbols and the background border. 100 | // Space adjusts `scale`, which makes the existing symbols smaller. It also needs to adjust `translate` because the symbols will need to be recentered. 101 | // The symbols are wrapped in a group so they can be moved around instead of adjusted individually. 102 | 103 | const width = size 104 | 105 | // If the sigil is for a star, and space is set to 'none', change the height so a rectangle is returned. 106 | 107 | const height = space === 'none' && phonemes.length === 2 108 | ? size / 2 109 | : size 110 | 111 | const groupTransformation = function f() { 112 | if (space === 'none') { 113 | return phonemes.length === 1 114 | ? `scale(2)` 115 | : phonemes.length === 2 116 | ? `` 117 | : `` 118 | } else if (space === 'large') { 119 | return phonemes.length === 1 120 | ? `translate(${(size*0.5) - (size*0.125)},${(size*0.5) - (size*0.125)}) scale(0.50)` 121 | : phonemes.length === 2 122 | ? `translate(${(size*0.5) - (size*0.25)},${(size*0.5) - (size*0.125)}) scale(0.50)` 123 | : `translate(${(size*0.5) - (size*0.25)},${(size*0.5) - (size*0.25)}) scale(0.50)` 124 | } else { 125 | return phonemes.length === 1 126 | ? `translate(${(size*0.5) - (size*0.1875)},${(size*0.5) - (size*0.1875)}) scale(0.75)` 127 | : phonemes.length === 2 128 | ? `translate(${(size*0.5) - (size*0.3750)},${(size*0.5) - (size*0.1875)}) scale(0.75)` 129 | : `translate(${(size*0.5) - (size*0.3750)},${(size*0.5) - (size*0.3750)}) scale(0.75)` 130 | } 131 | }() 132 | 133 | // Merge arguments, computed property values and inner SVG substring and return. 134 | 135 | return ` 136 | 144 | 145 | 146 | ${innerSVG} 147 | 148 | 149 | ` 150 | } 151 | -------------------------------------------------------------------------------- /preview/pages/index.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { useEffect, useState } from 'react' 3 | import ob from 'urbit-ob' 4 | 5 | const sequence = num => Array.from(Array(num), (_, i) => i); 6 | 7 | const compose = (...fs) => { 8 | return fs.reduceRight((f, g) => (...args) => g(f(...args)), (v) => v) 9 | } 10 | const randInt = max => Math.floor(Math.random() * Math.floor(max)); 11 | 12 | const noSig = str => str.replace(/~+/g, ''); 13 | 14 | const COMET = 'COMET' 15 | const MOON = 'MOON' 16 | const PLANET = 'PLANET' 17 | const STAR = 'STAR' 18 | const GALAXY = 'GALAXY' 19 | 20 | 21 | const randPatp = k => { 22 | let b = 8; 23 | if (k === COMET) b = 128; 24 | if (k === MOON) b = 64; 25 | if (k === PLANET) b = 32; 26 | if (k === STAR) b = 16; 27 | if (k === GALAXY) b = 8; 28 | return randInt(Math.pow(2, b) - 1); 29 | } 30 | 31 | 32 | const randPlanet = () => compose(noSig, ob.patp, randPatp)(PLANET); 33 | 34 | export default function Home() { 35 | useEffect(() => { 36 | import('../lib/index') 37 | }, []) 38 | 39 | const [point, setPoint] = useState('ridlur-figsun') 40 | const [size, setSize] = useState(150) 41 | const [qty, setQty] = useState(1) 42 | const [detail, setDetail] = useState('default') 43 | const [space, setSpace] = useState('large') 44 | 45 | return ( 46 |
47 | 48 | Sigil Preview 49 | 50 | 51 |
52 | 53 | { 54 | sequence(qty).map(() => { 55 | return ( 56 | 57 | ) 58 | }) 59 | } 60 | 61 |
64 |
65 |

Sigil-js V2.0

66 | 67 | 73 | 74 | 80 | 81 | 87 | 88 | 94 | 95 | 101 | 102 | 108 | 109 | 115 | 116 |
117 | Space 118 | 124 | 130 | 136 |
137 |
138 |
141 |
142 | Size 143 | 149 | 155 | 161 | 162 | 168 | 169 | 175 |
176 | Detail Mode 177 | 183 | 189 | 190 |

191 | Viewing {point} 192 |

193 | 194 |
195 |
196 |
197 |
198 | ) 199 | } 200 | -------------------------------------------------------------------------------- /src/symbols/icon.ts: -------------------------------------------------------------------------------- 1 | const index = {bac:"",bal:"",ban:"",bar:"",bat:"",bec:"",bel:"",ben:"",bep:"",ber:"",bes:"",bet:"",bex:"",bic:"",bid:"",bil:"",bin:"",bis:"",bit:"",bol:"",bon:"",bor:"",bos:"",bot:"",bud:"",bur:"",bus:"",byl:"",byn:"",byr:"",byt:"",dab:"",dac:"",dal:"",dan:"",dap:"",dar:"",das:"",dat:"",dav:"",deb:"",dec:"",def:"",deg:"",del:"",dem:"",den:"",dep:"",der:"",des:"",det:"",dev:"",dex:"",dib:"",dif:"",dig:"",dil:"",din:"",dir:"",dis:"",div:"",doc:"",dol:"",don:"",dop:"",dor:"",dos:"",dot:"",dov:"",doz:"",duc:"",dul:"",dun:"",dur:"",dus:"",dut:"",dux:"",dyl:"",dyn:"",dyr:"",dys:"",dyt:"",fab:"",fad:"",fal:"",fam:"",fan:"",fas:"",feb:"",fed:"",fel:"",fen:"",fep:"",fer:"",fes:"",fet:"",fex:"",fid:"",fig:"",fil:"",fin:"",fip:"",fir:"",fit:"",fod:"",fog:"",fol:"",fon:"",fop:"",for:"",fos:"",fot:"",ful:"",fun:"",fur:"",fus:"",fyl:"",fyn:"",fyr:"",hab:"",hac:"",had:"",hal:"",han:"",hap:"",har:"",has:"",hat:"",hav:"",heb:"",hec:"",hep:"",hes:"",het:"",hex:"",hid:"",hil:"",hin:"",hob:"",hoc:"",hod:"",hol:"",hop:"",hos:"",hul:"",hus:"",hut:"",lab:"",lac:"",lad:"",lag:"",lan:"",lap:"",lar:"",las:"",lat:"",lav:"",leb:"",lec:"",led:"",leg:"",len:"",lep:"",ler:"",let:"",lev:"",lex:"",lib:"",lid:"",lig:"",lin:"",lis:"",lit:"",liv:"",loc:"",lod:"",lom:"",lon:"",lop:"",lor:"",los:"",luc:"",lud:"",lug:"",lun:"",lup:"",lur:"",lus:"",lut:"",lux:"",lyd:"",lyn:"",lyr:"",lys:"",lyt:"",lyx:"",mac:"",mag:"",mal:"",map:"",mar:"",mas:"",mat:"",meb:"",mec:"",med:"",meg:"",mel:"",mep:"",mer:"",mes:"",met:"",mev:"",mex:"",mic:"",mid:"",mig:"",mil:"",min:"",mip:"",mir:"",mis:"",mit:"",moc:"",mod:"",mog:"",mol:"",mon:"",mop:"",mor:"",mos:"",mot:"",mud:"",mug:"",mul:"",mun:"",mur:"",mus:"",mut:"",myl:"",myn:"",myr:"",nac:"",nal:"",nam:"",nap:"",nar:"",nat:"",nav:"",neb:"",nec:"",ned:"",nel:"",nem:"",nep:"",ner:"",nes:"",net:"",nev:"",nex:"",nib:"",nid:"",nil:"",nim:"",nis:"",noc:"",nod:"",nol:"",nom:"",nop:"",nor:"",nos:"",nov:"",nub:"",nul:"",num:"",nup:"",nus:"",nut:"",nux:"",nyd:"",nyl:"",nym:"",nyr:"",nys:"",nyt:"",nyx:"",pac:"",pad:"",pag:"",pal:"",pan:"",par:"",pas:"",pat:"",pec:"",ped:"",peg:"",pel:"",pem:"",pen:"",per:"",pes:"",pet:"",pex:"",pic:"",pid:"",pil:"",pin:"",pit:"",poc:"",pod:"",pol:"",pon:"",pos:"",pub:"",pun:"",pur:"",put:"",pyl:"",pyx:"",rab:"",rac:"",rad:"",rag:"",ral:"",ram:"",ran:"",rap:"",rav:"",reb:"",rec:"",red:"",ref:"",reg:"",rel:"",rem:"",ren:"",rep:"",res:"",ret:"",rev:"",rex:"",rib:"",ric:"",rid:"",rig:"",ril:"",rin:"",rip:"",ris:"",rit:"",riv:"",roc:"",rol:"",ron:"",rop:"",ros:"",rov:"",ruc:"",rud:"",rul:"",rum:"",run:"",rup:"",rus:"",rut:"",rux:"",ryc:"",ryd:"",ryg:"",ryl:"",rym:"",ryn:"",ryp:"",rys:"",ryt:"",ryx:"",sab:"",sal:"",sam:"",san:"",sap:"",sar:"",sat:"",sav:"",seb:"",sec:"",sed:"",sef:"",seg:"",sel:"",sem:"",sen:"",sep:"",ser:"",set:"",sev:"",sib:"",sic:"",sid:"",sig:"",sil:"",sim:"",sip:"",sit:"",siv:"",soc:"",sog:"",sol:"",som:"",son:"",sop:"",sor:"",sov:"",sub:"",sud:"",sug:"",sul:"",sum:"",sun:"",sup:"",sur:"",sut:"",syd:"",syl:"",sym:"",syn:"",syp:"",syr:"",syt:"",syx:"",tab:"",tac:"",tad:"",tag:"",tal:"",tam:"",tan:"",tap:"",tar:"",tas:"",teb:"",tec:"",ted:"",teg:"",tel:"",tem:"",ten:"",tep:"",ter:"",tes:"",tev:"",tex:"",tic:"",tid:"",til:"",tim:"",tin:"",tip:"",tir:"",tob:"",toc:"",tod:"",tog:"",tol:"",tom:"",ton:"",top:"",tor:"",tuc:"",tud:"",tug:"",tul:"",tun:"",tus:"",tux:"",tyc:"",tyd:"",tyl:"",tyn:"",typ:"",tyr:"",tyv:"",wac:"",wal:"",wan:"",wat:"",web:"",wed:"",weg:"",wel:"",wen:"",wep:"",wer:"",wes:"",wet:"",wex:"",wic:"",wid:"",win:"",wis:"",wit:"",wol:"",wor:"",wyc:"",wyd:"",wyl:"",wyn:"",wyt:"",wyx:"",zod:""}; export default index --------------------------------------------------------------------------------