ref: 81d1e4e01d7001e6a0107792e6167136787295d6
dir: /index.js/
import {categories} from "./view.js" import {Stage, SpriteAreas, StageAreas} from "./stage.js" import {load, fromSB3, toSB3} from "./external.js" import {Scrollable} from "./areas.js" import {CostumeAreas} from "./paint.js" import {getMessage, getName, locale} from "./translation.js" import * as core from "./core.js" let buttonsElement = document.createElement("div") let spriteListElement = document.createElement("div") let categoryListElement = document.createElement("div") let startButton = document.createElement("button") let stopButton = document.createElement("button") let expandButton = document.createElement("button") let downloadButton = document.createElement("button") let uploadButton = document.createElement("a") let logoElement = document.createElement("a") let controlElement = document.createElement("div") let codeTabButton = document.createElement("button") let costumeTabButton = document.createElement("button") let soundTabButton = document.createElement("button") let addSpriteButton = document.createElement("button") let errorButton = document.createElement("button") buttonsElement.classList.add("buttons") spriteListElement.classList.add("sprite-list") categoryListElement.classList.add("categories") codeTabButton.classList.add("code-tab") costumeTabButton.classList.add("costume-tab") soundTabButton.classList.add("sound-tab") controlElement.classList.add("sprite-control") addSpriteButton.classList.add("add-sprite") startButton.classList.add("start-button") stopButton.classList.add("stop-button") expandButton.classList.add("expand-button") downloadButton.classList.add("download-button") uploadButton.classList.add("upload-button", "button") logoElement.classList.add("logo") startButton.title = getMessage("gui.controls.go") ?? "go" stopButton.title = getMessage("gui.controls.stop") ?? "stop" expandButton.title = getMessage("gui.stageHeader.stageSizeFull") ?? "full screen" downloadButton.title = getMessage("gui.menuBar.downloadToComputer") ?? "download" uploadButton.title = getMessage("gui.sharedMessages.loadFromComputerTitle") ?? "upload" codeTabButton.title = getMessage("gui.gui.codeTab") ?? "code" soundTabButton.title = getMessage("gui.gui.soundsTab") ?? "sounds" startButton.append(getIcon("green-flag")) stopButton.append(getIcon("stop-sign")) expandButton.append(getIcon("expand")) downloadButton.append(getIcon("download")) uploadButton.append(getIcon("upload")) codeTabButton.append(getIcon("code")) costumeTabButton.append(getIcon("costumes")) soundTabButton.append(getIcon("sounds")) logoElement.insertAdjacentHTML("beforeend", getLogo()) addSpriteButton.append(getIcon("add")) errorButton.append(getIcon("error")) buttonsElement.append(logoElement, startButton, stopButton, expandButton, uploadButton, downloadButton, errorButton) controlElement.append(codeTabButton, costumeTabButton, soundTabButton) spriteListElement.append(addSpriteButton) uploadButton.href = "?upload" logoElement.href = "about" errorButton.style.setProperty("display", "none", "important") soundTabButton.style.setProperty("display", "none") startButton.addEventListener("click", () => stage.start()) stopButton.addEventListener("click", () => stage.stop()) expandButton.addEventListener("click", () => stage.element.requestFullscreen()) downloadButton.addEventListener("click", async () => download(await toSB3(stage), stage.name + ".sb3")) addSpriteButton.addEventListener("click", addSprite) codeTabButton.addEventListener("click", codeTab) costumeTabButton.addEventListener("click", costumeTab) addEventListener("error", ({error}) => logError(error)) addEventListener("unhandledrejection", ({reason}) => logError(reason)) errorButton.addEventListener("click", showErrors) let categoryNames = new Map() for (let category of categories) { if (category === "lists") continue let key = "CATEGORY_" + category.toUpperCase() if (category === "custom") key = "CATEGORY_MYBLOCKS" if (category === "extensions") key = "gui.gui.addExtension" let names = {extensions: "add extension", custom: "my blocks"} let name = getMessage(key) ?? names[category] ?? category categoryNames.set(category, name) let button = document.createElement("button") button.classList.add(category) button.append(name) categoryListElement.append(button) button.addEventListener("click", () => { let element = document.querySelector(".category-name-" + category) if (!element) return element.closest(".library").scrollTo({top: element.offsetTop - 16, behavior: "smooth"}) }) } new Scrollable({element: spriteListElement, y: false}) let spriteListScroll = addScrollbars(spriteListElement, ["horizontal"]) spriteListScroll.classList.add("sprite-list-scroll") let map = new Map() async function main() { try { return await loadStage() ?? makeStage() } catch (error) { if (location.search !== "") location.href = location.pathname throw error } } async function fetchCostume(sprite, url, x, y) { let response = await fetch(url) if (!response.ok) throw new Error() let blob = await response.blob() return sprite.makeCostume({blob, x, y}) } async function loadStage() { let query = location.search.slice(1) if (query === "upload") { let input = document.createElement("input") input.type = "file" input.style.cssText = "font-size: 2em; position: fixed; bottom: 50%; right: 50%; transform: translate(50%, 50%);" document.body.append(input) let handler = event => input.contains(event.target) || input.click() addEventListener("click", handler) let buffer = await new Promise((resolve, reject) => { input.addEventListener("change", () => { if (input.files.length === 0) return let reader = new FileReader() reader.readAsArrayBuffer(input.files[0]) reader.addEventListener("error", () => reject(reader.error)) reader.addEventListener("load", () => resolve(reader.result)) }) }) input.remove() removeEventListener("click", handler) let stage = await fromSB3(new Uint8Array(buffer)) if (stage) { let name = input.files[0].name if (name.endsWith(".sb3")) name = name.slice(0, -4) stage.name = name } return stage } if (/[^0-9]/.test(query)) { let response = await fetch(query) if (!response.ok) return let stage = await fromSB3(new Uint8Array(await response.arrayBuffer())) if (stage) { let name = decodeURIComponent(new URL(query, location.href).pathname.split("/").at(-1) ?? "") if (name.endsWith(".sb3")) name = name.slice(0, -4) if (name) stage.name = name } return stage } if (query) return load(query) } async function makeStage() { let stage = new Stage({name: getMessage("gui.gui.defaultProjectTitle")}) stage.variables.set(getMessage("gui.defaultProject.variable") ?? "my variable", {value: 0}) let sprite = stage.makeSprite({name: getMessage("gui.SpriteInfo.sprite", m => m + "1")}) sprite.costumes[0].remove() try { sprite.costume = await fetchCostume(sprite, "assets/costume1.png", 140, 180) await fetchCostume(sprite, "assets/costume2.png", 150, 190) } catch (error) { logError(error) } return stage } function addSprite() { let sprite = stage.makeSprite({name: getMessage("gui.SpriteInfo.sprite", m => m + (stage.sprites.length + 1))}) registerSprite(sprite) show(sprite) spriteListElement.scrollTo({left: spriteListElement.scrollWidth, behavior: "smooth"}) } function registerSprite(sprite, Areas = SpriteAreas, name = sprite.name) { let areas = new Areas(sprite, { Make, getName: (...args) => getName(sprite.stage ?? stage, ...args), toCategoryName: name => categoryNames.get(name), getExtensionName: name => getMessage(`gui.extension.${name}.name`) ?? name, messages: { getMissing: name => name === "motion" ? getMessage("MOTION_STAGE_SELECTED") ?? "stage selected: no motion blocks" : undefined, getOK: () => getMessage("gui.prompt.ok"), getCancel: () => getMessage("gui.prompt.cancel"), getVariable: () => getMessage("NEW_VARIABLE"), getList: () => getMessage("NEW_LIST"), getBlock: () => getMessage("NEW_PROCEDURE"), getAtomic: () => getMessage("gui.customProcedures.runWithoutScreenRefresh"), getTextInput: () => getMessage("gui.customProcedures.addAnInputNumberText", m => m + ` (${getMessage("gui.customProcedures.numberTextType")})`), getBooleanInput: () => getMessage("gui.customProcedures.addAnInputBoolean", m => m + ` (${getMessage("gui.customProcedures.booleanType")})`), getTextInputName: () => getMessage("gui.customProcedures.numberTextType"), getBooleanInputName: () => getMessage("gui.customProcedures.booleanType"), getBlockName: () => getMessage("PROCEDURE_DEFAULT_NAME"), getLocal: () => getMessage("gui.gui.variableScopeOptionSpriteOnly"), getGlobal: () => getMessage("gui.gui.variableScopeOptionAllSprites"), getMessage: () => getMessage("DEFAULT_BROADCAST_MESSAGE_NAME"), getAddMessage: () => getMessage("NEW_BROADCAST_MESSAGE"), getLabel: () => getMessage("gui.customProcedures.addALabel"), }, }) let isStage = Areas === StageAreas let button = document.createElement("button") let costumeAreas if (isStage) costumeAreas = new CostumeAreas(sprite.backdrops, {select: backdrop => sprite.backdrop = backdrop}) else costumeAreas = new CostumeAreas(sprite.costumes, {select: costume => sprite.costume = costume}) button.append(name) addSpriteButton.before(button) button.addEventListener("click", () => show(sprite)) let libraryScroll = addScrollbars(areas.libraryArea.element, ["vertical"]) let programScroll = addScrollbars(areas.programArea.element) libraryScroll.classList.add("library-scroll") programScroll.classList.add("program-scroll") map.set(sprite, { sprite, areas, costumeAreas, button, isStage, elements: { library: libraryScroll, program: programScroll, costumes: costumeAreas.costumeListElement, paint: costumeAreas.paintElement, toolbox: costumeAreas.toolboxElement, }, }) } function Make(area, make) { if (locale === "en") return if (make.type === core.Join.type) return () => make(getMessage("OPERATORS_JOIN_APPLE", m => m + " "), getMessage("OPERATORS_JOIN_BANANA")) if (make.type === core.TextIndex.type) return () => make(getMessage("OPERATORS_JOIN_APPLE")) if (make.type === core.TextLength.type) return () => make(getMessage("OPERATORS_JOIN_APPLE")) if (make.type === core.TextContains.type) return () => make(getMessage("OPERATORS_JOIN_APPLE"), getMessage("OPERATORS_LETTEROF_APPLE")) let [list] = (area.sprite ?? area.stage).lists.keys() if (make.type === core.Append.type) return () => make(list, getMessage("DEFAULT_LIST_ITEM")) if (make.type === core.Insert.type) return () => make(list, getMessage("DEFAULT_LIST_ITEM")) if (make.type === core.Replace.type) return () => make(list, 1, getMessage("DEFAULT_LIST_ITEM")) if (make.type === core.Find.type) return () => make(list, getMessage("DEFAULT_LIST_ITEM")) if (make.type === core.Contains.type) return () => make(list, getMessage("DEFAULT_LIST_ITEM")) if (make.type === area.stage.WhenReceived.type) return () => make(getMessage("DEFAULT_BROADCAST_MESSAGE_NAME")) if (make.type === area.stage.Broadcast.type) return () => make(getMessage("DEFAULT_BROADCAST_MESSAGE_NAME")) if (make.type === area.stage.BroadcastAndWait.type) return () => make(getMessage("DEFAULT_BROADCAST_MESSAGE_NAME")) if (!area.sprite) return if (make.type === area.sprite.Say.type) return () => make(getMessage("LOOKS_HELLO")) if (make.type === area.sprite.SayFor.type) return () => make(getMessage("LOOKS_HELLO")) if (make.type === area.sprite.Think.type) return () => make(getMessage("LOOKS_HMM")) if (make.type === area.sprite.ThinkFor.type) return () => make(getMessage("LOOKS_HMM")) } let shown function show(sprite) { if (shown) { shown.elements.library.remove() shown.elements.program.remove() shown.elements.paint.remove() shown.elements.costumes.remove() shown.elements.toolbox.remove() shown.button.disabled = false } shown = map.get(sprite) shown.button.disabled = true if (shown.isStage) costumeTabButton.title = getMessage("gui.gui.backdropsTab") ?? "backdrops" else costumeTabButton.title = getMessage("gui.gui.costumesTab") ?? "costumes" codeTab() } function codeTab() { codeTabButton.disabled = true costumeTabButton.disabled = false soundTabButton.disabled = false shown.elements.paint.remove() shown.elements.costumes.remove() shown.elements.toolbox.remove() document.body.append(shown.elements.library, shown.elements.program, categoryListElement) shown.areas.programArea.element.dispatchEvent(new Event("scroll")) shown.areas.libraryArea.element.dispatchEvent(new Event("scroll")) shown.areas.update() } function costumeTab() { codeTabButton.disabled = false costumeTabButton.disabled = true soundTabButton.disabled = false categoryListElement.remove() shown.elements.library.remove() shown.elements.program.remove() document.body.append(shown.elements.toolbox, shown.elements.costumes, shown.elements.paint) shown.elements.costumes.querySelectorAll(".costume-list > button")[(shown.isStage ? shown.sprite.backdropNumber() : shown.sprite.costumeNumber()) - 1].click() } function addScrollbar(element, type) { let scrollbar = document.createElement("div") scrollbar.classList.add("scrollbar", "scrollbar-" + type) scrollbar.style.setProperty("--x", "0") let down = false addEventListener("pointerup", () => down = false) scrollbar.addEventListener("pointerdown", update) addEventListener("pointermove", event => down && update(event)) element.addEventListener("scroll", () => { let progress if (type === "vertical") progress = element.scrollTop / (element.scrollHeight - element.clientHeight) if (type === "horizontal") progress = element.scrollLeft / (element.scrollWidth - element.clientWidth) scrollbar.style.setProperty("--x", String(progress || 0)) }) scrollbar.addEventListener("contextmenu", event => event.preventDefault()) function update(event) { down = true let rect = scrollbar.getBoundingClientRect() if (type === "vertical") { let progress = (event.y - rect.y - rect.height * 0.125) / rect.height / 0.75 element.scrollTo({top: progress * (element.scrollHeight - element.clientHeight), behavior: "instant"}) } if (type === "horizontal") { if (scrollbar.matches(":dir(rtl)")) { let progress = (- rect.right + rect.width * 0.125) / rect.width / 0.75 element.scrollTo({left: progress * (element.scrollWidth - element.clientWidth), behavior: "instant"}) } else { let progress = (event.x - rect.x - rect.width * 0.125) / rect.width / 0.75 element.scrollTo({left: progress * (element.scrollWidth - element.clientWidth), behavior: "instant"}) } } } return scrollbar } function addScrollbars(element, types = ["horizontal", "vertical"]) { let div = document.createElement("div") div.append(element, ...types.map(type => addScrollbar(element, type))) return div } function download(blob, name) { let a = document.createElement("a") document.body.append(a) let url = URL.createObjectURL(blob) a.href = url a.download = name a.click() URL.revokeObjectURL(url) a.remove() } let errors = "" function logError(error) { errorButton.removeAttribute("style") errors += String(error) + "\n" if (error.stack) errors += String(error.stack) + "\n" errors += "\n" } function showErrors() { let dialogue = document.createElement("dialog") dialogue.addEventListener("close", () => dialogue.remove()) let pre = document.createElement("pre") pre.append(errors.trimEnd()) dialogue.append(pre) let form = document.createElement("form") form.method = "dialog" dialogue.append(form) let button = document.createElement("button") button.append("okay") form.append(button) document.body.append(dialogue) dialogue.showModal() } function getIcon(name) { let icons = { "green-flag": `<svg viewbox="0 0 24 24" fill="#6C6" stroke="#444"><path d="M20.45,5.37C20.71,4.71,20.23,4,19.52,4H13h-1H7V3c0-0.55-0.45-1-1-1h0C5.45,2,5,2.45,5,3v1v10v8h2v-8h4h1h7.52 c0.71,0,1.19-0.71,0.93-1.37L19,9L20.45,5.37z"/></svg>`, "stop-sign": `<svg viewbox="0 0 24 24" fill="#E24" stroke="#444"><path d="M14.9,3H9.1C8.57,3,8.06,3.21,7.68,3.59l-4.1,4.1C3.21,8.06,3,8.57,3,9.1v5.8c0,0.53,0.21,1.04,0.59,1.41l4.1,4.1 C8.06,20.79,8.57,21,9.1,21h5.8c0.53,0,1.04-0.21,1.41-0.59l4.1-4.1C20.79,15.94,21,15.43,21,14.9V9.1c0-0.53-0.21-1.04-0.59-1.41 l-4.1-4.1C15.94,3.21,15.43,3,14.9,3z"/></svg>`, expand: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M6 14c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1h3c.55 0 1-.45 1-1s-.45-1-1-1H7v-2c0-.55-.45-1-1-1zm0-4c.55 0 1-.45 1-1V7h2c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1zm11 7h-2c-.55 0-1 .45-1 1s.45 1 1 1h3c.55 0 1-.45 1-1v-3c0-.55-.45-1-1-1s-1 .45-1 1v2zM14 6c0 .55.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1V6c0-.55-.45-1-1-1h-3c-.55 0-1 .45-1 1z"/></svg>`, download: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M16.59 9H15V4c0-.55-.45-1-1-1h-4c-.55 0-1 .45-1 1v5H7.41c-.89 0-1.34 1.08-.71 1.71l4.59 4.59c.39.39 1.02.39 1.41 0l4.59-4.59c.63-.63.19-1.71-.7-1.71zM5 19c0 .55.45 1 1 1h12c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1z"/></svg>`, upload: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M10 16h4c.55 0 1-.45 1-1v-5h1.59c.89 0 1.34-1.08.71-1.71L12.71 3.7c-.39-.39-1.02-.39-1.41 0L6.71 8.29c-.63.63-.19 1.71.7 1.71H9v5c0 .55.45 1 1 1zm-4 2h12c.55 0 1 .45 1 1s-.45 1-1 1H6c-.55 0-1-.45-1-1s.45-1 1-1z"/></svg>`, add: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M18 13h-5v5c0 .55-.45 1-1 1s-1-.45-1-1v-5H6c-.55 0-1-.45-1-1s.45-1 1-1h5V6c0-.55.45-1 1-1s1 .45 1 1v5h5c.55 0 1 .45 1 1s-.45 1-1 1z"/></svg>`, code: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M4,7v2c0,0.55-0.45,1-1,1h0c-0.55,0-1,0.45-1,1v2c0,0.55,0.45,1,1,1h0c0.55,0,1,0.45,1,1v2c0,1.66,1.34,3,3,3h2 c0.55,0,1-0.45,1-1v0c0-0.55-0.45-1-1-1H7c-0.55,0-1-0.45-1-1v-2c0-1.3-0.84-2.42-2-2.83v-0.34C5.16,11.42,6,10.3,6,9V7 c0-0.55,0.45-1,1-1h2c0.55,0,1-0.45,1-1v0c0-0.55-0.45-1-1-1H7C5.34,4,4,5.34,4,7z"/><path d="M21,10c-0.55,0-1-0.45-1-1V7c0-1.66-1.34-3-3-3h-2c-0.55,0-1,0.45-1,1v0c0,0.55,0.45,1,1,1h2c0.55,0,1,0.45,1,1v2 c0,1.3,0.84,2.42,2,2.83v0.34c-1.16,0.41-2,1.52-2,2.83v2c0,0.55-0.45,1-1,1h-2c-0.55,0-1,0.45-1,1v0c0,0.55,0.45,1,1,1h2 c1.66,0,3-1.34,3-3v-2c0-0.55,0.45-1,1-1h0c0.55,0,1-0.45,1-1v-2C22,10.45,21.55,10,21,10L21,10z"/></svg>`, costumes: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M7 14c-1.66 0-3 1.34-3 3 0 1.31-1.16 2-2 2 .92 1.22 2.49 2 4 2 2.21 0 4-1.79 4-4 0-1.66-1.34-3-3-3zm13.71-9.37l-1.34-1.34c-.39-.39-1.02-.39-1.41 0L9 12.25 11.75 15l8.96-8.96c.39-.39.39-1.02 0-1.41z"/></svg>`, sounds: `<svg viewbox="0 0 24 24" fill="#48E" stroke="#444"><path d="M3 10v4c0 .55.45 1 1 1h3l3.29 3.29c.63.63 1.71.18 1.71-.71V6.41c0-.89-1.08-1.34-1.71-.71L7 9H4c-.55 0-1 .45-1 1zm13.5 2c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 4.45v.2c0 .38.25.71.6.85C17.18 6.53 19 9.06 19 12s-1.82 5.47-4.4 6.5c-.36.14-.6.47-.6.85v.2c0 .63.63 1.07 1.21.85C18.6 19.11 21 15.84 21 12s-2.4-7.11-5.79-8.4c-.58-.23-1.21.22-1.21.85z"/></svg>`, error: `<svg viewbox="0 0 24 24" fill="#EA4" stroke="#444"><path d="M4.47 21h15.06c1.54 0 2.5-1.67 1.73-3L13.73 4.99c-.77-1.33-2.69-1.33-3.46 0L2.74 18c-.77 1.33.19 3 1.73 3zM12 14c-.55 0-1-.45-1-1v-2c0-.55.45-1 1-1s1 .45 1 1v2c0 .55-.45 1-1 1zm1 4h-2v-2h2v2z"/></svg>`, } let span = document.createElement("span") span.insertAdjacentHTML("beforeend", icons[name]) span.classList.add("icon") let svg = span.querySelector("svg") svg.style.setProperty("height", "1.5em") svg.style.setProperty("vertical-align", "middle") return span } function getLogo() { return ` <svg viewbox="0.75 0.5 44.5 17.75" fill="none" stroke-linejoin="round" stroke-linecap="round"> <g id="scrax"> <path id="s" d="M9 4v4s0-3-3-3c-2 0-3 5 1 5 3 0 3 5 0 5s-3-4-3-4v4" /> <path id="c" d="M16 6v3s0-2-2-2-3 1-3 4c0 1 1 3 3 3 3 0 3-3 3-3" /> <path id="r" d="M27 14c-5 2 0-6-8-5m-1 6 4-1m-3-9 1 4v5m-2-9c8-1 8 4 1 4" /> <path id="a" d="m32 13 3-1m-6-2 3-1m-6 3 4 1m-2-1c1-3 1-6 2-8m-3 1c4-2 3-3 6 7" /> <path id="x" d="m39 5 3 1m-6 8 4-8m-5 8h3m2 0 2-1m-6-6c1 1 4 5 5 6m-7-6 3-1" /> </g> <use href="#scrax" stroke="#444" stroke-width="6.25" /> <use href="#scrax" stroke="#FFF" stroke-width="6" /> <use href="#scrax" stroke="#444" stroke-width="1.75" filter="drop-shadow(0.25px 0.5px 0.5px #4444)" /> <g stroke-width="1.5"> <use href="#s" stroke="#9DF" /> <use href="#c" stroke="#F9D" /> <use href="#r" stroke="#9DF" /> <use href="#a" stroke="#F9D" /> <use href="#x" stroke="#D9F" /> </g> </svg>` } let css = ` *, ::before, ::after { box-sizing: border-box; scrollbar-width: none; } ::-webkit-scrollbar { display: none; } html, body { height: 100%; touch-action: pan-x pan-y; } body { font-family: "DejaVu Sans", "DejaVu LGC Sans", "Verdana", "Bitstream Vera Sans", "Geneva", sans-serif; margin: 0; display: grid; grid-template: "buttons sprites control" auto "stage main main" auto "top main main" auto "bottom main main" 1fr / 360px 1fr auto; -moz-user-select: -moz-none; -webkit-user-select: none; user-select: none; background: #FAFCFF; gap: 8px; padding: 8px; } .buttons { grid-area: buttons; } .library-scroll { grid-area: bottom; } .program-scroll { grid-area: main; } .stage { grid-area: stage; } .sprite-list-scroll { grid-area: sprites; } .categories { grid-area: top; } .sprite-control { grid-area: control; } .paint-container { grid-area: main; } .costume-list { grid-area: bottom; } .toolbox-container { grid-area: top; } .library-scroll, .program-scroll, .sprite-list-scroll { min-width: 0; min-height: 0; position: relative; display: grid; } .stage, .program, .paint-container, .library { background: #FFF; border: 1px solid #68A; border-radius: 8px; } .stage { background: #DEF; width: 100%; } .stage:fullscreen { border: none; border-radius: 0; } .sprite-list { display: grid; gap: 8px; justify-content: start; overflow: auto hidden; grid-auto-flow: column; margin: -4px 0 -4px; margin-inline-start: -9px; border-left: 1px solid #68A; border-right: 1px solid #68A; padding: 4px 8px; } .sprite-list button { overflow: hidden; width: 128px; min-width: max-content; } .sprite-list .add-sprite { width: auto; } .program { position: relative; overflow: auto scroll; touch-action: pan-x pan-y; padding: 32px; display: grid; gap: 32px; align-content: start; min-height: 100%; } .categories { font-size: 0.5em; display: grid; grid-template: 1fr 1fr / 1fr 1fr 1fr 1fr 1fr; gap: 8px; } .categories button { display: grid; justify-items: center; } .categories button::before { content: ""; background: linear-gradient(#FFF4, #0000 75%) var(--color); width: 24px; height: 24px; border: 2px solid #0006; border-color: oklab(from var(--color) calc(l - 0.25) a b); border-radius: 256px; margin: 0 auto; } .library { padding: 16px; overflow: hidden auto; touch-action: pan-y; display: grid; gap: 32px; align-content: start; background: #DEF; position: relative; width: 0; min-width: 100%; } .library-category { display: grid; gap: 24px; } .category-name { font-weight: bold; margin: 0 0 16px; } .sprite-control { display: grid; grid-auto-flow: column; gap: 8px; justify-content: start; } .buttons { display: grid; grid-auto-flow: column; gap: 8px; justify-content: start; } .logo { display: grid; align-items: center; transition: 0.25s ease-in-out filter; } .logo:hover { filter: drop-shadow(0 0 2px #0001); } .logo > svg { height: 32px; } button, .button { color: #444; background: linear-gradient(#FFF8, #0000, #48F1) #FAFAFA; border: 1px solid #68A; font: inherit; padding: 4px 8px; cursor: pointer; border-radius: 8px; text-align: center; transition: 0.125s ease-in-out background; display: inline-grid; align-items: center; justify-items: center; } .button > .icon:only-child, button > .icon:only-child { margin: 0 -4px; } button:hover, .button:hover { background-color: #DEF; } .start-button:hover { background-color: #DFE; } .stop-button:hover { background-color: #FDE; } button:active, .button:active { background-image: linear-gradient(#0000, #FFF4); } button:disabled { background: linear-gradient(#0000, #FFF4) #CCC; cursor: auto; } .library-category > button { margin-inline-end: auto; } .paint-container { overflow: hidden; display: flex; align-content: center; justify-content: center; align-items: center; justify-items: center; } .costume-list { display: grid; align-content: start; gap: 8px; } .costume-list button { gap: 16px; grid-template-columns: auto 1fr; padding: 8px; justify-items: start; } .costume-list img { width: 48px; height: 48px; object-fit: contain; background: #FFF; border-radius: 4px; padding: 8px; border: 1px solid #68A; } .toolbox { display: flex; gap: 0.5em; } .scrollbar { position: absolute; touch-action: none; } .scrollbar-vertical { top: 0; bottom: 0; right: -8px; width: 8px; } .scrollbar-vertical::before { width: 50%; height: 25%; left: 25%; top: calc(var(--x) * 75%); } .scrollbar-horizontal { left: 0; right: 0; bottom: -8px; height: 8px; } .scrollbar-horizontal::before { width: 25%; height: 50%; top: 25%; left: calc(var(--x) * 75%); } .scrollbar-horizontal:dir(rtl)::before { left: auto; right: calc(var(--x) * -75%); } .scrollbar::before { content: ""; position: absolute; background: #68A; opacity: 0; transition: 0.5s ease opacity; transition-delay: 1s; border-radius: 2048px; } .scrollbar:hover::before, .scrollbar:active::before { opacity: 1; transition-delay: 0s; } .categories button, .category-name, .missing-message, .add-extension, .ok, .cancel, .make, .add-input, .atomic-message, .choose-scope { text-transform: lowercase; } @media (max-width: 512px) { body { grid-template: "buttons buttons buttons" auto "stage bottom bottom" auto "top bottom bottom" auto "sprites sprites control" auto "main main main" 1fr / 180px 1fr; } .categories { grid-template: 1fr 1fr 1fr 1fr 1fr / 1fr 1fr; grid-auto-flow: column; gap: 0.5em; } .categories button { grid-auto-flow: column; align-items: center; justify-content: start; text-align: left; gap: 0.5em; padding: 0.5em; } .categories button::before { width: 12px; height: 12px; border-width: 1px; } .library { height: 0; min-height: 100%; } } ` let style = document.createElement("style") style.textContent = css document.head.append(style) export let stage = await main() document.title = "Scrax / " + stage.name registerSprite(stage, StageAreas, getMessage("gui.stageSelector.stage") ?? "Stage") for (let sprite of stage.sprites) registerSprite(sprite) show(stage.sprites[0] ?? stage) document.body.append(buttonsElement, spriteListScroll, controlElement, stage.element)