diff --git a/editor/canvas.js b/editor/canvas.js new file mode 100644 index 0000000..c089fec --- /dev/null +++ b/editor/canvas.js @@ -0,0 +1,255 @@ +import { + world, + select, + running, + play, + step, + pause, + selected, +} from "./index.js"; + +export const canvas = document.getElementById("main-canvas"); +export const ctx = canvas.getContext("2d"); + +// let world = World.new(); +let cx = 0; +let cy = 0; +let zoom = 1; +let grid = true; + +let click_x = 0, click_y = 0; +let mouse_x = 0, mouse_y = 0; +let mouse_down = false; +let hovered = false; + +export function draw() { + let zoom_factor = Math.ceil(Math.pow(2, zoom)); + let tile_size = 10 * zoom_factor; + ctx.fillStyle = "#202027"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + function stroke_rect(x, y, width, height) { + ctx.lineWidth = zoom_factor; + ctx.strokeRect( + Math.round(x * tile_size + cx + canvas.width / 2) - zoom_factor * 1.5, + Math.round(y * tile_size + cy + canvas.height / 2) - zoom_factor * 1.5, + Math.round(width * tile_size) + zoom_factor * 2, + Math.round(height * tile_size) + zoom_factor * 2, + ); + } + + function fill_rect(x, y, width, height) { + ctx.fillRect( + Math.round(x * tile_size + cx + canvas.width / 2) - zoom_factor * 1, + Math.round(y * tile_size + cy + canvas.height / 2) - zoom_factor * 1, + Math.round(width * tile_size) + zoom_factor * 1, + Math.round(height * tile_size) + zoom_factor * 1, + ); + } + + let panes = []; + + for (let name of world.panes()) { + let pane = world.get_pane(name); + + panes.push({ + x: pane.x, + y: pane.y, + width: pane.width, + height: pane.height + }); + + ctx.strokeStyle = "rgba(128, 128, 128, 0.5)"; + stroke_rect( + pane.x, + pane.y, + pane.width, + pane.height, + ); + + pane.free(); + } + + let from_y = Math.floor((-cy - canvas.height / 2) / tile_size); + let to_y = Math.ceil((-cy + canvas.height / 2) / tile_size); + let from_x = Math.floor((-cx - canvas.width / 2) / tile_size); + let to_x = Math.ceil((-cx + canvas.width / 2) / tile_size); + + let width = to_x - from_x + 1; + let height = to_y - from_y + 1; + + // TODO: memoize + let chars = world.draw(-from_x, -from_y, width, height); + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + ctx.font = `${tile_size}px Stackline Classic`; + + for (let y = from_y; y <= to_y; y++) { + for (let x = from_x; x <= to_x; x++) { + let x2 = x * tile_size + cx + canvas.width / 2; + let y2 = y * tile_size + cy + canvas.height / 2; + + let index = 3 * (width * (y - from_y) + x - from_x); + + let ch = String.fromCharCode(chars[index]); + if (ch !== " ") { + let fg = to_color(chars[index + 1]); + let bg = chars[index + 2] === 0 ? null : to_color(chars[index + 2]); + + if (bg) { + ctx.fillStyle = `rgba(${bg.join(",")})`; + ctx.fillRect( + Math.round(x2), + Math.round(y2), + Math.round(x2 + tile_size) - Math.round(x2), + Math.round(y2 + tile_size) - Math.round(y2), + ); + } + ctx.fillStyle = `rgba(${fg.join(",")})`; + ctx.fillText( + ch, + Math.round(x2), + Math.round(y2), + ); + } else if (grid) { + // Draw grid + let inside = false; + + for (let pane of panes) { + if (x >= pane.x && x < pane.x + pane.width && y >= pane.y && y < pane.y + pane.height) { + inside = true; + break; + } + } + + if (inside || width < 100 && height < 100) { + ctx.fillStyle = inside ? "#404040" : "#303030"; + + ctx.fillRect( + Math.round(x2 + tile_size / 2) - zoom_factor, + Math.round(y2 + tile_size / 2) - zoom_factor, + zoom_factor, + zoom_factor + ); + } + } + } + } + + if (hovered) { + let [x, y] = get_hovered(); + + ctx.strokeStyle = "rgba(230, 255, 230, 0.2)"; + ctx.fillStyle = "rgba(230, 255, 230, 0.07)"; + stroke_rect(x, y, 1, 1); + fill_rect(x, y, 1, 1); + } + + if (selected) { + let [x, y] = selected; + ctx.strokeStyle = "rgba(230, 230, 255, 0.1)"; + stroke_rect(x, y, 1, 1); + } +} + +export function resize() { + canvas.width = canvas.clientWidth; + canvas.height = canvas.clientHeight; + + draw(); +} + +export function loop() { + draw(); + window.requestAnimationFrame(loop); +} + +export function init() { + canvas.addEventListener("mousedown", (evt) => { + click_x = mouse_x = evt.clientX - canvas.offsetLeft; + click_y = mouse_y = evt.clientY - canvas.offsetTop; + mouse_down = true; + }); + + canvas.addEventListener("mousemove", (evt) => { + hovered = true; + + if (mouse_down) { + // TODO: numerically stable solution + cx += (evt.clientX - canvas.offsetLeft) - mouse_x; + cy += (evt.clientY - canvas.offsetTop) - mouse_y; + } + + mouse_x = evt.clientX - canvas.offsetLeft; + mouse_y = evt.clientY - canvas.offsetTop; + }); + + canvas.addEventListener("mouseup", (evt) => { + mouse_down = false; + + let dist = Math.sqrt((mouse_x - click_x) ** 2 + (mouse_y - click_y) ** 2); + if (dist < 10) { + select(...get_hovered()); + } + }); + + canvas.addEventListener("mouseenter", (evt) => { + hovered = true; + }); + + canvas.addEventListener("mouseleave", (evt) => { + mouse_down = false; + hovered = false; + }); + + canvas.addEventListener("wheel", (event) => { + const ZOOM_STRENGTH = -0.005; + let old_zoom = zoom; + zoom += event.deltaY * ZOOM_STRENGTH; + zoom = Math.min(Math.max(zoom, 0.0), 4.0); + + let delta = Math.ceil(Math.pow(2, zoom)) / Math.ceil(Math.pow(2, old_zoom)); + cx *= delta; + cy *= delta; + }); + + window.addEventListener("keydown", (event) => { + if (hovered) { + if (event.code === "Space") { + if (running) { + pause(); + } else if (event.shiftKey) { + play(); + } else { + step(); + } + } else if (event.code === "KeyG") { + grid = !grid; + } + } + }); + + + resize(); + window.addEventListener("resize", resize); + window.requestAnimationFrame(loop); +} + +export function get_hovered() { + let zoom_factor = Math.ceil(Math.pow(2, zoom)); + let tile_size = 10 * zoom_factor; + + let x = Math.floor((mouse_x - canvas.width / 2 - cx) / tile_size); + let y = Math.floor((mouse_y - canvas.height / 2 - cy) / tile_size); + + return [x, y]; +} + +export function to_color(num) { + let alpha = (num >> 24) & 0xff; + let red = (num >> 16) & 0xff; + let green = (num >> 8) & 0xff; + let blue = num & 0xff; + + return [red, green, blue, alpha]; +} diff --git a/editor/index.html b/editor/index.html index 3347583..26d8824 100644 --- a/editor/index.html +++ b/editor/index.html @@ -17,8 +17,20 @@ Your browser needs to support canvases, sowwy :( -
- +
+

