Skip to content

Create static.yml

Create static.yml #1

Workflow file for this run

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>DisplayKit - TFT_eSPI + U8g2 UI Builder</title>
<style>
:root {
/* Light mode colors */
--bg-primary: #ffffff;

Check failure on line 9 in .github/workflows/static.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/static.yml

Invalid workflow file

You have an error in your yaml syntax on line 9
--bg-secondary: #f6f8fa;
--bg-tertiary: #f0f0f0;
--border-color: #d0d7de;
--text-primary: #24292f;
--text-secondary: #57606a;
--text-tertiary: #8c959f;
--input-bg: #ffffff;
--input-border: #d0d7de;
--btn-primary: #2da44e;
--btn-primary-hover: #2c974b;
--btn-secondary: #f6f8fa;
--btn-secondary-border: #d0d7de;
--btn-danger: #da3633;
--outline-color: #fd8c73;
}
[data-theme="dark"] {
/* GitHub dark mode colors */
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--text-tertiary: #6e7681;
--input-bg: #0d1117;
--input-border: #30363d;
--btn-primary: #238636;
--btn-primary-hover: #2ea043;
--btn-secondary: #21262d;
--btn-secondary-border: #30363d;
--btn-danger: #da3633;
--outline-color: #f85149;
}
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
body {
margin: 0;
display: flex;
height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
}
.sidebar {
width: 300px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.logo {
width: 40px;
height: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.logo svg {
width: 100%;
height: 100%;
}
/* Logo colors adapt to theme */
[data-theme="dark"] .logo-light {
fill: #8b949e;
stroke: #6e7681;
}
[data-theme="dark"] .logo-dark {
fill: #6e7681;
}
[data-theme="light"] .logo-light {
fill: #d3d3d3;
stroke: #808080;
}
[data-theme="light"] .logo-dark {
fill: #808080;
}
.sidebar h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
flex: 1;
line-height: 1.4;
}
.brand-subtitle {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 4px;
margin-bottom: 8px;
font-weight: 400;
line-height: 1.5;
}
.section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-tertiary);
margin-bottom: 4px;
}
select,
input[type="number"],
input[type="text"],
textarea {
width: 100%;
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--input-border);
background: var(--input-bg);
color: var(--text-primary);
font-size: 12px;
}
select:focus,
input:focus,
textarea:focus {
outline: 2px solid var(--btn-primary);
outline-offset: -1px;
}
label {
font-size: 11px;
color: var(--text-secondary);
}
.btn {
padding: 6px 8px;
border-radius: 6px;
border: 1px solid var(--btn-primary);
background: var(--btn-primary);
color: white;
font-size: 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-right: 4px;
margin-bottom: 4px;
white-space: nowrap;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.btn:hover {
background: var(--btn-primary-hover);
border-color: var(--btn-primary-hover);
}
.btn.secondary {
background: var(--btn-secondary);
border: 1px solid var(--btn-secondary-border);
color: var(--text-primary);
}
.btn.secondary:hover {
background: var(--bg-tertiary);
border-color: var(--border-color);
}
.btn.danger {
background: var(--btn-danger);
border-color: var(--btn-danger);
}
.btn.danger:hover {
opacity: 0.9;
}
.btn:active {
transform: translateY(1px);
}
.btn.small {
padding: 4px 8px;
font-size: 11px;
}
#themeToggle {
min-width: 32px;
padding: 4px 8px;
}
#themeIcon {
font-size: 14px;
line-height: 1;
}
.btn-group {
display: flex;
flex-wrap: wrap;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.top-bar {
padding: 8px 12px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-secondary);
gap: 8px;
}
.top-left,
.top-right {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.top-bar span {
font-size: 12px;
color: var(--text-secondary);
}
.workspace {
display: flex;
flex: 1;
overflow: hidden;
}
.preview-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.preview-frame {
background: var(--bg-secondary);
padding: 16px;
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-color);
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
}
#preview {
position: relative;
background: black;
overflow: hidden;
width: 240px; /* default rectangle */
height: 320px;
}
.ui-element {
position: absolute;
border-radius: 4px;
cursor: move;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
user-select: none;
box-sizing: border-box;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.ui-element:hover:not(.selected) {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.ui-element.selected {
outline: 2px dashed var(--outline-color);
outline-offset: 2px;
box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2), 0 4px 12px rgba(0, 0, 0, 0.2);
z-index: 10;
}
.resize-handle {
position: absolute;
width: 8px;
height: 8px;
background: var(--outline-color);
border: 1px solid #fff;
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
z-index: 20;
cursor: nwse-resize;
}
.resize-handle.nw { top: -4px; left: -4px; cursor: nw-resize; }
.resize-handle.ne { top: -4px; right: -4px; cursor: ne-resize; }
.resize-handle.sw { bottom: -4px; left: -4px; cursor: sw-resize; }
.resize-handle.se { bottom: -4px; right: -4px; cursor: se-resize; }
.resize-handle.n { top: -4px; left: 50%; transform: translateX(-50%); cursor: n-resize; }
.resize-handle.s { bottom: -4px; left: 50%; transform: translateX(-50%); cursor: s-resize; }
.resize-handle.e { right: -4px; top: 50%; transform: translateY(-50%); cursor: e-resize; }
.resize-handle.w { left: -4px; top: 50%; transform: translateY(-50%); cursor: w-resize; }
.properties {
flex: 0 0 320px;
border-left: 1px solid var(--border-color);
padding: 10px;
background: var(--bg-secondary);
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
}
.prop-group {
margin-bottom: 6px;
}
.prop-row {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.prop-row > div {
flex: 1;
}
.code-panel {
height: 32%;
border-top: 1px solid var(--border-color);
padding: 8px;
background: var(--bg-secondary);
display: flex;
flex-direction: column;
gap: 6px;
}
#codeOutput {
flex: 1;
resize: vertical;
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 11px;
background: var(--input-bg);
color: var(--text-primary);
border-radius: 8px;
border: 1px solid var(--input-border);
padding: 8px;
}
.pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg-tertiary);
font-size: 10px;
color: var(--text-secondary);
}
input[type="color"] {
border-radius: 999px;
border: none;
padding: 0;
width: 32px;
height: 18px;
background: transparent;
}
.slider-inline {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
}
.slider-inline input[type="range"] {
width: 100px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<div style="flex: 1;">
<h2>DisplayKit</h2>
<div class="brand-subtitle">by CiferTech</div>
</div>
</div>
<div class="section-title">Display</div>
<div class="prop-group">
<div class="prop-row">
<div>
<label for="dispWidth">Width</label>
<input type="number" id="dispWidth" value="240" min="1" />
</div>
<div>
<label for="dispHeight">Height</label>
<input type="number" id="dispHeight" value="320" min="1" />
</div>
</div>
<div class="prop-row">
<div>
<label for="displayDriver">Driver</label>
<select id="displayDriver">
<option value="tft">TFT_eSPI</option>
<option value="u8g2">U8g2 OLED</option>
</select>
</div>
<div>
<label for="u8g2Preset">U8g2 Display</label>
<select id="u8g2Preset">
<!-- populated in JS -->
</select>
</div>
</div>
<button class="btn small" id="applyRes">Apply Resolution</button>
</div>
<div class="section-title">Screens</div>
<div class="prop-group">
<label for="screenSelect">Current screen</label>
<div class="prop-row">
<div>
<select id="screenSelect"></select>
</div>
<button class="btn small" id="addScreenBtn">+</button>
</div>
<div class="prop-row">
<div>
<label for="screenFnName">Function name</label>
<input type="text" id="screenFnName" placeholder="drawHomeScreen" />
</div>
<button class="btn small secondary" id="deleteScreenBtn">Del</button>
</div>
</div>
<div class="section-title">Basic Shapes</div>
<div class="btn-group">
<button class="btn small" data-add="rect">Rect</button>
<button class="btn small" data-add="roundrect">Round Rect</button>
<button class="btn small" data-add="circle">Circle</button>
<button class="btn small" data-add="line">Line</button>
<button class="btn small" data-add="divider">Divider</button>
</div>
<div class="section-title">UI Elements</div>
<div class="btn-group">
<button class="btn small" data-add="label">Label</button>
<button class="btn small" data-add="button">Button</button>
<button class="btn small" data-add="progress">Progress</button>
<button class="btn small" data-add="slider">Slider</button>
<button class="btn small" data-add="toggle">Toggle</button>
<button class="btn small" data-add="card">Card</button>
<button class="btn small" data-add="header">Header</button>
<button class="btn small" id="addImageBtn">Image</button>
</div>
<div class="section-title">Scene</div>
<div class="prop-group">
<label for="bgColor">Background color</label>
<div class="prop-row">
<div class="pill">
<input type="color" id="bgColor" value="#000000" />
<span>Screen</span>
</div>
</div>
</div>
<div class="section-title">Grid & Snap</div>
<div class="prop-group">
<label>
<input type="checkbox" id="snapCheckbox" />
Snap to grid
</label>
<div class="prop-row">
<div>
<label for="gridSize">Grid size (px)</label>
<input type="number" id="gridSize" value="4" min="1" />
</div>
</div>
</div>
<div class="prop-group">
<label>
<input type="checkbox" id="useSpriteCheckbox" />
Use sprite (TFT_eSprite) for screens
</label>
</div>
<div class="prop-group">
<button class="btn secondary small" id="clearAll">Clear all (screen)</button>
</div>
</div>
<div class="main">
<div class="top-bar">
<div class="top-left">
<span id="displayInfo">TFT_eSPI · 240x320 px</span>
<span class="pill">Multi-screen · Drag to position · Click to edit</span>
</div>
<div class="top-right">
<button class="btn small secondary" id="themeToggle" title="Toggle theme">
<span id="themeIcon">🌙</span>
</button>
<button class="btn small secondary" id="undoBtn">Undo</button>
<button class="btn small secondary" id="redoBtn">Redo</button>
<button class="btn small secondary" id="duplicateBtn">Duplicate</button>
<div class="slider-inline">
<span>Zoom</span>
<input type="range" id="zoomSlider" min="50" max="200" value="75" />
</div>
<button class="btn small secondary" id="exportJsonBtn">Export JSON</button>
<button class="btn small secondary" id="importJsonBtn">Import JSON</button>
</div>
</div>
<div class="workspace">
<div class="preview-wrapper">
<div class="preview-frame">
<div id="preview"></div>
</div>
</div>
<div class="properties">
<div class="section-title">Selected Element</div>
<div id="noSelection">No element selected</div>
<div id="propsPanel" style="display: none">
<div class="prop-group">
<label id="propTypeLabel">Type</label>
<div class="pill" id="elementTypePill"></div>
</div>
<div class="prop-group">
<div class="prop-row">
<div>
<label for="propX">X</label>
<input type="number" id="propX" />
</div>
<div>
<label for="propY">Y</label>
<input type="number" id="propY" />
</div>
</div>
<div class="prop-row">
<div>
<label for="propW">W</label>
<input type="number" id="propW" />
</div>
<div>
<label for="propH">H</label>
<input type="number" id="propH" />
</div>
</div>
</div>
<div class="prop-group" id="textGroup">
<label for="propText">Text</label>
<input type="text" id="propText" />
<div class="prop-row">
<div>
<label for="propTextSize">Text size (TFT)</label>
<input type="number" id="propTextSize" min="1" max="10" />
</div>
</div>
</div>
<div class="prop-group" id="fontGroup">
<label for="propFont">Font (U8g2)</label>
<select id="propFont"></select>
</div>
<div class="prop-group" id="valueGroup" style="display: none">
<label for="propValue">Value / Level (0–100)</label>
<input type="number" id="propValue" min="0" max="100" />
</div>
<div class="prop-group">
<label>Colors</label>
<div class="prop-row">
<div class="pill">
<input type="color" id="propFillColor" />
<span>Fill</span>
</div>
<div class="pill">
<input type="color" id="propStrokeColor" />
<span>Stroke</span>
</div>
<div class="pill">
<input type="color" id="propTextColor" />
<span>Text</span>
</div>
</div>
</div>
<div class="prop-group" id="actionGroup" style="display:none;">
<label for="propAction">On click</label>
<select id="propAction">
<option value="">None</option>
<option value="goto">Go to screen…</option>
</select>
<div class="prop-row">
<div>
<label for="propActionTarget">Target screen</label>
<select id="propActionTarget"></select>
</div>
</div>
</div>
<div class="prop-group">
<label>Align to screen</label>
<div class="btn-group">
<button class="btn small secondary" id="alignLeftBtn">Left</button>
<button class="btn small secondary" id="alignHCenterBtn">H-Center</button>
<button class="btn small secondary" id="alignRightBtn">Right</button>
<button class="btn small secondary" id="alignTopBtn">Top</button>
<button class="btn small secondary" id="alignVCenterBtn">V-Center</button>
<button class="btn small secondary" id="alignBottomBtn">Bottom</button>
</div>
</div>
<div class="prop-group">
<button class="btn secondary small" id="deleteElement">
Delete element
</button>
</div>
</div>
</div>
</div>
<div class="code-panel">
<div style="display: flex; justify-content: space-between; align-items: center">
<div class="section-title">Generated Code (TFT_eSPI / U8g2)</div>
<div>
<button class="btn small secondary" id="copyCode">Copy</button>
</div>
</div>
<textarea id="codeOutput" spellcheck="false"></textarea>
</div>
</div>
<!-- Hidden inputs -->
<input type="file" id="imageInput" accept="image/*" style="display: none" />
<input type="file" id="importJsonInput" accept="application/json" style="display:none" />
<script>
// ----------------- Data model -----------------
let elements = []; // alias to active screen elements
let screens = [];
let activeScreenId = null;
let selectedId = null;
let dispWidth = 240;
let dispHeight = 320;
let bgColor = "#000000";
let useSprite = false;
let zoomFactor = 0.75;
let snapToGrid = false;
let gridSize = 4;
// Driver mode: "tft" or "u8g2"
let driverMode = "tft";
let u8g2PresetId = "ssd1306_128x64_i2c_f";
const DEFAULT_U8G2_FONT = "u8g2_font_6x10_tf";
// Undo/Redo history
let history = [];
let historyIndex = -1;
let historyLocked = false; // avoid recursion while applying history
const preview = document.getElementById("preview");
const displayInfo = document.getElementById("displayInfo");
// Controls
const dispWidthInput = document.getElementById("dispWidth");
const dispHeightInput = document.getElementById("dispHeight");
const applyResBtn = document.getElementById("applyRes");
const bgColorInput = document.getElementById("bgColor");
const clearAllBtn = document.getElementById("clearAll");
const useSpriteCheckbox = document.getElementById("useSpriteCheckbox");
const snapCheckbox = document.getElementById("snapCheckbox");
const gridSizeInput = document.getElementById("gridSize");
const displayDriverSelect = document.getElementById("displayDriver");
const u8g2PresetSelect = document.getElementById("u8g2Preset");
const addButtons = document.querySelectorAll("[data-add]");
const addImageBtn = document.getElementById("addImageBtn");
const imageInput = document.getElementById("imageInput");
const imgCanvas = document.createElement("canvas");
const imgCtx = imgCanvas.getContext("2d");
// Screen controls
const screenSelect = document.getElementById("screenSelect");
const addScreenBtn = document.getElementById("addScreenBtn");
const deleteScreenBtn = document.getElementById("deleteScreenBtn");
const screenFnNameInput = document.getElementById("screenFnName");
// Props controls
const noSelection = document.getElementById("noSelection");
const propsPanel = document.getElementById("propsPanel");
const elementTypePill = document.getElementById("elementTypePill");
const propX = document.getElementById("propX");
const propY = document.getElementById("propY");
const propW = document.getElementById("propW");
const propH = document.getElementById("propH");
const propText = document.getElementById("propText");
const propTextSize = document.getElementById("propTextSize");
const propValue = document.getElementById("propValue");
const propFillColor = document.getElementById("propFillColor");
const propStrokeColor = document.getElementById("propStrokeColor");
const propTextColor = document.getElementById("propTextColor");
const deleteElementBtn = document.getElementById("deleteElement");
const valueGroup = document.getElementById("valueGroup");
const textGroup = document.getElementById("textGroup");
const fontGroup = document.getElementById("fontGroup");
const propFont = document.getElementById("propFont");
const actionGroup = document.getElementById("actionGroup");
const propAction = document.getElementById("propAction");
const propActionTarget = document.getElementById("propActionTarget");
// Alignment buttons
const alignLeftBtn = document.getElementById("alignLeftBtn");
const alignHCenterBtn = document.getElementById("alignHCenterBtn");
const alignRightBtn = document.getElementById("alignRightBtn");
const alignTopBtn = document.getElementById("alignTopBtn");
const alignVCenterBtn = document.getElementById("alignVCenterBtn");
const alignBottomBtn = document.getElementById("alignBottomBtn");
// Code output
const codeOutput = document.getElementById("codeOutput");
const copyCodeBtn = document.getElementById("copyCode");
// Top bar extra controls
const themeToggle = document.getElementById("themeToggle");
const themeIcon = document.getElementById("themeIcon");
const undoBtn = document.getElementById("undoBtn");
const redoBtn = document.getElementById("redoBtn");
const duplicateBtn = document.getElementById("duplicateBtn");
const zoomSlider = document.getElementById("zoomSlider");
const exportJsonBtn = document.getElementById("exportJsonBtn");
const importJsonBtn = document.getElementById("importJsonBtn");
const importJsonInput = document.getElementById("importJsonInput");
// --------------- Theme Toggle (Initialize Early) -----------------
function getTheme() {
// Check localStorage first, then system preference, default to light
const saved = localStorage.getItem("theme");
if (saved) return saved;
// Check system preference
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
}
function setTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
if (themeIcon) {
themeIcon.textContent = theme === "dark" ? "☀️" : "🌙";
}
}
function toggleTheme() {
const currentTheme = getTheme();
const newTheme = currentTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
}
// Initialize theme early
const savedTheme = getTheme();
setTheme(savedTheme);
// Theme toggle button event
if (themeToggle) {
themeToggle.addEventListener("click", toggleTheme);
}
// Listen for system theme changes (only if no manual preference is set)
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
setTheme(e.matches ? "dark" : "light");
}
});
}
// --------- U8g2 presets and fonts (extend these lists if you want "full") ----------
const U8G2_PRESETS = [
{
id: "ssd1306_128x64_i2c_f",
label: "SSD1306 128x64 I2C (F_HW_I2C)",
ctor: "U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);"
},
{
id: "ssd1306_128x64_spi_f",
label: "SSD1306 128x64 SPI (F_4W_HW_SPI)",
ctor: "U8G2_SSD1306_128X64_NONAME_F_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);"
},
{
id: "sh1106_128x64_i2c_f",
label: "SH1106 128x64 I2C (F_HW_I2C)",
ctor: "U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);"
},
{
id: "custom",
label: "Custom (edit manually in code)",
ctor: "// TODO: Add your U8g2 constructor here\n// Example:\n// U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);"
}
];
const U8G2_FONTS = [
"u8g2_font_5x8_tf",
"u8g2_font_6x10_tf",
"u8g2_font_7x14_tf",
"u8g2_font_8x13B_tf",
"u8g2_font_9x15B_tf",
"u8g2_font_10x20_tf",
"u8g2_font_helvR08_tf",
"u8g2_font_helvR10_tf",
"u8g2_font_helvR12_tf",
"u8g2_font_helvB08_tf",
"u8g2_font_helvB10_tf",
"u8g2_font_helvB12_tf",
"u8g2_font_profont10_mf",
"u8g2_font_profont12_mf",
"u8g2_font_profont15_mf",
"u8g2_font_t0_11b_mf",
"u8g2_font_t0_12b_mf",
"u8g2_font_ncenB08_tr",
"u8g2_font_ncenB10_tr",
"u8g2_font_ncenB12_tr",
"u8g2_font_ncenB14_tr",
"u8g2_font_6x12_tf",
"u8g2_font_8x8_mf",
"u8g2_font_inr16_mf",
"u8g2_font_inb16_mf"
// add more if you want full list
];
// --------------- Utils -----------------
function makeId() {
return "el_" + Math.random().toString(36).substr(2, 9);
}
function deepCloneState() {
return JSON.parse(
JSON.stringify({
screens,
activeScreenId,
dispWidth,
dispHeight,
bgColor,
useSprite,
snapToGrid,
gridSize,
driverMode,
u8g2PresetId
})
);
}
function applyStateSnapshot(snap) {
historyLocked = true;
screens = snap.screens || [];
activeScreenId = snap.activeScreenId || (screens[0] && screens[0].id) || null;
dispWidth = snap.dispWidth || 240;
dispHeight = snap.dispHeight || 320;
bgColor = snap.bgColor || "#000000";
useSprite = !!snap.useSprite;
snapToGrid = !!snap.snapToGrid;
gridSize = snap.gridSize || 4;
driverMode = snap.driverMode || "tft";
u8g2PresetId = snap.u8g2PresetId || "ssd1306_128x64_i2c_f";
dispWidthInput.value = dispWidth;
dispHeightInput.value = dispHeight;
bgColorInput.value = bgColor;
useSpriteCheckbox.checked = useSprite;
snapCheckbox.checked = snapToGrid;
gridSizeInput.value = gridSize;
displayDriverSelect.value = driverMode;
u8g2PresetSelect.value = u8g2PresetId;
refreshScreenUI();
if (activeScreenId && screens.some((s) => s.id === activeScreenId)) {
setActiveScreen(activeScreenId, false);
} else if (screens[0]) {
setActiveScreen(screens[0].id, false);
}
updatePreviewSize();
renderElements();
updatePropsInputs();
updateCode();
historyLocked = false;
}
function pushHistory() {
if (historyLocked) return;
const snap = deepCloneState();
history = history.slice(0, historyIndex + 1);
history.push(snap);
historyIndex = history.length - 1;
updateUndoRedoButtons();
}
function updateUndoRedoButtons() {
undoBtn.disabled = historyIndex <= 0;
redoBtn.disabled = historyIndex >= history.length - 1;
}
function hexToRgb565(hex) {
hex = hex.replace("#", "");
if (hex.length === 3) {
hex = hex
.split("")
.map((c) => c + c)
.join("");
}
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
const r5 = (r >> 3) & 0x1f;
const g6 = (g >> 2) & 0x3f;
const b5 = (b >> 3) & 0x1f;
const rgb565 = (r5 << 11) | (g6 << 5) | b5;
return "0x" + rgb565.toString(16).padStart(4, "0").toUpperCase();
}
function elementHasText(type) {
return ["label", "button", "header", "card"].includes(type);
}
function elementHasValue(type) {
return ["progress", "slider", "toggle"].includes(type);
}
function elementSupportsAction(type) {
return ["button", "card", "header", "label"].includes(type);
}
// --------------- Screens -----------------
function createScreen(name, fnName) {
const id = makeId();
const safeName = name || "Screen " + (screens.length + 1);
const defaultFn = "draw" + safeName.replace(/\s+/g, "");
const screen = {
id,
name: safeName,
fnName: fnName || defaultFn,
elements: [],
};
screens.push(screen);
return screen;
}
function refreshScreenUI() {
screenSelect.innerHTML = "";
screens.forEach((scr) => {
const opt = document.createElement("option");
opt.value = scr.id;
opt.textContent = scr.name;
screenSelect.appendChild(opt);
});
if (activeScreenId && screens.some((s) => s.id === activeScreenId)) {
screenSelect.value = activeScreenId;
}
}
function setActiveScreen(id, push = true) {
const screen = screens.find((s) => s.id === id);
if (!screen) return;
activeScreenId = id;
elements = screen.elements;
selectedId = null;
refreshScreenUI();
screenSelect.value = id;
screenFnNameInput.value = screen.fnName || "";
updatePreviewSize();
renderElements();
updatePropsInputs();
updateCode();
if (push) pushHistory();
}
function initScreens() {
if (screens.length === 0) {
const home = createScreen("Home", "drawHomeScreen");
createScreen("Settings", "drawSettingsScreen");
refreshScreenUI();
setActiveScreen(home.id, false);
} else {
refreshScreenUI();
setActiveScreen(screens[0].id, false);
}
updatePreviewSize();
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
}
function initU8g2Presets() {
u8g2PresetSelect.innerHTML = "";
U8G2_PRESETS.forEach(p => {
const opt = document.createElement("option");
opt.value = p.id;
opt.textContent = p.label;
u8g2PresetSelect.appendChild(opt);
});
u8g2PresetSelect.value = u8g2PresetId;
}
function initFontList() {
propFont.innerHTML = "";
U8G2_FONTS.forEach(name => {
const opt = document.createElement("option");
opt.value = name;
opt.textContent = name;
propFont.appendChild(opt);
});
}
// --------------- Preview layout -----------------
function updatePreviewSize() {
const previewWrapper = document.querySelector(".preview-wrapper");
if (!previewWrapper) return;
// Get available space (accounting for padding in preview-frame)
const wrapperRect = previewWrapper.getBoundingClientRect();
const availableWidth = wrapperRect.width - 32; // 16px padding on each side
const availableHeight = wrapperRect.height - 32; // 16px padding on each side
// Calculate base scale to fit in available space at 100% zoom
const baseScale = Math.min(
availableWidth / dispWidth,
availableHeight / dispHeight
);
// Apply zoom factor (allows scaling beyond 100%)
const scale = baseScale * zoomFactor;
preview.style.width = dispWidth * scale + "px";
preview.style.height = dispHeight * scale + "px";
preview.style.backgroundColor = bgColor;
preview.dataset.scale = scale;
const driverLabel = driverMode === "u8g2" ? "U8g2 OLED" : "TFT_eSPI";
displayInfo.textContent = `${driverLabel} · ${dispWidth}x${dispHeight} px`;
}
// --------------- Render preview elements -----------------
function renderElements() {
const scale = parseFloat(preview.dataset.scale || "1");
preview.innerHTML = "";
preview.style.position = "relative";
elements.forEach((el) => {
const div = document.createElement("div");
div.className = "ui-element" + (el.id === selectedId ? " selected" : "");
div.dataset.id = el.id;
const x = el.x * scale;
const y = el.y * scale;
const w = el.w * scale;
const h = el.h * scale;
div.style.left = x + "px";
div.style.top = y + "px";
div.style.width = w + "px";
div.style.height = h + "px";
const fill = el.fillColor || "#ffffff";
const stroke = el.strokeColor || "#ffffff";
const textColor = el.textColor || "#000000";
if (el.type === "image") {
div.style.backgroundImage = el.previewUrl ? `url(${el.previewUrl})` : "none";
div.style.backgroundSize = "contain";
div.style.backgroundPosition = "center";
div.style.backgroundRepeat = "no-repeat";
div.style.border = "1px solid rgba(255, 255, 255, 0.1)";
div.style.backgroundColor = "#000000";
div.style.borderRadius = "4px";
} else if (el.type === "circle") {
div.style.borderRadius = "50%";
div.style.backgroundColor = fill;
div.style.border = "1px solid " + stroke;
div.style.boxShadow = `0 3px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.2)`;
} else if (el.type === "line") {
// Line connects two points
const angle = Math.atan2(h, w) * (180 / Math.PI);
div.style.width = Math.sqrt(w * w + h * h) + "px";
div.style.height = "2px";
div.style.backgroundColor = stroke;
div.style.borderRadius = "1px";
div.style.transformOrigin = "0 50%";
div.style.transform = `rotate(${angle}deg)`;
div.style.boxShadow = `0 1px 2px rgba(0, 0, 0, 0.2)`;
} else if (el.type === "divider") {
div.style.height = Math.max(2, h) + "px";
div.style.width = w + "px";
div.style.backgroundColor = stroke;
div.style.borderRadius = "1px";
div.style.boxShadow = `0 1px 2px rgba(0, 0, 0, 0.2)`;
// Add subtle gradient for depth
div.style.background = `linear-gradient(to bottom, ${stroke}, ${stroke}dd)`;
} else if (el.type === "progress") {
div.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
div.style.border = "1px solid " + stroke;
div.style.borderRadius = Math.min(h / 2, 8) + "px";
div.style.overflow = "hidden";
const bar = document.createElement("div");
bar.style.position = "absolute";
bar.style.left = "0";
bar.style.top = "0";
bar.style.bottom = "0";
const val = Math.max(0, Math.min(100, el.value || 50));
bar.style.width = w * (val / 100) + "px";
bar.style.borderRadius = Math.min(h / 2, 8) + "px";
bar.style.backgroundColor = fill;
bar.style.boxShadow = `inset 0 1px 2px rgba(255, 255, 255, 0.2)`;
bar.style.transition = "width 0.2s ease";
div.appendChild(bar);
} else if (el.type === "slider") {
div.style.backgroundColor = "rgba(0, 0, 0, 0.3)";
div.style.borderRadius = h + "px";
div.style.border = "1px solid " + stroke;
div.style.position = "relative";
const track = document.createElement("div");
track.style.position = "absolute";
track.style.left = "0";
track.style.top = "50%";
track.style.transform = "translateY(-50%)";
track.style.height = "2px";
const val = Math.max(0, Math.min(100, el.value || 50));
track.style.width = w * (val / 100) + "px";
track.style.backgroundColor = fill;
track.style.borderRadius = "1px";
div.appendChild(track);
const knob = document.createElement("div");
const knobSize = Math.max(h - 6, 8);
knob.style.position = "absolute";
knob.style.top = "50%";
knob.style.transform = "translate(-50%, -50%)";
knob.style.width = knobSize + "px";
knob.style.height = knobSize + "px";
knob.style.borderRadius = "50%";
knob.style.backgroundColor = fill;
knob.style.border = "2px solid " + stroke;
knob.style.boxShadow = `0 2px 4px rgba(0, 0, 0, 0.4)`;
const trackWidth = w;
knob.style.left = (trackWidth * (val / 100)) + "px";
div.appendChild(knob);
} else if (el.type === "toggle") {
const radius = h / 2;
const val = Math.max(0, Math.min(100, el.value || 0));
const on = val >= 50;
div.style.backgroundColor = on ? fill : "rgba(0, 0, 0, 0.3)";
div.style.borderRadius = radius + "px";
div.style.border = "1px solid " + stroke;
div.style.transition = "background-color 0.2s ease";
const knob = document.createElement("div");
knob.style.position = "absolute";
knob.style.width = (h - 4) + "px";
knob.style.height = (h - 4) + "px";
knob.style.borderRadius = "50%";
knob.style.top = "2px";
knob.style.backgroundColor = on ? "#ffffff" : "#cccccc";
knob.style.boxShadow = `0 2px 4px rgba(0, 0, 0, 0.3)`;
knob.style.transition = "left 0.2s ease, background-color 0.2s ease";
knob.style.left = on ? (w - h + 2) + "px" : "2px";
div.appendChild(knob);
} else if (el.type === "header") {
div.style.backgroundColor = fill;
div.style.borderBottom = "2px solid " + stroke;
div.style.alignItems = "center";
div.style.justifyContent = "flex-start";
div.style.paddingLeft = Math.max(6, 4 * scale) + "px";
div.style.fontWeight = "600";
div.style.color = textColor;
div.style.textShadow = "0 1px 2px rgba(0, 0, 0, 0.1)";
div.textContent = el.text || "Header";
} else if (el.type === "card") {
div.style.backgroundColor = fill;
div.style.borderRadius = "8px";
div.style.border = "1px solid " + stroke;
div.style.padding = Math.max(6, 4 * scale) + "px";
div.style.color = textColor;
div.style.alignItems = "flex-start";
div.style.justifyContent = "flex-start";
div.style.boxShadow = `0 2px 4px rgba(0, 0, 0, 0.2)`;
div.textContent = el.text || "Card";
} else if (el.type === "rect") {
// Rectangle with enhanced styling
div.style.backgroundColor = fill;
div.style.border = "1px solid " + stroke;
div.style.borderRadius = "2px";
div.style.boxShadow = `0 2px 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.1)`;
} else if (el.type === "roundrect") {
// Rounded rectangle with enhanced styling
div.style.backgroundColor = fill;
div.style.border = "1px solid " + stroke;
div.style.borderRadius = "8px";
div.style.boxShadow = `0 3px 6px rgba(0, 0, 0, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.15)`;
} else {
// button, label
div.style.backgroundColor = el.type === "label" ? "transparent" : fill;
if (el.type === "button") {
div.style.borderRadius = "6px";
div.style.border = "1px solid " + stroke;
div.style.boxShadow = `0 2px 4px rgba(0, 0, 0, 0.2)`;
div.style.cursor = "pointer";
}
div.style.color = textColor;
if (elementHasText(el.type)) {
div.textContent = el.text || el.type;
if (el.type === "button") {
div.style.fontWeight = "500";
div.style.textShadow = "0 1px 1px rgba(0, 0, 0, 0.1)";
}
} else {
div.textContent = "";
}
}
if (el.type !== "image") {
div.style.fontSize = (el.textSize || 2) * 5 * scale + "px";
}
enableDrag(div, el.id);
// Add resize handles for selected elements (except images which are locked)
if (el.id === selectedId && el.type !== "image") {
addResizeHandles(div, el.id);
}
preview.appendChild(div);
});
}
// --------------- Resize handles -----------------
function addResizeHandles(elementDiv, id) {
const handles = [
{ class: "nw", cursor: "nw-resize" },
{ class: "ne", cursor: "ne-resize" },
{ class: "sw", cursor: "sw-resize" },
{ class: "se", cursor: "se-resize" },
{ class: "n", cursor: "n-resize" },
{ class: "s", cursor: "s-resize" },
{ class: "e", cursor: "e-resize" },
{ class: "w", cursor: "w-resize" }
];
handles.forEach(handle => {
const handleDiv = document.createElement("div");
handleDiv.className = `resize-handle ${handle.class}`;
handleDiv.dataset.handle = handle.class;
handleDiv.dataset.elementId = id;
enableResize(handleDiv, id, handle.class);
elementDiv.appendChild(handleDiv);
});
}
function enableResize(handle, elementId, direction) {
let isResizing = false;
let startX = 0;
let startY = 0;
let startWidth = 0;
let startHeight = 0;
let startXPos = 0;
let startYPos = 0;
handle.addEventListener("mousedown", (e) => {
e.stopPropagation();
e.preventDefault();
isResizing = true;
startX = e.clientX;
startY = e.clientY;
const el = elements.find((el) => el.id === elementId);
if (!el) return;
startWidth = el.w;
startHeight = el.h;
startXPos = el.x;
startYPos = el.y;
document.addEventListener("mousemove", onResizeMove);
document.addEventListener("mouseup", onResizeUp);
});
function onResizeMove(e) {
if (!isResizing) return;
const scale = parseFloat(preview.dataset.scale || "1");
const dx = (e.clientX - startX) / scale;
const dy = (e.clientY - startY) / scale;
const el = elements.find((el) => el.id === elementId);
if (!el) return;
let newWidth = startWidth;
let newHeight = startHeight;
let newX = startXPos;
let newY = startYPos;
// Handle different resize directions
if (direction.includes("e")) {
newWidth = Math.max(10, startWidth + dx);
}
if (direction.includes("w")) {
newWidth = Math.max(10, startWidth - dx);
newX = startXPos + dx;
}
if (direction.includes("s")) {
newHeight = Math.max(10, startHeight + dy);
}
if (direction.includes("n")) {
newHeight = Math.max(10, startHeight - dy);
newY = startYPos + dy;
}
// Apply snap to grid if enabled
if (snapToGrid && gridSize > 0) {
newWidth = Math.round(newWidth / gridSize) * gridSize;
newHeight = Math.round(newHeight / gridSize) * gridSize;
newX = Math.round(newX / gridSize) * gridSize;
newY = Math.round(newY / gridSize) * gridSize;
}
el.w = Math.round(newWidth);
el.h = Math.round(newHeight);
el.x = Math.round(newX);
el.y = Math.round(newY);
updatePropsInputs(false);
renderElements();
updateCode();
}
function onResizeUp() {
if (isResizing) {
pushHistory();
}
isResizing = false;
document.removeEventListener("mousemove", onResizeMove);
document.removeEventListener("mouseup", onResizeUp);
}
}
// --------------- Drag logic -----------------
function enableDrag(node, id) {
let isDragging = false;
let startX = 0;
let startY = 0;
let origX = 0;
let origY = 0;
node.addEventListener("mousedown", (e) => {
// Don't start dragging if clicking on a resize handle
if (e.target.classList.contains("resize-handle")) {
return;
}
e.preventDefault();
selectElement(id, false);
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const el = elements.find((el) => el.id === id);
if (!el) return;
origX = el.x;
origY = el.y;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
function onMouseMove(e) {
if (!isDragging) return;
const scale = parseFloat(preview.dataset.scale || "1");
let dx = (e.clientX - startX) / scale;
let dy = (e.clientY - startY) / scale;
let newX = origX + dx;
let newY = origY + dy;
if (snapToGrid && gridSize > 0) {
newX = Math.round(newX / gridSize) * gridSize;
newY = Math.round(newY / gridSize) * gridSize;
}
const el = elements.find((el) => el.id === id);
if (!el) return;
el.x = Math.round(newX);
el.y = Math.round(newY);
updatePropsInputs(false);
renderElements();
updateCode();
}
function onMouseUp() {
if (isDragging) {
pushHistory();
}
isDragging = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
}
// --------------- Selection & properties -----------------
function selectElement(id, push = true) {
selectedId = id;
updatePropsInputs();
renderElements();
if (push) pushHistory();
}
function refreshActionTargetOptions(el) {
propActionTarget.innerHTML = "";
screens.forEach(scr => {
const opt = document.createElement("option");
opt.value = scr.id;
opt.textContent = scr.name;
propActionTarget.appendChild(opt);
});
if (el && el.actionTargetScreenId) {
propActionTarget.value = el.actionTargetScreenId;
}
}
function updatePropsInputs(push = false) {
const el = elements.find((el) => el.id === selectedId);
if (!el) {
noSelection.style.display = "block";
propsPanel.style.display = "none";
return;
}
noSelection.style.display = "none";
propsPanel.style.display = "block";
elementTypePill.textContent = el.type.toUpperCase();
propX.value = el.x;
propY.value = el.y;
propW.value = el.w;
propH.value = el.h;
propText.value = el.text || "";
propTextSize.value = el.textSize || 2;
propFillColor.value = el.fillColor || "#ffffff";
propStrokeColor.value = el.strokeColor || "#ffffff";
propTextColor.value = el.textColor || "#ffffff";
propValue.value = el.value != null ? el.value : 50;
propFont.value = el.font || DEFAULT_U8G2_FONT;
textGroup.style.display = elementHasText(el.type) ? "block" : "none";
valueGroup.style.display = elementHasValue(el.type) ? "block" : "none";
fontGroup.style.display = elementHasText(el.type) ? "block" : "none";
if (el.type === "image") {
propW.disabled = true;
propH.disabled = true;
} else {
propW.disabled = false;
propH.disabled = false;
}
// Actions
if (elementSupportsAction(el.type)) {
actionGroup.style.display = "block";
propAction.value = el.actionType || "";
refreshActionTargetOptions(el);
} else {
actionGroup.style.display = "none";
}
if (push) pushHistory();
}
// --------------- Code generation (TFT_eSPI + U8g2) -----------------
function updateCode() {
if (driverMode === "u8g2") {
generateU8g2Code();
} else {
generateTFTCode();
}
}
function getScreenFnName(scr) {
return (scr.fnName && scr.fnName.trim().length)
? scr.fnName.trim()
: "draw" + scr.name.replace(/\s+/g, "");
}
function generateTFTCode() {
let code = "";
code += "#include <TFT_eSPI.h>\n";
code += "#include <SPI.h>\n\n";
code += "TFT_eSPI tft = TFT_eSPI();\n";
if (useSprite) {
code += "TFT_eSprite spr = TFT_eSprite(&tft);\n";
}
code += "\n";
code += "// NOTE: Touch / button input handling is not generated.\n";
code += "// Use the comments marked \"Action\" to wire your touch logic.\n\n";
const bg565 = hexToRgb565(bgColor);
// Collect all image elements
const imageElements = [];
screens.forEach((scr) => {
scr.elements.forEach((el) => {
if (
el.type === "image" &&
el.rgb565 &&
el.rgb565.length &&
el.imageWidth &&
el.imageHeight
) {
imageElements.push(el);
}
});
});
// Emit image arrays
imageElements.forEach((el) => {
const name = el.imageName || "img_" + el.id.replace(/[^a-zA-Z0-9_]/g, "_");
const totalPixels = el.imageWidth * el.imageHeight;
code += `const uint16_t ${name}[${totalPixels}] PROGMEM = {\n`;
for (let i = 0; i < el.rgb565.length; i++) {
const val = el.rgb565[i];
const hex = "0x" + val.toString(16).padStart(4, "0").toUpperCase();
const isLast = i === el.rgb565.length - 1;
if (i % 12 === 0) code += " ";
code += hex;
code += isLast ? "" : ", ";
if (i % 12 === 11 || isLast) code += "\n";
}
code += "};\n\n";
});
// Screen functions
screens.forEach((scr) => {
const fnName = getScreenFnName(scr);
const drv = useSprite ? "spr" : "tft";
code += `void ${fnName}() {\n`;
if (useSprite) {
code += ` spr.fillSprite(${bg565});\n`;
} else {
code += ` tft.fillScreen(${bg565});\n`;
}
code += ` // --- UI elements for screen: ${scr.name} ---\n`;
scr.elements.forEach((el) => {
const val = Math.max(0, Math.min(100, el.value != null ? el.value : 50));
if (el.type === "image") {
if (el.rgb565 && el.rgb565.length && el.imageWidth && el.imageHeight) {
const name = el.imageName || "img_" + el.id.replace(/[^a-zA-Z0-9_]/g, "_");
code += " // Image\n";
code += ` ${drv}.pushImage(${el.x}, ${el.y}, ${el.imageWidth}, ${el.imageHeight}, ${name});\n\n`;
}
return;
}
const fill565 = hexToRgb565(el.fillColor || "#FFFFFF");
const stroke565 = hexToRgb565(el.strokeColor || "#FFFFFF");
const text565 = hexToRgb565(el.textColor || "#FFFFFF");
if (el.type === "rect") {
code += ` ${drv}.fillRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${fill565});\n`;
} else if (el.type === "roundrect") {
code += ` ${drv}.fillRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, 4, ${fill565});\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, 4, ${stroke565});\n`;
} else if (el.type === "circle") {
const r = Math.floor(Math.min(el.w, el.h) / 2);
const cx = el.x + r;
const cy = el.y + r;
code += ` ${drv}.fillCircle(${cx}, ${cy}, ${r}, ${fill565});\n`;
} else if (el.type === "line") {
code += ` ${drv}.drawLine(${el.x}, ${el.y}, ${el.x + el.w}, ${el.y + el.h}, ${stroke565});\n`;
} else if (el.type === "divider") {
code += ` ${drv}.drawLine(${el.x}, ${el.y}, ${el.x + el.w}, ${el.y}, ${stroke565});\n`;
} else if (el.type === "label") {
const txt = (el.text || "").replace(/"/g, '\\"');
code += ` ${drv}.setTextColor(${text565});\n`;
code += ` ${drv}.setTextSize(${el.textSize || 2});\n`;
code += ` ${drv}.setCursor(${el.x}, ${el.y});\n`;
code += ` ${drv}.print("${txt}");\n\n`;
} else if (el.type === "button") {
const r = 6;
const txt = (el.text || "Button").replace(/"/g, '\\"');
code += " // Button\n";
code += ` ${drv}.fillRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r}, ${fill565});\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r}, ${stroke565});\n`;
code += ` ${drv}.setTextColor(${text565});\n`;
code += ` ${drv}.setTextSize(${el.textSize || 2});\n`;
code += ` ${drv}.setTextDatum(MC_DATUM);\n`;
code += ` ${drv}.drawString("${txt}", ${el.x + Math.floor(el.w / 2)}, ${el.y + Math.floor(el.h / 2)});\n`;
code += ` ${drv}.setTextDatum(TL_DATUM);\n\n`;
} else if (el.type === "progress") {
const innerW = Math.max(0, el.w - 4);
const barW = Math.floor(innerW * (val / 100));
code += ` // Progress bar (${val}%)\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, 3, ${stroke565});\n`;
code += ` ${drv}.fillRoundRect(${el.x + 2}, ${el.y + 2}, ${barW}, ${el.h - 4}, 3, ${fill565});\n\n`;
} else if (el.type === "slider") {
const r = Math.floor(el.h / 2);
const knobTravel = el.w - el.h;
const knobX = el.x + Math.floor(knobTravel * (val / 100));
const centerY = el.y + r;
code += ` // Slider (${val}%)\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r}, ${stroke565});\n`;
code += ` ${drv}.fillCircle(${knobX + r}, ${centerY}, ${r - 2}, ${fill565});\n\n`;
} else if (el.type === "toggle") {
const r = Math.floor(el.h / 2);
const centerY = el.y + r;
const on = val >= 50;
code += ` // Toggle (${on ? "ON" : "OFF"})\n`;
code += ` ${drv}.fillRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r}, ${on ? fill565 : hexToRgb565("#111827")});\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r}, ${stroke565});\n`;
const knobX = on ? el.x + el.w - r : el.x;
code += ` ${drv}.fillCircle(${knobX}, ${centerY}, ${r - 2}, ${on ? stroke565 : fill565});\n\n`;
} else if (el.type === "header") {
const txt = (el.text || "Header").replace(/"/g, '\\"');
code += " // Header bar\n";
code += ` ${drv}.fillRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${fill565});\n`;
code += ` ${drv}.drawLine(${el.x}, ${el.y + el.h - 1}, ${el.x + el.w}, ${el.y + el.h - 1}, ${stroke565});\n`;
code += ` ${drv}.setTextColor(${text565});\n`;
code += ` ${drv}.setTextSize(${el.textSize || 2});\n`;
code += ` ${drv}.setCursor(${el.x + 4}, ${el.y + Math.max(0, Math.floor(el.h / 2) - 4)});\n`;
code += ` ${drv}.print("${txt}");\n\n`;
} else if (el.type === "card") {
const txt = (el.text || "Card").replace(/"/g, '\\"');
code += " // Card\n";
code += ` ${drv}.fillRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, 6, ${fill565});\n`;
code += ` ${drv}.drawRoundRect(${el.x}, ${el.y}, ${el.w}, ${el.h}, 6, ${stroke565});\n`;
code += ` ${drv}.setTextColor(${text565});\n`;
code += ` ${drv}.setTextSize(${el.textSize || 2});\n`;
code += ` ${drv}.setCursor(${el.x + 4}, ${el.y + 4});\n`;
code += ` ${drv}.print("${txt}");\n\n`;
}
// Actions (comments only)
if (el.actionType === "goto" && el.actionTargetScreenId) {
const targetScreen = screens.find(s => s.id === el.actionTargetScreenId);
if (targetScreen) {
const targetFnName = getScreenFnName(targetScreen);
code += ` // Action: if touch is inside (${el.x}, ${el.y}, ${el.w}, ${el.h}) -> call ${targetFnName}();\n\n`;
}
}
});
if (useSprite) {
code += " spr.pushSprite(0, 0);\n";
}
code += "}\n\n";
});
// setup & loop
const defaultScreen =
screens.find((s) => s.id === activeScreenId) || screens[0];
code += "void setup() {\n";
code += " tft.init();\n";
code += " tft.setRotation(1);\n";
code += " tft.setTextDatum(TL_DATUM);\n";
code += " tft.setTextFont(1);\n";
if (useSprite) {
code += ` spr.createSprite(${dispWidth}, ${dispHeight});\n`;
}
if (defaultScreen) {
const fnName = getScreenFnName(defaultScreen);
code += ` ${fnName}();\n`;
}
code += "}\n\n";
code += "void loop() {\n";
code += " // Put your logic here\n";
code += " // Example touch handler pseudocode:\n";
code += " // int16_t tx, ty;\n";
code += " // if (touched) { handleTouch(tx, ty); }\n";
code += "}\n\n";
code += "// Example helper you can implement:\n";
code += "// void handleTouch(int16_t tx, int16_t ty) {\n";
code += "// // Check \"Action\" comments in the draw functions\n";
code += "// // and call the matching drawXXXScreen() when inside bounds.\n";
code += "// }\n";
codeOutput.value = code;
}
function generateU8g2Code() {
let code = "";
code += "#include <U8g2lib.h>\n";
code += "#include <Wire.h>\n\n";
const preset = U8G2_PRESETS.find(p => p.id === u8g2PresetId) || U8G2_PRESETS[0];
if (preset.id === "custom") {
code += preset.ctor + "\n\n";
} else {
code += preset.ctor + "\n\n";
}
code += "// NOTE: Input (buttons/encoder/touch) is not generated.\n";
code += "// Use the \"Action\" comments to wire your navigation between screens.\n\n";
// Screen functions
screens.forEach(scr => {
const fnName = getScreenFnName(scr);
code += `void ${fnName}() {\n`;
code += " u8g2.clearBuffer();\n";
code += ` // --- UI elements for screen: ${scr.name} ---\n`;
scr.elements.forEach(el => {
const val = Math.max(0, Math.min(100, el.value != null ? el.value : 50));
const font = el.font || DEFAULT_U8G2_FONT;
const txtEsc = (el.text || "").replace(/"/g, '\\"');
if (el.type === "image") {
code += ` // Image at (${el.x}, ${el.y}) not exported for U8g2 yet.\n`;
code += " // You can convert it to XBM and use u8g2.drawXBMP().\n\n";
return;
}
if (el.type === "rect") {
code += ` u8g2.drawBox(${el.x}, ${el.y}, ${el.w}, ${el.h});\n`;
} else if (el.type === "roundrect") {
const r = 4;
code += ` u8g2.drawRBox(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r});\n`;
} else if (el.type === "circle") {
const r = Math.floor(Math.min(el.w, el.h) / 2);
const cx = el.x + r;
const cy = el.y + r;
code += ` u8g2.drawDisc(${cx}, ${cy}, ${r});\n`;
} else if (el.type === "line") {
code += ` u8g2.drawLine(${el.x}, ${el.y}, ${el.x + el.w}, ${el.y + el.h});\n`;
} else if (el.type === "divider") {
code += ` u8g2.drawLine(${el.x}, ${el.y}, ${el.x + el.w}, ${el.y});\n`;
} else if (el.type === "label") {
code += ` u8g2.setFont(${font});\n`;
code += ` u8g2.drawUTF8(${el.x}, ${el.y} + 10, "${txtEsc}");\n\n`;
} else if (el.type === "button") {
const r = 4;
code += " // Button\n";
code += ` u8g2.drawRFrame(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r});\n`;
if (txtEsc.length) {
const textY = el.y + Math.floor(el.h / 2) + 4;
code += ` u8g2.setFont(${font});\n`;
code += ` u8g2.drawUTF8(${el.x + 4}, ${textY}, "${txtEsc}");\n`;
}
code += "\n";
} else if (el.type === "progress") {
const innerW = Math.max(0, el.w - 4);
const barW = Math.floor(innerW * (val / 100));
code += ` // Progress bar (${val}%)\n`;
code += ` u8g2.drawFrame(${el.x}, ${el.y}, ${el.w}, ${el.h});\n`;
code += ` u8g2.drawBox(${el.x + 2}, ${el.y + 2}, ${barW}, ${el.h - 4});\n\n`;
} else if (el.type === "slider") {
const r = Math.floor(el.h / 2);
const knobTravel = el.w - el.h;
const knobX = el.x + Math.floor(knobTravel * (val / 100));
const centerY = el.y + r;
code += ` // Slider (${val}%)\n`;
code += ` u8g2.drawRFrame(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r});\n`;
code += ` u8g2.drawDisc(${knobX + r}, ${centerY}, ${r - 2});\n\n`;
} else if (el.type === "toggle") {
const r = Math.floor(el.h / 2);
const centerY = el.y + r;
const on = val >= 50;
code += ` // Toggle (${on ? "ON" : "OFF"})\n`;
code += ` u8g2.drawRFrame(${el.x}, ${el.y}, ${el.w}, ${el.h}, ${r});\n`;
const knobX = on ? el.x + el.w - r : el.x;
code += ` u8g2.drawDisc(${knobX}, ${centerY}, ${r - 2});\n\n`;
} else if (el.type === "header") {
code += " // Header bar\n";
code += ` u8g2.drawBox(${el.x}, ${el.y}, ${el.w}, ${el.h});\n`;
if (txtEsc.length) {
const textY = el.y + el.h - 4;
code += ` u8g2.setFont(${font});\n`;
code += ` u8g2.setDrawColor(0);\n`;
code += ` u8g2.drawUTF8(${el.x + 4}, ${textY}, "${txtEsc}");\n`;
code += " u8g2.setDrawColor(1);\n\n";
}
} else if (el.type === "card") {
code += " // Card\n";
code += ` u8g2.drawRFrame(${el.x}, ${el.y}, ${el.w}, ${el.h}, 4);\n`;
if (txtEsc.length) {
const textY = el.y + 10;
code += ` u8g2.setFont(${font});\n`;
code += ` u8g2.drawUTF8(${el.x + 4}, ${textY}, "${txtEsc}");\n\n`;
}
}
// Actions (comments only)
if (el.actionType === "goto" && el.actionTargetScreenId) {
const targetScreen = screens.find(s => s.id === el.actionTargetScreenId);
if (targetScreen) {
const targetFnName = getScreenFnName(targetScreen);
code += ` // Action: if input is inside (${el.x}, ${el.y}, ${el.w}, ${el.h}) -> call ${targetFnName}();\n\n`;
}
}
});
code += " u8g2.sendBuffer();\n";
code += "}\n\n";
});
const defaultScreen =
screens.find((s) => s.id === activeScreenId) || screens[0];
code += "void setup() {\n";
code += " u8g2.begin();\n";
if (defaultScreen) {
const fnName = getScreenFnName(defaultScreen);
code += ` ${fnName}();\n`;
}
code += "}\n\n";
code += "void loop() {\n";
code += " // Put your input handling & screen switching here.\n";
code += " // For example, check buttons and call drawXXXScreen() as needed.\n";
code += "}\n";
codeOutput.value = code;
}
// --------------- Event handlers -----------------
applyResBtn.addEventListener("click", () => {
dispWidth = parseInt(dispWidthInput.value, 10) || 240;
dispHeight = parseInt(dispHeightInput.value, 10) || 320;
updatePreviewSize();
renderElements();
updateCode();
pushHistory();
});
bgColorInput.addEventListener("input", () => {
bgColor = bgColorInput.value;
updatePreviewSize();
updateCode();
pushHistory();
});
useSpriteCheckbox.addEventListener("change", () => {
useSprite = useSpriteCheckbox.checked;
updateCode();
pushHistory();
});
snapCheckbox.addEventListener("change", () => {
snapToGrid = snapCheckbox.checked;
pushHistory();
});
gridSizeInput.addEventListener("input", () => {
let v = parseInt(gridSizeInput.value, 10);
if (isNaN(v) || v <= 0) v = 1;
gridSize = v;
gridSizeInput.value = v;
pushHistory();
});
displayDriverSelect.addEventListener("change", () => {
driverMode = displayDriverSelect.value;
updatePreviewSize();
updateCode();
pushHistory();
});
u8g2PresetSelect.addEventListener("change", () => {
u8g2PresetId = u8g2PresetSelect.value;
updateCode();
pushHistory();
});
clearAllBtn.addEventListener("click", () => {
const scr = screens.find((s) => s.id === activeScreenId);
if (scr) {
scr.elements = [];
elements = scr.elements;
} else {
elements = [];
}
selectedId = null;
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
});
addButtons.forEach((btn) => {
btn.addEventListener("click", () => {
const type = btn.getAttribute("data-add");
addElement(type);
});
});
// Image upload
addImageBtn.addEventListener("click", () => {
imageInput.value = "";
imageInput.click();
});
imageInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
const w = img.width;
const h = img.height;
imgCanvas.width = w;
imgCanvas.height = h;
imgCtx.clearRect(0, 0, w, h);
imgCtx.drawImage(img, 0, 0, w, h);
const imageData = imgCtx.getImageData(0, 0, w, h);
const data = imageData.data;
const rgb565 = new Array(w * h);
for (let i = 0, p = 0; i < data.length; i += 4, p++) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const r5 = (r >> 3) & 0x1f;
const g6 = (g >> 2) & 0x3f;
const b5 = (b >> 3) & 0x1f;
const val = (r5 << 11) | (g6 << 5) | b5;
rgb565[p] = val;
}
const id = makeId();
const el = {
id,
type: "image",
x: Math.round((dispWidth - w) / 2),
y: Math.round((dispHeight - h) / 2),
w,
h,
text: "",
textSize: 2,
fillColor: "#ffffff",
strokeColor: "#ffffff",
textColor: "#000000",
value: 0,
imageName: "img_" + id.replace(/[^a-zA-Z0-9_]/g, "_"),
imageWidth: w,
imageHeight: h,
rgb565,
previewUrl: reader.result,
font: DEFAULT_U8G2_FONT
};
elements.push(el);
selectedId = id;
updatePreviewSize();
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
};
img.src = reader.result;
};
reader.readAsDataURL(file);
});
// Screen events
addScreenBtn.addEventListener("click", () => {
const screen = createScreen("Screen " + (screens.length + 1));
refreshScreenUI();
setActiveScreen(screen.id);
});
deleteScreenBtn.addEventListener("click", () => {
if (screens.length <= 1) {
alert("At least one screen is required.");
return;
}
screens = screens.filter((s) => s.id !== activeScreenId);
const next = screens[0];
setActiveScreen(next.id);
});
screenSelect.addEventListener("change", () => {
setActiveScreen(screenSelect.value);
});
screenFnNameInput.addEventListener("input", () => {
const scr = screens.find((s) => s.id === activeScreenId);
if (!scr) return;
scr.fnName = screenFnNameInput.value.trim();
updateCode();
pushHistory();
});
// Add element helper
function addElement(type) {
const id = makeId();
let baseW = 80;
let baseH = 30;
let text = "";
let textSize = 2;
let value = 50;
if (type === "label") {
baseW = 80;
baseH = 20;
text = "Label";
} else if (type === "button") {
baseW = 80;
baseH = 28;
text = "Button";
} else if (type === "header") {
baseW = dispWidth;
baseH = 24;
text = "Header";
} else if (type === "card") {
baseW = 100;
baseH = 50;
text = "Card";
} else if (type === "divider") {
baseW = 80;
baseH = 2;
} else if (type === "progress") {
baseW = 100;
baseH = 16;
value = 60;
} else if (type === "slider") {
baseW = 100;
baseH = 18;
value = 40;
} else if (type === "toggle") {
baseW = 50;
baseH = 20;
value = 0;
} else if (type === "circle") {
baseW = 30;
baseH = 30;
}
const el = {
id,
type,
x: Math.round((dispWidth - baseW) / 2),
y: Math.round((dispHeight - baseH) / 2),
w: baseW,
h: baseH,
text,
textSize,
fillColor: type === "label" ? "#000000" : "#ffffff",
strokeColor: "#ffffff",
textColor: "#000000",
value,
actionType: "",
actionTargetScreenId: null,
font: DEFAULT_U8G2_FONT
};
elements.push(el);
selectedId = id;
updatePreviewSize();
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
}
// Numeric binding helper
function bindNumeric(input, key) {
input.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
if (el.type === "image" && (key === "w" || key === "h")) {
input.value = el[key];
return;
}
let v = parseInt(input.value, 10);
if (isNaN(v)) v = 0;
if (snapToGrid && (key === "x" || key === "y") && gridSize > 0) {
v = Math.round(v / gridSize) * gridSize;
}
el[key] = v;
renderElements();
updateCode();
pushHistory();
});
}
bindNumeric(propX, "x");
bindNumeric(propY, "y");
bindNumeric(propW, "w");
bindNumeric(propH, "h");
bindNumeric(propTextSize, "textSize");
propText.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.text = propText.value;
renderElements();
updateCode();
pushHistory();
});
propFillColor.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.fillColor = propFillColor.value;
renderElements();
updateCode();
pushHistory();
});
propStrokeColor.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.strokeColor = propStrokeColor.value;
renderElements();
updateCode();
pushHistory();
});
propTextColor.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.textColor = propTextColor.value;
renderElements();
updateCode();
pushHistory();
});
propValue.addEventListener("input", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
let v = parseInt(propValue.value, 10);
if (isNaN(v)) v = 0;
v = Math.max(0, Math.min(100, v));
el.value = v;
renderElements();
updateCode();
pushHistory();
});
propFont.addEventListener("change", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.font = propFont.value || DEFAULT_U8G2_FONT;
updateCode();
pushHistory();
});
propAction.addEventListener("change", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
const val = propAction.value;
el.actionType = val || "";
if (!val) {
el.actionTargetScreenId = null;
} else {
// default target = first other screen
const target = screens.find(s => s.id !== activeScreenId) || screens[0];
el.actionTargetScreenId = target ? target.id : null;
}
refreshActionTargetOptions(el);
updateCode();
pushHistory();
});
propActionTarget.addEventListener("change", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
el.actionTargetScreenId = propActionTarget.value || null;
updateCode();
pushHistory();
});
deleteElementBtn.addEventListener("click", () => {
elements = elements.filter((el) => el.id !== selectedId);
const scr = screens.find((s) => s.id === activeScreenId);
if (scr) scr.elements = elements;
selectedId = null;
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
});
// Click to select element
preview.addEventListener("mousedown", (e) => {
if (e.target.classList.contains("ui-element")) {
const id = e.target.dataset.id;
selectElement(id, false);
pushHistory();
}
});
// Copy code
copyCodeBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(codeOutput.value);
copyCodeBtn.textContent = "Copied!";
setTimeout(() => {
copyCodeBtn.textContent = "Copy";
}, 1200);
} catch (e) {
alert("Could not copy. Please copy manually.");
}
});
// Undo / Redo
undoBtn.addEventListener("click", () => {
if (historyIndex <= 0) return;
historyIndex--;
updateUndoRedoButtons();
const snap = history[historyIndex];
applyStateSnapshot(snap);
});
redoBtn.addEventListener("click", () => {
if (historyIndex >= history.length - 1) return;
historyIndex++;
updateUndoRedoButtons();
const snap = history[historyIndex];
applyStateSnapshot(snap);
});
// Duplicate element
duplicateBtn.addEventListener("click", () => {
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
const id = makeId();
const copy = JSON.parse(JSON.stringify(el));
copy.id = id;
copy.x = el.x + 5;
copy.y = el.y + 5;
elements.push(copy);
selectedId = id;
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
});
// Zoom
zoomSlider.addEventListener("input", () => {
zoomFactor = parseInt(zoomSlider.value, 10) / 100;
updatePreviewSize();
renderElements();
});
// Update preview size on window resize
let resizeTimeout;
window.addEventListener("resize", () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
updatePreviewSize();
renderElements();
}, 100);
});
// Export JSON
exportJsonBtn.addEventListener("click", () => {
const data = deepCloneState();
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "ui_project.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
// Import JSON
importJsonBtn.addEventListener("click", () => {
importJsonInput.value = "";
importJsonInput.click();
});
importJsonInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
history = [];
historyIndex = -1;
applyStateSnapshot(data);
pushHistory();
} catch (err) {
alert("Invalid JSON project file.");
}
};
reader.readAsText(file);
});
// Alignment to screen
function alignSelected(mode) {
const el = elements.find(e => e.id === selectedId);
if (!el) return;
if (mode === "left") {
el.x = 0;
} else if (mode === "right") {
el.x = dispWidth - el.w;
} else if (mode === "hcenter") {
el.x = Math.round((dispWidth - el.w) / 2);
} else if (mode === "top") {
el.y = 0;
} else if (mode === "bottom") {
el.y = dispHeight - el.h;
} else if (mode === "vcenter") {
el.y = Math.round((dispHeight - el.h) / 2);
}
if (snapToGrid && gridSize > 0) {
el.x = Math.round(el.x / gridSize) * gridSize;
el.y = Math.round(el.y / gridSize) * gridSize;
}
updatePropsInputs(false);
renderElements();
updateCode();
pushHistory();
}
alignLeftBtn.addEventListener("click", () => alignSelected("left"));
alignRightBtn.addEventListener("click", () => alignSelected("right"));
alignHCenterBtn.addEventListener("click", () => alignSelected("hcenter"));
alignTopBtn.addEventListener("click", () => alignSelected("top"));
alignBottomBtn.addEventListener("click", () => alignSelected("bottom"));
alignVCenterBtn.addEventListener("click", () => alignSelected("vcenter"));
// Keyboard shortcuts (delete, arrows, undo/redo)
document.addEventListener("keydown", (e) => {
const tag = e.target.tagName.toLowerCase();
if (["input", "textarea", "select"].includes(tag)) return;
if ((e.key === "z" || e.key === "Z") && (e.ctrlKey || e.metaKey)) {
// Ctrl+Z / Cmd+Z
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
updateUndoRedoButtons();
applyStateSnapshot(history[historyIndex]);
}
return;
}
if ((e.key === "y" || (e.key === "Z" && e.shiftKey)) && (e.ctrlKey || e.metaKey)) {
// Ctrl+Y or Ctrl+Shift+Z
e.preventDefault();
if (historyIndex < history.length - 1) {
historyIndex++;
updateUndoRedoButtons();
applyStateSnapshot(history[historyIndex]);
}
return;
}
const el = elements.find((el) => el.id === selectedId);
if (!el) return;
if (e.key === "Delete" || e.key === "Backspace") {
e.preventDefault();
elements = elements.filter((x) => x.id !== selectedId);
const scr = screens.find((s) => s.id === activeScreenId);
if (scr) scr.elements = elements;
selectedId = null;
renderElements();
updatePropsInputs();
updateCode();
pushHistory();
return;
}
if (e.key.startsWith("Arrow")) {
e.preventDefault();
const step = e.shiftKey ? 10 : 1;
if (e.key === "ArrowLeft") el.x -= step;
if (e.key === "ArrowRight") el.x += step;
if (e.key === "ArrowUp") el.y -= step;
if (e.key === "ArrowDown") el.y += step;
if (snapToGrid && gridSize > 0) {
el.x = Math.round(el.x / gridSize) * gridSize;
el.y = Math.round(el.y / gridSize) * gridSize;
}
updatePropsInputs(false);
renderElements();
updateCode();
pushHistory();
}
});
// --------------- Init -----------------
bgColor = bgColorInput.value;
snapToGrid = snapCheckbox.checked;
gridSize = parseInt(gridSizeInput.value, 10) || 4;
initU8g2Presets();
initFontList();
initScreens();
</script>
</body>
</html>