@configuratorware/configurator-frontendgui 1.57.6 → 1.57.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App/Services/DesignDataService.js +17 -11
- package/App/Services/VisualizationService.js +12 -0
- package/App/Utils/getDesignAreaBorderColor.js +170 -0
- package/App/configuration.js +13 -0
- package/package.json +4 -4
- package/public/configuration.js +33 -0
- package/src/App/Services/DesignDataService.js +10 -1
- package/src/App/Services/VisualizationService.js +10 -0
- package/src/App/Utils/__tests__/getDesignAreaBorderColor.test.js +128 -0
- package/src/App/Utils/getDesignAreaBorderColor.js +158 -0
- package/src/App/configuration.js +13 -0
|
@@ -26,6 +26,7 @@ var _Actions4 = require("../Reducers/DesignView/Actions");
|
|
|
26
26
|
var _Selectors = require("../Reducers/DesignArea/Selectors");
|
|
27
27
|
var _Selectors2 = require("../Reducers/ImageGallery/Selectors");
|
|
28
28
|
var _Selectors3 = require("../Reducers/Configurator/Selectors");
|
|
29
|
+
var _getDesignAreaBorderColor = require("../Utils/getDesignAreaBorderColor");
|
|
29
30
|
var _Selectors4 = require("../Reducers/Fonts/Selectors");
|
|
30
31
|
var _Actions5 = require("../Reducers/Confirm/Actions");
|
|
31
32
|
var _Actions6 = require("../Reducers/ConfigurationMode/Actions");
|
|
@@ -1905,6 +1906,7 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
1905
1906
|
var options,
|
|
1906
1907
|
state,
|
|
1907
1908
|
showHighlight,
|
|
1909
|
+
staticBorderColor,
|
|
1908
1910
|
textureBuilderConfig,
|
|
1909
1911
|
variantIdentifier,
|
|
1910
1912
|
effectEnabled,
|
|
@@ -1916,10 +1918,14 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
1916
1918
|
options = _args22.length > 2 && _args22[2] !== undefined ? _args22[2] : {};
|
|
1917
1919
|
state = _ServiceLocator.Services.store.state;
|
|
1918
1920
|
showHighlight = (0, _get2["default"])(state, 'designArea.showHighlight');
|
|
1921
|
+
staticBorderColor = (0, _Selectors3.getClientHighlightColor)(state);
|
|
1919
1922
|
textureBuilderConfig = (0, _merge["default"])({
|
|
1920
1923
|
highlight: {
|
|
1921
|
-
borderColor: (0,
|
|
1922
|
-
|
|
1924
|
+
borderColor: (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColor') ? (0, _getDesignAreaBorderColor.createBorderColorResolver)(function (identifier) {
|
|
1925
|
+
return _ServiceLocator.Services.visualization.getDesignAreaUnderlyingColor(identifier);
|
|
1926
|
+
}, staticBorderColor, (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColorMode')) : staticBorderColor,
|
|
1927
|
+
backgroundColor: (0, _configuration.getConf)('visualization.designAreaHighlightBackgroundColor'),
|
|
1928
|
+
lineWidth: (0, _configuration.getConf)('visualization.designAreaHighlightBorderWidth', 2)
|
|
1923
1929
|
}
|
|
1924
1930
|
}, options.textureBuilderConfig);
|
|
1925
1931
|
variantIdentifier = (0, _get2["default"])(updateEventObject, 'data.variant', false);
|
|
@@ -2113,22 +2119,22 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
2113
2119
|
return _ref17.apply(this, arguments);
|
|
2114
2120
|
};
|
|
2115
2121
|
}();
|
|
2116
|
-
_context22.prev =
|
|
2117
|
-
_context22.next =
|
|
2122
|
+
_context22.prev = 8;
|
|
2123
|
+
_context22.next = 11;
|
|
2118
2124
|
return getDrawingContext(drawingFn, true /* update alpha */, true /* clean up texture before drawing */);
|
|
2119
|
-
case
|
|
2120
|
-
_context22.next =
|
|
2125
|
+
case 11:
|
|
2126
|
+
_context22.next = 16;
|
|
2121
2127
|
break;
|
|
2122
|
-
case
|
|
2123
|
-
_context22.prev =
|
|
2124
|
-
_context22.t0 = _context22["catch"](
|
|
2128
|
+
case 13:
|
|
2129
|
+
_context22.prev = 13;
|
|
2130
|
+
_context22.t0 = _context22["catch"](8);
|
|
2125
2131
|
// eslint-disable-next-line no-console
|
|
2126
2132
|
console.warn('Could not get drawing context, texture update is skipped!', _context22.t0.message);
|
|
2127
|
-
case
|
|
2133
|
+
case 16:
|
|
2128
2134
|
case "end":
|
|
2129
2135
|
return _context22.stop();
|
|
2130
2136
|
}
|
|
2131
|
-
}, _callee21, null, [[
|
|
2137
|
+
}, _callee21, null, [[8, 13]]);
|
|
2132
2138
|
}));
|
|
2133
2139
|
function redrawAllDesignAreas(_x20, _x21) {
|
|
2134
2140
|
return _redrawAllDesignAreas.apply(this, arguments);
|
|
@@ -185,6 +185,18 @@ var VisualizationService = exports["default"] = /*#__PURE__*/function (_Abstract
|
|
|
185
185
|
value: function render() {
|
|
186
186
|
return (0, _sendMessage["default"])(this.getComponent(), 'render');
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Average color of the product image underneath the border of the given design area.
|
|
191
|
+
* Only the 2D visualization implements this, everything else returns undefined.
|
|
192
|
+
* @param {string} designAreaIdentifier
|
|
193
|
+
* @return {{r: number, g: number, b: number}|null|undefined}
|
|
194
|
+
*/
|
|
195
|
+
}, {
|
|
196
|
+
key: "getDesignAreaUnderlyingColor",
|
|
197
|
+
value: function getDesignAreaUnderlyingColor(designAreaIdentifier) {
|
|
198
|
+
return (0, _sendMessage["default"])(this.getComponent(), 'getDesignAreaUnderlyingColor', designAreaIdentifier);
|
|
199
|
+
}
|
|
188
200
|
}, {
|
|
189
201
|
key: "update2DMaxZoom",
|
|
190
202
|
value: function update2DMaxZoom(maxZoom) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.rgbToHsl = exports.invertColor = exports.getContrastBorderColor = exports.getComplementaryBorderColor = exports.getBorderColorForAverage = exports.createBorderColorResolver = exports.BorderColorModes = void 0;
|
|
7
|
+
var _styles = require("@material-ui/core/styles");
|
|
8
|
+
var _Selectors = require("../Reducers/DesignArea/Selectors");
|
|
9
|
+
/**
|
|
10
|
+
* Minimum contrast between the border and the area it is drawn on, the WCAG AA requirement for
|
|
11
|
+
* graphical objects. Below it the derived color is not considered visible enough.
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
var MIN_CONTRAST_RATIO = 3;
|
|
15
|
+
/**
|
|
16
|
+
* Below this saturation the inverted color carries no usable hue, e.g. any grey inverts into
|
|
17
|
+
* another grey. Those fall back to a neutral border.
|
|
18
|
+
* @type {number}
|
|
19
|
+
*/
|
|
20
|
+
var MIN_SATURATION = 0.1;
|
|
21
|
+
/**
|
|
22
|
+
* The border keeps the complementary hue at a fixed high saturation, so it reads as an actual color
|
|
23
|
+
* instead of the pale tint that inverting a dark color produces.
|
|
24
|
+
* @type {number}
|
|
25
|
+
*/
|
|
26
|
+
var BORDER_SATURATION = 85;
|
|
27
|
+
/**
|
|
28
|
+
* Candidate lightness values, ordered from the most vivid one outwards. The first one that contrasts
|
|
29
|
+
* enough with the average wins, so the border only gets as pale (on a dark product) or as deep (on a
|
|
30
|
+
* light one) as it has to be to stay visible.
|
|
31
|
+
*/
|
|
32
|
+
var LIGHTNESS_ON_DARK_AVERAGE = [60, 65, 70, 75, 80, 85, 90, 95];
|
|
33
|
+
var LIGHTNESS_ON_LIGHT_AVERAGE = [40, 35, 30, 25, 20, 15, 10, 5];
|
|
34
|
+
/**
|
|
35
|
+
* Relative luminance above which the sampled average counts as light, roughly a mid grey.
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
var LIGHT_AVERAGE_LUMINANCE = 0.2;
|
|
39
|
+
var NEUTRAL_DARK = '#111111';
|
|
40
|
+
var NEUTRAL_LIGHT = '#eeeeee';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {{r: number, g: number, b: number}} color
|
|
44
|
+
* @return {{r: number, g: number, b: number}}
|
|
45
|
+
*/
|
|
46
|
+
var invertColor = exports.invertColor = function invertColor(_ref) {
|
|
47
|
+
var r = _ref.r,
|
|
48
|
+
g = _ref.g,
|
|
49
|
+
b = _ref.b;
|
|
50
|
+
return {
|
|
51
|
+
r: 255 - r,
|
|
52
|
+
g: 255 - g,
|
|
53
|
+
b: 255 - b
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Hue and saturation of an rgb color. Material-UI provides hslToRgb but not the other direction.
|
|
59
|
+
* @param {{r: number, g: number, b: number}} color
|
|
60
|
+
* @return {{hue: number, saturation: number, lightness: number}} Hue in degrees, rest in 0..1
|
|
61
|
+
*/
|
|
62
|
+
var rgbToHsl = exports.rgbToHsl = function rgbToHsl(_ref2) {
|
|
63
|
+
var r = _ref2.r,
|
|
64
|
+
g = _ref2.g,
|
|
65
|
+
b = _ref2.b;
|
|
66
|
+
var red = r / 255;
|
|
67
|
+
var green = g / 255;
|
|
68
|
+
var blue = b / 255;
|
|
69
|
+
var max = Math.max(red, green, blue);
|
|
70
|
+
var min = Math.min(red, green, blue);
|
|
71
|
+
var delta = max - min;
|
|
72
|
+
var lightness = (max + min) / 2;
|
|
73
|
+
if (!delta) {
|
|
74
|
+
return {
|
|
75
|
+
hue: 0,
|
|
76
|
+
saturation: 0,
|
|
77
|
+
lightness: lightness
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
var hue;
|
|
81
|
+
if (max === red) {
|
|
82
|
+
hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60;
|
|
83
|
+
} else if (max === green) {
|
|
84
|
+
hue = ((blue - red) / delta + 2) * 60;
|
|
85
|
+
} else {
|
|
86
|
+
hue = ((red - green) / delta + 4) * 60;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
hue: hue,
|
|
90
|
+
saturation: delta / (1 - Math.abs(2 * lightness - 1)),
|
|
91
|
+
lightness: lightness
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The available ways to derive the border color from the sampled product color.
|
|
97
|
+
* @see getBorderColorForAverage
|
|
98
|
+
*/
|
|
99
|
+
var BorderColorModes = exports.BorderColorModes = {
|
|
100
|
+
Contrast: 'contrast',
|
|
101
|
+
Complementary: 'complementary'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A near black or a near white border, whichever reads better on the given color.
|
|
106
|
+
* The border is only about one pixel wide, and for such a thin line a strong difference in
|
|
107
|
+
* brightness is what makes it visible, much more than a difference in hue.
|
|
108
|
+
* @param {string} averageHex
|
|
109
|
+
* @return {string} A hex color
|
|
110
|
+
*/
|
|
111
|
+
var getContrastBorderColor = exports.getContrastBorderColor = function getContrastBorderColor(averageHex) {
|
|
112
|
+
return (0, _styles.getContrastRatio)(averageHex, NEUTRAL_DARK) >= (0, _styles.getContrastRatio)(averageHex, NEUTRAL_LIGHT) ? NEUTRAL_DARK : NEUTRAL_LIGHT;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
|
|
117
|
+
* away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
|
|
118
|
+
* color rather than the pale tint that plain inversion produces.
|
|
119
|
+
* Averages without a usable hue, such as any grey, and the case where no candidate contrasts enough,
|
|
120
|
+
* fall back to a neutral border.
|
|
121
|
+
* @param {{r: number, g: number, b: number}} average
|
|
122
|
+
* @param {string} averageHex
|
|
123
|
+
* @return {string} A hex color
|
|
124
|
+
*/
|
|
125
|
+
var getComplementaryBorderColor = exports.getComplementaryBorderColor = function getComplementaryBorderColor(average, averageHex) {
|
|
126
|
+
var _rgbToHsl = rgbToHsl(invertColor(average)),
|
|
127
|
+
hue = _rgbToHsl.hue,
|
|
128
|
+
saturation = _rgbToHsl.saturation;
|
|
129
|
+
if (saturation >= MIN_SATURATION) {
|
|
130
|
+
var lightnessCandidates = (0, _styles.getLuminance)(averageHex) > LIGHT_AVERAGE_LUMINANCE ? LIGHTNESS_ON_LIGHT_AVERAGE : LIGHTNESS_ON_DARK_AVERAGE;
|
|
131
|
+
for (var _i = 0, _lightnessCandidates = lightnessCandidates; _i < _lightnessCandidates.length; _i++) {
|
|
132
|
+
var lightness = _lightnessCandidates[_i];
|
|
133
|
+
var candidate = (0, _Selectors.rgbString2hex)((0, _styles.hslToRgb)("hsl(".concat(Math.round(hue), ", ").concat(BORDER_SATURATION, "%, ").concat(lightness, "%)")));
|
|
134
|
+
if (candidate && (0, _styles.getContrastRatio)(averageHex, candidate) >= MIN_CONTRAST_RATIO) {
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return getContrastBorderColor(averageHex);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Derives a border color from the average color of the area the border is drawn on.
|
|
144
|
+
* Anything but the complementary mode uses the contrast mode, so a missing or misspelled
|
|
145
|
+
* configuration value ends up with the most legible option.
|
|
146
|
+
* @param {{r: number, g: number, b: number}} average
|
|
147
|
+
* @param {string} [mode] One of BorderColorModes
|
|
148
|
+
* @return {string} A hex color
|
|
149
|
+
*/
|
|
150
|
+
var getBorderColorForAverage = exports.getBorderColorForAverage = function getBorderColorForAverage(average) {
|
|
151
|
+
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : BorderColorModes.Contrast;
|
|
152
|
+
var averageHex = (0, _Selectors.rgbToHex)(average.r, average.g, average.b);
|
|
153
|
+
return mode === BorderColorModes.Complementary ? getComplementaryBorderColor(average, averageHex) : getContrastBorderColor(averageHex);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates the resolver that the texture builder calls for every highlighted design area.
|
|
158
|
+
* It falls back to the given color whenever the underlying color cannot be determined, e.g. for
|
|
159
|
+
* the 3D visualization, for fully transparent regions and for design areas that are not visible.
|
|
160
|
+
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
161
|
+
* @param {string} fallbackColor
|
|
162
|
+
* @param {string} [mode] One of BorderColorModes
|
|
163
|
+
* @return {function(object): string}
|
|
164
|
+
*/
|
|
165
|
+
var createBorderColorResolver = exports.createBorderColorResolver = function createBorderColorResolver(getUnderlyingColor, fallbackColor, mode) {
|
|
166
|
+
return function (designArea) {
|
|
167
|
+
var average = getUnderlyingColor(designArea && designArea.identifier);
|
|
168
|
+
return average ? getBorderColorForAverage(average, mode) : fallbackColor;
|
|
169
|
+
};
|
|
170
|
+
};
|
package/App/configuration.js
CHANGED
|
@@ -177,6 +177,19 @@ var applicationConfiguration = {
|
|
|
177
177
|
},
|
|
178
178
|
|
|
179
179
|
designAreaHighlightBackgroundColor: 'transparent',
|
|
180
|
+
// Width of the design area highlight border, in pixels at 100% zoom.
|
|
181
|
+
// Applies to the static border as well, 2 was the previously hard coded value.
|
|
182
|
+
designAreaHighlightBorderWidth: 2,
|
|
183
|
+
// Derive the design area highlight border color from the average color of the product
|
|
184
|
+
// image underneath the border, instead of using the client theme's highlightColor.
|
|
185
|
+
// This keeps the border visible on products whose color is close to the highlight color.
|
|
186
|
+
// 2D visualization only, the 3D one always uses the static color.
|
|
187
|
+
dynamicDesignAreaHighlightBorderColor: false,
|
|
188
|
+
// How the dynamic border color is derived from the sampled product color:
|
|
189
|
+
// 'contrast' near black or near white, whichever reads better (most legible, default)
|
|
190
|
+
// 'complementary' the inverted hue at a high saturation (more decorative, but a thin line
|
|
191
|
+
// in a mid bright color is much harder to see than a light or dark one)
|
|
192
|
+
dynamicDesignAreaHighlightBorderColorMode: 'contrast',
|
|
180
193
|
options: {
|
|
181
194
|
// when zoom = 1 use NearestFilter (for sharper product image)
|
|
182
195
|
sharperFullView: false,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@configuratorware/configurator-frontendgui",
|
|
3
|
-
"version": "1.57.
|
|
3
|
+
"version": "1.57.8",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"private": false,
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@babel/polyfill": "^7.12.1",
|
|
9
|
-
"@configuratorware/scripts": "1.57.
|
|
9
|
+
"@configuratorware/scripts": "1.57.8",
|
|
10
10
|
"@material-ui/core": "^4.12.4",
|
|
11
11
|
"@material-ui/icons": "^4.11.3",
|
|
12
12
|
"@material-ui/lab": "^4.0.0-alpha.61",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"react-router-dom": "^5.3.4",
|
|
37
37
|
"react-swipeable": "^5.5.1",
|
|
38
38
|
"react-zoom-pan-pinch": "^2.1.3",
|
|
39
|
-
"redhotmagma-graphics-editor": "1.57.
|
|
40
|
-
"redhotmagma-visualization": "1.57.
|
|
39
|
+
"redhotmagma-graphics-editor": "1.57.8",
|
|
40
|
+
"redhotmagma-visualization": "1.57.8",
|
|
41
41
|
"redux": "^4.1.0",
|
|
42
42
|
"redux-logger": "^3.0.6",
|
|
43
43
|
"redux-persist": "^5.10.0",
|
package/public/configuration.js
CHANGED
|
@@ -107,7 +107,23 @@ const externalConfiguration = {
|
|
|
107
107
|
//displayEditorRulers: true,
|
|
108
108
|
|
|
109
109
|
/*visualization: {
|
|
110
|
+
useVisualizationScript: {
|
|
111
|
+
src: '', // source of the visualization script in the public folder, e.g. 'visualization.js'
|
|
112
|
+
},
|
|
110
113
|
designAreaHighlightBackgroundColor: 'transparent',
|
|
114
|
+
// Width of the design area highlight border, in screen pixels. Stays the same at any zoom
|
|
115
|
+
// level and for any design area size. Applies to the static border as well.
|
|
116
|
+
designAreaHighlightBorderWidth: 2,
|
|
117
|
+
// Derive the design area highlight border color from the average color of the product
|
|
118
|
+
// image underneath the border, instead of using the client theme's highlightColor.
|
|
119
|
+
// This keeps the border visible on products whose color is close to the highlight color.
|
|
120
|
+
// 2D visualization only, the 3D one always uses the static color.
|
|
121
|
+
dynamicDesignAreaHighlightBorderColor: false,
|
|
122
|
+
// How the dynamic border color is derived from the sampled product color:
|
|
123
|
+
// 'contrast' near black or near white, whichever reads better (most legible, default)
|
|
124
|
+
// 'complementary' the inverted hue at a high saturation (more decorative, but a thin line
|
|
125
|
+
// in a mid bright color is much harder to see than a light or dark one)
|
|
126
|
+
dynamicDesignAreaHighlightBorderColorMode: 'contrast',
|
|
111
127
|
options: {
|
|
112
128
|
// when zoom = 1 use NearestFilter (for sharper product image)
|
|
113
129
|
sharperFullView: false,
|
|
@@ -115,12 +131,29 @@ const externalConfiguration = {
|
|
|
115
131
|
// Warning! not recommended, use better image resources instead
|
|
116
132
|
optimizeProductImage: false,
|
|
117
133
|
},
|
|
134
|
+
options3D: {
|
|
135
|
+
// create a ground to show a shadow under the model
|
|
136
|
+
useShadowGround: true,
|
|
137
|
+
// remove the shadow ground for screenshots
|
|
138
|
+
removeGroundForScreenshot: true,
|
|
139
|
+
// use an environment map
|
|
140
|
+
useEnvironmentMap: true,
|
|
141
|
+
// use programmatically generated baked shadow (instead of directional light shadow)
|
|
142
|
+
useBakedShadows: false,
|
|
143
|
+
// baked shadow parameters
|
|
144
|
+
bakedShadow: {
|
|
145
|
+
blurRadius: 6,
|
|
146
|
+
shadowOpacity: 0.8,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
118
149
|
},*/
|
|
119
150
|
|
|
120
151
|
/*designer: {
|
|
121
152
|
dpi: 300, // the default canvas dpi as project specific option
|
|
122
153
|
maxColorAmount: 10, // the default max number of colors in the colorizing feature
|
|
123
154
|
allowedImageUploadFormats: 'jpg,jpeg,svg,png,pdf,bmp,tif,tiff,eps,ai,gif', // the image formats users can upload to designareas / designer items
|
|
155
|
+
includeMetricContentSize: false, // set true to include the metric content size in the design data
|
|
156
|
+
bulkNamesPerNameDesignFiles: false, // set true to generate individual production svgs (-> production pdf in zip download) for each bulk name
|
|
124
157
|
},*/
|
|
125
158
|
|
|
126
159
|
//vectorizeImageQuality: 45000,
|
|
@@ -108,6 +108,7 @@ import {
|
|
|
108
108
|
isDesignerProductPreviewMode,
|
|
109
109
|
visualizationModeCheck,
|
|
110
110
|
} from '../Reducers/Configurator/Selectors';
|
|
111
|
+
import { createBorderColorResolver } from '../Utils/getDesignAreaBorderColor';
|
|
111
112
|
import { getFontMapping } from '../Reducers/Fonts/Selectors';
|
|
112
113
|
import { isConfirmResult } from '../Reducers/Confirm/Actions';
|
|
113
114
|
import { selectConfigurationMode } from '../Reducers/ConfigurationMode/Actions';
|
|
@@ -1602,11 +1603,19 @@ export default class DesignDataService {
|
|
|
1602
1603
|
|
|
1603
1604
|
const showHighlight = get(state, 'designArea.showHighlight');
|
|
1604
1605
|
|
|
1606
|
+
const staticBorderColor = getClientHighlightColor(state);
|
|
1605
1607
|
const textureBuilderConfig = merge(
|
|
1606
1608
|
{
|
|
1607
1609
|
highlight: {
|
|
1608
|
-
borderColor:
|
|
1610
|
+
borderColor: getConf('visualization.dynamicDesignAreaHighlightBorderColor')
|
|
1611
|
+
? createBorderColorResolver(
|
|
1612
|
+
identifier => Services.visualization.getDesignAreaUnderlyingColor(identifier),
|
|
1613
|
+
staticBorderColor,
|
|
1614
|
+
getConf('visualization.dynamicDesignAreaHighlightBorderColorMode')
|
|
1615
|
+
)
|
|
1616
|
+
: staticBorderColor,
|
|
1609
1617
|
backgroundColor: getConf('visualization.designAreaHighlightBackgroundColor'),
|
|
1618
|
+
lineWidth: getConf('visualization.designAreaHighlightBorderWidth', 2),
|
|
1610
1619
|
},
|
|
1611
1620
|
},
|
|
1612
1621
|
options.textureBuilderConfig
|
|
@@ -147,6 +147,16 @@ export default class VisualizationService extends AbstractVisualizationService {
|
|
|
147
147
|
return sendMessage(this.getComponent(), 'render');
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Average color of the product image underneath the border of the given design area.
|
|
152
|
+
* Only the 2D visualization implements this, everything else returns undefined.
|
|
153
|
+
* @param {string} designAreaIdentifier
|
|
154
|
+
* @return {{r: number, g: number, b: number}|null|undefined}
|
|
155
|
+
*/
|
|
156
|
+
getDesignAreaUnderlyingColor(designAreaIdentifier) {
|
|
157
|
+
return sendMessage(this.getComponent(), 'getDesignAreaUnderlyingColor', designAreaIdentifier);
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
update2DMaxZoom(maxZoom) {
|
|
151
161
|
return sendMessage(this.getComponent(), 'updateMaxZoomSetting', maxZoom);
|
|
152
162
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BorderColorModes,
|
|
3
|
+
createBorderColorResolver,
|
|
4
|
+
getBorderColorForAverage,
|
|
5
|
+
invertColor,
|
|
6
|
+
rgbToHsl,
|
|
7
|
+
} from '../getDesignAreaBorderColor';
|
|
8
|
+
|
|
9
|
+
// the dark green of the softshell demo item, the case the feature was built for
|
|
10
|
+
const JACKET_GREEN = { r: 45, g: 95, b: 25 };
|
|
11
|
+
|
|
12
|
+
test('invertColor inverts every channel', () => {
|
|
13
|
+
expect(invertColor({ r: 0, g: 128, b: 255 })).toEqual({ r: 255, g: 127, b: 0 });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('rgbToHsl', () => {
|
|
17
|
+
test('reports no saturation for greys', () => {
|
|
18
|
+
expect(rgbToHsl({ r: 128, g: 128, b: 128 })).toEqual({
|
|
19
|
+
hue: 0,
|
|
20
|
+
saturation: 0,
|
|
21
|
+
lightness: 128 / 255,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('reports the hue of the primary colors', () => {
|
|
26
|
+
expect(Math.round(rgbToHsl({ r: 255, g: 0, b: 0 }).hue)).toBe(0);
|
|
27
|
+
expect(Math.round(rgbToHsl({ r: 0, g: 255, b: 0 }).hue)).toBe(120);
|
|
28
|
+
expect(Math.round(rgbToHsl({ r: 0, g: 0, b: 255 }).hue)).toBe(240);
|
|
29
|
+
expect(rgbToHsl({ r: 255, g: 0, b: 0 }).saturation).toBe(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('reports the hue of a mixed color', () => {
|
|
33
|
+
// the complement of the dark green product color, a violet
|
|
34
|
+
expect(Math.round(rgbToHsl({ r: 210, g: 160, b: 230 }).hue)).toBe(283);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getBorderColorForAverage in contrast mode', () => {
|
|
39
|
+
test('is the default mode', () => {
|
|
40
|
+
expect(getBorderColorForAverage(JACKET_GREEN)).toBe(
|
|
41
|
+
getBorderColorForAverage(JACKET_GREEN, BorderColorModes.Contrast)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('falls back to the contrast mode for an unknown mode', () => {
|
|
46
|
+
expect(getBorderColorForAverage(JACKET_GREEN, 'something-else')).toBe(
|
|
47
|
+
getBorderColorForAverage(JACKET_GREEN, BorderColorModes.Contrast)
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('uses a near white border on a dark average', () => {
|
|
52
|
+
expect(getBorderColorForAverage(JACKET_GREEN, BorderColorModes.Contrast)).toBe('#eeeeee');
|
|
53
|
+
expect(getBorderColorForAverage({ r: 0, g: 0, b: 0 }, BorderColorModes.Contrast)).toBe('#eeeeee');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('uses a near black border on a light average', () => {
|
|
57
|
+
expect(getBorderColorForAverage({ r: 255, g: 255, b: 255 }, BorderColorModes.Contrast)).toBe(
|
|
58
|
+
'#111111'
|
|
59
|
+
);
|
|
60
|
+
expect(getBorderColorForAverage({ r: 255, g: 255, b: 150 }, BorderColorModes.Contrast)).toBe(
|
|
61
|
+
'#111111'
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('picks the better readable neutral for greys', () => {
|
|
66
|
+
expect(getBorderColorForAverage({ r: 128, g: 128, b: 128 }, BorderColorModes.Contrast)).toBe(
|
|
67
|
+
'#111111'
|
|
68
|
+
);
|
|
69
|
+
expect(getBorderColorForAverage({ r: 100, g: 100, b: 100 }, BorderColorModes.Contrast)).toBe(
|
|
70
|
+
'#eeeeee'
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('getBorderColorForAverage in complementary mode', () => {
|
|
76
|
+
const complementary = average => getBorderColorForAverage(average, BorderColorModes.Complementary);
|
|
77
|
+
|
|
78
|
+
test('keeps the complementary hue at a high saturation', () => {
|
|
79
|
+
expect(complementary(JACKET_GREEN)).toBe('#d789f5');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('goes darker on a light average', () => {
|
|
83
|
+
expect(complementary({ r: 255, g: 255, b: 150 })).toBe('#0f0fbd');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('goes lighter on a dark average', () => {
|
|
87
|
+
expect(complementary({ r: 20, g: 30, b: 90 })).toBe('#f0d642');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('falls back to a neutral border when there is no hue to keep', () => {
|
|
91
|
+
// grey inverts into another grey
|
|
92
|
+
expect(complementary({ r: 128, g: 128, b: 128 })).toBe('#111111');
|
|
93
|
+
expect(complementary({ r: 100, g: 100, b: 100 })).toBe('#eeeeee');
|
|
94
|
+
expect(complementary({ r: 0, g: 0, b: 0 })).toBe('#eeeeee');
|
|
95
|
+
expect(complementary({ r: 255, g: 255, b: 255 })).toBe('#111111');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('createBorderColorResolver', () => {
|
|
100
|
+
test('resolves the border color from the underlying color of the design area', () => {
|
|
101
|
+
const getUnderlyingColor = jest.fn().mockReturnValue(JACKET_GREEN);
|
|
102
|
+
const resolve = createBorderColorResolver(getUnderlyingColor, '#abcdef');
|
|
103
|
+
|
|
104
|
+
expect(resolve({ identifier: 'front' })).toBe('#eeeeee');
|
|
105
|
+
expect(getUnderlyingColor).toHaveBeenCalledWith('front');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('passes the mode through', () => {
|
|
109
|
+
const resolve = createBorderColorResolver(
|
|
110
|
+
() => JACKET_GREEN,
|
|
111
|
+
'#abcdef',
|
|
112
|
+
BorderColorModes.Complementary
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(resolve({ identifier: 'front' })).toBe('#d789f5');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('falls back to the given color when there is no underlying color', () => {
|
|
119
|
+
expect(createBorderColorResolver(() => null, '#abcdef')({ identifier: 'front' })).toBe('#abcdef');
|
|
120
|
+
expect(createBorderColorResolver(() => undefined, '#abcdef')({ identifier: 'front' })).toBe(
|
|
121
|
+
'#abcdef'
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('falls back to the given color without a design area', () => {
|
|
126
|
+
expect(createBorderColorResolver(() => null, '#abcdef')()).toBe('#abcdef');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getContrastRatio, getLuminance, hslToRgb } from '@material-ui/core/styles';
|
|
2
|
+
import { rgbString2hex, rgbToHex } from '../Reducers/DesignArea/Selectors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimum contrast between the border and the area it is drawn on, the WCAG AA requirement for
|
|
6
|
+
* graphical objects. Below it the derived color is not considered visible enough.
|
|
7
|
+
* @type {number}
|
|
8
|
+
*/
|
|
9
|
+
const MIN_CONTRAST_RATIO = 3;
|
|
10
|
+
/**
|
|
11
|
+
* Below this saturation the inverted color carries no usable hue, e.g. any grey inverts into
|
|
12
|
+
* another grey. Those fall back to a neutral border.
|
|
13
|
+
* @type {number}
|
|
14
|
+
*/
|
|
15
|
+
const MIN_SATURATION = 0.1;
|
|
16
|
+
/**
|
|
17
|
+
* The border keeps the complementary hue at a fixed high saturation, so it reads as an actual color
|
|
18
|
+
* instead of the pale tint that inverting a dark color produces.
|
|
19
|
+
* @type {number}
|
|
20
|
+
*/
|
|
21
|
+
const BORDER_SATURATION = 85;
|
|
22
|
+
/**
|
|
23
|
+
* Candidate lightness values, ordered from the most vivid one outwards. The first one that contrasts
|
|
24
|
+
* enough with the average wins, so the border only gets as pale (on a dark product) or as deep (on a
|
|
25
|
+
* light one) as it has to be to stay visible.
|
|
26
|
+
*/
|
|
27
|
+
const LIGHTNESS_ON_DARK_AVERAGE = [60, 65, 70, 75, 80, 85, 90, 95];
|
|
28
|
+
const LIGHTNESS_ON_LIGHT_AVERAGE = [40, 35, 30, 25, 20, 15, 10, 5];
|
|
29
|
+
/**
|
|
30
|
+
* Relative luminance above which the sampled average counts as light, roughly a mid grey.
|
|
31
|
+
* @type {number}
|
|
32
|
+
*/
|
|
33
|
+
const LIGHT_AVERAGE_LUMINANCE = 0.2;
|
|
34
|
+
const NEUTRAL_DARK = '#111111';
|
|
35
|
+
const NEUTRAL_LIGHT = '#eeeeee';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {{r: number, g: number, b: number}} color
|
|
39
|
+
* @return {{r: number, g: number, b: number}}
|
|
40
|
+
*/
|
|
41
|
+
export const invertColor = ({ r, g, b }) => ({ r: 255 - r, g: 255 - g, b: 255 - b });
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Hue and saturation of an rgb color. Material-UI provides hslToRgb but not the other direction.
|
|
45
|
+
* @param {{r: number, g: number, b: number}} color
|
|
46
|
+
* @return {{hue: number, saturation: number, lightness: number}} Hue in degrees, rest in 0..1
|
|
47
|
+
*/
|
|
48
|
+
export const rgbToHsl = ({ r, g, b }) => {
|
|
49
|
+
const red = r / 255;
|
|
50
|
+
const green = g / 255;
|
|
51
|
+
const blue = b / 255;
|
|
52
|
+
const max = Math.max(red, green, blue);
|
|
53
|
+
const min = Math.min(red, green, blue);
|
|
54
|
+
const delta = max - min;
|
|
55
|
+
const lightness = (max + min) / 2;
|
|
56
|
+
|
|
57
|
+
if (!delta) {
|
|
58
|
+
return { hue: 0, saturation: 0, lightness };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let hue;
|
|
62
|
+
if (max === red) {
|
|
63
|
+
hue = ((green - blue) / delta + (green < blue ? 6 : 0)) * 60;
|
|
64
|
+
} else if (max === green) {
|
|
65
|
+
hue = ((blue - red) / delta + 2) * 60;
|
|
66
|
+
} else {
|
|
67
|
+
hue = ((red - green) / delta + 4) * 60;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
hue,
|
|
72
|
+
saturation: delta / (1 - Math.abs(2 * lightness - 1)),
|
|
73
|
+
lightness,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The available ways to derive the border color from the sampled product color.
|
|
79
|
+
* @see getBorderColorForAverage
|
|
80
|
+
*/
|
|
81
|
+
export const BorderColorModes = {
|
|
82
|
+
Contrast: 'contrast',
|
|
83
|
+
Complementary: 'complementary',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A near black or a near white border, whichever reads better on the given color.
|
|
88
|
+
* The border is only about one pixel wide, and for such a thin line a strong difference in
|
|
89
|
+
* brightness is what makes it visible, much more than a difference in hue.
|
|
90
|
+
* @param {string} averageHex
|
|
91
|
+
* @return {string} A hex color
|
|
92
|
+
*/
|
|
93
|
+
export const getContrastBorderColor = averageHex =>
|
|
94
|
+
getContrastRatio(averageHex, NEUTRAL_DARK) >= getContrastRatio(averageHex, NEUTRAL_LIGHT)
|
|
95
|
+
? NEUTRAL_DARK
|
|
96
|
+
: NEUTRAL_LIGHT;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
|
|
100
|
+
* away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
|
|
101
|
+
* color rather than the pale tint that plain inversion produces.
|
|
102
|
+
* Averages without a usable hue, such as any grey, and the case where no candidate contrasts enough,
|
|
103
|
+
* fall back to a neutral border.
|
|
104
|
+
* @param {{r: number, g: number, b: number}} average
|
|
105
|
+
* @param {string} averageHex
|
|
106
|
+
* @return {string} A hex color
|
|
107
|
+
*/
|
|
108
|
+
export const getComplementaryBorderColor = (average, averageHex) => {
|
|
109
|
+
const { hue, saturation } = rgbToHsl(invertColor(average));
|
|
110
|
+
|
|
111
|
+
if (saturation >= MIN_SATURATION) {
|
|
112
|
+
const lightnessCandidates =
|
|
113
|
+
getLuminance(averageHex) > LIGHT_AVERAGE_LUMINANCE
|
|
114
|
+
? LIGHTNESS_ON_LIGHT_AVERAGE
|
|
115
|
+
: LIGHTNESS_ON_DARK_AVERAGE;
|
|
116
|
+
|
|
117
|
+
for (const lightness of lightnessCandidates) {
|
|
118
|
+
const candidate = rgbString2hex(
|
|
119
|
+
hslToRgb(`hsl(${Math.round(hue)}, ${BORDER_SATURATION}%, ${lightness}%)`)
|
|
120
|
+
);
|
|
121
|
+
if (candidate && getContrastRatio(averageHex, candidate) >= MIN_CONTRAST_RATIO) {
|
|
122
|
+
return candidate;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return getContrastBorderColor(averageHex);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Derives a border color from the average color of the area the border is drawn on.
|
|
132
|
+
* Anything but the complementary mode uses the contrast mode, so a missing or misspelled
|
|
133
|
+
* configuration value ends up with the most legible option.
|
|
134
|
+
* @param {{r: number, g: number, b: number}} average
|
|
135
|
+
* @param {string} [mode] One of BorderColorModes
|
|
136
|
+
* @return {string} A hex color
|
|
137
|
+
*/
|
|
138
|
+
export const getBorderColorForAverage = (average, mode = BorderColorModes.Contrast) => {
|
|
139
|
+
const averageHex = rgbToHex(average.r, average.g, average.b);
|
|
140
|
+
|
|
141
|
+
return mode === BorderColorModes.Complementary
|
|
142
|
+
? getComplementaryBorderColor(average, averageHex)
|
|
143
|
+
: getContrastBorderColor(averageHex);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates the resolver that the texture builder calls for every highlighted design area.
|
|
148
|
+
* It falls back to the given color whenever the underlying color cannot be determined, e.g. for
|
|
149
|
+
* the 3D visualization, for fully transparent regions and for design areas that are not visible.
|
|
150
|
+
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
151
|
+
* @param {string} fallbackColor
|
|
152
|
+
* @param {string} [mode] One of BorderColorModes
|
|
153
|
+
* @return {function(object): string}
|
|
154
|
+
*/
|
|
155
|
+
export const createBorderColorResolver = (getUnderlyingColor, fallbackColor, mode) => designArea => {
|
|
156
|
+
const average = getUnderlyingColor(designArea && designArea.identifier);
|
|
157
|
+
return average ? getBorderColorForAverage(average, mode) : fallbackColor;
|
|
158
|
+
};
|
package/src/App/configuration.js
CHANGED
|
@@ -186,6 +186,19 @@ let applicationConfiguration = {
|
|
|
186
186
|
src: '', // source of the visualization script in the public folder, e.g. 'visualization.js'
|
|
187
187
|
},
|
|
188
188
|
designAreaHighlightBackgroundColor: 'transparent',
|
|
189
|
+
// Width of the design area highlight border, in pixels at 100% zoom.
|
|
190
|
+
// Applies to the static border as well, 2 was the previously hard coded value.
|
|
191
|
+
designAreaHighlightBorderWidth: 2,
|
|
192
|
+
// Derive the design area highlight border color from the average color of the product
|
|
193
|
+
// image underneath the border, instead of using the client theme's highlightColor.
|
|
194
|
+
// This keeps the border visible on products whose color is close to the highlight color.
|
|
195
|
+
// 2D visualization only, the 3D one always uses the static color.
|
|
196
|
+
dynamicDesignAreaHighlightBorderColor: false,
|
|
197
|
+
// How the dynamic border color is derived from the sampled product color:
|
|
198
|
+
// 'contrast' near black or near white, whichever reads better (most legible, default)
|
|
199
|
+
// 'complementary' the inverted hue at a high saturation (more decorative, but a thin line
|
|
200
|
+
// in a mid bright color is much harder to see than a light or dark one)
|
|
201
|
+
dynamicDesignAreaHighlightBorderColorMode: 'contrast',
|
|
189
202
|
options: {
|
|
190
203
|
// when zoom = 1 use NearestFilter (for sharper product image)
|
|
191
204
|
sharperFullView: false,
|