docs: add filter retune design spec

This commit is contained in:
Keith Solomon
2026-08-06 14:35:47 -05:00
parent bf3ee12e51
commit b774a83dfc
@@ -0,0 +1,122 @@
# Filter Retune Design
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the warm, cool, vivid, and fade filter presets visibly distinct, so the eight-preset grid presents as a useful set of choices rather than four near-duplicates.
**User-reported problem:** On the screenshot at `http://basic-wp.test/blocks/solofilters-block/`, the four "color" presets (warm, cool, vivid, fade) all look almost identical to the unfiltered image. Only mono, dramatic, and sepia read as clearly different.
**Scope:** Retune exactly four presets. Do not touch mono, dramatic, sepia, or normal.
**Tech Stack:** WordPress 6.4+, Gutenberg block editor, `@wordpress/scripts` (webpack + Babel + SCSS), Jest (unit tests).
## Constraints
- Plugin slug: `image-filters`
- Plugin text domain: `image-filters`
- Block namespace: `ksolo`
- Block name: `ksolo/image-filter`
- `src/presets.js` is the source of truth for the canonical `cssFilter` string per preset.
- `src/styles/style.scss` compiles to the CSS WordPress serves. The intensity-scaling math (documented in the file header) has two patterns:
- **Additive bias** for values where identity is 1.0: `calc( bias + 1 * var(--filter-intensity) )` so at intensity=1 the value is `bias + 1`, matching the canonical cssFilter.
- **Direct multiply** for values where identity is 0: `calc( value * var(--filter-intensity) )` so at intensity=1 the value is the literal `value`.
- **Contract:** At intensity=1 the SCSS expression evaluates to the canonical `cssFilter` string. This must continue to hold.
- All user-facing strings pass through `__()` / `_e()` with the `image-filters` text domain. No new user-facing strings in this change.
## New `cssFilter` values (Option B — Instagram look)
| Preset | New `cssFilter` |
|---|---|
| warm | `saturate(1.5) sepia(0.6) brightness(1.08) contrast(1.1) hue-rotate(-8deg)` |
| cool | `saturate(0.85) hue-rotate(-30deg) brightness(0.95) contrast(1.08)` |
| vivid | `saturate(2.0) contrast(1.3) brightness(1.0) hue-rotate(-5deg)` |
| fade | `saturate(0.6) contrast(0.85) brightness(1.15) sepia(0.18)` |
**Rationale:** Each preset stays in its conceptual lane (warm stays warm, cool stays cool, vivid stays vibrant, fade stays washed) but the magnitudes are pushed to where they're clearly visible — comparable to the look on the bold presets in the user's screenshot. The hue-rotate additions on warm, cool, and vivid nudge the color cast in the right direction without changing the preset's identity.
## SCSS mirror (preserves the intensity=1 contract)
```scss
.wp-block-ksolo-image-filter {
.ksolo-image-filter-img.has-filter-warm {
filter: saturate( calc( 0.5 + 1 * var(--filter-intensity) ) )
sepia( calc( 0.6 * var(--filter-intensity) ) )
brightness( calc( 0.08 + 1 * var(--filter-intensity) ) )
contrast( calc( 0.1 + 1 * var(--filter-intensity) ) )
hue-rotate( calc( -8deg * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-cool {
filter: saturate( calc( 0.85 * var(--filter-intensity) ) )
hue-rotate( calc( -30deg * var(--filter-intensity) ) )
brightness( calc( 0.95 * var(--filter-intensity) ) )
contrast( calc( 0.08 + 1 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-vivid {
filter: saturate( calc( 1 + 1 * var(--filter-intensity) ) )
contrast( calc( 0.3 + 1 * var(--filter-intensity) ) )
brightness( calc( 0 + 1 * var(--filter-intensity) ) )
hue-rotate( calc( -5deg * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-fade {
filter: saturate( calc( 0.6 * var(--filter-intensity) ) )
contrast( calc( 0.85 * var(--filter-intensity) ) )
brightness( calc( 0.15 + 1 * var(--filter-intensity) ) )
sepia( calc( 0.18 * var(--filter-intensity) ) );
}
// mono, dramatic, sepia: unchanged
}
```
Pattern-per-component check (confirms the intensity=1 contract):
- **warm**
- `saturate(1.5)`: 0.5 + 1.0 → additive bias ✓
- `sepia(0.6)`: 0.6 × 1.0 → direct multiply ✓
- `brightness(1.08)`: 0.08 + 1.0 → additive bias ✓
- `contrast(1.1)`: 0.1 + 1.0 → additive bias ✓
- `hue-rotate(-8deg)`: -8deg × 1.0 → direct multiply ✓
- **cool**
- `saturate(0.85)`: < 1.0 → direct multiply ✓
- `hue-rotate(-30deg)`: direct multiply ✓
- `brightness(0.95)`: < 1.0 → direct multiply ✓
- `contrast(1.08)`: > 1.0 → additive bias ✓
- **vivid**
- `saturate(2.0)`: 1 + 1.0 → additive bias ✓
- `contrast(1.3)`: 0.3 + 1.0 → additive bias ✓
- `brightness(1.0)`: 0 + 1.0 → additive bias (zero bias keeps the value at identity) ✓
- `hue-rotate(-5deg)`: direct multiply ✓
- **fade**
- `saturate(0.6)`: < 1.0 → direct multiply ✓
- `contrast(0.85)`: < 1.0 → direct multiply ✓
- `brightness(1.15)`: > 1.0 → additive bias ✓
- `sepia(0.18)`: direct multiply ✓
## Out of scope
- No changes to `inspector.js`, `edit.js`, `save.js`, `transforms.js`, `index.js`, or `block.json`.
- No changes to mono, dramatic, sepia, normal.
- No changes to intensity math or to the intensity slider default.
- No changes to the swatch `color` in presets.js — the swatch is a UI hint, not a literal pixel sample, and the user did not flag it as wrong.
## Testing
1. **Existing tests pass unchanged**`tests/jest/presets.test.js`, `tests/jest/transforms.test.js`, `tests/jest/inspector.test.js`, `tests/jest/edit.test.js`, `tests/jest/styles.test.js` should all stay green.
2. **New lockstep test** (`tests/jest/lockstep.test.js`): for every preset in `PRESETS`, read the compiled `build/style-index.css` and assert the rule's `filter:` declaration contains the canonical `cssFilter` token sequence. Tokenisation: split the cssFilter string on whitespace, then split each token on `(` and `)` so the function name and argument are tracked separately. The compiled CSS rule for the preset must contain a token whose name+argument matches every token in the canonical cssFilter (the order of tokens must also match, because the SCSS rule writes them in the same order). This pins the source-of-truth contract: if `presets.js` and `style.scss` ever drift, the test fails.
3. **Manual visual confirmation** is the user's job — it requires a real WordPress site and an image. The dev's local Herd install at `http://basic-wp.test/blocks/solofilters-block/` is the verification target.
## Files to change
- `src/presets.js` — update the four `cssFilter` strings.
- `src/styles/style.scss` — update the four filter rule bodies; keep all other rules unchanged.
- `tests/jest/lockstep.test.js` — new file.
- `build/style-index.css` — regenerated by `npm run build`. No new dependencies.
## Verification
1. `npx wp-scripts test-unit-js` — all existing tests pass plus the new lockstep test.
2. `npm run build` — succeeds with no warnings beyond the existing Sass deprecation notice.
3. Manual: load the existing page, open each preset in the sidebar, confirm warm/cool/vivid/fade now read as clearly different from normal and from each other.