honu
This commit is contained in:
commit
6e065febaf
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.DS_Store
|
||||||
71
README.md
Normal file
71
README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# 🐢 Honu
|
||||||
|
|
||||||
|
LOGO-style turtle graphics powered by Shrimp.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:3002
|
||||||
|
|
||||||
|
This builds the app and serves the `dist/` folder with Python's built-in HTTP server.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs a bundled browser app to `dist/`. The `dist/` folder contains:
|
||||||
|
|
||||||
|
- `index.html` - The main page
|
||||||
|
- `app.js` - Bundled JavaScript (Parser + Compiler + ReefVM + CodeMirror)
|
||||||
|
|
||||||
|
You can serve the `dist/` folder with any static file server, or even open `index.html` directly in a browser.
|
||||||
|
|
||||||
|
## LOGO Commands
|
||||||
|
|
||||||
|
### Movement
|
||||||
|
- `forward n` - Move forward n pixels
|
||||||
|
- `back n` - Move backward n pixels
|
||||||
|
- `left n` - Turn left n degrees
|
||||||
|
- `right n` - Turn right n degrees
|
||||||
|
|
||||||
|
### Pen Control
|
||||||
|
- `penup` - Lift pen (don't draw)
|
||||||
|
- `pendown` - Lower pen (draw)
|
||||||
|
- `setcolor n` - Set pen color (0-15)
|
||||||
|
- `setwidth n` - Set pen width
|
||||||
|
- `clearscreen` - Clear the canvas
|
||||||
|
|
||||||
|
### Turtle Control
|
||||||
|
- `home` - Return to center, heading up
|
||||||
|
- `setheading n` - Set heading to n degrees
|
||||||
|
- `setpos x y` - Set position to (x, y)
|
||||||
|
- `position` - Get current position
|
||||||
|
|
||||||
|
### Control Flow
|
||||||
|
- `repeat n do: ... end` - Repeat commands n times
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- `print value` - Print to console
|
||||||
|
- `wait n` - Wait n milliseconds
|
||||||
|
- `stop` - Stop execution
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Write Shrimp/LOGO code in the editor
|
||||||
|
2. Press **Run** (or Cmd+Enter) to execute
|
||||||
|
3. Watch the turtle draw on the canvas
|
||||||
|
|
||||||
|
Example - Draw a square:
|
||||||
|
|
||||||
|
```shrimp
|
||||||
|
repeat 4 do:
|
||||||
|
forward 100
|
||||||
|
right 90
|
||||||
|
end
|
||||||
|
```
|
||||||
60
bun.lock
Normal file
60
bun.lock
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "fin",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.11.3",
|
||||||
|
"@codemirror/view": "^6.38.3",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"shrimp": "git+https://git.nose.space/probablycorey/shrimp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.19.1", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-q6NenYkEy2fn9+JyjIxMWcNjzTL/IhwqfzOut1/G3PrIFkrbl4AL7Wkse5tLrQUUyqGoAKU5+Pi5jnnXxH5HGw=="],
|
||||||
|
|
||||||
|
"@codemirror/commands": ["@codemirror/commands@6.10.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w=="],
|
||||||
|
|
||||||
|
"@codemirror/language": ["@codemirror/language@6.11.3", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.1.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA=="],
|
||||||
|
|
||||||
|
"@codemirror/lint": ["@codemirror/lint@6.9.2", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", "crelt": "^1.0.5" } }, "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ=="],
|
||||||
|
|
||||||
|
"@codemirror/search": ["@codemirror/search@6.5.11", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "crelt": "^1.0.5" } }, "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA=="],
|
||||||
|
|
||||||
|
"@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="],
|
||||||
|
|
||||||
|
"@codemirror/view": ["@codemirror/view@6.38.6", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw=="],
|
||||||
|
|
||||||
|
"@lezer/common": ["@lezer/common@1.3.0", "", {}, "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ=="],
|
||||||
|
|
||||||
|
"@lezer/generator": ["@lezer/generator@1.8.0", "", { "dependencies": { "@lezer/common": "^1.1.0", "@lezer/lr": "^1.3.0" }, "bin": { "lezer-generator": "src/lezer-generator.cjs" } }, "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg=="],
|
||||||
|
|
||||||
|
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
|
||||||
|
|
||||||
|
"@lezer/lr": ["@lezer/lr@1.4.3", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA=="],
|
||||||
|
|
||||||
|
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
|
||||||
|
|
||||||
|
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.15", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-qtAXMNGG4R0UGGI8zWrqm2B7BdXqx48vunJXBPzfDOHPA5WkRUZdTSbE7TFwO4jLhYqSE23YMWsM9NhE6ovobw=="],
|
||||||
|
|
||||||
|
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
|
||||||
|
|
||||||
|
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
||||||
|
|
||||||
|
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#0f39e9401eb7a0a7c906e150127f9829458a79b6", { "peerDependencies": { "typescript": "^5" } }, "0f39e9401eb7a0a7c906e150127f9829458a79b6"],
|
||||||
|
|
||||||
|
"shrimp": ["shrimp@git+https://git.nose.space/probablycorey/shrimp#d707ee7e6b074cc0d64179004e5b6cc8250e0c91", { "dependencies": { "@codemirror/view": "^6.38.3", "@lezer/generator": "^1.8.0", "bun-plugin-tailwind": "^0.0.15", "codemirror": "^6.0.2", "hono": "^4.9.8", "reefvm": "git+https://git.nose.space/defunkt/reefvm", "tailwindcss": "^4.1.11" } }, "d707ee7e6b074cc0d64179004e5b6cc8250e0c91"],
|
||||||
|
|
||||||
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
|
"tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
152
index.html
Normal file
152
index.html
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>🐢 Honu</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3e3e42;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #4ec9b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #858585;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-right: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #0e639c;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: #1177bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
background: #0d5689;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #858585;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
color: #4ec9b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
color: #f48771;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<h1>🐢 Honu</h1>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="run-btn">Run (Cmd+Enter)</button>
|
||||||
|
<button id="clear-btn">Clear</button>
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.875rem;">
|
||||||
|
<input type="checkbox" id="slow-mode" style="cursor: pointer;">
|
||||||
|
Slow Draw
|
||||||
|
</label>
|
||||||
|
<div class="status" id="status"></div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="container">
|
||||||
|
<div class="left-pane">
|
||||||
|
<div class="editor-container" id="editor"></div>
|
||||||
|
</div>
|
||||||
|
<div class="right-pane" id="right-pane" style="position: relative;">
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
<canvas id="turtle-canvas" style="position: absolute; top: 0; left: 0; pointer-events: none;"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="dist/app.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "fin",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun build src/app.ts --outdir=dist --target=browser && cp index.html dist/index.html && sed -i '' 's|src=\"dist/app.js\"|src=\"app.js\"|g' dist/index.html",
|
||||||
|
"serve": "cd dist && python3 -m http.server 3002",
|
||||||
|
"dev": "bun run build && bun run serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/language": "^6.11.3",
|
||||||
|
"@codemirror/view": "^6.38.3",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"shrimp": "git+https://git.nose.space/probablycorey/shrimp"
|
||||||
|
}
|
||||||
|
}
|
||||||
318
src/app.ts
Normal file
318
src/app.ts
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
import { EditorView, keymap } from '@codemirror/view'
|
||||||
|
import { EditorState, Prec } from '@codemirror/state'
|
||||||
|
import { defaultKeymap } from '@codemirror/commands'
|
||||||
|
import { basicSetup } from 'codemirror'
|
||||||
|
import { LRLanguage, LanguageSupport } from '@codemirror/language'
|
||||||
|
import { styleTags, tags } from '@lezer/highlight'
|
||||||
|
import { runCode as runShrimpCode, parser } from 'shrimp'
|
||||||
|
|
||||||
|
const highlighting = styleTags({
|
||||||
|
Identifier: tags.name,
|
||||||
|
Number: tags.number,
|
||||||
|
String: tags.string,
|
||||||
|
Boolean: tags.bool,
|
||||||
|
keyword: tags.keyword,
|
||||||
|
end: tags.keyword,
|
||||||
|
':': tags.keyword,
|
||||||
|
Null: tags.keyword,
|
||||||
|
Regex: tags.regexp,
|
||||||
|
Operator: tags.operator,
|
||||||
|
Word: tags.variableName,
|
||||||
|
Command: tags.function(tags.variableName),
|
||||||
|
'Params/Identifier': tags.definition(tags.variableName),
|
||||||
|
Paren: tags.paren,
|
||||||
|
})
|
||||||
|
|
||||||
|
const language = LRLanguage.define({
|
||||||
|
parser: parser.configure({ props: [highlighting] }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const shrimpLanguage = new LanguageSupport(language)
|
||||||
|
|
||||||
|
const STORAGE_KEY_CODE = 'logo-turtle-code'
|
||||||
|
const STORAGE_KEY_SLOW_MODE = 'logo-turtle-slow-mode'
|
||||||
|
|
||||||
|
const DEFAULT_CODE = `# Welcome to Fin - LOGO Turtle Graphics!
|
||||||
|
# Draw a square
|
||||||
|
|
||||||
|
repeat 4 do:
|
||||||
|
forward 100
|
||||||
|
right 90
|
||||||
|
end`
|
||||||
|
|
||||||
|
const loadSavedCode = (): string => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY_CODE)
|
||||||
|
return saved !== null ? saved : DEFAULT_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveCode = (code: string) => {
|
||||||
|
localStorage.setItem(STORAGE_KEY_CODE, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCommand = {
|
||||||
|
key: 'Mod-Enter',
|
||||||
|
run: () => {
|
||||||
|
runCode()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const startState = EditorState.create({
|
||||||
|
doc: loadSavedCode(),
|
||||||
|
extensions: [
|
||||||
|
basicSetup,
|
||||||
|
shrimpLanguage,
|
||||||
|
Prec.highest(keymap.of([runCommand])),
|
||||||
|
keymap.of(defaultKeymap),
|
||||||
|
EditorView.updateListener.of((update) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
saveCode(update.state.doc.toString())
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const view = new EditorView({
|
||||||
|
state: startState,
|
||||||
|
parent: document.getElementById('editor')!,
|
||||||
|
})
|
||||||
|
|
||||||
|
// UI elements
|
||||||
|
const canvas = document.getElementById('canvas') as HTMLCanvasElement
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
|
const turtleCanvas = document.getElementById('turtle-canvas') as HTMLCanvasElement
|
||||||
|
const turtleCtx = turtleCanvas.getContext('2d')!
|
||||||
|
const statusEl = document.getElementById('status')!
|
||||||
|
const slowModeCheckbox = document.getElementById('slow-mode') as HTMLInputElement
|
||||||
|
|
||||||
|
// Set canvas size to fill container
|
||||||
|
const resizeCanvas = () => {
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
canvas.width = rect.width
|
||||||
|
canvas.height = rect.height
|
||||||
|
turtleCanvas.width = rect.width
|
||||||
|
turtleCanvas.height = rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
resizeCanvas()
|
||||||
|
window.addEventListener('resize', resizeCanvas)
|
||||||
|
|
||||||
|
const showStatus = (text: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
statusEl.textContent = text
|
||||||
|
statusEl.className = `status ${type}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlowMode = (): boolean => {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY_SLOW_MODE)
|
||||||
|
return saved === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
let slowMode = loadSlowMode()
|
||||||
|
slowModeCheckbox.checked = slowMode
|
||||||
|
|
||||||
|
slowModeCheckbox.addEventListener('change', (e) => {
|
||||||
|
slowMode = (e.target as HTMLInputElement).checked
|
||||||
|
localStorage.setItem(STORAGE_KEY_SLOW_MODE, slowMode.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
const maybeWait = async () => {
|
||||||
|
if (slowMode) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Turtle {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
heading: number // degrees
|
||||||
|
penDown: boolean
|
||||||
|
color: string
|
||||||
|
lineWidth: number
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.x = canvas.width / 2
|
||||||
|
this.y = canvas.height / 2
|
||||||
|
this.heading = 0 // pointing up
|
||||||
|
this.penDown = true
|
||||||
|
this.color = '#000000'
|
||||||
|
this.lineWidth = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
async forward(distance: number) {
|
||||||
|
const radians = (this.heading - 90) * Math.PI / 180
|
||||||
|
const newX = this.x + distance * Math.cos(radians)
|
||||||
|
const newY = this.y + distance * Math.sin(radians)
|
||||||
|
|
||||||
|
if (this.penDown) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(this.x, this.y)
|
||||||
|
ctx.lineTo(newX, newY)
|
||||||
|
ctx.strokeStyle = this.color
|
||||||
|
ctx.lineWidth = this.lineWidth
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.x = newX
|
||||||
|
this.y = newY
|
||||||
|
|
||||||
|
if (slowMode) {
|
||||||
|
this.drawTurtle()
|
||||||
|
}
|
||||||
|
await maybeWait()
|
||||||
|
}
|
||||||
|
|
||||||
|
async back(distance: number) {
|
||||||
|
await this.forward(-distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
async left(degrees: number) {
|
||||||
|
this.heading -= degrees
|
||||||
|
if (slowMode) {
|
||||||
|
this.drawTurtle()
|
||||||
|
}
|
||||||
|
await maybeWait()
|
||||||
|
}
|
||||||
|
|
||||||
|
async right(degrees: number) {
|
||||||
|
this.heading += degrees
|
||||||
|
if (slowMode) {
|
||||||
|
this.drawTurtle()
|
||||||
|
}
|
||||||
|
await maybeWait()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTurtle() {
|
||||||
|
// Clear turtle layer
|
||||||
|
turtleCtx.clearRect(0, 0, turtleCanvas.width, turtleCanvas.height)
|
||||||
|
|
||||||
|
// Draw turtle on separate layer
|
||||||
|
turtleCtx.save()
|
||||||
|
turtleCtx.translate(this.x, this.y)
|
||||||
|
turtleCtx.rotate((this.heading - 90) * Math.PI / 180)
|
||||||
|
turtleCtx.font = '24px sans-serif'
|
||||||
|
turtleCtx.textAlign = 'center'
|
||||||
|
turtleCtx.textBaseline = 'middle'
|
||||||
|
turtleCtx.fillText('🐢', 0, 0)
|
||||||
|
turtleCtx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
penup() {
|
||||||
|
this.penDown = false
|
||||||
|
}
|
||||||
|
|
||||||
|
pendown() {
|
||||||
|
this.penDown = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setcolor(color: number) {
|
||||||
|
// Simple color palette (0-15)
|
||||||
|
const colors = [
|
||||||
|
'#000000', '#FF0000', '#00FF00', '#0000FF',
|
||||||
|
'#FFFF00', '#FF00FF', '#00FFFF', '#FFFFFF',
|
||||||
|
'#800000', '#008000', '#000080', '#808000',
|
||||||
|
'#800080', '#008080', '#808080', '#C0C0C0'
|
||||||
|
]
|
||||||
|
this.color = colors[color % colors.length]!
|
||||||
|
}
|
||||||
|
|
||||||
|
setwidth(width: number) {
|
||||||
|
this.lineWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
clearscreen() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
turtleCtx.clearRect(0, 0, turtleCanvas.width, turtleCanvas.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
home() {
|
||||||
|
this.x = canvas.width / 2
|
||||||
|
this.y = canvas.height / 2
|
||||||
|
this.heading = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setheading(degrees: number) {
|
||||||
|
this.heading = degrees
|
||||||
|
}
|
||||||
|
|
||||||
|
setpos(x: number, y: number) {
|
||||||
|
this.x = x + canvas.width / 2
|
||||||
|
this.y = canvas.height / 2 - y
|
||||||
|
}
|
||||||
|
|
||||||
|
position(): [number, number] {
|
||||||
|
return [
|
||||||
|
this.x - canvas.width / 2,
|
||||||
|
canvas.height / 2 - this.y
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let turtle = new Turtle()
|
||||||
|
|
||||||
|
const globals = {
|
||||||
|
forward: async (n: number) => await turtle.forward(n),
|
||||||
|
back: async (n: number) => await turtle.back(n),
|
||||||
|
left: async (n: number) => await turtle.left(n),
|
||||||
|
right: async (n: number) => await turtle.right(n),
|
||||||
|
penup: () => turtle.penup(),
|
||||||
|
pendown: () => turtle.pendown(),
|
||||||
|
setcolor: (n: number) => turtle.setcolor(n),
|
||||||
|
setwidth: (n: number) => turtle.setwidth(n),
|
||||||
|
clearscreen: () => turtle.clearscreen(),
|
||||||
|
home: () => turtle.home(),
|
||||||
|
setheading: (n: number) => turtle.setheading(n),
|
||||||
|
setpos: (x: number, y: number) => turtle.setpos(x, y),
|
||||||
|
position: () => turtle.position(),
|
||||||
|
repeat: async (n: number, fn: Function) => {
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
await fn(i)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
print: (value: any) => console.log(value),
|
||||||
|
wait: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
|
||||||
|
stop: () => { throw new Error('STOP') },
|
||||||
|
end: () => null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCode = async () => {
|
||||||
|
const code = view.state.doc.toString()
|
||||||
|
|
||||||
|
try {
|
||||||
|
showStatus('Parsing...', 'info')
|
||||||
|
|
||||||
|
const tree = parser.parse(code)
|
||||||
|
|
||||||
|
if (tree.cursor().node.type.isError) {
|
||||||
|
throw new Error('Parse error in code')
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('Compiling...', 'info')
|
||||||
|
|
||||||
|
turtle = new Turtle()
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
await runShrimpCode(code, globals)
|
||||||
|
|
||||||
|
showStatus('Running...', 'info')
|
||||||
|
|
||||||
|
turtle.drawTurtle()
|
||||||
|
|
||||||
|
showStatus('✓ Success', 'success')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
console.error('Error:', message)
|
||||||
|
showStatus(`✗ ${message}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('run-btn')!.addEventListener('click', runCode)
|
||||||
|
document.getElementById('clear-btn')!.addEventListener('click', () => {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
turtleCtx.clearRect(0, 0, turtleCanvas.width, turtleCanvas.height)
|
||||||
|
turtle = new Turtle()
|
||||||
|
showStatus('')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initial focus
|
||||||
|
view.focus()
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["bun"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user