mirror of https://github.com/rancher/dashboard.git
Add support for svg icons (#7769)
* Fix lint * Fix bad merge * Fix bad merge * Fix issue with custom color
This commit is contained in:
parent
38ad49d2f5
commit
9f3922424a
|
|
@ -315,6 +315,7 @@ BODY, .theme-light {
|
||||||
--header-bg : #{$lightest};
|
--header-bg : #{$lightest};
|
||||||
--header-btn-bg : transparent;
|
--header-btn-bg : transparent;
|
||||||
--header-btn-text : #{$darkest};
|
--header-btn-text : #{$darkest};
|
||||||
|
--header-btn-text-hover : #{$lightest};
|
||||||
--header-input-text : #{$darkest};
|
--header-input-text : #{$darkest};
|
||||||
--header-height : 55px;
|
--header-height : 55px;
|
||||||
--header-border : #{$medium};
|
--header-border : #{$medium};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders the icon in the top level menu.
|
||||||
|
* Icon can either be via a font via the 'icon' property or an svg via the 'src' property
|
||||||
|
*
|
||||||
|
* The trickiness here is that we want the icon to be the correct color - both normally and when hovered
|
||||||
|
* For a font icon, this is easy, since we just set the color css property
|
||||||
|
* For an svg icon included with the <img> tag this is harder - there is no way to apply css to
|
||||||
|
* the svg brought in this way - the workaround is to apply a css filter - in order to do this we
|
||||||
|
* need to generate the css filter for the required color - the code for that is in the 'svg-filter' utility
|
||||||
|
*
|
||||||
|
* We cache filters and css for given colors, so we only generate them once.
|
||||||
|
*
|
||||||
|
* This makes the code here look complex - but we are essentially generating the css filters
|
||||||
|
* and then injecting custom css into the document so that any icons included via svg will
|
||||||
|
* show with the desired colors for the theme.
|
||||||
|
*/
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { Solver } from '@shell/utils/svg-filter';
|
||||||
|
import { colorToRgb, mapStandardColors } from '@shell/utils/color';
|
||||||
|
|
||||||
|
const filterCache = {};
|
||||||
|
const cssCache = {};
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
header: {
|
||||||
|
color: '--header-btn-text',
|
||||||
|
hover: '--header-btn-text-hover'
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
color: '--link',
|
||||||
|
hover: '--primary-hover-text'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'IconOrSvg',
|
||||||
|
props: {
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
default: () => undefined,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: () => undefined,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: () => 'primary',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return { className: '' };
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
if (this.src) {
|
||||||
|
this.setColor();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
setColor() {
|
||||||
|
const uiColor = mapStandardColors(getComputedStyle(document.body).getPropertyValue(colors[this.color].color).trim());
|
||||||
|
const hoverColor = mapStandardColors(getComputedStyle(document.body).getPropertyValue(colors[this.color].hover).trim());
|
||||||
|
|
||||||
|
const uiColorRGB = colorToRgb(uiColor);
|
||||||
|
const hoverColorRGB = colorToRgb(hoverColor);
|
||||||
|
const uiColorStr = `${ uiColorRGB.r }-${ uiColorRGB.g }-${ uiColorRGB.b }`;
|
||||||
|
const hoverColorStr = `${ hoverColorRGB.r }-${ hoverColorRGB.g }-${ hoverColorRGB.b }`;
|
||||||
|
|
||||||
|
const className = `svg-icon-${ uiColorStr }-${ hoverColorStr }`;
|
||||||
|
|
||||||
|
if (!cssCache[className]) {
|
||||||
|
let hoverFilter = filterCache[hoverColor];
|
||||||
|
|
||||||
|
if (!hoverFilter) {
|
||||||
|
const solver = new Solver(hoverColorRGB);
|
||||||
|
const res = solver.solve();
|
||||||
|
|
||||||
|
hoverFilter = res?.filter;
|
||||||
|
filterCache[hoverColor] = hoverFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainFilter = filterCache[uiColor];
|
||||||
|
|
||||||
|
if (!mainFilter) {
|
||||||
|
const solver = new Solver(uiColorRGB);
|
||||||
|
const res = solver.solve();
|
||||||
|
|
||||||
|
mainFilter = res?.filter;
|
||||||
|
filterCache[uiColor] = mainFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add stylesheet (added as global styles)
|
||||||
|
const styles = `
|
||||||
|
img.${ className } {
|
||||||
|
${ mainFilter };
|
||||||
|
}
|
||||||
|
img.${ className }:hover {
|
||||||
|
${ hoverFilter };
|
||||||
|
}
|
||||||
|
button:hover > img.${ className } {
|
||||||
|
${ hoverFilter };
|
||||||
|
}
|
||||||
|
a.option:hover > img.${ className } {
|
||||||
|
${ hoverFilter };
|
||||||
|
} `;
|
||||||
|
|
||||||
|
const styleSheet = document.createElement('style');
|
||||||
|
|
||||||
|
styleSheet.innerText = styles;
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
|
||||||
|
cssCache[className] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vue.set(this, 'className', className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<img
|
||||||
|
v-if="src"
|
||||||
|
:src="src"
|
||||||
|
class="svg-icon"
|
||||||
|
:class="className"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-else-if="icon"
|
||||||
|
class="icon group-icon"
|
||||||
|
:class="icon"
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="icon icon-extension"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.svg-icon {
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -49,7 +49,7 @@ export default {
|
||||||
searchShortcut,
|
searchShortcut,
|
||||||
shellShortcut,
|
shellShortcut,
|
||||||
LOGGED_OUT,
|
LOGGED_OUT,
|
||||||
navHeaderRight: null
|
navHeaderRight: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -814,6 +814,11 @@ export default {
|
||||||
background-color: var(--success);
|
background-color: var(--success);
|
||||||
color: var(--success-text);
|
color: var(--success-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import BrandImage from '@shell/components/BrandImage';
|
import BrandImage from '@shell/components/BrandImage';
|
||||||
import ClusterProviderIcon from '@shell/components/ClusterProviderIcon';
|
import ClusterProviderIcon from '@shell/components/ClusterProviderIcon';
|
||||||
|
import IconOrSvg from '../IconOrSvg';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
||||||
|
|
@ -16,7 +17,11 @@ import { isRancherPrime } from '@shell/config/version';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
components: { BrandImage, ClusterProviderIcon },
|
components: {
|
||||||
|
BrandImage,
|
||||||
|
ClusterProviderIcon,
|
||||||
|
IconOrSvg
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
const { displayVersion, fullVersion } = getVersionInfo(this.$store);
|
const { displayVersion, fullVersion } = getVersionInfo(this.$store);
|
||||||
|
|
@ -139,6 +144,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
|
label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
|
||||||
icon: `icon-${ p.icon || 'copy' }`,
|
icon: `icon-${ p.icon || 'copy' }`,
|
||||||
|
svg: p.svg,
|
||||||
value: p.name,
|
value: p.name,
|
||||||
removable: p.removable !== false,
|
removable: p.removable !== false,
|
||||||
inStore: p.inStore || 'cluster',
|
inStore: p.inStore || 'cluster',
|
||||||
|
|
@ -341,9 +347,9 @@ export default {
|
||||||
class="option"
|
class="option"
|
||||||
:to="a.to"
|
:to="a.to"
|
||||||
>
|
>
|
||||||
<i
|
<IconOrSvg
|
||||||
class="icon group-icon"
|
:icon="a.icon"
|
||||||
:class="a.icon"
|
:src="a.svg"
|
||||||
/>
|
/>
|
||||||
<div>{{ a.label }}</div>
|
<div>{{ a.label }}</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -362,9 +368,9 @@ export default {
|
||||||
class="option"
|
class="option"
|
||||||
:to="a.to"
|
:to="a.to"
|
||||||
>
|
>
|
||||||
<i
|
<IconOrSvg
|
||||||
class="icon group-icon"
|
:icon="a.icon"
|
||||||
:class="a.icon"
|
:src="a.svg"
|
||||||
/>
|
/>
|
||||||
<div>{{ a.label }}</div>
|
<div>{{ a.label }}</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -383,9 +389,9 @@ export default {
|
||||||
class="option"
|
class="option"
|
||||||
:to="a.to"
|
:to="a.to"
|
||||||
>
|
>
|
||||||
<i
|
<IconOrSvg
|
||||||
class="icon group-icon"
|
:icon="a.icon"
|
||||||
:class="a.icon"
|
:src="a.svg"
|
||||||
/>
|
/>
|
||||||
<div>{{ a.label }}</div>
|
<div>{{ a.label }}</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -499,6 +505,9 @@ export default {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
fill: var(--link);
|
fill: var(--link);
|
||||||
}
|
}
|
||||||
|
img {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,11 @@ const DARK_CONTRAST_COLORS = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STANDARD_COLORS = {
|
||||||
|
black: '#000000',
|
||||||
|
white: '#ffffff',
|
||||||
|
};
|
||||||
|
|
||||||
// contrastColor(color, {light, dark}) returns which of 2 options is higher contrast with color
|
// contrastColor(color, {light, dark}) returns which of 2 options is higher contrast with color
|
||||||
export function contrastColor(color, contrastOptions = LIGHT_CONTRAST_COLORS) {
|
export function contrastColor(color, contrastOptions = LIGHT_CONTRAST_COLORS) {
|
||||||
let out = contrastOptions.light;
|
let out = contrastOptions.light;
|
||||||
|
|
@ -90,3 +95,43 @@ export function textColor(color) {
|
||||||
|
|
||||||
return (brightness > 125) ? 'black' : 'white';
|
return (brightness > 125) ? 'black' : 'white';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapStandardColors(color) {
|
||||||
|
return STANDARD_COLORS[color] || color;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rgbToRgb(rgb) {
|
||||||
|
const result = /^rgb\(([0-9]{1,3}),\s*([0-9]{1,3}),\s*([0-9]{1,3})\)$/i.exec(rgb);
|
||||||
|
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 10),
|
||||||
|
g: parseInt(result[2], 10),
|
||||||
|
b: parseInt(result[3], 10)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function colorToRgb(color) {
|
||||||
|
let value;
|
||||||
|
|
||||||
|
if (color.startsWith('rgb(')) {
|
||||||
|
value = rgbToRgb(color);
|
||||||
|
} else if (color.startsWith('#')) {
|
||||||
|
value = hexToRgb(color);
|
||||||
|
} else {
|
||||||
|
console.warn(`Unable to parse color: ${ color }`); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
|
||||||
|
return value || {
|
||||||
|
r: 0, g: 0, b: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Utilities to generate a css filter to give a required color
|
||||||
|
|
||||||
|
class Color {
|
||||||
|
constructor(r, g, b) {
|
||||||
|
this.set(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `rgb(${ Math.round(this.r) }, ${ Math.round(this.g) }, ${ Math.round(this.b) })`;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(r, g, b) {
|
||||||
|
this.r = this.clamp(r);
|
||||||
|
this.g = this.clamp(g);
|
||||||
|
this.b = this.clamp(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
hueRotate(angle = 0) {
|
||||||
|
angle = angle / 180 * Math.PI;
|
||||||
|
const sin = Math.sin(angle);
|
||||||
|
const cos = Math.cos(angle);
|
||||||
|
|
||||||
|
this.multiply([
|
||||||
|
0.213 + cos * 0.787 - sin * 0.213,
|
||||||
|
0.715 - cos * 0.715 - sin * 0.715,
|
||||||
|
0.072 - cos * 0.072 + sin * 0.928,
|
||||||
|
0.213 - cos * 0.213 + sin * 0.143,
|
||||||
|
0.715 + cos * 0.285 + sin * 0.140,
|
||||||
|
0.072 - cos * 0.072 - sin * 0.283,
|
||||||
|
0.213 - cos * 0.213 - sin * 0.787,
|
||||||
|
0.715 - cos * 0.715 + sin * 0.715,
|
||||||
|
0.072 + cos * 0.928 + sin * 0.072,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
grayscale(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.2126 + 0.7874 * (1 - value),
|
||||||
|
0.7152 - 0.7152 * (1 - value),
|
||||||
|
0.0722 - 0.0722 * (1 - value),
|
||||||
|
0.2126 - 0.2126 * (1 - value),
|
||||||
|
0.7152 + 0.2848 * (1 - value),
|
||||||
|
0.0722 - 0.0722 * (1 - value),
|
||||||
|
0.2126 - 0.2126 * (1 - value),
|
||||||
|
0.7152 - 0.7152 * (1 - value),
|
||||||
|
0.0722 + 0.9278 * (1 - value),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sepia(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.393 + 0.607 * (1 - value),
|
||||||
|
0.769 - 0.769 * (1 - value),
|
||||||
|
0.189 - 0.189 * (1 - value),
|
||||||
|
0.349 - 0.349 * (1 - value),
|
||||||
|
0.686 + 0.314 * (1 - value),
|
||||||
|
0.168 - 0.168 * (1 - value),
|
||||||
|
0.272 - 0.272 * (1 - value),
|
||||||
|
0.534 - 0.534 * (1 - value),
|
||||||
|
0.131 + 0.869 * (1 - value),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
saturate(value = 1) {
|
||||||
|
this.multiply([
|
||||||
|
0.213 + 0.787 * value,
|
||||||
|
0.715 - 0.715 * value,
|
||||||
|
0.072 - 0.072 * value,
|
||||||
|
0.213 - 0.213 * value,
|
||||||
|
0.715 + 0.285 * value,
|
||||||
|
0.072 - 0.072 * value,
|
||||||
|
0.213 - 0.213 * value,
|
||||||
|
0.715 - 0.715 * value,
|
||||||
|
0.072 + 0.928 * value,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiply(matrix) {
|
||||||
|
const newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
|
||||||
|
const newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
|
||||||
|
const newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
|
||||||
|
|
||||||
|
this.r = newR;
|
||||||
|
this.g = newG;
|
||||||
|
this.b = newB;
|
||||||
|
}
|
||||||
|
|
||||||
|
brightness(value = 1) {
|
||||||
|
this.linear(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
contrast(value = 1) {
|
||||||
|
this.linear(value, -(0.5 * value) + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
linear(slope = 1, intercept = 0) {
|
||||||
|
this.r = this.clamp(this.r * slope + intercept * 255);
|
||||||
|
this.g = this.clamp(this.g * slope + intercept * 255);
|
||||||
|
this.b = this.clamp(this.b * slope + intercept * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
invert(value = 1) {
|
||||||
|
this.r = this.clamp((value + this.r / 255 * (1 - 2 * value)) * 255);
|
||||||
|
this.g = this.clamp((value + this.g / 255 * (1 - 2 * value)) * 255);
|
||||||
|
this.b = this.clamp((value + this.b / 255 * (1 - 2 * value)) * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
hsl() {
|
||||||
|
// Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
|
||||||
|
const r = this.r / 255;
|
||||||
|
const g = this.g / 255;
|
||||||
|
const b = this.b / 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const l = (max + min) / 2;
|
||||||
|
let h = l;
|
||||||
|
let s = l;
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = s = 0;
|
||||||
|
} else {
|
||||||
|
const d = max - min;
|
||||||
|
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
h: h * 100,
|
||||||
|
s: s * 100,
|
||||||
|
l: l * 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clamp(value) {
|
||||||
|
if (value > 255) {
|
||||||
|
value = 255;
|
||||||
|
} else if (value < 0) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Solver {
|
||||||
|
constructor(rgb) {
|
||||||
|
this.target = new Color(rgb.r, rgb.g, rgb.b);
|
||||||
|
this.targetHSL = this.target.hsl();
|
||||||
|
this.reusedColor = new Color(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
solve() {
|
||||||
|
const result = this.solveNarrow(this.solveWide());
|
||||||
|
|
||||||
|
return {
|
||||||
|
values: result.values,
|
||||||
|
loss: result.loss,
|
||||||
|
filter: this.css(result.values),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
solveWide() {
|
||||||
|
const A = 5;
|
||||||
|
const c = 15;
|
||||||
|
const a = [60, 180, 18000, 600, 1.2, 1.2];
|
||||||
|
|
||||||
|
let best = { loss: Infinity };
|
||||||
|
|
||||||
|
for (let i = 0; best.loss > 25 && i < 3; i++) {
|
||||||
|
const initial = [50, 20, 3750, 50, 100, 100];
|
||||||
|
const result = this.spsa(A, a, c, initial, 1000);
|
||||||
|
|
||||||
|
if (result.loss < best.loss) {
|
||||||
|
best = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
solveNarrow(wide) {
|
||||||
|
const A = wide.loss;
|
||||||
|
const c = 2;
|
||||||
|
const A1 = A + 1;
|
||||||
|
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
|
||||||
|
|
||||||
|
return this.spsa(A, a, c, wide.values, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
spsa(A, a, c, values, iters) {
|
||||||
|
const alpha = 1;
|
||||||
|
const gamma = 0.16666666666666666;
|
||||||
|
|
||||||
|
let best = null;
|
||||||
|
let bestLoss = Infinity;
|
||||||
|
const deltas = new Array(6);
|
||||||
|
const highArgs = new Array(6);
|
||||||
|
const lowArgs = new Array(6);
|
||||||
|
|
||||||
|
for (let k = 0; k < iters; k++) {
|
||||||
|
const ck = c / Math.pow(k + 1, gamma);
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
deltas[i] = Math.random() > 0.5 ? 1 : -1;
|
||||||
|
highArgs[i] = values[i] + ck * deltas[i];
|
||||||
|
lowArgs[i] = values[i] - ck * deltas[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lossDiff = this.loss(highArgs) - this.loss(lowArgs);
|
||||||
|
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const g = lossDiff / (2 * ck) * deltas[i];
|
||||||
|
const ak = a[i] / Math.pow(A + k + 1, alpha);
|
||||||
|
|
||||||
|
values[i] = fix(values[i] - ak * g, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loss = this.loss(values);
|
||||||
|
|
||||||
|
if (loss < bestLoss) {
|
||||||
|
best = values.slice(0);
|
||||||
|
bestLoss = loss;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { values: best, loss: bestLoss };
|
||||||
|
|
||||||
|
function fix(value, idx) {
|
||||||
|
let max = 100;
|
||||||
|
|
||||||
|
if (idx === 2 /* saturate */) {
|
||||||
|
max = 7500;
|
||||||
|
} else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
|
||||||
|
max = 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idx === 3 /* hue-rotate */) {
|
||||||
|
if (value > max) {
|
||||||
|
value %= max;
|
||||||
|
} else if (value < 0) {
|
||||||
|
value = max + value % max;
|
||||||
|
}
|
||||||
|
} else if (value < 0) {
|
||||||
|
value = 0;
|
||||||
|
} else if (value > max) {
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loss(filters) {
|
||||||
|
// Argument is array of percentages.
|
||||||
|
const color = this.reusedColor;
|
||||||
|
|
||||||
|
color.set(0, 0, 0);
|
||||||
|
|
||||||
|
color.invert(filters[0] / 100);
|
||||||
|
color.sepia(filters[1] / 100);
|
||||||
|
color.saturate(filters[2] / 100);
|
||||||
|
color.hueRotate(filters[3] * 3.6);
|
||||||
|
color.brightness(filters[4] / 100);
|
||||||
|
color.contrast(filters[5] / 100);
|
||||||
|
|
||||||
|
const colorHSL = color.hsl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
Math.abs(color.r - this.target.r) +
|
||||||
|
Math.abs(color.g - this.target.g) +
|
||||||
|
Math.abs(color.b - this.target.b) +
|
||||||
|
Math.abs(colorHSL.h - this.targetHSL.h) +
|
||||||
|
Math.abs(colorHSL.s - this.targetHSL.s) +
|
||||||
|
Math.abs(colorHSL.l - this.targetHSL.l)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
css(filters) {
|
||||||
|
function fmt(idx, multiplier = 1) {
|
||||||
|
return Math.round(filters[idx] * multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `filter: invert(${ fmt(0) }%) sepia(${ fmt(1) }%) saturate(${ fmt(2) }%) hue-rotate(${ fmt(3, 3.6) }deg) brightness(${ fmt(4) }%) contrast(${ fmt(5) }%);`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue