Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SVG filter matrix to color monochrome images #1707

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@
"ext/js/core/event-listener-collection.js",
"ext/js/core/extension-error.js",
"ext/js/core/json.js",
"ext/js/core/utilities.js",
"ext/js/data/anki-note-data-creator.js",
"ext/js/data/array-buffer-util.js",
"ext/js/dictionary/dictionary-data-util.js",
Expand Down
8 changes: 3 additions & 5 deletions ext/css/structured-content.css
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,9 @@
white-space: pre-line;
}

.gloss-image-link[data-appearance=monochrome] .gloss-image {
/* Workaround for coloring monochrome gloss images due to issues with masking using a canvas without loading extra media */
/* drop-shadow with 0.01px blur is at minimum required for Firefox to render the shadow when used on a canvas */
--shadow-settings: 0 0 0.01px var(--text-color);
filter: grayscale(1) opacity(0.5) drop-shadow(var(--shadow-settings)) drop-shadow(var(--shadow-settings)) saturate(1000%) brightness(1000%);
.monochrome-svg-filter>svg {
width: 0;
height: 0;
}

.gloss-image-link[data-size-units=em] .gloss-image-container {
Expand Down
13 changes: 9 additions & 4 deletions ext/data/structured-content-style.json
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,10 @@
]
},
{
"selectors": [".gloss-image-link[data-appearance=monochrome] .gloss-image"],
"selectors": [".monochrome-svg-filter>svg"],
"styles": [
["--shadow-settings", "0 0 0.01px var(--text-color)"],
["filter", "grayscale(1) opacity(0.5) drop-shadow(var(--shadow-settings)) drop-shadow(var(--shadow-settings)) saturate(1000%) brightness(1000%)"],
["opacity", "0"]
["width", "0"],
["height", "0"]
]
},
{
Expand Down Expand Up @@ -335,5 +334,11 @@
"styles": [
["display", "none"]
]
},
{
"selectors": [".gloss-image-link[data-appearance=monochrome] .gloss-image"],
"styles": [
["opacity", "0"]
]
}
]
22 changes: 3 additions & 19 deletions ext/js/app/theme-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import {getColorInfo} from '../core/utilities.js';

/**
* This class is used to control theme attributes on DOM elements.
*/
Expand Down Expand Up @@ -188,7 +190,7 @@ export class ThemeController {
_addColor(target, cssColor) {
if (typeof cssColor !== 'string') { return; }

const color = this._getColorInfo(cssColor);
const color = getColorInfo(cssColor);
if (color === null) { return; }

const a = color[3];
Expand All @@ -199,22 +201,4 @@ export class ThemeController {
target[i] = target[i] * aInv + color[i] * a;
}
}

/**
* Decomposes a CSS color string into its RGBA values.
* @param {string} cssColor The color value to decompose. This value is expected to be in the form RGB(r, g, b) or RGBA(r, g, b, a).
* @returns {?number[]} The color and alpha values as [r, g, b, a]. The color component values range from [0, 255], and the alpha ranges from [0, 1].
*/
_getColorInfo(cssColor) {
const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)\s*$/.exec(cssColor);
if (m === null) { return null; }

const m4 = m[4];
return [
Number.parseInt(m[1], 10),
Number.parseInt(m[2], 10),
Number.parseInt(m[3], 10),
m4 ? Math.max(0, Math.min(1, Number.parseFloat(m4))) : 1,
];
}
}
49 changes: 49 additions & 0 deletions ext/js/core/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,52 @@ export function deferPromise() {
export function promiseTimeout(delay) {
return delay <= 0 ? Promise.resolve() : new Promise((resolve) => { setTimeout(resolve, delay); });
}

/**
* Decomposes a CSS color string into its RGBA values.
* @param {string} cssColor The color value to decompose. This value is expected to be in the form RGB(r, g, b), RGBA(r, g, b, a), or #rrggbb.
* @returns {?number[]} The color and alpha values as [r, g, b, a]. The color component values range from [0, 255], and the alpha ranges from [0, 1].
*/
export function getColorInfo(cssColor) {
const m = /^\s*rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)\s*$/.exec(cssColor);
if (m === null) { return getColorInfoHex(cssColor); }

const m4 = m[4];
return [
Number.parseInt(m[1], 10),
Number.parseInt(m[2], 10),
Number.parseInt(m[3], 10),
m4 ? Math.max(0, Math.min(1, Number.parseFloat(m4))) : 1,
];
}

