[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:
committed by
qsm-odoo
parent
4575c188c4
commit
42abf8f496
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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>
|
||||
<!--   -->
|
||||
<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-"
|
||||
|
||||
Reference in New Issue
Block a user