# Home Art (Moonarc)

A building drawn flat, for a listing that has no photograph yet: a house, a terrace, an apartment block, a villa, a cabin, a tower, an office or a shop, in the colour of the block or tone it sits on, with a shade, a light and the windows mixed from it. A seed varies the floors, the windows, the roof and the trees and draws the same building on every build; time lights the windows at dusk and at night and darkens the sky. As a tile it fills its box with the block's ground as the sky. Hovering or focusing a link around it lights a few more windows. Nothing moves on its own. Zero JavaScript.

- Import: `import HomeArt from '@moonarc/core/HomeArt'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/home-art.json`
- Tier A · category background · trigger pointer
- Readout: `<HomeArt kind="house" seed="12 Linden Row" tile block={2} />`
- Browser support: widely (every browser); elsewhere: nothing to fall back from
- Measured cost: 0 B JS (CSS 6.2 kB raw)
- Page: https://moonarc.dev/components/home-art/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `kind` | `'house' | 'terrace' | 'apartment' | 'villa' | 'cabin' | 'tower' | 'office' | 'shop'` | none | The building. Unset, the seed picks one. |
| `seed` | `string` | `''` | A word the drawing is varied by (the floors, the windows, the roof, the trees): the same word, the same building on every build. |
| `time` | `'day' | 'dusk' | 'night'` | `'day'` | Daylight; dusk, with some windows lit and a warm sky; or night, with most lit and a dark sky. |
| `tile` | `boolean` | `false` | Fill the box: the block's ground is the sky and the drawing stands on its ground line. |
| `block` | `1 | 2 | 3 | 4 | 5` | none | The colour block the tile paints (blocks.css). Unset, the tile takes the ground it sits on. |
| `colour` | `string` | none | The walls, any CSS colour. Unset, the block's first motif colour. |
| `accent` | `string` | none | The roof and the trim. Unset, the block's second motif colour. |
| `label` | `string` | none | What the picture shows, for a screen reader. Unset, the drawing is decoration and hidden from it. |

## Usage

```astro
<a href="/homes/12-linden-row/" class="block aspect-[4/3]">
  <HomeArt kind="house" seed="12 Linden Row" time="dusk" tile block={2} label="A two-storey house with a gable roof at dusk" />
</a>
```

## Reduced motion

The windows light at once on hover, with no fade.

## With ClientRouter

Static SVG; nothing to rebind.

## Craft

- One colour makes a colourway: the shade, the light, the glass and the door are mixed from the walls in OKLab, so a palette repaints every building with the site.
- The seed is hashed into a small generator, so a listing's address draws the same building on every build and two addresses rarely draw the same one.
- A window is lit or not when it is drawn; an unlit one lights when a link around the drawing is pointed at or focused, the way a house looks when someone is home.
- Drawn in a 160 by 120 box on one ground line, so a row of tiles of different kinds stands level.

## Replaces

- Stock photographs of houses on listing templates

## Source

```astro
---
/**
 * HomeArt — a building drawn flat, for a listing that has no photograph yet: a house, a terrace, an apartment block, a
 * villa, a cabin, a tower, an office or a shop, in the colour of the block or tone it sits on (a palette picks that by
 * contrast with the ground), with a shade, a light and the windows mixed from it. `seed` varies the drawing (the floors,
 * the windows, the roof, the trees) and draws the same one on every build; `time` lights the windows at dusk and at
 * night and darkens the sky. With `tile` it fills its box, the block's ground as the sky.
 *
 * Hovering or focusing a link around it lights a few more windows. Nothing moves on its own. Zero JavaScript.
 */
import type { HTMLAttributes } from 'astro/types';

const kinds = ['house', 'terrace', 'apartment', 'villa', 'cabin', 'tower', 'office', 'shop'] as const;
type Kind = (typeof kinds)[number];

interface Props extends HTMLAttributes<'div'> {
  /** The building. Unset, `seed` picks one. */
  kind?: Kind;
  /** A word the drawing is varied by: the same word, the same building. */
  seed?: string;
  /** Daylight, dusk (some windows lit, a warm sky) or night (most lit, a dark sky). */
  time?: 'day' | 'dusk' | 'night';
  /** Fill the box: the block's ground as the sky, the building on its ground line. */
  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 walls' colour, any CSS colour. Unset, the block's first motif colour. */
  colour?: string;
  /** The roof 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 building. */
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 G = 104; // the ground line, in a 160 by 120 box
const lit = time === 'night' ? 0.75 : time === 'dusk' ? 0.4 : 0;
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}"/>`);
/** A window: lit by time and seed; an unlit one is marked to light when a link around the drawing is pointed at. */
const win = (x: number, y: number, w: number, hh: number, rx = 0.8) => {
  const on = r() < lit;
  add(`<rect class="ma-home__w" data-on="${on ? '' : 'off'}" x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(hh)}" rx="${rx}"/>`);
};
const tree = (x: number, s = 1) => {
  rect('ma-home__d', x - 1.2 * s, G - 12 * s, 2.4 * s, 12 * s);
  add(`<circle class="ma-home__g" cx="${n(x)}" cy="${n(G - 17 * s)}" r="${n(9 * s)}"/><circle class="ma-home__gl" cx="${n(x - 3 * s)}" cy="${n(G - 20 * s)}" r="${n(4 * s)}"/>`);
};
const pine = (x: number, s = 1) => path('ma-home__g', `M${n(x)} ${n(G - 34 * s)}L${n(x + 9 * s)} ${n(G - 6 * s)}H${n(x - 9 * s)}Z`);
const door = (x: number, w = 9, hh = 15) => {
  rect('ma-home__d', x, G - hh, w, hh, 0.8);
  add(`<circle class="ma-home__l" cx="${n(x + w - 2.2)}" cy="${n(G - hh / 2)}" r="0.8"/>`);
};

