[IMP] website, *: change configurator palette selection

*: web

This commit brings some functionnal changes to the palette selection
screen:

- The list of suggested palettes does not depend on the uploaded logo
anymore. It is now a fixed list of 20 palettes manually chosen among the
existing ones.

- The recommended palette is not chosen among the existing palettes
based on the logo's extracted colors anymore. The recommended palette is
now a fully custom palette generated based on the two main colors
extracted from the logo.

- The selected palette has a border to indicate it is the currently
selected palette.

task-2518565

closes odoo/odoo#71268

X-original-commit: 23dee47ed51d0dfa54f68ef4b328aba957589bb9
Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Sébastien Mottet (oms)
2021-05-26 15:14:43 +00:00
parent afda66598c
commit b8e336aaae
5 changed files with 113 additions and 77 deletions
@@ -654,6 +654,25 @@ ColorpickerWidget.isCSSColor = function (cssColor) {
return ColorpickerWidget.convertCSSColorToRgba(cssColor) !== false;
};
/**
* Mixes two colors by applying a weighted average of their red, green and blue
* components.
*
* @static
* @param {string} cssColor1 - hexadecimal code or rgb() or rgba()
* @param {string} cssColor2 - hexadecimal code or rgb() or rgba()
* @param {number} weight - a number between 0 and 1
* @returns {string} - mixed color in hexadecimal format
*/
ColorpickerWidget.mixCssColors = function (cssColor1, cssColor2, weight) {
const rgba1 = ColorpickerWidget.convertCSSColorToRgba(cssColor1);
const rgba2 = ColorpickerWidget.convertCSSColorToRgba(cssColor2);
const rgb1 = [rgba1.red, rgba1.green, rgba1.blue];
const rgb2 = [rgba2.red, rgba2.green, rgba2.blue];
const [r, g, b] = rgb1.map((_, idx) => Math.round(rgb2[idx] + (rgb1[idx] - rgb2[idx]) * weight));
return ColorpickerWidget.convertRgbaToCSSColor(r, g, b);
};
const ColorpickerDialog = Dialog.extend({
/**
* @override
+14 -4
View File
@@ -294,9 +294,13 @@ class Website(models.Model):
@api.model
def configurator_apply(self, **kwargs):
def set_colors(selected_palette):
url = '/website/static/src/scss/options/user_values.scss'
custo = {'color-palettes-name': "'%s'" % selected_palette}
self.env['web_editor.assets'].make_scss_customization(url, custo)
if isinstance(selected_palette, list):
url = '/website/static/src/scss/options/colors/user_color_palette.scss'
values = {f'o-color-{i}': color for i, color in enumerate(selected_palette, 1)}
else:
url = '/website/static/src/scss/options/user_values.scss'
values = {'color-palettes-name': "'%s'" % selected_palette}
self.env['web_editor.assets'].make_scss_customization(url, values)
def set_features(selected_features):
feature_ids = self.env['website.configurator.feature'].browse(selected_features)
@@ -349,7 +353,13 @@ class Website(models.Model):
website = self.get_current_website()
url = self.env['ir.module.module'].search([('name', '=', kwargs['theme_name'])]).button_choose_theme()
theme = self.env['ir.module.module'].search([('name', '=', kwargs['theme_name'])])
url = theme.button_choose_theme()
# Force to refresh env after install of module
self._cr.commit()
api.Environment.reset()
self.env = api.Environment(theme._cr, theme._uid, theme._context)
website.configurator_done = True
@@ -4,6 +4,7 @@ import rpc from 'web.rpc';
import utils from 'web.utils';
import weUtils from 'web_editor.utils';
import session from 'web.session';
import {ColorpickerWidget} from 'web.Colorpicker';
import {_t, _lt} from 'web.core';
const {Component, Store, mount, QWeb} = owl;
@@ -11,8 +12,6 @@ const {useDispatch, useStore, useGetters, useRef} = owl.hooks;
const {Router, RouteComponent} = owl.router;
const {whenReady} = owl.utils;
const MAX_PALETTES = 16;
const WEBSITE_TYPES = {
1: {id: 1, label: _lt("a business website"), name: 'business'},
2: {id: 2, label: _lt("an online store"), name: 'online_store'},
@@ -28,28 +27,28 @@ const WEBSITE_PURPOSES = {
4: {id: 4, label: _lt("inform customers"), name: 'inform_customers'}
};
const hex2lab = (hex) => {
let r = parseInt(hex.substring(1, 3), 16) / 255, g = parseInt(hex.substring(3, 5), 16) / 255, b = parseInt(hex.substring(5, 7), 16) / 255, x, y, z;
[r, g, b] = [r, g, b].map((x) => (x > 0.04045) ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92);
x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
[x, y, z] = [x, y, z].map((x) => (x > 0.008856) ? Math.pow(x, 1 / 3) : (7.787 * x) + 16 / 116);
return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)];
};
const deltaE = (hexA, hexB) => {
let labA = hex2lab(hexA), labB = hex2lab(hexB);
let deltaL = labA[0] - labB[0], deltaA = labA[1] - labB[1], deltaB = labA[2] - labB[2];
let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]), c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
let deltaC = c1 - c2;
let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
let sc = 1.0 + 0.045 * c1, sh = 1.0 + 0.015 * c1;
let deltaLKlsl = deltaL / (1.0), deltaCkcsc = deltaC / (sc), deltaHkhsh = deltaH / (sh);
let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
return i < 0 ? 0 : Math.sqrt(i);
};
const PALETTE_NAMES = [
'avantgarde-2',
'bistro-6',
'bookstore-4',
'generic-4',
'bookstore-5',
'beauty-3',
'cobalt-1',
'odoo-experts-2',
'artists-4',
'bewise-1',
'generic-16',
'generic-12',
'clean-2',
'generic-8',
'vehicle-1',
'anelusia-4',
'nano-2',
'notes-3',
'graphene-2',
'enark-4',
];
//---------------------------------------------------------
// Components
@@ -215,8 +214,8 @@ class PaletteSelectionScreen extends Component {
this.dispatch('setRecommendedPalette', color1, color2);
}
selectPalette(paletteId) {
this.dispatch('selectPalette', paletteId);
selectPalette(paletteName) {
this.dispatch('selectPalette', paletteName);
this.env.router.navigate({to: 'CONFIGURATOR_FEATURES_SELECTION_SCREEN'});
}
}
@@ -335,7 +334,16 @@ const getters = {
getThemeName({state}, idx) {
return state.themes.length > idx ? state.themes[idx].name : undefined;
}
},
/**
* @param {Object} obj
* @param {string|undefined} [obj.state]
* @returns {string|false}
*/
getSelectedPaletteName({state}) {
const palette = state.selectedPalette;
return palette ? (palette.name || 'recommendedPalette') : false;
},
};
const actions = {
@@ -354,8 +362,12 @@ const actions = {
changeLogo({state}, data) {
state.logo = data;
},
selectPalette({state}, paletteId) {
state.selectedPalette = state.allPalettes[paletteId];
selectPalette({state}, paletteName) {
if (paletteName === 'recommendedPalette') {
state.selectedPalette = state.recommendedPalette;
} else {
state.selectedPalette = state.palettes[paletteName];
}
},
toggleFeature({state}, featureId) {
const feature = state.features[featureId];
@@ -364,23 +376,20 @@ const actions = {
feature.selected = !feature.selected || forceFeatureActive;
},
setRecommendedPalette({state}, color1, color2) {
let palettes = [];
if (color1 && color2) {
Object.values(state.allPalettes).forEach(palette => {
const delta1 = deltaE(color1, palette.color1);
const delta2 = deltaE(color2, palette.color2);
palette.score = (delta1 + delta2) / 2;
});
palettes = Object.values(state.allPalettes).sort((a, b) => a.score - b.score);
state.recommendedPalette = palettes[0];
if (color1 === color2) {
color2 = ColorpickerWidget.mixCssColors('#FFFFFF', color1, 0.2);
}
state.recommendedPalette = {
color1: color1,
color2: color2,
color3: ColorpickerWidget.mixCssColors('#FFFFFF', color2, 0.9),
color4: '#FFFFFF',
color5: ColorpickerWidget.mixCssColors(color1, '#000000', 0.75),
};
} else {
palettes = Object.values(state.allPalettes);
state.recommendedPalette = undefined;
}
const selectedPalettes = {};
palettes.slice(1, MAX_PALETTES + 1).forEach((palette) => {
selectedPalettes[palette.id] = palette;
});
state.palettes = selectedPalettes;
},
updateRecommendedThemes({state}, themes) {
state.themes = themes.slice(0, 3);
@@ -406,31 +415,18 @@ async function getInitialState() {
});
// Load palettes from the current CSS
const allPalettes = {}, palettes = {};
const palettes = {};
const style = window.getComputedStyle(document.documentElement);
const allPaletteNames = weUtils.getCSSVariableValue('palette-names', style).split(' ').map((name) => {
return name.replace(/'/g, "");
});
allPaletteNames.forEach((paletteName) => {
PALETTE_NAMES.forEach((paletteName) => {
const palette = {
id: paletteName
name: paletteName
};
for (let j = 1; j <= 5; j += 1) {
const color = weUtils.getCSSVariableValue(`o-palette-${paletteName}-o-color-${j}`, style);
palette[`color${j}`] = color;
}
let duplicate = false;
for (const validatedPalette of Object.values(allPalettes)) {
if (validatedPalette.color1.toLowerCase() === palette.color1.toLowerCase() && validatedPalette.color2.toLowerCase() === palette.color2.toLowerCase()) {
duplicate = true;
}
}
if (!duplicate) {
allPalettes[paletteName] = palette;
if (Object.keys(palettes).length < MAX_PALETTES) {
palettes[paletteName] = allPalettes[paletteName];
}
}
palettes[paletteName] = palette;
});
return Object.assign(r, {
@@ -440,7 +436,6 @@ async function getInitialState() {
selectedPalette: undefined,
recommendedPalette: undefined,
palettes: palettes,
allPalettes: allPalettes,
themes: [],
});
}
@@ -465,11 +460,21 @@ async function applyConfigurator(self, themeName) {
if (themeName !== undefined) {
$('body').append(self.env.loader);
const selectedFeatures = Object.values(self.state.features).filter((feature) => feature.selected).map((feature) => feature.id);
let selectedPalette = self.state.selectedPalette.name;
if (!selectedPalette) {
selectedPalette = [
self.state.selectedPalette.color1,
self.state.selectedPalette.color2,
self.state.selectedPalette.color3,
self.state.selectedPalette.color4,
self.state.selectedPalette.color5,
];
}
const data = {
selected_features: selectedFeatures,
logo: self.state.logo,
industry_id: self.state.selectedIndustry,
selected_palette: self.state.selectedPalette.id,
selected_palette: selectedPalette,
theme_name: themeName,
};
const resp = await rpc.query({
@@ -97,7 +97,7 @@
<div class="w-100 w-lg-25 order-lg-3 my-4 my-md-0 d-flex flex-column">
<div class="h4 text-center">
<b>Detect</b> from Logo</div>
<div class="d-flex flex-column flex-grow-1">
<div class="d-flex flex-column flex-grow-1 py-4">
<a href="#" t-on-click="uploadLogo" t-attf-class="o_configurator_logo_upload position-relative btn-link rounded bg-100 overflow-hidden d-flex flex-grow-1 justify-content-center align-items-center text-decoration-none {{state.logo? 'h-50' : ''}}">
<input type="file" class="logo_selection_input" t-on-change="changeLogo" style="display:none" name="logo_selection" t-ref="logoSelectionInput" accept="image/*"/>
<div class="o_configurator_logo_button text-center">
@@ -111,10 +111,11 @@
</a>
<div t-if="state.recommendedPalette" class="w-75 mx-auto px-2 pt-3" style="max-width: 184px;">
<h6 class="text-center text-success d-block badge mb-0 mt-n2">Detected Colors</h6>
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex" t-on-click="selectPalette(state.recommendedPalette.id)" t-attf-style="background-color: {{state.recommendedPalette.color3}}">
<div class="color_sample w-100 first" t-attf-style="background-color: {{state.recommendedPalette.color1}}"/>
<div class="color_sample w-100 second" t-attf-style="background-color: {{state.recommendedPalette.color2}}"/>
<div class="color_sample w-100 third" t-attf-style="background-color: {{state.recommendedPalette.color3}}"/>
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex {{getters.getSelectedPaletteName() == 'recommendedPalette' ? 'selected' : ''}}"
t-on-click="selectPalette('recommendedPalette')" t-attf-style="background-color: {{state.recommendedPalette.color3}}">
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color1}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color3}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{state.recommendedPalette.color2}}"/>
</div>
</div>
</div>
@@ -130,10 +131,11 @@
<div class="d-flex flex-wrap align-items-end">
<t t-foreach="getters.getPalettes()" t-as="palette" t-key="palette_index">
<div class="w-50 w-md-25 px-2 pt-3">
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex" t-on-click="selectPalette(palette.id)" t-attf-style="background-color: {{palette.color3}}">
<div class="color_sample w-100 first" t-attf-style="background-color: {{palette.color1}}"/>
<div class="color_sample w-100 second" t-attf-style="background-color: {{palette.color2}}"/>
<div class="color_sample w-100 third" t-attf-style="background-color: {{palette.color3}}"/>
<div t-attf-class="palette_card rounded-pill overflow-hidden d-flex {{getters.getSelectedPaletteName() == palette.name ? 'selected' : ''}}"
t-on-click="selectPalette(palette.name)" t-attf-style="background-color: {{palette.color3}}">
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color1}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color3}}"/>
<div class="color_sample w-100" t-attf-style="background-color: {{palette.color2}}"/>
</div>
</div>
</t>
@@ -193,7 +193,7 @@
padding-top: 30%;
}
&:hover {
&:hover, &.selected {
box-shadow: 0 0 0 1px #FFF, 0 0 0 3px theme-color("primary")
}
}