This commit is contained in:
Theis Pieter Hollebeek 2025-10-10 11:20:19 +02:00
parent 61050f58d9
commit bd689c285c
8 changed files with 267 additions and 226 deletions

3
deno.jsonc Normal file
View File

@ -0,0 +1,3 @@
{
"fmt": { "indentWidth": 4 }
}

View File

@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Karlkoder Docs</title> <title>Karlkoder Docs</title>
</head> </head>
<body> <body>
<h1>Docs</h1> <h1>Docs</h1>
</body> </body>
</html> </html>

View File

@ -1,67 +1,97 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta
name="viewport"
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> content="width=device-width, initial-scale=1.0"
<title>Karlkoder Playground</title> >
<link rel="stylesheet" href="./style.css">
<script type="importmap">
{
"imports": {
"lib": "./lib.js"
}
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.2/ace.js"></script>
<script src="./index.js" type="module"></script>
</head>
<body>
<main>
<div class="column" style="flex: 1"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<div id="buttons"> <title>Karlkoder Playground</title>
<a href="docs/index.html" target="_blank"> <link rel="stylesheet" href="./style.css">
<button>🤓 Docs</button> <script type="importmap">
</a> {
"imports": {
"lib": "./lib.js"
}
}
</script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.43.2/ace.js"
></script>
<script src="./index.js" type="module"></script>
</head>
<body>
<main>
<div class="column" style="flex: 1">
<div id="buttons">
<a
href="docs/index.html"
target="_blank"
>
<button>🤓 Docs</button>
</a>
<div class="dropdown-wrapper"> <div class="dropdown-wrapper">
<button id="save-button">💾 Save/Export</button> <button id="save-button">
💾 Save/Export
</button>
<div id="save-dropdown" class="dropdown-content"> <div
<div id="save-js" class="dropdown-option">Save JavaScript</div> id="save-dropdown"
<div id="save-html" class="dropdown-option">Export as HTML</div> class="dropdown-content"
</div> >
</div> <div
id="save-js"
class="dropdown-option"
>
Save JavaScript
</div>
<div
id="save-html"
class="dropdown-option"
>
Export as HTML
</div>
</div>
</div>
<button id="run-button">▶️ Run</button> <button id="run-button">▶️ Run</button>
</div> </div>
<section> <section>
<div class="section-header">Output</div> <div class="section-header">Output</div>
<canvas id="canvas-area"> <canvas id="canvas-area"> </canvas>
</canvas> </section>
</section>
<section> <section>
<div class="section-header">Console</div> <div class="section-header">
Console
</div>
<div id="console"> <div id="console">
<pre><code id="console-code">Karlkode 1.0</code></pre> <pre><code id="console-code">Karlkode 1.0</code></pre>
<input type="text" placeholder=">"> <input
</div> type="text"
</section> placeholder=">"
</div> >
</div>
</section>
</div>
<div class="column" style="flex: 3"> <div class="column" style="flex: 3">
<section>
<div class="section-header">
Code editor
</div>
<section> <div id="editor-area">
<div class="section-header">Code editor</div> <pre
id="editor"
<div id="editor-area"> >
<pre id="editor">import * as lib from "lib"; import * as lib from "lib";
lib.clear("green"); lib.clear("green");
@ -80,17 +110,20 @@ function loop(deltaT) {
lib.startGameLoop(loop); lib.startGameLoop(loop);
</pre> </pre
</div> >
</section> </div>
</div> </section>
</div>
</main>
<footer>
Copyright © 2025 S. F. Jakobsen (do whatever idc)
</footer>
</main> <!-- fix flash bug in furry browser -->
<script>
<footer>Copyright © 2025 S. F. Jakobsen (do whatever idc)</footer> 0;
</script>
<!-- fix flash bug in furry browser --> </body>
<script>0</script>
</body>
</html> </html>

View File

@ -3,7 +3,9 @@
async function runCode(code, consoleCode) { async function runCode(code, consoleCode) {
consoleCode.textContent += `\nRunning code....\n`; consoleCode.textContent += `\nRunning code....\n`;
try { try {
const module = await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`); const module = await import(
`data:text/javascript;charset=utf-8,${encodeURIComponent(code)}`
);
module.default?.(); module.default?.();
} catch (error) { } catch (error) {
consoleCode.textContent += `${error}\n`; consoleCode.textContent += `${error}\n`;
@ -11,7 +13,7 @@ async function runCode(code, consoleCode) {
} }
class Debounce { class Debounce {
timer = null timer = null;
lastCall = 0; lastCall = 0;
constructor(timeout) { constructor(timeout) {
@ -33,11 +35,10 @@ class Debounce {
fn(); fn();
this.lastCall = Date.now(); this.lastCall = Date.now();
this.timer = null; this.timer = null;
}, this.timeout) }, this.timeout);
} }
} }
const editor = ace.edit("editor"); const editor = ace.edit("editor");
editor.setTheme("ace/theme/gruvbox"); editor.setTheme("ace/theme/gruvbox");
editor.session.setMode("ace/mode/javascript"); editor.session.setMode("ace/mode/javascript");
@ -54,9 +55,9 @@ const consoleCode = document.querySelector("#console-code");
const sessionSaveDebounce = new Debounce(1000); const sessionSaveDebounce = new Debounce(1000);
editor.addEventListener("change", (ev) => { editor.addEventListener("change", (ev) => {
sessionSaveDebounce.run(() => { sessionSaveDebounce.run(() => {
sessionStorage.setItem("code", editor.getValue()) sessionStorage.setItem("code", editor.getValue());
}) });
}) });
runButton.onclick = (ev) => { runButton.onclick = (ev) => {
const code = editor.getValue(); const code = editor.getValue();
@ -70,7 +71,9 @@ function downloadFile(content, mime, extension) {
const element = document.createElement("a"); const element = document.createElement("a");
element.href = `data:${mime};charset=utf-8,${encodeURIComponent(content)}`; element.href = `data:${mime};charset=utf-8,${encodeURIComponent(content)}`;
element.download = filename.endsWith(extension) ? filename : filename + extension; element.download = filename.endsWith(extension)
? filename
: filename + extension;
element.style.display = "none"; element.style.display = "none";
document.body.appendChild(element); document.body.appendChild(element);
@ -81,33 +84,35 @@ function downloadFile(content, mime, extension) {
} }
function minifyJs(code) { function minifyJs(code) {
return code return code
.replace(/[\s\n]+/g, " ") .replace(/[\s\n]+/g, " ")
.replace(/;\s+/g, ";"); .replace(/;\s+/g, ";");
} }
saveButton.onclick = (ev) => { saveButton.onclick = (ev) => {
if (saveButton.classList.contains("active")) { if (saveButton.classList.contains("active")) {
saveButton.classList.remove("active"); saveButton.classList.remove("active");
saveDropdown.style.display = "none";
} else {
saveButton.classList.add("active");
saveDropdown.style.display = "block"; saveDropdown.style.display = "none";
} } else {
saveButton.classList.add("active");
saveDropdown.style.display = "block";
}
}; };
saveJsButton.onclick = (ev) => { saveJsButton.onclick = (ev) => {
downloadFile(editor.getValue(), "text/javascript", ".js"); downloadFile(editor.getValue(), "text/javascript", ".js");
}; };
saveHtmlButton.onclick = async (ev) => { saveHtmlButton.onclick = async (ev) => {
const js = editor.getValue().split("\n").map(line => " ".repeat(12) + line).join("\n"); const js = editor.getValue().split("\n").map((line) =>
let lib = await (await fetch("./lib.js")).text(); " ".repeat(12) + line
lib = minifyJs(lib); ).join("\n");
let lib = await (await fetch("./lib.js")).text();
lib = minifyJs(lib);
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -139,6 +144,5 @@ ${js}
</body> </body>
</html>`; </html>`;
downloadFile(html, "text/html", ".html"); downloadFile(html, "text/html", ".html");
}; };

9
lib.js
View File

@ -24,10 +24,11 @@ export function drawRect(x, y, width, height, color) {
} }
export function println(msg) { export function println(msg) {
if (consoleCode) if (consoleCode) {
consoleCode.textContent += `${msg}\n`; consoleCode.textContent += `${msg}\n`;
else } else {
console.log(msg); console.log(msg);
}
} }
export function startGameLoop(loopFunction) { export function startGameLoop(loopFunction) {

32
package-lock.json generated
View File

@ -1,19 +1,19 @@
{ {
"name": "karlkoder-playground", "name": "karlkoder-playground",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"@types/ace": "^0.0.52" "@types/ace": "^0.0.52"
} }
}, },
"node_modules/@types/ace": { "node_modules/@types/ace": {
"version": "0.0.52", "version": "0.0.52",
"resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.52.tgz", "resolved": "https://registry.npmjs.org/@types/ace/-/ace-0.0.52.tgz",
"integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==", "integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}
} }
}
} }

View File

@ -1,5 +1,5 @@
{ {
"devDependencies": { "devDependencies": {
"@types/ace": "^0.0.52" "@types/ace": "^0.0.52"
} }
} }

210
style.css
View File

@ -1,182 +1,182 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
color-scheme: light dark; color-scheme: light dark;
} }
body { body {
margin: 0; margin: 0;
height: 100vh; height: 100vh;
background-color: light-dark(#EEE, #333); background-color: light-dark(#eee, #333);
} }
main { main {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
height: 100vh; height: 100vh;
gap: 32px; gap: 32px;
} }
.section-header { .section-header {
background-color: white; background-color: white;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-top-right-radius: 8px; border-top-right-radius: 8px;
border: 1px solid light-dark(#BDBDBD, #222); border: 1px solid light-dark(#bdbdbd, #222);
padding: 4px; padding: 4px;
padding-left: 8px; padding-left: 8px;
font-family: sans-serif; font-family: sans-serif;
color: #747474; color: #747474;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1); box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
} }
section { section {
border-radius: 8px; border-radius: 8px;
width: 100%; width: 100%;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: black; background-color: black;
} }
section canvas { section canvas {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
section *:last-child { section *:last-child {
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px; border-bottom-right-radius: 8px;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1); box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1);
flex: 1; flex: 1;
} }
div#buttons { div#buttons {
width: 100%; width: 100%;
display: flex; display: flex;
flex: row; flex: row;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
gap: 5px; gap: 5px;
} }
div#buttons > * { div#buttons > * {
width: 100%; width: 100%;
} }
div#buttons > * > button { div#buttons > * > button {
width: 100%; width: 100%;
} }
.column { .column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
justify-content: space-evenly; justify-content: space-evenly;
align-items: flex-start; align-items: flex-start;
padding: 32px; padding: 32px;
gap: 32px; gap: 32px;
height: 100vh; height: 100vh;
flex: 1; flex: 1;
} }
#canvas-area { #canvas-area {
background-color: black; background-color: black;
width: 480px; width: 480px;
height: 360px; height: 360px;
} }
#console { #console {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#console pre { #console pre {
color: white; color: white;
background-color: black; background-color: black;
margin: 0; margin: 0;
width: 100%; width: 100%;
overflow-wrap: break-word; overflow-wrap: break-word;
word-break: break-all; word-break: break-all;
font-size: 1rem; font-size: 1rem;
padding: 4px 8px; padding: 4px 8px;
flex: 1; flex: 1;
} }
#console pre { #console pre {
width: 100%; width: 100%;
white-space: pre-wrap; white-space: pre-wrap;
} }
#console input { #console input {
width: 100%; width: 100%;
margin: 0; margin: 0;
outline: none; outline: none;
padding: none; padding: none;
color: white; color: white;
border: none; border: none;
background-color: #222; background-color: #222;
padding: 4px 8px; padding: 4px 8px;
font-family: monospace; font-family: monospace;
font-size: 1rem; font-size: 1rem;
flex-grow: 0; flex-grow: 0;
} }
#editor-area { #editor-area {
width: 100%; width: 100%;
} }
#editor { #editor {
margin: 0; margin: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
font-size: 16px; font-size: 16px;
} }
.dropdown-wrapper { .dropdown-wrapper {
position: relative; position: relative;
} }
.dropdown-content { .dropdown-content {
display: none; display: none;
background-color: white; background-color: white;
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
right: 0; right: 0;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1);
} }
.dropdown-option { .dropdown-option {
padding: 5px 10px; padding: 5px 10px;
font-family: sans-serif; font-family: sans-serif;
color: #424242; color: #424242;
font-size: 0.8rem; font-size: 0.8rem;
cursor: pointer; cursor: pointer;
} }
.dropdown-option:hover { .dropdown-option:hover {
background-color: #EEE; background-color: #eee;
} }
button { button {
background-color: #4FC3F7; background-color: #4fc3f7;
border: none; border: none;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1); box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1);
padding: 8px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition-duration: 200ms; transition-duration: 200ms;
} }
button.active { button.active {
background-color: #0288D1; background-color: #0288d1;
} }
button:hover { button:hover {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.2);
} }
footer { footer {
position: absolute; position: absolute;
bottom: 1px; bottom: 1px;
left: 1px; left: 1px;
} }