# Blog Post Thumbnail — Crystal Pattern

Instructions for generating the crystal field pattern used in blog post thumbnails and hero illustrations. This guide describes the **lattice base layer** — filters, gradients, and effects are layered on top of this base and described in later sections.

## Implementation

The generation logic lives in [`crystal-lattice.js`](./crystal-lattice.js) in this folder. The Storybook story at `stories/visuals/Crystals.stories.js` imports from this file to render previews. All generation code should stay in this folder — stories only consume it.

A working interactive tool that implements this full spec is hosted at [`tools/crystal-generator.html`](https://design.mirrorphysics.com/tools/crystal-generator.html). Use it as a reference for how the lattice, materials, gradients, dark mode, and positioning all come together.

## Seed

Every visual is driven by a single numeric **seed**. The seed must deterministically control ALL randomized decisions: crystal heights, which grid cells are populated, rotation direction, and any future filter/effect parameters added on top.

**After generating a visual, ALWAYS output the seed value** so the user can reproduce or iterate on the result. Format:

```
Seed: <value>
```

Use the seed to initialize a seeded PRNG (e.g., a simple mulberry32 or similar). All random calls in the generation pipeline must draw from this single PRNG stream.

## Crystal Shape

Each crystal is a **vertically-oriented, bilaterally symmetric hexagon** — like a diamond (square rotated 45°) stretched vertically along its center axis. It has:

- A **pointed top apex** where two sides meet at exactly 90°, each angling away at 45° from the vertical center
- Two **straight vertical sides** running down the left and right from the widest point
- A **pointed bottom apex** with the same 90° geometry as the top
- The **widest point** is at `y = w/2` from the top (and `w/2` from the bottom), where the diagonal meets the vertical edge

The shape is symmetric across both the vertical center axis and the horizontal midline (if height allowed — but height varies, so only vertical symmetry is guaranteed).

### Geometry

Given a crystal of width `w` and height `h`, anchored at top-left corner `(x, y)`:

```
        (x + w/2, y)          ← top apex
        ╱            ╲
      ╱                ╲       45° diagonals
    ╱                    ╲
(x, y + w/2)    (x + w, y + w/2)  ← widest point
    │                    │
    │                    │     vertical sides
    │                    │
(x, y + h - w/2)  (x + w, y + h - w/2)
    ╲                    ╱
      ╲                ╱       45° diagonals
        ╲            ╱
        (x + w/2, y + h)      ← bottom apex
```

The SVG path (6 points, clockwise from top apex):

```
M {x + w/2}  {y}
L {x + w}    {y + w/2}
L {x + w}    {y + h - w/2}
L {x + w/2}  {y + h}
L {x}        {y + h - w/2}
L {x}        {y + w/2}
Z
```

### Reference examples

**Short crystal** (w=69, h=139):
```svg
<svg width="69" height="139" viewBox="0 0 69 139" fill="none">
  <path d="M34.5 0L69 34.5V104.5L34.5 139L0 104.5V34.5Z" fill="#D9D9D9"/>
</svg>
```

**Medium crystal** (w=69, h=239):
```svg
<svg width="69" height="239" viewBox="0 0 69 239" fill="none">
  <path d="M34.5 0L69 34.5V204.5L34.5 239L0 204.5V34.5Z" fill="#D9D9D9"/>
</svg>
```

**Tall crystal** (w=69, h=369):
```svg
<svg width="69" height="369" viewBox="0 0 69 369" fill="none">
  <path d="M34.5 0L69 34.5V334.5L34.5 369L0 334.5V34.5Z" fill="#D9D9D9"/>
</svg>
```

### Constraints

- **Width**: fixed at ~69 SVG units for all crystals (matching reference assets)
- **Height**: varies per crystal (range roughly 139–369 units). Must be >= width so the vertical sides exist.
- **Minimum height** = width (`w`), which produces a pure diamond with no vertical sides

## Step 1 — Generate an Upright Crystal Field

The field contains **at most 12 crystals** total, split into two zones:

### Core lattice (6–8 crystals) — interlocking two-row layout

The core is a **brick-pattern interlock**: two rows of crystals where Row B is offset horizontally by half a pitch, so the pointed tips of each row nest into the gaps of the other row, creating a continuous **zig-zag seam** between them.

#### Row layout

- **Pitch** `P = crystalWidth + latticeGap` (e.g., 69 + 10 = 79 units).
- **Row A** (top): 3–4 crystals side by side at `x = col * P`. All crystals are **bottom-aligned** — their bottom apexes sit at the same y. Heights vary upward.
- **Row B** (bottom): same number of crystals, each shifted **left by half a pitch** (`x = col * P - P/2`). All crystals are **top-aligned** — their top apexes sit at the same y. Heights vary downward.

Example with 4 columns, pitch = 10:
```
Row A x positions:   0,  10,  20,  30
Row B x positions:  -5,   5,  15,  25
```

#### Interlock depth

Row B's top-apex y is positioned **above** Row A's bottom-apex y so the pointed tips overlap and interlock. The overlap amount (**interlock depth**) is calculated so the perpendicular gap between adjacent 45° diagonal edges equals `latticeGap`:

```
interlockDepth = P/2 - latticeGap × √2
```

This ensures the visible gap width is consistent everywhere — between vertical edges in the same row AND between diagonal edges at the zig-zag seam.

```
Row A:   ╲  ╱  ╲  ╱  ╲  ╱  ╲  ╱     ← bottom tips pointing down
          ╲╱    ╲╱    ╲╱    ╲╱
          ╱╲    ╱╲    ╱╲    ╱╲       ← zig-zag gap (uniform width)
         ╱  ╲  ╱  ╲  ╱  ╲  ╱  ╲
Row B:  ╱    ╲╱    ╲╱    ╲╱    ╲     ← top tips pointing up
```

### Scattered outliers (1–4 crystals)

The remaining crystals are placed as **isolated singles** around the core lattice. These give the pattern depth and prevent it from feeling like a simple block.

- Outliers sit on the same column grid (same pitch) but in columns outside the core lattice range.
- Each outlier column contains exactly 1 crystal.
- Outliers should be distributed on different sides of the core (not all on one side).
- Outliers tend to be shorter crystals (toward the minimum height end).

### Height variation
- Crystal heights should vary with **smooth, organic variation** — think Perlin noise, not pure random.
- Use the seed to generate a coherent noise field sampled at each grid position, then map the noise value to the height range (139–369 units).
- Neighboring crystals (both within a column and across adjacent columns) should have correlated heights so the pattern feels like a natural terrain.
- Short crystals (~139 units) and long crystals (~369 units) should both be present, with most falling in the middle range.

## Step 2 — Rotate the Entire Grid

After generating the upright crystal field, **rotate the entire SVG group by 45 degrees** in one of the four diagonal directions:

| Direction       | Rotation   |
|-----------------|------------|
| Northwest (NW)  | -45 deg    |
| Northeast (NE)  | +45 deg    |
| Southwest (SW)  | -135 deg   |
| Southeast (SE)  | +135 deg   |

The reference pattern uses **counterclockwise / NW (-45 deg)**. The PRNG picks the direction unless the user specifies one.

Apply the rotation as a single `transform="rotate(angle, cx, cy)"` on the parent `<g>` wrapping all crystals, where `(cx, cy)` is the center of the bounding box.

## Step 3 — Thumbnail Framing (Corner Overflow)

The final output is a **square thumbnail**. The lattice must always **overflow out of one corner** of this square — it should never be fully contained or centered. This creates the signature "emerging from the corner" composition.

### Corner selection
The PRNG picks one of the four corners: `top-left`, `top-right`, `bottom-left`, `bottom-right`. The user can also specify a corner explicitly.

### Positioning
The square `viewBox` is sized to ~70% of the lattice's largest dimension. It is positioned so that:
- The lattice center sits **outside** the chosen corner
- Roughly 30–50% of the lattice is visible inside the thumbnail
- The rest overflows beyond the thumbnail edge and is clipped

The thumbnail container must use `overflow: hidden` to clip the protruding crystals. Partial clipping at the edges is intentional — it gives the composition energy and implies the pattern continues beyond the frame.

## Step 4 — Material

Each thumbnail has a **material** that controls how crystals are rendered. The PRNG picks a material unless the user specifies one. Available materials: `plastic`, `glass`.

The `materialDefs(material, baseColor)` helper in `crystal-lattice.js` returns all colors and config for the chosen material.

---

### Material: Plastic (Neuphorism)

Crystals look softly extruded from the background surface, as if carved from the same material.

#### Principles

1. **Shape fill ≈ background** — the crystal fill and the thumbnail background are the same base color. The shape is the same surface, just raised.
2. **Bold dual shadows at full opacity** — a dark shadow offset bottom-right and a white shadow offset top-left. Both use the full shadow color (no reduced opacity). This is the key difference from a "glow" effect.
3. **Blur ≈ 2× offset** — produces the soft, pillowy depth. CSS reference: `box-shadow: 12px 12px 24px #c5c8ce, -12px -12px 24px #ffffff`.
4. **Surface gradient** — a very subtle diagonal gradient from slightly lighter (top-left) to slightly darker (bottom-right) reinforces the light direction.

### Color derivation

All colors derive from a single **base color** (default: Neuphorism `#e6e8ed`):

| Role           | Derivation                | Example     |
|----------------|---------------------------|-------------|
| Background     | base color                | `#e6e8ed`   |
| Fill light     | base lightened ~6%        | `#eceef2`   |
| Fill dark      | base darkened ~4%         | `#dddfe4`   |
| Shadow dark    | base darkened ~14%        | `#c5c8ce`   |
| Shadow light   | pure white                | `#ffffff`   |

### SVG filter structure

The CSS `box-shadow: 12px 12px 24px` translates to SVG as `stdDeviation="12" dx="8" dy="8"` (stdDeviation ≈ CSS blur / 2, offset slightly reduced since SVG blur spreads differently).

**Critical**: both shadows use `flood-opacity="1"` — full strength. Reducing opacity is what makes it look like a glow instead of neumorphism.

```xml
<filter id="neo" x="-80%" y="-80%" width="260%" height="260%">
  <!-- Dark shadow — bottom-right -->
  <feGaussianBlur in="SourceAlpha" stdDeviation="12" result="darkBlur"/>
  <feOffset dx="8" dy="8" in="darkBlur" result="darkOff"/>
  <feFlood flood-color="{shadowDark}" flood-opacity="1" result="darkColor"/>
  <feComposite in="darkColor" in2="darkOff" operator="in" result="darkShadow"/>

  <!-- Light highlight — top-left -->
  <feGaussianBlur in="SourceAlpha" stdDeviation="12" result="lightBlur"/>
  <feOffset dx="-8" dy="-8" in="lightBlur" result="lightOff"/>
  <feFlood flood-color="#ffffff" flood-opacity="1" result="lightColor"/>
  <feComposite in="lightColor" in2="lightOff" operator="in" result="lightShadow"/>

  <!-- Merge: shadows behind, shape on top -->
  <feMerge>
    <feMergeNode in="darkShadow"/>
    <feMergeNode in="lightShadow"/>
    <feMergeNode in="SourceGraphic"/>
  </feMerge>
</filter>
```

Each crystal path is rendered with `fill="url(#mat-fill)" filter="url(#mat-filter)"` — no stroke.

---

### Material: Glass

Crystals look like frosted glass panes — translucent, with subtle refraction highlights and a delicate white border on light-facing edges.

#### Principles

1. **Translucent fill** — a gradient from `rgba(255,255,255,0.25)` at the top-left to `rgba(255,255,255,0.08)` at the bottom-right. The background shows through.
2. **Gradient stroke** — a thin white border (1.5px) that fades from bright on the light-facing edges (top-left, ~60% opacity) to subtle on the shadow side (~15% opacity).
3. **Inner highlight** — a vertical gradient overlay from `rgba(255,255,255,0.4)` at the top to transparent at ~30%, simulating refracted light along the upper edge.
4. **Single soft drop shadow** — not dual neumorphic shadows. One shadow offset slightly bottom-right, blurred, using the base color darkened ~20% at ~30% opacity.

#### SVG structure

Each glass crystal is rendered as three layered paths:

```xml
<!-- 1. Translucent fill + drop shadow -->
<path d="..." fill="url(#mat-fill)" filter="url(#mat-filter)" />
<!-- 2. Inner highlight overlay -->
<path d="..." fill="url(#glass-inner)" />
<!-- 3. Border -->
<path d="..." fill="none" stroke="url(#glass-stroke)" stroke-width="1.5" />
```

## Step 5 — Color Gradient

Each thumbnail has a **color gradient** — two hues from the Fractal palette rendered as scattered radial gradient blobs with heavy blur behind the crystals.

### Color selection

The PRNG picks from curated aesthetic pairs unless the user specifies two hue names:

| Pair              | Character                  |
|-------------------|----------------------------|
| blue + pink       | Cool/warm contrast         |
| blue + orange     | Complementary              |
| purple + pink     | Analogous, vibrant         |
| blue + green      | Cool analogous             |
| orange + yellow   | Warm analogous             |
| purple + blue     | Cool analogous             |

### Background rendering

4 radial gradient orbs are placed behind the crystal SVG layer, each using the **300** (diffuse, higher opacity) or **500** (accent, lower opacity) stop from the chosen hues:

```jsx
<div style={{ position: 'absolute', inset: '-30%', filter: 'blur(80px)', pointerEvents: 'none' }}>
  {orbs.map(orb => (
    <div style={{
      position: 'absolute',
      width: `${orb.size}%`, height: `${orb.size}%`,
      left: `${orb.x}%`, top: `${orb.y}%`,
      background: `radial-gradient(circle, ${orb.color} 0%, transparent 70%)`,
      opacity: orb.opacity,
    }} />
  ))}
</div>
```

Orb positions, sizes, and opacities are seeded so the layout is reproducible.

### Material adaptation

- **Plastic on gradient**: fill becomes semi-transparent (~85% opacity) so the gradient bleeds through. Dark shadow is tinted 25% toward the primary hue's 500 stop.
- **Glass on gradient**: works naturally — translucent fill already shows the gradient through. No changes needed.
- **No gradient** (solid background): materials behave exactly as described in Step 4.

The `gradientDefs()` and `materialDefs()` helpers in `crystal-lattice.js` handle all of this.

## Step 6 — Typography Overlay

If the thumbnail includes text (e.g., blog post title, category label), follow these rules:

### Primary text (title, heading)
- **Font**: Figtree, weight 600 (Semibold)
- **Case**: normal sentence/title case
- **Color**: `--fg-primary` in both light and dark mode

### Secondary text (caption, category, date)
- **Font**: Geist Mono, weight 300 (Light)
- **Case**: ALL CAPS
- **Color**: `--fg-secondary` in both light and dark mode

### Placement
- Text should not overlap the dense core of the lattice. Place it in the open area opposite the crystal cluster (the corner the lattice is NOT overflowing from).
- Maintain generous padding from the thumbnail edges (minimum 32px at 1x).

## Summary of Parameters

| Parameter               | Typical value          | Notes                                          |
|-------------------------|------------------------|-------------------------------------------------|
| **Seed**                | any integer            | Controls ALL randomness; always output after gen |
| Total crystals          | max 12                 | Hard cap                                        |
| Core lattice crystals   | 6–8                    | Two interlocking rows: 3–4 columns × 2 rows    |
| Outlier crystals        | 1–4                    | Isolated singles around the core                |
| Crystal width           | 69 units               | Fixed for all crystals                          |
| Crystal height          | 139–369 units          | Seeded Perlin-like noise; must be >= width      |
| Lattice gap             | ~10 units              | Controls horizontal AND diagonal gap uniformly  |
| Pitch                   | width + gap (79)       | Center-to-center distance in the same row       |
| Interlock depth         | P/2 − gap×√2          | Vertical overlap between Row A bottom / Row B top |
| Rotation                | ±45 or ±135 deg        | Seeded pick from NW / NE / SW / SE              |
| Corner                  | TL / TR / BL / BR      | Which corner the lattice overflows from         |
| Material                | plastic / glass        | Seeded pick; user can override                  |
| Gradient                | e.g. blue + pink       | Seeded from curated pairs; user can override     |
| Base color              | #e6e8ed                | Surface color; background + shadow derivation   |
