├── 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 = `';
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 |
33 |
34 |
35 |
36 |
Palette
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
Label
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
Grid
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | Status: N/A
102 | Last command: N/A
103 | DP: N/A
104 | CC: N/A
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | Input separator
126 |
127 |
128 |
138 |
139 | Status: N/A
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
Explain
155 |
Select DP and CC, and then click the starting point to add an execution path.
156 | Click a starting point again to remove it.
157 |
168 |
169 |
CC
170 |
171 |
172 |
173 |
174 |
175 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
Import
185 |
189 |
190 |
191 |
192 |
Export
193 |
194 |
PNG
195 |
SVG
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
209 |
210 |
211 |
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('