← back to Dw Design Bridge
feat: SVG pattern preview generator (botanical/deco/damask/pop/metallic/woven/patina/geometric)
c2c19fadb7c7c5a9f5501dbe90b7e3ee27e7ffcf · 2026-05-07 23:30:39 -0700 · Steve
Files touched
A jobs/2026-05-08-06-04-13-5b665c.jsonA jobs/2026-05-08-06-04-13-ad14fb.jsonA jobs/2026-05-08-06-04-13-fd95e4.jsonM public/index.htmlM server.js
Diff
commit c2c19fadb7c7c5a9f5501dbe90b7e3ee27e7ffcf
Author: Steve <steve@designerwallcoverings.com>
Date: Thu May 7 23:30:39 2026 -0700
feat: SVG pattern preview generator (botanical/deco/damask/pop/metallic/woven/patina/geometric)
---
jobs/2026-05-08-06-04-13-5b665c.json | 51 ++++++++++++++
jobs/2026-05-08-06-04-13-ad14fb.json | 30 ++++++++
jobs/2026-05-08-06-04-13-fd95e4.json | 51 ++++++++++++++
public/index.html | 6 +-
server.js | 131 ++++++++++++++++++++++++++++++++++-
5 files changed, 266 insertions(+), 3 deletions(-)
diff --git a/jobs/2026-05-08-06-04-13-5b665c.json b/jobs/2026-05-08-06-04-13-5b665c.json
new file mode 100644
index 0000000..b3df7d1
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-5b665c.json
@@ -0,0 +1,51 @@
+{
+ "id": "2026-05-08-06-04-13-5b665c",
+ "brief": "Modern apartment bedroom · pastel · peel-and-stick · botanical small-scale · serene",
+ "count": 3,
+ "status": "done",
+ "created_at": "2026-05-08T06:04:13.202Z",
+ "progress": "3/3",
+ "designs": [
+ {
+ "idx": 0,
+ "ok": true,
+ "design": {
+ "name": "Whispering Petals",
+ "pattern_concept": "Subtle botanical motifs in soft pastels, creating a calming, organic ambiance",
+ "palette": [
+ "#f5e6ea",
+ "#e0f2f1",
+ "#f0e6fa",
+ "#f5f5dc"
+ ],
+ "scale": "small",
+ "repeat": "drop match 21in vertical",
+ "material_pairing": "vinyl",
+ "style_tags": [
+ "modern",
+ "serene",
+ "botanical"
+ ],
+ "use_case": "modern apartment bedroom accent wall",
+ "marketing_copy": "Soft pastel botanicals bring tranquil elegance to modern bedrooms",
+ "trade_grade": "residential",
+ "price_band": "$$"
+ },
+ "generated_at": "2026-05-08T06:05:33.989Z"
+ },
+ {
+ "idx": 1,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:06:50.161Z"
+ },
+ {
+ "idx": 2,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:08:10.788Z"
+ }
+ ],
+ "started_at": "2026-05-08T06:04:13.202Z",
+ "finished_at": "2026-05-08T06:08:10.794Z"
+}
\ No newline at end of file
diff --git a/jobs/2026-05-08-06-04-13-ad14fb.json b/jobs/2026-05-08-06-04-13-ad14fb.json
new file mode 100644
index 0000000..ddfb07b
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-ad14fb.json
@@ -0,0 +1,30 @@
+{
+ "id": "2026-05-08-06-04-13-ad14fb",
+ "brief": "Restaurant private dining room · moody · art-deco geometric · gilded accents · silk-luxe",
+ "count": 3,
+ "status": "done",
+ "created_at": "2026-05-08T06:04:13.185Z",
+ "progress": "3/3",
+ "designs": [
+ {
+ "idx": 0,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:04:52.719Z"
+ },
+ {
+ "idx": 1,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:05:57.531Z"
+ },
+ {
+ "idx": 2,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:07:16.724Z"
+ }
+ ],
+ "started_at": "2026-05-08T06:04:13.185Z",
+ "finished_at": "2026-05-08T06:07:16.729Z"
+}
\ No newline at end of file
diff --git a/jobs/2026-05-08-06-04-13-fd95e4.json b/jobs/2026-05-08-06-04-13-fd95e4.json
new file mode 100644
index 0000000..6143634
--- /dev/null
+++ b/jobs/2026-05-08-06-04-13-fd95e4.json
@@ -0,0 +1,51 @@
+{
+ "id": "2026-05-08-06-04-13-fd95e4",
+ "brief": "Hotel lobby feature wall · trade-grade · jewel-toned · botanical large-scale · evening dramatic · class A vinyl",
+ "count": 3,
+ "status": "done",
+ "created_at": "2026-05-08T06:04:13.143Z",
+ "progress": "3/3",
+ "designs": [
+ {
+ "idx": 0,
+ "ok": true,
+ "design": {
+ "name": "Nocturnal Bloom",
+ "pattern_concept": "Dramatic, large-scale botanical motif with deep, jewel-toned foliage and flowers under a moody, twilight sky",
+ "palette": [
+ "#2E1B52",
+ "#4A235A",
+ "#5E3A8C",
+ "#1B1E3D"
+ ],
+ "scale": "large",
+ "repeat": "drop match 24in vertical",
+ "material_pairing": "vinyl",
+ "style_tags": [
+ "botanical",
+ "luxury",
+ "dramatic"
+ ],
+ "use_case": "hotel lobby feature wall",
+ "marketing_copy": "Dramatic botanical elegance in rich jewel tones for a statement hotel lobby wall",
+ "trade_grade": "class A",
+ "price_band": "$$$"
+ },
+ "generated_at": "2026-05-08T06:05:15.923Z"
+ },
+ {
+ "idx": 1,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:06:23.336Z"
+ },
+ {
+ "idx": 2,
+ "ok": false,
+ "error": "no JSON found in response",
+ "generated_at": "2026-05-08T06:07:43.227Z"
+ }
+ ],
+ "started_at": "2026-05-08T06:04:13.144Z",
+ "finished_at": "2026-05-08T06:07:43.234Z"
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 8079ef3..445ab42 100644
--- a/public/index.html
+++ b/public/index.html
@@ -82,15 +82,17 @@ async function refresh() {
<div class="brief">${escapeHtml(j.brief)}</div>
<div class="progress">${j.progress || ''} ${j.error ? '· ' + escapeHtml(j.error) : ''}</div>
<div class="designs">
- ${(j.designs || []).filter(d => d.ok).map(d => renderDesign(d.design)).join('')}
+ ${(j.designs || []).filter(d => d.ok).map(d => renderDesign(d.design, j.id, d.idx)).join('')}
</div>
</div>
`).join('') || '<div class="job"><div class="brief" style="color:var(--muted)">No jobs yet — queue one above.</div></div>';
}
-function renderDesign(d) {
+function renderDesign(d, jobId, idx) {
const palette = (d.palette || []).map(c => `<span style="background:${escapeHtml(c)}"></span>`).join('');
+ const previewSrc = jobId != null && idx != null ? `/jobs/${jobId}/${idx}.svg` : '';
return `<div class="design">
+ ${previewSrc ? `<img src="${previewSrc}" alt="${escapeHtml(d.name || '')}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:3px;margin-bottom:8px;display:block">` : ''}
<div class="name">${escapeHtml(d.name || 'Untitled')}</div>
<div class="palette">${palette}</div>
<div class="meta">
diff --git a/server.js b/server.js
index cc05dea..b1d2d22 100644
--- a/server.js
+++ b/server.js
@@ -81,6 +81,125 @@ Output STRICTLY one JSON object on a single line, no prose, no markdown fence:
}
}
+// SVG pattern generator — uses the design's palette + style_tags to render a tileable preview.
+// Deterministic per (id, idx) so previews are stable. Runs free + instantly (no LLM call).
+function patternSvg(design, seed) {
+ const pal = (design.palette && design.palette.length) ? design.palette : ['#1a1a1a', '#c9a14b', '#e8e8e8', '#5a3a1a'];
+ const tags = (design.style_tags || []).map(t => String(t).toLowerCase());
+ const scale = design.scale || 'medium';
+ const tileSize = { small: 80, medium: 140, large: 220, oversize: 320 }[scale] || 140;
+
+ // Seeded PRNG so previews are stable
+ let s = 0;
+ for (let i = 0; i < seed.length; i++) s = (s * 31 + seed.charCodeAt(i)) | 0;
+ const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
+
+ // Pick motif family from style_tags
+ let motif = 'geometric';
+ if (tags.some(t => /botan|floral|leaf|garden|jungle|tropical/.test(t))) motif = 'botanical';
+ else if (tags.some(t => /deco|art.deco|geometric|sunburst|fan/.test(t))) motif = 'deco';
+ else if (tags.some(t => /damask|victorian|brocade|chinoiserie|toile/.test(t))) motif = 'damask';
+ else if (tags.some(t => /op.art|memphis|pop|psychedelic|mod/.test(t))) motif = 'pop';
+ else if (tags.some(t => /metallic|foil|chrome|mylar|silver|gold|gild/.test(t))) motif = 'metallic';
+ else if (tags.some(t => /natural|grasscloth|cork|jute|raffia|woven|texture/.test(t))) motif = 'woven';
+ else if (tags.some(t => /aged|distressed|patina|crackle/.test(t))) motif = 'patina';
+
+ const fg = pal[1] || pal[0];
+ const fg2 = pal[2] || fg;
+ const accent = pal[3] || fg2;
+
+ let pattern = '';
+ if (motif === 'botanical') {
+ // Stylized leaf cluster
+ const cx = tileSize / 2, cy = tileSize / 2;
+ pattern = `
+ <ellipse cx="${cx}" cy="${cy - tileSize*0.18}" rx="${tileSize*0.14}" ry="${tileSize*0.32}" fill="${fg}" opacity="0.85" transform="rotate(-15 ${cx} ${cy})"/>
+ <ellipse cx="${cx}" cy="${cy - tileSize*0.18}" rx="${tileSize*0.14}" ry="${tileSize*0.32}" fill="${fg2}" opacity="0.7" transform="rotate(20 ${cx} ${cy})"/>
+ <circle cx="${cx}" cy="${cy + tileSize*0.18}" r="${tileSize*0.06}" fill="${accent}"/>
+ <path d="M ${cx} ${cy + tileSize*0.05} Q ${cx - tileSize*0.18} ${cy - tileSize*0.08} ${cx - tileSize*0.32} ${cy + tileSize*0.18}" stroke="${fg}" stroke-width="2" fill="none"/>
+ <path d="M ${cx} ${cy + tileSize*0.05} Q ${cx + tileSize*0.18} ${cy - tileSize*0.08} ${cx + tileSize*0.32} ${cy + tileSize*0.18}" stroke="${fg}" stroke-width="2" fill="none"/>
+ `;
+ } else if (motif === 'deco') {
+ // Sunburst / fan
+ const cx = tileSize / 2, cy = tileSize;
+ let rays = '';
+ for (let r = 0; r < 9; r++) {
+ const a = (r / 8) * Math.PI - Math.PI;
+ const x = cx + Math.cos(a) * tileSize * 0.7;
+ const y = cy + Math.sin(a) * tileSize * 0.7;
+ rays += `<line x1="${cx}" y1="${cy}" x2="${x.toFixed(1)}" y2="${y.toFixed(1)}" stroke="${r % 2 ? fg : accent}" stroke-width="2"/>`;
+ }
+ pattern = rays + `<circle cx="${cx}" cy="${cy}" r="${tileSize*0.12}" fill="${fg2}"/>`;
+ } else if (motif === 'damask') {
+ // Center floret + corner mirror
+ const cx = tileSize / 2, cy = tileSize / 2;
+ pattern = `
+ <path d="M ${cx} ${cy - tileSize*0.32} Q ${cx + tileSize*0.18} ${cy - tileSize*0.10} ${cx} ${cy} Q ${cx - tileSize*0.18} ${cy - tileSize*0.10} ${cx} ${cy - tileSize*0.32} Z" fill="${fg}"/>
+ <path d="M ${cx} ${cy + tileSize*0.32} Q ${cx + tileSize*0.18} ${cy + tileSize*0.10} ${cx} ${cy} Q ${cx - tileSize*0.18} ${cy + tileSize*0.10} ${cx} ${cy + tileSize*0.32} Z" fill="${fg2}"/>
+ <circle cx="${cx}" cy="${cy}" r="${tileSize*0.05}" fill="${accent}"/>
+ `;
+ } else if (motif === 'pop') {
+ // Circles + dots
+ let dots = '';
+ for (let i = 0; i < 5; i++) {
+ const cx = rand() * tileSize, cy = rand() * tileSize;
+ const r = (tileSize * 0.06) + rand() * tileSize * 0.10;
+ const c = [pal[0], fg, fg2, accent][Math.floor(rand() * 4)];
+ dots += `<circle cx="${cx.toFixed(0)}" cy="${cy.toFixed(0)}" r="${r.toFixed(1)}" fill="${c}"/>`;
+ }
+ pattern = dots;
+ } else if (motif === 'metallic') {
+ // Diagonal lines + highlight
+ let lines = '';
+ for (let i = -2; i < 8; i++) {
+ const x = i * tileSize / 6;
+ lines += `<line x1="${x}" y1="0" x2="${x + tileSize}" y2="${tileSize}" stroke="${i % 2 ? fg : fg2}" stroke-width="${1.5 + (i % 3)}" opacity="0.7"/>`;
+ }
+ pattern = lines;
+ } else if (motif === 'woven') {
+ // Crosshatch weave
+ let weave = '';
+ const step = tileSize / 8;
+ for (let i = 0; i < 8; i++) {
+ const x = i * step;
+ weave += `<rect x="${x}" y="0" width="${step*0.5}" height="${tileSize}" fill="${i % 2 ? fg : fg2}" opacity="0.8"/>`;
+ weave += `<rect x="0" y="${x}" width="${tileSize}" height="${step*0.5}" fill="${i % 2 ? accent : fg2}" opacity="0.4"/>`;
+ }
+ pattern = weave;
+ } else if (motif === 'patina') {
+ // Distressed splotches
+ let splotches = '';
+ for (let i = 0; i < 12; i++) {
+ const cx = rand() * tileSize, cy = rand() * tileSize;
+ const rx = (tileSize * 0.04) + rand() * tileSize * 0.08;
+ const ry = rx * (0.5 + rand());
+ const c = [fg, fg2, accent][Math.floor(rand() * 3)];
+ splotches += `<ellipse cx="${cx.toFixed(0)}" cy="${cy.toFixed(0)}" rx="${rx.toFixed(1)}" ry="${ry.toFixed(1)}" fill="${c}" opacity="${(0.3 + rand() * 0.5).toFixed(2)}"/>`;
+ }
+ pattern = splotches;
+ } else {
+ // geometric default — diamond grid
+ const half = tileSize / 2;
+ pattern = `
+ <polygon points="${half},0 ${tileSize},${half} ${half},${tileSize} 0,${half}" fill="${fg}" opacity="0.85"/>
+ <polygon points="${half},${tileSize*0.18} ${tileSize*0.82},${half} ${half},${tileSize*0.82} ${tileSize*0.18},${half}" fill="${fg2}"/>
+ <circle cx="${half}" cy="${half}" r="${tileSize*0.06}" fill="${accent}"/>
+ `;
+ }
+
+ // 2x2 tiled preview to show repeat
+ const W = tileSize * 2, H = tileSize * 2;
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="100%" height="auto" preserveAspectRatio="xMidYMid slice">
+ <defs>
+ <pattern id="p${seed}" x="0" y="0" width="${tileSize}" height="${tileSize}" patternUnits="userSpaceOnUse">
+ <rect width="${tileSize}" height="${tileSize}" fill="${pal[0]}"/>
+ ${pattern}
+ </pattern>
+ </defs>
+ <rect width="${W}" height="${H}" fill="url(#p${seed})"/>
+ </svg>`;
+}
+
async function runJob(id) {
const job = loadJob(id);
if (!job) return;
@@ -88,11 +207,18 @@ async function runJob(id) {
job.started_at = new Date().toISOString();
saveJob(id, job);
+ const jobDir = path.join(JOBS_DIR, id);
+ if (!fs.existsSync(jobDir)) fs.mkdirSync(jobDir, { recursive: true });
+
const designs = [];
for (let i = 0; i < job.count; i++) {
try {
const d = await generateDesign(job.brief);
- designs.push({ idx: i, ok: true, design: d, generated_at: new Date().toISOString() });
+ const seed = `${id}-${i}`;
+ const svg = patternSvg(d, seed);
+ const svgPath = path.join(jobDir, `${i}.svg`);
+ fs.writeFileSync(svgPath, svg);
+ designs.push({ idx: i, ok: true, design: d, preview_url: `/jobs/${id}/${i}.svg`, generated_at: new Date().toISOString() });
} catch (e) {
designs.push({ idx: i, ok: false, error: e.message, generated_at: new Date().toISOString() });
}
@@ -105,6 +231,9 @@ async function runJob(id) {
saveJob(id, job);
}
+// Serve generated SVG previews
+app.use('/jobs', express.static(JOBS_DIR));
+
app.post('/api/jobs', async (req, res) => {
const brief = String(req.body.brief || '').trim().slice(0, 1000);
const count = Math.max(1, Math.min(20, parseInt(req.body.count) || 5));
← 312628c initial: dw-design-bridge — AI factory × DW design generator
·
back to Dw Design Bridge
·
fix: progressive-truncation JSON parser handles qwen3 traili f660c5c →