├── site └── .gitkeep ├── .prettierignore ├── .gitignore ├── .prettierrc ├── src ├── element.ts ├── compiler.ts ├── psvg.ts ├── parser.ts └── transpiler.ts ├── .gitattributes ├── tsconfig.json ├── bin └── psvg.js ├── examples ├── helloworld.psvg ├── sierpinski.psvg ├── tree.psvg ├── koch.psvg ├── pythagoras.psvg ├── textanim.psvg ├── hilbert.psvg ├── schotter.psvg ├── shapemorph.psvg ├── poisson.psvg ├── sphere.psvg ├── terrain.psvg ├── pulsar.psvg ├── README.md ├── hilbert.svg ├── turing.psvg ├── helloworld.svg ├── shapemorph.svg └── turing.svg ├── package.json ├── tools ├── compile_examples.js └── make_site.js ├── README.md └── QUICKSTART.md /site/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | .DS_Store 3 | **/.DS_Store 4 | node_modules/ 5 | site/index.html 6 | dist 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /src/element.ts: -------------------------------------------------------------------------------- 1 | export interface PSVGElement { 2 | tagName: string; 3 | children: PSVGElement[]; 4 | attributes: Record; 5 | innerHTML: string; 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | site/* linguist-generated 2 | site/* inguist-detectable=false 3 | site linguist-generated 4 | site inguist-detectable=false 5 | *.html linguist-detectable=false 6 | *.psvg linguist-language=SVG 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["ESNext", "DOM"], 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /bin/psvg.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const fs = require('fs'); 4 | const { compilePSVG } = require('../dist/psvg'); 5 | 6 | if (!process.argv[2]) { 7 | console.log('usage: psvg input.psvg > output.svg'); 8 | process.exit(); 9 | } 10 | 11 | console.log(compilePSVG(fs.readFileSync(process.argv[2]).toString())); 12 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import { parsePSVG } from './parser'; 2 | import { transpilePSVG } from './transpiler'; 3 | 4 | export function evalPSVG(js: string): string { 5 | return Function(`"use strict";${js};return __out;`)(); 6 | } 7 | 8 | export function compilePSVG(psvg: string): string { 9 | let prgm = parsePSVG(psvg); 10 | // console.dir(prgm,{depth:null}); 11 | let js = transpilePSVG(prgm); 12 | // console.log(js); 13 | return evalPSVG(js); 14 | } 15 | -------------------------------------------------------------------------------- /src/psvg.ts: -------------------------------------------------------------------------------- 1 | import { evalPSVG, compilePSVG } from './compiler'; 2 | 3 | export { parsePSVG } from './parser'; 4 | export { PSVGFunc, transpilePSVG } from './transpiler'; 5 | export { evalPSVG, compilePSVG }; 6 | 7 | if (typeof window !== 'undefined') { 8 | window.addEventListener('load', function () { 9 | const psvgs = document.getElementsByTagName('PSVG'); 10 | for (let i = 0; i < psvgs.length; i++) { 11 | psvgs[i].outerHTML = compilePSVG(psvgs[i].outerHTML); 12 | } 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /examples/helloworld.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 17 | Hello World! 18 | 19 | 20 | 21 | 22 | 25 | PSVG 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/sierpinski.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/tree.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/koch.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lingdong/psvg", 3 | "version": "0.0.2", 4 | "description": "Programmable Scalable Vector Graphics -- drawings that draw themselves", 5 | "main": "dist/psvg.js", 6 | "module": "dist/psvg.mjs", 7 | "types": "dist/psvg.d.ts", 8 | "unpkg": "dist/psvg.global.js", 9 | "jsdelivr": "dist/psvg.global.js", 10 | "bin": { 11 | "psvg": "bin/psvg.js" 12 | }, 13 | "files": [ 14 | "dist", 15 | "bin", 16 | "examples", 17 | "psvg.ts" 18 | ], 19 | "scripts": { 20 | "build": "rimraf dist && tsup src/psvg.ts --format esm,cjs,iife --dts --global-name PSVG", 21 | "prepublishOnly": "npm run build", 22 | "release": "npx bumpp --all --commit --tag && npm publish --access public && git push", 23 | "site": "node ./tools/make_site.js", 24 | "examples": "node ./tools/compile_examples.js", 25 | "format:check": "prettier --check './**/*.{js,ts}'", 26 | "format:write": "prettier --write './**/*.{js,ts}'" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/LingDong-/psvg.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/LingDong-/psvg/issues" 34 | }, 35 | "homepage": "https://github.com/LingDong-/psvg#readme", 36 | "devDependencies": { 37 | "@types/node": "^14.14.8", 38 | "prettier": "^2.2.0", 39 | "rimraf": "^3.0.2", 40 | "tsup": "^3.8.0", 41 | "typescript": "^4.0.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/pythagoras.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/textanim.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 18 | 23 | 28 | Draws itself! 29 | 30 | 31 | 32 | 33 | 36 | 41 | PSVG 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/hilbert.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /tools/compile_examples.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { compilePSVG } = require('../dist/psvg'); 4 | const examplePath = path.resolve(__dirname, '../examples'); 5 | 6 | const overwrite = process.argv[2]||false; 7 | 8 | const examples = fs.readdirSync(examplePath).filter((x) => x.endsWith('.psvg')); 9 | 10 | for (const example of examples) { 11 | console.log(`compiling ${example}...`); 12 | const filepath = path.join(examplePath, example); 13 | const outpath = filepath.replace(/\.psvg$/, '.svg'); 14 | if (fs.existsSync(outpath) && !overwrite) { 15 | console.log(`skipped.`); 16 | continue; 17 | } 18 | const psvg = fs.readFileSync(filepath, 'utf-8'); 19 | const svg = compilePSVG(psvg); 20 | 21 | fs.writeFileSync(filepath.replace(/\.psvg$/, '.svg'), svg, 'utf-8'); 22 | } 23 | 24 | 25 | var md="# Gallery\nPSVG `examples/` showcase! You can also fiddle with these examples on the online [Playground](https://psvg.netlify.app/). \n\n"; 26 | for (const example of examples) { 27 | if (example.includes("helloworld")){ 28 | continue; // redundent with textanim and not as cool 29 | } 30 | const svg = example.replace(/\.psvg$/,'.svg'); 31 | const filepath = path.join(examplePath, example); 32 | const com = fs.readFileSync(filepath).toString().split("-->").map(x=>x.trim()).filter(x=>x.startsWith('\n')+' -->\n'; 33 | md+=`\n## [${example}](${example}) → [${svg}](${svg})\n\n`; 34 | md+=`![${svg}](${svg})\n\n`; 35 | md+="```xml\n"+com+"```\n\n"; 36 | } 37 | fs.writeFileSync(path.join(examplePath,"README.md"),md,'utf-8'); -------------------------------------------------------------------------------- /examples/schotter.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 32 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | COMPUTERGRAPFIK MIT SIMENS-SYSTEM 4004 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/shapemorph.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | -------------------------------------------------------------------------------- /examples/poisson.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/sphere.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 85 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /examples/terrain.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /examples/pulsar.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { PSVGElement } from './element'; 2 | 3 | export function parsePSVG(str: string): PSVGElement[] { 4 | str = str.replace(//gm, ''); 5 | let i: number = 0; 6 | const elts: PSVGElement[] = []; 7 | while (i <= str.length) { 8 | if (str[i] == '<') { 9 | let j = i + 1; 10 | let bodyStart = -1; 11 | let bodyEnd = -1; 12 | let quote = false; 13 | let lvl = 0; 14 | 15 | const getTagName = (open: string) => open.trim().split(' ')[0].trimEnd(); 16 | 17 | const getAttributes = (open: string) => { 18 | // oneliner doesn't work for safari: 19 | // return Object['fromEntries'](Array['from'](open.split(" ").slice(1).join(" ")['matchAll'](/(^| )([^ ]+?)\="([^"]*)"/g)).map((x:string)=>x.slice(2))); 20 | 21 | // stupid polyfill for safari: 22 | const attrsStr = open.split(' ').slice(1).join(' '); 23 | 24 | const matchAll = attrsStr.matchAll 25 | ? (re: RegExp) => attrsStr.matchAll(re) 26 | : (re: RegExp) => { 27 | const ms: RegExpMatchArray[] = []; 28 | while (1) { 29 | const m = re.exec(attrsStr); 30 | if (m) ms.push(m); 31 | else break; 32 | } 33 | return ms; 34 | }; 35 | 36 | const fromEntries = 37 | Object.fromEntries || 38 | ((a: any) => { 39 | const o = {}; 40 | a.map(([key, value]) => (o[key] = value)); 41 | return o; 42 | }); 43 | 44 | // @ts-ignore 45 | // prettier-ignore 46 | return fromEntries(Array['from'](matchAll(/(^| )([^ ]+?)\ *= *"([^"]*)"/g)).map((x: string) => x.slice(2))); 47 | }; 48 | 49 | const parseNormalTag = (): void => { 50 | const open = str.slice(i + 1, bodyStart - 1); 51 | const body = str.slice(bodyStart, bodyEnd); 52 | const elt: PSVGElement = { 53 | tagName: getTagName(open), 54 | attributes: getAttributes(open), 55 | children: parsePSVG(body), 56 | innerHTML: body, 57 | }; 58 | elts.push(elt); 59 | }; 60 | 61 | const parseSelfClosingTag = (): void => { 62 | const open = str.slice(i + 1, j); 63 | const elt: PSVGElement = { 64 | tagName: getTagName(open), 65 | attributes: getAttributes(open), 66 | children: [], 67 | innerHTML: '', 68 | }; 69 | elts.push(elt); 70 | }; 71 | 72 | while (j <= str.length) { 73 | if (str[j] == '\\') { 74 | j++; 75 | } 76 | if (str[j] == '"') { 77 | quote = !quote; 78 | } 79 | if (!quote) { 80 | if (str[j] == '>' && lvl == 0 && bodyStart == -1) { 81 | bodyStart = j + 1; 82 | } 83 | 84 | if (str[j] == '<') { 85 | if (str[j + 1] == '/') { 86 | lvl--; 87 | if (lvl == -1) { 88 | bodyEnd = j; 89 | } 90 | while (str[j] != '>') { 91 | j++; 92 | } 93 | if (lvl == -1) { 94 | parseNormalTag(); 95 | i = j; 96 | break; 97 | } 98 | } else { 99 | lvl++; 100 | } 101 | } else if (str[j] == '/' && str[j + 1] == '>') { 102 | lvl--; 103 | if (lvl == -1) { 104 | parseSelfClosingTag(); 105 | i = j; 106 | break; 107 | } 108 | } 109 | } 110 | j++; 111 | } 112 | } 113 | i++; 114 | } 115 | return elts; 116 | } 117 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Gallery 2 | PSVG `examples/` showcase! You can also fiddle with these examples on the online [Playground](https://psvg.netlify.app/). 3 | 4 | 5 | ## [hilbert.psvg](hilbert.psvg) → [hilbert.svg](hilbert.svg) 6 | 7 | ![hilbert.svg](hilbert.svg) 8 | 9 | ```xml 10 | 11 | 12 | 14 | ``` 15 | 16 | 17 | ## [koch.psvg](koch.psvg) → [koch.svg](koch.svg) 18 | 19 | ![koch.svg](koch.svg) 20 | 21 | ```xml 22 | 23 | 24 | ``` 25 | 26 | 27 | ## [poisson.psvg](poisson.psvg) → [poisson.svg](poisson.svg) 28 | 29 | ![poisson.svg](poisson.svg) 30 | 31 | ```xml 32 | 33 | 34 | 35 | 36 | ``` 37 | 38 | 39 | ## [pulsar.psvg](pulsar.psvg) → [pulsar.svg](pulsar.svg) 40 | 41 | ![pulsar.svg](pulsar.svg) 42 | 43 | ```xml 44 | 45 | 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | 53 | ## [pythagoras.psvg](pythagoras.psvg) → [pythagoras.svg](pythagoras.svg) 54 | 55 | ![pythagoras.svg](pythagoras.svg) 56 | 57 | ```xml 58 | 59 | 60 | 61 | ``` 62 | 63 | 64 | ## [schotter.psvg](schotter.psvg) → [schotter.svg](schotter.svg) 65 | 66 | ![schotter.svg](schotter.svg) 67 | 68 | ```xml 69 | 70 | 71 | 72 | ``` 73 | 74 | 75 | ## [shapemorph.psvg](shapemorph.psvg) → [shapemorph.svg](shapemorph.svg) 76 | 77 | ![shapemorph.svg](shapemorph.svg) 78 | 79 | ```xml 80 | 81 | 82 | ``` 83 | 84 | 85 | ## [sierpinski.psvg](sierpinski.psvg) → [sierpinski.svg](sierpinski.svg) 86 | 87 | ![sierpinski.svg](sierpinski.svg) 88 | 89 | ```xml 90 | 91 | 92 | ``` 93 | 94 | 95 | ## [sphere.psvg](sphere.psvg) → [sphere.svg](sphere.svg) 96 | 97 | ![sphere.svg](sphere.svg) 98 | 99 | ```xml 100 | 101 | 102 | ``` 103 | 104 | 105 | ## [terrain.psvg](terrain.psvg) → [terrain.svg](terrain.svg) 106 | 107 | ![terrain.svg](terrain.svg) 108 | 109 | ```xml 110 | 111 | 112 | 113 | ``` 114 | 115 | 116 | ## [textanim.psvg](textanim.psvg) → [textanim.svg](textanim.svg) 117 | 118 | ![textanim.svg](textanim.svg) 119 | 120 | ```xml 121 | 122 | 123 | 124 | ``` 125 | 126 | 127 | ## [tree.psvg](tree.psvg) → [tree.svg](tree.svg) 128 | 129 | ![tree.svg](tree.svg) 130 | 131 | ```xml 132 | 133 | 134 | ``` 135 | 136 | 137 | ## [turing.psvg](turing.psvg) → [turing.svg](turing.svg) 138 | 139 | ![turing.svg](turing.svg) 140 | 141 | ```xml 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ``` 153 | 154 | -------------------------------------------------------------------------------- /examples/hilbert.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/turing.psvg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 126 | 127 | 128 | 129 | 130 | 131 | 138 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /tools/make_site.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const examplePath = path.resolve(__dirname, '../examples'); 4 | const outPath = path.resolve(__dirname, '../site/index.html'); 5 | 6 | var exampleNames = fs 7 | .readdirSync(examplePath) 8 | .filter((x) => x.endsWith('.psvg')); 9 | exampleNames.sort(); 10 | var exampleTexts = exampleNames.map((x) => 11 | fs.readFileSync(path.join(examplePath, x)).toString() 12 | ); 13 | var examples = {}; 14 | exampleNames.map((x, i) => (examples[x] = exampleTexts[i])); 15 | 16 | let themeName = 'xq-light'; 17 | 18 | const html = String.raw; 19 | 20 | let siteHTML = html` 21 | 22 | 23 | 24 | 25 | 26 | 27 | 102 | 103 |

PSVG

104 | Programmable SVG format 105 |
106 |
107 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 |
124 | 125 | 130 | 136 | `; 137 | 138 | function main() { 139 | function debounce(func, wait) { 140 | var timeout; 141 | return function () { 142 | var context = this, 143 | args = arguments; 144 | var later = function () { 145 | timeout = null; 146 | func.apply(context, args); 147 | }; 148 | clearTimeout(timeout); 149 | timeout = setTimeout(later, wait); 150 | }; 151 | } 152 | 153 | const select = document.getElementById('select'); 154 | const output = document.getElementById('output'); 155 | const run = document.getElementById('compile'); 156 | const auto = document.getElementById('auto-compile'); 157 | 158 | const setExample = (name) => { 159 | select.value = name; 160 | CM.setValue(examples[name]); 161 | compile(); 162 | }; 163 | 164 | const compile = () => { 165 | output.innerHTML = PSVG.compilePSVG(CM.getValue()); 166 | }; 167 | const debouncedCompile = debounce(compile, 800); 168 | 169 | var CM = CodeMirror(document.getElementById('input'), { 170 | lineNumbers: true, 171 | matchBrackets: true, 172 | theme: themeName, 173 | mode: 'xml', 174 | extraKeys: { 175 | 'Ctrl-/': 'toggleComment', 176 | 'Cmd-/': 'toggleComment', 177 | 'Ctrl-Enter': compile, 178 | 'Cmd-Enter': compile, 179 | }, 180 | }); 181 | 182 | CM.on('change', (_, e) => { 183 | if (auto.checked && e.origin !== 'setValue') { 184 | debouncedCompile(); 185 | } 186 | }); 187 | 188 | setExample('koch.psvg'); 189 | select.onchange = () => setExample(select.value); 190 | run.onclick = compile; 191 | } 192 | 193 | fs.writeFileSync(outPath, siteHTML); 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](examples/textanim.svg)](examples/textanim.psvg) 2 | 3 | # PSVG - Programmable SVG 4 | 5 | **[Doc](QUICKSTART.md) | [Playground](https://psvg.netlify.app/) | [Examples](examples/) | [NPM](https://www.npmjs.com/package/@lingdong/psvg)** 6 | 7 | PSVG is an extension of the SVG (Scalable Vector Graphics) format that introduces programming language features like functions, control flows, and variables -- Instead of writing a program that draws a picture, write a picture that draws itself! 8 | 9 | PSVG is compliant with XML and HTML specs, so it can be easily embedded in a webpage or edited with an XML editor. 10 | 11 | This repo contains a [PSVG→SVG complier](psvg.ts) that transforms PSVG files to just regular SVG's. It can also automatically render all PSVG's on an HTML page when included as a ` 146 | ``` 147 | 148 | By including the script, all the `` elements on the webpage will be compiled to `` when the page loads. Again, don't include PSVG files that you don't trust. 149 | 150 | ### As a library 151 | 152 | Install locally in your project via npm 153 | 154 | ``` 155 | npm i @lingdong/psvg 156 | ``` 157 | 158 | ```js 159 | import { compilePSVG } from "@lingdong/psvg" 160 | 161 | console.log(compilePSVG("...")) 162 | ``` 163 | 164 | or 165 | 166 | ```js 167 | const { compilePSVG } = require("@lingdong/psvg") 168 | 169 | console.log(compilePSVG("...")) 170 | ``` 171 | 172 | Additionally, `parsePSVG()` `transpilePSVG()` and `evalPSVG()` which are individual steps of compilation are also exported. 173 | 174 | In browsers, functions are exported under the global variable `PSVG`. 175 | 176 | **Check out [QUICKSTART.md](QUICKSTART.md) for a quick introduction to the PSVG language.** 177 | 178 | ## Editor Support 179 | 180 | Syntax highlighting and auto-completion can be configured for editors by: 181 | 182 | ### VS Code 183 | 184 | Add the following lines to your `settting.json`. [details](https://code.visualstudio.com/docs/languages/overview#_can-i-map-additional-file-extensions-to-a-language) 185 | 186 | ```json 187 | "files.associations": { 188 | "*.psvg": "xml" 189 | } 190 | ``` 191 | 192 | ### GitHub 193 | 194 | To get highlighting for PSVG files in your repositories on GitHub, create `.gitattributes` file at the root of your repo with the following content. [details](https://github.com/github/linguist#using-gitattributes) 195 | 196 | ```ini 197 | *.psvg linguist-language=SVG 198 | ``` 199 | 200 | ### Other editors 201 | 202 | Since PSVG is compliant with XML and HTML specs, you can always alias your language id to XML or SVG via the corresponding config on your editor. 203 | -------------------------------------------------------------------------------- /QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # PSVG Quickstart Guide 2 | 3 | PSVG is a very simple language that tries keep true to the "look and feel" of the SVG format from which it's derived. It's duck-typed; In fact, it does not have typing at all: As XML attributes, all values are just strings. As you apply operations to the values, they're interpreted to appropriate types: numbers, strings, or arrays. Terrible as that might sound (to proponents of strong type systems and fast languages, that is), it's so designed to play well with the rest of the SVG, while remaining expressive and concise. 4 | 5 | ## A PSVG file 6 | 7 | The structure of a PSVG file is very much like that of an SVG file. Instead of wrapping everything with an `` tag, wrap everything with a `` tag: 8 | 9 | ```xml 10 | 11 | ... 12 | 13 | ... 14 | 15 | ``` 16 | 17 | As shown above, you can also assign a background color(or image/gradiant etc) for the file. `xmlns` can be ommitted. Any other attributes (e.g. `viewBox`) you specify will be also included in the `svg` tag of the compiled output. 18 | 19 | ## Variables 20 | 21 | Varaibles are declared with `var` tag. 22 | 23 | ```xml 24 | 25 | ``` 26 | 27 | Multiple variables can be declared in the same tag: 28 | 29 | ```xml 30 | 31 | ``` 32 | 33 | Varaibales can be used anywhere to parameterize SVG drawings. 34 | 35 | To do so, put them inside squiggly braces: 36 | 37 | ```xml 38 | 39 | 40 | ``` 41 | 42 | The squiggly braces basically means to "evaluate" what's inside, instead of taking the whole thing as a string verbatim. If you're using any variables, or doing math, you'll need squiggly braces to wrap them. 43 | 44 | For example, the first `rect` below will be red, while the second will actually be green. (Just a demo! It'd be really confusing if you do it IRL). 45 | 46 | ```xml 47 | 48 | 49 | 50 | ``` 51 | 52 | In addition to use a variable for an entire field, variables can also be interpolated, sort of like JavaScript's `${}`: 53 | 54 | ```xml 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | This is also handy for `path` `d`ata or `polyline` `points`: 62 | 63 | 64 | ```xml 65 | 66 | 67 | ``` 68 | 69 | To modify a variable, use `asgn` (or `assign`, if you dislike the abbreviation): 70 | 71 | ```xml 72 | 73 | 74 | 75 | ``` 76 | 77 | If you're curious why `set` is not used as the intuitive assignment keyword, it's because SVG spec took it already. 78 | 79 | ## Control Flow 80 | 81 | Simple if statements: 82 | 83 | ```xml 84 | 85 | 86 | 87 | ``` 88 | 89 | There's also a shorthand for `if not`: 90 | 91 | ```xml 92 | 93 | 94 | 95 | ``` 96 | 97 | If you need `else if` or `else`, the syntax is a little bit different, and might remind you of `switch` statements, or that `cond` in Lisp: 98 | 99 | ```xml 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | ``` 112 | 113 | After reaching the first `cond` that evaluates to true, the block is executed and the rest are skipped. The last `cond` does not need to have a test, and will be executed if all the ones before are skipped. 114 | 115 | For loops: 116 | 117 | ```xml 118 | 119 | 120 | 121 | ``` 122 | 123 | Similar to ``, the `true` attribute can also be replaced with say `false="{i>=10}"`. The `step` can be ommited. 124 | 125 | While loops: 126 | 127 | ```xml 128 | 129 | 130 | 131 | ``` 132 | 133 | ## Functions 134 | 135 | To define a function called `foo`, make a tag called `def-foo` and put the implementation inside. 136 | 137 | ```xml 138 | 139 | 140 | 141 | ``` 142 | 143 | The `0`s in `x="0" y="0"` are default values for parameters, you can also put empty string `x="" y=""` if you don't need them. But the `x` and `y` need to be there to declare the parameters. 144 | 145 | To invoke a function, use it as if it's an SVG command: 146 | 147 | ```xml 148 | 149 | ``` 150 | 151 | What if the function needs to return a value, to be used in some other calculations? 152 | 153 | ```xml 154 | 155 | 156 | 157 | ``` 158 | 159 | The conventional `fun(arg,arg,arg)` syntax from other programming languages can also be used to call a function: 160 | 161 | ```xml 162 | 163 | ``` 164 | 165 | ## Math 166 | 167 | Math operators work just like you might expect. You have `+` `-` `*` `/` etc. They mirror JavaScript's math behavior. You also get more advanced Math functions, like those found in JavaScript's `Math` object as builtin functions. Builtin functions in PSVG are all captalized, e.g. 168 | 169 | ```xml 170 | 171 | ``` 172 | 173 | Beware that some XML tools don't like `<` `>` and `&` even when they're inside strings, so you might need to write `x && y` instead of `x && y`, or `x < y` instead of `x < y`. 174 | 175 | ## Arrays 176 | 177 | Arrays in PSVG, as they're in SVG, are just strings with space or comma delimited values. 178 | 179 | ```xml 180 | 181 | 182 | ``` 183 | 184 | A collection of builtins are provided to operate on arrays. These builtins are functional, meaning that they do not modify the input, and instead return new arrays. 185 | 186 | - `CAT(arr1,arr2,val1,arr3,...)` concatenates arrays as well as values. 187 | - `NTH(arr,n)` returns the nth element of the array. 188 | - `COUNT(arr)` returns the length of the array. 189 | - `UPDATE(arr,i,val)` returns a new array with the `i`th element replaced by `val`. 190 | - `TAKE(arr,n)` and `DROP(arr,n)` can be used to slice the array. 191 | - `MAP(arr,f)` and `FILTER(arr,f)` are the functional functions. 192 | - `REV(arr)` reverses the array. 193 | - `FILL(val,n)` generates an array of length `n` filled with `val`. 194 | 195 | This makes it super easy to put an array of points in say a `path` or `polyline` element. 196 | 197 | ```xml 198 | 199 | 200 | 201 | ``` 202 | 203 | ```xml 204 | 205 | 206 | 207 | ``` 208 | 209 | ## Misc 210 | 211 | In addition to math and array functions listed before, there're also a couple other builtins for convenience: 212 | 213 | - `WIDTH` and `HEIGHT` are dimensions of current image. 214 | - `MAPVAL(val,istart,istop,ostart,ostop)` maps a value to another range, like Processing/p5.js `map()` 215 | - `CLAMP(val,lo,hi)` clamps a value within specified range. 216 | - `RANDOM()` gives a random number between 0 and 1. 217 | - `LERP(a,b,t)` linearly interpolates between `a` and `b` by parameter `t` 218 | 219 | 220 | That's it! You now know all the basics of the PSVG language. Feel free to head over to `examples/` folder to learn by examples. 221 | 222 | -------------------------------------------------------------------------------- /src/transpiler.ts: -------------------------------------------------------------------------------- 1 | import { PSVGElement } from './element'; 2 | 3 | export interface PSVGFunc { 4 | name: string; 5 | args: string[]; 6 | } 7 | 8 | export function transpilePSVG(prgm: PSVGElement[]): string { 9 | let funcs: Record = {}; 10 | function __val(x: string): any { 11 | if ( 12 | new RegExp( 13 | /^[+-]?(\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)$/g 14 | ).test(x) 15 | ) { 16 | return parseFloat(x); 17 | } 18 | if (x == `true` || x == `false`) { 19 | return x == `true`; 20 | } 21 | 22 | let hascm = x['includes'](','); 23 | if (hascm) { 24 | x = x.replace(/, */g, ','); 25 | let hasws = x['includes'](' '); 26 | var y = __tolist(x); 27 | if (!hasws) { 28 | y['allCommas'] = true; 29 | } 30 | return y; 31 | } 32 | if (x['includes'](' ')) { 33 | return __tolist(x); 34 | } 35 | return x; 36 | } 37 | function __makelist(x: any[]): any[] { 38 | x.toString = function () { 39 | return x.join(x['allCommas'] ? ',' : ' '); 40 | }; 41 | return x; 42 | } 43 | function __tolist(s: string): any[] { 44 | return __makelist( 45 | s 46 | .replace(/,/g, ' ') 47 | .split(' ') 48 | .filter((x) => x.length) 49 | .map(__val) 50 | ); 51 | } 52 | 53 | let builtins: Record = { 54 | NTH: function (x: any[] | string, i: number): any { 55 | if (typeof x == 'string') { 56 | x = __tolist(x); 57 | } 58 | return x[i]; 59 | }.toString(), 60 | TAKE: function (x: any[] | string, n: number): any[] { 61 | if (typeof x == 'string') { 62 | x = __tolist(x); 63 | } 64 | return __makelist(x.slice(0, n)); 65 | }.toString(), 66 | DROP: function (x: any[] | string, n: number): any[] { 67 | if (typeof x == 'string') { 68 | x = __tolist(x); 69 | } 70 | return __makelist(x.slice(n)); 71 | }.toString(), 72 | UPDATE: function (x: any[] | string, i: number, y: any): any[] { 73 | if (typeof x == 'string') { 74 | x = __tolist(x); 75 | } 76 | let z = x.slice(); 77 | z[i] = y; 78 | return __makelist(z); 79 | }.toString(), 80 | MAP: function (x: any[] | string, f: (x: any) => any): any[] { 81 | if (typeof x == 'string') { 82 | x = __tolist(x); 83 | } 84 | return __makelist(x.map((y) => f(__val(y)))); 85 | }.toString(), 86 | FILTER: function (x: any[] | string, f: (x: any) => any): any[] { 87 | if (typeof x == 'string') { 88 | x = __tolist(x); 89 | } 90 | return __makelist(x.filter((y) => f(__val(y)))); 91 | }.toString(), 92 | COUNT: function (x: any[] | string): number { 93 | if (typeof x == 'string') { 94 | x = __tolist(x); 95 | } 96 | return x.length; 97 | }.toString(), 98 | CAT: function (...args: any): any[] { 99 | return __makelist( 100 | [].concat( 101 | ...args 102 | .filter((y: any) => y.toString().length) 103 | .map((x: any) => (typeof x == 'string' ? __tolist(x) : x)) 104 | ) 105 | ); 106 | }.toString(), 107 | REV: function (x: any[] | string): any[] { 108 | if (typeof x == 'string') { 109 | x = __tolist(x); 110 | } 111 | return __makelist(x.slice().reverse()); 112 | }.toString(), 113 | FILL: function (x: any, n: number): any[] { 114 | return __makelist(new Array(n)['fill'](x)); 115 | }.toString(), 116 | 117 | LERP: function (x: number, y: number, t: number): number { 118 | return x * (1 - t) + y * t; 119 | }.toString(), 120 | CLAMP: function (x: number, lo: number, hi: number): number { 121 | [lo, hi] = [Math.min(lo, hi), Math.max(lo, hi)]; 122 | return Math.min(Math.max(x, lo), hi); 123 | }.toString(), 124 | MAPVAL: function ( 125 | x: number, 126 | istart: number, 127 | istop: number, 128 | ostart: number, 129 | ostop: number 130 | ): number { 131 | return ostart + (ostop - ostart) * ((x - istart) / (istop - istart)); 132 | }.toString(), 133 | }; 134 | Object.getOwnPropertyNames(Math).map( 135 | (x) => (builtins[x.toUpperCase()] = `Math["${x}"]`) 136 | ); 137 | 138 | return ( 139 | __val.toString() + 140 | ';' + 141 | __tolist.toString() + 142 | ';' + 143 | __makelist.toString() + 144 | ';' + 145 | Object['entries'](builtins) 146 | .map((x: string[]) => 'const ' + x[0] + '=' + x[1]) 147 | .join(';') + 148 | ";let __out='';" + 149 | transpilePSVGList(prgm) + 150 | ';' + 151 | '__out;' 152 | ); 153 | 154 | function transpilePSVGList(prgm: PSVGElement[]): string { 155 | let out: string = ''; 156 | let groups = 0; 157 | 158 | function transpileValue(x: string): string { 159 | x = x.trim(); 160 | x = x 161 | .replace(/>/g, '>') 162 | .replace(/</g, '<') 163 | .replace(/&/g, '&') 164 | .replace(/"/g, '"'); 165 | if ( 166 | x['startsWith']('{') && 167 | x['endsWith']('}') && 168 | (x.match(/{|}/g) || []).length == 2 169 | ) { 170 | return x.slice(1, -1); 171 | } 172 | if ( 173 | new RegExp( 174 | /^[+-]?(\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)$/g 175 | ).test(x) 176 | ) { 177 | return x; 178 | } 179 | // crappy safari doesn't support lookbehind yet 180 | // return '__val(`'+x.replace(/(?`;'; 194 | out += `const WIDTH=${w};const HEIGHT=${h};`; 195 | if (prgm[i].attributes.background) { 196 | out += `__out+=\`\`;`; 199 | } 200 | out += transpilePSVGList(prgm[i].children); 201 | out += `__out+='';`; 202 | } else if (prgm[i].tagName.toUpperCase()['startsWith']('DEF-')) { 203 | let name = prgm[i].tagName.split('-').slice(1).join('-'); 204 | funcs[name] = { name, args: Object.keys(prgm[i].attributes) }; 205 | out += `function ${name}(${Object['entries'](prgm[i].attributes).map( 206 | (x: string[]) => x[0] + '=' + transpileValue(x[1]) 207 | )}){`; 208 | out += transpilePSVGList(prgm[i].children); 209 | out += `};`; 210 | } else if (prgm[i].tagName.toUpperCase() == 'IF') { 211 | if (Object.keys(prgm[i].attributes).length == 0) { 212 | for (var j = 0; j < prgm[i].children.length; j++) { 213 | if (j != 0) { 214 | out += 'else '; 215 | } 216 | if (prgm[i].children[j].attributes.true) { 217 | out += `if (${transpileValue( 218 | prgm[i].children[j].attributes.true 219 | )})`; 220 | } else if (prgm[i].children[j].attributes.false) { 221 | out += `if (!(${transpileValue( 222 | prgm[i].children[j].attributes.false 223 | )}))`; 224 | } 225 | out += '{'; 226 | out += transpilePSVGList(prgm[i].children[j].children); 227 | out += '}'; 228 | } 229 | out += ';'; 230 | } else { 231 | if (prgm[i].attributes.true) { 232 | out += `if (${transpileValue(prgm[i].attributes.true)}){`; 233 | } else if (prgm[i].attributes.false) { 234 | out += `if (!(${transpileValue(prgm[i].attributes.false)})){`; 235 | } 236 | out += transpilePSVGList(prgm[i].children); 237 | out += '};'; 238 | } 239 | } else if (prgm[i].tagName.toUpperCase() == 'PUSH') { 240 | out += transpilePSVGList(prgm[i].children); 241 | } else if (prgm[i].tagName.toUpperCase() == 'TRANSLATE') { 242 | out += `__out+=\`\`;`; 245 | groups++; 246 | } else if (prgm[i].tagName.toUpperCase() == 'ROTATE') { 247 | if (prgm[i].attributes.rad) { 248 | out += `__out+=\`\`;`; 251 | } else { 252 | out += `__out+=\`\`;`; 255 | } 256 | groups++; 257 | } else if (prgm[i].tagName.toUpperCase() == 'STROKE') { 258 | out += '__out+=`\`;`; 314 | groups++; 315 | } else if (prgm[i].tagName.toUpperCase() == 'VAR') { 316 | for (var k in prgm[i].attributes) { 317 | out += `let ${k}=${transpileValue(prgm[i].attributes[k])};`; 318 | } 319 | } else if ( 320 | prgm[i].tagName.toUpperCase() == 'ASGN' || 321 | prgm[i].tagName.toUpperCase() == 'ASSIGN' 322 | ) { 323 | for (var k in prgm[i].attributes) { 324 | out += `${k}=${transpileValue(prgm[i].attributes[k])};`; 325 | } 326 | } else if (prgm[i].tagName.toUpperCase() == 'RETURN') { 327 | for (var j = 0; j < groups; j++) { 328 | out += "__out+='';"; 329 | } 330 | if (prgm[i].attributes.value) { 331 | out += `return ${transpileValue(prgm[i].attributes.value)};`; 332 | } else { 333 | out += `return;`; 334 | } 335 | } else if (prgm[i].tagName.toUpperCase() == 'FOR') { 336 | let name: string; 337 | for (var k in prgm[i].attributes) { 338 | if (!['true', 'false', 'step']['includes'](k)) { 339 | name = k; 340 | break; 341 | } 342 | } 343 | let step: string = prgm[i].attributes['step'] ?? '1'; 344 | 345 | out += `for (let ${name}=${transpileValue(prgm[i].attributes[name])};`; 346 | if (prgm[i].attributes.true) { 347 | out += `${transpileValue(prgm[i].attributes.true)};`; 348 | } else { 349 | out += `!(${transpileValue(prgm[i].attributes.false)});`; 350 | } 351 | out += `${name}+=${transpileValue(step)}){`; 352 | out += transpilePSVGList(prgm[i].children); 353 | out += '};'; 354 | } else if (prgm[i].tagName.toUpperCase() == 'WHILE') { 355 | if (prgm[i].attributes.true) { 356 | out += `while (${transpileValue(prgm[i].attributes.true)}){`; 357 | } else { 358 | out += `while (!(${transpileValue(prgm[i].attributes.false)})){`; 359 | } 360 | out += transpilePSVGList(prgm[i].children); 361 | out += '};'; 362 | } else if (prgm[i].tagName in funcs) { 363 | out += prgm[i].tagName + '('; 364 | let args = funcs[prgm[i].tagName].args; 365 | for (var j = 0; j < args.length; j++) { 366 | let v = prgm[i].attributes[args[j]]; 367 | out += v === undefined ? 'undefined' : transpileValue(v); 368 | out += ','; 369 | } 370 | out += ');'; 371 | } else { 372 | out += '__out+=`<' + prgm[i].tagName + ' '; 373 | for (var k in prgm[i].attributes) { 374 | out += `${k}="\${${transpileValue(prgm[i].attributes[k])}}" `; 375 | } 376 | let needInner = ['TEXT', 'STYLE']['includes']( 377 | prgm[i].tagName.toUpperCase() 378 | ); 379 | if (prgm[i].children.length || needInner) { 380 | out += '>`;'; 381 | out += transpilePSVGList(prgm[i].children); 382 | if (needInner) { 383 | out += 384 | '__out+=`' + 385 | prgm[i].innerHTML.replace(/`/g, '/`').replace(/<.*?>/g, '') + 386 | '`;'; 387 | } 388 | out += "__out+='';"; 389 | } else { 390 | out += '/>`;'; 391 | } 392 | } 393 | } 394 | for (var i = 0; i < groups; i++) { 395 | out += "__out+='';"; 396 | } 397 | return out; 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /examples/helloworld.svg: -------------------------------------------------------------------------------- 1 | 2 | Hello World! 3 | 4 | Hello World! 5 | 6 | Hello World! 7 | 8 | Hello World! 9 | 10 | Hello World! 11 | 12 | Hello World! 13 | 14 | Hello World! 15 | 16 | Hello World! 17 | 18 | Hello World! 19 | 20 | Hello World! 21 | 22 | Hello World! 23 | 24 | Hello World! 25 | 26 | Hello World! 27 | 28 | Hello World! 29 | 30 | Hello World! 31 | 32 | Hello World! 33 | 34 | Hello World! 35 | 36 | Hello World! 37 | 38 | Hello World! 39 | 40 | Hello World! 41 | 42 | Hello World! 43 | 44 | Hello World! 45 | 46 | Hello World! 47 | 48 | Hello World! 49 | 50 | Hello World! 51 | 52 | Hello World! 53 | 54 | Hello World! 55 | 56 | Hello World! 57 | 58 | Hello World! 59 | 60 | Hello World! 61 | 62 | Hello World! 63 | 64 | Hello World! 65 | 66 | Hello World! 67 | 68 | Hello World! 69 | 70 | Hello World! 71 | 72 | Hello World! 73 | 74 | Hello World! 75 | 76 | Hello World! 77 | 78 | Hello World! 79 | 80 | Hello World! 81 | 82 | Hello World! 83 | 84 | Hello World! 85 | 86 | Hello World! 87 | 88 | Hello World! 89 | 90 | Hello World! 91 | 92 | Hello World! 93 | 94 | Hello World! 95 | 96 | Hello World! 97 | 98 | Hello World! 99 | 100 | Hello World! 101 | 102 | Hello World! 103 | 104 | Hello World! 105 | 106 | Hello World! 107 | 108 | Hello World! 109 | 110 | Hello World! 111 | 112 | Hello World! 113 | 114 | Hello World! 115 | 116 | Hello World! 117 | 118 | Hello World! 119 | 120 | Hello World! 121 | 122 | Hello World! 123 | 124 | Hello World! 125 | 126 | Hello World! 127 | 128 | Hello World! 129 | 130 | Hello World! 131 | 132 | Hello World! 133 | 134 | Hello World! 135 | 136 | Hello World! 137 | 138 | Hello World! 139 | 140 | Hello World! 141 | 142 | Hello World! 143 | 144 | Hello World! 145 | 146 | Hello World! 147 | 148 | Hello World! 149 | 150 | Hello World! 151 | 152 | PSVG 153 | 154 | PSVG 155 | 156 | PSVG 157 | 158 | PSVG 159 | 160 | PSVG 161 | 162 | PSVG 163 | 164 | PSVG 165 | 166 | PSVG 167 | 168 | PSVG 169 | 170 | PSVG 171 | 172 | PSVG 173 | 174 | PSVG 175 | 176 | PSVG 177 | 178 | PSVG 179 | 180 | PSVG 181 | 182 | PSVG 183 | 184 | PSVG 185 | 186 | PSVG 187 | 188 | PSVG 189 | 190 | PSVG 191 | 192 | PSVG 193 | 194 | PSVG 195 | 196 | PSVG 197 | 198 | PSVG 199 | 200 | PSVG 201 | 202 | PSVG 203 | 204 | PSVG 205 | 206 | PSVG 207 | 208 | PSVG 209 | 210 | PSVG 211 | 212 | PSVG 213 | 214 | PSVG 215 | 216 | PSVG 217 | 218 | PSVG 219 | 220 | PSVG 221 | 222 | PSVG 223 | 224 | PSVG 225 | 226 | PSVG 227 | 228 | PSVG 229 | 230 | PSVG 231 | 232 | PSVG 233 | 234 | PSVG 235 | 236 | PSVG 237 | 238 | PSVG 239 | 240 | PSVG 241 | 242 | PSVG 243 | 244 | PSVG 245 | 246 | PSVG 247 | 248 | PSVG 249 | 250 | PSVG 251 | 252 | PSVG 253 | 254 | PSVG 255 | 256 | PSVG 257 | 258 | PSVG 259 | 260 | PSVG 261 | 262 | PSVG 263 | 264 | PSVG 265 | 266 | PSVG 267 | 268 | PSVG 269 | 270 | PSVG 271 | 272 | PSVG 273 | 274 | PSVG 275 | 276 | PSVG 277 | 278 | PSVG 279 | 280 | PSVG 281 | -------------------------------------------------------------------------------- /examples/shapemorph.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/turing.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------