ref: 86a1bffad7063d1f51dfe44c2126bbdf8b7fe5c8
dir: /view.js/
import {BooleanSlot, Variable, List, TextParameter, BooleanParameter, Define} from "./core.js" export let categories = ["motion", "looks", "sound", "events", "control", "sensing", "operators", "variables", "lists", "custom", "extensions"] function toChildElement(block, child, options) { if (child instanceof Array) child = child[0] if (typeof child === "string") child = document.createTextNode(child) if (child instanceof Node) return child return toSelectElement(block, child.options, options) } function toChildrenElement(block, options) { let inputs = [] for (let [i, reference] of block.type.references?.entries() ?? []) { inputs.push(toReferenceElement(block, options[reference + "s"] ?? [block.names[0]], i, options)) } for (let child of block.inputs) { inputs.push(toReporterElement(child, options)) } let name = options.getName?.(block.type, ...inputs) ?? block.type.name(...inputs) return name.map(child => toChildElement(block, child, options)) } function resizeSelect(element) { let div = document.createElement("div") div.classList.add("block", "stack") div.style.setProperty("white-space", "pre") div.style.setProperty("position", "fixed") div.style.setProperty("width", "max-content") let select = document.createElement("select") let option = document.createElement("option") option.append(element.selectedOptions[0]?.textContent ?? "") select.append(option) div.append(select) document.body.append(div) let width = select.offsetWidth div.remove() element.style.setProperty("width", `${width}px`) } function resizeInput(input) { let span = document.createElement("span") span.classList.add("input") span.style.setProperty("white-space", "pre") span.append(input.value) span.style.setProperty("position", "fixed") span.style.setProperty("width", "max-content") document.body.append(span) let width = span.offsetWidth span.remove() input.style.setProperty("width", `${width}px`) } function toReferenceElement(block, names, index, options) { let element = document.createElement("select") for (let name of names) { let option = document.createElement("option") element.append(option) option.append(name) if (name === block.names[index]) option.selected = true } if (options.frozen) element.disabled = true element.addEventListener("change", () => { resizeSelect(element) block.names[index] = element.value }) resizeSelect(element) return element } function toReporterElement(block, options) { if (block.type.shape === "slot") return toSlotElement(block, options) let element = document.createElement("span") element.classList.add("block", "reporter", categories.includes(block.type.category) ? block.type.category : "extensions") if (block.type.shape === "reporter" && block.type.output === "boolean") { element.classList.add("boolean") } if ([Variable, List, TextParameter, BooleanParameter].some(fn => fn.type === block.type)) { element.append(block.names[0]) } else { let children = toChildrenElement(block, options) element.append(...children) } element.dataset.id = block.id return element } function toSlotElement(block, options) { if (block.type === BooleanSlot.type) { let span = document.createElement("span") span.classList.add("slot") span.dataset.id = block.id return span } if (block.type.toElement) return block.type.toElement(block, {frozen: options.frozen}) if (block.type.options) return toEnumeratedElement(block, options) let input = document.createElement("input") input.classList.add("input") input.value = block.value if (options.frozen) input.disabled = true resizeInput(input) input.addEventListener("input", () => resizeInput(input)) input.addEventListener("blur", () => { let value = block.type.normalise(input.value) input.value = value block.value = value resizeInput(input) }) let span = document.createElement("span") span.append(input) span.dataset.id = block.id return span } function toEnumeratedElement(block, options) { let element = document.createElement("select") element.dataset.id = block.id if (!block.type.canFit) element.classList.add("allow-block") if (block.type.dynamic) { let optionElement = document.createElement("option") element.append(optionElement) optionElement.append(options.getAddMessage?.(block.type) ?? "add another") if (!options.addValue) optionElement.disabled = true } for (let option of block.type.options) { let optionElement = document.createElement("option") element.append(optionElement) optionElement.append(options.getName?.(block.type, option) ?? block.type.name?.(option) ?? option) optionElement.value = option } if (options.frozen) element.disabled = true if (element.querySelectorAll("option:enabled").length <= 1) element.disabled = true element.addEventListener("change", () => { if (block.type.dynamic && element.selectedIndex === 0) { options.addValue?.(block) element.value = block.value return } block.value = element.value resizeSelect(element) }) element.value = block.value resizeSelect(element) return element } function toSelectElement(block, options, options0) { let element = document.createElement("select") let actions = [] for (let option of options) { let optionElement = document.createElement("option") element.append(optionElement) optionElement.append(option.label) actions.push(() => { let other = option.morph(block) if (other.type.cap && block.parent && block.parent.stack.indexOf(block) !== block.parent.stack.length - 1) { for (let [i, option] of options.entries()) { if (option.type === block.type) { element.children[i].selected = true break } } return } let element0 = element.closest(`[data-id="${block.id}"]`) element0.dataset.id = other.id element0.classList.toggle("cap", Boolean(other.type.cap)) options0.replace(block, other, Boolean(option.update)) block = other }) if (block.type === option.type) optionElement.selected = true } if (!options0.replace || options0.frozen) element.disabled = true if (element.querySelectorAll("option:enabled").length <= 1) element.disabled = true element.addEventListener("change", () => { actions[element.selectedIndex]() resizeSelect(element) }) resizeSelect(element) return element } function toSingleElement(block, options) { if (block.type.shape === "reporter") return toReporterElement(block, options) if (block.type === Define.type) return toDefineElement(block, options) if (block.type.category === "custom") return toCustomBlockElement(block, options) let div = document.createElement("div") div.dataset.id = block.id div.classList.add("block", "stack", categories.includes(block.type.category) ? block.type.category : "extensions") let line = document.createElement("div") line.classList.add("line") line.append(...toChildrenElement(block, options)) div.append(line) if (block.type.hat) { let div2 = document.createElement("div") div2.append(div) div.classList.add("hat") for (let child of block.stack) div2.append(toSingleElement(child, options)) return div2 } else { if (block.stack) { div.classList.add("c") let mouth = document.createElement("div") mouth.classList.add("mouth") div.append(mouth) for (let child of block.stack) mouth.append(toSingleElement(child, options)) } } while (block.complement) { block = block.complement let line = document.createElement("div") line.classList.add("line") line.dataset.id = block.id div.append(line) line.append(...toChildrenElement(block, options)) if (block.stack) { let mouth = document.createElement("div") mouth.classList.add("mouth") div.append(mouth) for (let child of block.stack) mouth.append(toSingleElement(child, options)) } } if (block.stack) { let line = document.createElement("div") line.classList.add("line") div.append(line) if (block.type.loop) line.append(getIcon("loop-arrow")) } if (block.type.cap) div.classList.add("cap") return div } function toDefineElement(block, options) { let div = document.createElement("div") div.dataset.id = block.id div.classList.add("block", "stack", "define", "custom") let line = document.createElement("div") line.classList.add("line") div.append(line) let div1 = document.createElement("div") div1.append(div) for (let child of block.stack) div1.append(toSingleElement(child, options)) let div2 = document.createElement("div") div2.classList.add("block", "stack", "custom", "prototype") if (block.atomic) div2.append(getIcon("bolt")) let line2 = document.createElement("div") line2.classList.add("line") div2.append(line2) line.append(...options.getName?.(block.type, div2) ?? block.type.name(div2)) for (let part of block.names[0]) { if (typeof part === "string") { let span = document.createElement("span") span.append(part) line2.append(span) continue } let element = document.createElement("span") element.classList.add("block", "reporter", "custom", "parameter") if (part.type === "boolean") element.classList.add("boolean") element.append(part.name) line2.append(element) } return div1 } function toCustomBlockElement(block, options) { let div = document.createElement("div") div.classList.add("block", "stack", "custom") let line = document.createElement("div") line.classList.add("line") div.append(line) div.dataset.id = block.id let i = 0 for (let part of block.names[0]) { if (typeof part === "string") { line.append(part) continue } let element = toReporterElement(block.inputs[i], options) element.dataset.index = i++ line.append(element) } return div } export function toElement(block, options = {}) { let stack if (!block.parent || block.type.shape !== undefined) stack = [block] else stack = block.parent.stack.slice(block.parent.stack.indexOf(block)) let div = document.createElement("div") div.classList.add("script") div.append(...stack.map(block => toSingleElement(block, options))) return div } export function query(id, node = document) { return node.querySelector(`[data-id="${id}"]`) } export function closestScriptID(element0) { if (element0.closest(":enabled")) return let element = element0.closest(".script") if (!element) return element = element.querySelector(".hat") ?? element.querySelector(".define") if (!element) return return Number(element.dataset.id) } export function closestBlockID(element0) { if (element0.closest(":enabled")) return let element = element0.closest(".block[data-id]") if (!element) return return Number(element.dataset.id) } export function closestParameterIndex(element0) { let element = element0.closest(".parameter:not([data-id])") if (!element) return let define = element.closest(".define") let parameters = [...define.querySelectorAll(".parameter")] return {index: parameters.indexOf(element), id: Number(define.dataset.id), element} } export function closestSlotID(element0) { let element = element0.closest(".slot") if (!element) return return Number(element.dataset.id) } export function closestNamePartIndex(element0) { let element = element0.closest(".define > .line > .custom > .line > span") if (!element) return let block = element.closest(".define") return {index: [...element.parentNode.childNodes].indexOf(element), id: Number(block.dataset.id)} } function getIcon(name) { let span = document.createElement("span") span.insertAdjacentHTML("beforeend", icons[name]) let svg = span.querySelector("svg") svg.style.setProperty("height", "1.5em") svg.style.setProperty("vertical-align", "middle") return span } let icons = { "loop-arrow": `<svg viewbox="3 0 16 24" fill="#EEE" stroke="#444" class="loop-arrow"><path transform-origin="12 12" transform="rotate(90)" d="M10 9V7.41c0-.89-1.08-1.34-1.71-.71L3.7 11.29c-.39.39-.39 1.02 0 1.41l4.59 4.59c.63.63 1.71.19 1.71-.7V14.9c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/></svg>`, bolt: `<svg viewbox="6 0 12 24" fill="#EEE" stroke="#444"><path d="M10.67,21L10.67,21c-0.35,0-0.62-0.31-0.57-0.66L11,14H7.5c-0.88,0-0.33-0.75-0.31-0.78c1.26-2.23,3.15-5.53,5.65-9.93 c0.1-0.18,0.3-0.29,0.5-0.29h0c0.35,0,0.62,0.31,0.57,0.66L13.01,10h3.51c0.4,0,0.62,0.19,0.4,0.66c-3.29,5.74-5.2,9.09-5.75,10.05 C11.07,20.89,10.88,21,10.67,21z"/></svg>`, } let css = ` .block { color: #444; pointer-events: none; white-space: nowrap; } .line, .mouth::before, .reporter { background: var(--color); border: 2px solid #0006; border-color: oklab(from var(--color) calc(l - 0.25) a b); background-origin: border-box; pointer-events: auto; touch-action: none; cursor: move; } .stack { width: max-content; position: relative; } .mouth { width: 0; margin-inline-start: 1.125em; position: relative; min-height: 2em; } .line { padding: 0.5em; border-radius: 0.5em; margin-bottom: -2px; position: relative; min-height: 2em; display: grid; grid-auto-flow: column; align-items: baseline; gap: 0.5em; justify-content: start; min-width: 4em; } :not(.prototype) > .line:not(:first-child):last-child { padding: 0 0.5em; justify-content: end; align-content: center; } .c > .line:first-child, :not(.c) > .line, .reporter { background-image: linear-gradient(#FFF4, #0000 75%); } .c > .line:not(first-child):last-child { background-image: linear-gradient(#0000 25%, #0001); } .c > .line:not(first-child):last-child::after { background-image: linear-gradient(#0001, #0001); } .c > .line:not(:first-child):not(:dir(rtl)) { border-top-left-radius: 0; } .c > .line:not(:last-child):not(:dir(rtl)) { border-bottom-left-radius: 0; } .c > .line:not(:first-child):dir(rtl) { border-top-right-radius: 0; } .c > .line:not(:last-child):dir(rtl) { border-bottom-right-radius: 0; } .c > .line { min-width: 10em; } .block select { background: linear-gradient(#FFF4, #0000 75%) var(--color); font: inherit; color: inherit; padding: 0.25em 0.5em; border: 1px solid #0006; border-radius: 0.5em; cursor: pointer; } .block select.allow-block { border-radius: 128em; background: linear-gradient(#6662, #FFF4); border-width: 2px; } .block select:disabled, .input:disabled { pointer-events: none; } .input { background: linear-gradient(#EEE, #FFF); font: inherit; color: #666; padding: 0.25em 0.5em; border: 2px solid #888; border-radius: 128em; cursor: text; min-width: 2.5em; text-align: center; overflow: hidden; } .input:empty::before { content: " "; white-space: pre; } .line::before, .line::after { content: ""; width: 1.5em; height: 0.5em; position: absolute; inset-inline-start: 1em; border-radius: 0 0 calc(0.5em - 2px) calc(0.5em - 2px); } .line::before { top: 0; background: linear-gradient(#0006, #0008); background: linear-gradient(#0000, #0004) oklab(from var(--color) calc(l - 0.25) a b); background-clip: padding-box; width: 1.5em; height: calc(0.5em - 2px); } .line::after { top: 100%; background-color: inherit; z-index: 1; border: 2px solid #0006; border-color: oklab(from var(--color) calc(l - 0.25) a b); border-top: none; } .c > .line:not(:first-child)::before, .c > .line:not(:last-child)::after { inset-inline-start: calc(2em + 2px); } .mouth::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1.125em; width: 1.25em; border-width: 0 2px; z-index: 1; } .mouth:empty::before { bottom: -2px; } .hat { padding-top: 1em; } .hat > .line { min-width: 8em; } .hat > .line:not(:dir(rtl)) { border-top-left-radius: 0; } .hat > .line:dir(rtl) { border-top-right-radius: 0; } .hat > .line:last-child::before { content: ""; background: linear-gradient(#FFF4, #FFF4); background-color: inherit; border: 2px solid #0006; border-color: oklab(from var(--color) calc(l - 0.25) a b); border-bottom: none; position: absolute; top: auto; inset-inline-start: -2px; bottom: 100%; width: 6em; height: 1em; border-radius: 50% 50% 0 0 / 100% 100% 0 0; } .define > .line:last-child::before { content: none; } .define > .line { border-top-left-radius: 1.5em; border-top-right-radius: 1.5em; padding: 1em; padding-inline-start: 0.5em; } .prototype { display: flex; gap: 0.5em; align-items: baseline; } .cap > .line:last-child::after { content: none; } .reporter { display: grid; grid-auto-flow: column; align-items: center; padding: 0.25em 0.5em; border-radius: 100em; width: max-content; grid-gap: 0.5em; } .boolean { border-width: 2px 3px; border-radius: 0; padding: 0.25em 1em; clip-path: polygon(0 50%, 8888em -8888em, calc(100% - 8888em) -8888em, 100% 50%, calc(100% - 8888em) 8888em, 8888em 8888em); position: relative; overflow: hidden; } .boolean::before, .boolean::after { content: ""; border: 888em solid #0000; border-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="4" height="4" fill="%230006"><path d="M 0 2 2 0 4 2 2 4 M 0 0 0 4 4 4 4 0" /></svg>') 50%; position: absolute; top: 50%; transform: translate(0, -50%); pointer-events: none; width: 64em; } .boolean::before { left: 0; } .boolean::after { right: 0; } .slot { background: linear-gradient(#0008, #0004); clip-path: polygon(0 50%, 88em -88em, calc(100% - 88em) -88em, 100% 50%, calc(100% - 88em) 88em, 88em 88em); width: 3em; height: 1.5em; display: grid; } .slot::before { content: "x"; align-self: center; color: #0000; } .line > input, .parameter > input { color: inherit; font: inherit; background: none; border: none; padding: 0; margin: 0; min-width: 0.5em; } .loop-arrow:dir(rtl) { transform: scale(-1, 1); } .control { --color: #FB5; } .events { --color: #FD6; } .looks { --color: #CBF; } .sound { --color: #E9E; } .variables { --color: #F94; } .operators { --color: #8C8; } .lists { --color: #F96; } .motion { --color: #8BF; } .sensing { --color: #8CE; } .custom { --color: #F9A; } .define, .parameter { --color: #FBC; } .extensions { --color: #1EA; } ` let paintCSS = ` .line { background: none !important; border: 2px solid #0000; border-radius: 0; pointer-events: visiblepainted; } .line::before { content: none !important; } .line::after { content: paint(stack-block) !important; border: none; border-radius: 0; pointer-events: visiblepainted; top: -2px !important; bottom: -0.5em !important; left: -2px !important; right: -2px !important; width: auto; height: auto; z-index: -1; pointer-events: none; background: none !important; } .line:dir(rtl)::after { transform: scale(-1, 1); } .boolean { clip-path: none; background: paint(boolean-block) border-box !important; border-color: #0000 !important; } .boolean::before, .boolean::after { content: none; } .hat > .line { --hat: true; } .cap > .line:last-child { --cap: true; } .define > .line::after { --define: true; } .c > .line:not(:last-child) { --mouth-below: true; } .c > .line:not(:first-child) { --mouth-above: true; } .hat > .line::after { top: calc(-2px - 1em) !important; } .c > .line:not(:first-child)::after { top: -0.5em !important; } .mouth::before { z-index: -2; } .c > .line:first-child { z-index: 4100; } .script { position: relative; } ` if (CSS.paintWorklet) { CSS.paintWorklet?.addModule(import.meta.resolve("./worklet.js")) css += paintCSS for (let i = 0 ; i <= 4000 ; i++) css += `.block.stack:nth-child(${i + 1}) { z-index: ${4000 - i}; }\n` } let style = document.createElement("style") style.textContent = css document.head.append(style)