---
/**
* RoomArt — a room drawn flat, for a stay that has no photograph of it yet: a single, a double, a twin, a suite, a family room, a
* bunk room or a loft, in the colours of the block or tone it sits on, with the window's sky by the time of day. `seed`
* varies the drawing (where the window is, the rug, the plant, the picture on the wall) and draws the same room on
* every build; `time` sets the sky and lights the lamp at dusk and at night. With `tile` it fills its box, the block's
* ground as the wall.
*
* Hovering or focusing a link around it switches the lamp on. Nothing moves on its own. Zero JavaScript.
*/
import type { HTMLAttributes } from 'astro/types';
const kinds = ['single', 'double', 'twin', 'suite', 'family', 'bunk', 'loft'] as const;
type Kind = (typeof kinds)[number];
interface Props extends HTMLAttributes<'div'> {
/** The room. Unset, `seed` picks one. */
kind?: Kind;
/** A word the drawing is varied by: the same word, the same room. */
seed?: string;
/** The sky in the window: daylight, dusk (the lamp on) or night (the lamp on, the moon out). */
time?: 'day' | 'dusk' | 'night';
/** Fill the box: the block's ground as the wall, the floor along the bottom. */
tile?: boolean;
/** The block the tile paints (1–5, `blocks.css`); unset, the tile takes the ground it sits on. */
block?: 1 | 2 | 3 | 4 | 5;
/** The furniture's colour, any CSS colour. Unset, the block's first motif colour. */
colour?: string;
/** The bedding and the trim. Unset, the block's second motif colour. */
accent?: string;
/** What the picture shows, for a screen reader. Unset, the drawing is decoration and hidden from it. */
label?: string;
}
const { kind, seed = '', time = 'day', tile = false, block, colour, accent, label, class: className, style, ...rest } = Astro.props;
let h = 2166136261;
for (const c of `${kind ?? ''}:${seed}`) h = Math.imul(h ^ c.charCodeAt(0), 16777619);
/** A seeded number in [0, 1): the same seed, the same room. */
const r = () => ((h = Math.imul(h ^ (h >>> 15), 2246822507) + 0x6d2b79f5) >>> 0) / 4294967296;
const pick = <T,>(a: readonly T[]): T => a[Math.floor(r() * a.length)]!;
const art: Kind = kind ?? pick(kinds);
const F = 86; // the floor line, in a 160 by 120 box
const out: string[] = [];
const add = (s: string) => out.push(s);
const n = (v: number) => +v.toFixed(1);
const rect = (cls: string, x: number, y: number, w: number, hh: number, rx = 0) => add(`<rect class="${cls}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(hh)}"${rx ? ` rx="${rx}"` : ''}/>`);
const path = (cls: string, d: string) => add(`<path class="${cls}" d="${d}"/>`);
// the window, on the side the bed is not: its sky by the time of day
const right = r() < 0.5;
const wx = right ? 104 : 18;
const wide = art === 'suite' ? 44 : 36;
const sky = time === 'night' ? 'ma-room__night' : time === 'dusk' ? 'ma-room__dusk' : 'ma-room__day';
const windowAt = (x: number, w: number) => {
rect('ma-room__frame', x - 2, 14, w + 4, 42, 1.5);
rect(sky, x, 16, w, 38);
if (time === 'night') add(`<circle class="ma-room__moon" cx="${n(x + w * 0.7)}" cy="26" r="4"/>`);
else if (time === 'day') add(`<circle class="ma-room__sun" cx="${n(x + w * 0.3)}" cy="27" r="5"/>`);
else add(`<rect class="ma-room__glow" x="${n(x)}" y="40" width="${n(w)}" height="14"/>`);
path('ma-room__bars', `M${n(x + w / 2)} 16V54M${n(x)} 35H${n(x + w)}`);
rect('ma-room__sill', x - 4, 56, w + 8, 3, 1);
};
// the room's furniture: a bed of its width at x, its pillows and its cover
const bed = (x: number, w: number, top = F - 26) => {
rect('ma-room__b', x, top - 14, 4, F - top + 14);
rect('ma-room__b', x, top - 14, w * 0.18, 16, 2);
rect('ma-room__l', x + 3, top, w - 3, 10, 2);
for (let p = 0; p < (w > 40 ? 2 : 1); p++) rect('ma-room__p', x + 6 + p * (w / 2 - 4), top - 4, w / 2 - 8 > 14 ? 14 : w / 2 - 8, 6, 3);
rect('ma-room__a', x + w * 0.38, top + 2, w * 0.62, 12, 2);
rect('ma-room__s', x + 4, F - 6, 3, 6);
rect('ma-room__s', x + w - 4, F - 6, 3, 6);
};
const lampAt = (x: number) => {
rect('ma-room__s', x - 7, F - 20, 14, 20, 1.5);
rect('ma-room__d', x - 0.6, F - 34, 1.2, 14);
path('ma-room__shade', `M${n(x - 6)} ${F - 34}L${n(x - 3.5)} ${F - 42}H${n(x + 3.5)}L${n(x + 6)} ${F - 34}Z`);
add(`<circle class="ma-room__lamp" data-on="${time === 'day' ? 'off' : ''}" cx="${n(x)}" cy="${F - 36}" r="13"/>`);
};
const plant = (x: number) => {
path('ma-room__d', `M${n(x - 5)} ${F}L${n(x - 6)} ${F - 9}H${n(x + 6)}L${n(x + 5)} ${F}Z`);
add(`<ellipse class="ma-room__g" cx="${n(x - 4)}" cy="${F - 16}" rx="4" ry="8" transform="rotate(-20 ${n(x - 4)} ${F - 16})"/><ellipse class="ma-room__gl" cx="${n(x + 4)}" cy="${F - 17}" rx="4" ry="8" transform="rotate(22 ${n(x + 4)} ${F - 17})"/>`);
};
if (art === 'loft') path('ma-room__roof', `M-10 -10H170V2L${right ? 40 : 120} 30L${right ? -10 : 170} 2Z`);
windowAt(wx, wide);
if (r() < 0.7) {
const px = right ? 30 : 118;
rect('ma-room__frame', px, 20, 18, 14, 1);
rect(r() < 0.5 ? 'ma-room__a' : 'ma-room__l', px + 2, 22, 14, 10);
}
// the rug, under everything on the floor
add(`<ellipse class="ma-room__rug" cx="${right ? 56 : 104}" cy="${F + 12}" rx="${n(32 + r() * 10)}" ry="7"/>`);
if (art === 'twin') {
bed(right ? 8 : 72, 30);
lampAt(right ? 50 : 114);
bed(right ? 60 : 124, 30);
} else if (art === 'single') {
// one narrow bed against the wall, a desk and its chair where a second bed would be
bed(right ? 10 : 118, 32);
const dx = right ? 62 : 64;
rect('ma-room__b', dx, F - 26, 34, 3, 1);
rect('ma-room__s', dx + 2, F - 23, 2.5, 23);
rect('ma-room__s', dx + 29.5, F - 23, 2.5, 23);
rect('ma-room__a', dx + 10, F - 18, 12, 3, 1);
rect('ma-room__s', dx + 11, F - 15, 2, 15);
rect('ma-room__s', dx + 19, F - 15, 2, 15);
rect('ma-room__s', dx + 19, F - 30, 2, 15);
lampAt(right ? 50 : 110);
} else if (art === 'family') {
bed(right ? 8 : 102, 50);
lampAt(right ? 66 : 92);
bed(right ? 76 : 58, 26);
} else if (art === 'bunk') {
const x = right ? 16 : 84;
rect('ma-room__b', x, F - 56, 4, 56);
rect('ma-room__b', x + 52, F - 56, 4, 56);
for (const top of [F - 52, F - 22]) {
rect('ma-room__l', x + 4, top, 48, 8, 1.5);
rect('ma-room__p', x + 7, top - 4, 13, 5, 2.5);
rect('ma-room__a', x + 22, top + 1, 30, 9, 1.5);
}
path('ma-room__ladder', `M${x + 44} ${F}V${F - 52}M${x + 50} ${F}V${F - 52}M${x + 44} ${F - 10}H${x + 50}M${x + 44} ${F - 20}H${x + 50}M${x + 44} ${F - 30}H${x + 50}M${x + 44} ${F - 40}H${x + 50}`);
lampAt(right ? 84 : 66);
} else {
const w = art === 'loft' ? 66 : 60;
const x = right ? 14 : 146 - w;
bed(x, w, art === 'loft' ? F - 18 : F - 26);
lampAt(right ? x + w + 12 : x - 12);
if (art === 'suite') {
const ax = right ? 112 : 18;
rect('ma-room__b', ax, F - 22, 28, 22, 4);
rect('ma-room__a', ax + 3, F - 30, 22, 14, 3);
rect('ma-room__s', ax - 2, F - 26, 5, 18, 2);
rect('ma-room__s', ax + 25, F - 26, 5, 18, 2);
}
}
if (r() < 0.6 && art !== 'suite') plant(right ? 150 : 10);
const a11y = label ? { role: 'img' as const, 'aria-label': label } : { 'aria-hidden': 'true' as const };
const vars = [colour && `--ma-room-body:${colour}`, accent && `--ma-room-accent:${accent}`, typeof style === 'string' && style].filter(Boolean).join(';') || undefined;
---
<div class:list={['ma-room', tile && 'ma-room--tile', className]} data-ma-block={block} data-kind={art} data-time={time} style={vars} {...rest}>
<svg viewBox="0 0 160 120" class="ma-room__svg" preserveAspectRatio={tile ? 'xMidYMax slice' : undefined} focusable="false" {...a11y}>
<rect class="ma-room__wall" x="-200" y="-200" width="560" height={200 + F} />
<rect class="ma-room__floor" x="-200" y={F} width="560" height="200" />
<rect class="ma-room__skirt" x="-200" y={F - 2} width="560" height="2" />
<g set:html={out.join('')} />
</svg>
</div>
<style is:global>
@layer components {
/* the furniture's colour, and the shade, light, pale and deep tones mixed from it, so a colourway is one value */
:where(.ma-room) {
--_body: var(--ma-room-body, var(--ma-motif-1, var(--primary, currentColor)));
--_accent: var(--ma-room-accent, var(--ma-motif-2, var(--accent, currentColor)));
--_shade: color-mix(in oklab, var(--_body) 76%, #000);
--_light: color-mix(in oklab, var(--_body) 22%, #fff);
--_pale: color-mix(in oklab, var(--_body) 8%, #fff);
--_deep: color-mix(in oklab, var(--_body) 32%, #17120e);
--_wall: var(--ma-block-ground, color-mix(in oklab, var(--_body) 10%, #fff));
--_lit: oklch(0.9 0.12 85);
display: grid;
place-items: end center;
}
:where(.ma-room__svg) { width: 100%; height: 100%; overflow: visible; }
:where(.ma-room--tile) { position: relative; overflow: hidden; width: 100%; height: 100%; background: var(--_wall); }
:where(.ma-room--tile .ma-room__svg) { position: absolute; inset: 0; overflow: hidden; }
:where(.ma-room__wall) { fill: var(--_wall); }
:where(.ma-room:not(.ma-room--tile) .ma-room__wall) { fill: none; }
:where(.ma-room__floor) { fill: color-mix(in oklab, var(--_wall) 74%, var(--_deep)); }
:where(.ma-room__skirt) { fill: color-mix(in oklab, var(--_wall) 60%, var(--_deep)); }
:where(.ma-room__roof) { fill: color-mix(in oklab, var(--_wall) 86%, var(--_deep)); }
:where(.ma-room__rug) { fill: color-mix(in oklab, var(--_accent) 55%, var(--_wall)); }
:where(.ma-room__frame) { fill: var(--_pale); stroke: var(--_shade); stroke-width: 0.8; }
:where(.ma-room__sill) { fill: var(--_light); }
:where(.ma-room__bars) { fill: none; stroke: var(--_pale); stroke-width: 1.6; }
:where(.ma-room__day) { fill: oklch(0.86 0.06 230); }
:where(.ma-room__dusk) { fill: oklch(0.76 0.11 50); }
:where(.ma-room__night) { fill: oklch(0.26 0.06 265); }
:where(.ma-room__sun) { fill: oklch(0.95 0.09 95); }
:where(.ma-room__glow) { fill: oklch(0.68 0.15 30 / 0.55); }
:where(.ma-room__moon) { fill: oklch(0.96 0.03 95); }
:where(.ma-room__b) { fill: var(--_body); }
:where(.ma-room__s) { fill: var(--_shade); }
:where(.ma-room__l) { fill: var(--_light); }
:where(.ma-room__p) { fill: var(--_pale); }
:where(.ma-room__a) { fill: var(--_accent); }
:where(.ma-room__d) { fill: var(--_deep); }
:where(.ma-room__shade) { fill: var(--_light); }
:where(.ma-room__ladder) { fill: none; stroke: var(--_shade); stroke-width: 1.6; stroke-linecap: round; }
:where(.ma-room__g) { fill: color-mix(in oklab, var(--_accent) 40%, oklch(0.55 0.12 150)); }
:where(.ma-room__gl) { fill: color-mix(in oklab, var(--_accent) 30%, oklch(0.7 0.13 140)); }
/* the lamp's light: off by day, on at dusk and at night, and on when a link around the drawing is pointed at */
:where(.ma-room__lamp) { fill: var(--_lit); opacity: 0; transition: opacity var(--ma-duration-slow, 400ms) var(--ma-ease-out, ease-out); }
:where(.ma-room__lamp[data-on='']) { opacity: 0.45; }
:where(a:is(:hover, :focus-visible) .ma-room__lamp) { opacity: 0.45; }
@media (prefers-reduced-motion: reduce) {
:where(.ma-room__lamp) { transition: none; }
}
}
</style>