From 3b72cd88cc99d4bb2a242ffd0e9d57995452ebdd Mon Sep 17 00:00:00 2001 From: filemon Date: Tue, 7 Apr 2026 11:54:27 -0300 Subject: [PATCH] Add room scene foundation for Blobbi room customization (Phase 1 POC) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the initial room-scene architecture for the home room: - Declarative wall types (paint, wallpaper, brick) and floor types (wood, tile, carpet) - Floor rendering with CSS 3D perspective transform for realistic depth - Optional theme-based color derivation (palette input, not full replacement) - Safe persistence in kind 11125 content under 'roomCustomization' section - Scene layer renders behind Blobbi in the center content area only Architecture: types, defaults, resolver, content helpers, render components, and a useRoomScene hook — all isolated under src/blobbi/rooms/scene/. --- .../rooms/components/BlobbiHomeRoom.tsx | 11 +- .../rooms/scene/components/FloorLayer.tsx | 193 +++++++++++++++++ .../rooms/scene/components/RoomSceneLayer.tsx | 105 ++++++++++ .../rooms/scene/components/WallLayer.tsx | 161 ++++++++++++++ src/blobbi/rooms/scene/defaults.ts | 53 +++++ src/blobbi/rooms/scene/hooks/useRoomScene.ts | 65 ++++++ src/blobbi/rooms/scene/index.ts | 33 +++ .../rooms/scene/lib/room-scene-content.ts | 197 ++++++++++++++++++ src/blobbi/rooms/scene/resolver.ts | 160 ++++++++++++++ src/blobbi/rooms/scene/types.ts | 96 +++++++++ 10 files changed, 1073 insertions(+), 1 deletion(-) create mode 100644 src/blobbi/rooms/scene/components/FloorLayer.tsx create mode 100644 src/blobbi/rooms/scene/components/RoomSceneLayer.tsx create mode 100644 src/blobbi/rooms/scene/components/WallLayer.tsx create mode 100644 src/blobbi/rooms/scene/defaults.ts create mode 100644 src/blobbi/rooms/scene/hooks/useRoomScene.ts create mode 100644 src/blobbi/rooms/scene/index.ts create mode 100644 src/blobbi/rooms/scene/lib/room-scene-content.ts create mode 100644 src/blobbi/rooms/scene/resolver.ts create mode 100644 src/blobbi/rooms/scene/types.ts diff --git a/src/blobbi/rooms/components/BlobbiHomeRoom.tsx b/src/blobbi/rooms/components/BlobbiHomeRoom.tsx index c0db3e12..15bf07fd 100644 --- a/src/blobbi/rooms/components/BlobbiHomeRoom.tsx +++ b/src/blobbi/rooms/components/BlobbiHomeRoom.tsx @@ -4,6 +4,7 @@ * BlobbiHomeRoom — The main living / play room. * * Layout: + * - Room scene background (wall + floor with perspective) * - BlobbiRoomHero (stats crown, Blobbi visual, name) * - Unified bottom bar: Photo (left) | Carousel (center) | Companion (right) * - Inline activity (music player, sing card) above the bottom bar @@ -21,6 +22,7 @@ import { ROOM_BOTTOM_BAR_CLASS } from '../lib/room-layout'; import { BlobbiRoomHero } from './BlobbiRoomHero'; import { RoomActionButton } from './RoomActionButton'; import { ItemCarousel, type CarouselEntry } from './ItemCarousel'; +import { RoomSceneLayer, useRoomScene } from '../scene'; interface BlobbiHomeRoomProps { ctx: BlobbiRoomContext; @@ -29,6 +31,7 @@ interface BlobbiHomeRoomProps { export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) { const { + profile, isActiveFloatingCompanion, setShowPhotoModal, isCurrentCompanion, @@ -52,6 +55,9 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) { actionInProgress, } = ctx; + // ── Room Scene (wall + floor behind Blobbi) ── + const roomScene = useRoomScene('home', profile?.event?.content ?? ''); + // Build carousel entries: toys + music + sing const carouselItems = useMemo(() => { const toys = getLiveShopItems() @@ -87,7 +93,10 @@ export function BlobbiHomeRoom({ ctx }: BlobbiHomeRoomProps) { }; return ( -
+
+ {/* ── Room Scene Background ── */} + + {/* ── Hero (Blobbi + stats) ── */} diff --git a/src/blobbi/rooms/scene/components/FloorLayer.tsx b/src/blobbi/rooms/scene/components/FloorLayer.tsx new file mode 100644 index 00000000..ed4d3337 --- /dev/null +++ b/src/blobbi/rooms/scene/components/FloorLayer.tsx @@ -0,0 +1,193 @@ +// src/blobbi/rooms/scene/components/FloorLayer.tsx + +/** + * FloorLayer — Renders the floor surface with visual depth. + * + * The floor receives CSS 3D perspective from its parent container + * (RoomSceneLayer). This component renders the surface pattern only. + * Different floor types produce different textures: + * + * - wood: Horizontal planks with grain lines and color variation + * - tile: Checkerboard/grid pattern + * - carpet: Solid textured surface + * + * The component fills its parent container entirely. + */ + +import { useMemo, useId } from 'react'; +import { darkenHex, lightenHex, blendHex } from '@/lib/colorUtils'; +import type { FloorConfig } from '../types'; + +interface FloorLayerProps { + config: FloorConfig; +} + +export function FloorLayer({ config }: FloorLayerProps) { + const { type, color, accentColor } = config; + + switch (type) { + case 'wood': + return ; + case 'tile': + return ; + case 'carpet': + return ; + default: + return ; + } +} + +// ─── Wood Floor ─────────────────────────────────────────────────────────────── + +function WoodFloor({ color, accentColor }: { color: string; accentColor?: string }) { + const patternId = useId(); + const grainColor = accentColor ?? darkenHex(color, 0.18); + const plankGap = darkenHex(color, 0.3); + + // Alternate plank colors for natural variation + const plankColors = useMemo(() => [ + color, + lightenHex(color, 0.05), + darkenHex(color, 0.04), + blendHex(color, grainColor, 0.15), + lightenHex(color, 0.03), + darkenHex(color, 0.07), + ], [color, grainColor]); + + return ( +
+ {/* Base fill */} +
+ + {/* SVG plank pattern for realistic wood */} + + + + {/* 6 planks, each 38px tall with 2px gap */} + {plankColors.map((pc, i) => ( + + {/* Plank body */} + + {/* Subtle grain lines within plank */} + + + {/* Plank gap line */} + + + ))} + + + + + + {/* Subtle light gradient: lighter near wall, darker in distance */} +
+
+ ); +} + +// ─── Tile Floor ─────────────────────────────────────────────────────────────── + +function TileFloor({ color, accentColor }: { color: string; accentColor?: string }) { + const groutColor = accentColor ?? darkenHex(color, 0.2); + const altTile = lightenHex(color, 0.06); + + // Checkerboard tile pattern via CSS gradients + const tilePattern = useMemo(() => { + const size = 50; // tile size in px + + return { + backgroundImage: [ + // Checkerboard: conic gradient creates four quadrants + `conic-gradient(${altTile} 0.25turn, ${color} 0.25turn 0.5turn, ${altTile} 0.5turn 0.75turn, ${color} 0.75turn)`, + ].join(', '), + backgroundSize: `${size * 2}px ${size * 2}px`, + }; + }, [color, altTile]); + + return ( +
+
+ + {/* Grout lines overlay */} +
+ + {/* Light gradient for depth */} +
+
+ ); +} + +// ─── Carpet Floor ───────────────────────────────────────────────────────────── + +function CarpetFloor({ color }: { color: string }) { + return ( +
+ {/* Carpet texture: very subtle noise */} +
+ + {/* Light gradient for depth */} +
+
+ ); +} diff --git a/src/blobbi/rooms/scene/components/RoomSceneLayer.tsx b/src/blobbi/rooms/scene/components/RoomSceneLayer.tsx new file mode 100644 index 00000000..9fbc9d19 --- /dev/null +++ b/src/blobbi/rooms/scene/components/RoomSceneLayer.tsx @@ -0,0 +1,105 @@ +// src/blobbi/rooms/scene/components/RoomSceneLayer.tsx + +/** + * RoomSceneLayer — The composite room background behind Blobbi. + * + * Renders as an absolutely-positioned layer that fills its parent entirely. + * Must be placed inside a container with `position: relative`. + * + * Visual structure (top to bottom): + * ┌──────────────────────────┐ + * │ │ Wall (~62% of height) + * │ WallLayer │ Flat, front-facing + * │ │ + * ├──────────────────────────┤ Baseboard shadow + * │ ╲ ╱ │ + * │ ╲ FloorLayer ╱ │ Floor (~38% of height) + * │ ╲ ╱ │ CSS 3D perspective transform + * └──────────────────────────┘ + * + * The floor uses CSS `perspective` + `rotateX` with `transform-origin: top center` + * to create depth. The top edge of the floor stays at the wall-floor junction + * while the surface recedes into the distance, creating a natural room feel. + * + * The baseboard is a subtle shadow gradient at the junction line. + * + * A soft vignette around the edges adds subtle depth framing. + */ + +import type { ResolvedRoomScene } from '../types'; +import { WallLayer } from './WallLayer'; +import { FloorLayer } from './FloorLayer'; + +interface RoomSceneLayerProps { + scene: ResolvedRoomScene; +} + +/** Wall occupies the top portion, floor the bottom. */ +const WALL_PERCENT = 62; +const FLOOR_PERCENT = 100 - WALL_PERCENT; // 38% + +export function RoomSceneLayer({ scene }: RoomSceneLayerProps) { + return ( +