← back to Quadrille Showroom
Scene luxury pass 1: limewash plaster left wall (was brick), warm wide-plank oak floor, removed TV/medallion rug/sphere plants/framed art, single olive in stone planter, brass frame + gallery placard per board, picture-light graze on board wall, cognac-leather chairs
575ee371ca2f5fb1eee18cfa6600ef7215330ebf · 2026-06-27 00:28:35 -0700 · Steve
Files touched
A ART-DIRECTION.mdA WINGBOARD-SPEC.mdM public/js/showroom.js
Diff
commit 575ee371ca2f5fb1eee18cfa6600ef7215330ebf
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 27 00:28:35 2026 -0700
Scene luxury pass 1: limewash plaster left wall (was brick), warm wide-plank oak floor, removed TV/medallion rug/sphere plants/framed art, single olive in stone planter, brass frame + gallery placard per board, picture-light graze on board wall, cognac-leather chairs
---
ART-DIRECTION.md | 55 ++++++++++++
WINGBOARD-SPEC.md | 66 +++++++++++++++
public/js/showroom.js | 230 ++++++++++++++++++++++++++++++--------------------
3 files changed, 261 insertions(+), 90 deletions(-)
diff --git a/ART-DIRECTION.md b/ART-DIRECTION.md
new file mode 100644
index 0000000..3643749
--- /dev/null
+++ b/ART-DIRECTION.md
@@ -0,0 +1,55 @@
+# The Quadrille House — Art Direction (luxury gallery)
+
+Governing design system for the showroom's professional pass. DTD verdict (2026-06-27):
+**A — scene-first**, but under ONE shared system (folding in Codex's coherence dissent).
+Every overnight pass — 3D scene AND 2D HUD — conforms to this so the result reads
+coherently premium, not a polished mismatch.
+
+## North star
+A hushed, museum-grade private showroom — de Gournay / flagship-atelier energy.
+Restraint over decoration. The wallcoverings are the only loud thing; everything
+else is quiet, warm, and expensive. NOT a game level, NOT a SaaS dashboard.
+
+## Palette (CSS vars + Three material colors)
+- Ground/architecture neutrals: Plaster `#e7e1d6`, Warm Stone `#cfc7b8`, Greige `#b8afa0`
+- Floor: warm oak `#9a7a52` (satin) OR honed stone `#d8d3c8`
+- Accent metal: brushed brass `#b9933f` / `#c9a96e` (rail, hairlines, labels)
+- Ink / text: `#1a1206` on light, `#f0e6cf` on dark
+- Shadows: never pure black — warm charcoal `#1c1c22`
+
+## Materials (Three PBR — replace procedural cartoon canvases)
+- Walls: matte limewash plaster (roughness ~0.95, faint large-scale mottle, NOT brick)
+- Floor: wide-plank warm oak (subtle grain, satin, roughness ~0.4) or honed stone
+- Board frames: thin brushed-brass or dark walnut surround on each 30" sample wing
+- Rail: real brushed brass (metalness ~0.9, roughness ~0.25), slim
+- Remove: canvas brick wall, sphere "toy" plants, boxy TV, procedural medallion rug
+- Replace plants (if kept) with a single restrained olive/fig in a stone planter; or drop
+- Furniture: one low travertine/oak console + a leather bench — minimal, off to the side
+
+## Lighting
+- Soft museum wash: warm key (done), cool fill (done), brass-catching rim (done)
+- Add gentle picture-light glow grazing the board wall (warm, no hard hotspots)
+- Tone: ACES, exposure ~1.06; shadows soft (PCFSoft 2048, done)
+
+## Typography (HUD)
+- Display/serif for pattern names + wordmark: a refined serif (Cormorant/Canela feel) —
+ load a webfont; current Georgia is the placeholder
+- Labels/eyebrows: tracked uppercase sans, small, `letter-spacing: 2–2.5px`
+- Numbers/specs: tabular lining figures
+- Reduce gold saturation in chrome; use it as a hairline accent, not fills
+
+## Board presentation
+- Each 30" board: subtle frame + a small engraved-look label plate under it
+ (pattern name + colorway), brass-on-dark, like a gallery placard
+- Brass rail spans the wall; boards hang from it with a hint of a hinge bracket
+
+## Hard constraints (locked — never regress)
+- Boards 30" wide × 6 ft tall; 8 per wall; Peruse pages the rest
+- Fan-out = the focused board's L/R panels open to present ONE design
+- Peruse: 4 ft back, centered, open board, dwell 5 s, advance
+- China Seas specs 27"×27" straight match hand-screen-print (Grasscloth 36" free match),
+ shown in the spec-sheet detail card
+- Local only on :7690 (DTD verdict C earlier) — Kamatera deploy/DNS is GATED
+
+## Verify-before-done (every pass)
+`node scripts/walkthrough-record.js` → check FPS ≥ 50 + a screenshot, then commit.
diff --git a/WINGBOARD-SPEC.md b/WINGBOARD-SPEC.md
new file mode 100644
index 0000000..a6960a9
--- /dev/null
+++ b/WINGBOARD-SPEC.md
@@ -0,0 +1,66 @@
+# Wingboard Flip / Peruse — corrected interaction model (Phillip Jeffries reference)
+
+Source of truth for the board display + flip interaction. Supersedes the earlier
+"butterfly book-match" model. Grounded in Phillip Jeffries showrooms (they call them
+**"wingboards"**; showroom designers are shown **"flipping through wingboards"**, 430+
+boards) and two reference images Steve provided (a sample wing binder; a BOYADISPLAY
+wall-mounted wing rack fanned open).
+
+## The real-world fixture (what we're recreating)
+A wall-mounted **wingboard rack**: a horizontal brushed-brass rail; many large sample
+boards (each ~30" wide × 6 ft tall) hang from it, **hinged on a vertical edge**, packed
+tightly. Closed, you see only a **~4" sliver** of each board's wallcovering at its
+exposed edge — like the fore-edges of books on a shelf. You **flip/swing a wingboard
+open** on its vertical hinge (like turning a big page) to reveal its FULL wallcovering;
+flipped boards fan to the side. (Image #3 = the wall rack fanned open. Image #2 = the
+binder form: closed wings stacked left & right, the selected one open in the center.)
+
+## Showroom layout (50 at a time)
+- **50 wingboards** visible at once on the rail (Steve: "show 50 at a time"). Peruse pages
+ through all 883.
+- Boards overlap at a **~4" pitch** (0.1016 m) so each shows a 4" sliver of its design —
+ occlusion gives the sliver for free; NO image slicing needed. (Slicers exist if ever
+ wanted: TextureCrop.ts canvas-crop, img_tools.py Pillow, force-crop sharp.)
+- Slight per-board angle (fan-deck rake) so the exposed sliver faces the viewer.
+- The exposed sliver must show the actual wallcovering (the board's right/leading 4").
+
+## Flip / open interaction
+- **Closed:** 4" sliver only.
+- **Open (flip):** the selected wingboard swings open on its vertical hinge to face the
+ viewer and present its FULL design. Open it **~65°** (Steve: "open up to 65 degrees on
+ each") — i.e. the panel rotates out ~65° from the rail plane to present the design;
+ if a two-leaf board, each leaf opens 65° to show one continuous design across both.
+- **Isolate:** when one board is open, the others dim/recede so the open design is the
+ hero (Steve: "isolate the design ... to see entire design").
+- **One continuous design:** the open board shows ONE whole wallcovering (NOT mirrored).
+ If two leaves, left leaf = left half, right leaf = right half (continuous), not a
+ book-match mirror.
+
+## On load
+- **All wingboards closed except the MIDDLE one, which is open** showing its design
+ (Steve: "onload wings are all closed except for middle design opened").
+
+## Peruse (auto-flip)
+- Camera **4 ft back, centered** on the open board.
+- Open board N (flip to ~65°), present its full design isolated, **dwell 5 s**, then close
+ it and open board N+1 — an automated "flipping through wingboards." Pages window→window,
+ endless.
+- **Allow pause for more info:** Space pauses the tour on the current open board and
+ surfaces the full spec card / more info; resume continues.
+
+## Brand on the wall (Steve: "put the brand in high res on wall too")
+- A feature wall carries the house **brand wordmark in HIGH-RES** (crisp, large) — like
+ PJ's logo in their showrooms. Use a high-res canvas/texture (≥2048px) or vector-crisp
+ text; the current low-res "DESIGNER WALLCOVERINGS" sign is the placeholder to replace.
+
+## Aesthetic (per ART-DIRECTION.md)
+Restrained luxury gallery — limewash plaster, warm oak/stone, brushed-brass rail,
+soft museum lighting, engraved placards. Phillip-Jeffries-grade calm.
+
+## Verify (every pass)
+`node scripts/walkthrough-record.js` → FPS ≥ 50, specs intact (27"×27"), no console
+errors; LOOK at the screenshots; iterate on errors. Test over and over all night.
+
+## Design ideas: 4 Horsemen
+Use Figma (authenticated, Pro) + 21st Dev + Magic for the HUD + wingboard-browser design
+direction; implement in the showroom's vanilla CSS/HTML + Three.js.
diff --git a/public/js/showroom.js b/public/js/showroom.js
index 51180b6..6c9b92e 100644
--- a/public/js/showroom.js
+++ b/public/js/showroom.js
@@ -184,12 +184,12 @@ function srgb(tex) { if (tex && THREE.sRGBEncoding) tex.encoding = THREE.sRGBEnc
// MATERIALS — all shared
// ============================================================
function initMaterials() {
- MAT.floor = new THREE.MeshStandardMaterial({ color: 0x33333b, roughness: 0.35, metalness: 0.0, envMapIntensity: 0.7 }); // polished concrete
- MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xdcdcd4, roughness: 0.95, metalness: 0.0 });
- MAT.wall = new THREE.MeshStandardMaterial({ color: 0xccc7bc, roughness: 0.92, metalness: 0.0, envMapIntensity: 0.4 }); // matte gallery paint
- MAT.chrome = new THREE.MeshStandardMaterial({ color: 0xcdcdd2, roughness: 0.22, metalness: 0.95, envMapIntensity: 1.0 }); // real brushed metal
+ MAT.floor = new THREE.MeshStandardMaterial({ color: 0x9a7a52, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }); // warm satin oak
+ MAT.ceiling = new THREE.MeshStandardMaterial({ color: 0xe7e1d6, roughness: 0.96, metalness: 0.0 }); // warm plaster
+ MAT.wall = new THREE.MeshStandardMaterial({ color: 0xe2dccf, roughness: 0.95, metalness: 0.0, envMapIntensity: 0.35 }); // matte limewash plaster
+ MAT.chrome = new THREE.MeshStandardMaterial({ color: 0xc9a96e, roughness: 0.25, metalness: 0.9, envMapIntensity: 1.0 }); // brushed brass
MAT.wood = new THREE.MeshStandardMaterial({ color: 0x6b4a32, roughness: 0.5, metalness: 0.0, envMapIntensity: 0.5 }); // satin walnut
- MAT.chair = new THREE.MeshStandardMaterial({ color: 0x3a3a44, roughness: 0.7, metalness: 0.1 });
+ MAT.chair = new THREE.MeshStandardMaterial({ color: 0x4a3528, roughness: 0.65, metalness: 0.05, envMapIntensity: 0.4 }); // cognac leather
MAT.dark = new THREE.MeshStandardMaterial({ color: 0x0d0d10, roughness: 0.4, metalness: 0.3 });
MAT.frame = new THREE.MeshStandardMaterial({ color: 0x32323a, roughness: 0.55, metalness: 0.3 });
MAT.lightPanel = new THREE.MeshBasicMaterial({ color: 0xfffff5 });
@@ -199,20 +199,27 @@ function initMaterials() {
MAT.books = [0x1a5276, 0x7d3c98, 0x1e8449, 0xb9770e, 0x922b21, 0x2c3e50, 0xd4ac0d, 0xa04000]
.map(c => new THREE.MeshStandardMaterial({ color: c, roughness: 0.8, metalness: 0.0 }));
- // Brick texture
+ // Limewash plaster — large-scale soft mottle (NOT brick). Matches the other walls
+ // but with a faint hand-troweled cloudiness so the left wall reads as real plaster.
const bc = document.createElement('canvas');
- bc.width = 128; bc.height = 128;
+ bc.width = 256; bc.height = 256;
const bx = bc.getContext('2d');
- bx.fillStyle = '#6b3a2a'; bx.fillRect(0, 0, 128, 128);
- for (let row = 0; row < 4; row++) {
- for (let col = 0; col < 2; col++) {
- bx.fillStyle = `rgb(${100+Math.random()*40|0},${50+Math.random()*25|0},${35+Math.random()*20|0})`;
- bx.fillRect(col * 64 + (row % 2 ? 32 : 0) + 2, row * 32 + 2, 60, 28);
- }
+ bx.fillStyle = '#e2dccf'; bx.fillRect(0, 0, 256, 256);
+ // Soft overlapping translucent blobs = limewash cloudiness (warm + cool drift)
+ for (let i = 0; i < 40; i++) {
+ const cx = Math.random() * 256, cy = Math.random() * 256, r = 30 + Math.random() * 60;
+ const warm = Math.random() > 0.5;
+ const g = bx.createRadialGradient(cx, cy, 0, cx, cy, r);
+ const tint = warm ? '210,200,184' : '196,191,180';
+ g.addColorStop(0, `rgba(${tint},${0.05 + Math.random() * 0.06})`);
+ g.addColorStop(1, 'rgba(231,225,214,0)');
+ bx.fillStyle = g;
+ bx.beginPath(); bx.arc(cx, cy, r, 0, Math.PI * 2); bx.fill();
}
const bt = srgb(new THREE.CanvasTexture(bc));
- bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(3, 2);
- MAT.brick = new THREE.MeshStandardMaterial({ map: bt, roughness: 0.95, metalness: 0.0 });
+ bt.wrapS = bt.wrapT = THREE.RepeatWrapping; bt.repeat.set(2, 2);
+ if (renderer.capabilities) bt.anisotropy = renderer.capabilities.getMaxAnisotropy();
+ MAT.brick = new THREE.MeshStandardMaterial({ map: bt, color: 0xe2dccf, roughness: 0.96, metalness: 0.0, envMapIntensity: 0.35 });
}
// ============================================================
@@ -309,18 +316,33 @@ function generatePatternCanvas(patternType, baseHex) {
function buildRoom() {
const W = CONFIG.room.width, D = CONFIG.room.depth, H = CONFIG.room.height;
- // Floor — polished concrete with subtle grain
- const fc = document.createElement('canvas'); fc.width = 256; fc.height = 256;
+ // Floor — warm wide-plank oak (satin). Planks run the room depth with subtle grain
+ // streaks + hairline seams between boards.
+ const fc = document.createElement('canvas'); fc.width = 256; fc.height = 512;
const fx = fc.getContext('2d');
- fx.fillStyle = '#3a3a42'; fx.fillRect(0, 0, 256, 256);
- // Grain texture
- for (let i = 0; i < 4000; i++) { fx.fillStyle = `rgba(${50+Math.random()*30|0},${50+Math.random()*30|0},${55+Math.random()*30|0},0.25)`; fx.fillRect(Math.random()*256, Math.random()*256, 2, 2); }
- // Subtle tile grid
- fx.strokeStyle = 'rgba(80,80,88,0.15)'; fx.lineWidth = 1;
- for (let i = 0; i <= 4; i++) { fx.beginPath(); fx.moveTo(i*64,0); fx.lineTo(i*64,256); fx.stroke(); fx.beginPath(); fx.moveTo(0,i*64); fx.lineTo(256,i*64); fx.stroke(); }
- const ft = srgb(new THREE.CanvasTexture(fc)); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(4, 4);
+ fx.fillStyle = '#9a7a52'; fx.fillRect(0, 0, 256, 512);
+ const planks = 4, pw = 256 / planks;
+ for (let p = 0; p < planks; p++) {
+ // Per-plank base tone variation
+ const base = 138 + (Math.random() * 26 - 13);
+ fx.fillStyle = `rgb(${base|0},${(base*0.79)|0},${(base*0.54)|0})`;
+ fx.fillRect(p * pw, 0, pw, 512);
+ // Lengthwise grain streaks
+ for (let s = 0; s < 36; s++) {
+ const gx = p * pw + Math.random() * pw;
+ fx.strokeStyle = `rgba(${(base*0.7)|0},${(base*0.55)|0},${(base*0.36)|0},${0.05 + Math.random() * 0.12})`;
+ fx.lineWidth = 0.5 + Math.random();
+ fx.beginPath(); fx.moveTo(gx, 0);
+ fx.bezierCurveTo(gx + (Math.random() - 0.5) * 6, 170, gx + (Math.random() - 0.5) * 6, 340, gx + (Math.random() - 0.5) * 4, 512);
+ fx.stroke();
+ }
+ // Hairline seam at plank edge
+ fx.strokeStyle = 'rgba(60,42,24,0.5)'; fx.lineWidth = 1;
+ fx.beginPath(); fx.moveTo(p * pw, 0); fx.lineTo(p * pw, 512); fx.stroke();
+ }
+ const ft = srgb(new THREE.CanvasTexture(fc)); ft.wrapS = ft.wrapT = THREE.RepeatWrapping; ft.repeat.set(3, 3);
if (renderer.capabilities) ft.anisotropy = renderer.capabilities.getMaxAnisotropy();
- const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.34, metalness: 0.0, envMapIntensity: 0.7 }));
+ const floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(W, D), new THREE.MeshStandardMaterial({ map: ft, roughness: 0.42, metalness: 0.0, envMapIntensity: 0.5 }));
floorMesh.rotation.x = -Math.PI/2; floorMesh.receiveShadow = true; scene.add(floorMesh);
// Ceiling
@@ -457,6 +479,14 @@ function buildLighting() {
rim.position.set(0, CONFIG.room.height + 0.6, -CONFIG.room.depth / 2 - 1.2);
rim.target.position.set(0, 1.2, 0); scene.add(rim); scene.add(rim.target);
+ // PICTURE LIGHT — a soft warm wash grazing the board wall from above, like a
+ // gallery picture light. No shadow (FPS-safe); wide penumbra so no hard hotspot.
+ const pic = new THREE.SpotLight(0xfff0d8, 0.55, 8, Math.PI * 0.42, 0.85, 1.2);
+ pic.position.set(0, CONFIG.room.height - 0.15, -CONFIG.room.depth / 2 + 1.0);
+ pic.target.position.set(0, 1.2, -CONFIG.room.depth / 2);
+ pic.castShadow = false;
+ scene.add(pic); scene.add(pic.target);
+
// Light panel meshes (visual only, no light cost)
const H = CONFIG.room.height;
[[-1.5, -1.0], [1.5, -1.0], [-1.5, 1.5], [1.5, 1.5]].forEach(([x, z]) => {
@@ -489,7 +519,7 @@ function buildFurniture() {
leg.position.set(-1.0+lx, 0.37, 1.5+lz); scene.add(leg);
});
- // 4 Chairs (simplified — just seat + back, shared material)
+ // 4 Chairs (simplified — leather-tone seat + back, shared material)
[[-1.6,1.5,0],[-0.4,1.5,Math.PI],[-1.0,1.0,Math.PI/2],[-1.0,2.0,-Math.PI/2]].forEach(([cx,cz,rot]) => {
const seat = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.04, 0.4), MAT.chair);
seat.position.set(cx, 0.46, cz); seat.rotation.y = rot; scene.add(seat);
@@ -497,77 +527,60 @@ function buildFurniture() {
back.position.set(cx - Math.sin(rot)*0.19, 0.68, cz - Math.cos(rot)*0.19); back.rotation.y = rot; scene.add(back);
});
- // TV on brick wall with dynamic slideshow
- const tv = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.9), MAT.dark);
- tv.position.set(-W/2+0.06, 1.5, 0.5); scene.add(tv);
- // TV bezel
- const bezel = new THREE.Mesh(new THREE.BoxGeometry(0.01, 0.52, 0.87), new THREE.MeshLambertMaterial({ color: 0x1a1a1a }));
- bezel.position.set(-W/2+0.075, 1.5, 0.5); scene.add(bezel);
- // Screen canvas
- const tvCanvas = document.createElement('canvas'); tvCanvas.width = 480; tvCanvas.height = 270;
- const tvCtx = tvCanvas.getContext('2d');
- const tvTex = new THREE.CanvasTexture(tvCanvas);
- const scr = new THREE.Mesh(new THREE.PlaneGeometry(0.44, 0.78), new THREE.MeshBasicMaterial({ map: tvTex }));
- scr.rotation.y = Math.PI/2; scr.position.set(-W/2+0.085, 1.5, 0.5); scene.add(scr);
- // Store for slideshow updates
- window._tvScreen = { canvas: tvCanvas, ctx: tvCtx, texture: tvTex, slideIdx: 0 };
- drawTVSlide(0);
+ // (Removed: boxy TV + bezel + slideshow screen — the wallcoverings are the only
+ // loud thing in the room per the gallery art-direction.) The TV-slideshow helpers
+ // null-guard on window._tvScreen, so leaving it unset makes them no-op.
// Bookshelves — near front wall, flanking doorway (don't block wing racks)
buildBookShelf(-2.8, D/2-0.3, Math.PI, 1.2);
buildBookShelf(2.8, D/2-0.3, Math.PI, 1.2);
- // Rug under meeting table
- const rugCanvas = document.createElement('canvas'); rugCanvas.width = 256; rugCanvas.height = 128;
- const rc = rugCanvas.getContext('2d');
- rc.fillStyle = '#4a3a2a'; rc.fillRect(0, 0, 256, 128);
- // Border pattern
- rc.strokeStyle = '#c9a96e'; rc.lineWidth = 6;
- rc.strokeRect(8, 8, 240, 112);
- rc.strokeStyle = '#8a7a5a'; rc.lineWidth = 2;
- rc.strokeRect(16, 16, 224, 96);
- // Center medallion
- rc.fillStyle = '#6a5a3a'; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.fill();
- rc.strokeStyle = '#c9a96e'; rc.lineWidth = 1.5; rc.beginPath(); rc.ellipse(128, 64, 40, 25, 0, 0, Math.PI*2); rc.stroke();
- const rugTex = srgb(new THREE.CanvasTexture(rugCanvas));
- const rug = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 1.4), new THREE.MeshStandardMaterial({ map: rugTex, roughness: 0.95, metalness: 0.0 }));
- rug.rotation.x = -Math.PI/2; rug.position.set(-1.0, 0.005, 1.5); scene.add(rug);
-
- // Potted plants — 2 corners
- [[-W/2+0.4, D/2-0.4], [W/2-0.4, -D/2+0.4]].forEach(([px, pz]) => {
- // Pot
- const pot = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.08, 0.2, 16), new THREE.MeshStandardMaterial({ color: 0x8b5e3c, roughness: 0.85, metalness: 0.0 }));
- pot.position.set(px, 0.1, pz); scene.add(pot);
- // Soil
- const soil = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.09, 0.02, 16), new THREE.MeshStandardMaterial({ color: 0x3a2a1a, roughness: 1.0, metalness: 0.0 }));
- soil.position.set(px, 0.21, pz); scene.add(soil);
- // Foliage (sphere cluster)
- const foliageMat = new THREE.MeshStandardMaterial({ color: 0x2d5a1e, roughness: 0.9, metalness: 0.0 });
- const f1 = new THREE.Mesh(new THREE.SphereGeometry(0.15, 12, 9), foliageMat);
- f1.position.set(px, 0.45, pz); scene.add(f1);
- const f2 = new THREE.Mesh(new THREE.SphereGeometry(0.1, 10, 7), foliageMat);
- f2.position.set(px+0.08, 0.55, pz-0.05); scene.add(f2);
+ // (Removed: procedural medallion rug — clean warm-oak floor reads more gallery.)
+
+ // ONE restrained olive in a honed-stone planter — quiet, expensive, off to the side
+ // by the left wall. Replaces the two sphere "toy" plants.
+ buildOliveTree(-W/2 + 0.55, 0, D/2 - 0.55);
+
+ // (Removed: framed abstract "art" on the right wall — the sample boards are the art.)
+}
+
+// A single olive/fig in a square honed-stone planter — soft layered canopy (not a
+// hard sphere), slim trunk, restrained. Built from a handful of low-poly meshes.
+function buildOliveTree(px, py, pz) {
+ const g = new THREE.Group();
+ g.position.set(px, py, pz);
+
+ // Square honed-stone planter (warm stone, matte)
+ const stoneMat = new THREE.MeshStandardMaterial({ color: 0xcfc7b8, roughness: 0.85, metalness: 0.0, envMapIntensity: 0.3 });
+ const planter = new THREE.Mesh(new THREE.BoxGeometry(0.34, 0.34, 0.34), stoneMat);
+ planter.position.set(0, 0.17, 0); planter.castShadow = true; planter.receiveShadow = true; g.add(planter);
+ // Soil cap
+ const soil = new THREE.Mesh(new THREE.BoxGeometry(0.30, 0.02, 0.30), new THREE.MeshStandardMaterial({ color: 0x2e261c, roughness: 1.0, metalness: 0.0 }));
+ soil.position.set(0, 0.345, 0); g.add(soil);
+
+ // Slim trunk
+ const trunkMat = new THREE.MeshStandardMaterial({ color: 0x6e5c45, roughness: 0.9, metalness: 0.0 });
+ const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.022, 0.03, 0.5, 7), trunkMat);
+ trunk.position.set(0, 0.6, 0); trunk.castShadow = true; g.add(trunk);
+
+ // Soft olive canopy — several low-poly tufts at varied scale = a layered, organic
+ // silhouette rather than one cartoon ball. Muted grey-green (olive foliage).
+ const leafMat = new THREE.MeshStandardMaterial({ color: 0x6f7a5a, roughness: 0.92, metalness: 0.0, envMapIntensity: 0.25 });
+ const tufts = [
+ [0.00, 0.92, 0.00, 0.20],
+ [0.11, 0.99, 0.04, 0.13],
+ [-0.10, 0.97, -0.05, 0.12],
+ [0.04, 1.08, -0.06, 0.11],
+ [-0.05, 1.05, 0.08, 0.10]
+ ];
+ tufts.forEach(([tx, ty, tz, r]) => {
+ const tuft = new THREE.Mesh(new THREE.IcosahedronGeometry(r, 0), leafMat);
+ tuft.position.set(tx, ty, tz);
+ tuft.rotation.set(Math.random(), Math.random(), Math.random());
+ tuft.castShadow = true; g.add(tuft);
});
- // Framed art on right wall
- const artCanvas = document.createElement('canvas'); artCanvas.width = 256; artCanvas.height = 192;
- const ax = artCanvas.getContext('2d');
- // Generate an abstract wallcovering pattern
- ax.fillStyle = '#1a2744'; ax.fillRect(0, 0, 256, 192);
- ax.globalAlpha = 0.3; ax.fillStyle = '#c9a96e';
- for (let y = 0; y < 6; y++) for (let x = 0; x < 4; x++) {
- const cx = x*64+32+(y%2?32:0), cy = y*32+16;
- ax.beginPath(); ax.moveTo(cx, cy-12); ax.quadraticCurveTo(cx+14, cy, cx, cy+12);
- ax.quadraticCurveTo(cx-14, cy, cx, cy-12); ax.fill();
- }
- ax.globalAlpha = 1;
- // Frame border
- ax.strokeStyle = '#c9a96e'; ax.lineWidth = 4; ax.strokeRect(2, 2, 252, 188);
- const artTex = new THREE.CanvasTexture(artCanvas);
- const artFrame = new THREE.Mesh(new THREE.BoxGeometry(0.04, 0.55, 0.72), MAT.frame);
- artFrame.position.set(W/2-0.04, 1.5, -0.5); scene.add(artFrame);
- const artFace = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 0.65), new THREE.MeshBasicMaterial({ map: artTex }));
- artFace.rotation.y = -Math.PI/2; artFace.position.set(W/2-0.06, 1.5, -0.5); scene.add(artFace);
+ scene.add(g);
}
function buildBookShelf(x, z, rot, unitW) {
@@ -793,6 +806,43 @@ function buildWingWall() {
planeR.userData = { isWingFace: true, parentPivot: pivot, leaf: 'R' };
leafR.add(planeR); pivot.add(leafR);
+ // Thin brushed-brass surround behind the board (stays put when leaves fan open).
+ // Four slim bars frame the 30" board face like a gallery mount.
+ const frameT = 0.018, frameD = 0.01, frameZ = -THICK / 2 - 0.004;
+ const fpad = 0.012; // frame sits just outside the board edges
+ const fw = wingW + fpad * 2, fh = H + fpad * 2;
+ const brassBar = (w, h, x, y) => {
+ const bar = new THREE.Mesh(new THREE.BoxGeometry(w, h, frameD), MAT.chrome);
+ bar.position.set(x, yC + y, frameZ); bar.castShadow = false; bar.receiveShadow = true;
+ pivot.add(bar);
+ };
+ brassBar(fw, frameT, 0, fh / 2 - frameT / 2); // top
+ brassBar(fw, frameT, 0, -fh / 2 + frameT / 2); // bottom
+ brassBar(frameT, fh, -fw / 2 + frameT / 2, 0); // left
+ brassBar(frameT, fh, fw / 2 - frameT / 2, 0); // right
+
+ // Gallery placard — engraved-look brass-on-dark plate under the board.
+ const pName = (product.pattern_name || product.name || 'Pattern').toString();
+ const pColor = (product.color_name || product.color || '').toString();
+ const plc = document.createElement('canvas'); plc.width = 256; plc.height = 64;
+ const pcx = plc.getContext('2d');
+ pcx.fillStyle = '#1c1c22'; pcx.fillRect(0, 0, 256, 64);
+ pcx.strokeStyle = 'rgba(201,169,110,0.5)'; pcx.lineWidth = 2; pcx.strokeRect(3, 3, 250, 58);
+ pcx.fillStyle = '#c9a96e'; pcx.textAlign = 'center';
+ pcx.font = '600 22px Georgia, serif';
+ pcx.fillText(pName.length > 22 ? pName.slice(0, 21) + '…' : pName, 128, 28);
+ if (pColor) {
+ pcx.fillStyle = '#9a8c6e'; pcx.font = '12px sans-serif';
+ pcx.fillText(pColor.toUpperCase().slice(0, 30), 128, 48);
+ }
+ const plTex = new THREE.CanvasTexture(plc);
+ const placard = new THREE.Mesh(
+ new THREE.PlaneGeometry(Math.min(wingW * 0.9, 0.32), 0.075),
+ new THREE.MeshBasicMaterial({ map: plTex, transparent: true })
+ );
+ placard.position.set(0, 0.115, 0.012);
+ pivot.add(placard);
+
const wingImg = product.tile || product.image;
let widthInches = 27;
← 5921951 Boards 30"×6ft (real sample wings, 8/wall + paging); fan-out
·
back to Quadrille Showroom
·
Clean+clear pass: bright even gallery lighting, true-colour 15f8c33 →