# SKILL.md — Image Effects Toolkit

## Purpose
You are an agent that applies one of two visual effects to a user-provided input image. Based on the user's chosen effect, you must:
1. Read this SKILL.md to understand the effect's exact methodology and parameters.
2. Write your own Python script implementing the steps.
3. Run it on the input image.
4. Return the output image.

**Do not change or "upgrade" any parameter values.** Treat them as fixed contracts.

---

## Effect Index

| ID | Name | Output Type | Core Tech |
|----|------|-------------|-----------|
| `ascii_pixel` | ASCII Pixel Effect | Static image (PNG) | PIL, rembg |
| `dot_shape` | Dot Shape Effect | Static image (or per-frame for video) | PIL, rembg |

If the user names an effect ambiguously, ask which one. Otherwise default by request keywords ("ascii" → `ascii_pixel`, "dots/shapes/blue" → `dot_shape`).

---

# EFFECT 1: ASCII Pixel Effect

## Description
A layered ASCII rendering of the subject, flattened to a single still image. The subject (isolated via `rembg`) is rendered as colored ASCII characters whose density is inversely proportional to luminance (dark = dense glyphs). The background is a blurred, darkened, desaturated, pixelated version of the original, overlaid with a faint pixel grid and static background dots. A static glow is applied on high-luminance subject cells.

## Output
A single PNG image (RGB) with all three layers composited in order: blurred BG → pixel grid → ASCII glyphs.

## Fixed Parameters
| Parameter | Value |
|---|---|
| `TARGET_WIDTH` | 900 px (maintain aspect ratio) |
| `CELL_W` × `CELL_H` | 11 × 14 px |
| `GRID_STEP` | 24 px |
| `PIXEL_SIZE` | (used in BG pixelate downscale ÷ upscale) |
| Blur radius | 14 (GaussianBlur) |
| BG darken factor | 0.65 (35% darker) |
| BG desaturation | 50% blend with luminance greyscale |
| Subject mask threshold | mask mean > 0.25 |
| Lum supplement threshold | lum > 0.45 (**reference only — DO NOT use as supplement**) |
| ASCII ramp | `@#S08Xox+=;:-,. ` (dark→bright = dense→sparse) |
| BG dot color | (40, 65, 100) dark blue |
| BG dot opacity | 30% |
| Grid opacity | 5% |
| Resampling | LANCZOS (resize), BOX (pixelate down), NEAREST (pixelate up) |

## The 7 Steps

### Step 1 — Load and Resize
- Open image, convert to RGB.
- Resize to `TARGET_WIDTH = 900` px, maintain aspect ratio, LANCZOS filter.

### Step 2 — Build Blurred Background
1. `GaussianBlur(radius=14)` on the resized image.
2. Darken: multiply pixel values by `0.65`.
3. Desaturate 50%: blend 50% with luminance greyscale of itself.
4. Pixelate: downscale by `PIXEL_SIZE` with BOX filter, upscale back with NEAREST.
- This is the base back layer behind the ASCII.

### Step 3 — Subject Mask (rembg)
- Run `rembg` on the resized image to obtain the alpha mask (subject silhouette).
- **Use the rembg mask only.** Do NOT add a luminance supplement — supplements cause bright BG areas (glows, light sources) to become ASCII characters instead of background dots, which is wrong.
- A cell is "subject" if its sampled mask mean > `0.25`.

### Step 4 — Pixel Grid Overlay
- White boxes drawn at every `GRID_STEP = 24` px interval.
- Opacity 5% (`globalAlpha = 0.05`).
- Drawn **only on subject cells**, not on background.
- Static layer (drawn once).

### Step 5 — Build Cell Grid
- Cell size `CELL_W × CELL_H = 11 × 14` px.
- For each cell, sample average RGB and luminance from the original (resized) image.
- **Subject cells**: pick ASCII char from ramp using inverted luminance (dark = dense char from left of ramp). Color = `normalize_color(r,g,b)`.
- **Background cells**: char `.`, color `(40, 65, 100)`.

