q-style appRoot { width: 100%; min-height: 100vh; display: grid; grid-template-columns: minmax(280px, 22vw) 1fr; gap: 18px; padding: 18px; box-sizing: border-box; } q-style panel { background: rgba(15, 23, 42, 0.94); border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 18px; box-shadow: 0 28px 80px rgba(0, 0, 0, 0.38); } q-style sidePanel { min-width: 0; display: grid; grid-template-rows: auto auto 1fr; gap: 14px; overflow: hidden; } q-style sideHead { padding: 20px; border-bottom: 1px solid rgba(148, 163, 184, 0.22); } q-style titleText { margin: 0; font-size: 24px; font-weight: 900; letter-spacing: 0; } q-style subText { margin: 8px 0 0 0; color: #94a3b8; line-height: 1.45; font-size: 14px; } q-style toolbar { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 0 20px; } q-style buttonBase { min-height: 42px; border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 10px; background: #1f2937; color: #e5e7eb; font-weight: 850; cursor: pointer; } q-style buttonPrimary { background: #22c55e; color: #04130a; border-color: rgba(255, 255, 255, 0.18); box-shadow: 0 14px 38px rgba(34, 197, 94, 0.22); } q-style buttonDanger { background: rgba(239, 68, 68, 0.18); color: #fecaca; border-color: rgba(239, 68, 68, 0.42); } q-style emitterListWrap { min-height: 0; display: grid; grid-template-rows: auto 1fr auto; gap: 10px; overflow: auto; padding: 0 20px 20px 20px; } q-style labelCaps { margin: 0 0 9px 0; color: #94a3b8; font-size: 12px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.08em; } q-style emitterList { display: flex; flex-wrap: wrap; align-content: start; gap: 8px; min-height: 0; overflow: auto; } q-style emitterChip { min-height: 36px; max-width: 100%; border: 1px solid rgba(148, 163, 184, 0.28); border-radius: 999px; background: rgba(15, 23, 42, 0.78); color: #e5e7eb; padding: 0 13px; font-size: 14px; font-weight: 800; cursor: pointer; } q-style selectedChip { color: #04130a; background: #22c55e; border-color: transparent; } q-style workspace { min-width: 0; min-height: 0; display: grid; grid-template-rows: 1fr; } q-style stageShell { min-width: 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 10px; } q-style stageLabel { display: flex; justify-content: space-between; align-items: baseline; gap: 18px; color: #94a3b8; font-size: 14px; } q-style stageStrong { color: #e5e7eb; } q-style stage { position: relative; width: 100%; min-height: 0; overflow: hidden; border-radius: 22px; border: 1px solid rgba(148, 163, 184, 0.36); background: linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px), radial-gradient(circle at 50% 100%, rgba(34, 197, 94, 0.16), transparent 45%), linear-gradient(180deg, #020617, #0f172a); background-size: 32px 32px, 32px 32px, 100% 100%, 100% 100%; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05), 0 28px 80px rgba(0, 0, 0, 0.36); } q-style modalBackdrop { position: fixed; inset: 0; z-index: 1000; display: none; place-items: center; padding: 24px; box-sizing: border-box; background: rgba(2, 6, 23, 0.74); } q-style modalOpen { display: grid; } q-style dialog { width: min(1180px, 96vw); max-height: 90vh; overflow: auto; } q-style formPanel { min-width: 0; padding: 20px; } q-style formGrid { display: grid; grid-template-columns: repeat(3, minmax(220px, 1fr)); gap: 14px; align-items: start; } q-style fieldGroup { min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding: 14px; border: 1px solid rgba(148, 163, 184, 0.22); border-radius: 14px; background: rgba(2, 6, 23, 0.28); } q-style fieldGroupDouble { grid-column: span 2; } q-style fieldGroupFull { grid-column: 1 / -1; } q-style groupTitle { grid-column: 1 / -1; padding: 0; color: #e5e7eb; font-size: 13px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.08em; } q-style field { display: grid; gap: 6px; } q-style fieldWide { grid-column: 1 / -1; } q-style fieldLabel { color: #94a3b8; font-size: 12px; font-weight: 850; } q-style inputBase { width: 100%; min-width: 0; min-height: 38px; box-sizing: border-box; border: 1px solid rgba(148, 163, 184, 0.30); border-radius: 9px; background: rgba(2, 6, 23, 0.68); color: #e5e7eb; padding: 8px 10px; } q-style textareaBase { width: 100%; min-height: 214px; box-sizing: border-box; border: 1px solid rgba(148, 163, 184, 0.30); border-radius: 12px; background: rgba(2, 6, 23, 0.76); color: #dbeafe; padding: 12px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; line-height: 1.5; } q-style exportPanel { min-width: 0; padding: 18px; display: grid; gap: 12px; width: min(760px, 94vw); max-height: 82vh; } q-style exportActions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } q-style mobileRoot { grid-template-columns: 1fr; } q-theme particleEditorTheme { div.app { appRoot } aside.sidebar { panel sidePanel } header.side-head { sideHead } h1.title { titleText } p.subtitle { subText } div.toolbar { toolbar } button.tool { buttonBase } button.primary { buttonPrimary } button.danger { buttonDanger } section.emitter-list-wrap { emitterListWrap } p.label { labelCaps } div.emitter-list { emitterList } button.emitter-chip { emitterChip } button.emitter-chip-selected { selectedChip } main.workspace { workspace } section.stage-shell { stageShell } div.stage-label { stageLabel } strong.stage-name { stageStrong } div.stage { stage } div.modal-backdrop { modalBackdrop } div.modal-open { modalOpen } div.dialog { dialog } div.form-panel { panel formPanel } div.form-grid { formGrid } fieldset.field-group { fieldGroup } fieldset.field-group-double { fieldGroupDouble } fieldset.field-group-full { fieldGroupFull } legend.group-title { groupTitle } label.field { field } label.wide { fieldWide } span.field-label { fieldLabel } input.editor-input { inputBase } textarea.editor-textarea { textareaBase } div.export-panel { panel exportPanel } div.export-actions { exportActions } } q-component particleEditorApp { q-property emitters: q-script { return []; } q-property selectedIndex: -1 q-property counter: 0 q-property pathDraft: q-script { return []; } q-property pathEditingIndex: -1 function defaultEmitter() { this.counter = this.counter + 1; return { name: "emitter" + this.counter, id: "particle-emitter-" + this.counter, running: "true", src: "assets/particle.png", mask: "", emittermask: "", color: "#08c473", coloropacity: "0.28", emitrate: "74", interval: "10", maxactiveparticles: "166", maxactiveparticlesvariation: "18", lifetime: "700", lifetimevariation: "90", x: "110", y: "114", path: "", duration: "3000", delay: "0", sleep: "0", width: "0", height: "0", xvariation: "845", yvariation: "20", xvelocity: "0", yvelocity: "-1.75", xvelocityvariation: "0", yvelocityvariation: "0.04", xacceleration: "0", yacceleration: "0.0028", xaccelerationvariation: "0", yaccelerationvariation: "0.005", startsize: "25", endsize: "1", startsizevariation: "7", endsizevariation: "5", startopacity: "0.5", endopacity: "0.02", startopacityvariation: "0.16", endopacityvariation: "0.02", zindex: "1", style: "position: absolute; inset: 0px; display: block; pointer-events: none; overflow: hidden; z-index: 1;" }; } function stage() { return this.querySelector("#particle-stage"); } function input(id) { return this.querySelector("#" + id); } function fieldValue(id) { return this.input(id).value; } function fieldChecked(id) { return this.input(id).checked ? "true" : "false"; } function setValue(id, value) { this.input(id).value = value == null ? "" : String(value); } function setChecked(id, value) { this.input(id).checked = String(value) == "true"; } function createEmitterElement(data) { var element = document.createElement("particle-emitter"); data.element = element; this.applyEmitterAttributes(data); this.stage().appendChild(element); return element; } function fieldDefinitions() { return [ { title: "Identity", className: "field-group", fields: [ ["Name", "pe-name", "text", "", "", ""], ["Element ID", "pe-id", "text", "", "", ""], ["Running", "pe-running", "checkbox", "", "", ""] ] }, { title: "Sprite / Image", className: "field-group field-group-double", fields: [ ["Image src", "pe-src", "text", "", "", ""], ["Sprite mask", "pe-mask", "text", "", "", ""], ["Emitter mask", "pe-emittermask", "text", "", "", ""], ["Tint Color", "pe-color", "color", "", "", ""], ["Tint Opacity", "pe-coloropacity", "number", "0.01", "0", "1"] ] }, { title: "Emission", className: "field-group", fields: [ ["Emit Rate", "pe-emitrate", "number", "1", "", ""], ["Interval", "pe-interval", "number", "1", "", ""], ["Max Active", "pe-maxactiveparticles", "number", "1", "", ""], ["Max Active Variation", "pe-maxactiveparticlesvariation", "number", "1", "", ""] ] }, { title: "Lifetime", className: "field-group", fields: [ ["Lifetime", "pe-lifetime", "number", "1", "", ""], ["Lifetime Variation", "pe-lifetimevariation", "number", "1", "", ""] ] }, { title: "Position", className: "field-group", fields: [ ["X", "pe-x", "number", "1", "", ""], ["Y", "pe-y", "number", "1", "", ""], ["Emitter Width", "pe-width", "number", "1", "0", ""], ["Emitter Height", "pe-height", "number", "1", "0", ""], ["X Variation", "pe-xvariation", "number", "1", "", ""], ["Y Variation", "pe-yvariation", "number", "1", "", ""] ] }, { title: "Velocity", className: "field-group", fields: [ ["X Velocity", "pe-xvelocity", "number", "0.01", "", ""], ["Y Velocity", "pe-yvelocity", "number", "0.01", "", ""], ["X Velocity Variation", "pe-xvelocityvariation", "number", "0.01", "", ""], ["Y Velocity Variation", "pe-yvelocityvariation", "number", "0.01", "", ""] ] }, { title: "Acceleration", className: "field-group", fields: [ ["X Acceleration", "pe-xacceleration", "number", "0.0001", "", ""], ["Y Acceleration", "pe-yacceleration", "number", "0.0001", "", ""], ["X Accel Variation", "pe-xaccelerationvariation", "number", "0.0001", "", ""], ["Y Accel Variation", "pe-yaccelerationvariation", "number", "0.0001", "", ""] ] }, { title: "Size", className: "field-group", fields: [ ["Start Size", "pe-startsize", "number", "1", "", ""], ["End Size", "pe-endsize", "number", "1", "", ""], ["Start Size Variation", "pe-startsizevariation", "number", "1", "", ""], ["End Size Variation", "pe-endsizevariation", "number", "1", "", ""] ] }, { title: "Opacity", className: "field-group", fields: [ ["Start Opacity", "pe-startopacity", "number", "0.01", "0", "1"], ["End Opacity", "pe-endopacity", "number", "0.01", "0", "1"], ["Start Opacity Variation", "pe-startopacityvariation", "number", "0.01", "0", "1"], ["End Opacity Variation", "pe-endopacityvariation", "number", "0.01", "0", "1"] ] }, { title: "Element Style", className: "field-group field-group-full", fields: [ ["Style", "pe-style", "textarea", "", "", ""], ["Z Index", "pe-zindex", "number", "1", "", ""] ] } ]; } function appendPathNumberField(fieldset, captionText, id, step, min) { var label = document.createElement("label"); var caption = document.createElement("span"); var numberInput = document.createElement("input"); label.className = "field"; caption.className = "field-label"; caption.textContent = captionText; numberInput.className = "editor-input"; numberInput.id = id; numberInput.type = "number"; numberInput.step = step; numberInput.min = min; label.appendChild(caption); label.appendChild(numberInput); fieldset.appendChild(label); } function buildPathFields(grid) { var editor = this; var fieldset = document.createElement("fieldset"); var legend = document.createElement("legend"); var controlRow = document.createElement("div"); var pathButton = document.createElement("button"); var pathLabel = document.createElement("label"); var pathCaption = document.createElement("span"); var pathInput = document.createElement("input"); var note = document.createElement("div"); fieldset.className = "field-group field-group-full"; legend.className = "group-title"; legend.textContent = "Emitter Path"; controlRow.className = "path-control-row"; pathButton.type = "button"; pathButton.id = "setEmitterPathButton"; pathButton.className = "tool primary path-editor-button"; pathButton.textContent = "Set Path"; pathLabel.className = "field"; pathCaption.className = "field-label"; pathCaption.textContent = "Normalized Path Points (X Y X Y ...)"; pathInput.className = "editor-input"; pathInput.id = "pe-path"; pathInput.type = "text"; pathInput.placeholder = "0.1 0.5 0.5 0.2 0.9 0.8"; note.id = "pathPointStatus"; note.className = "path-field-note"; pathLabel.appendChild(pathCaption); pathLabel.appendChild(pathInput); controlRow.appendChild(pathButton); controlRow.appendChild(pathLabel); fieldset.appendChild(legend); fieldset.appendChild(controlRow); fieldset.appendChild(note); this.appendPathNumberField(fieldset, "Duration (ms)", "pe-duration", "1", "1"); this.appendPathNumberField(fieldset, "Delay (ms)", "pe-delay", "1", "0"); this.appendPathNumberField(fieldset, "Sleep (ms)", "pe-sleep", "1", "0"); grid.appendChild(fieldset); pathButton.addEventListener("click", function() { editor.beginPathEditor(); }); pathInput.addEventListener("input", function() { editor.updatePathStatus(); }); pathInput.addEventListener("change", function() { editor.normalizePathInput(); editor.updateSelected(); editor.updatePathStatus(); }); } function createPathOverlay() { var stage = this.stage(); var existing = this.querySelector("#pathEditorOverlay"); if (existing) { return existing; } var editor = this; var overlay = document.createElement("div"); var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); var hint = document.createElement("div"); overlay.id = "pathEditorOverlay"; overlay.className = "path-editor-overlay"; svg.id = "pathEditorSvg"; hint.className = "path-editor-overlay-hint"; hint.textContent = "Click anywhere on the stage to append a normalized path point"; overlay.appendChild(svg); overlay.appendChild(hint); stage.appendChild(overlay); overlay.addEventListener("click", function(event) { editor.addPathPoint(event); }); window.addEventListener("resize", function() { if (overlay.style.display != "none") { editor.renderPathOverlay(); } }); return overlay; } function parsePath(value) { var text = value == null ? "" : String(value); var values = text.trim() == "" ? [] : text.trim().split(/[\s,]+/); var points = []; var index = 0; while (index + 1 < values.length) { var x = Number(values[index]); var y = Number(values[index + 1]); if (Number.isFinite(x)) { if (Number.isFinite(y)) { points.push({ x: Math.max(0, Math.min(1, x)), y: Math.max(0, Math.min(1, y)) }); } } index = index + 2; } return points; } function formatPathCoordinate(value) { var text = Number(value).toFixed(4); text = text.replace(/0+$/, ""); text = text.replace(/\.$/, ""); return text == "" ? "0" : text; } function serializePath(points) { var editor = this; var values = []; points.forEach(function(point) { values.push(editor.formatPathCoordinate(point.x)); values.push(editor.formatPathCoordinate(point.y)); }); return values.join(" "); } function normalizePathInput() { var pathInput = this.input("pe-path"); var normalized = this.serializePath(this.parsePath(pathInput.value)); pathInput.value = normalized; return normalized; } function beginPathEditor() { if (this.selectedIndex < 0) { return; } this.normalizePathInput(); this.updateSelected(); this.pathEditingIndex = this.selectedIndex; this.pathDraft = this.parsePath(this.emitters[this.selectedIndex].path); this.closeEditorDialog(); this.createPathOverlay().style.display = "block"; this.querySelector("#pathEditorControls").style.display = "grid"; this.renderPathOverlay(); } function addPathPoint(event) { var overlay = this.querySelector("#pathEditorOverlay"); var rect = overlay.getBoundingClientRect(); if (rect.width <= 0) { return; } if (rect.height <= 0) { return; } this.pathDraft.push({ x: Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)), y: Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height)) }); this.renderPathOverlay(); } function undoPathPoint() { if (this.pathDraft.length > 0) { this.pathDraft.pop(); this.renderPathOverlay(); } } function clearPathPoints() { this.pathDraft = []; this.renderPathOverlay(); } function finishPathEditor(saveChanges) { var overlay = this.querySelector("#pathEditorOverlay"); if (overlay) { overlay.style.display = "none"; } this.querySelector("#pathEditorControls").style.display = "none"; if (saveChanges == true) { this.selectedIndex = this.pathEditingIndex; this.setValue("pe-path", this.serializePath(this.pathDraft)); this.updateSelected(); this.updatePathStatus(); } this.pathEditingIndex = -1; this.pathDraft = []; this.openEditorDialog(); } function renderPathOverlay() { var overlay = this.querySelector("#pathEditorOverlay"); var svg = this.querySelector("#pathEditorSvg"); var status = this.querySelector("#pathEditorStatus"); var undoButton = this.querySelector("#undoPathButton"); var clearButton = this.querySelector("#clearPathButton"); var width = Math.max(1, overlay.clientWidth); var height = Math.max(1, overlay.clientHeight); var svgNamespace = "http://www.w3.org/2000/svg"; var pointsText = []; var index = 0; svg.setAttribute("viewBox", "0 0 " + width + " " + height); svg.innerHTML = ""; this.pathDraft.forEach(function(point) { pointsText.push((point.x * width) + "," + (point.y * height)); }); if (pointsText.length > 1) { var line = document.createElementNS(svgNamespace, "polyline"); line.setAttribute("points", pointsText.join(" ")); line.setAttribute("fill", "none"); line.setAttribute("stroke", "#22c55e"); line.setAttribute("stroke-width", "3"); line.setAttribute("stroke-linecap", "round"); line.setAttribute("stroke-linejoin", "round"); svg.appendChild(line); } while (index < this.pathDraft.length) { var point = this.pathDraft[index]; var circle = document.createElementNS(svgNamespace, "circle"); circle.setAttribute("cx", String(point.x * width)); circle.setAttribute("cy", String(point.y * height)); circle.setAttribute("r", index == 0 ? "7" : "5"); circle.setAttribute("fill", index == 0 ? "#f8fafc" : "#22c55e"); circle.setAttribute("stroke", "#020617"); circle.setAttribute("stroke-width", "2"); svg.appendChild(circle); index = index + 1; } status.textContent = this.pathDraft.length + " path point(s) — coordinates are stored from 0.0 to 1.0"; undoButton.disabled = this.pathDraft.length == 0; clearButton.disabled = this.pathDraft.length == 0; } function updatePathStatus() { var status = this.querySelector("#pathPointStatus"); if (!status) { return; } var count = this.parsePath(this.fieldValue("pe-path")).length; status.textContent = count == 0 ? "No path is defined. The emitter continues using its fixed X/Y origin." : count + " point(s) defined. Each transfer is linearly tweened for duration / number of points."; } function buildForm() { var editor = this; var grid = this.querySelector("#particle-form-grid"); grid.innerHTML = ""; this.fieldDefinitions().forEach(function(group) { var fieldset = document.createElement("fieldset"); var legend = document.createElement("legend"); fieldset.className = group.className; legend.className = "group-title"; legend.textContent = group.title; fieldset.appendChild(legend); group.fields.forEach(function(field) { var label = document.createElement("label"); var caption = document.createElement("span"); var input = field[2] == "textarea" ? document.createElement("textarea") : document.createElement("input"); label.className = "field"; caption.className = "field-label"; caption.textContent = field[0]; input.className = "editor-input"; input.id = field[1]; if (field[2] != "textarea") { input.type = field[2]; input.step = field[3]; input.min = field[4]; input.max = field[5]; } input.oninput = function() { editor.updateSelected(); }; input.onchange = function() { editor.updateSelected(); }; label.appendChild(caption); label.appendChild(input); fieldset.appendChild(label); }); grid.appendChild(fieldset); if (group.title == "Position") { editor.buildPathFields(grid); } }); this.createPathOverlay(); this.updatePathStatus(); } function wireForm() { var editor = this; this.querySelectorAll(".editor-input").forEach(function(input) { if (input.id != "pe-path") { input.oninput = function() { editor.updateSelected(); }; input.onchange = function() { editor.updateSelected(); }; } }); } function wireActions() { var editor = this; this.querySelector("#newEmitterButton").addEventListener("click", function() { editor.addEmitter(); }); this.querySelector("#deleteEmitterButton").addEventListener("click", function() { editor.deleteSelected(); }); this.querySelector("#deleteDialogButton").addEventListener("click", function() { editor.deleteSelected(); }); this.querySelector("#exportEmitterButton").addEventListener("click", function() { editor.openExportDialog(); }); this.querySelector("#copyExportButton").addEventListener("click", function() { editor.copyExport(); }); this.querySelector("#refreshExportButton").addEventListener("click", function() { editor.refreshExport(); }); this.querySelector("#closeEditorButton").addEventListener("click", function() { editor.closeEditorDialog(); }); this.querySelector("#closeExportButton").addEventListener("click", function() { editor.closeExportDialog(); }); this.querySelector("#undoPathButton").addEventListener("click", function() { editor.undoPathPoint(); }); this.querySelector("#clearPathButton").addEventListener("click", function() { editor.clearPathPoints(); }); this.querySelector("#cancelPathButton").addEventListener("click", function() { editor.finishPathEditor(false); }); this.querySelector("#savePathButton").addEventListener("click", function() { editor.finishPathEditor(true); }); } function openDialog(id) { this.querySelector("#" + id).style.display = "grid"; } function closeDialog(id) { this.querySelector("#" + id).style.display = "none"; } function openEditorDialog() { this.openDialog("editorDialog"); } function closeEditorDialog() { this.closeDialog("editorDialog"); } function openExportDialog() { this.refreshExport(); this.openDialog("exportDialog"); } function closeExportDialog() { this.closeDialog("exportDialog"); } function applyEmitterAttributes(data) { data.element.setAttribute("id", data.id); data.element.setAttribute("running", data.running); data.element.setAttribute("src", data.src); data.element.setAttribute("mask", data.mask); data.element.setAttribute("emittermask", data.emittermask); data.element.setAttribute("color", data.color); data.element.setAttribute("coloropacity", data.coloropacity); data.element.setAttribute("emitrate", data.emitrate); data.element.setAttribute("interval", data.interval); data.element.setAttribute("maxactiveparticles", data.maxactiveparticles); data.element.setAttribute("maxactiveparticlesvariation", data.maxactiveparticlesvariation); data.element.setAttribute("lifetime", data.lifetime); data.element.setAttribute("lifetimevariation", data.lifetimevariation); data.element.setAttribute("x", data.x); data.element.setAttribute("y", data.y); data.element.setAttribute("path", data.path); data.element.setAttribute("duration", data.duration); data.element.setAttribute("delay", data.delay); data.element.setAttribute("sleep", data.sleep); data.element.setAttribute("width", data.width); data.element.setAttribute("height", data.height); data.element.setAttribute("xvariation", data.xvariation); data.element.setAttribute("yvariation", data.yvariation); data.element.setAttribute("xvelocity", data.xvelocity); data.element.setAttribute("yvelocity", data.yvelocity); data.element.setAttribute("xvelocityvariation", data.xvelocityvariation); data.element.setAttribute("yvelocityvariation", data.yvelocityvariation); data.element.setAttribute("xacceleration", data.xacceleration); data.element.setAttribute("yacceleration", data.yacceleration); data.element.setAttribute("xaccelerationvariation", data.xaccelerationvariation); data.element.setAttribute("yaccelerationvariation", data.yaccelerationvariation); data.element.setAttribute("startsize", data.startsize); data.element.setAttribute("endsize", data.endsize); data.element.setAttribute("startsizevariation", data.startsizevariation); data.element.setAttribute("endsizevariation", data.endsizevariation); data.element.setAttribute("startopacity", data.startopacity); data.element.setAttribute("endopacity", data.endopacity); data.element.setAttribute("startopacityvariation", data.startopacityvariation); data.element.setAttribute("endopacityvariation", data.endopacityvariation); data.element.setAttribute("zindex", data.zindex); data.element.setAttribute("style", data.style); } function readForm() { var previous = this.emitters[this.selectedIndex]; return { name: this.fieldValue("pe-name"), id: this.fieldValue("pe-id"), running: this.fieldChecked("pe-running"), src: this.fieldValue("pe-src"), mask: this.fieldValue("pe-mask"), emittermask: this.fieldValue("pe-emittermask"), color: this.fieldValue("pe-color"), coloropacity: this.fieldValue("pe-coloropacity"), emitrate: this.fieldValue("pe-emitrate"), interval: this.fieldValue("pe-interval"), maxactiveparticles: this.fieldValue("pe-maxactiveparticles"), maxactiveparticlesvariation: this.fieldValue("pe-maxactiveparticlesvariation"), lifetime: this.fieldValue("pe-lifetime"), lifetimevariation: this.fieldValue("pe-lifetimevariation"), x: this.fieldValue("pe-x"), y: this.fieldValue("pe-y"), path: this.fieldValue("pe-path"), duration: this.fieldValue("pe-duration"), delay: this.fieldValue("pe-delay"), sleep: this.fieldValue("pe-sleep"), width: this.fieldValue("pe-width"), height: this.fieldValue("pe-height"), xvariation: this.fieldValue("pe-xvariation"), yvariation: this.fieldValue("pe-yvariation"), xvelocity: this.fieldValue("pe-xvelocity"), yvelocity: this.fieldValue("pe-yvelocity"), xvelocityvariation: this.fieldValue("pe-xvelocityvariation"), yvelocityvariation: this.fieldValue("pe-yvelocityvariation"), xacceleration: this.fieldValue("pe-xacceleration"), yacceleration: this.fieldValue("pe-yacceleration"), xaccelerationvariation: this.fieldValue("pe-xaccelerationvariation"), yaccelerationvariation: this.fieldValue("pe-yaccelerationvariation"), startsize: this.fieldValue("pe-startsize"), endsize: this.fieldValue("pe-endsize"), startsizevariation: this.fieldValue("pe-startsizevariation"), endsizevariation: this.fieldValue("pe-endsizevariation"), startopacity: this.fieldValue("pe-startopacity"), endopacity: this.fieldValue("pe-endopacity"), startopacityvariation: this.fieldValue("pe-startopacityvariation"), endopacityvariation: this.fieldValue("pe-endopacityvariation"), zindex: this.fieldValue("pe-zindex"), style: this.fieldValue("pe-style"), element: previous.element }; } function writeForm(data) { this.setValue("pe-name", data.name); this.setValue("pe-id", data.id); this.setChecked("pe-running", data.running); this.setValue("pe-src", data.src); this.setValue("pe-mask", data.mask); this.setValue("pe-emittermask", data.emittermask); this.setValue("pe-color", data.color); this.setValue("pe-coloropacity", data.coloropacity); this.setValue("pe-emitrate", data.emitrate); this.setValue("pe-interval", data.interval); this.setValue("pe-maxactiveparticles", data.maxactiveparticles); this.setValue("pe-maxactiveparticlesvariation", data.maxactiveparticlesvariation); this.setValue("pe-lifetime", data.lifetime); this.setValue("pe-lifetimevariation", data.lifetimevariation); this.setValue("pe-x", data.x); this.setValue("pe-y", data.y); this.setValue("pe-path", data.path); this.setValue("pe-duration", data.duration); this.setValue("pe-delay", data.delay); this.setValue("pe-sleep", data.sleep); this.setValue("pe-width", data.width); this.setValue("pe-height", data.height); this.setValue("pe-xvariation", data.xvariation); this.setValue("pe-yvariation", data.yvariation); this.setValue("pe-xvelocity", data.xvelocity); this.setValue("pe-yvelocity", data.yvelocity); this.setValue("pe-xvelocityvariation", data.xvelocityvariation); this.setValue("pe-yvelocityvariation", data.yvelocityvariation); this.setValue("pe-xacceleration", data.xacceleration); this.setValue("pe-yacceleration", data.yacceleration); this.setValue("pe-xaccelerationvariation", data.xaccelerationvariation); this.setValue("pe-yaccelerationvariation", data.yaccelerationvariation); this.setValue("pe-startsize", data.startsize); this.setValue("pe-endsize", data.endsize); this.setValue("pe-startsizevariation", data.startsizevariation); this.setValue("pe-endsizevariation", data.endsizevariation); this.setValue("pe-startopacity", data.startopacity); this.setValue("pe-endopacity", data.endopacity); this.setValue("pe-startopacityvariation", data.startopacityvariation); this.setValue("pe-endopacityvariation", data.endopacityvariation); this.setValue("pe-zindex", data.zindex); this.setValue("pe-style", data.style); this.updatePathStatus(); } function addEmitter(openEditor) { var data = this.defaultEmitter(); this.createEmitterElement(data); this.emitters.push(data); this.selectEmitter(this.emitters.length - 1, openEditor); } function selectEmitter(index, openEditor) { this.selectedIndex = index; this.writeForm(this.emitters[index]); this.renderEmitterList(); this.refreshExport(); if (openEditor == true) { this.openEditorDialog(); } } function updateSelected() { var data = this.readForm(); this.emitters[this.selectedIndex] = data; this.applyEmitterAttributes(data); this.renderEmitterList(); this.refreshExport(); } function deleteSelected() { var data = this.emitters[this.selectedIndex]; data.element.remove(); this.emitters.splice(this.selectedIndex, 1); if (this.emitters.length == 0) { this.addEmitter(false); return; } this.selectEmitter(0, false); } function renderEmitterList() { var editor = this; var list = this.querySelector("#emitter-list"); list.innerHTML = ""; this.emitters.forEach(function(data, index) { var button = document.createElement("button"); button.type = "button"; button.className = index == editor.selectedIndex ? "emitter-chip emitter-chip-selected" : "emitter-chip"; button.textContent = data.name; button.onclick = function() { editor.selectEmitter(index, true); }; list.appendChild(button); }); } function exportName(name) { var clean = String(name).replace(/[^A-Za-z0-9_]/g, "_"); if (clean.charAt(0) >= "0" && clean.charAt(0) <= "9") { clean = "emitter_" + clean; } return clean; } function qvalue(value) { var text = String(value); if (text == "true" || text == "false") { return text; } if (text != "" && String(Number(text)) == text) { return text; } return JSON.stringify(text); } function appendLine(lines, indent, name, value) { if (String(value) != "") { lines.push(indent + name + ": " + this.qvalue(value)); } } function emitterToQHTML(data) { var lines = ["particle-emitter " + this.exportName(data.name) + " {"]; this.appendLine(lines, " ", "id", data.id); this.appendLine(lines, " ", "running", data.running); this.appendLine(lines, " ", "src", data.src); this.appendLine(lines, " ", "mask", data.mask); this.appendLine(lines, " ", "emitterMask", data.emittermask); this.appendLine(lines, " ", "color", data.color); this.appendLine(lines, " ", "colorOpacity", data.coloropacity); this.appendLine(lines, " ", "emitRate", data.emitrate); this.appendLine(lines, " ", "interval", data.interval); this.appendLine(lines, " ", "maxActiveParticles", data.maxactiveparticles); this.appendLine(lines, " ", "maxActiveParticlesVariation", data.maxactiveparticlesvariation); this.appendLine(lines, " ", "lifetime", data.lifetime); this.appendLine(lines, " ", "lifetimeVariation", data.lifetimevariation); this.appendLine(lines, " ", "x", data.x); this.appendLine(lines, " ", "y", data.y); this.appendLine(lines, " ", "path", data.path); this.appendLine(lines, " ", "duration", data.duration); this.appendLine(lines, " ", "delay", data.delay); this.appendLine(lines, " ", "sleep", data.sleep); this.appendLine(lines, " ", "width", data.width); this.appendLine(lines, " ", "height", data.height); this.appendLine(lines, " ", "xVariation", data.xvariation); this.appendLine(lines, " ", "yVariation", data.yvariation); this.appendLine(lines, " ", "xVelocity", data.xvelocity); this.appendLine(lines, " ", "yVelocity", data.yvelocity); this.appendLine(lines, " ", "xVelocityVariation", data.xvelocityvariation); this.appendLine(lines, " ", "yVelocityVariation", data.yvelocityvariation); this.appendLine(lines, " ", "xAcceleration", data.xacceleration); this.appendLine(lines, " ", "yAcceleration", data.yacceleration); this.appendLine(lines, " ", "xAccelerationVariation", data.xaccelerationvariation); this.appendLine(lines, " ", "yAccelerationVariation", data.yaccelerationvariation); this.appendLine(lines, " ", "startSize", data.startsize); this.appendLine(lines, " ", "endSize", data.endsize); this.appendLine(lines, " ", "startSizeVariation", data.startsizevariation); this.appendLine(lines, " ", "endSizeVariation", data.endsizevariation); this.appendLine(lines, " ", "startOpacity", data.startopacity); this.appendLine(lines, " ", "endOpacity", data.endopacity); this.appendLine(lines, " ", "startOpacityVariation", data.startopacityvariation); this.appendLine(lines, " ", "endOpacityVariation", data.endopacityvariation); this.appendLine(lines, " ", "zIndex", data.zindex); this.appendLine(lines, " ", "style", data.style); lines.push("}"); return lines.join(String.fromCharCode(10)); } function refreshExport() { var editor = this; var blocks = this.emitters.map(function(data) { return editor.emitterToQHTML(data); }); var nl = String.fromCharCode(10); var openTag = String.fromCharCode(60) + 'q-html version="7"' + String.fromCharCode(62); var closeTag = String.fromCharCode(60) + '/q-html' + String.fromCharCode(62); this.querySelector("#exportText").value = openTag + nl + blocks.join(nl + nl) + nl + closeTag; } function copyExport() { var output = this.querySelector("#exportText"); output.focus(); output.select(); document.execCommand("copy"); } particleEditorTheme { div.app { aside.sidebar { header.side-head { h1.title { text { Particle Editor } } p.subtitle { text { QHTML7 component state drives live particle-emitter instances and exportable QHTML. } } } div.toolbar { button#newEmitterButton.tool.primary { text { New Emitter } } button#deleteEmitterButton.tool.danger { text { Delete } } } section.emitter-list-wrap { p.label { text { Emitters } } div.emitter-list { id: "emitter-list" } button#exportEmitterButton.tool { text { Export QHTML } } } } main.workspace { section.stage-shell { div.stage-label { strong.stage-name { text { Live Stage } } span { text { Emits from QHTML-owned object data } } } div.stage { id: "particle-stage" } div#pathEditorControls.path-editor-controls { span#pathEditorStatus.path-editor-status { text { 0 path point(s) } } button#undoPathButton.tool { text { Undo } } button#clearPathButton.tool.danger { text { Clear } } button#cancelPathButton.tool { text { Cancel } } button#savePathButton.tool.primary { text { Save } } } } div#editorDialog.modal-backdrop { div.dialog { div.form-panel { p.label { text { Selected Emitter } } div#particle-form-grid.form-grid { } div.export-actions { button#deleteDialogButton.tool.danger { text { Delete } } button#closeEditorButton.tool { text { Close } } } } } } div#exportDialog.modal-backdrop { div.export-panel { p.label { text { Selected Emitter } } textarea.editor-textarea { id: "exportText" spellcheck: "false" } div.export-actions { button#copyExportButton.tool.primary { text { Copy QHTML } } button#refreshExportButton.tool { text { Refresh } } button#closeExportButton.tool { text { Close } } } } } } } } } particleEditorApp editor { }