Improved performance of filter application by omitting idle effects, implemented brightness and vibrance filters

This commit is contained in:
Igor Zinken
2021-01-08 20:15:38 +01:00
parent f06c4a0402
commit d8382e043a
5 changed files with 116 additions and 27 deletions

View File

@@ -28,9 +28,9 @@
<template #content>
<div class="form" @keyup.enter="requestLayerAdd()">
<div class="wrapper input">
<label v-t="'levels'"></label>
<label v-t="'gamma'"></label>
<slider
v-model="levels"
v-model="gamma"
:min="0"
:max="100"
:tooltip="'none'"
@@ -45,6 +45,24 @@
:tooltip="'none'"
/>
</div>
<div class="wrapper input">
<label v-t="'brightness'"></label>
<slider
v-model="brightness"
:min="0"
:max="100"
:tooltip="'none'"
/>
</div>
<div class="wrapper input">
<label v-t="'vibrance'"></label>
<slider
v-model="vibrance"
:min="0"
:max="100"
:tooltip="'none'"
/>
</div>
<div class="wrapper input">
<label v-t="'desaturate'"></label>
<toggle-button
@@ -103,12 +121,20 @@ export default {
filters() {
return this.activeLayer.filters;
},
levels: {
gamma: {
get() {
return this.internalValue.levels * 100;
return this.internalValue.gamma * 100;
},
set( value ) {
this.internalValue.levels = value / 100;
this.internalValue.gamma = value / 100;
}
},
brightness: {
get() {
return this.internalValue.brightness * 100;
},
set( value ) {
this.internalValue.brightness = value / 100;
}
},
contrast: {
@@ -119,6 +145,14 @@ export default {
this.internalValue.contrast = value / 100;
}
},
vibrance: {
get() {
return this.internalValue.vibrance * 100;
},
set( value ) {
this.internalValue.vibrance = value / 100;
}
},
},
watch: {
internalValue: {

View File

@@ -1,8 +1,10 @@
{
"en-US": {
"filters": "Filters",
"levels": "Levels",
"gamma": "Gamma",
"contrast": "Contrast",
"brightness": "Brightness",
"vibrance": "Vibrance",
"desaturate": "Desaturate",
"save": "Save",
"reset": "Reset",

View File

@@ -23,11 +23,13 @@
let defaultFilters = null;
const FiltersFactory = {
create({ levels = .5, contrast = 0, desaturate = false } = {}) {
create({ gamma = .5, brightness = .5, contrast = 0, vibrance = .5, desaturate = false } = {}) {
return {
levels,
gamma,
brightness,
contrast,
desaturate,
vibrance,
};
},
@@ -37,9 +39,11 @@ const FiltersFactory = {
*/
serialize( filters ) {
return {
l: filters.levels,
g: filters.gamma,
b: filters.brightness,
c: filters.contrast,
d: filters.desaturate,
v: filters.vibrance,
};
},
@@ -49,9 +53,11 @@ const FiltersFactory = {
*/
deserialize( filters = {} ) {
return FiltersFactory.create({
levels: filters.l,
gamma: filters.g,
brightness: filters.b,
contrast: filters.c,
desaturate: filters.d,
vibrance: filters.v,
});
}
};
@@ -61,7 +67,9 @@ export const hasFilters = filters => {
if ( !defaultFilters ) {
defaultFilters = FiltersFactory.create();
}
return filters.levels !== defaultFilters.levels ||
filters.contrast !== defaultFilters.contrast ||
filters.desaturate !== defaultFilters.desaturate;
return filters.gamma !== defaultFilters.gamma ||
filters.brightness !== defaultFilters.brightness ||
filters.contrast !== defaultFilters.contrast ||
filters.desaturate !== defaultFilters.desaturate ||
filters.vibrance !== defaultFilters.vibrance;
};

View File

@@ -20,9 +20,13 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import FiltersFactory from "@/factories/filters-factory";
const MAX_8BIT = 255;
const HALF = .5;
const defaultFilters = FiltersFactory.create();
self.addEventListener( "message", async ({ data }) => {
const { id, cmd } = data;
let imageData;
@@ -37,21 +41,29 @@ self.addEventListener( "message", async ({ data }) => {
/* internal methods */
const renderFilters = ( imageData, width, height, filters ) => {
const brightness = ( filters.brightness * 2 );//( filters.brightness * 2 ) - 1; // -1 to 1 range
const contrast = Math.pow((( filters.contrast * 100 ) + 100 ) / 100, 2 ); // -100 to 100 range
const levels = filters.levels * 2; // 0 to 2 range
const gamma = ( filters.gamma * 2 ); // 0 to 2 range
const vibrance = -(( filters.vibrance * 200 ) - 100 ); // -100 to 100 range
const { desaturate } = filters; // boolean
const pixels = imageData.data;
let r, g, b, a;
const doBrightness = filters.brightness !== defaultFilters.brightness;
const doContrast = filters.contrast !== defaultFilters.contrast;
const doGamma = filters.gamma !== defaultFilters.gamma;
const doVibrance = filters.vibrance !== defaultFilters.vibrance;
for ( let x = 0; x < width; ++x ) {
for ( let y = 0; y < height; ++y ) {
const i = ( y * width + x ) * 4;
// 1. adjust level (note we leave the alpha channel unchanged)
if ( levels ) {
pixels[ i ] = pixels[ i ] * levels * levels; // R
pixels[ i + 1 ] = pixels[ i + 1 ] * levels * levels; // G
pixels[ i + 2 ] = pixels[ i + 2 ] * levels * levels; // B
// 1. adjust gamma (note we leave the alpha channel unchanged)
if ( doGamma ) {
pixels[ i ] = pixels[ i ] * gamma * gamma; // R
pixels[ i + 1 ] = pixels[ i + 1 ] * gamma * gamma; // G
pixels[ i + 2 ] = pixels[ i + 2 ] * gamma * gamma; // B
}
// 2. desaturate (note we leave the alpha channel unchanged)
if ( desaturate ) {
@@ -60,12 +72,31 @@ const renderFilters = ( imageData, width, height, filters ) => {
pixels[ i + 1 ] = grayScale; // G
pixels[ i + 2 ] = grayScale; // B
}
// 3. adjust contrast (note we leave the alpha channel unchanged)
if ( contrast ) {
// 3. adjust brightness (note we leave the alpha channel unchanged)
if ( doBrightness ) {
pixels[ i ] = pixels[ i ] * brightness;
pixels[ i + 1 ] = pixels[ i + 1 ] * brightness;
pixels[ i + 2 ] = pixels[ i + 2 ] * brightness;
}
// 4. adjust contrast (note we leave the alpha channel unchanged)
if ( doContrast ) {
pixels[ i ] = (( pixels[ i ] / MAX_8BIT - HALF ) * contrast + HALF ) * MAX_8BIT; // R
pixels[ i + 1 ] = (( pixels[ i + 1 ] / MAX_8BIT - HALF ) * contrast + HALF ) * MAX_8BIT; // G
pixels[ i + 2 ] = (( pixels[ i + 2 ] / MAX_8BIT - HALF ) * contrast + HALF ) * MAX_8BIT; // B
}
// 5. adjust vibrance (note we leave the alpha channel unchanged)
if ( doVibrance ) {
r = pixels[ i ];
g = pixels[ i + 1 ];
b = pixels[ i + 2 ];
const max = Math.max( r, g, b );
const avg = ( r + g + b ) / 3;
const amt = (( Math.abs( max - avg ) * 2 / MAX_8BIT ) * vibrance ) / 10; // 100;
pixels[ i ] = r !== max ? r + ( max - r ) * amt : r;
pixels[ i + 1 ] = g !== max ? g + ( max - g ) * amt : g;
pixels[ i + 2 ] = b !== max ? b + ( max - b ) * amt : b;
}
}
}
return imageData;

View File

@@ -5,21 +5,27 @@ describe( "Filters factory", () => {
it( "should create a default filter structure when no arguments are passed", () => {
const filters = FiltersFactory.create();
expect( filters ).toEqual({
levels: .5,
gamma: .5,
brightness: .5,
contrast: 0,
vibrance: .5,
desaturate: false,
});
});
it( "should be able to create a filters list from given arguments", () => {
const filters = FiltersFactory.create({
levels: .7,
gamma: .7,
brightness: .6,
contrast: .3,
vibrance: .2,
desaturate: true,
});
expect( filters ).toEqual({
levels: .7,
gamma: .7,
brightness: .6,
contrast: .3,
vibrance: .2,
desaturate: true,
});
});
@@ -28,8 +34,10 @@ describe( "Filters factory", () => {
describe( "when serializing and deserializing a filters list", () => {
it( "should do so without data loss", async () => {
const filters = FiltersFactory.create({
levels: .7,
gamma: .7,
brightness: .6,
contrast: .3,
vibrance: .2,
desaturate: true,
});
const serialized = FiltersFactory.serialize( filters );
@@ -46,10 +54,16 @@ describe( "Filters factory", () => {
});
it( "should consider a configuration where one of the properties deviates from the default as active", () => {
let filter = FiltersFactory.create({ levels: .7 });
let filter = FiltersFactory.create({ gamma: .7 });
expect( hasFilters( filter )).toBe( true );
filter = FiltersFactory.create({ contrast: .3 });
filter = FiltersFactory.create({ brightness: .3 });
expect( hasFilters( filter )).toBe( true );
filter = FiltersFactory.create({ contrast: .6 });
expect( hasFilters( filter )).toBe( true );
filter = FiltersFactory.create({ vibrance: .4 });
expect( hasFilters( filter )).toBe( true );
filter = FiltersFactory.create({ desaturate: true });