Skip to content

Commit

Permalink
Merge pull request #1815 from googlefonts/bg-image-colorize
Browse files Browse the repository at this point in the history
[background image] Support colorization of the background image
  • Loading branch information
justvanrossum authored Nov 20, 2024
2 parents 2ae227f + 6af58b3 commit 2d7cb8c
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 3 deletions.
45 changes: 44 additions & 1 deletion src/fontra/client/core/font-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { TaskPool } from "./task-pool.js";
import {
assert,
chain,
colorizeImage,
getCharFromCodePoint,
mapObjectValues,
throttleCalls,
Expand Down Expand Up @@ -133,11 +134,40 @@ export class FontController {

getBackgroundImage(imageIdentifier) {
// This returns a promise for the requested background image
const cacheEntry = this._getBackgroundImageCacheEntry(imageIdentifier);
return cacheEntry.imagePromise;
}

getBackgroundImageColorized(imageIdentifier, color) {
// This returns a promise for the requested colorized background image
if (!color) {
return this.getBackgroundImage(imageIdentifier);
}
const cacheEntry = this._getBackgroundImageCacheEntry(imageIdentifier);
if (cacheEntry.color !== color) {
cacheEntry.color = color;
cacheEntry.imageColorizedPromise = new Promise((resolve, reject) => {
cacheEntry.imagePromise.then((image) => {
if (image) {
colorizeImage(image, color).then((image) => {
cacheEntry.imageColorized = image;
resolve(image);
});
} else {
resolve(null);
}
});
});
}
return cacheEntry.imageColorizedPromise;
}

_getBackgroundImageCacheEntry(imageIdentifier) {
let cacheEntry = this._backgroundImageCache.get(imageIdentifier);
if (!cacheEntry) {
cacheEntry = this._cacheBackgroundImageFromIdentifier(imageIdentifier);
}
return cacheEntry.imagePromise;
return cacheEntry;
}

getBackgroundImageCached(imageIdentifier, onLoad = null) {
Expand All @@ -156,6 +186,19 @@ export class FontController {
return cacheEntry?.image;
}

getBackgroundImageColorizedCached(imageIdentifier, color, onLoad = null) {
if (!color) {
return this.getBackgroundImageCached(imageIdentifier, onLoad);
}
const cacheEntry = this._backgroundImageCache.get(imageIdentifier);
if ((!cacheEntry?.imageColorizedPromise || cacheEntry.color !== color) && onLoad) {
this.getBackgroundImageColorized(imageIdentifier, color).then((image) =>
onLoad(image)
);
}
return cacheEntry?.imageColorized;
}

_cacheBackgroundImageFromIdentifier(imageIdentifier) {
return this._cacheBackgroundImageFromDataURLPromise(
imageIdentifier,
Expand Down
35 changes: 35 additions & 0 deletions src/fontra/client/core/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function scheduleCalls(func, timeout = 0) {
timeoutID = null;
func(...args);
}, timeout);
return timeoutID;
};
}

Expand Down Expand Up @@ -635,3 +636,37 @@ export function readFileOrBlobAsDataURL(fileOrBlob) {
reader.readAsDataURL(fileOrBlob);
});
}

