@configuratorware/configurator-frontendgui 1.57.9 → 1.57.11
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/Reducers/Configurator/Actions.js +39 -18
- package/App/Services/DesignDataService.js +22 -12
- package/App/Utils/getDesignAreaBorderColor.js +161 -5
- package/package.json +5 -5
- package/src/App/Reducers/Configurator/Actions.js +21 -9
- package/src/App/Reducers/Configurator/__tests__/Actions.test.js +121 -0
- package/src/App/Services/DesignDataService.js +19 -7
- package/src/App/Utils/__tests__/getDesignAreaBorderColor.test.js +137 -0
- package/src/App/Utils/getDesignAreaBorderColor.js +131 -3
|
@@ -39,6 +39,10 @@ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object
|
|
|
39
39
|
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
|
|
40
40
|
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
41
41
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
|
42
|
+
function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); }
|
|
43
|
+
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
44
|
+
function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } }
|
|
45
|
+
function _arrayWithHoles(r) { if (Array.isArray(r)) return r; }
|
|
42
46
|
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
|
|
43
47
|
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
44
48
|
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
@@ -246,15 +250,32 @@ var applyVariantAmountPreSelection = exports.applyVariantAmountPreSelection = fu
|
|
|
246
250
|
var children = (0, _flatten["default"])((0, _Selectors.getChildren)(configurator).map(function (variant) {
|
|
247
251
|
return Object.values((0, _Selectors.getDeepestChildIdentifiers)(variant));
|
|
248
252
|
}));
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
var applicableAmounts = Object.keys(itemAmounts).filter(function (identifier) {
|
|
254
|
+
return children.indexOf(identifier) !== -1 && +itemAmounts[identifier] > 0;
|
|
255
|
+
}).map(function (identifier) {
|
|
256
|
+
return [identifier, +itemAmounts[identifier]];
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// designerPreSelection is parsed once from the URL and stays in conf for the whole session, so
|
|
260
|
+
// without consuming it here it would re-apply (and re-clear existing amounts) on every later
|
|
261
|
+
// configuration load, e.g. switching the configuration variant or loading a saved configuration
|
|
262
|
+
(0, _configuration.setConf)('designerPreSelection.itemAmounts', undefined);
|
|
263
|
+
if (applicableAmounts.length === 0) {
|
|
264
|
+
return;
|
|
257
265
|
}
|
|
266
|
+
|
|
267
|
+
// only the explicitly passed amounts should apply, so clear whatever the item already had - by
|
|
268
|
+
// the keys actually selected, since a selected amount can be stored at any item level, not just
|
|
269
|
+
// on the deepest children
|
|
270
|
+
Object.keys((0, _Selectors.getSelectedAmounts)(configurator) || {}).forEach(function (identifier) {
|
|
271
|
+
return _ServiceLocator.Services.configurator.setSelectedAmount(identifier, 0);
|
|
272
|
+
});
|
|
273
|
+
applicableAmounts.forEach(function (_ref4) {
|
|
274
|
+
var _ref5 = _slicedToArray(_ref4, 2),
|
|
275
|
+
identifier = _ref5[0],
|
|
276
|
+
amount = _ref5[1];
|
|
277
|
+
return _ServiceLocator.Services.configurator.setSelectedAmount(identifier, amount);
|
|
278
|
+
});
|
|
258
279
|
}
|
|
259
280
|
};
|
|
260
281
|
var selectOptionclassification = exports.selectOptionclassification = function selectOptionclassification(optionclassification) {
|
|
@@ -413,7 +434,7 @@ var receiveCodesnippets = exports.receiveCodesnippets = function receiveCodesnip
|
|
|
413
434
|
};
|
|
414
435
|
var fetchConfigurationForDesignerProductPreview = exports.fetchConfigurationForDesignerProductPreview = function fetchConfigurationForDesignerProductPreview() {
|
|
415
436
|
return /*#__PURE__*/function () {
|
|
416
|
-
var
|
|
437
|
+
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3(dispatch, getState) {
|
|
417
438
|
var itemIdentifier, _getAvailableConfigur2, designerEnabled, initialDesignData;
|
|
418
439
|
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
|
|
419
440
|
while (1) switch (_context3.prev = _context3.next) {
|
|
@@ -448,7 +469,7 @@ var fetchConfigurationForDesignerProductPreview = exports.fetchConfigurationForD
|
|
|
448
469
|
}, _callee3);
|
|
449
470
|
}));
|
|
450
471
|
return function (_x4, _x5) {
|
|
451
|
-
return
|
|
472
|
+
return _ref6.apply(this, arguments);
|
|
452
473
|
};
|
|
453
474
|
}();
|
|
454
475
|
};
|
|
@@ -866,10 +887,10 @@ var getSwitchOptionData = function getSwitchOptionData(optionsList) {
|
|
|
866
887
|
try {
|
|
867
888
|
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
|
|
868
889
|
var component = _step4.value;
|
|
869
|
-
switchOptionData[component.identifier] = component.selectedoptions.map(function (
|
|
870
|
-
var identifier =
|
|
871
|
-
amount =
|
|
872
|
-
inputText =
|
|
890
|
+
switchOptionData[component.identifier] = component.selectedoptions.map(function (_ref7) {
|
|
891
|
+
var identifier = _ref7.identifier,
|
|
892
|
+
amount = _ref7.amount,
|
|
893
|
+
inputText = _ref7.inputText;
|
|
873
894
|
return {
|
|
874
895
|
identifier: identifier,
|
|
875
896
|
inputText: inputText,
|
|
@@ -902,7 +923,7 @@ var viewOptionDetails = exports.viewOptionDetails = function viewOptionDetails(o
|
|
|
902
923
|
};
|
|
903
924
|
var loadConfiguration = exports.loadConfiguration = function loadConfiguration() {
|
|
904
925
|
return /*#__PURE__*/function () {
|
|
905
|
-
var
|
|
926
|
+
var _ref8 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4(dispatch, getState) {
|
|
906
927
|
var loadCode, action;
|
|
907
928
|
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
|
|
908
929
|
while (1) switch (_context4.prev = _context4.next) {
|
|
@@ -939,7 +960,7 @@ var loadConfiguration = exports.loadConfiguration = function loadConfiguration()
|
|
|
939
960
|
}, _callee4, null, [[5, 13, 17, 20]]);
|
|
940
961
|
}));
|
|
941
962
|
return function (_x6, _x7) {
|
|
942
|
-
return
|
|
963
|
+
return _ref8.apply(this, arguments);
|
|
943
964
|
};
|
|
944
965
|
}();
|
|
945
966
|
};
|
|
@@ -952,7 +973,7 @@ var receiveDesignerData = exports.receiveDesignerData = function receiveDesigner
|
|
|
952
973
|
};
|
|
953
974
|
var loadDesignerData = exports.loadDesignerData = function loadDesignerData(configuration) {
|
|
954
975
|
return /*#__PURE__*/function () {
|
|
955
|
-
var
|
|
976
|
+
var _ref9 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5(dispatch) {
|
|
956
977
|
return _regeneratorRuntime().wrap(function _callee5$(_context5) {
|
|
957
978
|
while (1) switch (_context5.prev = _context5.next) {
|
|
958
979
|
case 0:
|
|
@@ -986,7 +1007,7 @@ var loadDesignerData = exports.loadDesignerData = function loadDesignerData(conf
|
|
|
986
1007
|
}, _callee5, null, [[4, 10, 13, 16]]);
|
|
987
1008
|
}));
|
|
988
1009
|
return function (_x8) {
|
|
989
|
-
return
|
|
1010
|
+
return _ref9.apply(this, arguments);
|
|
990
1011
|
};
|
|
991
1012
|
}();
|
|
992
1013
|
};
|
|
@@ -1907,6 +1907,11 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
1907
1907
|
state,
|
|
1908
1908
|
showHighlight,
|
|
1909
1909
|
staticBorderColor,
|
|
1910
|
+
dynamicDesignAreaHighlightBorderColor,
|
|
1911
|
+
dynamicDesignAreaHighlightBorderColorMode,
|
|
1912
|
+
getDesignAreaUnderlyingColor,
|
|
1913
|
+
allDesignAreasForBorderColor,
|
|
1914
|
+
borderColor,
|
|
1910
1915
|
textureBuilderConfig,
|
|
1911
1916
|
variantIdentifier,
|
|
1912
1917
|
effectEnabled,
|
|
@@ -1919,11 +1924,16 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
1919
1924
|
state = _ServiceLocator.Services.store.state;
|
|
1920
1925
|
showHighlight = (0, _get2["default"])(state, 'designArea.showHighlight');
|
|
1921
1926
|
staticBorderColor = (0, _Selectors3.getClientHighlightColor)(state);
|
|
1927
|
+
dynamicDesignAreaHighlightBorderColor = (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColor');
|
|
1928
|
+
dynamicDesignAreaHighlightBorderColorMode = (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColorMode');
|
|
1929
|
+
getDesignAreaUnderlyingColor = function getDesignAreaUnderlyingColor(identifier) {
|
|
1930
|
+
return _ServiceLocator.Services.visualization.getDesignAreaUnderlyingColor(identifier);
|
|
1931
|
+
}; // 2D invokes drawingFn once per area with a single-element list; unify borders from the full view.
|
|
1932
|
+
allDesignAreasForBorderColor = this.getDesignAreas();
|
|
1933
|
+
borderColor = dynamicDesignAreaHighlightBorderColor ? (0, _getDesignAreaBorderColor.createBorderColorResolver)(getDesignAreaUnderlyingColor, staticBorderColor, dynamicDesignAreaHighlightBorderColorMode, allDesignAreasForBorderColor) : staticBorderColor;
|
|
1922
1934
|
textureBuilderConfig = (0, _merge["default"])({
|
|
1923
1935
|
highlight: {
|
|
1924
|
-
borderColor:
|
|
1925
|
-
return _ServiceLocator.Services.visualization.getDesignAreaUnderlyingColor(identifier);
|
|
1926
|
-
}, staticBorderColor, (0, _configuration.getConf)('visualization.dynamicDesignAreaHighlightBorderColorMode')) : staticBorderColor,
|
|
1936
|
+
borderColor: borderColor,
|
|
1927
1937
|
backgroundColor: (0, _configuration.getConf)('visualization.designAreaHighlightBackgroundColor'),
|
|
1928
1938
|
lineWidth: (0, _configuration.getConf)('visualization.designAreaHighlightBorderWidth', 2)
|
|
1929
1939
|
}
|
|
@@ -2119,22 +2129,22 @@ var DesignDataService = exports["default"] = /*#__PURE__*/function () {
|
|
|
2119
2129
|
return _ref17.apply(this, arguments);
|
|
2120
2130
|
};
|
|
2121
2131
|
}();
|
|
2122
|
-
_context22.prev =
|
|
2123
|
-
_context22.next = 11;
|
|
2124
|
-
return getDrawingContext(drawingFn, true /* update alpha */, true /* clean up texture before drawing */);
|
|
2125
|
-
case 11:
|
|
2132
|
+
_context22.prev = 13;
|
|
2126
2133
|
_context22.next = 16;
|
|
2134
|
+
return getDrawingContext(drawingFn, true /* update alpha */, true /* clean up texture before drawing */);
|
|
2135
|
+
case 16:
|
|
2136
|
+
_context22.next = 21;
|
|
2127
2137
|
break;
|
|
2128
|
-
case
|
|
2129
|
-
_context22.prev =
|
|
2130
|
-
_context22.t0 = _context22["catch"](
|
|
2138
|
+
case 18:
|
|
2139
|
+
_context22.prev = 18;
|
|
2140
|
+
_context22.t0 = _context22["catch"](13);
|
|
2131
2141
|
// eslint-disable-next-line no-console
|
|
2132
2142
|
console.warn('Could not get drawing context, texture update is skipped!', _context22.t0.message);
|
|
2133
|
-
case
|
|
2143
|
+
case 21:
|
|
2134
2144
|
case "end":
|
|
2135
2145
|
return _context22.stop();
|
|
2136
2146
|
}
|
|
2137
|
-
}, _callee21,
|
|
2147
|
+
}, _callee21, this, [[13, 18]]);
|
|
2138
2148
|
}));
|
|
2139
2149
|
function redrawAllDesignAreas(_x20, _x21) {
|
|
2140
2150
|
return _redrawAllDesignAreas.apply(this, arguments);
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.rgbToHsl = exports.invertColor = exports.getContrastBorderColor = exports.getComplementaryBorderColor = exports.getBorderColorForAverage = exports.createBorderColorResolver = exports.BorderColorModes = void 0;
|
|
6
|
+
exports.rgbToHsl = exports.invertColor = exports.getContrastBorderColor = exports.getComplementaryBorderColor = exports.getBorderColorForAverage = exports.createBorderColorResolver = exports.computeUnifiedContrastBorders = exports.BorderColorModes = void 0;
|
|
7
7
|
var _styles = require("@material-ui/core/styles");
|
|
8
8
|
var _Selectors = require("../Reducers/DesignArea/Selectors");
|
|
9
|
+
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; }
|
|
10
|
+
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
|
|
11
|
+
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
|
|
12
|
+
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
|
|
13
|
+
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
|
|
14
|
+
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
|
|
15
|
+
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
|
|
9
16
|
/**
|
|
10
17
|
* Minimum contrast between the border and the area it is drawn on, the WCAG AA requirement for
|
|
11
18
|
* graphical objects. Below it the derived color is not considered visible enough.
|
|
@@ -112,6 +119,135 @@ var getContrastBorderColor = exports.getContrastBorderColor = function getContra
|
|
|
112
119
|
return (0, _styles.getContrastRatio)(averageHex, NEUTRAL_DARK) >= (0, _styles.getContrastRatio)(averageHex, NEUTRAL_LIGHT) ? NEUTRAL_DARK : NEUTRAL_LIGHT;
|
|
113
120
|
};
|
|
114
121
|
|
|
122
|
+
/**
|
|
123
|
+
* @param {string} neutral
|
|
124
|
+
* @param {Array<{averageHex: string}>} areas
|
|
125
|
+
* @return {number}
|
|
126
|
+
*/
|
|
127
|
+
var minContrastForNeutralAcrossAreas = function minContrastForNeutralAcrossAreas(neutral, areas) {
|
|
128
|
+
return Math.min.apply(Math, _toConsumableArray(areas.map(function (_ref3) {
|
|
129
|
+
var averageHex = _ref3.averageHex;
|
|
130
|
+
return (0, _styles.getContrastRatio)(averageHex, neutral);
|
|
131
|
+
})));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} neutral
|
|
136
|
+
* @param {Array<{averageHex: string}>} areas
|
|
137
|
+
* @return {boolean}
|
|
138
|
+
*/
|
|
139
|
+
var neutralMeetsMinContrastOnAllAreas = function neutralMeetsMinContrastOnAllAreas(neutral, areas) {
|
|
140
|
+
return minContrastForNeutralAcrossAreas(neutral, areas) >= MIN_CONTRAST_RATIO;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Picks one near-black or near-white border for every design area in a highlight pass when possible,
|
|
145
|
+
* so similar product shades do not flip border color at the luminance tie-break.
|
|
146
|
+
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
147
|
+
* @param {Array<{identifier: string}>} designAreas
|
|
148
|
+
* @return {Map<string, string>}
|
|
149
|
+
*/
|
|
150
|
+
var computeUnifiedContrastBorders = exports.computeUnifiedContrastBorders = function computeUnifiedContrastBorders(getUnderlyingColor, designAreas) {
|
|
151
|
+
var areas = [];
|
|
152
|
+
var _iterator = _createForOfIteratorHelper(designAreas),
|
|
153
|
+
_step;
|
|
154
|
+
try {
|
|
155
|
+
for (_iterator.s(); !(_step = _iterator.n()).done;) {
|
|
156
|
+
var designArea = _step.value;
|
|
157
|
+
var _identifier3 = designArea && designArea.identifier;
|
|
158
|
+
if (!_identifier3) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
var average = getUnderlyingColor(_identifier3);
|
|
162
|
+
if (!average) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
areas.push({
|
|
166
|
+
identifier: _identifier3,
|
|
167
|
+
averageHex: (0, _Selectors.rgbToHex)(average.r, average.g, average.b)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
_iterator.e(err);
|
|
172
|
+
} finally {
|
|
173
|
+
_iterator.f();
|
|
174
|
+
}
|
|
175
|
+
var borderByIdentifier = new Map();
|
|
176
|
+
if (!areas.length) {
|
|
177
|
+
return borderByIdentifier;
|
|
178
|
+
}
|
|
179
|
+
var darkVotes = 0;
|
|
180
|
+
var lightVotes = 0;
|
|
181
|
+
for (var _i = 0, _areas = areas; _i < _areas.length; _i++) {
|
|
182
|
+
var averageHex = _areas[_i].averageHex;
|
|
183
|
+
var vote = getContrastBorderColor(averageHex);
|
|
184
|
+
if (vote === NEUTRAL_DARK) {
|
|
185
|
+
darkVotes += 1;
|
|
186
|
+
} else {
|
|
187
|
+
lightVotes += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
var majorityNeutral;
|
|
191
|
+
if (darkVotes > lightVotes) {
|
|
192
|
+
majorityNeutral = NEUTRAL_DARK;
|
|
193
|
+
} else if (lightVotes > darkVotes) {
|
|
194
|
+
majorityNeutral = NEUTRAL_LIGHT;
|
|
195
|
+
}
|
|
196
|
+
if (majorityNeutral && neutralMeetsMinContrastOnAllAreas(majorityNeutral, areas)) {
|
|
197
|
+
var _iterator2 = _createForOfIteratorHelper(areas),
|
|
198
|
+
_step2;
|
|
199
|
+
try {
|
|
200
|
+
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
|
|
201
|
+
var identifier = _step2.value.identifier;
|
|
202
|
+
borderByIdentifier.set(identifier, majorityNeutral);
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
_iterator2.e(err);
|
|
206
|
+
} finally {
|
|
207
|
+
_iterator2.f();
|
|
208
|
+
}
|
|
209
|
+
return borderByIdentifier;
|
|
210
|
+
}
|
|
211
|
+
var qualifyingNeutrals = [NEUTRAL_DARK, NEUTRAL_LIGHT].filter(function (neutral) {
|
|
212
|
+
return neutralMeetsMinContrastOnAllAreas(neutral, areas);
|
|
213
|
+
});
|
|
214
|
+
if (qualifyingNeutrals.length) {
|
|
215
|
+
var maximinNeutral = qualifyingNeutrals.reduce(function (best, neutral) {
|
|
216
|
+
var minContrast = minContrastForNeutralAcrossAreas(neutral, areas);
|
|
217
|
+
if (!best) {
|
|
218
|
+
return {
|
|
219
|
+
neutral: neutral,
|
|
220
|
+
minContrast: minContrast
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return minContrast > best.minContrast ? {
|
|
224
|
+
neutral: neutral,
|
|
225
|
+
minContrast: minContrast
|
|
226
|
+
} : best;
|
|
227
|
+
}, null).neutral;
|
|
228
|
+
var _iterator3 = _createForOfIteratorHelper(areas),
|
|
229
|
+
_step3;
|
|
230
|
+
try {
|
|
231
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
232
|
+
var _identifier = _step3.value.identifier;
|
|
233
|
+
borderByIdentifier.set(_identifier, maximinNeutral);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
_iterator3.e(err);
|
|
237
|
+
} finally {
|
|
238
|
+
_iterator3.f();
|
|
239
|
+
}
|
|
240
|
+
return borderByIdentifier;
|
|
241
|
+
}
|
|
242
|
+
for (var _i2 = 0, _areas2 = areas; _i2 < _areas2.length; _i2++) {
|
|
243
|
+
var _areas2$_i = _areas2[_i2],
|
|
244
|
+
_identifier2 = _areas2$_i.identifier,
|
|
245
|
+
_averageHex = _areas2$_i.averageHex;
|
|
246
|
+
borderByIdentifier.set(_identifier2, getContrastBorderColor(_averageHex));
|
|
247
|
+
}
|
|
248
|
+
return borderByIdentifier;
|
|
249
|
+
};
|
|
250
|
+
|
|
115
251
|
/**
|
|
116
252
|
* Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
|
|
117
253
|
* away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
|
|
@@ -128,8 +264,8 @@ var getComplementaryBorderColor = exports.getComplementaryBorderColor = function
|
|
|
128
264
|
saturation = _rgbToHsl.saturation;
|
|
129
265
|
if (saturation >= MIN_SATURATION) {
|
|
130
266
|
var lightnessCandidates = (0, _styles.getLuminance)(averageHex) > LIGHT_AVERAGE_LUMINANCE ? LIGHTNESS_ON_LIGHT_AVERAGE : LIGHTNESS_ON_DARK_AVERAGE;
|
|
131
|
-
for (var
|
|
132
|
-
var lightness = _lightnessCandidates[
|
|
267
|
+
for (var _i3 = 0, _lightnessCandidates = lightnessCandidates; _i3 < _lightnessCandidates.length; _i3++) {
|
|
268
|
+
var lightness = _lightnessCandidates[_i3];
|
|
133
269
|
var candidate = (0, _Selectors.rgbString2hex)((0, _styles.hslToRgb)("hsl(".concat(Math.round(hue), ", ").concat(BORDER_SATURATION, "%, ").concat(lightness, "%)")));
|
|
134
270
|
if (candidate && (0, _styles.getContrastRatio)(averageHex, candidate) >= MIN_CONTRAST_RATIO) {
|
|
135
271
|
return candidate;
|
|
@@ -160,11 +296,31 @@ var getBorderColorForAverage = exports.getBorderColorForAverage = function getBo
|
|
|
160
296
|
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
161
297
|
* @param {string} fallbackColor
|
|
162
298
|
* @param {string} [mode] One of BorderColorModes
|
|
299
|
+
* @param {Array<{identifier: string}>} [designAreas] When set in contrast mode, one border is chosen for the pass
|
|
163
300
|
* @return {function(object): string}
|
|
164
301
|
*/
|
|
165
|
-
var createBorderColorResolver = exports.createBorderColorResolver = function createBorderColorResolver(getUnderlyingColor, fallbackColor, mode) {
|
|
302
|
+
var createBorderColorResolver = exports.createBorderColorResolver = function createBorderColorResolver(getUnderlyingColor, fallbackColor, mode, designAreas) {
|
|
303
|
+
var useUnifiedContrast = mode !== BorderColorModes.Complementary && designAreas && designAreas.length;
|
|
304
|
+
var unifiedBorderByIdentifier = null;
|
|
305
|
+
var getUnifiedBorderByIdentifier = function getUnifiedBorderByIdentifier() {
|
|
306
|
+
if (!useUnifiedContrast) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
var sampledCount = designAreas.filter(function (designArea) {
|
|
310
|
+
return (designArea === null || designArea === void 0 ? void 0 : designArea.identifier) && getUnderlyingColor(designArea.identifier);
|
|
311
|
+
}).length;
|
|
312
|
+
if (!unifiedBorderByIdentifier || sampledCount > 0 && unifiedBorderByIdentifier.size < sampledCount) {
|
|
313
|
+
unifiedBorderByIdentifier = computeUnifiedContrastBorders(getUnderlyingColor, designAreas);
|
|
314
|
+
}
|
|
315
|
+
return unifiedBorderByIdentifier;
|
|
316
|
+
};
|
|
166
317
|
return function (designArea) {
|
|
167
|
-
var
|
|
318
|
+
var identifier = designArea && designArea.identifier;
|
|
319
|
+
var unified = getUnifiedBorderByIdentifier();
|
|
320
|
+
if (unified && identifier && unified.has(identifier)) {
|
|
321
|
+
return unified.get(identifier);
|
|
322
|
+
}
|
|
323
|
+
var average = getUnderlyingColor(identifier);
|
|
168
324
|
return average ? getBorderColorForAverage(average, mode) : fallbackColor;
|
|
169
325
|
};
|
|
170
326
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@configuratorware/configurator-frontendgui",
|
|
3
|
-
"version": "1.57.
|
|
3
|
+
"version": "1.57.11",
|
|
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.11",
|
|
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.11",
|
|
40
|
+
"redhotmagma-visualization": "1.57.11",
|
|
41
41
|
"redux": "^4.1.0",
|
|
42
42
|
"redux-logger": "^3.0.6",
|
|
43
43
|
"redux-persist": "^5.10.0",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"css-loader": "^5.2.7",
|
|
65
65
|
"file-loader": "^6.2.0",
|
|
66
66
|
"mini-css-extract-plugin": "^2.7.6",
|
|
67
|
-
"
|
|
67
|
+
"sass": "1.32.0",
|
|
68
68
|
"react": "^18.2.0",
|
|
69
69
|
"react-dom": "^18.2.0",
|
|
70
70
|
"react-refresh": "^0.14.0",
|
|
@@ -9,7 +9,7 @@ import uniq from 'lodash/uniq';
|
|
|
9
9
|
import { t } from 'Framework/i18n';
|
|
10
10
|
import Api from 'Framework/Api';
|
|
11
11
|
import { createActionWithGlobalState } from 'Utils/Redux/createActionWithGlobalState';
|
|
12
|
-
import { getConf } from '../../configuration';
|
|
12
|
+
import { getConf, setConf } from '../../configuration';
|
|
13
13
|
import { Services } from '../../ServiceLocator';
|
|
14
14
|
import {
|
|
15
15
|
dismissError,
|
|
@@ -204,17 +204,29 @@ export const applyVariantAmountPreSelection = configurator => {
|
|
|
204
204
|
getChildren(configurator).map(variant => Object.values(getDeepestChildIdentifiers(variant)))
|
|
205
205
|
);
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
207
|
+
const applicableAmounts = Object.keys(itemAmounts)
|
|
208
|
+
.filter(identifier => children.indexOf(identifier) !== -1 && +itemAmounts[identifier] > 0)
|
|
209
|
+
.map(identifier => [identifier, +itemAmounts[identifier]]);
|
|
211
210
|
|
|
212
|
-
|
|
211
|
+
// designerPreSelection is parsed once from the URL and stays in conf for the whole session, so
|
|
212
|
+
// without consuming it here it would re-apply (and re-clear existing amounts) on every later
|
|
213
|
+
// configuration load, e.g. switching the configuration variant or loading a saved configuration
|
|
214
|
+
setConf('designerPreSelection.itemAmounts', undefined);
|
|
213
215
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
216
|
+
if (applicableAmounts.length === 0) {
|
|
217
|
+
return;
|
|
217
218
|
}
|
|
219
|
+
|
|
220
|
+
// only the explicitly passed amounts should apply, so clear whatever the item already had - by
|
|
221
|
+
// the keys actually selected, since a selected amount can be stored at any item level, not just
|
|
222
|
+
// on the deepest children
|
|
223
|
+
Object.keys(getSelectedAmounts(configurator) || {}).forEach(identifier =>
|
|
224
|
+
Services.configurator.setSelectedAmount(identifier, 0)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
applicableAmounts.forEach(([identifier, amount]) =>
|
|
228
|
+
Services.configurator.setSelectedAmount(identifier, amount)
|
|
229
|
+
);
|
|
218
230
|
}
|
|
219
231
|
};
|
|
220
232
|
|
|
@@ -127,6 +127,127 @@ describe('Reducers/Configurator/Actions', () => {
|
|
|
127
127
|
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_red_s', 2);
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
+
test('applyVariantAmountPreSelection clears the item default amount before applying the preselection', () => {
|
|
131
|
+
const mockConfigurator = {
|
|
132
|
+
configuration: {
|
|
133
|
+
item: {
|
|
134
|
+
itemGroup: {
|
|
135
|
+
children: [{ identifier: 'hoodie_white_default' }, { identifier: 'hoodie_black' }],
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
customdata: {
|
|
139
|
+
selectedAmounts: { hoodie_white_default: 1 },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const mockPreSelection = {
|
|
145
|
+
itemAmounts: {
|
|
146
|
+
hoodie_black: 50,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
setConf('designerPreSelection', mockPreSelection);
|
|
151
|
+
|
|
152
|
+
applyVariantAmountPreSelection(mockConfigurator);
|
|
153
|
+
|
|
154
|
+
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_white_default', 0);
|
|
155
|
+
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_black', 50);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('applyVariantAmountPreSelection clears a default amount stored at a non-leaf identifier', () => {
|
|
159
|
+
const mockConfigurator = {
|
|
160
|
+
configuration: {
|
|
161
|
+
item: {
|
|
162
|
+
itemGroup: {
|
|
163
|
+
children: [
|
|
164
|
+
{
|
|
165
|
+
identifier: 'hoodie_red',
|
|
166
|
+
itemGroup: {
|
|
167
|
+
children: [
|
|
168
|
+
{ identifier: 'hoodie_red_s', itemgroupentryIdentifier: 's' },
|
|
169
|
+
{ identifier: 'hoodie_red_m', itemgroupentryIdentifier: 'm' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
customdata: {
|
|
177
|
+
// the default amount is stored on the colour-level identifier, not a deepest child
|
|
178
|
+
selectedAmounts: { hoodie_red: 1 },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const mockPreSelection = {
|
|
184
|
+
itemAmounts: {
|
|
185
|
+
hoodie_red_s: 50,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
setConf('designerPreSelection', mockPreSelection);
|
|
190
|
+
|
|
191
|
+
applyVariantAmountPreSelection(mockConfigurator);
|
|
192
|
+
|
|
193
|
+
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_red', 0);
|
|
194
|
+
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_red_s', 50);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('applyVariantAmountPreSelection leaves existing amounts untouched when nothing in the preselection applies', () => {
|
|
198
|
+
const mockConfigurator = {
|
|
199
|
+
configuration: {
|
|
200
|
+
item: {
|
|
201
|
+
itemGroup: {
|
|
202
|
+
children: [{ identifier: 'hoodie_untouched' }],
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
customdata: {
|
|
206
|
+
selectedAmounts: { hoodie_untouched: 1 },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const mockPreSelection = {
|
|
212
|
+
itemAmounts: {
|
|
213
|
+
unknown_identifier: 50,
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
setConf('designerPreSelection', mockPreSelection);
|
|
218
|
+
|
|
219
|
+
const callsBefore = configuratorService.setSelectedAmount.mock.calls.length;
|
|
220
|
+
|
|
221
|
+
applyVariantAmountPreSelection(mockConfigurator);
|
|
222
|
+
|
|
223
|
+
expect(configuratorService.setSelectedAmount.mock.calls.length).toBe(callsBefore);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('applyVariantAmountPreSelection only applies the preselection once', () => {
|
|
227
|
+
const mockConfigurator = {
|
|
228
|
+
configuration: {
|
|
229
|
+
item: {
|
|
230
|
+
itemGroup: {
|
|
231
|
+
children: [{ identifier: 'hoodie_once' }],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
setConf('designerPreSelection', { itemAmounts: { hoodie_once: 7 } });
|
|
238
|
+
|
|
239
|
+
applyVariantAmountPreSelection(mockConfigurator);
|
|
240
|
+
expect(configuratorService.setSelectedAmount).toBeCalledWith('hoodie_once', 7);
|
|
241
|
+
|
|
242
|
+
const callsAfterFirstApply = configuratorService.setSelectedAmount.mock.calls.length;
|
|
243
|
+
|
|
244
|
+
// simulates a later configuration load (e.g. switching variant, loading a saved configuration) -
|
|
245
|
+
// designerPreSelection is still in conf, but its itemAmounts must have been consumed by now
|
|
246
|
+
applyVariantAmountPreSelection(mockConfigurator);
|
|
247
|
+
|
|
248
|
+
expect(configuratorService.setSelectedAmount.mock.calls.length).toBe(callsAfterFirstApply);
|
|
249
|
+
});
|
|
250
|
+
|
|
130
251
|
describe('receiveConfiguration', () => {
|
|
131
252
|
const configurator = {
|
|
132
253
|
configuration: {
|
|
@@ -1604,16 +1604,28 @@ export default class DesignDataService {
|
|
|
1604
1604
|
const showHighlight = get(state, 'designArea.showHighlight');
|
|
1605
1605
|
|
|
1606
1606
|
const staticBorderColor = getClientHighlightColor(state);
|
|
1607
|
+
const dynamicDesignAreaHighlightBorderColor = getConf(
|
|
1608
|
+
'visualization.dynamicDesignAreaHighlightBorderColor'
|
|
1609
|
+
);
|
|
1610
|
+
const dynamicDesignAreaHighlightBorderColorMode = getConf(
|
|
1611
|
+
'visualization.dynamicDesignAreaHighlightBorderColorMode'
|
|
1612
|
+
);
|
|
1613
|
+
const getDesignAreaUnderlyingColor = identifier =>
|
|
1614
|
+
Services.visualization.getDesignAreaUnderlyingColor(identifier);
|
|
1615
|
+
// 2D invokes drawingFn once per area with a single-element list; unify borders from the full view.
|
|
1616
|
+
const allDesignAreasForBorderColor = this.getDesignAreas();
|
|
1617
|
+
const borderColor = dynamicDesignAreaHighlightBorderColor
|
|
1618
|
+
? createBorderColorResolver(
|
|
1619
|
+
getDesignAreaUnderlyingColor,
|
|
1620
|
+
staticBorderColor,
|
|
1621
|
+
dynamicDesignAreaHighlightBorderColorMode,
|
|
1622
|
+
allDesignAreasForBorderColor
|
|
1623
|
+
)
|
|
1624
|
+
: staticBorderColor;
|
|
1607
1625
|
const textureBuilderConfig = merge(
|
|
1608
1626
|
{
|
|
1609
1627
|
highlight: {
|
|
1610
|
-
borderColor
|
|
1611
|
-
? createBorderColorResolver(
|
|
1612
|
-
identifier => Services.visualization.getDesignAreaUnderlyingColor(identifier),
|
|
1613
|
-
staticBorderColor,
|
|
1614
|
-
getConf('visualization.dynamicDesignAreaHighlightBorderColorMode')
|
|
1615
|
-
)
|
|
1616
|
-
: staticBorderColor,
|
|
1628
|
+
borderColor,
|
|
1617
1629
|
backgroundColor: getConf('visualization.designAreaHighlightBackgroundColor'),
|
|
1618
1630
|
lineWidth: getConf('visualization.designAreaHighlightBorderWidth', 2),
|
|
1619
1631
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BorderColorModes,
|
|
3
|
+
computeUnifiedContrastBorders,
|
|
3
4
|
createBorderColorResolver,
|
|
4
5
|
getBorderColorForAverage,
|
|
6
|
+
getContrastBorderColor,
|
|
5
7
|
invertColor,
|
|
6
8
|
rgbToHsl,
|
|
7
9
|
} from '../getDesignAreaBorderColor';
|
|
@@ -96,6 +98,70 @@ describe('getBorderColorForAverage in complementary mode', () => {
|
|
|
96
98
|
});
|
|
97
99
|
});
|
|
98
100
|
|
|
101
|
+
const grey = value => ({ r: value, g: value, b: value });
|
|
102
|
+
|
|
103
|
+
describe('computeUnifiedContrastBorders', () => {
|
|
104
|
+
test('uses one border for similar greys that would flip individually', () => {
|
|
105
|
+
const colors = {
|
|
106
|
+
left: grey(100),
|
|
107
|
+
right: grey(120),
|
|
108
|
+
};
|
|
109
|
+
const borders = computeUnifiedContrastBorders(id => colors[id], [
|
|
110
|
+
{ identifier: 'left' },
|
|
111
|
+
{ identifier: 'right' },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
expect(getContrastBorderColor('#646464')).toBe('#eeeeee');
|
|
115
|
+
expect(getContrastBorderColor('#787878')).toBe('#111111');
|
|
116
|
+
expect(borders.get('left')).toBe(borders.get('right'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('uses majority neutral when it is visible on every area', () => {
|
|
120
|
+
const colors = {
|
|
121
|
+
a: grey(50),
|
|
122
|
+
b: grey(60),
|
|
123
|
+
c: grey(128),
|
|
124
|
+
};
|
|
125
|
+
const borders = computeUnifiedContrastBorders(id => colors[id], [
|
|
126
|
+
{ identifier: 'a' },
|
|
127
|
+
{ identifier: 'b' },
|
|
128
|
+
{ identifier: 'c' },
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(borders.get('a')).toBe('#eeeeee');
|
|
132
|
+
expect(borders.get('b')).toBe('#eeeeee');
|
|
133
|
+
expect(borders.get('c')).toBe('#eeeeee');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('falls back to maximin when majority neutral fails on an area', () => {
|
|
137
|
+
const colors = {
|
|
138
|
+
dark: grey(30),
|
|
139
|
+
mid: grey(128),
|
|
140
|
+
};
|
|
141
|
+
const borders = computeUnifiedContrastBorders(id => colors[id], [
|
|
142
|
+
{ identifier: 'dark' },
|
|
143
|
+
{ identifier: 'mid' },
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
expect(borders.get('dark')).toBe('#eeeeee');
|
|
147
|
+
expect(borders.get('mid')).toBe('#eeeeee');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('allows per-area colors when no single neutral works everywhere', () => {
|
|
151
|
+
const colors = {
|
|
152
|
+
light: { r: 255, g: 255, b: 255 },
|
|
153
|
+
dark: { r: 0, g: 0, b: 0 },
|
|
154
|
+
};
|
|
155
|
+
const borders = computeUnifiedContrastBorders(id => colors[id], [
|
|
156
|
+
{ identifier: 'light' },
|
|
157
|
+
{ identifier: 'dark' },
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
expect(borders.get('light')).toBe('#111111');
|
|
161
|
+
expect(borders.get('dark')).toBe('#eeeeee');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
99
165
|
describe('createBorderColorResolver', () => {
|
|
100
166
|
test('resolves the border color from the underlying color of the design area', () => {
|
|
101
167
|
const getUnderlyingColor = jest.fn().mockReturnValue(JACKET_GREEN);
|
|
@@ -125,4 +191,75 @@ describe('createBorderColorResolver', () => {
|
|
|
125
191
|
test('falls back to the given color without a design area', () => {
|
|
126
192
|
expect(createBorderColorResolver(() => null, '#abcdef')()).toBe('#abcdef');
|
|
127
193
|
});
|
|
194
|
+
|
|
195
|
+
test('unifies contrast borders across design areas when the list is given', () => {
|
|
196
|
+
const colors = {
|
|
197
|
+
left: grey(100),
|
|
198
|
+
right: grey(120),
|
|
199
|
+
};
|
|
200
|
+
const resolve = createBorderColorResolver(id => colors[id], '#abcdef', BorderColorModes.Contrast, [
|
|
201
|
+
{ identifier: 'left' },
|
|
202
|
+
{ identifier: 'right' },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
expect(resolve({ identifier: 'left' })).toBe(resolve({ identifier: 'right' }));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('unifies when resolved one area at a time like 2D drawing passes', () => {
|
|
209
|
+
const colors = {
|
|
210
|
+
chestLeft: grey(100),
|
|
211
|
+
chestRight: grey(120),
|
|
212
|
+
collar: grey(115),
|
|
213
|
+
};
|
|
214
|
+
const allAreas = [
|
|
215
|
+
{ identifier: 'chestLeft' },
|
|
216
|
+
{ identifier: 'chestRight' },
|
|
217
|
+
{ identifier: 'collar' },
|
|
218
|
+
];
|
|
219
|
+
const resolve = createBorderColorResolver(
|
|
220
|
+
id => colors[id],
|
|
221
|
+
'#abcdef',
|
|
222
|
+
BorderColorModes.Contrast,
|
|
223
|
+
allAreas
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const chestLeftBorder = resolve({ identifier: 'chestLeft' });
|
|
227
|
+
const chestRightBorder = resolve({ identifier: 'chestRight' });
|
|
228
|
+
const collarBorder = resolve({ identifier: 'collar' });
|
|
229
|
+
|
|
230
|
+
expect(chestLeftBorder).toBe(chestRightBorder);
|
|
231
|
+
expect(chestRightBorder).toBe(collarBorder);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('resolves per area when design areas are omitted', () => {
|
|
235
|
+
const resolve = createBorderColorResolver(
|
|
236
|
+
id => (id === 'left' ? grey(100) : grey(120)),
|
|
237
|
+
'#abcdef',
|
|
238
|
+
BorderColorModes.Contrast
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(resolve({ identifier: 'left' })).toBe('#eeeeee');
|
|
242
|
+
expect(resolve({ identifier: 'right' })).toBe('#111111');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('does not unify borders in complementary mode', () => {
|
|
246
|
+
const colors = {
|
|
247
|
+
left: grey(100),
|
|
248
|
+
right: grey(120),
|
|
249
|
+
};
|
|
250
|
+
const resolve = createBorderColorResolver(
|
|
251
|
+
id => colors[id],
|
|
252
|
+
'#abcdef',
|
|
253
|
+
BorderColorModes.Complementary,
|
|
254
|
+
[{ identifier: 'left' }, { identifier: 'right' }]
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(resolve({ identifier: 'left' })).toBe(
|
|
258
|
+
getBorderColorForAverage(colors.left, BorderColorModes.Complementary)
|
|
259
|
+
);
|
|
260
|
+
expect(resolve({ identifier: 'right' })).toBe(
|
|
261
|
+
getBorderColorForAverage(colors.right, BorderColorModes.Complementary)
|
|
262
|
+
);
|
|
263
|
+
expect(resolve({ identifier: 'left' })).not.toBe(resolve({ identifier: 'right' }));
|
|
264
|
+
});
|
|
128
265
|
});
|
|
@@ -95,6 +95,105 @@ export const getContrastBorderColor = averageHex =>
|
|
|
95
95
|
? NEUTRAL_DARK
|
|
96
96
|
: NEUTRAL_LIGHT;
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} neutral
|
|
100
|
+
* @param {Array<{averageHex: string}>} areas
|
|
101
|
+
* @return {number}
|
|
102
|
+
*/
|
|
103
|
+
const minContrastForNeutralAcrossAreas = (neutral, areas) =>
|
|
104
|
+
Math.min(...areas.map(({ averageHex }) => getContrastRatio(averageHex, neutral)));
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} neutral
|
|
108
|
+
* @param {Array<{averageHex: string}>} areas
|
|
109
|
+
* @return {boolean}
|
|
110
|
+
*/
|
|
111
|
+
const neutralMeetsMinContrastOnAllAreas = (neutral, areas) =>
|
|
112
|
+
minContrastForNeutralAcrossAreas(neutral, areas) >= MIN_CONTRAST_RATIO;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Picks one near-black or near-white border for every design area in a highlight pass when possible,
|
|
116
|
+
* so similar product shades do not flip border color at the luminance tie-break.
|
|
117
|
+
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
118
|
+
* @param {Array<{identifier: string}>} designAreas
|
|
119
|
+
* @return {Map<string, string>}
|
|
120
|
+
*/
|
|
121
|
+
export const computeUnifiedContrastBorders = (getUnderlyingColor, designAreas) => {
|
|
122
|
+
const areas = [];
|
|
123
|
+
|
|
124
|
+
for (const designArea of designAreas) {
|
|
125
|
+
const identifier = designArea && designArea.identifier;
|
|
126
|
+
if (!identifier) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const average = getUnderlyingColor(identifier);
|
|
130
|
+
if (!average) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
areas.push({
|
|
134
|
+
identifier,
|
|
135
|
+
averageHex: rgbToHex(average.r, average.g, average.b),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const borderByIdentifier = new Map();
|
|
140
|
+
|
|
141
|
+
if (!areas.length) {
|
|
142
|
+
return borderByIdentifier;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let darkVotes = 0;
|
|
146
|
+
let lightVotes = 0;
|
|
147
|
+
|
|
148
|
+
for (const { averageHex } of areas) {
|
|
149
|
+
const vote = getContrastBorderColor(averageHex);
|
|
150
|
+
if (vote === NEUTRAL_DARK) {
|
|
151
|
+
darkVotes += 1;
|
|
152
|
+
} else {
|
|
153
|
+
lightVotes += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let majorityNeutral;
|
|
158
|
+
if (darkVotes > lightVotes) {
|
|
159
|
+
majorityNeutral = NEUTRAL_DARK;
|
|
160
|
+
} else if (lightVotes > darkVotes) {
|
|
161
|
+
majorityNeutral = NEUTRAL_LIGHT;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (majorityNeutral && neutralMeetsMinContrastOnAllAreas(majorityNeutral, areas)) {
|
|
165
|
+
for (const { identifier } of areas) {
|
|
166
|
+
borderByIdentifier.set(identifier, majorityNeutral);
|
|
167
|
+
}
|
|
168
|
+
return borderByIdentifier;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const qualifyingNeutrals = [NEUTRAL_DARK, NEUTRAL_LIGHT].filter(neutral =>
|
|
172
|
+
neutralMeetsMinContrastOnAllAreas(neutral, areas)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (qualifyingNeutrals.length) {
|
|
176
|
+
const maximinNeutral = qualifyingNeutrals.reduce((best, neutral) => {
|
|
177
|
+
const minContrast = minContrastForNeutralAcrossAreas(neutral, areas);
|
|
178
|
+
if (!best) {
|
|
179
|
+
return { neutral, minContrast };
|
|
180
|
+
}
|
|
181
|
+
return minContrast > best.minContrast ? { neutral, minContrast } : best;
|
|
182
|
+
}, null).neutral;
|
|
183
|
+
|
|
184
|
+
for (const { identifier } of areas) {
|
|
185
|
+
borderByIdentifier.set(identifier, maximinNeutral);
|
|
186
|
+
}
|
|
187
|
+
return borderByIdentifier;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const { identifier, averageHex } of areas) {
|
|
191
|
+
borderByIdentifier.set(identifier, getContrastBorderColor(averageHex));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return borderByIdentifier;
|
|
195
|
+
};
|
|
196
|
+
|
|
98
197
|
/**
|
|
99
198
|
* Keeps the hue of the inverted (complementary) color at a high saturation and moves the lightness
|
|
100
199
|
* away from the average, only as far as needed to reach a visible contrast, so the border is a vivid
|
|
@@ -150,9 +249,38 @@ export const getBorderColorForAverage = (average, mode = BorderColorModes.Contra
|
|
|
150
249
|
* @param {function(string): ({r: number, g: number, b: number}|null|undefined)} getUnderlyingColor
|
|
151
250
|
* @param {string} fallbackColor
|
|
152
251
|
* @param {string} [mode] One of BorderColorModes
|
|
252
|
+
* @param {Array<{identifier: string}>} [designAreas] When set in contrast mode, one border is chosen for the pass
|
|
153
253
|
* @return {function(object): string}
|
|
154
254
|
*/
|
|
155
|
-
export const createBorderColorResolver = (getUnderlyingColor, fallbackColor, mode) =>
|
|
156
|
-
const
|
|
157
|
-
|
|
255
|
+
export const createBorderColorResolver = (getUnderlyingColor, fallbackColor, mode, designAreas) => {
|
|
256
|
+
const useUnifiedContrast = mode !== BorderColorModes.Complementary && designAreas && designAreas.length;
|
|
257
|
+
let unifiedBorderByIdentifier = null;
|
|
258
|
+
|
|
259
|
+
const getUnifiedBorderByIdentifier = () => {
|
|
260
|
+
if (!useUnifiedContrast) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const sampledCount = designAreas.filter(
|
|
264
|
+
designArea => designArea?.identifier && getUnderlyingColor(designArea.identifier)
|
|
265
|
+
).length;
|
|
266
|
+
if (
|
|
267
|
+
!unifiedBorderByIdentifier ||
|
|
268
|
+
(sampledCount > 0 && unifiedBorderByIdentifier.size < sampledCount)
|
|
269
|
+
) {
|
|
270
|
+
unifiedBorderByIdentifier = computeUnifiedContrastBorders(getUnderlyingColor, designAreas);
|
|
271
|
+
}
|
|
272
|
+
return unifiedBorderByIdentifier;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return designArea => {
|
|
276
|
+
const identifier = designArea && designArea.identifier;
|
|
277
|
+
const unified = getUnifiedBorderByIdentifier();
|
|
278
|
+
|
|
279
|
+
if (unified && identifier && unified.has(identifier)) {
|
|
280
|
+
return unified.get(identifier);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const average = getUnderlyingColor(identifier);
|
|
284
|
+
return average ? getBorderColorForAverage(average, mode) : fallbackColor;
|
|
285
|
+
};
|
|
158
286
|
};
|