fix: use linear interpolation for intensity so intensity=0 is filter:none

This commit is contained in:
Keith Solomon
2026-08-06 15:51:41 -05:00
parent 4572abfcd1
commit b738d7d6da
3 changed files with 77 additions and 61 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ export const PRESETS = [
slug: 'vivid',
label: 'Vivid',
color: '#e63946',
cssFilter: 'saturate(2.0) contrast(1.3) brightness(1.0) hue-rotate(-5deg)',
cssFilter: 'saturate(2.0) contrast(1.3) hue-rotate(-5deg)',
},
{
slug: 'fade',
+38 -30
View File
@@ -5,20 +5,29 @@
* src/presets.js. The cssFilter value in presets.js is the source of truth;
* the rules below reproduce it. If they diverge, treat it as a bug.
*
* Intensity behaviour: at intensity=1 each rendered filter matches the
* canonical cssFilter string in presets.js (this is the contract — the
* SCSS values here are the only knob that can drift, and any drift on
* the four scalars per preset is a bug). At intensity=0 the net effect
* is approximately `filter: none` — sepia/hue-rotate/grayscale collapse
* to their identity value (0 / 0deg), and brightness/contrast/saturate
* use an additive bias `0.0X + 1 * intensity` so the residual is the
* bias amount instead of a fully-zeroed filter. Where the canonical
* value sits below 1 (saturate(0.95), contrast(0.9), brightness(0.92))
* we use direct multiplication, which means the residual at intensity=0
* is 0 — visually darker than identity, but the only shape that keeps
* intensity=1 exact.
* Intensity behaviour: each scalar where identity=1 (saturate, contrast,
* brightness) is interpolated linearly between identity (1) at
* intensity=0 and the canonical value (v) at intensity=1:
*
* @package SoloFiltersImageEnhancements
* - if v > 1: calc(1 + (v - 1) * var(--filter-intensity))
* - if v < 1: calc(1 - (1 - v) * var(--filter-intensity))
* - if v = 1: identity (no filter component needed)
*
* Zero-identity scalars (sepia, hue-rotate, grayscale) use direct
* multiply: calc(v * var(--filter-intensity)). At intensity=0 they
* collapse to 0, which is the identity value for these properties.
*
* Net effect: at intensity=0 every rule evaluates to `filter: none`
* (identity is identity for every component). At intensity=1 every
* rule evaluates to the canonical cssFilter for the active preset.
*
* Selector shape: the wrapper <figure> carries `wp-block-ksolo-image-filter`
* AND the `has-filter-<slug>` class on the same element (see edit.js and
* save.js), and the inner <img> ALSO carries `ksolo-image-filter-img` and
* the same `has-filter-<slug>` class. We target the <img> by its stable,
* always-present pair of classes (`.ksolo-image-filter-img.has-filter-<slug>`)
* scoped under the wrapper so the rule still applies when Gutenberg's
* `useBlockProps` adds additional wrapper-level classes.
*/
.wp-block-ksolo-image-filter {
@@ -50,47 +59,46 @@
// additional wrapper-level classes.
.wp-block-ksolo-image-filter {
.ksolo-image-filter-img.has-filter-warm {
filter: saturate( calc( 0.5 + 1 * var(--filter-intensity) ) )
filter: saturate( calc( 1 + 0.5 * 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) ) )
brightness( calc( 1 + 0.08 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.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) ) )
filter: saturate( calc( 1 - 0.15 * 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) ) );
brightness( calc( 1 - 0.05 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.08 * 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) ) )
contrast( calc( 1 + 0.3 * 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) ) )
filter: saturate( calc( 1 - 0.4 * var(--filter-intensity) ) )
contrast( calc( 1 - 0.15 * var(--filter-intensity) ) )
brightness( calc( 1 + 0.15 * var(--filter-intensity) ) )
sepia( calc( 0.18 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-mono {
filter: grayscale( calc( 1 * var(--filter-intensity) ) ) contrast( calc( 0.05 + 1 * var(--filter-intensity) ) );
filter: grayscale( calc( 1 * var(--filter-intensity) ) ) contrast( calc( 1 + 0.05 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-dramatic {
filter: contrast( calc( 0.35 + 1 * var(--filter-intensity) ) )
saturate( calc( 0.15 + 1 * var(--filter-intensity) ) )
brightness( calc( 0.92 * var(--filter-intensity) ) );
filter: contrast( calc( 1 + 0.35 * var(--filter-intensity) ) )
saturate( calc( 1 + 0.15 * var(--filter-intensity) ) )
brightness( calc( 1 - 0.08 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-sepia {
filter: sepia( calc( 0.85 * var(--filter-intensity) ) )
saturate( calc( 0.1 + 1 * var(--filter-intensity) ) )
contrast( calc( 0.05 + 1 * var(--filter-intensity) ) );
saturate( calc( 1 + 0.1 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.05 * var(--filter-intensity) ) );
}
}
+38 -30
View File
@@ -8,20 +8,29 @@ exports[`src/styles/style.scss matches the snapshot (intensity math shape is loc
* src/presets.js. The cssFilter value in presets.js is the source of truth;
* the rules below reproduce it. If they diverge, treat it as a bug.
*
* Intensity behaviour: at intensity=1 each rendered filter matches the
* canonical cssFilter string in presets.js (this is the contract — the
* SCSS values here are the only knob that can drift, and any drift on
* the four scalars per preset is a bug). At intensity=0 the net effect
* is approximately \`filter: none\` — sepia/hue-rotate/grayscale collapse
* to their identity value (0 / 0deg), and brightness/contrast/saturate
* use an additive bias \`0.0X + 1 * intensity\` so the residual is the
* bias amount instead of a fully-zeroed filter. Where the canonical
* value sits below 1 (saturate(0.95), contrast(0.9), brightness(0.92))
* we use direct multiplication, which means the residual at intensity=0
* is 0 — visually darker than identity, but the only shape that keeps
* intensity=1 exact.
* Intensity behaviour: each scalar where identity=1 (saturate, contrast,
* brightness) is interpolated linearly between identity (1) at
* intensity=0 and the canonical value (v) at intensity=1:
*
* @package SoloFiltersImageEnhancements
* - if v > 1: calc(1 + (v - 1) * var(--filter-intensity))
* - if v < 1: calc(1 - (1 - v) * var(--filter-intensity))
* - if v = 1: identity (no filter component needed)
*
* Zero-identity scalars (sepia, hue-rotate, grayscale) use direct
* multiply: calc(v * var(--filter-intensity)). At intensity=0 they
* collapse to 0, which is the identity value for these properties.
*
* Net effect: at intensity=0 every rule evaluates to \`filter: none\`
* (identity is identity for every component). At intensity=1 every
* rule evaluates to the canonical cssFilter for the active preset.
*
* Selector shape: the wrapper <figure> carries \`wp-block-ksolo-image-filter\`
* AND the \`has-filter-<slug>\` class on the same element (see edit.js and
* save.js), and the inner <img> ALSO carries \`ksolo-image-filter-img\` and
* the same \`has-filter-<slug>\` class. We target the <img> by its stable,
* always-present pair of classes (\`.ksolo-image-filter-img.has-filter-<slug>\`)
* scoped under the wrapper so the rule still applies when Gutenberg's
* \`useBlockProps\` adds additional wrapper-level classes.
*/
.wp-block-ksolo-image-filter {
@@ -53,48 +62,47 @@ exports[`src/styles/style.scss matches the snapshot (intensity math shape is loc
// additional wrapper-level classes.
.wp-block-ksolo-image-filter {
.ksolo-image-filter-img.has-filter-warm {
filter: saturate( calc( 0.5 + 1 * var(--filter-intensity) ) )
filter: saturate( calc( 1 + 0.5 * 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) ) )
brightness( calc( 1 + 0.08 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.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) ) )
filter: saturate( calc( 1 - 0.15 * 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) ) );
brightness( calc( 1 - 0.05 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.08 * 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) ) )
contrast( calc( 1 + 0.3 * 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) ) )
filter: saturate( calc( 1 - 0.4 * var(--filter-intensity) ) )
contrast( calc( 1 - 0.15 * var(--filter-intensity) ) )
brightness( calc( 1 + 0.15 * var(--filter-intensity) ) )
sepia( calc( 0.18 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-mono {
filter: grayscale( calc( 1 * var(--filter-intensity) ) ) contrast( calc( 0.05 + 1 * var(--filter-intensity) ) );
filter: grayscale( calc( 1 * var(--filter-intensity) ) ) contrast( calc( 1 + 0.05 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-dramatic {
filter: contrast( calc( 0.35 + 1 * var(--filter-intensity) ) )
saturate( calc( 0.15 + 1 * var(--filter-intensity) ) )
brightness( calc( 0.92 * var(--filter-intensity) ) );
filter: contrast( calc( 1 + 0.35 * var(--filter-intensity) ) )
saturate( calc( 1 + 0.15 * var(--filter-intensity) ) )
brightness( calc( 1 - 0.08 * var(--filter-intensity) ) );
}
.ksolo-image-filter-img.has-filter-sepia {
filter: sepia( calc( 0.85 * var(--filter-intensity) ) )
saturate( calc( 0.1 + 1 * var(--filter-intensity) ) )
contrast( calc( 0.05 + 1 * var(--filter-intensity) ) );
saturate( calc( 1 + 0.1 * var(--filter-intensity) ) )
contrast( calc( 1 + 0.05 * var(--filter-intensity) ) );
}
}
"