export function colorizeImage(inputImage, color) {
const w = inputImage.naturalWidth;
const h = inputImage.naturalHeight;
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const context = canvas.getContext("2d");

// First step, draw the image
context.drawImage(inputImage, 0, 0, w, h);
// Second step, reduce saturation to zero (making the image grayscale)
context.fillStyle = "black";
context.globalCompositeOperation = "saturation";
context.fillRect(0, 0, w, h);
// Last step, colorize the image, using screen (inverse multiply)
context.fillStyle = color;
context.globalCompositeOperation = "screen";
context.fillRect(0, 0, w, h);

return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
const outputImage = new Image();
outputImage.width = inputImage.width;
outputImage.height = inputImage.height;
const url = URL.createObjectURL(blob);
outputImage.onload = () => {
URL.revokeObjectURL(url);
resolve(outputImage);
};
outputImage.src = url;
});
});
}
1 change: 1 addition & 0 deletions src/fontra/client/lang/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "Achse %0 hinzufügen",
"axes.undo.delete": "Achse %0 entfernen",
"axes.undo.edit": "Achse %0 bearbeiten",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "Transparenz",
"canvas.clean-view-and-hand-tool": "Ungehinderte Sicht und Hand Werkzeug",
"cross-axis-mapping.axis-participates":
Expand Down
1 change: 1 addition & 0 deletions src/fontra/client/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "add axis %0",
"axes.undo.delete": "delete axis %0",
"axes.undo.edit": "edit axis %0",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "Opacity",
"canvas.clean-view-and-hand-tool": "Clean View and Hand Tool",
"cross-axis-mapping.axis-participates":
Expand Down
1 change: 1 addition & 0 deletions src/fontra/client/lang/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "add axis %0",
"axes.undo.delete": "delete axis %0",
"axes.undo.edit": "edit axis %0",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "Opacity",
"canvas.clean-view-and-hand-tool": "Prévisualisation et outil de déplacement",
"cross-axis-mapping.axis-participates":
Expand Down
1 change: 1 addition & 0 deletions src/fontra/client/lang/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "補完軸%0を追加",
"axes.undo.delete": "補完軸%0を削除",
"axes.undo.edit": "補完軸%0を編集",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "Opacity",
"canvas.clean-view-and-hand-tool": "塗りのプレビューと手のひらツール",
"cross-axis-mapping.axis-participates":
Expand Down
1 change: 1 addition & 0 deletions src/fontra/client/lang/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "add axis %0",
"axes.undo.delete": "delete axis %0",
"axes.undo.edit": "edit axis %0",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "Opacity",
"canvas.clean-view-and-hand-tool": "Schone weergave en Hand gereedschap",
"cross-axis-mapping.axis-participates":
Expand Down
1 change: 1 addition & 0 deletions src/fontra/client/lang/zh-CN.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const strings = {
"axes.undo.add": "添加参数轴 %0",
"axes.undo.delete": "删除参数轴 %0",
"axes.undo.edit": "编辑参数轴 %0",
"background-image.labels.colorize": "Colorize",
"background-image.labels.opacity": "透明度",
"canvas.clean-view-and-hand-tool": "预览与拖拽工具",
"cross-axis-mapping.axis-participates": "选中后,该参数轴参与映射",
Expand Down
89 changes: 88 additions & 1 deletion src/fontra/client/web-components/ui-form.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as html from "../core/html-utils.js";
import { SimpleElement } from "../core/html-utils.js";
import { QueueIterator } from "../core/queue-iterator.js";
import { enumerate, hyphenatedToCamelCase, round } from "../core/utils.js";
import {
enumerate,
hyphenatedToCamelCase,
round,
scheduleCalls,
} from "../core/utils.js";
import { RangeSlider } from "/web-components/range-slider.js";
import "/web-components/rotary-control.js";

Expand Down Expand Up @@ -79,6 +84,16 @@ export class Form extends SimpleElement {
height: 1.6em;
}
.ui-form-value input[type="checkbox"] {
width: initial;
height: initial;
}
.ui-form-value input[type="color"] {
height: 2em;
width: 4em;
}
.ui-form-value input[type="text"] {
width: 100%;
}
Expand Down Expand Up @@ -404,6 +419,78 @@ export class Form extends SimpleElement {
valueElement.appendChild(rangeElement);
}

_addColorPicker(valueElement, fieldItem) {
const parseColor = fieldItem.parseColor || ((v) => v);
const formatColor = fieldItem.formatColor || ((v) => v);

let checkboxElement;
const colorInputElement = html.input({ type: "color" });
colorInputElement.value = formatColor(fieldItem.value);

{
// color picker change closure
let valueStream = undefined;

const oninputFunc = scheduleCalls((event) => {
if (checkboxElement) {
checkboxElement.checked = true;
}
const value = parseColor(colorInputElement.value);
if (!valueStream) {
valueStream = new QueueIterator(5, true);
this._fieldChanging(fieldItem, value, valueStream);
}

if (valueStream) {
valueStream.put(value);
this._dispatchEvent("doChange", { key: fieldItem.key, value: value });
} else {
this._fieldChanging(fieldItem, value, undefined);
}
}, fieldItem.continuousDelay || 0);

let oninputTimer;

colorInputElement.oninput = (event) => {
oninputTimer = oninputFunc(event);
};

colorInputElement.onchange = (event) => {
if (checkboxElement) {
checkboxElement.checked = true;
}
if (valueStream) {
valueStream.done();
valueStream = undefined;
if (oninputTimer) {
clearTimeout(oninputTimer);
oninputTimer = undefined;
}
this._dispatchEvent("endChange", { key: fieldItem.key });
} else {
this._dispatchEvent("doChange", { key: fieldItem.key, value: value });
}
};
}

valueElement.appendChild(colorInputElement);

if (fieldItem.allowNoColor) {
checkboxElement = html.input({
type: "checkbox",
checked: !!fieldItem.value,
onchange: (event) => {
this._fieldChanging(
fieldItem,
checkboxElement.checked ? parseColor(colorInputElement.value) : undefined,
undefined
);
},
});
valueElement.appendChild(checkboxElement);
}
}

addEventListener(eventName, handler, options) {
this.contentElement.addEventListener(eventName, handler, options);
}
Expand Down
17 changes: 17 additions & 0 deletions src/fontra/views/editor/panel-selection-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
makeUPlusStringFromCodePoint,
parseSelection,
range,
rgbaToHex,
round,
splitGlyphNameExtension,
throttleCalls,
Expand Down Expand Up @@ -292,6 +293,22 @@ export default class SelectionInfoPanel extends Panel {
}),
});

formContents.push({
type: "color-picker",
key: backgroundImageKey("color"),
label: translate("background-image.labels.colorize"),
continuousDelay: 150,
allowNoColor: true,
value: backgroundImage.color,
parseColor: (value) => {
const matches = value.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i);
const channels = matches.slice(1, 4).map((ch) => parseInt(ch, 16) / 255);
return { red: channels[0], green: channels[1], blue: channels[2] };
},
formatColor: (value) =>
value ? rgbaToHex([value.red, value.green, value.blue]) : "#000000",
});

formContents.push({
type: "edit-number-slider",
key: backgroundImageKey("opacity"),
Expand Down
9 changes: 8 additions & 1 deletion src/fontra/views/editor/visualization-layer-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,15 @@ registerVisualizationLayerDefinition({
return;
}

const image = model.fontController.getBackgroundImageCached(
const image = model.fontController.getBackgroundImageColorizedCached(
backgroundImage.identifier,
backgroundImage.color
? rgbaToCSS([
backgroundImage.color.red,
backgroundImage.color.green,
backgroundImage.color.blue,
])
: null,
() => controller.requestUpdate()
);

Expand Down

0 comments on commit 2d7cb8c

Please sign in to comment.