[IMP] web_editor: add webgl image filters

This commit adds webgl image filter to the existing image options,
including a custom filter that lets the user access color-correction
options (saturation, brightness, contrast, sepia, ...) as well as
blending modes for color filters.

Part of https://github.com/odoo/odoo/pull/53114
task-2279190

closes odoo/odoo#53114

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Samuel Degueldre
2020-08-19 08:32:23 +00:00
committed by qsm-odoo
parent 4575c188c4
commit 42abf8f496
4 changed files with 305 additions and 12 deletions
@@ -8,11 +8,188 @@ const modifierFields = [
'filter',
'quality',
'mimetype',
'glFilter',
'originalId',
'originalSrc',
'resizeWidth',
'aspectRatio',
];
// webgl color filters
const _applyAll = (result, filter, filters) => {
filters.forEach(f => {
if (f[0] === 'blend') {
const cv = f[1];
const ctx = result.getContext('2d');
ctx.globalCompositeOperation = f[2];
ctx.globalAlpha = f[3];
ctx.drawImage(cv, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 1.0;
} else {
filter.addFilter(...f);
}
});
};
let applyAll;
const glFilters = {
blur: filter => filter.addFilter('blur', 10),
'1977': (filter, cv) => {
const ctx = cv.getContext('2d');
ctx.fillStyle = 'rgb(243, 106, 188)';
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'screen', .3],
['brightness', .1],
['contrast', .1],
['saturation', .3],
]);
},
aden: (filter, cv) => {
const ctx = cv.getContext('2d');
ctx.fillStyle = 'rgb(66, 10, 14)';
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'darken', .2],
['brightness', .2],
['contrast', -.1],
['saturation', -.15],
['hue', 20],
]);
},
brannan: (filter, cv) => {
const ctx = cv.getContext('2d');
ctx.fillStyle = 'rgb(161, 44, 191)';
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'lighten', .31],
['sepia', .5],
['contrast', .4],
]);
},
earlybird: (filter, cv) => {
const ctx = cv.getContext('2d');
const gradient = ctx.createRadialGradient(
cv.width / 2, cv.height / 2, 0,
cv.width / 2, cv.height / 2, Math.hypot(cv.width, cv.height) / 2
);
gradient.addColorStop(.2, '#D0BA8E');
gradient.addColorStop(1, '#1D0210');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'overlay', .2],
['sepia', .2],
['contrast', -.1],
]);
},
inkwell: (filter, cv) => {
applyAll(filter, [
['sepia', .3],
['brightness', .1],
['contrast', -.1],
['desaturateLuminance'],
]);
},
// Needs hue blending mode for perfect reproduction. Close enough?
maven: (filter, cv) => {
applyAll(filter, [
['sepia', .25],
['brightness', -.05],
['contrast', -.05],
['saturation', .5],
]);
},
toaster: (filter, cv) => {
const ctx = cv.getContext('2d');
const gradient = ctx.createRadialGradient(
cv.width / 2, cv.height / 2, 0,
cv.width / 2, cv.height / 2, Math.hypot(cv.width, cv.height) / 2
);
gradient.addColorStop(0, '#0F4E80');
gradient.addColorStop(1, '#3B003B');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'screen', .5],
['brightness', -.1],
['contrast', .5],
]);
},
walden: (filter, cv) => {
const ctx = cv.getContext('2d');
ctx.fillStyle = '#CC4400';
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'screen', .3],
['sepia', .3],
['brightness', .1],
['saturation', .6],
['hue', 350],
]);
},
valencia: (filter, cv) => {
const ctx = cv.getContext('2d');
ctx.fillStyle = '#3A0339';
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'exclusion', .5],
['sepia', .08],
['brightness', .08],
['contrast', .08],
]);
},
xpro: (filter, cv) => {
const ctx = cv.getContext('2d');
const gradient = ctx.createRadialGradient(
cv.width / 2, cv.height / 2, 0,
cv.width / 2, cv.height / 2, Math.hypot(cv.width, cv.height) / 2
);
gradient.addColorStop(.4, '#E0E7E6');
gradient.addColorStop(1, '#2B2AA1');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, cv.width, cv.height);
applyAll(filter, [
['blend', cv, 'color-burn', .7],
['sepia', .3],
]);
},
custom: (filter, cv, filterOptions) => {
const options = Object.assign({
blend: 'normal',
filterColor: '',
blur: '0',
desaturateLuminance: '0',
saturation: '0',
contrast: '0',
brightness: '0',
sepia: '0',
}, JSON.parse(filterOptions || "{}"));
const filters = [];
if (options.filterColor) {
const ctx = cv.getContext('2d');
ctx.fillStyle = options.filterColor;
ctx.fillRect(0, 0, cv.width, cv.height);
filters.push(['blend', cv, options.blend, 1]);
}
delete options.blend;
delete options.filterColor;
filters.push(...Object.entries(options).map(([filter, amount]) => [filter, parseInt(amount) / 100]));
applyAll(filter, filters);
},
};
/**
* Applies data-attributes modifications to an img tag and returns a dataURL
* containing the result. This function does not modify the original image.
@@ -22,10 +199,21 @@ const modifierFields = [
*/
async function applyModifications(img) {
const data = Object.assign({
glFilter: '',
filter: '#0000',
quality: '95',
}, img.dataset);
let {width, height, resizeWidth, quality, filter, mimetype, originalSrc} = data;
let {
width,
height,
resizeWidth,
quality,
filter,
mimetype,
originalSrc,
glFilter,
filterOptions,
} = data;
[width, height, resizeWidth] = [width, height, resizeWidth].map(s => parseFloat(s));
quality = parseInt(quality);
@@ -44,6 +232,18 @@ async function applyModifications(img) {
const ctx = result.getContext('2d');
ctx.drawImage(croppedImg, 0, 0, croppedImg.width, croppedImg.height, 0, 0, result.width, result.height);
// GL filter
if (glFilter) {
const glf = new window.WebGLImageFilter();
const cv = document.createElement('canvas');
cv.width = result.width;
cv.height = result.height;
applyAll = _applyAll.bind(null, result);
glFilters[glFilter](glf, cv, filterOptions);
const filtered = glf.apply(result);
ctx.drawImage(filtered, 0, 0, filtered.width, filtered.height, 0, 0, result.width, result.height);
}
// Color filter
ctx.fillStyle = filter || '#0000';
ctx.fillRect(0, 0, result.width, result.height);
@@ -2993,15 +2993,34 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
/**
* @see this.selectClass for parameters
*/
setFilter(previewMode, widgetValue, params) {
this._getImg().dataset.filter = normalizeColor(widgetValue);
setQuality(previewMode, widgetValue, params) {
this._getImg().dataset.quality = widgetValue;
return this._applyOptions();
},
/**
* @see this.selectClass for parameters
*/
setQuality(previewMode, widgetValue, params) {
this._getImg().dataset.quality = widgetValue;
glFilter(previewMode, widgetValue, params) {
const dataset = this._getImg().dataset;
if (widgetValue) {
dataset.glFilter = widgetValue;
} else {
delete dataset.glFilter;
}
return this._applyOptions();
},
/**
* @see this.selectClass for parameters
*/
customFilter(previewMode, widgetValue, params) {
const img = this._getImg();
const {filterOptions} = img.dataset;
const {filterProperty} = params;
if (filterProperty === 'filterColor') {
widgetValue = normalizeColor(widgetValue);
}
const newOptions = Object.assign(JSON.parse(filterOptions || "{}"), {[filterProperty]: widgetValue});
img.dataset.filterOptions = JSON.stringify(newOptions);
return this._applyOptions();
},
@@ -3026,8 +3045,16 @@ const ImageHandlerOption = SnippetOptionWidget.extend({
return img.naturalWidth;
case 'setFilter':
return img.dataset.filter;
case 'glFilter':
return img.dataset.glFilter || "";
case 'setQuality':
return img.dataset.quality || 95;
case 'customFilter': {
const {filterProperty} = params;
const options = JSON.parse(img.dataset.filterOptions || "{}");
const defaultValue = filterProperty === 'blend' ? 'normal' : 0;
return options[filterProperty] || defaultValue;
}
}
return this._super(...arguments);
},
+1
View File
@@ -21,6 +21,7 @@
<script type="text/javascript" src="/web_editor/static/lib/jquery-cropper/jquery-cropper.js"/>
<script type="text/javascript" src="/web_editor/static/lib/jQuery.transfo.js"/>
<script type="text/javascript" src="/web/static/lib/nearest/jquery.nearest.js"/>
<script type="text/javascript" src="/web_editor/static/lib/webgl-image-filter/webgl-image-filter.js"/>
<!-- odoo utils -->
+72 -7
View File
@@ -39,18 +39,83 @@
</template>
<template id="snippet_options_image_optimization_widgets">
<t t-set="filter_label">Color Filter</t>
<t t-set="width_label">Width</t>
<t t-set="quality_label">Quality</t>
<div class="o_we_external_warning d-none">
<we-title title="Quality options are unavailable for external images. If you want to change this image's quality, please first download it from the original source and upload it in Odoo.">
<i class="fa fa-warning" /> External Image
</we-title>
</div>
<!-- &emsp; -->
<we-colorpicker t-att-string="indent and (' ⌙ %s' % filter_label) or filter_label" data-set-filter="" data-excluded="common, theme"/>
<t t-set="filter_label">Filter</t>
<we-select t-att-string="indent and (' ⌙ %s' % filter_label) or filter_label">
<we-button data-gl-filter="">None</we-button>
<we-button data-gl-filter="blur">Blur</we-button>
<we-button data-gl-filter="1977">1977</we-button>
<we-button data-gl-filter="aden">Aden</we-button>
<we-button data-gl-filter="brannan">Brannan</we-button>
<we-button data-gl-filter="earlybird">EarlyBird</we-button>
<we-button data-gl-filter="inkwell">Inkwell</we-button>
<we-button data-gl-filter="maven">Maven</we-button>
<we-button data-gl-filter="toaster">Toaster</we-button>
<we-button data-gl-filter="walden">Walden</we-button>
<we-button data-gl-filter="valencia">Valencia</we-button>
<we-button data-gl-filter="xpro">Xpro</we-button>
<we-button data-gl-filter="custom" data-name="custom_glfilter_opt">Custom</we-button>
</we-select>
<t t-set="color_label">Color</t>
<t t-set="saturation_label">Saturation</t>
<t t-set="contrast_label">Contrast</t>
<t t-set="brightness_label">Brightness</t>
<t t-set="sepia_label">Sepia</t>
<t t-set="blur_label">Blur</t>
<we-row t-att-string="indent and ('  ⌙ %s' % color_label) or ('⌙ %s' % color_label)">
<we-select data-filter-property="blend" data-dependencies="custom_glfilter_opt">
<we-button data-custom-filter="normal">Normal</we-button>
<we-button data-custom-filter="overlay">overlay</we-button>
<we-button data-custom-filter="screen">screen</we-button>
<we-button data-custom-filter="multiply">multiply</we-button>
<we-button data-custom-filter="lighter">add</we-button>
<we-button data-custom-filter="exclusion">exclusion</we-button>
<we-button data-custom-filter="darken">darken</we-button>
<we-button data-custom-filter="lighten">lighten</we-button>
</we-select>
<we-colorpicker data-dependencies="custom_glfilter_opt" data-custom-filter="" data-filter-property="filterColor" data-excluded="common, theme"/>
</we-row>
<we-range t-att-string="indent and ('  ⌙ %s' % saturation_label) or ('⌙ %s' % saturation_label)"
data-dependencies="custom_glfilter_opt"
data-custom-filter=""
data-filter-property="saturation"
data-min="-100"
data-step="10"/>
<we-range t-att-string="indent and ('  ⌙ %s' % contrast_label) or ('⌙ %s' % contrast_label)"
data-dependencies="custom_glfilter_opt"
data-custom-filter=""
data-filter-property="contrast"
data-min="-100"
data-step="10"/>
<we-range t-att-string="indent and ('  ⌙ %s' % brightness_label) or ('⌙ %s' % brightness_label)"
data-dependencies="custom_glfilter_opt"
data-custom-filter=""
data-filter-property="brightness"
data-min="-100"
data-step="10"/>
<we-range t-att-string="indent and ('  ⌙ %s' % sepia_label) or ('⌙ %s' % sepia_label)"
data-dependencies="custom_glfilter_opt"
data-custom-filter=""
data-filter-property="sepia"
data-step="5"/>
<we-range t-att-string="indent and ('  ⌙ %s' % blur_label) or ('⌙ %s' % blur_label)"
data-dependencies="custom_glfilter_opt"
data-custom-filter=""
data-filter-property="blur"
data-max="2000"
data-step="100"/>
<t t-set="width_label">Width</t>
<we-select t-att-string="indent and (' ⌙ %s' % width_label) or width_label" data-name="width_select_opt"/>
<t t-set="quality_label">Quality</t>
<we-range t-att-string="indent and (' ⌙ %s' % quality_label) or quality_label" data-set-quality=""/>
<div class="o_we_image_weight d-none"><small/><b/></div>
</template>
@@ -136,11 +201,11 @@
</t>
</div>
<!-- Color Filter -->
<!-- Color filter -->
<div t-att-data-selector="selector"
t-att-data-exclude="exclude"
t-att-data-target="target">
<we-colorpicker string="⌙ Filter"
<we-colorpicker string="⌙ Color filter"
data-select-style="true"
data-css-property="background-color"
data-color-prefix="bg-"