|
3 | 3 | * Works in all webkit2gtk versions. |
4 | 4 | */ |
5 | 5 |
|
| 6 | +import {hexToRgb, rgbToHsl01, hslToRgb01} from './color'; |
| 7 | + |
6 | 8 | export interface Filters { |
7 | 9 | blur: number; |
8 | 10 | brightness: number; |
@@ -71,6 +73,10 @@ export function hasActiveCrop(f: Filters): boolean { |
71 | 73 | return f.cropX !== 0 || f.cropY !== 0 || f.cropW !== 1 || f.cropH !== 1; |
72 | 74 | } |
73 | 75 |
|
| 76 | +export function hasPaletteGrade(f: Filters): boolean { |
| 77 | + return f.paletteStops.length >= 2 && f.paletteStrength > 0; |
| 78 | +} |
| 79 | + |
74 | 80 | export function applyFilters( |
75 | 81 | img: HTMLImageElement, |
76 | 82 | filters: Filters, |
@@ -139,10 +145,9 @@ export function applyFilters( |
139 | 145 |
|
140 | 146 | // Build the palette-grade LUT once, if needed. Output depends only on source |
141 | 147 | // luminance, so a 256-entry table captures the entire tone-map grade. |
142 | | - const paletteLUT = |
143 | | - filters.paletteStops.length >= 2 && filters.paletteStrength > 0 |
144 | | - ? buildPaletteLUT(filters.paletteStops) |
145 | | - : null; |
| 148 | + const paletteLUT = hasPaletteGrade(filters) |
| 149 | + ? buildPaletteLUT(filters.paletteStops) |
| 150 | + : null; |
146 | 151 |
|
147 | 152 | // Single-pass per-pixel adjustments |
148 | 153 | const needsPixel = |
@@ -297,11 +302,12 @@ export function applyFilters( |
297 | 302 | b += lift * (1 - b / 255); |
298 | 303 | } |
299 | 304 |
|
300 | | - // Palette grade — map this pixel's luminance to the ramp color and |
301 | | - // blend. The LUT already encodes "hue+sat from ramp, lightness from src", |
302 | | - // so we just index by source luminance and lerp by strength. |
| 305 | + // Palette grade — LUT is indexed by HSL lightness (max+min)/2, |
| 306 | + // consistent with how the ramp was built. |
303 | 307 | if (paletteLUT) { |
304 | | - const y = (0.2126 * r + 0.7152 * g + 0.0722 * b) | 0; |
| 308 | + const cMax = r > g ? (r > b ? r : b) : g > b ? g : b; |
| 309 | + const cMin = r < g ? (r < b ? r : b) : g < b ? g : b; |
| 310 | + const y = ((cMax + cMin) / 2) | 0; |
305 | 311 | const yi = y < 0 ? 0 : y > 255 ? 255 : y; |
306 | 312 | const base = yi * 3; |
307 | 313 | const t = paletteBlend; |
@@ -506,7 +512,7 @@ export function hasActiveFilters(f: Filters): boolean { |
506 | 512 | f.clarity > 0 || |
507 | 513 | f.posterize > 0 || |
508 | 514 | f.noise > 0 || |
509 | | - (f.paletteStops.length >= 2 && f.paletteStrength > 0) || |
| 515 | + hasPaletteGrade(f) || |
510 | 516 | hasActiveCrop(f) || |
511 | 517 | f.resizeW !== 0 || |
512 | 518 | f.resizeH !== 0 |
@@ -537,9 +543,12 @@ export function buildPaletteLUT(stops: string[]): Uint8ClampedArray | null { |
537 | 543 | // swatches in any order and the grade always runs shadow→highlight. |
538 | 544 | const hsl: {h: number; s: number; l: number}[] = []; |
539 | 545 | for (const hex of stops) { |
540 | | - const parsed = parseHexRgb(hex); |
541 | | - if (!parsed) return null; |
542 | | - hsl.push(rgbToHsl01(parsed.r, parsed.g, parsed.b)); |
| 546 | + const {r, g, b} = hexToRgb(hex); |
| 547 | + if (r === 0 && g === 0 && b === 0 && hex !== '#000000') { |
| 548 | + // hexToRgb returns {0,0,0} for invalid input |
| 549 | + return null; |
| 550 | + } |
| 551 | + hsl.push(rgbToHsl01(r, g, b)); |
543 | 552 | } |
544 | 553 | hsl.sort((a, b) => a.l - b.l); |
545 | 554 |
|
@@ -569,60 +578,6 @@ export function buildPaletteLUT(stops: string[]): Uint8ClampedArray | null { |
569 | 578 | return lut; |
570 | 579 | } |
571 | 580 |
|
572 | | -function parseHexRgb(hex: string): {r: number; g: number; b: number} | null { |
573 | | - if (!hex || typeof hex !== 'string') return null; |
574 | | - const h = hex.startsWith('#') ? hex.slice(1) : hex; |
575 | | - if (h.length !== 6) return null; |
576 | | - const n = parseInt(h, 16); |
577 | | - if (Number.isNaN(n)) return null; |
578 | | - return {r: (n >> 16) & 0xff, g: (n >> 8) & 0xff, b: n & 0xff}; |
579 | | -} |
580 | | - |
581 | | -// RGB (0-255) → HSL (h,s,l in 0..1) |
582 | | -function rgbToHsl01( |
583 | | - r: number, |
584 | | - g: number, |
585 | | - b: number |
586 | | -): {h: number; s: number; l: number} { |
587 | | - const rn = r / 255, |
588 | | - gn = g / 255, |
589 | | - bn = b / 255; |
590 | | - const max = Math.max(rn, gn, bn); |
591 | | - const min = Math.min(rn, gn, bn); |
592 | | - const l = (max + min) / 2; |
593 | | - let h = 0; |
594 | | - let s = 0; |
595 | | - if (max !== min) { |
596 | | - const d = max - min; |
597 | | - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); |
598 | | - if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; |
599 | | - else if (max === gn) h = ((bn - rn) / d + 2) / 6; |
600 | | - else h = ((rn - gn) / d + 4) / 6; |
601 | | - } |
602 | | - return {h, s, l}; |
603 | | -} |
604 | | - |
605 | | -// HSL (0..1) → RGB (0..1) |
606 | | -function hslToRgb01(h: number, s: number, l: number): [number, number, number] { |
607 | | - if (s === 0) return [l, l, l]; |
608 | | - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; |
609 | | - const p = 2 * l - q; |
610 | | - return [ |
611 | | - hue2rgb(p, q, h + 1 / 3), |
612 | | - hue2rgb(p, q, h), |
613 | | - hue2rgb(p, q, h - 1 / 3), |
614 | | - ]; |
615 | | -} |
616 | | - |
617 | | -function hue2rgb(p: number, q: number, t: number): number { |
618 | | - if (t < 0) t += 1; |
619 | | - if (t > 1) t -= 1; |
620 | | - if (t < 1 / 6) return p + (q - p) * 6 * t; |
621 | | - if (t < 1 / 2) return q; |
622 | | - if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; |
623 | | - return p; |
624 | | -} |
625 | | - |
626 | 581 | // Interpolate hue (0..1) along the shortest arc around the wheel. |
627 | 582 | function lerpHue(a: number, b: number, t: number): number { |
628 | 583 | let d = b - a; |
|
0 commit comments