vermiparous

This commit is contained in:
Theis Pieter Hollebeek 2025-10-10 21:28:06 +02:00
parent a4f2da333f
commit 2514cd3669
5 changed files with 85 additions and 54 deletions

View File

@ -14,6 +14,8 @@
<main> <main>
<div class="column" style="flex: 1"> <div class="column" style="flex: 1">
<div id="buttons"> <div id="buttons">
<button id="import-button">🙏 Import</button>
<a <a
href="docs/index.html" href="docs/index.html"
target="_blank" target="_blank"
@ -42,6 +44,12 @@
> >
Export as HTML Export as HTML
</div> </div>
<div
id="save-karlkoder"
class="dropdown-option"
>
Export as Karlkoder File
</div>
</div> </div>
</div> </div>

View File

@ -7,6 +7,8 @@ import { SpriteEditor } from "./sprite_editor.js";
import { SpriteProvider } from "./sprite_provider.js"; import { SpriteProvider } from "./sprite_provider.js";
import { Gamelib } from "./gamelib.js"; import { Gamelib } from "./gamelib.js";
import { CodeStopper } from "./code_stopper.js"; import { CodeStopper } from "./code_stopper.js";
import { Vermiparous } from "./vermiparous.js";
import { promptUpload } from "./prompt_upload.js";
const playgroundConsole = new PlaygroundConsole( const playgroundConsole = new PlaygroundConsole(
document.querySelector("#console-code"), document.querySelector("#console-code"),
@ -38,11 +40,13 @@ editor.session.setMode("ace/mode/javascript");
editor.setValue(sessionStorage.getItem("code") ?? editor.getValue(), -1); editor.setValue(sessionStorage.getItem("code") ?? editor.getValue(), -1);
const importButton = document.querySelector("#import-button");
const runButton = document.querySelector("#run-button"); const runButton = document.querySelector("#run-button");
const saveButton = document.querySelector("#save-button"); const saveButton = document.querySelector("#save-button");
const saveDropdown = document.querySelector("#save-dropdown"); const saveDropdown = document.querySelector("#save-dropdown");
const saveJsButton = document.querySelector("#save-js"); const saveJsButton = document.querySelector("#save-js");
const saveHtmlButton = document.querySelector("#save-html"); const saveHtmlButton = document.querySelector("#save-html");
const saveKarlkoderButton = document.querySelector("#save-karlkoder");
const toggleSpriteEditorButton = document.querySelector("#toggle-sprite-editor-button"); const toggleSpriteEditorButton = document.querySelector("#toggle-sprite-editor-button");
const sessionSaveDebounce = new Debounce(1000); const sessionSaveDebounce = new Debounce(1000);
@ -52,6 +56,17 @@ editor.addEventListener("change", (ev) => {
}); });
}); });
importButton.onclick = async (ev) => {
const files = await promptUpload("", false);
if (files.length === 0) {
return;
}
if (files.length > 1) {
throw new Error("unreachable: something went wrong !");
}
const item = Vermiparous.de(await fetch(URL.createObjectURL(files[0])).then((x) => x.bytes()));
};
runButton.onclick = (ev) => { runButton.onclick = (ev) => {
const code = editor.getValue(); const code = editor.getValue();
@ -148,6 +163,19 @@ ${js}
downloadFile(html, "text/html", ".html"); downloadFile(html, "text/html", ".html");
}; };
saveKarlkoderButton.onclick = (ev) => {
downloadFile(
Vermiparous.en(
editor.getValue()
.split("\n")
.map((line) => " ".repeat(12) + line).join("\n"),
spriteEditor.sprites,
),
"",
".karlkode",
);
};
toggleSpriteEditorButton.addEventListener("click", () => { toggleSpriteEditorButton.addEventListener("click", () => {
const cont = document.querySelector("#sprite-editor-container"); const cont = document.querySelector("#sprite-editor-container");
if (cont.style.getPropertyValue("display") === "none") { if (cont.style.getPropertyValue("display") === "none") {

19
src/prompt_upload.js Normal file
View File

@ -0,0 +1,19 @@
export function promptUpload(accept, multiple) {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.accept = accept;
input.multiple = multiple;
input.style = "display: none;";
input.addEventListener("cancel", () => {
resolve([]);
input.remove();
});
input.addEventListener("change", () => {
resolve(input.files);
input.remove();
});
document.body.append(input);
input.click();
});
}

View File

@ -1,3 +1,5 @@
import { promptUpload } from "./prompt_upload.js";
export class SpriteEditor { export class SpriteEditor {
constructor(rootEl, sprites) { constructor(rootEl, sprites) {
this.list = rootEl.querySelector("#sprite-editor-sprite-list"); this.list = rootEl.querySelector("#sprite-editor-sprite-list");
@ -13,39 +15,26 @@ export class SpriteEditor {
this.renderList(); this.renderList();
} }
promptUpload() { async promptUpload() {
const input = document.createElement("input"); for (const file of await promptUpload("image/*", true)) {
input.type = "file"; const rootName = file.name;
input.accept = "image/*"; let fullName = file.name;
input.multiple = true; for (let i = 0; this.sprites.some((x) => x.name === fullName); ++i) {
input.style = "display: none;"; const extensionIdx = rootName.split("").findLastIndex((x) => x === ".");
input.addEventListener("cancel", () => { let name = rootName;
input.remove(); let extension = "";
}); if (extensionIdx !== -1) {
input.addEventListener("change", async () => { name = rootName.slice(0, extensionIdx);
for (const file of input.files) { extension = rootName.slice(extensionIdx);
const rootName = file.name;
let fullName = file.name;
for (let i = 0; this.sprites.some((x) => x.name === fullName); ++i) {
const extensionIdx = rootName.split("").findLastIndex((x) => x === ".");
let name = rootName;
let extension = "";
if (extensionIdx !== -1) {
name = rootName.slice(0, extensionIdx);
extension = rootName.slice(extensionIdx);
}
fullName = `${name}-${i}${extension}`;
} }
this.addSprite({ fullName = `${name}-${i}${extension}`;
name: fullName,
mime: file.type,
bytes: await fetch(URL.createObjectURL(file)).then((x) => x.bytes()),
});
} }
input.remove(); this.addSprite({
}); name: fullName,
document.body.append(input); mime: file.type,
input.click(); bytes: await fetch(URL.createObjectURL(file)).then((x) => x.bytes()),
});
}
} }
addSprite({ name, bytes, mime }) { addSprite({ name, bytes, mime }) {

View File

@ -2,43 +2,30 @@ const KEYWORDS = ["asset", "code"];
const MIN_KW_LENGTH = Math.min(...KEYWORDS.map((x) => x.length)); const MIN_KW_LENGTH = Math.min(...KEYWORDS.map((x) => x.length));
const SEMICOLON_CHARCODE = ";".charCodeAt(0); const SEMICOLON_CHARCODE = ";".charCodeAt(0);
function isKeyword(buffer: number[]) { function isKeyword(buffer) {
const kw = String.fromCharCode(...buffer); const kw = buffer.map((x) => String.fromCharCode(x)).join("");
return KEYWORDS.includes(kw); return KEYWORDS.includes(kw);
} }
function strToBytes(str: string): number[] { function strToBytes(str) {
return str.split("").map((x) => x.charCodeAt(0)); return str.split("").map((x) => x.charCodeAt(0));
} }
type EncAsset = {
name: string;
mime: string;
content: Uint8Array;
};
type DecAssic = {
tag: "code" | "asset";
name: string;
mime: string;
content: Uint8Array;
};
export class Vermiparous { export class Vermiparous {
private constructor() {} static en(code, assets) {
static en(code: string, assets: EncAsset[]): Uint8Array { const ret = [];
const ret: number[] = [];
for (const asset of assets) { for (const asset of assets) {
console.log(asset);
ret.push(...strToBytes("asset")); ret.push(...strToBytes("asset"));
ret.push(...strToBytes(asset.name.length.toString())); ret.push(...strToBytes(asset.name.length.toString()));
ret.push(...strToBytes(";")); ret.push(...strToBytes(";"));
ret.push(...strToBytes(asset.mime.length.toString())); ret.push(...strToBytes(asset.mime.length.toString()));
ret.push(...strToBytes(";")); ret.push(...strToBytes(";"));
ret.push(...strToBytes(asset.content.length.toString())); ret.push(...strToBytes(asset.bytes.length.toString()));
ret.push(...strToBytes(";")); ret.push(...strToBytes(";"));
ret.push(...strToBytes(asset.name)); ret.push(...strToBytes(asset.name));
ret.push(...strToBytes(asset.mime)); ret.push(...strToBytes(asset.mime));
ret.push(...asset.content); ret.push(...asset.bytes);
} }
ret.push(...strToBytes("code")); ret.push(...strToBytes("code"));
ret.push(...strToBytes("0;0;")); ret.push(...strToBytes("0;0;"));
@ -48,7 +35,7 @@ export class Vermiparous {
return new Uint8Array(ret); return new Uint8Array(ret);
} }
static de(bytes: Uint8Array): DecAssic[] { static de(bytes) {
const ret = []; const ret = [];
let buffer = []; let buffer = [];
let idx = 0; let idx = 0;
@ -92,7 +79,7 @@ export class Vermiparous {
} }
const [name, mime, content] = consoom; const [name, mime, content] = consoom;
ret.push({ ret.push({
tag: String.fromCharCode(...tag) as DecAssic["tag"], tag: String.fromCharCode(...tag),
name: String.fromCharCode(...name), name: String.fromCharCode(...name),
mime: String.fromCharCode(...mime), mime: String.fromCharCode(...mime),
content: new Uint8Array(content), content: new Uint8Array(content),