diff --git a/src/presets.js b/src/presets.js index 1929c77..8e768b4 100644 --- a/src/presets.js +++ b/src/presets.js @@ -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', diff --git a/src/styles/style.scss b/src/styles/style.scss index bd21523..38cb245 100644 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -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
carries `wp-block-ksolo-image-filter` + * AND the `has-filter-` class on the same element (see edit.js and + * save.js), and the inner ALSO carries `ksolo-image-filter-img` and + * the same `has-filter-` class. We target the by its stable, + * always-present pair of classes (`.ksolo-image-filter-img.has-filter-`) + * 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) ) ); } } diff --git a/tests/jest/__snapshots__/intensity.test.js.snap b/tests/jest/__snapshots__/intensity.test.js.snap index 5fdfbc5..fafafa2 100644 --- a/tests/jest/__snapshots__/intensity.test.js.snap +++ b/tests/jest/__snapshots__/intensity.test.js.snap @@ -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
carries \`wp-block-ksolo-image-filter\` + * AND the \`has-filter-\` class on the same element (see edit.js and + * save.js), and the inner ALSO carries \`ksolo-image-filter-img\` and + * the same \`has-filter-\` class. We target the by its stable, + * always-present pair of classes (\`.ksolo-image-filter-img.has-filter-\`) + * 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) ) ); } } "