if (art === 'house') {
  const w = 64 + Math.round(r() * 18);
  const x = 80 - w / 2;
  const top = G - 44;
  const roof = pick(['gable', 'hip', 'flat'] as const);
  if (r() < 0.6) rect('ma-home__s', x + w * 0.68, top - 22, 7, 16);
  if (roof === 'flat') rect('ma-home__a', x - 3, top - 4, w + 6, 5, 1);
  else path('ma-home__a', roof === 'gable' ? `M${n(x - 5)} ${top}L80 ${top - 26}L${n(x + w + 5)} ${top}Z` : `M${n(x - 5)} ${top}L${n(x + 14)} ${top - 20}H${n(x + w - 14)}L${n(x + w + 5)} ${top}Z`);
  rect('ma-home__b', x, top, w, 44);
  rect('ma-home__sh', x + w * 0.72, top, w * 0.28, 44);
  const cols = w > 74 ? 3 : 2;
  for (let i = 0; i < cols; i++) win(x + 8 + i * ((w - 16 - 11) / (cols - 1)), top + 7, 11, 11);
  door(x + 10);
  win(x + w - 25, G - 16, 13, 10);
  if (roof === 'gable') win(77, top - 14, 6, 7, 3);
  tree(r() < 0.5 ? x - 12 : x + w + 12, 0.85 + r() * 0.3);
} else if (art === 'terrace') {
  const units = 3;
  const w = 32;
  const x0 = 80 - (units * w) / 2;
  for (let u = 0; u < units; u++) {
    const x = x0 + u * w;
    const floors = 2 + (r() < 0.35 ? 1 : 0);
    const top = G - floors * 19 - 4;
    const cls = u === 1 ? 'ma-home__l' : u === 2 ? 'ma-home__s' : 'ma-home__b';
    path('ma-home__a', `M${n(x - 1)} ${top}L${n(x + w / 2)} ${top - 12}L${n(x + w + 1)} ${top}Z`);
    rect(cls, x, top, w, G - top);
    for (let f = 0; f < floors - 1; f++) { win(x + 5, top + 6 + f * 19, 9, 11); win(x + 18, top + 6 + f * 19, 9, 11); }
    door(x + 5, 8, 14);
    win(x + 18, G - 15, 9, 10);
  }
  rect('ma-home__s', x0 - 2, G - 2, units * w + 4, 2);
} else if (art === 'apartment') {
  const floors = 4 + Math.floor(r() * 3);
  const fh = 13;
  const w = 88;
  const x = 80 - w / 2;
  const top = G - floors * fh - 6;
  rect('ma-home__a', x + w * 0.62, top - 7, 18, 8);
  rect('ma-home__a', x - 2, top - 2, w + 4, 4, 1);
  rect('ma-home__b', x, top, w, G - top);
  rect('ma-home__sh', x + w * 0.5, top, w * 0.5, G - top);
  for (let f = 0; f < floors; f++) {
    const y = top + 5 + f * fh;
    for (let i = 0; i < 5; i++) win(x + 6 + i * 16.5, y, 9, 7);
    if (f % 2 === 1 && f < floors - 1) rect('ma-home__a', x + 3, y + 8.5, w - 6, 1.6);
  }
  rect('ma-home__d', 76, G - 9, 12, 9, 0.8);
  tree(x - 11, 0.8);
  tree(x + w + 11, 0.7 + r() * 0.2);
} else if (art === 'villa') {
  const w = 116;
  const x = 80 - w / 2;
  const up = r() < 0.7;
  rect('ma-home__b', x, G - 26, w, 26);
  rect('ma-home__a', x - 6, G - 29, w + 12, 4, 1);
  for (let i = 0; i < 5; i++) win(x + 6 + i * 21.5, G - 21, 17, 17, 0.6);
  if (up) {
    rect('ma-home__l', x + 18, G - 50, 62, 21);
    rect('ma-home__a', x + 12, G - 53, 74, 4, 1);
    for (let i = 0; i < 3; i++) win(x + 24 + i * 19, G - 46, 14, 13, 0.6);
  }
  rect('ma-home__pool', x + 6, G + 3, 58, 4, 2);
  const px = x + w + 4;
  add(`<path class="ma-home__ss" d="M${n(px)} ${G}Q${n(px - 2)} ${G - 20} ${n(px + 3)} ${G - 36}"/>`);
  path('ma-home__g', `M${n(px + 3)} ${G - 36}q-12 -2 -16 6q10 -6 16 -6q-6 -10 -16 -8q10 2 16 8q2 -11 12 -12q-9 4 -12 12q11 -4 15 4q-8 -6 -15 -4Z`);
} else if (art === 'cabin') {
  const w = 70;
  const x = 80 - w / 2;
  path('ma-home__a', `M${n(x - 4)} ${G}L80 ${G - 62}L${n(x + w + 4)} ${G}Z`);
  path('ma-home__b', `M${n(x + 4)} ${G}L80 ${G - 52}L${n(x + w - 4)} ${G}Z`);
  path('ma-home__w', `M66 ${G - 4}L80 ${G - 38}L94 ${G - 4}Z`);
  add(`<path class="ma-home__ws" d="M80 ${G - 38}V${G - 4}M70 ${G - 14}H90"/>`);
  if (lit) add(`<path class="ma-home__w" data-on="" d="M66 ${G - 4}L80 ${G - 38}L94 ${G - 4}Z"/>`);
  rect('ma-home__s', x - 8, G - 3, w + 16, 3);
  pine(x - 16, 1);
  pine(x - 26, 0.75);
  pine(x + w + 16, 0.9 + r() * 0.2);
} else if (art === 'tower') {
  const w = 46;
  const x = 80 - w / 2;
  const top = 12 + Math.round(r() * 8);
  rect('ma-home__a', x + 10, top - 8, w - 20, 8);
  rect('ma-home__b', x, top, w, G - top);
  rect('ma-home__sh', x + w * 0.55, top, w * 0.45, G - top);
  for (let y = top + 5; y < G - 12; y += 7) for (let i = 0; i < 4; i++) win(x + 4 + i * 10.5, y, 8, 4, 0.4);
  rect('ma-home__d', 74, G - 10, 12, 10, 0.6);
  rect('ma-home__l', x - 22, G - 34, 20, 34);
  for (let y = G - 30; y < G - 4; y += 7) win(x - 19, y, 14, 4, 0.4);
  rect('ma-home__s', x + w + 3, G - 24, 22, 24);
  for (let y = G - 20; y < G - 4; y += 7) win(x + w + 6, y, 16, 4, 0.4);
} else if (art === 'office') {
  const w = 112;
  const x = 80 - w / 2;
  const floors = 3;
  const top = G - floors * 17 - 6;
  rect('ma-home__b', x, top, w, G - top);
  rect('ma-home__a', x, top, w, 6);
  for (let f = 0; f < floors; f++) for (let i = 0; i < 7; i++) win(x + 4 + i * 15.6, top + 9 + f * 17, 13, 12, 0.4);
  rect('ma-home__a', 62, G - 18, 36, 3, 1);
  rect('ma-home__d', 70, G - 15, 20, 15);
  tree(x - 10, 0.8);
} else {
  const w = 88;
  const x = 80 - w / 2;
  const top = G - 52;
  rect('ma-home__b', x, top, w, 52);
  rect('ma-home__a', x - 2, top - 4, w + 4, 5, 1);
  win(x + 12, top + 7, 16, 12);
  win(x + 36, top + 7, 16, 12);
  win(x + 60, top + 7, 16, 12);
  rect('ma-home__a', x, top + 23, w, 6);
  for (let i = 0; i < 8; i++) path(i % 2 ? 'ma-home__p' : 'ma-home__a', `M${n(x + i * 11)} ${top + 29}h11l-1.5 6h-8Z`);
  win(x + 6, G - 16, 50, 16, 0.4);
  door(x + 64, 14, 18);
}

