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

[background image] Support colorization of the background image #1815

Merged
merged 17 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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