replace sprite with asset

This commit is contained in:
Theis Pieter Hollebeek 2025-10-13 18:17:02 +02:00
parent 5bb2f25f91
commit 21126c5852
10 changed files with 138 additions and 145 deletions

View File

@ -220,7 +220,7 @@ lib.startGameLoop(function () {
`lib.drawSprite(x: number, y: number, width: number, height: number, name: string) -> void`
Draws a sprite imported in the sprite editor.
Draws a sprite imported in the asset editor.
/* todo: link to sprite editor docs */

View File

@ -115,25 +115,25 @@ lib.startGameLoop(loop);
</section>
</div>
<div class="column" id="sprite-editor-container" style="flex-grow: 0">
<div id="toggle-sprite-editor-button">
<div class="column" id="asset-editor-container" style="flex-grow: 0">
<div id="toggle-asset-editor-button">
&lsaquo;
</div>
<section id="sprite-editor">
<section id="asset-editor">
<div class="section-header">
Sprite editor
Asset editor
</div>
<div id="sprite-editor-preview">
<p id="sprite-editor-preview-title"></p>
<img id="sprite-editor-preview-image" src="">
<code id="sprite-editor-preview-snippet"></code>
<div id="asset-editor-preview">
<p id="asset-editor-preview-title"></p>
<img id="asset-editor-preview-image" src="">
<code id="asset-editor-preview-snippet"></code>
</div>
<p id="sprite-editor-sprite-list-title">Uploaded sprites</p>
<ul id="sprite-editor-sprite-list"></ul>
<button id="sprite-editor-upload-button">Upload new sprite</button>
<p id="asset-editor-asset-list-title">Uploaded assets</p>
<ul id="asset-editor-asset-list"></ul>
<button id="asset-editor-upload-button">Upload new asset</button>
</section>
</div>
</main>

View File

@ -1,13 +1,13 @@
import { promptUpload } from "./prompt_upload.js";
export class SpriteEditor {
export class AssetEditor {
constructor(rootEl) {
this.list = rootEl.querySelector("#sprite-editor-sprite-list");
this.editor = rootEl.querySelector("#sprite-editor");
this.toggleButton = rootEl.querySelector("#toggle-sprite-editor-button");
this.list = rootEl.querySelector("#asset-editor-asset-list");
this.editor = rootEl.querySelector("#asset-editor");
this.toggleButton = rootEl.querySelector("#toggle-asset-editor-button");
this.container = rootEl;
this.previewedId = null;
if (localStorage.getItem("sprite-editor-expanded")) {
if (localStorage.getItem("asset-editor-expanded")) {
this.editor.style.display = "block";
this.container.style.flexGrow = "1";
this.toggleButton.innerHTML = "&rsaquo;";
@ -15,22 +15,22 @@ export class SpriteEditor {
}
this.preview = {};
this.preview.title = rootEl.querySelector("#sprite-editor-preview-title");
this.preview.image = rootEl.querySelector("#sprite-editor-preview-image");
this.preview.snippet = rootEl.querySelector("#sprite-editor-preview-snippet");
this.sprites = [];
this.preview.title = rootEl.querySelector("#asset-editor-preview-title");
this.preview.image = rootEl.querySelector("#asset-editor-preview-image");
this.preview.snippet = rootEl.querySelector("#asset-editor-preview-snippet");
this.assets = [];
rootEl.querySelector("#sprite-editor-upload-button").addEventListener("click", () => {
rootEl.querySelector("#asset-editor-upload-button").addEventListener("click", () => {
this.promptUpload();
});
this.renderList();
}
importSprites(sprites) {
this.sprites = [];
for (const sprite of sprites) {
this.addSprite({ name: sprite.name, bytes: sprite.bytes, mime: sprite.mime });
importAssets(assets) {
this.assets = [];
for (const asset of assets) {
this.addAsset({ name: asset.name, bytes: asset.bytes, mime: asset.mime });
}
this.renderList();
}
@ -39,7 +39,7 @@ export class SpriteEditor {
for (const file of await promptUpload("image/*", true)) {
const rootName = file.name;
let fullName = file.name;
for (let i = 0; this.sprites.some((x) => x.name === fullName); ++i) {
for (let i = 0; this.assets.some((x) => x.name === fullName); ++i) {
const extensionIdx = rootName.split("").findLastIndex((x) => x === ".");
let name = rootName;
let extension = "";
@ -49,7 +49,7 @@ export class SpriteEditor {
}
fullName = `${name}-${i}${extension}`;
}
this.addSprite({
this.addAsset({
name: fullName,
mime: file.type,
bytes: await fetch(URL.createObjectURL(file)).then((x) => x.bytes()),
@ -57,24 +57,24 @@ export class SpriteEditor {
}
}
addSprite({ name, bytes, mime }) {
addAsset({ name, bytes, mime }) {
const id = Math.round(Math.random() * 1e6);
this.sprites.push({ id, bytes, name, mime });
this.assets.push({ id, bytes, name, mime });
this.renderList();
}
deleteSprite(id) {
this.sprites = this.sprites.filter((x) => x.id !== id);
deleteAsset(id) {
this.assets = this.assets.filter((x) => x.id !== id);
this.renderList();
}
setPreview(id) {
this.previewedId = id;
const sprite = this.sprites.find((x) => x.id === id);
this.preview.title.textContent = sprite.name;
this.preview.image.src = `data:${sprite.mime};base64,${sprite.bytes.toBase64()}`;
const asset = this.assets.find((x) => x.id === id);
this.preview.title.textContent = asset.name;
this.preview.image.src = `data:${asset.mime};base64,${asset.bytes.toBase64()}`;
const sanitizedName = sprite.name.replace(/</g, "&lt;").replace(/&/g, "&amp;");
const sanitizedName = asset.name.replace(/</g, "&lt;").replace(/&/g, "&amp;");
this.preview.snippet.innerHTML = "lib.drawSprite(<br>" +
" x, y,<br>" +
" width, height,<br>" +
@ -89,11 +89,11 @@ export class SpriteEditor {
}
renderList() {
const children = this.sprites
.map((sprite) => {
const children = this.assets
.map((asset) => {
const listItem = document.createElement("li");
listItem.classList.add("sprite-editor-list-item");
if (this.previewedId === sprite.id) {
listItem.classList.add("asset-editor-list-item");
if (this.previewedId === asset.id) {
listItem.setAttribute("previewed", true);
}
listItem.addEventListener("click", () => {
@ -101,10 +101,10 @@ export class SpriteEditor {
li.removeAttribute("previewed");
}
listItem.setAttribute("previewed", true);
this.setPreview(sprite.id);
this.setPreview(asset.id);
});
const name = document.createElement("span");
name.textContent = sprite.name;
name.textContent = asset.name;
listItem.append(name);
const deleteButton = document.createElement("button");
deleteButton.innerHTML = "&times;";
@ -114,7 +114,7 @@ export class SpriteEditor {
if (listItem.getAttribute("previewed")) {
this.clearPreview();
}
this.deleteSprite(sprite.id);
this.deleteAsset(asset.id);
});
listItem.append(deleteButton);
return listItem;
@ -124,18 +124,18 @@ export class SpriteEditor {
}
toggleEditor() {
if (!localStorage.getItem("sprite-editor-expanded")) {
if (!localStorage.getItem("asset-editor-expanded")) {
this.editor.style.display = "block";
this.container.style.flexGrow = "1";
this.toggleButton.innerHTML = "&rsaquo;";
this.toggleButton.setAttribute("expanded", true);
localStorage.setItem("sprite-editor-expanded", true);
localStorage.setItem("asset-editor-expanded", true);
} else {
this.editor.style.display = "none";
this.container.style.flexGrow = "0";
this.toggleButton.innerHTML = "&lsaquo;";
this.toggleButton.removeAttribute("expanded");
localStorage.removeItem("sprite-editor-expanded");
localStorage.removeItem("asset-editor-expanded");
}
}
}

27
src/asset_provider.js Normal file
View File

@ -0,0 +1,27 @@
export class AssetProvider {
constructor() {
this.assets = [];
}
injectAssets(assets) {
this.assets = assets;
}
url(name) {
const asset = this.assets.find((x) => x.name === name);
if (!asset) {
throw new Error(`Asset with name '${name}' does not exist`);
}
return `data:${asset.mime};base64,${asset.bytes.toBase64()}`;
}
getAll() {
const result = {};
for (const asset of this.assets) {
result[asset.name] = this.url(asset.name);
}
return result;
}
}

View File

@ -3,10 +3,10 @@ export class Gamelib {
keyPressHandlers = new Map();
keyReleaseHandlers = new Map();
constructor(console, codeStopper, spriteProvider, canvasElement) {
constructor(console, codeStopper, assetProvider, canvasElement) {
this.console = console;
this.codeStopper = codeStopper;
this.spriteProvider = spriteProvider;
this.assetProvider = assetProvider;
this.canvas = canvasElement;
this.width = 480;
@ -80,7 +80,7 @@ export class Gamelib {
const cx = this.cx;
const image = new Image();
image.src = this.spriteProvider.url(name);
image.src = this.assetProvider.url(name);
image.onload = () => {
cx.drawImage(image, x, y, width, height);
};

View File

@ -1,27 +1,3 @@
async function asyncIdbRequest(req) {
return await new Promise((resolve, reject) => {
req.onerror = () => {
reject(req.error);
};
req.onsuccess = () => {
resolve(req.result);
};
});
}
const Idb = new Proxy(globalThis.indexedDB, {
get(target, prop, _receiver) {
if (typeof target[prop] === "function") {
return (...args) => {
const req = target[prop](...args);
return asyncIdbRequest(req);
};
}
return Reflect.get(...arguments);
},
});
export class Gesundheit {
static #isInternalConstructing = false;
static #idb = globalThis.indexedDB;
@ -33,8 +9,25 @@ export class Gesundheit {
Gesundheit.#isInternalConstructing = false;
}
static async load(name) {
const db = await Idb.open(name);
console.log(db);
static load(name) {
this.#idb.deleteDatabase(name);
const req = this.#idb.open(name);
req.onerror = () => {
console.log("error", req.error);
};
req.onupgradeneeded = () => {
const db = req.result;
const objectStore = db.createObjectStore("asset", {
keyPath: "filename",
});
console.log("upgrade pls", db);
};
req.onsuccess = () => {
console.log("success", req.result);
};
}
}

View File

@ -1,8 +1,8 @@
import { minifyJs } from "./utils.js";
export class HtmlExporter {
constructor(spriteProvider) {
this.spriteProvider = spriteProvider;
constructor(assetProvider) {
this.assetProvider = assetProvider;
}
async export(projectName, code) {
@ -13,7 +13,7 @@ export class HtmlExporter {
let lib = await (await fetch("./src/gamelib.js")).text();
lib = minifyJs(lib);
const sprites = this.spriteProvider.getAll();
const assets = this.assetProvider.getAll();
const html = `
<!DOCTYPE html>
@ -41,15 +41,15 @@ export class HtmlExporter {
<script type="module">
import { Gamelib } from "gamelib";
const sprites = ${JSON.stringify(sprites)}
const assets = ${JSON.stringify(assets)}
class SpriteProvider {
class AssetProvider {
url(name) {
return sprites[name];
return assets[name];
}
}
const gamelib = new Gamelib(console, null, new SpriteProvider(), document.querySelector('canvas'));
const gamelib = new Gamelib(console, null, new AssetProvider(), document.querySelector('canvas'));
window.karlkoder = {
lib() {

View File

@ -3,8 +3,8 @@
import { Debounce } from "./debounce.js";
import { PlaygroundConsole } from "./playground_console.js";
import { CodeRunner } from "./code_runner.js";
import { SpriteEditor } from "./sprite_editor.js";
import { SpriteProvider } from "./sprite_provider.js";
import { AssetEditor } from "./asset_editor.js";
import { AssetProvider } from "./asset_provider.js";
import { Gamelib } from "./gamelib.js";
import { CodeStopper } from "./code_stopper.js";
import { Vermiparous } from "./vermiparous.js";
@ -37,25 +37,25 @@ const playgroundConsole = new PlaygroundConsole(
editor,
);
window.addEventListener("DOMContentLoaded", () => {
addEventListener("DOMContentLoaded", () => {
playgroundConsole.log("Karlkode 1.0");
});
const codeStopper = new CodeStopper();
const spriteProvider = new SpriteProvider();
const assetProvider = new AssetProvider();
const codeRunner = new CodeRunner(playgroundConsole, codeStopper);
const spriteEditor = new SpriteEditor(document.querySelector("#sprite-editor-container"), []);
const assetEditor = new AssetEditor(document.querySelector("#asset-editor-container"), []);
new ConsoleInput(document.querySelector("#console-input"), playgroundConsole, codeRunner);
const htmlExporter = new HtmlExporter(spriteProvider);
const htmlExporter = new HtmlExporter(assetProvider);
const gamelib = new Gamelib(
playgroundConsole,
codeStopper,
spriteProvider,
assetProvider,
document.querySelector("canvas"),
);
@ -66,7 +66,7 @@ globalThis.karlkoder = {
};
const runButton = document.querySelector("#run-button");
const toggleSpriteEditorButton = document.querySelector("#toggle-sprite-editor-button");
const toggleAssetEditorButton = document.querySelector("#toggle-asset-editor-button");
const projectName = document.querySelector("#project-name");
const saveButton = document.querySelector("#save-button");
const loadButton = document.querySelector("#load-button");
@ -90,7 +90,7 @@ loadButton.onclick = async () => {
);
}
const items = Vermiparous.de(await fetch(URL.createObjectURL(files[0])).then((x) => x.bytes()));
const sprites = items
const assets = items
.filter((x) => x.tag === "asset")
.map((x) => {
delete x.tag;
@ -98,8 +98,8 @@ loadButton.onclick = async () => {
});
const code = items.find((x) => x.tag === "code");
delete code.tag;
spriteEditor.importSprites(
sprites.map(({ name, mime, content }) => ({ name, mime, bytes: content })),
assetEditor.importAssets(
assets.map(({ name, mime, content }) => ({ name, mime, bytes: content })),
);
projectName.value = code.name;
const dec = new TextDecoder();
@ -109,7 +109,7 @@ loadButton.onclick = async () => {
runButton.onclick = () => {
const code = editor.getValue();
karlkoder.lib().spriteProvider.injectSprites(spriteEditor.sprites);
karlkoder.lib().assetProvider.injectAssets(assetEditor.assets);
codeRunner.setCode(code);
codeRunner.toggle();
document.querySelector("canvas").focus();
@ -135,7 +135,7 @@ function saveKarlKoder() {
Vermiparous.en(
projectName.value,
editor.getValue(),
spriteEditor.sprites,
assetEditor.assets,
),
".karlkode",
);
@ -150,6 +150,6 @@ addEventListener("keydown", (ev) => {
}
});
toggleSpriteEditorButton.addEventListener("click", () => spriteEditor.toggleEditor());
toggleAssetEditorButton.addEventListener("click", () => assetEditor.toggleEditor());
Gesundheit.load("spritedb");
Gesundheit.load("assetdb");

View File

@ -1,27 +0,0 @@
export class SpriteProvider {
constructor() {
this.sprites = [];
}
injectSprites(sprites) {
this.sprites = sprites;
}
url(name) {
const sprite = this.sprites.find((x) => x.name === name);
if (!sprite) {
throw new Error(`Sprite with name '${name}' does not exist`);
}
return `data:${sprite.mime};base64,${sprite.bytes.toBase64()}`;
}
getAll() {
const result = {};
for (const sprite of this.sprites) {
result[sprite.name] = this.url(sprite.name);
}
return result;
}
}

View File

@ -284,7 +284,7 @@ footer {
left: 1px;
}
#toggle-sprite-editor-button {
#toggle-asset-editor-button {
color: white;
cursor: pointer;
font-size: 32px;
@ -293,40 +293,40 @@ footer {
border-radius: 0.5rem;
}
#toggle-sprite-editor-button[expanded] {
#toggle-asset-editor-button[expanded] {
background-color: #242424;
border-radius: 0.5rem 0 0 0.5rem;
}
#sprite-editor {
#asset-editor {
background-color: #242424;
display: none;
}
#sprite-editor-container {
#asset-editor-container {
flex-direction: row;
align-items: stretch;
gap: 0;
min-width: min-content;
}
#sprite-editor-preview {
#asset-editor-preview {
display: flex;
flex-direction: column;
margin-bottom: 2rem;
}
#sprite-editor-preview-title {
#asset-editor-preview-title {
margin: 1rem;
overflow-wrap: break-word;
font-weight: bold;
}
#sprite-editor-preview-title:empty {
#asset-editor-preview-title:empty {
display: none;
}
#sprite-editor-preview-image {
#asset-editor-preview-image {
background-color: #1d2021;
object-fit: contain;
width: 100%;
@ -334,12 +334,12 @@ footer {
max-height: min(40vh, 400px);
}
#sprite-editor-sprite-list-title {
#asset-editor-asset-list-title {
font-weight: bold;
text-align: center;
}
#sprite-editor-sprite-list {
#asset-editor-asset-list {
margin: 0;
padding: 0;
display: flex;
@ -349,12 +349,12 @@ footer {
list-style-type: none;
}
#sprite-editor-sprite-list:not(:empty) {
#asset-editor-asset-list:not(:empty) {
border-top: 1px solid #131313;
border-bottom: 1px solid #131313;
}
#sprite-editor-sprite-list li {
#asset-editor-asset-list li {
display: flex;
cursor: pointer;
background-color: #242424;
@ -365,29 +365,29 @@ footer {
gap: 1rem;
}
#sprite-editor-sprite-list li:hover, #sprite-editor-sprite-list li[previewed] {
#asset-editor-asset-list li:hover, #asset-editor-asset-list li[previewed] {
background-color: #131313;
}
#sprite-editor-sprite-list .delete-button {
#asset-editor-asset-list .delete-button {
background-color: #c83e4d;
padding: 0.25rem 0.75rem;
}
#sprite-editor-sprite-list .delete-button:hover {
#asset-editor-asset-list .delete-button:hover {
background-color: #9f2d3a;
}
#sprite-editor-sprite-list .delete-button:active {
#asset-editor-asset-list .delete-button:active {
background-color: #7f242f;
}
#sprite-editor-upload-button {
#asset-editor-upload-button {
display: block;
margin: 1rem auto;
}
#sprite-editor-preview-snippet {
#asset-editor-preview-snippet {
background-color: #1d2021;
border: 1px solid black;
border-radius: 0.25rem;
@ -396,11 +396,11 @@ footer {
white-space: pre;
}
#sprite-editor-preview-snippet .str {
#asset-editor-preview-snippet .str {
color: #b8ba37;
}
#sprite-editor-preview-snippet:empty {
#asset-editor-preview-snippet:empty {
display: none;
}