const sky = time === 'night' ? 'ma-home__night' : time === 'dusk' ? 'ma-home__dusk' : '';
const a11y = label ? { role: 'img' as const, 'aria-label': label } : { 'aria-hidden': 'true' as const };
const vars = [colour && `--ma-home-body:${colour}`, accent && `--ma-home-accent:${accent}`, typeof style === 'string' && style].filter(Boolean).join(';') || undefined;
---

<div class:list={['ma-home', tile && 'ma-home--tile', className]} data-ma-block={block} data-kind={art} data-time={time} style={vars} {...rest}>
  <svg viewBox="0 0 160 120" class="ma-home__svg" preserveAspectRatio={tile ? 'xMidYMax slice' : undefined} focusable="false" {...a11y}>
    {sky && <rect class={sky} x="-200" y="-200" width="560" height="304" />}
    {time === 'night' && <g class="ma-home__stars"><circle cx="20" cy="18" r="0.8" /><circle cx="44" cy="10" r="0.6" /><circle cx="118" cy="14" r="0.8" /><circle cx="140" cy="30" r="0.6" /><circle cx="96" cy="8" r="0.6" /></g>}
    <rect class="ma-home__ground" x="-200" y={G} width="560" height="200" />
    <g set:html={out.join('')} />
  </svg>
