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 { }