From f2b5978cae84cad5e2d2bc56aebff0bfb72dacf6 Mon Sep 17 00:00:00 2001 From: dylan <> Date: Fri, 5 May 2023 14:59:52 -0700 Subject: [PATCH] Starting on sprite editor --- cart.ts | 3 +- cart_unpacked.json | 37 +++++++- codetab.ts | 222 +++++++++++++++++++++++++++++++++++++++++++++ colors.ts | 2 + editmode.ts | 215 +++---------------------------------------- sheet.ts | 19 +++- spritetab.ts | 64 +++++++++++++ 7 files changed, 352 insertions(+), 210 deletions(-) create mode 100644 codetab.ts create mode 100644 spritetab.ts diff --git a/cart.ts b/cart.ts index 29adc11..f8fab9b 100644 --- a/cart.ts +++ b/cart.ts @@ -1,6 +1,7 @@ import fakeCart from "./cart_unpacked.json" assert { type: "json" }; +import { Sheet } from "./sheet.ts"; -const cart = fakeCart; +const cart = fakeCart as Array; export const loadCart = (_name: string) => { return; diff --git a/cart_unpacked.json b/cart_unpacked.json index 9a21964..7f9c82b 100644 --- a/cart_unpacked.json +++ b/cart_unpacked.json @@ -5,6 +5,41 @@ }, { "sheet_type": "code", - "value": "speed = 2; return (8)" + "value": "speed = 2;\nreturn 8;" + }, + { + "sheet_type": "spritesheet", + "value": [ + [ + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 2, 2, 2, 2, 2, 2, 2 + ], + [ + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 1, 1, 3, 3, 2, + 2, 3, 3, 1, 1, 3, 3, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 1, 1, 1, 1, 1, 1, 2, + 2, 3, 3, 1, 1, 3, 3, 2, + 2, 3, 3, 1, 1, 3, 3, 2, + 2, 2, 2, 2, 2, 2, 2, 2 + ], + [ + 2, 2, 2, 2, 2, 2, 2, 2, + 2, 4, 4, 4, 4, 5, 5, 2, + 2, 4, 4, 4, 5, 5, 5, 2, + 2, 4, 4, 5, 5, 5, 6, 2, + 2, 4, 5, 5, 5, 6, 6, 2, + 2, 5, 5, 5, 6, 6, 6, 2, + 2, 5, 5, 6, 6, 6, 6, 2, + 2, 2, 2, 2, 2, 2, 2, 2 + ] + ] } ] \ No newline at end of file diff --git a/codetab.ts b/codetab.ts new file mode 100644 index 0000000..0bb8df3 --- /dev/null +++ b/codetab.ts @@ -0,0 +1,222 @@ +import { clearScreen, fillRect } from "./window.ts"; +import { fontWidth, fontHeight } from "./font.ts"; +import { drawText } from "./builtins.ts"; +import { COLOR } from "./colors.ts"; +import {getSheet, setSheet} from "./sheet.ts"; +import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts"; + +// TODO: Make scrolling work +const state = { + scrollX: 0, + scrollY: 0, + anchor: 0, + focus: 0, + get focusX() {return indexToGrid(this.code, this.focus).x;}, + get focusY() {return indexToGrid(this.code, this.focus).y;}, + get anchorX() {return indexToGrid(this.code, this.anchor).x;}, + get anchorY() {return indexToGrid(this.code, this.anchor).y;}, + isCollapsed() { + return this.anchor === this.focus; + }, + clampInRange(n: number) { + return Math.max(0, Math.min(n, this.code.length)) + }, + setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) { + if (typeof anchor !== "number") { + anchor = gridToIndex(this.code, anchor.x, anchor.y); + } + focus = focus ?? anchor; + if (typeof focus !== "number") { + focus = gridToIndex(this.code, focus.x, focus.y); + } + this.anchor = this.clampInRange(anchor), + this.focus = this.clampInRange(focus); + }, + setFocus(focus: number | {x: number, y: number}) { + if (typeof focus !== "number") { + focus = gridToIndex(this.code, focus.x, focus.y); + } + this.focus = this.clampInRange(focus); + }, + insertText(text: string) { + const {code, anchor, focus} = this; + this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus)); + this.setSelection(Math.min(anchor, focus) + text.length); + }, + indent(indentString: string) { + const lines = this.code.split("\n"); + const {focusX, focusY, anchorX, anchorY} = this; + const newLines = lines.map((line, i) => { + if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) { + return indentString+line; + } else { + return line; + } + }); + this.code = newLines.join("\n"); + this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY}); + }, + outdent(outdentRegex: RegExp) { + const lines = this.code.split("\n"); + const {focusX, focusY, anchorX, anchorY} = this; + const newLines = lines.map((line, i) => { + const match = line.match(outdentRegex); + if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) { + return line.slice(match[0].length); + } else { + return line; + } + }); + this.code = newLines.join("\n"); + this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY}); + }, + backspace() { + const {code, focus} = this; + if (this.isCollapsed()) { + if (focus > 0) { + this.code = code.slice(0, focus-1) + code.slice(focus); + this.setSelection(focus-1); + } + } else { + this.insertText(""); + } + }, + delete() { + const {code, focus} = this; + if (this.isCollapsed()) { + if (focus < code.length) { + this.code = code.slice(0, focus) + code.slice(1+focus); + } + } else { + this.insertText(""); + } + }, + get code() { + const {sheet_type, value} = getSheet(0); + if (sheet_type !== "code") { + throw "Trying to run a non-code sheet as code." + } + return value; + }, + set code(val) { + setSheet(0, "code", val); + } +} + +const indexToGrid = (str: string, index: number) => { + const linesUpTo = str.slice(0,index).split("\n"); + return { + x: linesUpTo[linesUpTo.length-1].length, + y: linesUpTo.length - 1, + } +} + +const gridToIndex = (str: string, x: number, y: number) => { + const lines = str.split("\n"); + if (y < 0) { + return 0; + } + if (y >= lines.length) { + return str.length; + } + return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1; +} + +const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => { + const { + scrollX, + scrollY, + anchor, + focus, + } = state; + const { + x: focusX, + y: focusY, + } = indexToGrid(code, focus); + const { + x: anchorX, + y: anchorY, + } = indexToGrid(code, anchor); + fillRect(x, y, w, h, COLOR.DARKBLUE); + if (anchor === focus) { + fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED); + } else { + // TODO: Draw this selection better + fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN); + fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW); + } + // TODO: Add syntax highlighting use "npm:js-tokens" maybe? + code.split("\n").forEach((line, i) => { + drawText(x-scrollX, 1+y+i*(fontHeight+1)-scrollY, line); + }); +} + +const update = () => { + const { focus, focusX, focusY} = state; + const keyboardString = getKeyboardString(); + if (keyboardString) { + state.insertText(keyboardString); + } + // TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z + // TODO: Make ctrl-/ do commenting out (take inspiration from tab) + + if (keyPressed(K.ENTER)) { + // TODO: Make this play nicely with indentation + state.insertText("\n"); + } + if (keyPressed(K.TAB)) { + if (!shiftKeyDown()) { + if (state.isCollapsed()) { + state.insertText("\t"); + } else { + state.indent("\t"); + } + } else { + state.outdent(/^(\t| )/); + } + } + if (keyPressed(K.BACKSPACE)) { + state.backspace(); + } + if (keyPressed(K.DELETE)) { + state.delete(); + } + if (keyPressed(K.ARROW_RIGHT)) { + if (shiftKeyDown()) { + state.setFocus(focus+1); + } else { + state.setSelection(focus+1); + } + } + if (keyPressed(K.ARROW_LEFT)) { + if (shiftKeyDown()) { + state.setFocus(focus-1); + } else { + state.setSelection(focus-1); + } + } + if (keyPressed(K.ARROW_DOWN)) { + if (shiftKeyDown()) { + state.setFocus({x: focusX, y: focusY+1}); + } else { + state.setSelection({x: focusX, y: focusY+1}); + } + } + if (keyPressed(K.ARROW_UP)) { + if (shiftKeyDown()) { + state.setFocus({x: focusX, y: focusY-1}); + } else { + state.setSelection({x: focusX, y: focusY-1}); + } + } +} + +const draw = () => { + clearScreen(); + drawCodeField(state.code, 0, 8, 128, 112); +} + +export const codetab = { + update, + draw, +} \ No newline at end of file diff --git a/colors.ts b/colors.ts index 0eb935f..9e781b1 100644 --- a/colors.ts +++ b/colors.ts @@ -1,4 +1,5 @@ const colors = { + TRANSPARENT: [0, 0, 0], BLACK: [0, 0, 0], WHITE: [1, 1, 1], RED: [1, 0, 0], @@ -6,6 +7,7 @@ const colors = { GREEN: [0, 1, 0], BLUE: [0, 0, 1], DARKBLUE: [0.1, 0.05, 0.4], + BROWN: [0.6, 0.5, 0.4], } as const; export const palette: Array<[number, number, number, number]> = Object.values(colors).map(val => [...val, 1]); diff --git a/editmode.ts b/editmode.ts index 0b41990..27ff2b9 100644 --- a/editmode.ts +++ b/editmode.ts @@ -1,221 +1,28 @@ import { clearScreen, fillRect } from "./window.ts"; -import { fontWidth, fontHeight } from "./font.ts"; -import { drawText } from "./builtins.ts"; +import { codetab } from "./codetab.ts"; +import { spritetab } from "./spritetab.ts"; import { COLOR } from "./colors.ts"; -import {getSheet, setSheet} from "./sheet.ts"; -import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts"; // deno-lint-ignore prefer-const -let tab: "code" | "sprite" | "map" | "sfx" | "music" = "code"; - -// TODO: Make scrolling work -const codeTabState = { - scrollX: 0, - scrollY: 0, - anchor: 0, - focus: 0, - get focusX() {return indexToGrid(this.code, this.focus).x;}, - get focusY() {return indexToGrid(this.code, this.focus).y;}, - get anchorX() {return indexToGrid(this.code, this.anchor).x;}, - get anchorY() {return indexToGrid(this.code, this.anchor).y;}, - isCollapsed() { - return this.anchor === this.focus; - }, - clampInRange(n: number) { - return Math.max(0, Math.min(n, this.code.length)) - }, - setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) { - if (typeof anchor !== "number") { - anchor = gridToIndex(this.code, anchor.x, anchor.y); - } - focus = focus ?? anchor; - if (typeof focus !== "number") { - focus = gridToIndex(this.code, focus.x, focus.y); - } - this.anchor = this.clampInRange(anchor), - this.focus = this.clampInRange(focus); - }, - setFocus(focus: number | {x: number, y: number}) { - if (typeof focus !== "number") { - focus = gridToIndex(this.code, focus.x, focus.y); - } - this.focus = this.clampInRange(focus); - }, - insertText(text: string) { - const {code, anchor, focus} = this; - this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus)); - this.setSelection(Math.min(anchor, focus) + text.length); - }, - indent(indentString: string) { - const lines = this.code.split("\n"); - const {focusX, focusY, anchorX, anchorY} = this; - const newLines = lines.map((line, i) => { - if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) { - return indentString+line; - } else { - return line; - } - }); - this.code = newLines.join("\n"); - this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY}); - }, - outdent(outdentRegex: RegExp) { - const lines = this.code.split("\n"); - const {focusX, focusY, anchorX, anchorY} = this; - const newLines = lines.map((line, i) => { - const match = line.match(outdentRegex); - if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) { - return line.slice(match[0].length); - } else { - return line; - } - }); - this.code = newLines.join("\n"); - this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY}); - }, - backspace() { - const {code, focus} = this; - if (this.isCollapsed()) { - if (focus > 0) { - this.code = code.slice(0, focus-1) + code.slice(focus); - this.setSelection(focus-1); - } - } else { - this.insertText(""); - } - }, - delete() { - const {code, focus} = this; - if (this.isCollapsed()) { - if (focus < code.length) { - this.code = code.slice(0, focus) + code.slice(1+focus); - } - } else { - this.insertText(""); - } - }, - get code() { - return getSheet(0); - }, - set code(val) { - setSheet(0, "code", val); - } -} - -const indexToGrid = (str: string, index: number) => { - const linesUpTo = str.slice(0,index).split("\n"); - return { - x: linesUpTo[linesUpTo.length-1].length, - y: linesUpTo.length - 1, - } -} - -const gridToIndex = (str: string, x: number, y: number) => { - const lines = str.split("\n"); - if (y < 0) { - return 0; - } - if (y >= lines.length) { - return str.length; - } - return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1; -} - -const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => { - const { - scrollX, - scrollY, - anchor, - focus, - } = codeTabState; - const { - x: focusX, - y: focusY, - } = indexToGrid(code, focus); - const { - x: anchorX, - y: anchorY, - } = indexToGrid(code, anchor); - fillRect(x, y, w, h, COLOR.DARKBLUE); - if (anchor === focus) { - fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED); - } else { - // TODO: Draw this selection better - fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN); - fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW); - } - code.split("\n").forEach((line, i) => { - drawText(x-scrollX, 1+y+i*(fontHeight+1)-scrollY, line); - }); -} +let tab: "code" | "sprite" | "map" | "sfx" | "music" = "sprite"; const update = () => { if (tab === "code") { - const { focus, focusX, focusY} = codeTabState; - const keyboardString = getKeyboardString(); - if (keyboardString) { - codeTabState.insertText(keyboardString); - } - // TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z - // TODO: Make ctrl-/ do commenting out (take inspiration from tab) - - if (keyPressed(K.ENTER)) { - // TODO: Make this play nicely with indentation - codeTabState.insertText("\n"); - } - if (keyPressed(K.TAB)) { - if (!shiftKeyDown()) { - if (codeTabState.isCollapsed()) { - codeTabState.insertText("\t"); - } else { - codeTabState.indent("\t"); - } - } else { - codeTabState.outdent(/^(\t| )/); - } - } - if (keyPressed(K.BACKSPACE)) { - codeTabState.backspace(); - } - if (keyPressed(K.DELETE)) { - codeTabState.delete(); - } - if (keyPressed(K.ARROW_RIGHT)) { - if (shiftKeyDown()) { - codeTabState.setFocus(focus+1); - } else { - codeTabState.setSelection(focus+1); - } - } - if (keyPressed(K.ARROW_LEFT)) { - if (shiftKeyDown()) { - codeTabState.setFocus(focus-1); - } else { - codeTabState.setSelection(focus-1); - } - } - if (keyPressed(K.ARROW_DOWN)) { - if (shiftKeyDown()) { - codeTabState.setFocus({x: focusX, y: focusY+1}); - } else { - codeTabState.setSelection({x: focusX, y: focusY+1}); - } - } - if (keyPressed(K.ARROW_UP)) { - if (shiftKeyDown()) { - codeTabState.setFocus({x: focusX, y: focusY-1}); - } else { - codeTabState.setSelection({x: focusX, y: focusY-1}); - } - } + codetab.update(); + } else if (tab === "sprite") { + spritetab.update(); } } const draw = () => { clearScreen(); if (tab === "code") { - drawCodeField(getSheet(0), 0, 8, 128, 112); + codetab.draw(); + } else if (tab === "sprite") { + spritetab.draw(); } + fillRect(0, 0, 128, 8, COLOR.RED); + fillRect(0, 120, 128, 8, COLOR.RED); } export const editmode = { diff --git a/sheet.ts b/sheet.ts index 95df41c..3995fd2 100644 --- a/sheet.ts +++ b/sheet.ts @@ -1,10 +1,18 @@ import { getCart } from "./cart.ts"; import { runCode, addToContext } from "./runcode.ts"; -export type SheetType = "code" | "spritesheet" | "map" | "sfx" | "patterns" | "fonts"; +// "code" | "spritesheet" | "map" | "sfx" | "patterns" | "fonts" +export type Sheet = { + sheet_type: "code", + value: string, +} | { + sheet_type: "spritesheet", + value: Array>, +} +export type SheetType = Sheet["sheet_type"]; export const getSheet = (n: number) => { - return getCart()[n].value; + return getCart()[n]; } // deno-lint-ignore no-explicit-any @@ -13,8 +21,11 @@ export const setSheet = (n: number, type: SheetType, value: any) => { } export const codeSheet = (sheet: number) => { - const code = getSheet(sheet); - return runCode(code); + const {sheet_type, value} = getSheet(sheet); + if (sheet_type !== "code") { + throw "Trying to run a non-code sheet as code." + } + return runCode(value); } addToContext("code_sheet", codeSheet); \ No newline at end of file diff --git a/spritetab.ts b/spritetab.ts new file mode 100644 index 0000000..77365e5 --- /dev/null +++ b/spritetab.ts @@ -0,0 +1,64 @@ +import { clearScreen, fillRect, setPixelColor } from "./window.ts"; +import { fontWidth, fontHeight } from "./font.ts"; +import { drawText, drawSprite } from "./builtins.ts"; +import { COLOR } from "./colors.ts"; +import {getSheet, setSheet} from "./sheet.ts"; + +const state = { + selectedIndex: 0, + get sprites() { + const {sheet_type, value} = getSheet(2); + if (sheet_type !== "spritesheet") { + throw "Trying to use a non-sprite sheet as a spritesheet." + } + return value; + }, + set sprites(val) { + setSheet(0, "spritesheet", val); + } +} + +const update = () => { +} + +const draw = () => { + const {sprites, selectedIndex} = state; + clearScreen(); + fillRect(0, 8, 128, 112, COLOR.BROWN); + // Draw the palette + const paletteX = 88; + const paletteY = 12; + fillRect(paletteX-1, paletteY-1, 32+2, 32+2, COLOR.BLACK); + Object.keys(COLOR).forEach((name, i) => { + const swatchX = paletteX+8*(i%4); + const swatchY = paletteY+8*Math.floor(i/4); + fillRect(swatchX, swatchY, 8, 8, COLOR[name as keyof typeof COLOR]); + if (i === 0) { + // transparent + Array(64).fill(0).map((_z, j) => { + const jx = j%8; + const jy = Math.floor(j/8); + setPixelColor(swatchX+jx, swatchY+jy, (jx+jy)%2 ? COLOR.BLACK : COLOR.WHITE); + }) + } + }); + // Draw the current sprite + const spriteX = 8; + const spriteY = 12; + fillRect(spriteX-1, spriteY-1, 64+2, 64+2, COLOR.BLACK); + sprites[selectedIndex].forEach((pix, i) => { + fillRect(spriteX+8*(i%8), spriteY+8*Math.floor(i/8), 8, 8, pix); + }); + // Draw the spritesheet + const sheetX = 0; + const sheetY = 88; + fillRect(sheetX, sheetY-1, 128, 64+1, COLOR.BLACK); + sprites.forEach((_sprite, i) => { + drawSprite(sheetX+8*(i%16), sheetY+8*Math.floor(i/16), i); + }); +} + +export const spritetab = { + update, + draw, +} \ No newline at end of file