From 9065eebfcb0853a27221ef144ca722b007dad078 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:17:13 +0000 Subject: [PATCH] Encode the full Glitztype piece in a local share URL --- ventures/glitztype/app.js | 719 +++++++++++++++++++++++++++----------- 1 file changed, 515 insertions(+), 204 deletions(-) diff --git a/ventures/glitztype/app.js b/ventures/glitztype/app.js index 08352d1..9534e22 100644 --- a/ventures/glitztype/app.js +++ b/ventures/glitztype/app.js @@ -1,35 +1,63 @@ // Glitztype App Logic +// The preloaded sample and a typed word share one path: state goes through +// renderPreview(), which draws on canvas. A share URL encodes that full state +// locally (base64url in ?s=) so a friend opens the exact same piece. Time.save +// is only an extra; the copied link never depends on a server id. (function () { const trySection = document.getElementById("try"); if (!trySection) return; - let currentText = "Glitztype"; - let currentStyle = "rainbow"; - let currentFont = "Arial"; - let currentBend = 0; - let currentColors = { - rainbow: ["#ff0000", "#ffff00", "#00ff00", "#00ffff", "#0000ff", "#ff00ff"], - chrome: "#cccccc", - neon: "#00ffff", - arch: "#ff6600", - gradient: "#ff00ff", - shadow: "#000000", - outline: "#ff0000", - italic: "#0066cc", - bold: "#333333", - wave: "#9932cc", - stripe: "#ff4500", - glow: "#ffd700" + const SAMPLE = { + text: "Nadia", + style: "rainbow", + font: "Impact", + bend: 8, + colors: { + rainbow: ["#ff0000", "#ff8800", "#ffff00", "#00cc44", "#0088ff", "#cc00ff"], + chrome: "#d8d8e0", + neon: "#39ff14", + arch: "#ff6600", + gradient: "#ff2bd6", + shadow: "#1a1a2e", + outline: "#ff1744", + italic: "#0066cc", + bold: "#222222", + wave: "#9932cc", + stripe: "#ff4500", + glow: "#ffd700" + } }; - // Sample styles mimicking WordArt - expanded to a dozen + const FONTS = [ + "Arial", + "Courier New", + "Georgia", + "Impact", + "Verdana", + "Comic Sans MS", + "Times New Roman" + ]; + + const STYLE_KEYS = [ + "rainbow", "chrome", "neon", "arch", "gradient", "shadow", + "outline", "italic", "bold", "wave", "stripe", "glow" + ]; + + let currentText = SAMPLE.text; + let currentStyle = SAMPLE.style; + let currentFont = SAMPLE.font; + let currentBend = SAMPLE.bend; + let currentColors = cloneColors(SAMPLE.colors); + + // Sample styles mimicking WordArt - a dozen clean-room treatments const styles = { rainbow: { name: "Rainbow Gradient", - draw: function(ctx, text, x, y, fontSize) { - const gradient = ctx.createLinearGradient(0, 0, 300, 0); - const colors = currentColors.rainbow; + draw: function (ctx, text, x, y, fontSize) { + const width = Math.max(ctx.measureText(text).width, fontSize); + const gradient = ctx.createLinearGradient(x - width / 2, y, x + width / 2, y); + const colors = Array.isArray(currentColors.rainbow) ? currentColors.rainbow : SAMPLE.colors.rainbow; for (let i = 0; i < colors.length; i++) { gradient.addColorStop(i / (colors.length - 1), colors[i]); } @@ -39,40 +67,59 @@ }, chrome: { name: "Chrome", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.chrome; - ctx.strokeStyle = "#333333"; - ctx.lineWidth = 2; + draw: function (ctx, text, x, y, fontSize) { + const width = Math.max(ctx.measureText(text).width, fontSize); + const gradient = ctx.createLinearGradient(x - width / 2, y - fontSize, x + width / 2, y + fontSize / 2); + const base = colorOf("chrome", "#c0c0c0"); + gradient.addColorStop(0, "#ffffff"); + gradient.addColorStop(0.35, base); + gradient.addColorStop(0.55, "#6e6e78"); + gradient.addColorStop(0.75, "#f4f4f8"); + gradient.addColorStop(1, "#8a8a96"); + ctx.strokeStyle = "#2a2a32"; + ctx.lineWidth = Math.max(2, fontSize / 18); ctx.strokeText(text, x, y); + ctx.fillStyle = gradient; ctx.fillText(text, x, y); + ctx.strokeStyle = "rgba(255,255,255,0.7)"; + ctx.lineWidth = 1; + ctx.strokeText(text, x, y - 1); } }, neon: { name: "Neon Outline", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = "#000000"; - ctx.strokeStyle = currentColors.neon; - ctx.lineWidth = 3; + draw: function (ctx, text, x, y, fontSize) { + const neon = colorOf("neon", "#00ffff"); + ctx.shadowColor = neon; + ctx.shadowBlur = fontSize / 3; + ctx.strokeStyle = neon; + ctx.lineWidth = Math.max(3, fontSize / 12); ctx.lineJoin = "round"; ctx.strokeText(text, x, y); + ctx.shadowBlur = fontSize / 6; + ctx.strokeText(text, x, y); + ctx.fillStyle = "#050510"; + ctx.shadowBlur = 0; ctx.fillText(text, x, y); } }, arch: { name: "Arch", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.arch; - ctx.font = `bold ${fontSize}px Arial`; - const chars = text.split(''); - const arcRadius = 50; - const angleStep = Math.PI / (chars.length + 1); + draw: function (ctx, text, x, y, fontSize) { + ctx.fillStyle = colorOf("arch", "#ff6600"); + ctx.font = `bold ${fontSize}px ${currentFont}`; + const chars = text.split(""); + const arcRadius = Math.max(fontSize * 1.6, chars.length * fontSize * 0.42); + const spread = Math.min(Math.PI * 0.85, Math.max(0.4, chars.length * 0.28)); + const start = Math.PI / 2 + spread / 2; + const angleStep = chars.length > 1 ? spread / (chars.length - 1) : 0; for (let i = 0; i < chars.length; i++) { - const angle = angleStep * (i + 1); + const angle = start - angleStep * i; const charX = x + Math.cos(angle) * arcRadius; - const charY = y - Math.sin(angle) * arcRadius; + const charY = y + fontSize * 0.35 - Math.sin(angle) * arcRadius + arcRadius * 0.35; ctx.save(); ctx.translate(charX, charY); - ctx.rotate(Math.PI/2 - angle); + ctx.rotate(Math.PI / 2 - angle); ctx.fillText(chars[i], 0, 0); ctx.restore(); } @@ -80,60 +127,68 @@ }, gradient: { name: "Vertical Gradient", - draw: function(ctx, text, x, y, fontSize) { - const gradient = ctx.createLinearGradient(0, 0, 0, 100); - gradient.addColorStop(0, currentColors.gradient); - gradient.addColorStop(1, "#800080"); + draw: function (ctx, text, x, y, fontSize) { + const gradient = ctx.createLinearGradient(x, y - fontSize, x, y + fontSize * 0.4); + gradient.addColorStop(0, colorOf("gradient", "#ff00ff")); + gradient.addColorStop(1, "#4b0082"); ctx.fillStyle = gradient; ctx.fillText(text, x, y); } }, shadow: { name: "Drop Shadow", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = "#aaaaaa"; - ctx.fillText(text, x + 3, y + 3); - ctx.fillStyle = currentColors.shadow; + draw: function (ctx, text, x, y, fontSize) { + const offset = Math.max(3, fontSize / 12); + ctx.fillStyle = "rgba(0,0,0,0.35)"; + ctx.fillText(text, x + offset, y + offset); + ctx.fillStyle = colorOf("shadow", "#111111"); ctx.fillText(text, x, y); } }, outline: { name: "Thick Outline", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = "#ffffff"; - ctx.strokeStyle = currentColors.outline; - ctx.lineWidth = 5; + draw: function (ctx, text, x, y, fontSize) { ctx.lineJoin = "round"; + ctx.miterLimit = 2; + ctx.lineWidth = Math.max(5, fontSize / 7); + ctx.strokeStyle = colorOf("outline", "#ff0000"); ctx.strokeText(text, x, y); + ctx.fillStyle = "#ffffff"; ctx.fillText(text, x, y); } }, italic: { name: "Italic Slant", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.italic; - ctx.font = `bold italic ${fontSize}px Arial`; + draw: function (ctx, text, x, y, fontSize) { + ctx.fillStyle = colorOf("italic", "#0066cc"); + ctx.font = `bold italic ${fontSize}px ${currentFont}`; ctx.fillText(text, x, y); } }, bold: { name: "Extra Bold", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.bold; - ctx.font = `900 ${fontSize}px Arial`; + draw: function (ctx, text, x, y, fontSize) { + ctx.fillStyle = colorOf("bold", "#333333"); + ctx.font = `900 ${fontSize}px ${currentFont}`; + ctx.lineWidth = Math.max(1, fontSize / 28); + ctx.strokeStyle = colorOf("bold", "#333333"); + ctx.strokeText(text, x, y); ctx.fillText(text, x, y); } }, wave: { name: "Wave", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.wave; - ctx.font = `bold ${fontSize}px Arial`; - const chars = text.split(''); + draw: function (ctx, text, x, y, fontSize) { + ctx.fillStyle = colorOf("wave", "#9932cc"); + ctx.font = `bold ${fontSize}px ${currentFont}`; + const chars = text.split(""); + const advance = fontSize * 0.62; + const total = Math.max(advance, (chars.length - 1) * advance); for (let i = 0; i < chars.length; i++) { - const waveOffset = Math.sin(i * 0.5) * 15; + const waveOffset = Math.sin(i * 0.7 + currentBend / 30) * (fontSize * 0.28); ctx.save(); - ctx.translate(x - (text.length * fontSize/4) + (i * fontSize/2), y + waveOffset); + ctx.translate(x - total / 2 + i * advance, y + waveOffset); + ctx.rotate(Math.cos(i * 0.7) * 0.18); ctx.fillText(chars[i], 0, 0); ctx.restore(); } @@ -141,56 +196,242 @@ }, stripe: { name: "Striped", - draw: function(ctx, text, x, y, fontSize) { - ctx.fillStyle = currentColors.stripe; + draw: function (ctx, text, x, y, fontSize) { + ctx.fillStyle = colorOf("stripe", "#ff4500"); ctx.fillText(text, x, y); - - // Add stripes - ctx.strokeStyle = "#ffffff"; - ctx.lineWidth = 2; - const textWidth = ctx.measureText(text).width; - for (let i = -textWidth; i < textWidth; i += 10) { + ctx.save(); + ctx.beginPath(); + // Clip stripes to the glyphs so the PNG stays a sticker, not a box. + ctx.fillText(text, x, y); + // The second fill is only to establish the clip path via destination-in below. + const textWidth = Math.max(ctx.measureText(text).width, fontSize); + ctx.globalCompositeOperation = "source-atop"; + ctx.strokeStyle = "rgba(255,255,255,0.85)"; + ctx.lineWidth = Math.max(2, fontSize / 18); + for (let i = -textWidth; i < textWidth; i += Math.max(8, fontSize / 5)) { ctx.beginPath(); - ctx.moveTo(x + i, y - 30); - ctx.lineTo(x + i + 30, y + 30); + ctx.moveTo(x + i, y - fontSize); + ctx.lineTo(x + i + fontSize * 0.6, y + fontSize * 0.6); ctx.stroke(); } + ctx.restore(); } }, glow: { name: "Glow", - draw: function(ctx, text, x, y, fontSize) { - ctx.shadowColor = currentColors.glow; - ctx.shadowBlur = 15; - ctx.fillStyle = "#ffffff"; + draw: function (ctx, text, x, y, fontSize) { + const glow = colorOf("glow", "#ffd700"); + ctx.shadowColor = glow; + ctx.shadowBlur = fontSize / 2; + ctx.fillStyle = glow; + ctx.fillText(text, x, y); + ctx.shadowBlur = fontSize / 5; + ctx.fillStyle = "#fff8dc"; ctx.fillText(text, x, y); ctx.shadowBlur = 0; } } }; + function cloneColors(src) { + const out = {}; + STYLE_KEYS.forEach(function (key) { + const value = src && src[key] != null ? src[key] : SAMPLE.colors[key]; + out[key] = Array.isArray(value) ? value.slice() : value; + }); + return out; + } + + function colorOf(key, fallback) { + const value = currentColors[key]; + if (typeof value === "string" && value) return value; + return fallback; + } + + function hexColor(value, fallback) { + const s = String(value || "").trim(); + if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.toLowerCase(); + if (/^#[0-9a-fA-F]{3}$/.test(s)) { + return ("#" + s[1] + s[1] + s[2] + s[2] + s[3] + s[3]).toLowerCase(); + } + return fallback; + } + + function pickerValue() { + const value = currentColors[currentStyle]; + if (Array.isArray(value) && value.length) return hexColor(value[0], "#ff0000"); + return hexColor(value, "#ff0000"); + } + + function clampText(value) { + const text = String(value == null ? "" : value).replace(/\s+/g, " ").trim(); + return text.slice(0, 42); + } + + function clampBend(value) { + const n = parseInt(value, 10); + if (!isFinite(n)) return 0; + return Math.max(-30, Math.min(30, n)); + } + + function clampFont(value) { + return FONTS.indexOf(value) >= 0 ? value : "Arial"; + } + + function clampStyle(value) { + return styles[value] ? value : "rainbow"; + } + + // Full piece: text, style, font, bend, and every style color. This is what + // the share URL carries, and what renderPreview reads. + function pieceState() { + return { + text: currentText, + style: currentStyle, + font: currentFont, + bend: currentBend, + colors: cloneColors(currentColors) + }; + } + + function applyPiece(piece) { + const next = normalizePiece(piece); + if (!next) return false; + currentText = next.text; + currentStyle = next.style; + currentFont = next.font; + currentBend = next.bend; + currentColors = next.colors; + syncControls(); + renderStyleGrid(); + renderPreview(); + return true; + } + + function normalizePiece(piece) { + if (!piece || typeof piece !== "object") return null; + const text = clampText(piece.text); + if (!text) return null; + const colors = cloneColors(SAMPLE.colors); + const incoming = piece.colors && typeof piece.colors === "object" ? piece.colors : null; + if (incoming) { + STYLE_KEYS.forEach(function (key) { + if (key === "rainbow" && Array.isArray(incoming.rainbow) && incoming.rainbow.length >= 2) { + colors.rainbow = incoming.rainbow.slice(0, 8).map(function (c, i) { + return hexColor(c, SAMPLE.colors.rainbow[i] || "#ff0000"); + }); + return; + } + if (typeof incoming[key] === "string") { + colors[key] = hexColor(incoming[key], SAMPLE.colors[key]); + } + }); + } + return { + text: text, + style: clampStyle(piece.style), + font: clampFont(piece.font), + bend: clampBend(piece.bend), + colors: colors + }; + } + + function encodeState(obj) { + const json = JSON.stringify({ + text: obj.text, + style: obj.style, + font: obj.font, + bend: obj.bend, + colors: obj.colors + }); + return btoa(unescape(encodeURIComponent(json))) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/g, ""); + } + + function decodeState(token) { + try { + let b64 = String(token).replace(/-/g, "+").replace(/_/g, "/"); + while (b64.length % 4) b64 += "="; + const json = decodeURIComponent(escape(atob(b64))); + return normalizePiece(JSON.parse(json)); + } catch (e) { + return null; + } + } + + function shareUrl() { + const url = new URL(window.location.href); + url.searchParams.set("s", encodeState(pieceState())); + url.hash = ""; + return url.toString(); + } + + function writeUrl() { + try { + const url = new URL(window.location.href); + url.searchParams.set("s", encodeState(pieceState())); + url.hash = ""; + window.history.replaceState({}, "", url); + } catch (e) { /* file:// or a locked history */ } + } + + function readUrl() { + const params = new URLSearchParams(window.location.search); + const token = params.get("s"); + if (!token) return null; + // A server link id is short and has no encoded payload. Ignore it so the + // page never pretends a partial or remote token restored the piece. + if (/^[a-z0-9]{6,32}$/.test(token) && token.indexOf("-") < 0 && token.indexOf("_") < 0) { + return null; + } + return decodeState(token); + } + + function copyText(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + return new Promise(function (resolve, reject) { + const input = document.createElement("textarea"); + input.value = text; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.left = "-9999px"; + document.body.appendChild(input); + input.select(); + try { + document.execCommand("copy") ? resolve() : reject(new Error("copy failed")); + } catch (e) { + reject(e); + } + document.body.removeChild(input); + }); + } + + function fontOptions() { + return FONTS.map(function (font) { + return ``; + }).join(""); + } + // Create UI elements function createUI() { trySection.innerHTML = `
New style pack drops every Friday
`; renderStyleGrid(); setupEventListeners(); - renderPreview(); } - // Render style grid + // Render style grid. Each tile is a live canvas of the current word, drawn + // by the same style functions as the main preview — not a canned thumbnail. function renderStyleGrid() { const grid = document.getElementById("styleGrid"); if (!grid) return; - grid.innerHTML = ''; - Object.keys(styles).forEach(styleKey => { + grid.innerHTML = ""; + STYLE_KEYS.forEach(function (styleKey) { const style = styles[styleKey]; - const el = document.createElement("div"); + const el = document.createElement("button"); + el.type = "button"; el.className = "style-option"; el.dataset.style = styleKey; - el.textContent = style.name; - if (styleKey === currentStyle) { - el.classList.add("selected"); - } + el.setAttribute("aria-pressed", styleKey === currentStyle ? "true" : "false"); + if (styleKey === currentStyle) el.classList.add("selected"); + + const canvas = document.createElement("canvas"); + canvas.width = 220; + canvas.height = 84; + canvas.className = "style-thumb"; + canvas.setAttribute("aria-hidden", "true"); + const label = document.createElement("span"); + label.textContent = style.name; + el.appendChild(canvas); + el.appendChild(label); grid.appendChild(el); + paintStyle(canvas, styleKey, currentText || " ", 28); }); } + function paintStyle(canvas, styleKey, text, fontSize) { + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.font = `bold ${fontSize}px ${currentFont}`; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + const draw = styles[styleKey] ? styles[styleKey].draw : styles.rainbow.draw; + ctx.save(); + if (currentBend !== 0 && styleKey !== "arch" && styleKey !== "wave") { + ctx.translate(canvas.width / 2, canvas.height / 2); + ctx.rotate(currentBend * Math.PI / 180); + ctx.translate(-canvas.width / 2, -canvas.height / 2); + } + draw(ctx, text, canvas.width / 2, canvas.height / 2, fontSize); + ctx.restore(); + } + + function syncControls() { + const input = document.getElementById("textInput"); + const font = document.getElementById("fontSelect"); + const bend = document.getElementById("bendSlider"); + const color = document.getElementById("colorPicker"); + if (input) input.value = currentText; + if (font) font.value = currentFont; + if (bend) bend.value = String(currentBend); + if (color) color.value = pickerValue(); + } + // Setup event listeners function setupEventListeners() { - document.getElementById("textInput").addEventListener("input", function(e) { - currentText = e.target.value || " "; + document.getElementById("textInput").addEventListener("input", function (e) { + currentText = clampText(e.target.value) || " "; + renderStyleGrid(); renderPreview(); + writeUrl(); }); - document.getElementById("fontSelect").addEventListener("change", function(e) { - currentFont = e.target.value; + document.getElementById("fontSelect").addEventListener("change", function (e) { + currentFont = clampFont(e.target.value); + renderStyleGrid(); renderPreview(); + writeUrl(); }); - document.getElementById("bendSlider").addEventListener("input", function(e) { - currentBend = parseInt(e.target.value); + document.getElementById("bendSlider").addEventListener("input", function (e) { + currentBend = clampBend(e.target.value); + renderStyleGrid(); renderPreview(); + writeUrl(); }); - document.getElementById("colorPicker").addEventListener("input", function(e) { - // Update the current style's color - currentColors[currentStyle] = e.target.value; + document.getElementById("colorPicker").addEventListener("input", function (e) { + const picked = hexColor(e.target.value, "#ff0000"); + if (currentStyle === "rainbow") { + currentColors.rainbow = rainbowFrom(picked); + } else { + currentColors[currentStyle] = picked; + } + renderStyleGrid(); renderPreview(); + writeUrl(); }); document.getElementById("downloadBtn").addEventListener("click", downloadPNG); - - document.getElementById("shareBtn").addEventListener("click", copyShareURL); + document.getElementById("shareLink").addEventListener("click", function (e) { + e.preventDefault(); + copyShareURL(); + }); - document.querySelectorAll(".style-option").forEach(el => { - el.addEventListener("click", function() { - document.querySelectorAll(".style-option").forEach(e => e.classList.remove("selected")); - this.classList.add("selected"); - currentStyle = this.dataset.style; - // Update color picker to match current style's color - document.getElementById("colorPicker").value = currentColors[currentStyle]; - renderPreview(); + document.getElementById("styleGrid").addEventListener("click", function (e) { + const el = e.target.closest(".style-option"); + if (!el) return; + document.querySelectorAll(".style-option").forEach(function (node) { + node.classList.remove("selected"); + node.setAttribute("aria-pressed", "false"); }); + el.classList.add("selected"); + el.setAttribute("aria-pressed", "true"); + currentStyle = clampStyle(el.dataset.style); + document.getElementById("colorPicker").value = pickerValue(); + renderPreview(); + writeUrl(); }); } - // Render preview + function rainbowFrom(hex) { + const base = hexColor(hex, "#ff0000"); + const hsl = hexToHsl(base); + const stops = []; + for (let i = 0; i < 6; i++) { + stops.push(hslToHex((hsl.h + i * 48) % 360, Math.max(70, hsl.s), Math.max(48, hsl.l))); + } + return stops; + } + + function hexToHsl(hex) { + const n = parseInt(hex.slice(1), 16); + const r = ((n >> 16) & 255) / 255; + const g = ((n >> 8) & 255) / 255; + const b = (n & 255) / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + const l = (max + min) / 2; + const d = max - min; + const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1)); + if (d !== 0) { + if (max === r) h = ((g - b) / d) % 6; + else if (max === g) h = (b - r) / d + 2; + else h = (r - g) / d + 4; + h *= 60; + if (h < 0) h += 360; + } + return { h: h, s: s * 100, l: l * 100 }; + } + + function hslToHex(h, s, l) { + s /= 100; + l /= 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0; + let g = 0; + let b = 0; + if (h < 60) { r = c; g = x; } + else if (h < 120) { r = x; g = c; } + else if (h < 180) { g = c; b = x; } + else if (h < 240) { g = x; b = c; } + else if (h < 300) { r = x; b = c; } + else { r = c; b = x; } + const to = function (v) { + return Math.round((v + m) * 255).toString(16).padStart(2, "0"); + }; + return "#" + to(r) + to(g) + to(b); + } + + // Render preview. Sample and user input both land here. function renderPreview() { const canvas = document.getElementById("previewCanvas"); if (!canvas) return; - - const ctx = canvas.getContext("2d"); - ctx.clearRect(0, 0, canvas.width, canvas.height); - - ctx.font = `bold 48px ${currentFont}`; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - - const styleFunc = styles[currentStyle] ? styles[currentStyle].draw : styles.rainbow.draw; - - // Apply bend transformation if needed - if (currentBend !== 0) { - ctx.save(); - ctx.translate(canvas.width / 2, canvas.height / 2); - ctx.rotate(currentBend * Math.PI / 180); - ctx.translate(-canvas.width / 2, -canvas.height / 2); - styleFunc(ctx, currentText, canvas.width / 2, canvas.height / 2, 48); - ctx.restore(); - } else { - styleFunc(ctx, currentText, canvas.width / 2, canvas.height / 2, 48); - } + const text = currentText || " "; + const fontSize = text.length > 16 ? 56 : text.length > 10 ? 72 : 84; + paintStyle(canvas, currentStyle, text, fontSize); } - // Download PNG + // Download a transparent PNG of the same canvas the preview just drew. function downloadPNG() { const canvas = document.getElementById("previewCanvas"); if (!canvas) return; - + renderPreview(); + const slug = (clampText(currentText) || "wordart") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-|-$/g, "") || "wordart"; const link = document.createElement("a"); - link.download = `${currentText || "wordart"}-${currentStyle}.png`; + link.download = slug + "-" + currentStyle + ".png"; link.href = canvas.toDataURL("image/png"); link.click(); } - - // Copy share URL + + // Copy a URL that encodes the full piece locally. No server round-trip. function copyShareURL() { - // Create state object to encode - const state = { - text: currentText, - style: currentStyle, - font: currentFont, - bend: currentBend, - colors: currentColors + const link = document.getElementById("shareLink"); + const url = shareUrl(); + writeUrl(); + const flash = function (label) { + if (!link) return; + const previous = link.textContent; + link.textContent = label; + setTimeout(function () { link.textContent = previous; }, 1600); }; - - // Use Time.save to get a shareable link - Time.save(state).then(function(link) { - if (link) { - const fullUrl = new URL(window.location); - fullUrl.searchParams.set('s', link); - navigator.clipboard.writeText(fullUrl.toString()).then(function() { - alert("Share URL copied to clipboard!"); - }).catch(function() { - // Fallback if clipboard fails - prompt("Copy this share URL:", fullUrl.toString()); - }); - } else { - // Fallback for when backend is unavailable - const baseUrl = window.location.origin + window.location.pathname; - const params = new URLSearchParams({ - text: currentText, - style: currentStyle, - font: currentFont, - bend: currentBend - }); - const fallbackUrl = `${baseUrl}?${params}`; - navigator.clipboard.writeText(fallbackUrl).then(function() { - alert("Share URL copied to clipboard! (local encoding)"); - }).catch(function() { - prompt("Copy this share URL:", fallbackUrl); - }); - } + copyText(url).then(function () { + flash("Copied"); + }).catch(function () { + flash("Copy the link"); + window.prompt("Share link", url); }); } - // Initialize on load - window.addEventListener("DOMContentLoaded", function() { + function boot() { createUI(); - - // Check for shared state - Time.load().then(function(data) { - if (data) { - currentText = data.text || currentText; - currentStyle = data.style || currentStyle; - currentFont = data.font || currentFont; - currentBend = data.bend || currentBend; - if (data.colors) { - currentColors = {...currentColors, ...data.colors}; - } - document.getElementById("textInput").value = currentText; - document.getElementById("fontSelect").value = currentFont; - document.getElementById("bendSlider").value = currentBend; - // Update color picker to match current style's color - document.getElementById("colorPicker").value = currentColors[currentStyle]; - renderStyleGrid(); - renderPreview(); - } - }); - }); -})(); \ No newline at end of file + const fromUrl = readUrl(); + if (fromUrl) { + applyPiece(fromUrl); + return; + } + // First load: the sample is real input through the same apply/render path + // a typed word and a shared URL use. Nothing is a canned picture. + applyPiece(SAMPLE); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", boot); + } else { + boot(); + } + + window.Glitztype = { + encodeState: function () { return encodeState(pieceState()); }, + decodeState: decodeState, + piece: pieceState, + sample: function () { return normalizePiece(SAMPLE); } + }; +})();