318 lines
19 KiB
HTML
318 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<script src="./support.js"></script>
|
|
</head>
|
|
<body>
|
|
<x-dc>
|
|
<helmet>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@500;700;800;900&display=swap" rel="stylesheet">
|
|
<style>
|
|
@keyframes bubbleRise { 0% { transform: translateY(0) scale(1); opacity: .55; } 100% { transform: translateY(-120vh) scale(1.3); opacity: 0; } }
|
|
@keyframes dolphinFloat { 0%, 100% { transform: translateY(0) rotate(-2deg); } 50% { transform: translateY(-12px) rotate(2deg); } }
|
|
body { margin: 0; font-family: 'Nunito', system-ui, sans-serif; }
|
|
a { color: oklch(80% 0.14 302); }
|
|
a:hover { color: oklch(92% 0.10 302); }
|
|
</style>
|
|
</helmet>
|
|
|
|
<div style="position:relative;width:100%;min-height:100vh;box-sizing:border-box;overflow:hidden;background:linear-gradient(180deg, oklch(56% 0.14 305) 0%, oklch(38% 0.12 300) 45%, oklch(21% 0.08 298) 100%);">
|
|
|
|
<div style="position:absolute;left:4%;bottom:-40px;width:14px;height:14px;border-radius:50%;background:oklch(94% 0.03 302 / .5);animation:bubbleRise 9s linear infinite;"></div>
|
|
<div style="position:absolute;left:11%;bottom:-40px;width:22px;height:22px;border-radius:50%;background:oklch(94% 0.03 302 / .35);animation:bubbleRise 12s linear infinite;animation-delay:2s;"></div>
|
|
<div style="position:absolute;left:19%;bottom:-40px;width:10px;height:10px;border-radius:50%;background:oklch(94% 0.03 302 / .5);animation:bubbleRise 7s linear infinite;animation-delay:1s;"></div>
|
|
<div style="position:absolute;left:27%;bottom:-40px;width:18px;height:18px;border-radius:50%;background:oklch(94% 0.03 302 / .42);animation:bubbleRise 10.5s linear infinite;animation-delay:4s;"></div>
|
|
<div style="position:absolute;left:34%;bottom:-40px;width:8px;height:8px;border-radius:50%;background:oklch(94% 0.03 302 / .5);animation:bubbleRise 6.8s linear infinite;animation-delay:3s;"></div>
|
|
<div style="position:absolute;left:42%;bottom:-40px;width:24px;height:24px;border-radius:50%;background:oklch(94% 0.03 302 / .3);animation:bubbleRise 13.5s linear infinite;animation-delay:5.5s;"></div>
|
|
<div style="position:absolute;left:49%;bottom:-40px;width:12px;height:12px;border-radius:50%;background:oklch(94% 0.03 302 / .48);animation:bubbleRise 8.4s linear infinite;animation-delay:1.6s;"></div>
|
|
<div style="position:absolute;left:57%;bottom:-40px;width:16px;height:16px;border-radius:50%;background:oklch(94% 0.03 302 / .4);animation:bubbleRise 11.2s linear infinite;animation-delay:6.2s;"></div>
|
|
<div style="position:absolute;left:64%;bottom:-40px;width:9px;height:9px;border-radius:50%;background:oklch(94% 0.03 302 / .5);animation:bubbleRise 7.4s linear infinite;animation-delay:2.4s;"></div>
|
|
<div style="position:absolute;left:72%;bottom:-40px;width:20px;height:20px;border-radius:50%;background:oklch(94% 0.03 302 / .32);animation:bubbleRise 14s linear infinite;animation-delay:3.8s;"></div>
|
|
<div style="position:absolute;left:80%;bottom:-40px;width:11px;height:11px;border-radius:50%;background:oklch(94% 0.03 302 / .48);animation:bubbleRise 9.2s linear infinite;animation-delay:5s;"></div>
|
|
<div style="position:absolute;left:88%;bottom:-40px;width:26px;height:26px;border-radius:50%;background:oklch(94% 0.03 302 / .28);animation:bubbleRise 15s linear infinite;animation-delay:1.2s;"></div>
|
|
<div style="position:absolute;left:95%;bottom:-40px;width:13px;height:13px;border-radius:50%;background:oklch(94% 0.03 302 / .45);animation:bubbleRise 10.2s linear infinite;animation-delay:7s;"></div>
|
|
|
|
<div style="position:relative;z-index:1;padding-top:10px;">
|
|
<dc-import name="AppHeader" title="Mein Zimmer" mascot="dolphin-remote.png" link-href="./Dolphin%20Beats.dc.html" link-label="♪ Musik" hint-size="100%,116px"></dc-import>
|
|
</div>
|
|
|
|
<div style="position:relative;z-index:1;max-width:1180px;margin:0 auto;padding:0 32px 64px;box-sizing:border-box;">
|
|
|
|
<div style="margin-top:18px;">
|
|
<div style="font-size:19px;font-weight:900;color:oklch(97% 0.01 302);text-align:center;margin-bottom:14px;">Szenen</div>
|
|
<div style="display:flex;flex-wrap:wrap;justify-content:center;gap:12px;">
|
|
<sc-for list="{{ scenes }}" as="scene" hint-placeholder-count="5">
|
|
<button onClick="{{ activateScene }}" data-id="{{ scene.id }}" style="{{ scene.style }}">
|
|
<span style="font-size:22px;">{{ scene.emoji }}</span>
|
|
<span>{{ scene.name }}</span>
|
|
</button>
|
|
</sc-for>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(300px, 1fr));gap:20px;margin-top:30px;">
|
|
|
|
<sc-for list="{{ lights }}" as="light" hint-placeholder-count="4">
|
|
<div style="{{ light.cardStyle }}">
|
|
<div style="display:flex;align-items:center;gap:14px;">
|
|
<div style="{{ light.iconWrapStyle }}">
|
|
<sc-if value="{{ light.isBeyond }}" hint-placeholder-val="{{ true }}">
|
|
<svg viewBox="0 0 48 48" width="34" height="34" aria-hidden="true">
|
|
<circle cx="24" cy="24" r="17" fill="none" stroke="currentColor" stroke-width="3"></circle>
|
|
<circle cx="24" cy="24" r="9" fill="currentColor" opacity=".85"></circle>
|
|
<circle cx="24" cy="24" r="22" fill="none" stroke="currentColor" stroke-width="1.5" opacity=".4"></circle>
|
|
</svg>
|
|
</sc-if>
|
|
<sc-if value="{{ light.isStrip }}" hint-placeholder-val="{{ false }}">
|
|
<svg viewBox="0 0 48 48" width="34" height="34" aria-hidden="true">
|
|
<rect x="4" y="19" width="40" height="10" rx="5" fill="none" stroke="currentColor" stroke-width="3"></rect>
|
|
<circle cx="13" cy="24" r="2.4" fill="currentColor"></circle>
|
|
<circle cx="24" cy="24" r="2.4" fill="currentColor"></circle>
|
|
<circle cx="35" cy="24" r="2.4" fill="currentColor"></circle>
|
|
</svg>
|
|
</sc-if>
|
|
<sc-if value="{{ light.isCeiling }}" hint-placeholder-val="{{ false }}">
|
|
<svg viewBox="0 0 48 48" width="34" height="34" aria-hidden="true">
|
|
<path d="M24 4v7" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path>
|
|
<path d="M9 30 L24 11 L39 30 Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"></path>
|
|
<path d="M16 37h16" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity=".55"></path>
|
|
<path d="M20 43h8" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity=".3"></path>
|
|
</svg>
|
|
</sc-if>
|
|
</div>
|
|
<div style="flex:1;min-width:0;font-size:19px;font-weight:900;color:oklch(20% 0.03 300);">{{ light.name }}</div>
|
|
<button onClick="{{ toggleLight }}" data-id="{{ light.id }}" style="{{ light.switchStyle }}">
|
|
<span style="{{ light.knobStyle }}"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div style="margin-top:16px;">
|
|
<div style="display:flex;gap:6px;">
|
|
<sc-for list="{{ light.levels }}" as="lvl" hint-placeholder-count="5">
|
|
<button onClick="{{ setLevel }}" data-id="{{ lvl.lightId }}" data-level="{{ lvl.value }}" style="{{ lvl.style }}"></button>
|
|
</sc-for>
|
|
</div>
|
|
</div>
|
|
|
|
<sc-if value="{{ light.hasColors }}" hint-placeholder-val="{{ true }}">
|
|
<div style="margin-top:14px;">
|
|
<div style="display:flex;flex-wrap:wrap;gap:10px;">
|
|
<sc-for list="{{ light.swatches }}" as="sw" hint-placeholder-count="5">
|
|
<button onClick="{{ setColor }}" data-id="{{ sw.lightId }}" data-color="{{ sw.value }}" title="{{ sw.name }}" style="{{ sw.style }}"></button>
|
|
</sc-for>
|
|
</div>
|
|
</div>
|
|
</sc-if>
|
|
</div>
|
|
</sc-for>
|
|
|
|
<div style="background:oklch(96% 0.012 300 / .55);backdrop-filter:blur(6px);border-radius:22px;padding:18px 20px 20px;box-shadow:0 8px 24px oklch(15% 0.05 300 / .35);">
|
|
<div style="display:flex;align-items:center;gap:14px;">
|
|
<div style="width:56px;height:56px;border-radius:16px;flex:none;display:flex;align-items:center;justify-content:center;background:oklch(88% 0.03 300);color:oklch(35% 0.05 300);">
|
|
<svg viewBox="0 0 48 48" width="34" height="34" aria-hidden="true">
|
|
<rect x="7" y="7" width="34" height="34" rx="4" fill="none" stroke="currentColor" stroke-width="3"></rect>
|
|
<path d="M7 16h34M7 24h34M7 32h34" stroke="currentColor" stroke-width="2.5"></path>
|
|
</svg>
|
|
</div>
|
|
<div style="flex:1;min-width:0;">
|
|
<div style="font-size:19px;font-weight:900;color:oklch(20% 0.03 300);">Rollo</div>
|
|
<div style="font-size:13px;font-weight:700;color:oklch(45% 0.03 300 / .8);">{{ shutterLabel }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:16px;display:flex;gap:18px;align-items:stretch;">
|
|
<div style="width:104px;flex:none;border-radius:12px;overflow:hidden;position:relative;background:linear-gradient(180deg, oklch(82% 0.08 220), oklch(70% 0.09 200));border:3px solid oklch(35% 0.04 300);">
|
|
<div style="position:absolute;left:0;right:0;top:0;{{ shutterFillStyle }}"></div>
|
|
</div>
|
|
|
|
<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:10px;">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
|
|
<sc-for list="{{ shutterPresets }}" as="p" hint-placeholder-count="4">
|
|
<button onClick="{{ setShutter }}" data-value="{{ p.value }}" style="{{ p.style }}">{{ p.name }}</button>
|
|
</sc-for>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;">
|
|
<button onClick="{{ shutterUp }}" style="{{ upStyle }}">▲</button>
|
|
<button onClick="{{ shutterStop }}" style="{{ stopStyle }}">■</button>
|
|
<button onClick="{{ shutterDown }}" style="{{ downStyle }}">▼</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</x-dc>
|
|
<script type="text/x-dc" data-dc-script data-props="{"$preview": {"width": 1280, "height": 900}}">
|
|
const SWATCHES = [
|
|
{ name: "Warmweiß", value: "oklch(90% 0.06 85)" },
|
|
{ name: "Sonnengelb", value: "oklch(85% 0.16 95)" },
|
|
{ name: "Korallenrot", value: "oklch(65% 0.20 25)" },
|
|
{ name: "Delfinblau", value: "oklch(70% 0.15 235)" },
|
|
{ name: "Riffgrün", value: "oklch(75% 0.16 155)" },
|
|
{ name: "Quallenlila", value: "oklch(65% 0.18 310)" },
|
|
];
|
|
|
|
const SCENES = [
|
|
{ id: "lesen", name: "Lesen", emoji: "📖", lights: [{ on: true, level: 4, color: "oklch(90% 0.06 85)" }, { on: true, level: 2, color: "oklch(90% 0.06 85)" }, { on: false, level: 2, color: "oklch(70% 0.15 235)" }], ceiling: { on: true, level: 3 }, shutter: 0 },
|
|
{ id: "spielen", name: "Spielen", emoji: "🐬", lights: [{ on: true, level: 5, color: "oklch(75% 0.16 155)" }, { on: true, level: 5, color: "oklch(70% 0.15 235)" }, { on: true, level: 4, color: "oklch(65% 0.18 310)" }], ceiling: { on: true, level: 5 }, shutter: 0 },
|
|
{ id: "party", name: "Disco", emoji: "🪩", lights: [{ on: true, level: 5, color: "oklch(65% 0.18 310)" }, { on: true, level: 5, color: "oklch(65% 0.20 25)" }, { on: true, level: 5, color: "oklch(70% 0.15 235)" }], ceiling: { on: false, level: 2 }, shutter: 85 },
|
|
{ id: "nacht", name: "Gute Nacht", emoji: "🌙", lights: [{ on: true, level: 1, color: "oklch(70% 0.15 235)" }, { on: false, level: 1, color: "oklch(70% 0.15 235)" }, { on: true, level: 1, color: "oklch(65% 0.18 310)" }], ceiling: { on: false, level: 1 }, shutter: 100 },
|
|
{ id: "aufwachen", name: "Aufwachen", emoji: "🌞", lights: [{ on: true, level: 3, color: "oklch(85% 0.16 95)" }, { on: true, level: 3, color: "oklch(85% 0.16 95)" }, { on: false, level: 2, color: "oklch(85% 0.16 95)" }], ceiling: { on: true, level: 2 }, shutter: 0 },
|
|
];
|
|
|
|
class Component extends DCLogic {
|
|
state = {
|
|
lights: [
|
|
{ id: "beyond1", kind: "beyond", name: "Hue Beyond links", on: true, level: 4, color: "oklch(70% 0.15 235)" },
|
|
{ id: "beyond2", kind: "beyond", name: "Hue Beyond rechts", on: false, level: 3, color: "oklch(75% 0.16 155)" },
|
|
{ id: "strip", kind: "strip", name: "Lichtband Bett", on: true, level: 3, color: "oklch(65% 0.18 310)" },
|
|
{ id: "ceiling", kind: "ceiling", name: "Deckenlampe", on: true, level: 5, color: null },
|
|
],
|
|
shutter: 0,
|
|
moving: null,
|
|
scene: null,
|
|
};
|
|
|
|
componentWillUnmount() {
|
|
if (this._mover) clearInterval(this._mover);
|
|
}
|
|
|
|
patch(id, changes) {
|
|
this.setState((s) => ({
|
|
scene: null,
|
|
lights: s.lights.map((l) => (l.id === id ? Object.assign({}, l, changes) : l)),
|
|
}));
|
|
}
|
|
|
|
move(dir) {
|
|
if (this._mover) clearInterval(this._mover);
|
|
this.setState({ moving: dir, scene: null });
|
|
this._mover = setInterval(() => {
|
|
this.setState((s) => {
|
|
const next = s.shutter + dir * 4;
|
|
if (next <= 0) { clearInterval(this._mover); this._mover = null; return { shutter: 0, moving: null }; }
|
|
if (next >= 100) { clearInterval(this._mover); this._mover = null; return { shutter: 100, moving: null }; }
|
|
return { shutter: next };
|
|
});
|
|
}, 90);
|
|
}
|
|
|
|
stop() {
|
|
if (this._mover) clearInterval(this._mover);
|
|
this._mover = null;
|
|
this.setState({ moving: null });
|
|
}
|
|
|
|
glow(color, level) {
|
|
return `0 0 0 4px ${color.replace(")", " / .28)")}, 0 10px 26px ${color.replace(")", ` / ${0.18 + level * 0.08})`)}`;
|
|
}
|
|
|
|
renderVals() {
|
|
const shutter = Math.round(this.state.shutter);
|
|
|
|
const lights = this.state.lights.map((l) => {
|
|
const tint = l.color || "oklch(92% 0.05 90)";
|
|
const active = l.on;
|
|
return {
|
|
id: l.id,
|
|
name: l.name,
|
|
isBeyond: l.kind === "beyond",
|
|
isStrip: l.kind === "strip",
|
|
isCeiling: l.kind === "ceiling",
|
|
hasColors: l.kind !== "ceiling",
|
|
cardStyle: `background:oklch(96% 0.012 300 / .55);backdrop-filter:blur(6px);border-radius:22px;padding:18px 20px 20px;box-shadow:0 8px 24px oklch(15% 0.05 300 / .35)${active ? `, 0 0 0 3px ${tint.replace(")", " / .55)")}` : ""};`,
|
|
iconWrapStyle: `width:56px;height:56px;border-radius:16px;flex:none;display:flex;align-items:center;justify-content:center;transition:background .15s ease;background:${active ? tint : "oklch(88% 0.02 300)"};color:${active ? "oklch(28% 0.05 300)" : "oklch(55% 0.02 300)"};${active ? `box-shadow:0 0 22px ${tint.replace(")", " / .7)")};` : ""}`,
|
|
switchStyle: `width:74px;height:42px;flex:none;border:none;cursor:pointer;border-radius:999px;padding:4px;display:flex;justify-content:${active ? "flex-end" : "flex-start"};background:${active ? "oklch(62% 0.15 302)" : "oklch(84% 0.01 300)"};transition:background .15s ease;`,
|
|
knobStyle: "width:34px;height:34px;border-radius:999px;background:#fff;box-shadow:0 2px 6px oklch(20% 0.03 300 / .4);display:block;",
|
|
levels: [1, 2, 3, 4, 5].map((v) => ({
|
|
lightId: l.id,
|
|
value: v,
|
|
style: `flex:1;height:46px;min-width:0;border:none;cursor:pointer;border-radius:12px;background:${active && l.level >= v ? tint : "oklch(88% 0.02 300)"};opacity:${active ? 1 : 0.55};`,
|
|
})),
|
|
swatches: SWATCHES.map((sw) => ({
|
|
lightId: l.id,
|
|
value: sw.value,
|
|
name: sw.name,
|
|
style: `width:46px;height:46px;border-radius:999px;cursor:pointer;background:${sw.value};border:${l.color === sw.value ? "4px solid oklch(28% 0.05 300)" : "4px solid oklch(96% 0.012 300 / .8)"};box-shadow:0 3px 8px oklch(20% 0.03 300 / .35);`,
|
|
})),
|
|
};
|
|
});
|
|
|
|
const presetBtn = (active) =>
|
|
`border:none;cursor:pointer;font-family:inherit;font-size:14px;font-weight:800;padding:0 10px;height:48px;border-radius:14px;background:${active ? "oklch(62% 0.15 302)" : "oklch(89% 0.02 300)"};color:${active ? "#fff" : "oklch(28% 0.04 300)"};`;
|
|
|
|
const label =
|
|
shutter >= 99 ? "Ganz zu" :
|
|
shutter >= 80 ? "Fast zu" :
|
|
shutter >= 40 ? "Halb zu" :
|
|
shutter <= 1 ? "Offen" : `${shutter}% zu`;
|
|
|
|
return {
|
|
lights,
|
|
toggleLight: (e) => {
|
|
const id = e.currentTarget.dataset.id;
|
|
const l = this.state.lights.find((x) => x.id === id);
|
|
this.patch(id, { on: !l.on });
|
|
},
|
|
setLevel: (e) => {
|
|
const d = e.currentTarget.dataset;
|
|
this.patch(d.id, { level: Number(d.level), on: true });
|
|
},
|
|
setColor: (e) => {
|
|
const d = e.currentTarget.dataset;
|
|
this.patch(d.id, { color: d.color, on: true });
|
|
},
|
|
shutterLabel: this.state.moving ? (this.state.moving > 0 ? "Fährt runter …" : "Fährt hoch …") : label,
|
|
shutterFillStyle: `height:${shutter}%;transition:height .2s linear;background:repeating-linear-gradient(180deg, oklch(70% 0.03 300) 0px, oklch(70% 0.03 300) 9px, oklch(58% 0.03 300) 9px, oklch(58% 0.03 300) 12px);box-shadow:0 4px 10px oklch(20% 0.03 300 / .4);`,
|
|
shutterPresets: [
|
|
{ name: "Offen", value: 0, style: presetBtn(shutter <= 1) },
|
|
{ name: "Halb zu", value: 50, style: presetBtn(shutter > 1 && shutter < 80) },
|
|
{ name: "Fast zu", value: 85, style: presetBtn(shutter >= 80 && shutter < 99) },
|
|
{ name: "Ganz zu", value: 100, style: presetBtn(shutter >= 99) },
|
|
],
|
|
setShutter: (e) => {
|
|
this.stop();
|
|
this.setState({ shutter: Number(e.currentTarget.dataset.value), scene: null });
|
|
},
|
|
shutterUp: () => this.move(-1),
|
|
shutterDown: () => this.move(1),
|
|
shutterStop: () => this.stop(),
|
|
upStyle: `border:none;cursor:pointer;font-family:inherit;font-size:20px;font-weight:900;height:52px;border-radius:14px;background:${this.state.moving === -1 ? "oklch(62% 0.15 302)" : "oklch(89% 0.02 300)"};color:${this.state.moving === -1 ? "#fff" : "oklch(28% 0.04 300)"};`,
|
|
downStyle: `border:none;cursor:pointer;font-family:inherit;font-size:20px;font-weight:900;height:52px;border-radius:14px;background:${this.state.moving === 1 ? "oklch(62% 0.15 302)" : "oklch(89% 0.02 300)"};color:${this.state.moving === 1 ? "#fff" : "oklch(28% 0.04 300)"};`,
|
|
stopStyle: "border:none;cursor:pointer;font-family:inherit;font-size:18px;font-weight:900;height:52px;border-radius:14px;background:oklch(65% 0.18 25);color:#fff;",
|
|
scenes: SCENES.map((sc) => ({
|
|
id: sc.id,
|
|
name: sc.name,
|
|
emoji: sc.emoji,
|
|
style: `border:none;cursor:pointer;font-family:inherit;font-size:15px;font-weight:800;padding:12px 20px;border-radius:999px;display:flex;align-items:center;gap:10px;min-height:52px;background:${
|
|
this.state.scene === sc.id ? "oklch(97% 0.01 302)" : "oklch(97% 0.01 302 / .18)"
|
|
};color:${this.state.scene === sc.id ? "oklch(26% 0.05 300)" : "oklch(97% 0.01 302)"};box-shadow:${
|
|
this.state.scene === sc.id ? "0 6px 18px oklch(15% 0.05 300 / .4)" : "none"
|
|
};`,
|
|
})),
|
|
activateScene: (e) => {
|
|
const sc = SCENES.find((x) => x.id === e.currentTarget.dataset.id);
|
|
if (!sc) return;
|
|
this.stop();
|
|
this.setState((s) => ({
|
|
scene: sc.id,
|
|
shutter: sc.shutter,
|
|
lights: s.lights.map((l, i) =>
|
|
l.kind === "ceiling"
|
|
? Object.assign({}, l, sc.ceiling)
|
|
: Object.assign({}, l, sc.lights[i] || {})
|
|
),
|
|
}));
|
|
},
|
|
};
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|