</div>

<style is:global>
  @layer components {
    /* the walls' colour, and the shade, light, glass and deep tones mixed from it, so a colourway is one value */
    :where(.ma-home) {
      --_body: var(--ma-home-body, var(--ma-motif-1, var(--primary, currentColor)));
      --_accent: var(--ma-home-accent, var(--ma-motif-2, var(--accent, currentColor)));
      --_shade: color-mix(in oklab, var(--_body) 76%, #000);
      --_light: color-mix(in oklab, var(--_body) 58%, #fff);
      --_glass: color-mix(in oklab, var(--_body) 14%, #fff);
      --_deep: color-mix(in oklab, var(--_body) 32%, #17120e);
      --_lit: oklch(0.88 0.12 85);
      display: grid;
      place-items: end center;
    }
    :where(.ma-home__svg) { width: 100%; height: 100%; overflow: visible; }
    /* the tile: the block's ground is the sky, the drawing fills the box from its ground line up */
    :where(.ma-home--tile) { position: relative; overflow: hidden; width: 100%; height: 100%; background: var(--ma-block-ground, transparent); }
    :where(.ma-home--tile .ma-home__svg) { position: absolute; inset: 0; overflow: hidden; }
    :where(.ma-home__ground) { fill: color-mix(in oklab, var(--ma-block-ground, var(--_body)) 82%, #000); }
    :where(.ma-home:not(.ma-home--tile) .ma-home__ground) { fill: none; stroke: var(--_shade); stroke-width: 0.8; }
    :where(.ma-home__dusk) { fill: oklch(0.72 0.12 45 / 0.38); }
    :where(.ma-home__night) { fill: oklch(0.2 0.05 265 / 0.72); }
    :where(.ma-home__stars) { fill: #fff; opacity: 0.8; }
    :where(.ma-home__b) { fill: var(--_body); }
    :where(.ma-home__s) { fill: var(--_shade); }
    :where(.ma-home__sh) { fill: var(--_shade); fill-opacity: 0.35; }
    :where(.ma-home__l) { fill: var(--_light); }
    :where(.ma-home__a) { fill: var(--_accent); }
    :where(.ma-home__p) { fill: var(--_glass); }
    :where(.ma-home__d) { fill: var(--_deep); }
    :where(.ma-home__g) { fill: color-mix(in oklab, var(--_accent) 40%, oklch(0.55 0.12 150)); }
    :where(.ma-home__gl) { fill: color-mix(in oklab, var(--_accent) 30%, oklch(0.7 0.13 140)); }
    :where(.ma-home__ss) { fill: none; stroke: var(--_deep); stroke-width: 2.4; stroke-linecap: round; }
    :where(.ma-home__ws) { fill: none; stroke: var(--_shade); stroke-width: 1.4; }
    :where(.ma-home__pool) { fill: oklch(0.8 0.08 220); }
    :where(.ma-home__w) { fill: var(--_glass); transition: fill var(--ma-duration-slow, 400ms) var(--ma-ease-out, ease-out); }
    :where(.ma-home__w[data-on='']) { fill: var(--_lit); }
    :where(.ma-home[data-time='day'] .ma-home__w) { fill: color-mix(in oklab, var(--_glass) 78%, oklch(0.75 0.06 230)); }
    /* a link around the drawing, pointed at or focused: the unlit windows light, as a house does when someone is home */
    :where(a:is(:hover, :focus-visible) .ma-home__w[data-on='off']) { fill: var(--_lit); }
    @media (forced-colors: active) {
      :where(.ma-home__svg) { forced-color-adjust: none; }
    }
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-home__w) { transition: none; }
    }
  }
</style>

```
