ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
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)