shithub: scrax

ref: 81d1e4e01d7001e6a0107792e6167136787295d6
dir: /index.js/

View raw version
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)