├── CNAME ├── .gitignore ├── .vscode └── settings.json ├── .prettierrc.json ├── src ├── index.css ├── index.js ├── piet.js ├── index.html ├── piet-run.js └── piet-ui.js ├── README.md ├── .eslintrc.json ├── LICENSE ├── package.json ├── prod.config.js ├── prod-debug.config.js ├── webpack.config.js └── functions └── api.svg.js /CNAME: -------------------------------------------------------------------------------- 1 | piet.bubbler.space -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | dist/** -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": false 4 | } -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "trailingComma": "all", 7 | "printWidth": 80, 8 | "arrowParens": "avoid", 9 | "endOfLine": "auto" 10 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | svg { 2 | margin: none; 3 | padding: 2px; 4 | border: 2px solid #ccc; 5 | } 6 | 7 | #svg-palette { 8 | width: 368px; 9 | height: 128px; 10 | } 11 | 12 | textarea { 13 | resize: none; 14 | font-family: monospace; 15 | white-space: pre-wrap; 16 | overflow: hidden; 17 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as bootstrap from 'bootstrap'; 2 | import $ from 'jquery'; 3 | import Snap from 'snapsvg'; 4 | import PietUI from './piet-ui.js'; 5 | import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import 'bootstrap-icons/font/bootstrap-icons.css'; 7 | import './index.css'; 8 | 9 | function init() { 10 | const ui = new PietUI(); 11 | } 12 | 13 | window.onload = () => { 14 | init(); 15 | console.log('loaded'); 16 | }; 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A browser-based Piet editor/interpreter 2 | 3 | ## Features 4 | 5 | * An interpreter that fully conforms to the [Piet specification](https://www.dangermouse.net/esoteric/piet.html) 6 | * Code editor with a palette with command overlay (easier to choose the next color) 7 | * Debug view that shows where the pointer is and is heading 8 | * Run the same code on multiple inputs (test cases) at once 9 | * Import and export code as images and [ascii-piet](https://github.com/dloscutoff/ascii-piet) format 10 | * Permalink that stores code and test inputs -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "es2021": true 6 | }, 7 | "extends": ["eslint:recommended", "airbnb-base", "plugin:prettier/recommended"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "import/extensions": [ 14 | "off" 15 | ], 16 | "no-bitwise": "off", 17 | "no-unused-vars": "warn", 18 | "no-console": "off", 19 | "no-empty": "off", 20 | "spaced-comment": "warn", 21 | "prettier/prettier": "warn", 22 | "no-param-reassign": "off" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bubbler-4 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piet", 3 | "version": "1.0.0", 4 | "description": "A browser-based Piet editor/interpreter", 5 | "private": true, 6 | "scripts": { 7 | "build": "webpack", 8 | "build-prod": "webpack --config prod.config.js", 9 | "build-debug": "webpack --config prod-debug.config.js", 10 | "start": "webpack serve --open", 11 | "deploy": "cp CNAME dist/ && gh-pages -d dist" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Bubbler-4/piet.git" 16 | }, 17 | "keywords": [], 18 | "author": "Bubbler-4", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/Bubbler-4/piet/issues" 22 | }, 23 | "homepage": "https://github.com/Bubbler-4/piet#readme", 24 | "devDependencies": { 25 | "css-loader": "^6.7.1", 26 | "eslint": "^8.12.0", 27 | "eslint-config-airbnb-base": "^15.0.0", 28 | "eslint-config-prettier": "^8.5.0", 29 | "eslint-plugin-import": "^2.25.4", 30 | "eslint-plugin-prettier": "^4.0.0", 31 | "file-loader": "^6.2.0", 32 | "gh-pages": "^3.2.3", 33 | "html-loader": "^3.1.0", 34 | "html-webpack-plugin": "^5.5.0", 35 | "imports-loader": "^3.1.1", 36 | "prettier": "2.6.2", 37 | "style-loader": "^3.3.1", 38 | "webpack": "^5.71.0", 39 | "webpack-cli": "^4.10.0", 40 | "webpack-dev-server": "^4.7.4" 41 | }, 42 | "dependencies": { 43 | "@popperjs/core": "^2.11.6", 44 | "bootstrap": "^5.1.3", 45 | "bootstrap-icons": "^1.8.1", 46 | "jquery": "^3.6.0", 47 | "js-base64": "^3.7.2", 48 | "lodash": "^4.17.21", 49 | "pako": "^2.0.4", 50 | "snapsvg": "^0.5.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | index: './src/index.js', 7 | }, 8 | output: { 9 | filename: '[name].[contenthash].bundle.js', 10 | path: path.resolve(__dirname, 'dist'), 11 | publicPath: '/', 12 | clean: true, 13 | }, 14 | plugins: [ 15 | new HTMLWebpackPlugin({ 16 | template: 'src/index.html', 17 | }), 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'], 24 | }, 25 | { 26 | test: /\.html$/, 27 | loader: 'html-loader', 28 | }, 29 | { 30 | test: require.resolve('snapsvg/dist/snap.svg.js'), 31 | use: 'imports-loader?wrapper=window&additionalCode=module.exports=0;', 32 | }, 33 | { 34 | test: /\.woff$/, 35 | include: path.resolve( 36 | __dirname, 37 | './node_modules/bootstrap-icons/font/fonts', 38 | ), 39 | use: { 40 | loader: 'file-loader', 41 | options: { 42 | name: '[name].[ext]', 43 | outputPath: '.', 44 | publicPath: '/', 45 | }, 46 | }, 47 | }, 48 | ], 49 | }, 50 | optimization: { 51 | runtimeChunk: 'single', 52 | splitChunks: { 53 | cacheGroups: { 54 | vendor: { 55 | test: /[\\/]node_modules[\\/]/, 56 | name: 'vendors', 57 | chunks: 'all', 58 | }, 59 | }, 60 | }, 61 | }, 62 | devServer: { 63 | static: './dist', 64 | }, 65 | mode: 'production', 66 | // devtool: 'eval-source-map', 67 | }; 68 | -------------------------------------------------------------------------------- /prod-debug.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | index: './src/index.js', 7 | }, 8 | output: { 9 | filename: '[name].[contenthash].bundle.js', 10 | path: path.resolve(__dirname, 'dist'), 11 | publicPath: '/', 12 | clean: true, 13 | }, 14 | plugins: [ 15 | new HTMLWebpackPlugin({ 16 | template: 'src/index.html', 17 | }), 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'], 24 | }, 25 | { 26 | test: /\.html$/, 27 | loader: 'html-loader', 28 | }, 29 | { 30 | test: require.resolve('snapsvg/dist/snap.svg.js'), 31 | use: 'imports-loader?wrapper=window&additionalCode=module.exports=0;', 32 | }, 33 | { 34 | test: /\.woff$/, 35 | include: path.resolve( 36 | __dirname, 37 | './node_modules/bootstrap-icons/font/fonts', 38 | ), 39 | use: { 40 | loader: 'file-loader', 41 | options: { 42 | name: '[name].[ext]', 43 | outputPath: '.', 44 | publicPath: '/', 45 | }, 46 | }, 47 | }, 48 | ], 49 | }, 50 | optimization: { 51 | runtimeChunk: 'single', 52 | splitChunks: { 53 | cacheGroups: { 54 | vendor: { 55 | test: /[\\/]node_modules[\\/]/, 56 | name: 'vendors', 57 | chunks: 'all', 58 | }, 59 | }, 60 | }, 61 | }, 62 | devServer: { 63 | static: './dist', 64 | }, 65 | mode: 'development', 66 | devtool: 'inline-source-map', 67 | }; 68 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | index: './src/index.js', 7 | }, 8 | output: { 9 | filename: '[name].[contenthash].bundle.js', 10 | path: path.resolve(__dirname, 'dist'), 11 | // publicPath: '/piet/', 12 | clean: true, 13 | }, 14 | plugins: [ 15 | new HTMLWebpackPlugin({ 16 | template: 'src/index.html', 17 | }), 18 | ], 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/, 23 | use: ['style-loader', 'css-loader'], 24 | }, 25 | { 26 | test: /\.html$/, 27 | loader: 'html-loader', 28 | }, 29 | { 30 | test: require.resolve('snapsvg/dist/snap.svg.js'), 31 | use: 'imports-loader?wrapper=window&additionalCode=module.exports=0;', 32 | }, 33 | { 34 | test: /\.woff$/, 35 | include: path.resolve( 36 | __dirname, 37 | './node_modules/bootstrap-icons/font/fonts', 38 | ), 39 | use: { 40 | loader: 'file-loader', 41 | options: { 42 | name: '[name].[ext]', 43 | outputPath: '.', 44 | publicPath: '/', 45 | }, 46 | }, 47 | }, 48 | ], 49 | }, 50 | optimization: { 51 | runtimeChunk: 'single', 52 | splitChunks: { 53 | cacheGroups: { 54 | vendor: { 55 | test: /[\\/]node_modules[\\/]/, 56 | name: 'vendors', 57 | chunks: 'all', 58 | }, 59 | }, 60 | }, 61 | }, 62 | devServer: { 63 | static: './dist', 64 | }, 65 | mode: 'development', 66 | devtool: 'eval-source-map', 67 | }; 68 | -------------------------------------------------------------------------------- /functions/api.svg.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | 3 | export function onRequest(context) { 4 | const data = context.request.url 5 | .split('?')[1] 6 | .split(',') 7 | .map(x => +x); 8 | // data: rows,cols,...colors 9 | const rows = data[0]; 10 | const cols = data[1]; 11 | const svgHeader = ``; 12 | const svgDefs = ''; 13 | let svgBody = ''; 14 | for (let r = 0; r < rows; r += 1) { 15 | for (let c = 0; c < cols; c += 1) { 16 | const idx = 2 + r * cols + c; 17 | svgBody += ``; 18 | } 19 | } 20 | const rowIndex = r => { 21 | if (r < 26) { 22 | return String.fromCharCode(r + 65); 23 | } 24 | if (r < 26 + 26 * 26) { 25 | return String.fromCharCode(((r / 26) | 0) + 65, (r % 26) + 65); 26 | } 27 | return String.fromCharCode(((r / 26 / 26) | 0) + 65, ((r / 26) | 0) % 26 + 65, r % 26 + 65); 28 | }; 29 | for (let r = 0; r < rows; r += 1) { 30 | svgBody += `${rowIndex(r)}`; 31 | } 32 | for (let c = 0; c < cols; c += 1) { 33 | svgBody += `${c + 1}`; 34 | } 35 | const svgFooter = ''; 36 | const response = new Response(svgHeader + svgDefs + svgBody + svgFooter); 37 | response.headers.set('Content-Type', 'image/svg+xml'); 38 | return response; 39 | } 40 | -------------------------------------------------------------------------------- /src/piet.js: -------------------------------------------------------------------------------- 1 | export default class Piet { 2 | static commandTextForward = [ 3 | ['', '+', '/', '>', 'dup', 'inC'], 4 | ['push', '-', '%', 'DP+', 'roll', 'outN'], 5 | ['pop', '*', '!', 'CC+', 'inN', 'outC'], 6 | ]; 7 | 8 | static commandTextReverse = [ 9 | ['', 'inC', 'dup', '>', '/', '+'], 10 | ['pop', 'outC', 'inN', 'CC+', '!', '*'], 11 | ['push', 'outN', 'roll', 'DP+', '%', '-'], 12 | ]; 13 | 14 | static commandText(forward) { 15 | return forward ? this.commandTextForward : this.commandTextReverse; 16 | } 17 | 18 | static { 19 | this.char2color = {}; 20 | this.colorvec2color = {}; 21 | this.colors = []; 22 | const hueMask = [4, 6, 2, 3, 1, 5]; 23 | const lightStr = [ 24 | ['C0', 'FF'], 25 | ['00', 'FF'], 26 | ['00', 'C0'], 27 | ]; 28 | for (let darkness = 0; darkness < 3; darkness += 1) { 29 | const light = lightStr[darkness]; 30 | for (let hue = 0; hue < 6; hue += 1) { 31 | const charcode = darkness * 32 + hue + 48; 32 | let colorcode = '#'; 33 | const colorvec = []; 34 | for (let i = 0; i < 3; i += 1) { 35 | const nextVal = light[(hueMask[hue] >> (2 - i)) & 1]; 36 | colorcode += nextVal; 37 | colorvec.push(Number.parseInt(nextVal, 16)); 38 | } 39 | colorvec.push(255); 40 | const char = String.fromCharCode(charcode); 41 | this.colors.push({ charcode, char, colorcode, colorvec }); 42 | this.char2color[char] = darkness * 6 + hue; 43 | this.colorvec2color[colorvec] = darkness * 6 + hue; 44 | } 45 | } 46 | this.colors.push({ 47 | charcode: '@'.charCodeAt(), 48 | char: '@', 49 | colorcode: '#FFFFFF', 50 | colorvec: [255, 255, 255, 255], 51 | }); 52 | this.char2color['@'] = 18; 53 | this.colorvec2color[this.colors[18].colorvec] = 18; 54 | this.colors.push({ 55 | charcode: ' '.charCodeAt(), 56 | char: ' ', 57 | colorcode: '#000000', 58 | colorvec: [0, 0, 0, 255], 59 | }); 60 | this.char2color[' '] = 19; 61 | this.colorvec2color[this.colors[19].colorvec] = 19; 62 | } 63 | 64 | static compress(codeGrid, width, height) { 65 | function gcd(a, b) { 66 | let [x, y] = [a, b]; 67 | while (y !== 0) { 68 | [x, y] = [y, x % y]; 69 | } 70 | return x; 71 | } 72 | const g = gcd(width, height); 73 | function check(s) { 74 | for (let r = 0; r < height / s; r += 1) { 75 | for (let c = 0; c < width / s; c += 1) { 76 | const rmin = r * s; 77 | const rmax = (r + 1) * s; 78 | const cmin = c * s; 79 | const cmax = (c + 1) * s; 80 | const expected = codeGrid[rmin][cmin]; 81 | for (let rr = rmin; rr < rmax; rr += 1) { 82 | for (let cc = cmin; cc < cmax; cc += 1) { 83 | if (expected !== codeGrid[rr][cc]) { 84 | return false; 85 | } 86 | } 87 | } 88 | } 89 | } 90 | return true; 91 | } 92 | for (let gg = g; gg >= 1; gg -= 1) { 93 | if (g % gg === 0) { 94 | if (check(gg)) { 95 | return codeGrid 96 | .filter((r, i) => i % gg === 0) 97 | .map(row => row.filter((c, i) => i % gg === 0)); 98 | } 99 | } 100 | } 101 | return codeGrid; 102 | } 103 | 104 | static fromAsciiPiet(apcode) { 105 | const apgrid = apcode.replace(/[@A-Z_]/g, s => { 106 | if (s === '@') { 107 | return ' \n'; 108 | } 109 | if (s === '_') { 110 | return '?\n'; 111 | } 112 | return `${s.toLowerCase()}\n`; 113 | }); 114 | const ap = 'tvrsqulnjkimdfbcae? '; 115 | const codeGrid = apgrid 116 | .split('\n') 117 | .map(s => [...s].map(c => (ap.indexOf(c) + 20) % 20)); 118 | const cols = Math.max(1, ...codeGrid.map(row => row.length)); 119 | codeGrid.forEach(row => { 120 | while (row.length < cols) { 121 | row.push(19); 122 | } 123 | }); 124 | return codeGrid; 125 | } 126 | 127 | toAsciiPiet(compact) { 128 | const ap = 'tvrsqulnjkimdfbcae? '; 129 | const codeStr = this.code.map(row => { 130 | const rowStr = row 131 | .map(cell => ap[cell]) 132 | .join('') 133 | .trimEnd(); 134 | return rowStr; 135 | }); 136 | if (!compact) { 137 | return codeStr.join('\n'); 138 | } 139 | return codeStr 140 | .map((row, i) => { 141 | if (i === codeStr.length - 1) { 142 | return row; 143 | } 144 | const [front, back] = [row.slice(0, -1), row.slice(-1)]; 145 | if (back === '?') { 146 | return `${front}_`; 147 | } 148 | return front + back.toUpperCase(); 149 | }) 150 | .join(''); 151 | } 152 | 153 | constructor(code) { 154 | const codeStr = code || '0'; 155 | this.code = codeStr 156 | .split(/\r?\n/) 157 | .map(row => [...row].map(cell => Piet.char2color[cell])); 158 | this.rows = this.code.length; 159 | this.cols = this.code[0].length; 160 | this.history = [this.code]; 161 | this.historyIndex = 0; 162 | this.canUndo = false; 163 | this.canRedo = false; 164 | } 165 | 166 | prepareHistoryForAction() { 167 | this.history.splice(this.historyIndex + 1, Infinity); 168 | const codeCopy = this.code.map(row => row.map(cell => cell)); 169 | this.history.push(codeCopy); 170 | this.historyIndex += 1; 171 | this.canUndo = true; 172 | this.canRedo = false; 173 | this.code = this.history[this.historyIndex]; 174 | } 175 | 176 | undo() { 177 | if (!this.canUndo) return; 178 | this.historyIndex -= 1; 179 | this.canUndo = this.historyIndex > 0; 180 | this.canRedo = true; 181 | this.code = this.history[this.historyIndex]; 182 | this.rows = this.code.length; 183 | this.cols = this.code[0].length; 184 | } 185 | 186 | redo() { 187 | if (!this.canRedo) return; 188 | this.historyIndex += 1; 189 | this.canUndo = true; 190 | this.canRedo = this.historyIndex < this.history.length - 1; 191 | this.code = this.history[this.historyIndex]; 192 | this.rows = this.code.length; 193 | this.cols = this.code[0].length; 194 | } 195 | 196 | updateCell(r, c, clr) { 197 | this.prepareHistoryForAction(); 198 | this.code[r][c] = clr; 199 | } 200 | 201 | extendCode(direction) { 202 | this.prepareHistoryForAction(); 203 | if (direction === 'r') { 204 | this.code.forEach(row => { 205 | row.push(19); 206 | }); 207 | this.cols += 1; 208 | } else if (direction === 'l') { 209 | this.code.forEach(row => { 210 | row.unshift(19); 211 | }); 212 | this.cols += 1; 213 | } else if (direction === 'd') { 214 | this.code.push(this.code[0].map(() => 19)); 215 | this.rows += 1; 216 | } else if (direction === 'u') { 217 | this.code.unshift(this.code[0].map(() => 19)); 218 | this.rows += 1; 219 | } 220 | } 221 | 222 | shrinkCode(direction) { 223 | this.prepareHistoryForAction(); 224 | if ( 225 | ((direction === 'r' || direction === 'l') && this.cols === 1) || 226 | ((direction === 'u' || direction === 'd') && this.rows === 1) 227 | ) { 228 | console.log('Dimension cannot be reduced from 1'); 229 | } else if (direction === 'r') { 230 | this.code.forEach(row => { 231 | row.pop(); 232 | }); 233 | this.cols -= 1; 234 | } else if (direction === 'l') { 235 | this.code.forEach(row => { 236 | row.shift(); 237 | }); 238 | this.cols -= 1; 239 | } else if (direction === 'd') { 240 | this.code.pop(); 241 | this.rows -= 1; 242 | } else if (direction === 'u') { 243 | this.code.shift(); 244 | this.rows -= 1; 245 | } 246 | } 247 | 248 | replaceCode(newCode) { 249 | this.prepareHistoryForAction(); 250 | this.code = newCode; 251 | this.rows = this.code.length; 252 | this.cols = this.code[0].length; 253 | this.history[this.historyIndex] = this.code; 254 | } 255 | 256 | plain() { 257 | const ret = `${this.rows},${this.cols}`; 258 | return `${ret},${this.code.join(',')}`; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Piet 6 | 7 | 8 | 9 |
10 |
11 |
12 |

Piet interpreter & editor

13 |
14 |
15 |
16 |
17 |
Code
18 | 19 |
20 |
21 |
22 |
23 | 32 | 212 |
213 |
214 |
215 | 216 | -------------------------------------------------------------------------------- /src/piet-run.js: -------------------------------------------------------------------------------- 1 | import Piet from './piet.js'; 2 | 3 | function divmod(x, y) { 4 | if ((x >= 0n && y > 0n) || (x < 0n && y < 0n)) { 5 | return [x / y, x % y]; 6 | } 7 | const xabs = x >= 0n ? x : -x; 8 | const [yabs, ysgn] = y >= 0n ? [y, 1n] : [-y, -1n]; 9 | let div = xabs / yabs; 10 | let mod = xabs % yabs; 11 | if (mod !== 0n) { 12 | div += 1n; 13 | mod = yabs - mod; 14 | } 15 | return [-div, mod * ysgn]; 16 | } 17 | 18 | export default class PietRun { 19 | static { 20 | this.rOff = [0, 1, 0, -1]; 21 | this.cOff = [1, 0, -1, 0]; 22 | this.dpText = ['Right', 'Down', 'Left', 'Up']; 23 | this.ccText = [ 24 | 'Top', 25 | 'Bottom', 26 | 'Right', 27 | 'Left', 28 | 'Bottom', 29 | 'Top', 30 | 'Left', 31 | 'Right', 32 | ]; 33 | this.abbrev = { 34 | push: '', 35 | pop: 'x', 36 | '+': '+', 37 | '-': '-', 38 | '*': '*', 39 | '/': '/', 40 | '%': '%', 41 | '!': '!', 42 | '>': '>', 43 | 'DP+': 'D', 44 | 'CC+': 'C', 45 | dup: 'd', 46 | roll: 'r', 47 | inN: 'I', 48 | inC: 'i', 49 | outN: 'O', 50 | outC: 'o', 51 | }; 52 | this.cmds = { 53 | push: (self, size) => { 54 | self.stack.push(BigInt(size)); 55 | self.lastCmd = `push ${size}`; 56 | }, 57 | pop: self => { 58 | self.stack.pop(); 59 | }, 60 | '+': self => { 61 | if (self.stack.length >= 2) { 62 | const [top2, top] = self.stack.splice(-2); 63 | self.stack.push(top2 + top); 64 | } 65 | }, 66 | '-': self => { 67 | if (self.stack.length >= 2) { 68 | const [top2, top] = self.stack.splice(-2); 69 | self.stack.push(top2 - top); 70 | } 71 | }, 72 | '*': self => { 73 | if (self.stack.length >= 2) { 74 | const [top2, top] = self.stack.splice(-2); 75 | self.stack.push(top2 * top); 76 | } 77 | }, 78 | '/': self => { 79 | if ( 80 | self.stack.length >= 2 && 81 | self.stack[self.stack.length - 1] !== 0n 82 | ) { 83 | const [top2, top] = self.stack.splice(-2); 84 | self.stack.push(divmod(top2, top)[0]); 85 | } 86 | }, 87 | '%': self => { 88 | if ( 89 | self.stack.length >= 2 && 90 | self.stack[self.stack.length - 1] !== 0n 91 | ) { 92 | const [top2, top] = self.stack.splice(-2); 93 | self.stack.push(divmod(top2, top)[1]); 94 | } 95 | }, 96 | '!': self => { 97 | if (self.stack.length >= 1) { 98 | const top = self.stack.pop(); 99 | self.stack.push(top === 0n ? 1n : 0n); 100 | } 101 | }, 102 | '>': self => { 103 | if (self.stack.length >= 2) { 104 | const [top2, top] = self.stack.splice(-2); 105 | self.stack.push(top2 > top ? 1n : 0n); 106 | } 107 | }, 108 | 'DP+': self => { 109 | if (self.stack.length >= 1) { 110 | const top = self.stack.pop(); 111 | self.dp = (self.dp + Number(top % 4n) + 4) % 4; 112 | } 113 | }, 114 | 'CC+': self => { 115 | if (self.stack.length >= 1) { 116 | const top = self.stack.pop(); 117 | self.cc = (self.cc + Number(top % 2n) + 2) % 2; 118 | } 119 | }, 120 | dup: self => { 121 | if (self.stack.length >= 1) { 122 | const top = self.stack.pop(); 123 | self.stack.push(top, top); 124 | } 125 | }, 126 | roll: self => { 127 | if (self.stack.length >= 2) { 128 | const [top2, top] = self.stack.splice(-2); 129 | if (top2 >= 0n && self.stack.length >= Number(top2)) { 130 | const rot = divmod(-top, top2)[1]; 131 | const removed = self.stack.splice(-Number(top2), Number(rot)); 132 | self.stack.push(...removed); 133 | } else { 134 | self.stack.push(top2, top); 135 | } 136 | } 137 | }, 138 | inN: self => { 139 | // skip whitespaces, accept digits and stop before non-digits 140 | // if no digits found, no-op 141 | const spaces = self.input.match(/^\s*/)[0]; 142 | self.input = self.input.slice(spaces.length); 143 | const number = self.input.match(/^[-+]?[0-9]+/); 144 | if (number !== null) { 145 | self.stack.push(BigInt(number[0])); 146 | self.input = self.input.slice(number[0].length); 147 | } 148 | }, 149 | inC: self => { 150 | if (self.input !== '') { 151 | const cp = self.input.codePointAt(0); 152 | if (cp >= 65536) { 153 | self.input = self.input.slice(2); 154 | } else { 155 | self.input = self.input.slice(1); 156 | } 157 | self.stack.push(BigInt(cp)); 158 | } 159 | }, 160 | outN: self => { 161 | if (self.stack.length >= 1) { 162 | const top = self.stack.pop(); 163 | self.output += top.toString(); 164 | } 165 | }, 166 | outC: self => { 167 | if (self.stack.length >= 1) { 168 | const top = self.stack.pop(); 169 | if (top >= 0n && top <= 1114111n) { 170 | self.output += String.fromCodePoint(Number(top)); 171 | } 172 | } 173 | }, 174 | }; 175 | } 176 | 177 | constructor(code, input) { 178 | this.dp = 0; 179 | this.cc = 0; 180 | this.curR = 0; 181 | this.curC = 0; 182 | this.lastCmd = 'N/A'; 183 | this.lastChange = 'none'; 184 | this.input = input; 185 | this.output = ''; 186 | this.stack = []; 187 | this.tryHistory = []; 188 | this.finished = false; 189 | const rows = code.length; 190 | const cols = code[0].length; 191 | // for each cell (except black): reference to area object 192 | // for each area: list of cells, frontier cell for each dp and cc 193 | // white is 1 area per cell 194 | this.areas = []; 195 | for (let r = 0; r < rows; r += 1) { 196 | const areasRow = []; 197 | for (let c = 0; c < cols; c += 1) { 198 | areasRow.push(undefined); 199 | } 200 | this.areas.push(areasRow); 201 | } 202 | for (let r = 0; r < rows; r += 1) { 203 | for (let c = 0; c < cols; c += 1) { 204 | const curColor = code[r][c]; 205 | if (this.areas[r][c] === undefined && curColor !== 19) { 206 | const newArea = { 207 | cells: [], 208 | color: curColor, 209 | frontier: [0, 0, 0, 0, 0, 0, 0, 0], 210 | frontierOut: [0, 0, 0, 0, 0, 0, 0, 0], 211 | frontierBlocked: Array(8).fill(false), 212 | }; 213 | const stack = [[r, c]]; 214 | while (stack.length !== 0) { 215 | const [curR, curC] = stack.pop(); 216 | if (this.areas[curR][curC] === undefined) { 217 | this.areas[curR][curC] = newArea; 218 | newArea.cells.push([curR, curC]); 219 | [ 220 | [curR - 1, curC], 221 | [curR, curC - 1], 222 | [curR + 1, curC], 223 | [curR, curC + 1], 224 | ].forEach(([nextR, nextC]) => { 225 | if ( 226 | nextR >= 0 && 227 | nextR < rows && 228 | nextC >= 0 && 229 | nextC < cols && 230 | code[nextR][nextC] === curColor && 231 | curColor !== 18 232 | ) { 233 | stack.push([nextR, nextC]); 234 | } 235 | }); 236 | } 237 | } 238 | // frontier calculation (right x2, down x2, left x2, up x2) 239 | for (let i = 0; i < 8; i += 1) { 240 | newArea.frontier[i] = [r, c]; 241 | } 242 | newArea.cells.forEach(([curR, curC]) => { 243 | const conds = [ 244 | ([fr, fc]) => curC > fc || (curC === fc && curR < fr), 245 | ([fr, fc]) => curC > fc || (curC === fc && curR > fr), 246 | ([fr, fc]) => curR > fr || (curR === fr && curC > fc), 247 | ([fr, fc]) => curR > fr || (curR === fr && curC < fc), 248 | ([fr, fc]) => curC < fc || (curC === fc && curR > fr), 249 | ([fr, fc]) => curC < fc || (curC === fc && curR < fr), 250 | ([fr, fc]) => curR < fr || (curR === fr && curC < fc), 251 | ([fr, fc]) => curR < fr || (curR === fr && curC > fc), 252 | ]; 253 | conds.forEach((cond, i) => { 254 | const front = newArea.frontier[i]; 255 | if (cond(front)) { 256 | newArea.frontier[i] = [curR, curC]; 257 | } 258 | }); 259 | }); 260 | for (let i = 0; i < 8; i += 1) { 261 | const [fr, fc] = newArea.frontier[i]; 262 | const outR = fr + PietRun.rOff[(i / 2) | 0]; 263 | const outC = fc + PietRun.cOff[(i / 2) | 0]; 264 | newArea.frontierOut[i] = [outR, outC]; 265 | newArea.frontierBlocked[i] = 266 | outR < 0 || 267 | outR >= rows || 268 | outC < 0 || 269 | outC >= cols || 270 | code[outR][outC] === 19; 271 | } 272 | } 273 | } 274 | } 275 | const startArea = this.areas[0][0]; 276 | [this.curR, this.curC] = startArea.frontier[this.dp * 2 + this.cc]; 277 | } 278 | 279 | step() { 280 | const cycleDetected = this.tryHistory.some( 281 | ([r, c, dp, cc]) => 282 | this.curR === r && this.curC === c && this.dp === dp && this.cc === cc, 283 | ); 284 | if (cycleDetected) { 285 | this.finished = true; 286 | return; 287 | } 288 | const curArea = this.areas[this.curR][this.curC]; 289 | const { color, frontierOut, frontierBlocked } = curArea; 290 | const [nextR, nextC] = frontierOut[this.dp * 2 + this.cc]; 291 | const blocked = frontierBlocked[this.dp * 2 + this.cc]; 292 | if (blocked) { 293 | this.tryHistory.push([this.curR, this.curC, this.dp, this.cc]); 294 | if (this.lastChange === 'cc') { 295 | this.dp = (this.dp + 1) & 3; 296 | this.lastChange = 'dp'; 297 | } else { 298 | this.cc = 1 - this.cc; 299 | this.lastChange = 'cc'; 300 | } 301 | [this.curR, this.curC] = curArea.frontier[this.dp * 2 + this.cc]; 302 | this.lastCmd = 'blocked'; 303 | // console.log('blocked'); 304 | } else { 305 | this.lastChange = 'none'; 306 | if (color === 18) { 307 | this.tryHistory.push([this.curR, this.curC, this.dp, this.cc]); 308 | const nextArea = this.areas[nextR][nextC]; 309 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 310 | this.lastCmd = 'noop'; 311 | // console.log('noop'); 312 | } else { 313 | this.tryHistory.length = 0; 314 | const nextArea = this.areas[nextR][nextC]; 315 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 316 | const nextColor = nextArea.color; 317 | if (nextColor === 18) { 318 | this.lastCmd = 'noop'; 319 | // console.log('noop'); 320 | } else { 321 | const lightDiff = (((nextColor / 6 + 3) | 0) - ((color / 6) | 0)) % 3; 322 | const hueDiff = ((nextColor % 6) + 6 - (color % 6)) % 6; 323 | this.lastCmd = Piet.commandTextForward[lightDiff][hueDiff]; 324 | // console.log('cmd:', lightDiff, hueDiff, this.lastCmd); 325 | PietRun.cmds[this.lastCmd](this, curArea.cells.length); 326 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 327 | } 328 | } 329 | } 330 | } 331 | 332 | dryStep() { 333 | const cycleDetected = this.tryHistory.some( 334 | ([r, c, dp, cc]) => 335 | this.curR === r && this.curC === c && this.dp === dp && this.cc === cc, 336 | ); 337 | if (cycleDetected) { 338 | this.finished = true; 339 | return { finished: true }; 340 | } 341 | const [lastR, lastC] = [this.curR, this.curC]; 342 | const curArea = this.areas[this.curR][this.curC]; 343 | const { color, frontierOut, frontierBlocked } = curArea; 344 | const [nextR, nextC] = frontierOut[this.dp * 2 + this.cc]; 345 | const blocked = frontierBlocked[this.dp * 2 + this.cc]; 346 | if (blocked) { 347 | this.tryHistory.push([this.curR, this.curC, this.dp, this.cc]); 348 | if (this.lastChange === 'cc') { 349 | this.dp = (this.dp + 1) & 3; 350 | this.lastChange = 'dp'; 351 | } else { 352 | this.cc = 1 - this.cc; 353 | this.lastChange = 'cc'; 354 | } 355 | [this.curR, this.curC] = curArea.frontier[this.dp * 2 + this.cc]; 356 | this.lastCmd = 'blocked'; 357 | return { finished: false, blocked: true }; 358 | // console.log('blocked'); 359 | } 360 | this.lastChange = 'none'; 361 | if (color === 18) { 362 | this.tryHistory.push([this.curR, this.curC, this.dp, this.cc]); 363 | const nextArea = this.areas[nextR][nextC]; 364 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 365 | this.lastCmd = 'noop'; 366 | // console.log('noop'); 367 | } else { 368 | this.tryHistory.length = 0; 369 | const nextArea = this.areas[nextR][nextC]; 370 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 371 | const nextColor = nextArea.color; 372 | if (nextColor === 18) { 373 | this.lastCmd = 'noop'; 374 | // console.log('noop'); 375 | } else { 376 | const lightDiff = (((nextColor / 6 + 3) | 0) - ((color / 6) | 0)) % 3; 377 | const hueDiff = ((nextColor % 6) + 6 - (color % 6)) % 6; 378 | this.lastCmd = Piet.commandTextForward[lightDiff][hueDiff]; 379 | // console.log('cmd:', lightDiff, hueDiff, this.lastCmd); 380 | // PietRun.cmds[this.lastCmd](this, curArea.cells.length); 381 | [this.curR, this.curC] = nextArea.frontier[this.dp * 2 + this.cc]; 382 | } 383 | } 384 | let abb = ''; 385 | if (this.lastCmd === 'push') { 386 | abb = `${curArea.cells.length}`; 387 | } else if (this.lastCmd !== 'noop') { 388 | abb = PietRun.abbrev[this.lastCmd]; 389 | } 390 | return { 391 | finished: false, 392 | blocked: false, 393 | data: [lastR, lastC, nextR, nextC, abb], 394 | stop: abb === 'D' || abb === 'C', // stop tracing at DP+ or CC+ 395 | }; 396 | } 397 | 398 | dryRun(row, col, dp, cc) { 399 | const startArea = this.areas[row][col]; 400 | [this.curR, this.curC] = startArea.frontier[dp * 2 + cc]; 401 | this.dp = dp; 402 | this.cc = cc; 403 | const history = {}; 404 | this.finished = false; 405 | const path = []; 406 | let gFinished = false; 407 | let gStop = false; 408 | while ( 409 | !gFinished && 410 | !gStop && 411 | !history[[this.curR, this.curC, this.dp, this.cc]] 412 | ) { 413 | history[[this.curR, this.curC, this.dp, this.cc]] = true; 414 | const { finished, blocked, data, stop } = this.dryStep(); 415 | if (finished) { 416 | gFinished = true; 417 | } else if (!blocked) { 418 | path.push(data); 419 | gStop = stop; 420 | } 421 | } 422 | return path; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/piet-ui.js: -------------------------------------------------------------------------------- 1 | import Snap from 'snapsvg'; 2 | import $ from 'jquery'; 3 | import pako from 'pako'; 4 | import { Base64 } from 'js-base64'; 5 | import Piet from './piet.js'; 6 | import PietRun from './piet-run.js'; 7 | 8 | function adjustHeight() { 9 | this.style.height = 'auto'; 10 | this.style.height = `${this.scrollHeight}px`; 11 | } 12 | 13 | // cyrb53 from https://stackoverflow.com/a/52171480/4595904 14 | function cyrb53(str, seed = 0) { 15 | let h1 = 0xdeadbeef ^ seed; 16 | let h2 = 0x41c6ce57 ^ seed; 17 | [...str].forEach(c => { 18 | const ch = c.charCodeAt(); 19 | h1 = Math.imul(h1 ^ ch, 2654435761); 20 | h2 = Math.imul(h2 ^ ch, 1597334677); 21 | }); 22 | h1 = 23 | Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ 24 | Math.imul(h2 ^ (h2 >>> 13), 3266489909); 25 | h2 = 26 | Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ 27 | Math.imul(h1 ^ (h1 >>> 13), 3266489909); 28 | return `${h1}${h2}`; 29 | } 30 | 31 | export default class PietUI { 32 | constructor(code) { 33 | $('textarea') 34 | .each(function setAttr() { 35 | this.setAttribute( 36 | 'style', 37 | `height:${this.scrollHeight}px;overflow-y:hidden;`, 38 | ); 39 | }) 40 | .on('input', adjustHeight); 41 | 42 | this.paletteSvg = Snap('#svg-palette'); 43 | this.paletteRects = []; 44 | this.paletteOverlays = []; 45 | for (let light = 0; light < 3; light += 1) { 46 | this.paletteOverlays.push([]); 47 | for (let hue = 0; hue < 6; hue += 1) { 48 | const color = light * 6 + hue; 49 | const curRect = this.paletteSvg.el('rect', { 50 | width: 60, 51 | height: 30, 52 | x: 60 * hue, 53 | y: 30 * light, 54 | fill: Piet.colors[color].colorcode, 55 | }); 56 | this.paletteRects.push(curRect); 57 | const curCmd = this.paletteSvg 58 | .text(60 * hue + 30, 30 * light + 15, '') 59 | .attr({ 60 | 'text-anchor': 'middle', 61 | 'dominant-baseline': 'middle', 62 | 'pointer-events': 'none', 63 | }); 64 | if (hue === 4 && light >= 1) { 65 | curCmd.attr({ fill: 'white' }); 66 | } else { 67 | curCmd.attr({ fill: 'black' }); 68 | } 69 | this.paletteOverlays[light].push(curCmd); 70 | this.paletteSvg.g(curRect, curCmd).click(e => { 71 | e.stopPropagation(); 72 | e.preventDefault(); 73 | console.log(e); 74 | console.log(color); 75 | this.edit.updateColor(color); 76 | }); 77 | } 78 | } 79 | for (let color = 18; color <= 19; color += 1) { 80 | const curRect = this.paletteSvg 81 | .el('rect', { 82 | width: 180, 83 | height: 30, 84 | x: 180 * (color - 18), 85 | y: 90, 86 | fill: Piet.colors[color].colorcode, 87 | }) 88 | .click(e => { 89 | e.stopPropagation(); 90 | e.preventDefault(); 91 | console.log(e); 92 | console.log(color); 93 | this.edit.updateColor(color); 94 | }); 95 | this.paletteRects.push(curRect); 96 | } 97 | this.codeSvg = Snap('#svg-code'); 98 | this.code = new Piet(code); 99 | const codeRect = this.codeSvg 100 | .el('rect', { width: 30, height: 30 }) 101 | .toDefs(); 102 | this.codeRects = [[]]; 103 | this.codeRects.update = () => { 104 | let prevRows = this.codeRects.length; 105 | let prevCols = this.codeRects[0].length; 106 | while (prevCols < this.code.cols) { 107 | const c = prevCols; 108 | this.codeRects.forEach((row, r) => { 109 | const newRect = this.codeSvg.use(codeRect); 110 | newRect.attr({ 111 | x: 30 * c, 112 | y: 30 * r, 113 | fill: Piet.colors[this.code.code[r][c]].colorcode, 114 | }); 115 | row.push(newRect); 116 | }); 117 | prevCols += 1; 118 | } 119 | while (prevCols > this.code.cols) { 120 | this.codeRects.forEach(row => row.pop().remove()); 121 | prevCols -= 1; 122 | } 123 | while (prevRows < this.code.rows) { 124 | const r = prevRows; 125 | const newRow = this.code.code[r].map((color, c) => { 126 | const curRect = this.codeSvg.use(codeRect); 127 | return curRect.attr({ 128 | x: 30 * c, 129 | y: 30 * r, 130 | fill: Piet.colors[color].colorcode, 131 | }); 132 | }); 133 | this.codeRects.push(newRow); 134 | prevRows += 1; 135 | } 136 | while (prevRows > this.code.rows) { 137 | this.codeRects.pop().forEach(rect => rect.remove()); 138 | prevRows -= 1; 139 | } 140 | this.codeRects.forEach((row, r) => { 141 | row.forEach((rect, c) => { 142 | rect.attr({ fill: Piet.colors[this.code.code[r][c]].colorcode }); 143 | }); 144 | }); 145 | this.codeSvg.attr({ 146 | width: 30 * this.code.cols + 8, 147 | height: 30 * this.code.rows + 8, 148 | }); 149 | }; 150 | this.codeRects.update(); 151 | 152 | const undoButton = $('#grid-undo'); 153 | const redoButton = $('#grid-redo'); 154 | const setUndoRedoButtonState = () => { 155 | undoButton.prop('disabled', !this.code.canUndo); 156 | redoButton.prop('disabled', !this.code.canRedo); 157 | }; 158 | undoButton.on('click', () => { 159 | if (this.code.canUndo) { 160 | this.code.undo(); 161 | this.codeRects.update(); 162 | setUndoRedoButtonState(); 163 | } 164 | }); 165 | redoButton.on('click', () => { 166 | if (this.code.canRedo) { 167 | this.code.redo(); 168 | this.codeRects.update(); 169 | setUndoRedoButtonState(); 170 | } 171 | }); 172 | $('#nav-edit-tab').on('click', () => { 173 | setUndoRedoButtonState(); 174 | }); 175 | 176 | this.edit = { 177 | mode: 'write', 178 | color: 0, 179 | forward: true, 180 | selectColor: clr => { 181 | this.paletteRects[clr].attr({ 182 | stroke: 'gray', 183 | 'stroke-width': 6, 184 | x: '+=3', 185 | y: '+=3', 186 | width: '-=6', 187 | height: '-=6', 188 | }); 189 | if (clr < 18) { 190 | const commandText = Piet.commandText(this.edit.forward); 191 | const curHue = clr % 6; 192 | const curLight = (clr / 6) | 0; 193 | for (let lightOff = 0; lightOff < 3; lightOff += 1) { 194 | for (let hueOff = 0; hueOff < 6; hueOff += 1) { 195 | const s = commandText[lightOff][hueOff]; 196 | const nextHue = (curHue + hueOff) % 6; 197 | const nextLight = (curLight + lightOff) % 3; 198 | this.paletteOverlays[nextLight][nextHue].node.textContent = s; 199 | } 200 | } 201 | } else { 202 | for (let light = 0; light < 3; light += 1) { 203 | for (let hue = 0; hue < 6; hue += 1) { 204 | this.paletteOverlays[light][hue].node.textContent = ''; 205 | } 206 | } 207 | } 208 | }, 209 | unselectColor: clr => { 210 | this.paletteRects[clr].attr({ 211 | stroke: undefined, 212 | 'stroke-width': undefined, 213 | x: '-=3', 214 | y: '-=3', 215 | width: '+=6', 216 | height: '+=6', 217 | }); 218 | }, 219 | updateColor: clr => { 220 | const prevColor = this.edit.color; 221 | this.edit.color = clr; 222 | this.edit.unselectColor(prevColor); 223 | this.edit.selectColor(clr); 224 | }, 225 | updateCodeColor1: (r, c, clr) => { 226 | if (this.code.code[r][c] === clr) return; 227 | this.code.updateCell(r, c, clr); 228 | this.codeRects[r][c].attr({ fill: Piet.colors[clr].colorcode }); 229 | setUndoRedoButtonState(); 230 | }, 231 | }; 232 | this.edit.selectColor(0); 233 | 234 | ['l', 'u', 'r', 'd'].forEach(direction => { 235 | ['plus', 'minus'].forEach(action => { 236 | const id = `#grid-${direction}${action}`; 237 | const button = $(id); 238 | button.on('click', e => { 239 | e.stopPropagation(); 240 | e.preventDefault(); 241 | if (action === 'plus') { 242 | this.code.extendCode(direction); 243 | } else { 244 | this.code.shrinkCode(direction); 245 | } 246 | this.codeRects.update(); 247 | setUndoRedoButtonState(); 248 | }); 249 | }); 250 | }); 251 | 252 | const writeButton = $('#grid-write'); 253 | const pickButton = $('#grid-pick'); 254 | writeButton.on('click', () => { 255 | this.codeSvg.unclick(); 256 | this.codeSvg.drag( 257 | (dx, dy, x, y, e) => { 258 | const curR = ((e.offsetY - 3) / 30) | 0; 259 | const curC = ((e.offsetX - 3) / 30) | 0; 260 | if ( 261 | curR >= 0 && 262 | curR < this.code.rows && 263 | curC >= 0 && 264 | curC < this.code.cols 265 | ) { 266 | this.edit.updateCodeColor1(curR, curC, this.edit.color); 267 | } 268 | }, 269 | (x, y, e) => { 270 | const curR = ((e.offsetY - 3) / 30) | 0; 271 | const curC = ((e.offsetX - 3) / 30) | 0; 272 | if ( 273 | curR >= 0 && 274 | curR < this.code.rows && 275 | curC >= 0 && 276 | curC < this.code.cols 277 | ) { 278 | this.edit.updateCodeColor1(curR, curC, this.edit.color); 279 | } 280 | }, 281 | () => {}, 282 | ); 283 | }); 284 | pickButton.on('click', () => { 285 | this.codeSvg.undrag(); 286 | this.codeSvg.click(e => { 287 | const curR = ((e.offsetY - 3) / 30) | 0; 288 | const curC = ((e.offsetX - 3) / 30) | 0; 289 | if ( 290 | curR >= 0 && 291 | curR < this.code.rows && 292 | curC >= 0 && 293 | curC < this.code.cols 294 | ) { 295 | this.edit.updateColor(this.code.code[curR][curC]); 296 | } 297 | }); 298 | }); 299 | writeButton.trigger('click'); 300 | $('#nav-edit-tab').on('click', () => { 301 | writeButton.trigger('click'); 302 | }); 303 | 304 | const forwardButton = $('#grid-forward'); 305 | const backwardButton = $('#grid-backward'); 306 | forwardButton.on('click', () => { 307 | this.edit.forward = true; 308 | this.edit.updateColor(this.edit.color); 309 | }); 310 | backwardButton.on('click', () => { 311 | this.edit.forward = false; 312 | this.edit.updateColor(this.edit.color); 313 | }); 314 | 315 | this.export = { 316 | pngButton: $('#export-png'), 317 | svgButton: $('#export-svg'), 318 | asciiGrid: $('#export-ascii-grid'), 319 | asciiMini: $('#export-ascii-mini'), 320 | shareContent: $('#share-content'), 321 | permButton: $('#export-perm'), 322 | golfButton: $('#export-golf'), 323 | updateExportLink: () => { 324 | const { rows, cols } = this.code; 325 | const matrix = this.code.code; 326 | const canvas = $(''); 327 | canvas.prop({ width: cols, height: rows }); 328 | const canvasEl = canvas.get(0); 329 | const ctx = canvasEl.getContext('2d'); 330 | const imdata = ctx.getImageData(0, 0, cols, rows); 331 | for (let r = 0; r < rows; r += 1) { 332 | for (let c = 0; c < cols; c += 1) { 333 | const idx = (r * cols + c) * 4; 334 | const { colorvec } = Piet.colors[matrix[r][c]]; 335 | for (let i = 0; i < 4; i += 1) { 336 | imdata.data[idx + i] = colorvec[i]; 337 | } 338 | } 339 | } 340 | ctx.putImageData(imdata, 0, 0); 341 | canvasEl.toBlob(blob => { 342 | const pngUrl = URL.createObjectURL(blob); 343 | this.export.pngButton.prop({ href: pngUrl }); 344 | }); 345 | // const pngUrl = canvasEl.toDataURL('image/png'); 346 | // this.export.pngButton.prop({ href: pngUrl }); 347 | const svgStr = this.codeSvg 348 | .outerSVG() 349 | .replace(' { 356 | this.export.updateExportLink(); 357 | }); 358 | this.export.asciiGrid.on('click', () => { 359 | const ap = this.code.toAsciiPiet(false); 360 | this.export.shareContent.val(ap); 361 | adjustHeight.call(this.export.shareContent.get(0)); 362 | }); 363 | this.export.asciiMini.on('click', () => { 364 | const ap = this.code.toAsciiPiet(true); 365 | this.export.shareContent.val(ap); 366 | adjustHeight.call(this.export.shareContent.get(0)); 367 | }); 368 | const getPermalink = () => { 369 | // code, sep, lim, input, esc 370 | const exportObj = { 371 | code: this.code.code, 372 | sep: $('#test-sep').val(), 373 | lim: $('#test-limit').val(), 374 | esc: $('#test-escape').prop('checked'), 375 | input: $('#test-input').val(), 376 | }; 377 | this.export.shareContent.val(JSON.stringify(exportObj)); 378 | const compressed = pako.deflate(JSON.stringify(exportObj)); 379 | const b64 = Base64.fromUint8Array(compressed, true); 380 | return `${document.URL.split('#')[0]}#${b64}`; 381 | }; 382 | this.export.permButton.on('click', () => { 383 | const permalink = getPermalink(); 384 | this.export.shareContent.val(permalink); 385 | adjustHeight.call(this.export.shareContent.get(0)); 386 | }); 387 | this.export.golfButton.on('click', () => { 388 | const asciiPiet = this.code.toAsciiPiet(true); 389 | const bytes = asciiPiet.length; 390 | const codels = this.code.rows * this.code.cols; 391 | const permalink = getPermalink(); 392 | const hash = cyrb53(permalink); 393 | const postHeader = `# [Piet] + [ascii-piet], ${bytes} bytes (${this.code.rows}\xd7${this.code.cols}=${codels} codels)`; 394 | const postMain = ['```none', asciiPiet, '```'].join('\n'); 395 | const postFooter = `[Try Piet online!][piet-${hash}]`; 396 | const postImage = `![](${document.URL.split('#')[0]}api.svg?${this.code.plain()})`; 397 | const links = [ 398 | '[Piet]: https://www.dangermouse.net/esoteric/piet.html', 399 | '[ascii-piet]: https://github.com/dloscutoff/ascii-piet', 400 | `[piet-${hash}]: ${permalink}`, 401 | ].join('\n'); 402 | const post = [postHeader, postMain, postFooter, postImage, links].join('\n\n'); 403 | this.export.shareContent.val(post); 404 | adjustHeight.call(this.export.shareContent.get(0)); 405 | }); 406 | 407 | this.import = { 408 | fileButton: $('#import-file'), 409 | fileInputButton: $('#import-file-input'), 410 | asciiButton: $('#import-ascii'), 411 | asciiText: $('#share-content'), 412 | }; 413 | this.import.asciiButton.on('click', () => { 414 | const ascii = this.import.asciiText.val(); 415 | const codeGrid = Piet.fromAsciiPiet(ascii); 416 | console.log(codeGrid); 417 | this.code.replaceCode(codeGrid); 418 | this.codeRects.update(); 419 | this.export.updateExportLink(); 420 | }); 421 | this.import.fileButton.on('click', () => 422 | this.import.fileInputButton.trigger('click'), 423 | ); 424 | this.import.fileInputButton.on('change', e => { 425 | const file = e.target.files[0]; 426 | const url = URL.createObjectURL(file); 427 | const img = new Image(); 428 | img.onload = () => { 429 | URL.revokeObjectURL(url); 430 | let width = img.naturalWidth; 431 | let height = img.naturalHeight; 432 | const canvas = $(''); 433 | canvas.prop({ width, height }); 434 | const canvasEl = canvas.get(0); 435 | const ctx = canvasEl.getContext('2d'); 436 | ctx.drawImage(img, 0, 0); 437 | const imdata = ctx.getImageData(0, 0, width, height); 438 | const codeGrid = []; 439 | if (width === 0 || height === 0) { 440 | codeGrid.push([19]); // fallback to single black cell 441 | width = 1; 442 | height = 1; 443 | } else { 444 | for (let r = 0; r < height; r += 1) { 445 | codeGrid.push([]); 446 | for (let c = 0; c < width; c += 1) { 447 | const idx = r * width * 4 + c * 4; 448 | const color = 449 | Piet.colorvec2color[imdata.data.slice(idx, idx + 4)] ?? 19; 450 | codeGrid[r].push(color); 451 | } 452 | } 453 | } 454 | const codeGrid2 = Piet.compress(codeGrid, width, height); 455 | this.code.replaceCode(codeGrid2); 456 | this.codeRects.update(); 457 | this.export.updateExportLink(); 458 | }; 459 | img.src = url; 460 | e.target.value = ''; 461 | }); 462 | 463 | $('#nav-debug-tab').on('click', () => { 464 | const codeGrid = this.code.code; 465 | const startEl = $('#debug-start'); 466 | const stepEl = $('#debug-step'); 467 | const resetEl = $('#debug-reset'); 468 | const inputEl = $('#debug-input'); 469 | const outputEl = $('#debug-output'); 470 | const stackEl = $('#debug-stack'); 471 | const statusEl = $('#debug-status'); 472 | const dpEl = $('#debug-dp'); 473 | const ccEl = $('#debug-cc'); 474 | const cmdEl = $('#debug-cmd'); 475 | const speedEl = $('#debug-speed'); 476 | const runEl = $('#debug-run'); 477 | const pauseEl = $('#debug-pause'); 478 | startEl.off('click'); 479 | stepEl.off('click'); 480 | resetEl.off('click'); 481 | runEl.off('click'); 482 | pauseEl.off('click'); 483 | let animationId; 484 | let animationStartTime; 485 | let animationSpeed; 486 | let animationSteps; 487 | let origInput; 488 | let arrowEl; 489 | const updateUi = () => { 490 | const { dp, cc, input, output, stack, lastCmd } = this.runner; 491 | const dpDesc = PietRun.dpText[dp]; 492 | const ccDesc = PietRun.ccText[dp * 2 + cc]; 493 | dpEl.text(`${dp} (${dpDesc})`); 494 | ccEl.text(`${cc} (${ccDesc})`); 495 | inputEl.val(input); 496 | outputEl.val(output); 497 | const stackStr = stack.map(n => n.toString()).join(' '); 498 | stackEl.val(stackStr); 499 | adjustHeight.call(inputEl.get(0)); 500 | adjustHeight.call(outputEl.get(0)); 501 | adjustHeight.call(stackEl.get(0)); 502 | cmdEl.text(lastCmd); 503 | }; 504 | startEl.on('click', () => { 505 | outputEl.val(''); 506 | adjustHeight.call(outputEl.get(0)); 507 | if (codeGrid[0][0] === 19) { 508 | statusEl.text('Error: Starting black cell detected'); 509 | dpEl.text('N/A'); 510 | ccEl.text('N/A'); 511 | } else { 512 | origInput = inputEl.val(); 513 | this.runner = new PietRun(codeGrid, origInput); 514 | console.log(this.runner); 515 | startEl.prop('disabled', true); 516 | stepEl.prop('disabled', false); 517 | resetEl.prop('disabled', false); 518 | runEl.prop('disabled', false); 519 | inputEl.prop('readonly', true); 520 | statusEl.text('Running'); 521 | updateUi(); 522 | arrowEl = this.codeSvg.polygon(20, 5, 20, 25, 30, 15); 523 | arrowEl.attr({ fill: 'gray' }); 524 | const mat = Snap.matrix().translate( 525 | this.runner.curC * 30, 526 | this.runner.curR * 30, 527 | ); 528 | arrowEl.transform(mat); 529 | } 530 | }); 531 | stepEl.on('click', () => { 532 | console.log('step'); 533 | statusEl.text('Running'); 534 | this.runner.step(); 535 | updateUi(); 536 | const mat = Snap.matrix() 537 | .translate(this.runner.curC * 30, this.runner.curR * 30) 538 | .rotate(this.runner.dp * 90, 15, 15); 539 | arrowEl.transform(mat); 540 | if (this.runner.finished) { 541 | stepEl.prop('disabled', true); 542 | statusEl.text('Finished'); 543 | arrowEl.remove(); 544 | } 545 | }); 546 | runEl.on('click', () => { 547 | animationSpeed = Number(speedEl.val()); 548 | if ( 549 | Number.isSafeInteger(animationSpeed) && 550 | animationSpeed > 0 && 551 | animationSpeed <= 100 552 | ) { 553 | runEl.prop('disabled', true); 554 | pauseEl.prop('disabled', false); 555 | stepEl.prop('disabled', true); 556 | statusEl.text('Running'); 557 | animationStartTime = performance.now(); 558 | animationSteps = 0; 559 | const updateFrame = time => { 560 | animationId = requestAnimationFrame(updateFrame); 561 | const timeElapsed = time - animationStartTime; 562 | const nextSteps = Math.ceil((animationSpeed * timeElapsed) / 1000); 563 | for (; animationSteps < nextSteps; animationSteps += 1) { 564 | stepEl.trigger('click'); 565 | if (this.runner.finished) { 566 | pauseEl.prop('disabled', true); 567 | cancelAnimationFrame(animationId); 568 | return; 569 | } 570 | } 571 | }; 572 | animationId = requestAnimationFrame(updateFrame); 573 | } else { 574 | console.log('Invalid speed value'); 575 | statusEl.text('Invalid speed value. Allowed values: 1 - 100'); 576 | } 577 | }); 578 | pauseEl.on('click', () => { 579 | runEl.prop('disabled', false); 580 | pauseEl.prop('disabled', true); 581 | stepEl.prop('disabled', false); 582 | cancelAnimationFrame(animationId); 583 | }); 584 | resetEl.on('click', () => { 585 | console.log('reset'); 586 | startEl.prop('disabled', false); 587 | stepEl.prop('disabled', true); 588 | resetEl.prop('disabled', true); 589 | inputEl.prop('readonly', false); 590 | inputEl.val(origInput); 591 | adjustHeight.call(inputEl.get(0)); 592 | statusEl.text('N/A'); 593 | dpEl.text('N/A'); 594 | ccEl.text('N/A'); 595 | cmdEl.text('N/A'); 596 | if (arrowEl !== undefined) arrowEl.remove(); 597 | }); 598 | }); 599 | $('#nav-edit-tab, #nav-test-tab, #nav-explain-tab, #nav-share-tab').on( 600 | 'click', 601 | () => { 602 | $('#debug-reset').trigger('click'); 603 | }, 604 | ); 605 | 606 | $('#nav-test-tab').on('click', () => { 607 | const sepEl = $('#test-sep'); 608 | const limitEl = $('#test-limit'); 609 | const runEl = $('#test-run'); 610 | const stopEl = $('#test-stop'); 611 | const escapeEl = $('#test-escape'); 612 | const statusEl = $('#test-status'); 613 | const inputEl = $('#test-input'); 614 | const outputEl = $('#test-output'); 615 | 616 | runEl.on('click', () => { 617 | const sep = sepEl.val() === '' ? '---' : sepEl.val(); 618 | sepEl.val(sep); 619 | const limitVal = Number(limitEl.val()); 620 | const limit = 621 | Number.isSafeInteger(limitVal) && limitVal > 0 ? limitVal : 10000; 622 | limitEl.val(limit); 623 | runEl.prop('disabled', true); 624 | stopEl.prop('disabled', false); 625 | inputEl.prop('readonly', true); 626 | const doEscape = escapeEl.prop('checked'); 627 | let inputs = inputEl.val().split(`\n${sep}\n`); 628 | if (doEscape) { 629 | inputs = inputs.map(s => 630 | s.replace( 631 | /\\[0'"\\nrvtbf]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\}|\\x[0-9a-fA-F]{2}/g, 632 | match => { 633 | if (match.startsWith('\\u') || match.startsWith('\\x')) { 634 | const hex = match.startsWith('\\u{') 635 | ? match.slice(3, -1) 636 | : match.slice(2); 637 | return String.fromCodePoint(Number.parseInt(hex, 16)); 638 | } 639 | return { 640 | '\\0': '\0', 641 | "\\'": "'", 642 | '\\"': '"', 643 | '\\\\': '\\', 644 | '\\n': '\n', 645 | '\\r': '\r', 646 | '\\v': '\v', 647 | '\\t': '\t', 648 | '\\b': '\b', 649 | '\\f': '\f', 650 | }[match]; 651 | }, 652 | ), 653 | ); 654 | } 655 | const runners = inputs.map(input => new PietRun(this.code.code, input)); 656 | const stepsPerRunner = runners.map(() => 0); 657 | let alive = runners.length; 658 | const steps = 10; // steps per ms 659 | const startTime = performance.now(); 660 | let stepsRun = 0; 661 | let runId; 662 | statusEl.text('Running'); 663 | const update = time => { 664 | if (alive === 0) { 665 | stopEl.trigger('click'); 666 | return; 667 | } 668 | runId = requestAnimationFrame(update); 669 | const totalSteps = Math.ceil(steps * (time - startTime)); 670 | const singleSteps = Math.round((totalSteps - stepsRun) / alive); 671 | runners.forEach((runner, i) => { 672 | if (runner.finished) return; 673 | for (let s = 0; s < singleSteps; s += 1) { 674 | runner.step(); 675 | stepsRun += 1; 676 | stepsPerRunner[i] += 1; 677 | if (stepsPerRunner[i] >= limit) { 678 | runner.finished = true; 679 | } 680 | if (runner.finished) { 681 | alive -= 1; 682 | break; 683 | } 684 | } 685 | }); 686 | }; 687 | stopEl.one('click', () => { 688 | cancelAnimationFrame(runId); 689 | const outputs = runners.map(runner => runner.output); 690 | runners.forEach((runner, i) => { 691 | if (!runner.finished) { 692 | outputs[i] += '\n** Aborted'; 693 | } else if (stepsPerRunner[i] >= limit) { 694 | outputs[i] += '\n** Exceeded Step Limit'; 695 | } 696 | }); 697 | outputEl.val(outputs.join('\n---\n')); 698 | adjustHeight.call(outputEl.get(0)); 699 | statusEl.text('Finished'); 700 | stopEl.prop('disabled', true); 701 | runEl.prop('disabled', false); 702 | inputEl.prop('readonly', false); 703 | }); 704 | runId = requestAnimationFrame(update); 705 | }); 706 | }); 707 | 708 | // explained: push[num] pop[x] [+] [-] [*] [/] [%] [!] [>] [D]P+ [C]C+ 709 | // [d]up [r]oll [I]nN [i]nC [O]utN [o]utC 710 | this.codeSvg 711 | .circle(0, 0, 5) 712 | .attr({ fill: 'gray', stroke: 'none' }) 713 | .marker(-5, -5, 10, 10, 0, 0) 714 | .attr({ id: 'mark2' }); 715 | this.codeSvg 716 | .polygon([0, -10, 0, 10, 10, 0]) 717 | .attr({ fill: 'black', stroke: 'none' }) 718 | .marker(0, -10, 10, 20, 10, 0) 719 | .attr({ id: 'mark1' }); 720 | // path[r][c] = [...[fromr, fromc, tor, toc, cmd]] 721 | // pathG[r][c] = svg group that contains all line segments 722 | this.explain = { 723 | path: [[]], 724 | pathG: [[]], 725 | initPath: () => { 726 | this.explain.path = this.code.code.map(row => row.map(() => [])); 727 | this.explain.pathG = this.code.code.map(row => 728 | row.map(() => this.codeSvg.g()), 729 | ); 730 | this.explain.runner = new PietRun(this.code.code, ''); 731 | }, 732 | destroyPath: () => { 733 | this.explain.pathG.forEach(row => row.forEach(g => g.remove())); 734 | }, 735 | drawCmd: (groupEl, prevRow, prevCol, nextRow, nextCol, cmd, isFirst) => { 736 | const x1 = prevCol * 30 + 15; 737 | const y1 = prevRow * 30 + 15; 738 | const x2 = nextCol * 30 + 15; 739 | const y2 = nextRow * 30 + 15; 740 | const line = groupEl.line(x1, y1, x2, y2); 741 | // line.attr({ stroke: 'black', 'marker-end': 'url(#mark2)' }); 742 | line.attr({ stroke: 'black' }); 743 | line.node.style['marker-end'] = Snap.url('mark2'); 744 | if (isFirst) { 745 | line.node.style['marker-start'] = Snap.url('mark1'); 746 | // line.attr({ 'marker-start': 'url(#mark1)' }); 747 | } else { 748 | line.node.style['marker-start'] = Snap.url('mark2'); 749 | // line.attr({ 'marker-start': 'url(#mark2)' }); 750 | } 751 | if (cmd !== '') { 752 | let [xt, yt] = [0, 0]; 753 | if (nextCol === prevCol + 1) { 754 | [xt, yt] = [prevCol * 30 + 22, prevRow * 30 + 8]; 755 | } else if (nextRow === prevRow + 1) { 756 | [xt, yt] = [prevCol * 30 + 22, prevRow * 30 + 22]; 757 | } else if (nextCol === prevCol - 1) { 758 | [xt, yt] = [prevCol * 30 + 8, prevRow * 30 + 22]; 759 | } else if (nextRow === prevRow - 1) { 760 | [xt, yt] = [prevCol * 30 + 8, prevRow * 30 + 8]; 761 | } 762 | const cmdText = groupEl.text(xt, yt, cmd); 763 | const prevCode = this.code.code[prevRow][prevCol]; 764 | cmdText.attr({ 765 | 'text-anchor': 'middle', 766 | 'dominant-baseline': 'middle', 767 | 'pointer-events': 'none', 768 | 'font-size': '10px', 769 | fill: prevCode === 10 || prevCode === 16 ? 'white' : 'black', 770 | }); 771 | } 772 | }, 773 | drawDash: (groupEl, prevRow, prevCol, nextRow, nextCol) => { 774 | const x1 = prevCol * 30 + 15; 775 | const y1 = prevRow * 30 + 15; 776 | const x2 = nextCol * 30 + 15; 777 | const y2 = nextRow * 30 + 15; 778 | const line = groupEl.line(x1, y1, x2, y2); 779 | line.attr({ stroke: 'black', 'stroke-dasharray': '4' }); 780 | }, 781 | addPath: (row, col, dp, cc) => { 782 | const path = this.explain.runner.dryRun(row, col, dp, cc); 783 | this.explain.path[row][col] = path; 784 | let [curRow, curCol] = [row, col]; 785 | const g = this.explain.pathG[row][col]; 786 | path.forEach(([prevRow, prevCol, nextRow, nextCol, cmd], i) => { 787 | // draw commands, and dashed lines on big area jumps 788 | if (curRow !== prevRow || curCol !== prevCol) { 789 | this.explain.drawDash(g, curRow, curCol, prevRow, prevCol); 790 | } 791 | this.explain.drawCmd( 792 | g, 793 | prevRow, 794 | prevCol, 795 | nextRow, 796 | nextCol, 797 | cmd, 798 | i === 0, 799 | ); 800 | [curRow, curCol] = [nextRow, nextCol]; 801 | }); 802 | }, 803 | removePath: (row, col) => { 804 | this.explain.path[row][col].length = 0; 805 | this.explain.pathG[row][col] 806 | .children() 807 | .forEach(child => child.remove()); 808 | }, 809 | togglePath: (row, col, dp, cc) => { 810 | if (this.explain.path[row][col].length === 0) { 811 | this.explain.addPath(row, col, dp, cc); 812 | } else { 813 | this.explain.removePath(row, col); 814 | } 815 | }, 816 | exportButton: $('#export-explained'), 817 | exportUrl: undefined, 818 | updateExportLink: () => { 819 | if (this.explain.exportUrl !== undefined) { 820 | URL.revokeObjectURL(this.explain.exportUrl); 821 | } 822 | const svgStr = this.codeSvg 823 | .outerSVG() 824 | .replace(' { 833 | // on svg click: if there's already a path, remove it; 834 | // otherwise add a new path starting there 835 | this.codeSvg.undrag(); 836 | this.codeSvg.unclick(); 837 | this.explain.initPath(); 838 | this.codeSvg.click(e => { 839 | const curR = ((e.offsetY - 3) / 30) | 0; 840 | const curC = ((e.offsetX - 3) / 30) | 0; 841 | if ( 842 | curR >= 0 && 843 | curR < this.code.rows && 844 | curC >= 0 && 845 | curC < this.code.cols && 846 | this.code.code[curR][curC] !== 19 847 | ) { 848 | let dp = 0; 849 | if ($('#explain-dp-0').prop('checked')) { 850 | dp = 0; 851 | } else if ($('#explain-dp-1').prop('checked')) { 852 | dp = 1; 853 | } else if ($('#explain-dp-2').prop('checked')) { 854 | dp = 2; 855 | } else if ($('#explain-dp-3').prop('checked')) { 856 | dp = 3; 857 | } 858 | let cc = 0; 859 | if ($('#explain-cc-0').prop('checked')) { 860 | cc = 0; 861 | } else if ($('#explain-cc-1').prop('checked')) { 862 | cc = 1; 863 | } 864 | this.explain.togglePath(curR, curC, dp, cc); 865 | // console.log(this.explain); 866 | this.explain.updateExportLink(); 867 | } 868 | }); 869 | }); 870 | $('#nav-debug-tab, #nav-test-tab, #nav-share-tab').on('click', () => { 871 | this.codeSvg.undrag(); 872 | this.codeSvg.unclick(); 873 | this.explain.destroyPath(); 874 | }); 875 | $('#nav-edit-tab').on('click', () => { 876 | this.explain.destroyPath(); 877 | }); 878 | 879 | const loadFromPermalink = () => { 880 | const hash = document.location.hash.slice(1); 881 | const compressed = Base64.toUint8Array(hash); 882 | try { 883 | const exportJSON = pako.inflate(compressed, { to: 'string' }); 884 | const exportObj = JSON.parse(exportJSON); 885 | // ensure that the obj has all five fields 886 | if ( 887 | exportObj.code === undefined || 888 | exportObj.sep === undefined || 889 | exportObj.lim === undefined || 890 | exportObj.esc === undefined || 891 | exportObj.input === undefined 892 | ) { 893 | console.log(exportObj); 894 | throw new TypeError('Decoded object does not have necessary fields'); 895 | } 896 | this.code.replaceCode(exportObj.code); 897 | this.codeRects.update(); 898 | $('#test-sep').val(exportObj.sep); 899 | $('#test-limit').val(exportObj.lim); 900 | $('#test-escape').val(exportObj.esc); 901 | $('#test-input').val(exportObj.input); 902 | $('#nav-test-tab').trigger('click'); 903 | setTimeout(() => adjustHeight.call($('#test-input').get(0)), 1000); 904 | } catch (err) { 905 | console.log(err); 906 | } 907 | }; 908 | if (document.location.hash !== '') { 909 | loadFromPermalink(); 910 | } 911 | } 912 | } 913 | --------------------------------------------------------------------------------