Coordinates:

+
+

State:

+
+

Signal:

+
+

Stack:

+
    +
    +

    Tile:

    +
    + Type: +
    diff --git a/editor/index.js b/editor/index.js index 7b9c349..ed21522 100644 --- a/editor/index.js +++ b/editor/index.js @@ -6,12 +6,12 @@ import init, { available_tiles, } from "/stackline-wasm/pkg/stackline_wasm.js"; +import * as canvas from "./canvas.js"; +import * as inspect from "./inspect.js"; + let promises = []; promises.push(init()); -const canvas = document.getElementById("main-canvas"); -const ctx = canvas.getContext("2d"); - let font = new FontFace("Stackline Classic", "url(\"/font/StacklineClassic-Medium.otf\")"); promises.push(font.load()); await Promise.all(promises); @@ -19,268 +19,48 @@ await Promise.all(promises); font = await font.loaded; document.fonts.add(font); -let world = World.deserialize(await (await fetch("/stackline/tests/other/prime.json")).json()); +export let world = World.deserialize(await (await fetch("/stackline/tests/other/prime.json")).json()); world.init(); -// let world = World.new(); -let cx = 0; -let cy = 0; -let zoom = 1; -let grid = true; - -let click_x = 0, click_y = 0; -let mouse_x = 0, mouse_y = 0; -let mouse_down = false; -let hovered = false; -let selected = null; - -let running = null; - let tile = world.get(4, 0); tile.signal = new Signal(); tile.state = "Active"; world.set(4, 0, tile); -// let pane = Pane.empty(5, 5); -// world.set_pane("main", pane); - -// let available = available_tiles(); -// console.log(available); -// for (let n = 0; n < available.length; n++) { -// world.set( -// n % 5, -// ~~(n / 5), -// new FullTile(available[n]) -// ); -// } - console.log(world.toString()); console.log(world.serialize()); -resize(); -window.addEventListener("resize", resize); -window.requestAnimationFrame(loop); - -function draw() { - let zoom_factor = Math.ceil(Math.pow(2, zoom)); - let tile_size = 10 * zoom_factor; - ctx.fillStyle = "#202027"; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - function stroke_rect(x, y, width, height) { - ctx.lineWidth = zoom_factor; - ctx.strokeRect( - Math.round(x * tile_size + cx + canvas.width / 2) - zoom_factor * 1.5, - Math.round(y * tile_size + cy + canvas.height / 2) - zoom_factor * 1.5, - Math.round(width * tile_size) + zoom_factor * 2, - Math.round(height * tile_size) + zoom_factor * 2, - ); - } - - function fill_rect(x, y, width, height) { - ctx.fillRect( - Math.round(x * tile_size + cx + canvas.width / 2) - zoom_factor * 1, - Math.round(y * tile_size + cy + canvas.height / 2) - zoom_factor * 1, - Math.round(width * tile_size) + zoom_factor * 1, - Math.round(height * tile_size) + zoom_factor * 1, - ); - } - - let panes = []; - - for (let name of world.panes()) { - let pane = world.get_pane(name); - - panes.push({ - x: pane.x, - y: pane.y, - width: pane.width, - height: pane.height - }); - - ctx.strokeStyle = "rgba(128, 128, 128, 0.5)"; - stroke_rect( - pane.x, - pane.y, - pane.width, - pane.height, - ); - - pane.free(); - } - - let from_y = Math.floor((-cy - canvas.height / 2) / tile_size); - let to_y = Math.ceil((-cy + canvas.height / 2) / tile_size); - let from_x = Math.floor((-cx - canvas.width / 2) / tile_size); - let to_x = Math.ceil((-cx + canvas.width / 2) / tile_size); - - let width = to_x - from_x + 1; - let height = to_y - from_y + 1; - - // TODO: memoize - let chars = world.draw(-from_x, -from_y, width, height); - ctx.textAlign = "left"; - ctx.textBaseline = "top"; - ctx.font = `${tile_size}px Stackline Classic`; - - for (let y = from_y; y <= to_y; y++) { - for (let x = from_x; x <= to_x; x++) { - let x2 = x * tile_size + cx + canvas.width / 2; - let y2 = y * tile_size + cy + canvas.height / 2; - - let index = 3 * (width * (y - from_y) + x - from_x); - - let ch = String.fromCharCode(chars[index]); - if (ch !== " ") { - let fg = to_color(chars[index + 1]); - let bg = chars[index + 2] === 0 ? null : to_color(chars[index + 2]); - - if (bg) { - ctx.fillStyle = `rgba(${bg.join(",")})`; - ctx.fillRect( - Math.round(x2), - Math.round(y2), - Math.round(x2 + tile_size) - Math.round(x2), - Math.round(y2 + tile_size) - Math.round(y2), - ); - } - ctx.fillStyle = `rgba(${fg.join(",")})`; - ctx.fillText( - ch, - Math.round(x2), - Math.round(y2), - ); - } else if (grid) { - // Draw grid - let inside = false; - - for (let pane of panes) { - if (x >= pane.x && x < pane.x + pane.width && y >= pane.y && y < pane.y + pane.height) { - inside = true; - break; - } - } - - if (inside || width < 100 && height < 100) { - ctx.fillStyle = inside ? "#404040" : "#303030"; - - ctx.fillRect( - Math.round(x2 + tile_size / 2) - zoom_factor, - Math.round(y2 + tile_size / 2) - zoom_factor, - zoom_factor, - zoom_factor - ); - } - } - } - } - - if (hovered) { - let [x, y] = get_hovered(); - - ctx.strokeStyle = "rgba(230, 255, 230, 0.2)"; - ctx.fillStyle = "rgba(230, 255, 230, 0.07)"; - stroke_rect(x, y, 1, 1); - fill_rect(x, y, 1, 1); - } +export let selected = null; +export function select(x, y) { + selected = [x, y]; + inspect.update_selected(x, y); } -function resize() { - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; - - draw(); -} - -function loop() { - draw(); - window.requestAnimationFrame(loop); -} - -canvas.addEventListener("mousedown", (evt) => { - click_x = mouse_x = evt.clientX; - click_y = mouse_y = evt.clientY; - mouse_down = true; -}); - -canvas.addEventListener("mousemove", (evt) => { - hovered = true; - - if (mouse_down) { - // TODO: numerically stable solution - cx += evt.clientX - mouse_x; - cy += evt.clientY - mouse_y; - } - - mouse_x = evt.clientX; - mouse_y = evt.clientY; -}); +export let running = null; -canvas.addEventListener("mouseup", (evt) => { - mouse_down = false; +export function step() { + world.init(); + world.step(); - let dist = Math.sqrt((mouse_x - click_x) ** 2 + (mouse_y - click_y) ** 2); - if (dist < 10) { - selected = get_hovered(); + if (selected) { + inspect.update_selected(selected[0], selected[1]); } -}); - -canvas.addEventListener("mouseenter", (evt) => { - hovered = true; -}); - -canvas.addEventListener("mouseleave", (evt) => { - mouse_down = false; - hovered = false; -}); - -canvas.addEventListener("wheel", (event) => { - const ZOOM_STRENGTH = -0.005; - let old_zoom = zoom; - zoom += event.deltaY * ZOOM_STRENGTH; - zoom = Math.min(Math.max(zoom, 0.0), 4.0); - - let delta = Math.ceil(Math.pow(2, zoom)) / Math.ceil(Math.pow(2, old_zoom)); - cx *= delta; - cy *= delta; -}); +} -window.addEventListener("keydown", (event) => { - if (hovered) { - if (event.code === "Space") { - if (running !== null) { - clearInterval(running); - running = null; - } else if (event.shiftKey) { - world.init(); - running = setInterval(() => { - world.step(); - }, 100); - } else { - world.init(); - world.step(); - } - } else if (event.code === "KeyG") { - grid = !grid; +export function play() { + world.init(); + running = setInterval(() => { + world.step(); + if (selected) { + inspect.update_selected(selected[0], selected[1]); } - } -}); - -function get_hovered() { - let zoom_factor = Math.ceil(Math.pow(2, zoom)); - let tile_size = 10 * zoom_factor; - - let x = Math.floor((mouse_x - canvas.width / 2 - cx) / tile_size); - let y = Math.floor((mouse_y - canvas.height / 2 - cy) / tile_size); - - return [x, y]; + }, 100); } -function to_color(num) { - let alpha = (num >> 24) & 0xff; - let red = (num >> 16) & 0xff; - let green = (num >> 8) & 0xff; - let blue = num & 0xff; - - return [red, green, blue, alpha]; +export function pause() { + clearInterval(running); + running = null; } + +canvas.init(); +inspect.init(); diff --git a/editor/inspect.js b/editor/inspect.js new file mode 100644 index 0000000..bb61410 --- /dev/null +++ b/editor/inspect.js @@ -0,0 +1,75 @@ +import { + world, +} from "./index.js"; + +import { + available_tiles, +} from "/stackline-wasm/pkg/stackline_wasm.js"; + +const right_pane = document.getElementById("right-pane"); +const coordinates_elem = document.getElementById("coordinates"); +const stack_elem = document.getElementById("stack"); +const signal_elem = document.getElementById("signal"); +const state_elem = document.getElementById("state"); +const tile_elem = document.getElementById("tile"); +const tile_name_elem = document.getElementById("tile-name"); + +export function update_selected(x, y) { + coordinates.innerText = `(${x}, ${y})`; + + let full_tile = world.get(x, y); + + if (!full_tile) { + right_pane.classList.remove("selected"); + return; + } + + let signal = full_tile.signal; + while (stack_elem.children.length > 0) { + stack_elem.removeChild(stack_elem.firstChild); + } + + if (signal) { + signal_elem.classList.add("has-signal"); + for (let element of signal.stack) { + let li = document.createElement("li"); + if (typeof element === "string") { + li.innerText = `"${element}"`; + } else if (typeof element === "number") { + li.innerText = element.toString(); + } else { + throw new Error("Unexpected element type: " + typeof element); + } + stack_elem.appendChild(li); + } + + signal.free(); + } else { + signal_elem.classList.remove("has-signal"); + } + + let tile = full_tile.tile; + if (tile) { + tile_elem.classList.add("has-tile"); + + let name = Object.keys(tile)[0]; + tile_name_elem.innerText = name; + } else { + tile_elem.classList.remove("has-tile"); + } + + state_elem.innerText = full_tile.state; + + full_tile.free(); + + right_pane.classList.add("selected"); +} + +export function init() { + // for (let name of available_tiles()) { + // let option = document.createElement("option"); + // option.innerText = name; + // option.value = name; + // tile_name_elem.appendChild(option); + // } +} diff --git a/editor/style.css b/editor/style.css index 7d70bb1..6c8b023 100644 --- a/editor/style.css +++ b/editor/style.css @@ -1,3 +1,8 @@ +@font-face { + font-family: "Stackline Classic"; + src: url("/font/StacklineClassic-Medium.otf"); +} + body { margin: 0; height: 100vh; @@ -8,7 +13,13 @@ body { } #left-pane, #right-pane { + min-width: 20em; max-width: 25vw; + background: #18181b; + color: #f0f0f0; + padding: 1.5em 1em; + font-family: monospace; + font-size: 15px; } #middle-pane { @@ -19,3 +30,79 @@ body { width: 100%; height: 100vh; } + +h2 { + font-size: inherit; + margin: 0; + color: white; + /* font-weight: normal; */ + /* text-decoration: underline; */ +} + +h3 { + font-size: inherit; + margin: 0; + color: white; + font-weight: normal; +} + +#right-pane:not(.selected)::before { + content: "Nothing selected"; + font-style: italic; + color: #808080; +} + +#right-pane:not(.selected) > * { + display: none; +} + +#signal, #tile { + margin-top: 0.25em; + margin-bottom: 0.25em; +} + +#signal:not(.has-signal), #tile:not(.has-tile) { + padding-left: 0; + border-left: none; + margin-left: 0; +} + +#signal:not(.has-signal) > *, #tile:not(.has-tile) > * { + display: none; +} + +#signal:not(.has-signal)::before { + content: "No signal"; +} + +#tile:not(.has-tile)::before { + content: "No tile"; +} + +#signal:not(.has-signal)::before, #tile:not(.has-signal)::before { + font-style: italic; + color: #808080; +} + +#stack { + list-style: none; + padding-left: 0em; + margin: 0 0; +} + +#stack > li { + margin-top: 2px; + margin-bottom: 2px; +} + +#stack > li::before { + content: "\25b6"; + margin-right: 0.5em; + color: #808080; +} + +.indent { + margin-left: 0.5em; + padding-left: calc(0.5em - 1px); + border-left: 1px solid #808080; +} diff --git a/font/StacklineClassic-Medium.otf b/font/StacklineClassic-Medium.otf index ab86693..924e7ca 100644 Binary files a/font/StacklineClassic-Medium.otf and b/font/StacklineClassic-Medium.otf differ diff --git a/font/StacklineClassic-Medium.pfs b/font/StacklineClassic-Medium.pfs index dfaa353..6690ff3 100644 --- a/font/StacklineClassic-Medium.pfs +++ b/font/StacklineClassic-Medium.pfs @@ -1,7 +1,7 @@ Stackline Classic Shad Amethyst Medium -11:11:10:9:-1:0:10 +11:11:10:9:-1:0:10:0 65:AAPAzBhGCMEf4wRgnjgAAA== 77:ABwZxj3G6MkYIwRgnjgAAA== 66:AB/hgjBGCP4YIwRgn+AAAA== @@ -30,7 +30,7 @@ Medium 90:AB/yDAMAwDAMAwBgn/AAAA== 112:AAAAAD8DEGIMQfAwBgHgAA== 62:AAAAYAYAYAcBgGAYAAAAAA== -60:AAAAGAYBgOAGAGAGAAAAAA== +60:AAAAMAwDAcAMAMAMAAAAAA== 43:AAEAIAQAgf8CAEAIAQAAAA== 9532:AACAEAIAQP+BACAEAIAAAA== 9474:AACAEAIAQAgBACAEAIAAAA== @@ -105,4 +105,18 @@ Medium 10689:AA/jBkhIiQkiJITBj+AAAA== 10688:AA/jBkJIiSEiJCTBj+AAAA== 8805:AA/jBkhIiQkiJOTBj+AAAA== -8804:AA/jBkJIiSEiJOTBj+AAAA== \ No newline at end of file +8804:AA/jBkJIiSEiJOTBj+AAAA== +49:AACAMA4CwBgDAGAMA8AAAA== +48:AAPAxBiDEGIMQYgxA8AAAA== +50:AAPAhACAMAwDAMAxB+AAAA== +51:AAPAjAGAMBwAwBgjA8AAAA== +52:AABAGAcBoGQPwBACAOAAAA== +53:AAfgxBgDAHwAwBgjA8AAAA== +54:AAPAxBgDAHwMQYgxA8AAAA== +55:AAfgjAGAYAwDAGAMAYAAAA== +56:AAPAxBiDEDwMQYgxA8AAAA== +57:AAPAxBiDED4AQQgxA8AAAA== +46:AAAAAAAAAAAAAAAYAwAAAA== +44:AAAAAAAAAAAAAAAYAwDAAA== +9500:CAEAIAQAgB+CAEAIAQAgAA== +9492:CAEAIAQAgB+AAAAAAAAAAA== \ No newline at end of file