Add theme system with 5 presets and selector UI
- 5 themes: Standard, Modern, Warm, Dark, Scandinavian - ThemeManager mutates shared COLORS, clears cache, re-renders - Updates scene background and light intensities per theme - Theme selector buttons with color swatch in sidebar - Exported COLORS from renderer.js for theme access
This commit is contained in:
@@ -52,6 +52,28 @@
|
|||||||
.room-item.active { background: #4a90d9; color: #fff; }
|
.room-item.active { background: #4a90d9; color: #fff; }
|
||||||
.room-item .area { font-size: 11px; opacity: 0.7; }
|
.room-item .area { font-size: 11px; opacity: 0.7; }
|
||||||
|
|
||||||
|
.theme-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
margin: 2px 3px 2px 0;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.theme-btn.active { border-color: #4a90d9; }
|
||||||
|
.theme-swatch {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 4px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
#info {
|
#info {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
@@ -74,6 +96,8 @@
|
|||||||
<div id="floor-buttons"></div>
|
<div id="floor-buttons"></div>
|
||||||
<h3>Rooms</h3>
|
<h3>Rooms</h3>
|
||||||
<div id="room-list"></div>
|
<div id="room-list"></div>
|
||||||
|
<h3>Theme</h3>
|
||||||
|
<div id="theme-buttons"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="info">Click a room to select it. Click furniture to edit. Scroll to zoom, drag to orbit.</div>
|
<div id="info">Click a room to select it. Click furniture to edit. Scroll to zoom, drag to orbit.</div>
|
||||||
@@ -90,6 +114,7 @@
|
|||||||
import { HouseRenderer } from './renderer.js';
|
import { HouseRenderer } from './renderer.js';
|
||||||
import { DesignState } from './state.js';
|
import { DesignState } from './state.js';
|
||||||
import { InteractionManager } from './interaction.js';
|
import { InteractionManager } from './interaction.js';
|
||||||
|
import { ThemeManager } from './themes.js';
|
||||||
|
|
||||||
const viewer = document.getElementById('viewer');
|
const viewer = document.getElementById('viewer');
|
||||||
const houseRenderer = new HouseRenderer(viewer);
|
const houseRenderer = new HouseRenderer(viewer);
|
||||||
@@ -97,6 +122,7 @@
|
|||||||
let selectedRoom = null;
|
let selectedRoom = null;
|
||||||
let designState = null;
|
let designState = null;
|
||||||
let interaction = null;
|
let interaction = null;
|
||||||
|
let themeManager = null;
|
||||||
|
|
||||||
houseRenderer.loadHouse('../data/sample-house.json').then(async (house) => {
|
houseRenderer.loadHouse('../data/sample-house.json').then(async (house) => {
|
||||||
document.getElementById('house-name').textContent = house.name;
|
document.getElementById('house-name').textContent = house.name;
|
||||||
@@ -117,8 +143,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
themeManager = new ThemeManager(houseRenderer);
|
||||||
buildFloorButtons();
|
buildFloorButtons();
|
||||||
buildRoomList();
|
buildRoomList();
|
||||||
|
buildThemeButtons();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
document.getElementById('house-name').textContent = 'Error loading data';
|
document.getElementById('house-name').textContent = 'Error loading data';
|
||||||
document.getElementById('info').textContent = err.message;
|
document.getElementById('info').textContent = err.message;
|
||||||
@@ -170,6 +198,23 @@
|
|||||||
viewer.addEventListener('roomclick', (e) => {
|
viewer.addEventListener('roomclick', (e) => {
|
||||||
selectRoom(e.detail.roomId);
|
selectRoom(e.detail.roomId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function buildThemeButtons() {
|
||||||
|
const container = document.getElementById('theme-buttons');
|
||||||
|
container.innerHTML = '';
|
||||||
|
for (const theme of themeManager.getThemes()) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'theme-btn' + (theme.id === themeManager.currentTheme ? ' active' : '');
|
||||||
|
btn.innerHTML = `<span class="theme-swatch" style="background:${theme.swatch}"></span>${theme.name}`;
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
themeManager.applyTheme(theme.id);
|
||||||
|
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
buildRoomList(); // re-render room list since floor was rebuilt
|
||||||
|
});
|
||||||
|
container.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
124
src/themes.js
Normal file
124
src/themes.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { COLORS } from './renderer.js';
|
||||||
|
|
||||||
|
const THEMES = {
|
||||||
|
default: {
|
||||||
|
name: 'Standard',
|
||||||
|
swatch: '#e8e0d4',
|
||||||
|
colors: {
|
||||||
|
wall: { exterior: 0xe8e0d4, interior: 0xf5f0eb },
|
||||||
|
floor: { tile: 0xc8beb0, hardwood: 0xb5894e },
|
||||||
|
ceiling: 0xfaf8f5,
|
||||||
|
door: 0x8b6914,
|
||||||
|
window: 0x87ceeb,
|
||||||
|
windowFrame: 0xd0d0d0,
|
||||||
|
grid: 0xcccccc,
|
||||||
|
selected: 0x4a90d9
|
||||||
|
},
|
||||||
|
scene: { background: 0xf0f0f0, ambientIntensity: 0.6, directionalIntensity: 0.8 }
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
name: 'Modern',
|
||||||
|
swatch: '#f5f5f5',
|
||||||
|
colors: {
|
||||||
|
wall: { exterior: 0xf5f5f5, interior: 0xffffff },
|
||||||
|
floor: { tile: 0xe0e0e0, hardwood: 0xc4a882 },
|
||||||
|
ceiling: 0xffffff,
|
||||||
|
door: 0x333333,
|
||||||
|
window: 0xa8d4f0,
|
||||||
|
windowFrame: 0x666666,
|
||||||
|
grid: 0xe0e0e0,
|
||||||
|
selected: 0x2196f3
|
||||||
|
},
|
||||||
|
scene: { background: 0xfafafa, ambientIntensity: 0.7, directionalIntensity: 0.6 }
|
||||||
|
},
|
||||||
|
warm: {
|
||||||
|
name: 'Warm',
|
||||||
|
swatch: '#ddd0b8',
|
||||||
|
colors: {
|
||||||
|
wall: { exterior: 0xddd0b8, interior: 0xf0e8d8 },
|
||||||
|
floor: { tile: 0xb8a890, hardwood: 0x9b6b3a },
|
||||||
|
ceiling: 0xf5efe5,
|
||||||
|
door: 0x6b4423,
|
||||||
|
window: 0x8bc4e0,
|
||||||
|
windowFrame: 0x8b7355,
|
||||||
|
grid: 0xc8b8a0,
|
||||||
|
selected: 0xd48b2c
|
||||||
|
},
|
||||||
|
scene: { background: 0xf5efe5, ambientIntensity: 0.5, directionalIntensity: 0.9 }
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
name: 'Dark',
|
||||||
|
swatch: '#3a3a3a',
|
||||||
|
colors: {
|
||||||
|
wall: { exterior: 0x3a3a3a, interior: 0x4a4a4a },
|
||||||
|
floor: { tile: 0x2a2a2a, hardwood: 0x5a4030 },
|
||||||
|
ceiling: 0x333333,
|
||||||
|
door: 0x5a4030,
|
||||||
|
window: 0x4080b0,
|
||||||
|
windowFrame: 0x555555,
|
||||||
|
grid: 0x444444,
|
||||||
|
selected: 0x64b5f6
|
||||||
|
},
|
||||||
|
scene: { background: 0x222222, ambientIntensity: 0.4, directionalIntensity: 1.0 }
|
||||||
|
},
|
||||||
|
scandinavian: {
|
||||||
|
name: 'Scandi',
|
||||||
|
swatch: '#f0ece4',
|
||||||
|
colors: {
|
||||||
|
wall: { exterior: 0xf0ece4, interior: 0xfaf6f0 },
|
||||||
|
floor: { tile: 0xe8ddd0, hardwood: 0xd4b88c },
|
||||||
|
ceiling: 0xffffff,
|
||||||
|
door: 0xc4a87a,
|
||||||
|
window: 0xc0ddf0,
|
||||||
|
windowFrame: 0xb0b0b0,
|
||||||
|
grid: 0xd8d8d8,
|
||||||
|
selected: 0x5b9bd5
|
||||||
|
},
|
||||||
|
scene: { background: 0xf8f6f2, ambientIntensity: 0.65, directionalIntensity: 0.7 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThemeManager — applies visual themes by mutating COLORS and re-rendering.
|
||||||
|
*/
|
||||||
|
export class ThemeManager {
|
||||||
|
constructor(renderer) {
|
||||||
|
this.renderer = renderer;
|
||||||
|
this.currentTheme = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme(themeId) {
|
||||||
|
const theme = THEMES[themeId];
|
||||||
|
if (!theme) return;
|
||||||
|
this.currentTheme = themeId;
|
||||||
|
|
||||||
|
// Mutate the shared COLORS object
|
||||||
|
Object.assign(COLORS.wall, theme.colors.wall);
|
||||||
|
Object.assign(COLORS.floor, theme.colors.floor);
|
||||||
|
COLORS.ceiling = theme.colors.ceiling;
|
||||||
|
COLORS.door = theme.colors.door;
|
||||||
|
COLORS.window = theme.colors.window;
|
||||||
|
COLORS.windowFrame = theme.colors.windowFrame;
|
||||||
|
COLORS.grid = theme.colors.grid;
|
||||||
|
COLORS.selected = theme.colors.selected;
|
||||||
|
|
||||||
|
// Update scene background and lights
|
||||||
|
this.renderer.scene.background.setHex(theme.scene.background);
|
||||||
|
this.renderer.scene.traverse(child => {
|
||||||
|
if (child.isAmbientLight) child.intensity = theme.scene.ambientIntensity;
|
||||||
|
if (child.isDirectionalLight) child.intensity = theme.scene.directionalIntensity;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear cached materials/geometry and re-render to pick up new colors
|
||||||
|
this.renderer._clearFloor();
|
||||||
|
this.renderer.showFloor(this.renderer.currentFloor);
|
||||||
|
}
|
||||||
|
|
||||||
|
getThemes() {
|
||||||
|
return Object.entries(THEMES).map(([id, t]) => ({
|
||||||
|
id,
|
||||||
|
name: t.name,
|
||||||
|
swatch: t.swatch
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user