+
;
+export type FiltersProps = DeepPartial;
export const DEFAULT_DUOTONE_1 = "#FF0000";
export const DEFAULT_DUOTONE_2 = "#0099FF";
+const DEFAULT_QUICK_SETTINGS = {
+ desaturate : false,
+ invert : false,
+ whiteBalance : false,
+};
+
+const DEFAULT_DUOTONE = {
+ enabled: false,
+ color1: DEFAULT_DUOTONE_1,
+ color2: DEFAULT_DUOTONE_2,
+};
+
+const DEFAULT_HSL = {
+ hue: 0,
+ sat: 0,
+ lightness: 0,
+};
+
let defaultFilters: Filters | null = null;
const FiltersFactory = {
@@ -40,18 +59,9 @@ const FiltersFactory = {
contrast = 0,
vibrance = .5,
threshold = -1, // -1 == off, working range is 0 - 255
- desaturate = false,
- invert = false,
- duotone = {
- enabled: false,
- color1: DEFAULT_DUOTONE_1,
- color2: DEFAULT_DUOTONE_2,
- },
- hsl = {
- hue: 0,
- sat: 0,
- lightness: 0,
- },
+ quick = DEFAULT_QUICK_SETTINGS,
+ duotone = DEFAULT_DUOTONE,
+ hsl = DEFAULT_HSL,
blur = 0,
}: FiltersProps = {}): Filters {
return {
@@ -61,12 +71,20 @@ const FiltersFactory = {
gamma,
brightness,
contrast,
- desaturate,
- invert,
vibrance,
threshold,
- duotone,
- hsl,
+ quick: {
+ ...DEFAULT_QUICK_SETTINGS,
+ ...quick
+ },
+ hsl: {
+ ...DEFAULT_HSL,
+ ...hsl,
+ },
+ duotone: {
+ ...DEFAULT_DUOTONE,
+ ...duotone,
+ },
blur,
};
},
@@ -76,7 +94,7 @@ const FiltersFactory = {
* for project storage
*/
serialize( filters: Filters ): any {
- const { duotone, hsl } = filters;
+ const { duotone, hsl, quick } = filters;
return {
e: filters.enabled,
m: filters.blendMode,
@@ -84,8 +102,9 @@ const FiltersFactory = {
g: filters.gamma,
b: filters.brightness,
c: filters.contrast,
- d: filters.desaturate,
- i: filters.invert,
+ d: quick.desaturate,
+ i: quick.invert,
+ w: quick.whiteBalance,
v: filters.vibrance,
t: filters.threshold,
de: duotone.enabled,
@@ -103,30 +122,33 @@ const FiltersFactory = {
* inside a stored projects layer
*/
deserialize( filters: any = {} ): Filters {
- return FiltersFactory.create({
- enabled: filters.e,
- blendMode: filters.m,
- opacity: filters.o,
- gamma: filters.g,
- brightness: filters.b,
- contrast: filters.c,
- desaturate: filters.d,
- vibrance: filters.v,
- threshold: filters.t,
- // these can be undefined as these were added in later app versions
- invert: filters.i ?? false,
- duotone: {
+ // nullish coalescing fallbacks as some properties were added in later app versions
+ return FiltersFactory.create({
+ enabled: filters.e,
+ blendMode: filters.m,
+ opacity: filters.o,
+ gamma: filters.g,
+ brightness: filters.b,
+ contrast: filters.c,
+ vibrance: filters.v,
+ threshold: filters.t,
+ quick: {
+ desaturate: filters.d,
+ invert: filters.i ?? false,
+ whiteBalance: filters.w ?? false,
+ },
+ duotone: {
enabled: filters.de ?? false,
color1: filters.d1 ?? DEFAULT_DUOTONE_1,
color2: filters.d2 ?? DEFAULT_DUOTONE_2,
- },
- hsl: {
+ },
+ hsl: {
hue: filters.hh ?? 0,
sat: filters.hs ?? 0,
lightness: filters.hl ?? 0,
- },
- blur: filters.bl,
- });
+ },
+ blur: filters.bl,
+ });
}
};
export default FiltersFactory;
@@ -151,15 +173,20 @@ export const isEqual = ( filters: Filters, filtersToCompareTo?: Filters ): boole
filters.gamma === filtersToCompareTo.gamma &&
filters.brightness === filtersToCompareTo.brightness &&
filters.contrast === filtersToCompareTo.contrast &&
- filters.desaturate === filtersToCompareTo.desaturate &&
- filters.invert === filtersToCompareTo.invert &&
filters.vibrance === filtersToCompareTo.vibrance &&
filters.threshold === filtersToCompareTo.threshold &&
+ // quick filters
+ filters.quick.desaturate === filtersToCompareTo.quick.desaturate &&
+ filters.quick.invert === filtersToCompareTo.quick.invert &&
+ filters.quick.whiteBalance === filtersToCompareTo.quick.whiteBalance &&
+ // duotone
filters.duotone.enabled === filtersToCompareTo.duotone.enabled &&
filters.duotone.color1 === filtersToCompareTo.duotone.color1 &&
filters.duotone.color2 === filtersToCompareTo.duotone.color2 &&
+ // HSL
filters.hsl.hue === filtersToCompareTo.hsl.hue &&
filters.hsl.sat === filtersToCompareTo.hsl.sat &&
filters.hsl.lightness === filtersToCompareTo.hsl.lightness &&
+ // blur
filters.blur === filtersToCompareTo.blur;
};
diff --git a/src/model/types/_util.ts b/src/model/types/_util.ts
new file mode 100644
index 0000000..40634fa
--- /dev/null
+++ b/src/model/types/_util.ts
@@ -0,0 +1,13 @@
+export type DeepPartial = T extends Function
+ ? T
+ : T extends Array
+ ? _DeepPartialArray
+ : T extends object
+ ? _DeepPartialObject
+ : T | undefined;
+
+type _DeepPartialArray = Array>;
+
+type _DeepPartialObject = {
+ [Key in keyof T]?: DeepPartial;
+};
diff --git a/src/model/types/filters.ts b/src/model/types/filters.ts
index 0edf28f..81310ac 100644
--- a/src/model/types/filters.ts
+++ b/src/model/types/filters.ts
@@ -22,6 +22,24 @@
*/
import type { BlendModes } from "@/definitions/blend-modes";
+type DuoToneFilter = {
+ enabled: boolean;
+ color1?: string;
+ color2?: string;
+};
+
+type QuickFilter = {
+ desaturate: boolean;
+ invert: boolean;
+ whiteBalance: boolean;
+};
+
+type HSLFilter = {
+ hue: number, // -180 to +180 range (degrees)
+ sat: number, // -1 to +1 range
+ lightness: number, // -1 to +1 range
+};
+
export type Filters = {
enabled: boolean;
blendMode: BlendModes;
@@ -31,17 +49,8 @@ export type Filters = {
contrast: number;
vibrance: number;
threshold: number;
- desaturate: boolean;
- invert: boolean;
- duotone: {
- enabled: boolean;
- color1?: string;
- color2?: string;
- };
- hsl: {
- hue: number, // -180 to +180 range (degrees)
- sat: number, // -1 to +1 range
- lightness: number, // -1 to +1 range
- };
+ quick: QuickFilter;
+ duotone: DuoToneFilter;
+ hsl: HSLFilter;
blur: number;
-};
\ No newline at end of file
+};
diff --git a/src/rendering/filters/adjustments.ts b/src/rendering/filters/adjustments.ts
index e8e49ad..9ea6a9a 100644
--- a/src/rendering/filters/adjustments.ts
+++ b/src/rendering/filters/adjustments.ts
@@ -33,6 +33,9 @@ const defaultFilters = FiltersFactory.create();
/**
* Apply generic adjustment filters (brightness, contrast, gamma, vibrance, threshold)
* onto provided RGBA pixel Array.
+ *
+ * These can be combined in a single function as these filters do not
+ * require an initial pass (for instant to calculate averages, etc.)
*/
export const applyAdjustments = ( pixels: Uint8ClampedArray, filters: Filters ): void =>
{
@@ -41,18 +44,19 @@ export const applyAdjustments = ( pixels: Uint8ClampedArray, filters: Filters ):
const gamma = filters.gamma * 2; // 0 to 2 range
const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range
- const { desaturate, invert, threshold } = filters;
-
- let r, g, b, a;
- let grayScale, max, avg, amt;
- const gammaSquared = gamma * gamma;
-
const doBrightness = filters.brightness !== defaultFilters.brightness;
const doContrast = filters.contrast !== defaultFilters.contrast;
const doGamma = filters.gamma !== defaultFilters.gamma;
const doVibrance = filters.vibrance !== defaultFilters.vibrance;
const doThreshold = filters.threshold !== defaultFilters.threshold;
- const doInvert = filters.invert !== defaultFilters.invert;
+ const doInvert = filters.quick.invert;
+ const doDesaturate = filters.quick.desaturate;
+
+ const { threshold } = filters;
+
+ let r, g, b, a;
+ let grayScale, max, avg, amt;
+ const gammaSquared = gamma * gamma;
// loop through the pixels, note we increment the iterator by four
// as each pixel is defined by four RGBA channel values : red, green, blue and the alpha channel
@@ -84,7 +88,7 @@ export const applyAdjustments = ( pixels: Uint8ClampedArray, filters: Filters ):
}
// desaturate
- if ( desaturate ) {
+ if ( doDesaturate ) {
grayScale = r * 0.3 + g * 0.59 + b * 0.11;
r = grayScale;
g = grayScale;
diff --git a/src/rendering/filters/hsl.ts b/src/rendering/filters/hsl.ts
index 4b1c197..bf1a76f 100644
--- a/src/rendering/filters/hsl.ts
+++ b/src/rendering/filters/hsl.ts
@@ -22,8 +22,6 @@
*/
import { RGBtoHSL, HSLtoRGB } from "@/utils/color-util";
-const HALF_PI = Math.PI / 2;
-
/**
* Applies HSL balancing onto provided data
*
diff --git a/src/rendering/filters/white-balance.ts b/src/rendering/filters/white-balance.ts
new file mode 100644
index 0000000..e7531df
--- /dev/null
+++ b/src/rendering/filters/white-balance.ts
@@ -0,0 +1,33 @@
+export const applyWhiteBalance = ( pixels: Uint8ClampedArray ): void => {
+ const totalPixels = pixels.length / 4;
+
+ let totalR = 0, totalG = 0, totalB = 0;
+
+ // Step 1: Calculate the sum of all RGB values
+ for ( let i = 0, len = pixels.length; i < len; i += 4) {
+ totalR += pixels[ i ];
+ totalG += pixels[ i + 1 ];
+ totalB += pixels[ i + 2 ];
+ }
+
+ // Step 2: Find the average value for each channel
+ const avgR = totalR / totalPixels;
+ const avgG = totalG / totalPixels;
+ const avgB = totalB / totalPixels;
+
+ // Step 3: Calculate the overall average brightness
+ const avgGray = ( avgR + avgG + avgB ) / 3;
+
+ // Step 4: Calculate scaling factors for each channel
+ const scaleR = avgGray / avgR;
+ const scaleG = avgGray / avgG;
+ const scaleB = avgGray / avgB;
+
+ // Step 5: Apply the scaling factors to every pixel
+ for ( let i = 0, len = pixels.length; i < len; i += 4 ) {
+ pixels[i] = Math.min(255, pixels[i] * scaleR); // New Red
+ pixels[i + 1] = Math.min(255, pixels[i + 1] * scaleG); // New Green
+ pixels[i + 2] = Math.min(255, pixels[i + 2] * scaleB); // New Blue
+ // data[i+3] is Alpha (transparency), leave it unchanged
+ }
+};
diff --git a/src/styles/form.scss b/src/styles/form.scss
index 515d3d7..043b13d 100644
--- a/src/styles/form.scss
+++ b/src/styles/form.scss
@@ -28,6 +28,14 @@ $inputWidth: 65%;
margin-bottom: variables.$spacing-small;
}
+ &--toggle {
+ justify-content: space-between;
+
+ &.wrapper--full {
+ justify-content: initial;
+ }
+ }
+
// use where there is no