mirror of
https://github.com/remko/waforth
synced 2025-01-30 08:34:58 +01:00
thurtle: Share & Export
This commit is contained in:
parent
69bb024a5f
commit
b701efd803
7 changed files with 313 additions and 45 deletions
|
@ -151,7 +151,8 @@ async function handleBuildFinished(result) {
|
||||||
if (watch) {
|
if (watch) {
|
||||||
// Simple static file server
|
// Simple static file server
|
||||||
createServer(async function (req, res) {
|
createServer(async function (req, res) {
|
||||||
let f = path.join(__dirname, "public", req.url);
|
const url = req.url.replace(/\?.*/g, "");
|
||||||
|
let f = path.join(__dirname, "public", url);
|
||||||
try {
|
try {
|
||||||
if ((await fs.promises.lstat(f)).isDirectory()) {
|
if ((await fs.promises.lstat(f)).isDirectory()) {
|
||||||
f = path.join(f, "index.html");
|
f = path.join(f, "index.html");
|
||||||
|
@ -163,7 +164,7 @@ if (watch) {
|
||||||
const data = await fs.promises.readFile(f);
|
const data = await fs.promises.readFile(f);
|
||||||
res.writeHead(
|
res.writeHead(
|
||||||
200,
|
200,
|
||||||
req.url.endsWith(".svg")
|
url.endsWith(".svg")
|
||||||
? {
|
? {
|
||||||
"Content-Type": "image/svg+xml",
|
"Content-Type": "image/svg+xml",
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"repository": "github:remko/waforth",
|
"repository": "github:remko/waforth",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/node": "^17.0.31",
|
"@types/node": "^17.0.31",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"esbuild": "^0.14.36",
|
"esbuild": "^0.14.36",
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"eslint-plugin-react-hooks": "^4.4.0",
|
"eslint-plugin-react-hooks": "^4.4.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"immutability-helper": "^3.1.1",
|
"immutability-helper": "^3.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
|
|
|
@ -29,6 +29,7 @@ declare global {
|
||||||
};
|
};
|
||||||
g: JSXElement<Omit<SVGGraphicsElement, "transform">> & {
|
g: JSXElement<Omit<SVGGraphicsElement, "transform">> & {
|
||||||
xmlns: "http://www.w3.org/2000/svg";
|
xmlns: "http://www.w3.org/2000/svg";
|
||||||
|
stroke?: string;
|
||||||
transform?: string;
|
transform?: string;
|
||||||
};
|
};
|
||||||
[elemName: string]: any;
|
[elemName: string]: any;
|
||||||
|
|
|
@ -2,6 +2,7 @@ export type Program = {
|
||||||
name: string;
|
name: string;
|
||||||
program: string;
|
program: string;
|
||||||
isExample: boolean;
|
isExample: boolean;
|
||||||
|
isEphemeral?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const examples: Program[] = [
|
const examples: Program[] = [
|
||||||
|
@ -98,7 +99,6 @@ const examples: Program[] = [
|
||||||
98 100 */ RECURSE
|
98 100 */ RECURSE
|
||||||
;
|
;
|
||||||
|
|
||||||
PENUP -500 -180 SETXY PENDOWN
|
|
||||||
140 SPIRAL
|
140 SPIRAL
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
@ -152,11 +152,6 @@ PENUP -500 -180 SETXY PENDOWN
|
||||||
;
|
;
|
||||||
|
|
||||||
: SNOWFLAKE ( -- )
|
: SNOWFLAKE ( -- )
|
||||||
PENUP
|
|
||||||
LENGTH 4 / NEGATE
|
|
||||||
LENGTH 2/ NEGATE
|
|
||||||
SETXY
|
|
||||||
PENDOWN
|
|
||||||
3 0 DO
|
3 0 DO
|
||||||
LENGTH DEPTH SIDE
|
LENGTH DEPTH SIDE
|
||||||
120 RIGHT
|
120 RIGHT
|
||||||
|
@ -173,6 +168,7 @@ SNOWFLAKE
|
||||||
program: `
|
program: `
|
||||||
450 CONSTANT SIZE
|
450 CONSTANT SIZE
|
||||||
7 CONSTANT BRANCHES
|
7 CONSTANT BRANCHES
|
||||||
|
160 CONSTANT SPREAD
|
||||||
|
|
||||||
VARIABLE RND
|
VARIABLE RND
|
||||||
HERE RND !
|
HERE RND !
|
||||||
|
@ -192,14 +188,13 @@ HERE RND !
|
||||||
OVER FORWARD
|
OVER FORWARD
|
||||||
BRANCHES 0 DO
|
BRANCHES 0 DO
|
||||||
OVER 2/
|
OVER 2/
|
||||||
160 CHOOSE 80 -
|
SPREAD CHOOSE SPREAD 2/ -
|
||||||
RECURSE
|
RECURSE
|
||||||
LOOP
|
LOOP
|
||||||
SWAP BACKWARD
|
PENUP SWAP BACKWARD PENDOWN
|
||||||
LEFT
|
LEFT
|
||||||
;
|
;
|
||||||
|
|
||||||
PENUP 0 SIZE NEGATE SETXY PENDOWN
|
|
||||||
1 SETPENSIZE
|
1 SETPENSIZE
|
||||||
SIZE 0 PLANT
|
SIZE 0 PLANT
|
||||||
`,
|
`,
|
||||||
|
@ -226,10 +221,11 @@ export function getProgram(name: string): Program | undefined {
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePrograms() {
|
function savePrograms() {
|
||||||
|
console.log("SavePrograms", programs);
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
"thurtle:programs",
|
"thurtle:programs",
|
||||||
JSON.stringify(programs.filter((p) => !p.isExample))
|
JSON.stringify(programs.filter((p) => !p.isExample && !p.isEphemeral))
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -237,17 +233,23 @@ function savePrograms() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveProgram(name: string, program: string): boolean {
|
export function saveProgram(
|
||||||
|
name: string,
|
||||||
|
program: string,
|
||||||
|
ephemeral = false
|
||||||
|
): boolean {
|
||||||
const prg = getProgram(name);
|
const prg = getProgram(name);
|
||||||
let isNew = false;
|
let isNew = false;
|
||||||
if (prg != null) {
|
if (prg != null) {
|
||||||
prg.program = program;
|
prg.program = program;
|
||||||
|
prg.isEphemeral = ephemeral;
|
||||||
} else {
|
} else {
|
||||||
isNew = true;
|
isNew = true;
|
||||||
programs.push({
|
programs.push({
|
||||||
name,
|
name,
|
||||||
isExample: false,
|
isExample: false,
|
||||||
program,
|
program,
|
||||||
|
isEphemeral: ephemeral,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
savePrograms();
|
savePrograms();
|
||||||
|
|
|
@ -31,7 +31,10 @@
|
||||||
|
|
||||||
.output {
|
.output {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
|
max-height: calc(
|
||||||
|
100vh - 450px
|
||||||
|
); /* hack because i can't figure out how to auto scale this */
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
label {
|
||||||
|
|
|
@ -11,7 +11,9 @@ import {
|
||||||
saveProgram,
|
saveProgram,
|
||||||
} from "./programs";
|
} from "./programs";
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor";
|
||||||
import path from "path";
|
import { saveAs } from "file-saver";
|
||||||
|
|
||||||
|
declare let bootstrap: any;
|
||||||
|
|
||||||
function About() {
|
function About() {
|
||||||
return (
|
return (
|
||||||
|
@ -83,14 +85,35 @@ const rootEl = (
|
||||||
<div class="main d-flex flex-column p-2">
|
<div class="main d-flex flex-column p-2">
|
||||||
<div class="d-flex flex-row flex-grow-1">
|
<div class="d-flex flex-row flex-grow-1">
|
||||||
<div class="left-pane d-flex flex-column">
|
<div class="left-pane d-flex flex-column">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row flex-wrap flex-md-nowrap py-2">
|
||||||
<select class="form-select mb-2" data-hook="examples"></select>
|
<select class="form-select" data-hook="examples"></select>
|
||||||
<div>
|
<div class="ms-auto me-auto ms-md-2 me-md-0 mt-1 mt-md-0">
|
||||||
<div class="btn-group ms-2">
|
<div class="btn-group w-xs-100">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
aria-label="Run"
|
||||||
|
data-hook="run"
|
||||||
|
onclick={run}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="bi bi-play-fill"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-light"
|
class="btn btn-light border"
|
||||||
data-hook="save-btn"
|
data-hook="save-btn"
|
||||||
|
aria-label="Save"
|
||||||
onclick={save}
|
onclick={save}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -113,7 +136,7 @@ const rootEl = (
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-light dropdown-toggle dropdown-toggle-split"
|
class="btn btn-light dropdown-toggle dropdown-toggle-split border"
|
||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
|
@ -139,39 +162,54 @@ const rootEl = (
|
||||||
Delete
|
Delete
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="dropdown-divider"></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick={share}>
|
||||||
|
Share
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick={downloadSVG}>
|
||||||
|
Download as SVG
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick={downloadPNG}>
|
||||||
|
Download as PNG
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{editor.el}
|
{editor.el}
|
||||||
<button data-hook="run" class="btn btn-primary mt-2">
|
|
||||||
Run
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column ms-3 right-pane">
|
<div class="d-flex flex-column ms-3 right-pane">
|
||||||
<svg
|
<svg
|
||||||
class="world"
|
class="world"
|
||||||
viewBox="0 0 1000 1000"
|
viewBox="-500 -500 1000 1000"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-hook="world"
|
||||||
>
|
>
|
||||||
<g
|
<g
|
||||||
|
fill-opacity="0"
|
||||||
|
stroke="#000"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
transform="translate(500 500)"
|
id="paths"
|
||||||
>
|
></g>
|
||||||
<g xmlns="http://www.w3.org/2000/svg" id="paths"></g>
|
<image
|
||||||
<image
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
id="turtle"
|
||||||
id="turtle"
|
data-hook="turtle"
|
||||||
width="50"
|
width="50"
|
||||||
height="50"
|
height="50"
|
||||||
href={turtle}
|
href={turtle}
|
||||||
/>
|
/>
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
<form>
|
<form>
|
||||||
<div class="form-group mt-3">
|
<div class="form-group mt-3">
|
||||||
<label>Output</label>
|
<label>Output</label>
|
||||||
<pre class="output" data-hook="output"></pre>
|
<pre class="mb-0 output" data-hook="output"></pre>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -260,6 +298,55 @@ const rootEl = (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal" tabIndex={-1} data-hook="share-modal">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
Share '<span data-hook="title"></span>'
|
||||||
|
</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Share URL</p>
|
||||||
|
<input
|
||||||
|
data-hook="url"
|
||||||
|
onClick={selectShareURL}
|
||||||
|
type="text"
|
||||||
|
readOnly={true}
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-hook="saving-modal" class="modal" tabIndex={-1} role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<p>Saving as PNG ...</p>
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
document.body.appendChild(rootEl);
|
document.body.appendChild(rootEl);
|
||||||
|
@ -279,6 +366,18 @@ const outputEl = rootEl.querySelector(
|
||||||
const deleteActionEl = rootEl.querySelector(
|
const deleteActionEl = rootEl.querySelector(
|
||||||
"[data-hook=delete-action]"
|
"[data-hook=delete-action]"
|
||||||
) as HTMLAnchorElement;
|
) as HTMLAnchorElement;
|
||||||
|
const shareModalEl = rootEl.querySelector("[data-hook=share-modal]");
|
||||||
|
const shareModal = new bootstrap.Modal(shareModalEl);
|
||||||
|
const shareModalTitleEl = rootEl.querySelector(
|
||||||
|
"[data-hook=share-modal] [data-hook=title]"
|
||||||
|
) as HTMLSpanElement;
|
||||||
|
const savingModal = new bootstrap.Modal(
|
||||||
|
rootEl.querySelector("[data-hook=saving-modal]")
|
||||||
|
);
|
||||||
|
const shareModalURLEl = rootEl.querySelector(
|
||||||
|
"[data-hook=share-modal] [data-hook=url]"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const worldEl = rootEl.querySelector("[data-hook=world]") as SVGSVGElement;
|
||||||
|
|
||||||
enum PenState {
|
enum PenState {
|
||||||
Up = 0,
|
Up = 0,
|
||||||
|
@ -292,12 +391,14 @@ type Path = {
|
||||||
|
|
||||||
let rotation = 0;
|
let rotation = 0;
|
||||||
let position = { x: 0, y: 0 };
|
let position = { x: 0, y: 0 };
|
||||||
|
let boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||||
let pen = PenState.Down;
|
let pen = PenState.Down;
|
||||||
let visible = true;
|
let visible = true;
|
||||||
let paths: Array<Path> = [{ d: [`M${position.x} ${position.y}`] }];
|
let paths: Array<Path> = [{ d: [`M${position.x} ${position.y}`] }];
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
position.x = position.y = 0;
|
position.x = position.y = 0;
|
||||||
|
boundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||||
rotation = 270;
|
rotation = 270;
|
||||||
pen = PenState.Down;
|
pen = PenState.Down;
|
||||||
paths = [{ d: [`M ${position.x} ${position.y}`] }];
|
paths = [{ d: [`M ${position.x} ${position.y}`] }];
|
||||||
|
@ -317,14 +418,14 @@ function forward(d: number) {
|
||||||
paths[paths.length - 1].d.push(
|
paths[paths.length - 1].d.push(
|
||||||
[pen === PenState.Down ? "l" : "m", dx, dy].join(" ")
|
[pen === PenState.Down ? "l" : "m", dx, dy].join(" ")
|
||||||
);
|
);
|
||||||
|
updatePosition(position.x + dx, position.y + dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setXY(x: number, y: number) {
|
function setXY(x: number, y: number) {
|
||||||
paths[paths.length - 1].d.push(
|
paths[paths.length - 1].d.push(
|
||||||
[pen === PenState.Down ? "l" : "M", x, y].join(" ")
|
[pen === PenState.Down ? "l" : "M", x, y].join(" ")
|
||||||
);
|
);
|
||||||
position.x = x;
|
updatePosition(x, y);
|
||||||
position.y = y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setPen(s: PenState) {
|
function setPen(s: PenState) {
|
||||||
|
@ -339,10 +440,29 @@ function setVisible(b: boolean) {
|
||||||
visible = b;
|
visible = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePosition(x: number, y: number) {
|
||||||
|
position.x = x;
|
||||||
|
position.y = y;
|
||||||
|
if (x < boundingBox.minX) {
|
||||||
|
boundingBox.minX = x;
|
||||||
|
}
|
||||||
|
if (x > boundingBox.maxX) {
|
||||||
|
boundingBox.maxX = x;
|
||||||
|
}
|
||||||
|
if (y < boundingBox.minY) {
|
||||||
|
boundingBox.minY = y;
|
||||||
|
}
|
||||||
|
if (y > boundingBox.maxY) {
|
||||||
|
boundingBox.maxY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Drawing
|
// Drawing
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
const padding = 0.05;
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
patshEl.innerHTML = "";
|
patshEl.innerHTML = "";
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
|
@ -363,6 +483,24 @@ function draw() {
|
||||||
position.x - 25
|
position.x - 25
|
||||||
} ${position.y - 25})`
|
} ${position.y - 25})`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const width = boundingBox.maxX - boundingBox.minX;
|
||||||
|
const height = boundingBox.maxY - boundingBox.minY;
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
worldEl.setAttribute("viewBox", "-500 -500 1000 1000");
|
||||||
|
} else {
|
||||||
|
const paddingX = width * padding;
|
||||||
|
const paddingY = height * padding;
|
||||||
|
worldEl.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
[
|
||||||
|
Math.floor(boundingBox.minX - paddingX),
|
||||||
|
Math.floor(boundingBox.minY - paddingY),
|
||||||
|
Math.ceil(width + 2 * paddingX),
|
||||||
|
Math.ceil(height + 2 * paddingY),
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -427,10 +565,86 @@ function del(ev: MouseEvent) {
|
||||||
loadProgram(DEFAULT_PROGRAM);
|
loadProgram(DEFAULT_PROGRAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getSVG(): Promise<{
|
||||||
|
data: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}> {
|
||||||
|
await run();
|
||||||
|
const n = worldEl.cloneNode(true) as SVGSVGElement;
|
||||||
|
n.querySelector("[data-hook=turtle]")?.remove();
|
||||||
|
const viewBox = n.getAttribute("viewBox")!.split(" ");
|
||||||
|
n.setAttribute("width", parseInt(viewBox[2]) + "");
|
||||||
|
n.setAttribute("height", parseInt(viewBox[3]) + "");
|
||||||
|
return {
|
||||||
|
width: parseInt(n.getAttribute("width")!),
|
||||||
|
height: parseInt(n.getAttribute("height")!),
|
||||||
|
data: n.outerHTML,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSVG(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
const blob = new Blob([(await getSVG()).data], { type: "image/svg+xml" });
|
||||||
|
saveAs(blob, programsEl.value + ".svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPNG(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
savingModal.show();
|
||||||
|
const svg = await getSVG();
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.style.display = "none";
|
||||||
|
document.body.appendChild(img);
|
||||||
|
try {
|
||||||
|
const dataURL = await new Promise<string>((resolve, reject) => {
|
||||||
|
img.onerror = (e) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = svg.width;
|
||||||
|
canvas.height = svg.height;
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
resolve(canvas.toDataURL("image/png"));
|
||||||
|
};
|
||||||
|
img.src = "data:image/svg+xml;base64," + btoa(svg.data);
|
||||||
|
});
|
||||||
|
const blob = await (await fetch(dataURL)).blob();
|
||||||
|
saveAs(blob, programsEl.value + ".png");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
img.remove();
|
||||||
|
savingModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function share(ev: MouseEvent) {
|
||||||
|
ev.preventDefault();
|
||||||
|
shareModalTitleEl.innerText = programsEl.value;
|
||||||
|
shareModalURLEl.value = `${window.location.protocol}//${
|
||||||
|
window.location.host
|
||||||
|
}${window.location.pathname}?pn=${encodeURIComponent(
|
||||||
|
programsEl.value
|
||||||
|
)}&p=${encodeURIComponent(btoa(editor.getValue()))}&ar=1`;
|
||||||
|
shareModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectShareURL() {
|
||||||
|
shareModalURLEl.focus();
|
||||||
|
shareModalURLEl.setSelectionRange(0, 9999);
|
||||||
|
shareModalURLEl.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
shareModalEl.addEventListener("shown.bs.modal", () => {
|
||||||
|
selectShareURL();
|
||||||
|
});
|
||||||
|
|
||||||
programsEl.addEventListener("change", (ev) => {
|
programsEl.addEventListener("change", (ev) => {
|
||||||
loadProgram((ev.target! as HTMLSelectElement).value);
|
loadProgram((ev.target! as HTMLSelectElement).value);
|
||||||
});
|
});
|
||||||
loadPrograms();
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@ -465,7 +679,12 @@ async function run() {
|
||||||
setRotation(-90 - stack.pop());
|
setRotation(-90 - stack.pop());
|
||||||
});
|
});
|
||||||
forth.interpret(thurtleFS);
|
forth.interpret(thurtleFS);
|
||||||
forth.onEmit = (c) => outputEl.appendChild(document.createTextNode(c));
|
forth.onEmit = (c) => {
|
||||||
|
outputEl.appendChild(document.createTextNode(c));
|
||||||
|
if (c === "\n") {
|
||||||
|
outputEl.scrollTop = outputEl.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
forth.interpret(editor.getValue());
|
forth.interpret(editor.getValue());
|
||||||
editor.focus();
|
editor.focus();
|
||||||
|
|
||||||
|
@ -477,14 +696,44 @@ async function run() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runButtonEl.addEventListener("click", () => run());
|
|
||||||
document.addEventListener("keydown", (ev) => {
|
document.addEventListener("keydown", (ev) => {
|
||||||
if (ev.key == "Enter" && (ev.metaKey || ev.ctrlKey)) {
|
if (ev.key == "Enter" && (ev.metaKey || ev.ctrlKey)) {
|
||||||
run();
|
run();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
////////////////////////////////////////////////////////////////////
|
||||||
draw();
|
|
||||||
|
|
||||||
loadProgram(DEFAULT_PROGRAM);
|
function parseQS(sqs = window.location.search) {
|
||||||
|
const qs: Record<string, string> = {};
|
||||||
|
const sqss = sqs
|
||||||
|
.substring(sqs.indexOf("?") + 1)
|
||||||
|
.replace(/\+/, " ")
|
||||||
|
.split("&");
|
||||||
|
for (const p of sqss) {
|
||||||
|
const j = p.indexOf("=");
|
||||||
|
if (j > 0) {
|
||||||
|
qs[decodeURIComponent(p.substring(0, j))] = decodeURIComponent(
|
||||||
|
p.substring(j + 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return qs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qs = parseQS();
|
||||||
|
if (qs.p) {
|
||||||
|
saveProgram(qs.pn ?? "", atob(qs.p), true);
|
||||||
|
loadPrograms();
|
||||||
|
loadProgram(qs.pn);
|
||||||
|
} else {
|
||||||
|
loadPrograms();
|
||||||
|
loadProgram(qs.pn ?? DEFAULT_PROGRAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qs.ar) {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
reset();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -31,6 +31,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||||
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
|
||||||
|
|
||||||
|
"@types/file-saver@^2.0.5":
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.5.tgz#9ee342a5d1314bb0928375424a2f162f97c310c7"
|
||||||
|
integrity sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==
|
||||||
|
|
||||||
"@types/node@^17.0.31":
|
"@types/node@^17.0.31":
|
||||||
version "17.0.31"
|
version "17.0.31"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d"
|
||||||
|
@ -639,6 +644,11 @@ file-entry-cache@^6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache "^3.0.4"
|
flat-cache "^3.0.4"
|
||||||
|
|
||||||
|
file-saver@^2.0.5:
|
||||||
|
version "2.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
|
||||||
|
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==
|
||||||
|
|
||||||
fill-range@^7.0.1:
|
fill-range@^7.0.1:
|
||||||
version "7.0.1"
|
version "7.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||||
|
|
Loading…
Add table
Reference in a new issue