/**
* Decomposes a CSS hex color string into its RGBA values.
* @param {string} cssColorHex The color value to decompose. This value is expected to be in the form #rrggbb.
* @returns {?number[]} The color and alpha values as [r, g, b, a]. The color component values range from [0, 255], and the alpha ranges from [0, 1].
*/
function getColorInfoHex(cssColorHex) {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(cssColorHex);
if (m === null) { return null; }
return [
Number.parseInt(m[1], 16),
Number.parseInt(m[2], 16),
Number.parseInt(m[3], 16),
1,
];
}

/**
* Generates an svg filter matrix for filtering to an absolute color value using feColorMatrix
* Example usage: `<feColorMatrix type="matrix" values="' + generateSvgFilterMatrix(colors) + '"/>`
* @param {number[]} colors The color and alpha values as [r, g, b, a]. The color component values range from [0, 255], and the alpha ranges from [0, 1].
* @returns {string}
*/
export function generateSvgFilterMatrix(colors) {
const matrix = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]];
matrix[0][3] = colors[0] / 255;
matrix[1][3] = colors[1] / 255;
matrix[2][3] = colors[2] / 255;
matrix[3][3] = colors[3];
return matrix.flat().join(' ');
}
42 changes: 42 additions & 0 deletions ext/js/display/structured-content-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import {generateSvgFilterMatrix, getColorInfo} from '../core/utilities.js';
import {DisplayContentManager} from '../display/display-content-manager.js';
import {getLanguageFromText} from '../language/text-utilities.js';
import {AnkiTemplateRendererContentManager} from '../templates/anki-template-renderer-content-manager.js';
Expand Down Expand Up @@ -166,6 +167,24 @@ export class StructuredContentGenerator {
imageContainer.appendChild(image);

if (this._contentManager instanceof DisplayContentManager) {
if (appearance === 'monochrome') {
const cssColor = getComputedStyle(document.documentElement).getPropertyValue('--text-color');
const targetColor = getColorInfo(cssColor);
if (targetColor) {
const filterId = 'monochrome-svg-filter-' + targetColor.join('');
if (document.getElementById(filterId)) {
image.style.filter = 'url(#' + filterId + ')';
} else {
const monochromeSvgFilter = document.querySelector('.monochrome-svg-filter') ?? this._createElement('span', 'monochrome-svg-filter');
this._createFilterSvg(monochromeSvgFilter, filterId, targetColor);

image.style.filter = 'url(#' + filterId + ')';

document.body.appendChild(monochromeSvgFilter);
}
}
}

this._contentManager.loadMedia(
path,
dictionary,
Expand Down Expand Up @@ -282,6 +301,29 @@ export class StructuredContentGenerator {
}
}

/**
* @param {Element} parentNode
* @param {string} filterId
* @param {number[]} targetColor
*/
_createFilterSvg(parentNode, filterId, targetColor) {
const svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
parentNode.appendChild(svgElement);

const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
svgElement.appendChild(svgDefs);

const svgFilter = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
svgFilter.setAttribute('id', filterId);
svgFilter.setAttribute('color-interpolation-filters', 'sRGB');
svgDefs.appendChild(svgFilter);

const svgColorMatrix = document.createElementNS('http://www.w3.org/2000/svg', 'feColorMatrix');
svgColorMatrix.setAttribute('type', 'matrix');
svgColorMatrix.setAttribute('values', generateSvgFilterMatrix(targetColor));
svgFilter.appendChild(svgColorMatrix);
}

/**
* @param {import('structured-content').Element} content
* @param {string} dictionary
Expand Down
Loading