### Step 6 — Composite Layers (Single Image)
Flatten three layers into one PIL `Image` (same size as the resized input):
1. **Layer 1** — blurred BG from Step 2.
2. **Layer 2** — pixel grid overlay from Step 4 (white boxes at `GRID_STEP=24`, alpha 5%, subject cells only). Paste with alpha onto Layer 1.
3. **Layer 3** — ASCII glyphs from Step 5, drawn with `PIL.ImageDraw` using a monospace bold font sized to fit `CELL_W × CELL_H`. BG cells render `.` in `(40,65,100)` at 30% opacity; subject cells render their ramp char in `normalize_color(r,g,b)`.

Save the result as a PNG.

### Step 7 — Static Rendering Details
Since output is a still image, the animation behaviors do **not** apply. Preserve only their static-frame equivalents:
- **Glow**: on high-luminance subject cells (e.g., `lum > 0.45`), draw the glyph twice — once blurred (soft halo via `ImageFilter.GaussianBlur` on a glow-only sub-layer) then the sharp glyph on top. Halo intensity scales with luminance.
- **Background dots**: rendered at 30% opacity, static.
- **Do not** render pulse, shine sweep, flicker, or hover ripple — these are animation-only and have no static representation.

## Color Normalization Formula
```python
def normalize_color(r, g, b):
    mx = max(r, g, b, 1)
    return int(r/mx*255), int(g/mx*255), int(b/mx*255)
```
Preserves hue while boosting all channels so the brightest channel becomes 255. Subject pixels get their color maximized.

---

# EFFECT 2: Dot Shape Effect

## Description
A graphic motion effect on a blue background. White circles and squares (random 50/50 mix) are placed on a grid, sized by the subject's local inverted luminance (dark areas → big shapes, bright → small). Animated ASCII text in a lighter blue fills the background behind everything. Subject is isolated per frame via `rembg`.

**Approved final look:** Blue background, white shapes (50/50 circle/square), ASCII text in slightly lighter blue behind, shapes at 1.5× density (`GRID_STEP=27`, `MAX_RADIUS=12`).

## Output
- Static image: single rendered image.
- Video: ffmpeg extract frames → process each frame → ffmpeg reassemble.

## Fixed Parameters
| Parameter | Value | Notes |
|---|---|---|
| `TARGET_SIZE` | 900 | Square canvas |
| `GRID_STEP` | 27 | Spacing between shapes (1.5× base) |
| `MIN_RADIUS` | 2 | Smallest shape |
| `MAX_RADIUS` | 12 | Largest shape (1.5× base) |
| `BG_COLOR` | (36, 73, 209) | Blue |
| `SHAPE_COLOR` | (255, 255, 255) | White |
| `ASCII_COLOR` | (70, 105, 230) | Lighter blue, same hue |
| `ASCII_CELL` | 12 | ASCII grid cell size (px) |
| `MASK_THRESH` | 30 | rembg alpha threshold |
| `ASCII_RAMP` | `@#S08Xox+=;:-,. ` | Skip last 3 chars for BG |
| `FONT` | Courier New Bold | `/System/Library/Fonts/Supplemental/Courier New Bold.ttf` |
| Shape mix | 50/50 | `rng.random() < 0.5` → circle, else square |
| Luminance mapping | inverted | `inv_lum = 1.0 - avg_lum` |

## The Stack (per frame)
1. **rembg** — extract subject mask from the frame.
2. **Layer 1 — Animated ASCII background**: full canvas tiled with random chars from the ramp (skip last 3 chars), color `(70, 105, 230)`. Different chars per frame produce flicker.
3. **Layer 2 — White shapes**: at every `GRID_STEP=27` px point, choose circle or square (50/50, seeded per frame). Size mapped from inverted local luminance.

## Luminance → Radius Formula
```python
inv_lum = 1.0 - avg_lum                              # dark areas = bigger shapes
r = int(MIN_RADIUS + inv_lum * (MAX_RADIUS - MIN_RADIUS))
r = max(MIN_RADIUS, min(MAX_RADIUS, r))
```

## Per-Frame Seeding (animation feel)
```python
ascii_rng = random.Random(frame_idx * 137 + 7)   # ASCII flicker
shape_rng = random.Random(frame_idx * 31  + 13)  # shape circle/square choice
```
Consistent, non-repeating randomness across frames. For a single static image, use `frame_idx = 0`.

## Variations (reference only — default is the **approved** one)
| Variant | Settings |
|---|---|
| Dots only | Only circles, no squares |
| Dots + squares (1×) | `GRID_STEP=18`, `MAX_RADIUS=8` |
| **Dots + squares (1.5×) — APPROVED DEFAULT** | `GRID_STEP=27`, `MAX_RADIUS=12` |
| Dots + squares (2×) | `GRID_STEP=36`, `MAX_RADIUS=16` (too big) |
| Inverted (blue bg) | `(36,73,209)` bg, white shapes — approved |
| Original (grey bg) | `(240,240,240)` bg, blue shapes |

Use the **approved default** unless the user explicitly requests another variant.

---

# Agent Execution Protocol

When the user provides an input image and requests an effect:

1. **Identify the effect** (`ascii_pixel` or `dot_shape`). Ask only if ambiguous.
2. **Write a Python script** in the working directory implementing the exact steps and parameters above. Required dependencies:
   - `Pillow` (PIL)
   - `rembg` (and `onnxruntime`)
   - `numpy`
   - For `dot_shape` video: `ffmpeg` on PATH.
3. **Run the script** against the user's input image path.
4. **Output**:
   - `ascii_pixel` → an `.html` file (self-contained, layers + JS) plus the blurred BG image it references (or inline as base64).
   - `dot_shape` → a `.png` (static) or `.mp4` (video).
5. **Return the output path** to the user. Do not embed the result in chat unless asked.

## Hard Rules
- Do **not** alter parameter values listed in the tables.
- Do **not** add a luminance supplement to the rembg mask in `ascii_pixel`.
- Do **not** invent new layers, blends, or post-processing not described here.
- Preserve the ASCII ramp exactly: `@#S08Xox+=;:-,. ` (trailing space included).
- Font path is macOS-style; on other OSes fall back to any available `Courier New Bold` / monospace bold TTF, but keep the visual weight equivalent.
- Subject detection is `rembg` only.

## Minimal Script Skeletons (guidance, not literal)

**ascii_pixel** outline:
```
load -> resize(900, LANCZOS)
bg = blur(14) -> darken(0.65) -> desaturate(0.5) -> pixelate(BOX↓, NEAREST↑)
mask = rembg(image)
canvas = bg.copy()
draw_grid_overlay(canvas, GRID_STEP=24, alpha=0.05, subject_only=True)
for each 11x14 cell:
    sample avg RGB, lum, mask_mean
    if mask_mean > 0.25:
        char = ramp[int((1-lum)*(len(ramp)-1))]
        color = normalize_color(r,g,b)
        if lum > 0.45: draw glow halo first
    else:
        char = '.'; color = (40,65,100) at 30% opacity
    draw_text(canvas, cell_xy, char, color, mono_bold_font)
save canvas as PNG
```

**dot_shape** outline:
```
load -> fit to 900x900
mask = rembg(image), threshold at 30
ascii_rng = Random(frame*137+7); shape_rng = Random(frame*31+13)
draw BG_COLOR fill
for each ASCII_CELL=12 grid point: draw random ramp char in ASCII_COLOR
for each GRID_STEP=27 point inside mask:
    avg_lum = local luminance
    r = clamp(MIN_RADIUS + (1-avg_lum)*(MAX_RADIUS-MIN_RADIUS))
    if shape_rng.random() < 0.5: draw circle else square, SHAPE_COLOR
save output
```
