diff --git a/src/components/layer-effects/layer-effects.vue b/src/components/layer-effects/layer-effects.vue
index 8fb3e49..3ef8bce 100644
--- a/src/components/layer-effects/layer-effects.vue
+++ b/src/components/layer-effects/layer-effects.vue
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
- * Igor Zinken 2021-2025 - https://www.igorski.nl
+ * Igor Zinken 2021-2026 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -120,6 +120,15 @@
:tooltip="'none'"
/>
+
+
+
+
+{
+ const invertedRadius = (( MAX_BLUR - radius ) / MAX_BLUR ) * 100;
+
+ const out = new Uint32Array( pixels.length );
+ const tmpLines = new Float32Array( Math.max( width, height ) * 4 );
+
+ const source = new Uint32Array( pixels.buffer );
+ const coeffs = gaussCoef( width / invertedRadius );
+
+ convolve( source, out, tmpLines, coeffs, width, height );
+ convolve( out, source, tmpLines, coeffs, height, width );
+};
+
+/* internal methods */
+
+/**
+ * Calculate gaussian blur using IRR filter
+ * https://software.intel.com/en-us/articles/iir-gaussian-blur-filter
+ */
+function gaussCoef( sigma: number ): Float32Array
+{
+ sigma = Math.max( sigma, 0.5 );
+
+ const a = Math.exp( 0.726 * 0.726 ) / sigma;
+ const g1 = Math.exp( -a );
+ const g2 = Math.exp( -2 * a );
+ const k = ( 1 - g1 ) * ( 1 - g1 ) / ( 1 + 2 * a * g1 - g2 );
+
+ const a0 = k;
+ const a1 = k * ( a - 1 ) * g1;
+ const a2 = k * ( a + 1 ) * g1;
+ const a3 = -k * g2;
+ const b1 = 2 * g1;
+ const b2 = -g2;
+ const leftCorner = ( a0 + a1 ) / ( 1 - b1 - b2 );
+ const rightCorner = ( a2 + a3 ) / ( 1 - b1 - b2 );
+
+ return new Float32Array([ a0, a1, a2, a3, b1, b2, leftCorner, rightCorner ]);
+}
+
+/**
+ * Blurs and transposes src into dest
+ */
+function convolve(
+ src: Uint32Array, dest: Uint32Array, lines: Float32Array, coeffs: Float32Array, width: number, height: number
+): void {
+ for ( let i = 0; i < height; i++ ) {
+ // RGBA values for source and destination pixel
+ let srcR: number, srcG: number, srcB: number, srcA: number;
+ let destR: number, destG: number, destB: number, destA: number;
+
+ let srcIndex = i * width;
+ let outIndex = i;
+ let lineIndex = 0;
+
+ // 1. left to right
+ let rgba = src[ srcIndex ];
+
+ let prevSrcR = rgba & 0xff;
+ let prevSrcG = ( rgba >> 8 ) & 0xff;
+ let prevSrcB = ( rgba >> 16 ) & 0xff;
+ let prevSrcA = ( rgba >> 24 ) & 0xff;
+
+ let prevPrevDestR = prevSrcR * coeffs[ 6 ];
+ let prevPrevDestG = prevSrcG * coeffs[ 6 ];
+ let prevPrevDestB = prevSrcB * coeffs[ 6 ];
+ let prevPrevDestA = prevSrcA * coeffs[ 6 ];
+
+ let prevDestR = prevPrevDestR;
+ let prevDestG = prevPrevDestG;
+ let prevDestB = prevPrevDestB;
+ let prevDestA = prevPrevDestA;
+
+ let a0 = coeffs[ 0 ];
+ let a1 = coeffs[ 1 ];
+ let b1 = coeffs[ 4 ];
+ let b2 = coeffs[ 5 ];
+
+ for ( let j = 0; j < width; j++ ) {
+ rgba = src[ srcIndex ];
+ srcR = rgba & 0xff;
+ srcG = ( rgba >> 8 ) & 0xff;
+ srcB = ( rgba >> 16 ) & 0xff;
+ srcA = ( rgba >> 24 ) & 0xff;
+
+ destR = srcR * a0 + prevSrcR * a1 + prevDestR * b1 + prevPrevDestR * b2;
+ destG = srcG * a0 + prevSrcG * a1 + prevDestG * b1 + prevPrevDestG * b2;
+ destB = srcB * a0 + prevSrcB * a1 + prevDestB * b1 + prevPrevDestB * b2;
+ destA = srcA * a0 + prevSrcA * a1 + prevDestA * b1 + prevPrevDestA * b2;
+
+ prevPrevDestR = prevDestR;
+ prevPrevDestG = prevDestG;
+ prevPrevDestB = prevDestB;
+ prevPrevDestA = prevDestA;
+
+ prevDestR = destR;
+ prevDestG = destG;
+ prevDestB = destB;
+ prevDestA = destA;
+
+ prevSrcR = srcR;
+ prevSrcG = srcG;
+ prevSrcB = srcB;
+ prevSrcA = srcA;
+
+ lines[ lineIndex ] = prevDestR;
+ lines[ lineIndex + 1 ] = prevDestG;
+ lines[ lineIndex + 2 ] = prevDestB;
+ lines[ lineIndex + 3 ] = prevDestA;
+
+ lineIndex += 4;
+ srcIndex++;
+ }
+
+ srcIndex--;
+ lineIndex -= 4;
+ outIndex += height * ( width - 1 );
+
+ // 2. right to left
+ rgba = src[ srcIndex ];
+
+ prevSrcR = rgba & 0xff;
+ prevSrcG = ( rgba >> 8 ) & 0xff;
+ prevSrcB = ( rgba >> 16 ) & 0xff;
+ prevSrcA = ( rgba >> 24 ) & 0xff;
+
+ prevPrevDestR = prevSrcR * coeffs[ 7 ];
+ prevPrevDestG = prevSrcG * coeffs[ 7 ];
+ prevPrevDestB = prevSrcB * coeffs[ 7 ];
+ prevPrevDestA = prevSrcA * coeffs[ 7 ];
+
+ prevDestR = prevPrevDestR;
+ prevDestG = prevPrevDestG;
+ prevDestB = prevPrevDestB;
+ prevDestA = prevPrevDestA;
+
+ srcR = prevSrcR;
+ srcG = prevSrcG;
+ srcB = prevSrcB;
+ srcA = prevSrcA;
+
+ a0 = coeffs[ 2 ];
+ a1 = coeffs[ 3 ];
+
+ for ( let j = width - 1; j >= 0; j-- ) {
+ destR = srcR * a0 + prevSrcR * a1 + prevDestR * b1 + prevPrevDestR * b2;
+ destG = srcG * a0 + prevSrcG * a1 + prevDestG * b1 + prevPrevDestG * b2;
+ destB = srcB * a0 + prevSrcB * a1 + prevDestB * b1 + prevPrevDestB * b2;
+ destA = srcA * a0 + prevSrcA * a1 + prevDestA * b1 + prevPrevDestA * b2;
+
+ prevPrevDestR = prevDestR;
+ prevPrevDestG = prevDestG;
+ prevPrevDestB = prevDestB;
+ prevPrevDestA = prevDestA;
+
+ prevDestR = destR;
+ prevDestG = destG;
+ prevDestB = destB;
+ prevDestA = destA;
+
+ prevSrcR = srcR;
+ prevSrcG = srcG;
+ prevSrcB = srcB;
+ prevSrcA = srcA;
+
+ rgba = src[ srcIndex ];
+ srcR = rgba & 0xff;
+ srcG = ( rgba >> 8 ) & 0xff;
+ srcB = ( rgba >> 16 ) & 0xff;
+ srcA = ( rgba >> 24 ) & 0xff;
+
+ rgba =
+ (( lines[ lineIndex ] + prevDestR ) << 0 ) +
+ (( lines[ lineIndex + 1 ] + prevDestG) << 8 ) +
+ (( lines[ lineIndex + 2 ] + prevDestB) << 16 ) +
+ (( lines[ lineIndex + 3 ] + prevDestA) << 24 );
+
+ dest[ outIndex ] = rgba;
+
+ srcIndex--;
+ lineIndex -= 4;
+ outIndex -= height;
+ }
+ }
+}
diff --git a/src/workers/filter.worker.ts b/src/workers/filter.worker.ts
index 02df318..6be62dd 100644
--- a/src/workers/filter.worker.ts
+++ b/src/workers/filter.worker.ts
@@ -25,6 +25,7 @@ import FiltersFactory from "@/factories/filters-factory";
import { imageDataAsFloat } from "@/utils/wasm-util";
import type { WasmFilterInstance } from "@/utils/wasm-util";
import { applyAdjustments } from "@/rendering/filters/adjustments";
+import { applyBlur } from "@/rendering/filters/blur";
import { applyDuotone } from "@/rendering/filters/duotone";
import wasmJs from "@/wasm/bin/filters.js";
@@ -80,6 +81,10 @@ self.addEventListener( "message", async ({ data }: MessageEvent ): Promise
function renderFilters( imageData: ImageData, filters: Filters ): Uint8ClampedArray {
const pixels = imageData.data;
+ if ( filters.blur > 0 ) {
+ applyBlur( pixels, imageData.width, imageData.height, filters.blur );
+ }
+
applyAdjustments( pixels, filters );
if ( filters.duotone.enabled ) {
@@ -107,6 +112,7 @@ function renderFiltersWasm( imageData: ImageData, filters: any ): Uint8ClampedAr
const doInvert = invert !== defaultFilters.invert;
const doThreshold = threshold !== defaultFilters.threshold;
const doDuotone = filters.duotone.enabled !== defaultFilters.duotone.enabled;
+ const doBlur = filters.blur > 0;
// run WASM operations
@@ -114,7 +120,7 @@ function renderFiltersWasm( imageData: ImageData, filters: any ): Uint8ClampedAr
wasmInstance._filter(
memory, length,
gamma, brightness, contrast, vibrance,/* threshold, duotone.color1, duotone.color2 */
- doGamma, desaturate, doBrightness, doContrast, doVibrance/*, doThreshold, doDuotone*/
+ doGamma, desaturate, doBrightness, doContrast, doVibrance/*, doThreshold, doDuotone, doBlur*/
);
});
}
diff --git a/tests/unit/factories/filters-factory.spec.ts b/tests/unit/factories/filters-factory.spec.ts
index 40b5c4f..d0925ef 100644
--- a/tests/unit/factories/filters-factory.spec.ts
+++ b/tests/unit/factories/filters-factory.spec.ts
@@ -22,6 +22,7 @@ describe( "Filters factory", () => {
color1: DEFAULT_DUOTONE_1,
color2: DEFAULT_DUOTONE_2,
},
+ blur: 0,
});
});
@@ -42,6 +43,7 @@ describe( "Filters factory", () => {
color1: "#FF9900",
color2: "#ABABAB",
},
+ blur: 33,
});
expect( filters ).toEqual({
enabled: false,
@@ -59,6 +61,7 @@ describe( "Filters factory", () => {
color1: "#FF9900",
color2: "#ABABAB",
},
+ blur: 33,
});
});
});
@@ -81,6 +84,7 @@ describe( "Filters factory", () => {
color1: "#FF9900",
color2: "#ABABAB",
},
+ blur: 50,
});
const serialized = FiltersFactory.serialize( filters );
const deserialized = FiltersFactory.deserialize( serialized );
@@ -126,13 +130,16 @@ describe( "Filters factory", () => {
filter = FiltersFactory.create({ duotone: { enabled: true }});
expect( hasFilters( filter )).toBe( true );
+ filter = FiltersFactory.create({ blur: 25 });
+ expect( hasFilters( filter )).toBe( true );
+
// we don't need to check for duotone colors as the enabled flag for the Object is sufficient
});
});
it( "should know when two filters instances are equal", () => {
const defaultFilter = FiltersFactory.create();
- [ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "threshold", "desaturate", "invert" ]
+ [ "enabled", "blendMode", "opacity", "gamma", "brightness", "contrast", "vibrance", "threshold", "desaturate", "invert", "blur" ]
.forEach( property => {
const filters = FiltersFactory.create({ [ property ]: .88 });
expect( isEqual( filters, defaultFilter )).toBe( false );