docs: add filter retune design spec
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user