Skip to content

Instantly share code, notes, and snippets.

@jseppi
Created December 18, 2019 01:31
Show Gist options
  • Save jseppi/a9b523bc00f31eafad86140518bf1ed3 to your computer and use it in GitHub Desktop.
Save jseppi/a9b523bc00f31eafad86140518bf1ed3 to your computer and use it in GitHub Desktop.
Render font previews // source https://jsbin.com/lomovel
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Render font previews</title>
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v1.5.0/mapbox-gl.css"
rel="stylesheet"
/>
<link
href="https://api.mapbox.com/mapbox-assembly/v0.24.0/assembly.min.css"
rel="stylesheet"
/>
<script
async
defer
src="https://api.mapbox.com/mapbox-assembly/v0.24.0/assembly.js"
></script>
<style>
body {
margin: 0;
padding: 0;
background: #3e3e3e;
}
.preview-image {
border: 1px solid yellow;
display:block;
margin-top: 10px;
}
</style>
</head>
<body>
<div id="render_map_1" style="position:absolute;top:-200px;width:1200px; height: 120px; visibility: hidden;" class="border"></div>
<script id="jsbin-javascript">
"use strict";
function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }
mapboxgl.accessToken = "pk.eyJ1IjoianNlcHBpbWJ4IiwiYSI6ImNqbGU1ODdtMzBpZjUzcG1pMWJnaHB2aHgifQ.xGVwKUpyJ-S5iyaLq7GFLA";
var owner = "jseppimbx";
// These would be rendered in workspace upon application mount
var renderMap = new mapboxgl.Map({
container: "render_map_1",
center: [0.525, -0.025],
zoom: 9,
pitch: 0,
bearing: 0,
fadeDuration: 0,
preserveDrawingBuffer: true,
localIdeographFontFamily: false
});
var renderMapBusy = false;
var renderCanvas = document.createElement('canvas');
var renderCtx = renderCanvas.getContext('2d');
// -----
function generateFontPreviewStyle(_ref) {
var owner = _ref.owner;
var name = _ref.name;
var text = _ref.text;
var color = _ref.color;
return {
version: 8,
glyphs: "mapbox://fonts/" + owner + "/{fontstack}/{range}.pbf",
sources: {
font: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "Point",
coordinates: [0, 0]
},
properties: {}
}]
}
}
},
layers: [{
id: "preview",
source: "font",
type: "symbol",
layout: {
"text-justify": "left",
"text-anchor": "left",
"text-field": text,
"text-font": [name],
"text-max-width": 800,
"text-size": 44
},
paint: {
"text-color": color,
"text-halo-color": color,
"text-halo-width": 0.3,
"text-halo-blur": 0
}
}]
};
}
// ***** redux-like stuff
var state = {};
state.fontPreviewsToRender = new Set();
state.renderedFontPreviews = {};
function makeFontKey(name, color, text) {
return name + "^" + color + "^" + text;
}
function fontKeyToParams(key) {
var parts = key.split("^");
return {
name: parts[0],
color: parts[1],
text: parts[2]
};
}
function addFontPreviewToRender(name, color, text) {
var key = makeFontKey(name, color, text);
if (!state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender.add(key);
}
}
function getFontPreviewsToRender() {
var paramsToRender = [];
state.fontPreviewsToRender.forEach(function (key) {
var params = fontKeyToParams(key);
paramsToRender.push(params);
});
return paramsToRender;
}
function saveRenderedFontPreviewData(name, color, text, dataURL) {
var key = makeFontKey(name, color, text);
state.renderedFontPreviews[key] = dataURL;
if (state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender["delete"](key);
}
}
// ***** fake output helpers
function addImage(name, color, text) {
var img = document.createElement("img");
var key = makeFontKey(name, color, text);
var dataURL = state.renderedFontPreviews[key];
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
// -------------------
function trimCanvas(canvas) {
// From https://stackoverflow.com/a/58882518
var context = canvas.getContext("2d");
var topLeft = {
x: canvas.width,
y: canvas.height,
update: function update(x, y) {
this.x = Math.min(this.x, x);
this.y = Math.min(this.y, y);
}
};
var bottomRight = {
x: 0,
y: 0,
update: function update(x, y) {
this.x = Math.max(this.x, x);
this.y = Math.max(this.y, y);
}
};
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var alpha = imageData.data[y * (canvas.width * 4) + x * 4 + 3];
if (alpha !== 0) {
topLeft.update(x, y);
bottomRight.update(x, y);
}
}
}
var width = bottomRight.x - topLeft.x;
var height = bottomRight.y - topLeft.y;
var croppedCanvas = context.getImageData(topLeft.x, topLeft.y, width, height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas, 0, 0);
return canvas;
}
function trimWebglCanvas(webglCanvas, callback) {
renderCanvas.width = webglCanvas.width;
renderCanvas.height = webglCanvas.height;
var img = new Image();
img.onload = function () {
renderCtx.drawImage(img, 0, 0);
var trimmed = trimCanvas(renderCanvas);
callback(trimmed);
};
img.src = webglCanvas.toDataURL();
}
// ***** render component stuff
var demoFontsToRender = ["Komika Hand Bold Italic", "Komika Hand Bold", "Komika Hand Italic", "Komika Hand Regular", "Komika Parch Regular", "Komika Title - Axis Regular", "Komika Title - Kaps Regular", "Komika Title - Paint Regular", "Komika Title - Wide Regular", "Komika Title Regular"];
var color = "#fff";
demoFontsToRender.forEach(function (name) {
addFontPreviewToRender(name, color, name);
});
// get initial list of job params
var initialJobParams = getFontPreviewsToRender();
var i = 0;
var windowMethod = "requestAnimationFrame";
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMapBusy) {
// requeue
window[windowMethod](function () {
return processRenderJobs(jobParams);
});
return;
}
// else
var jobStart = Date.now();
renderMapBusy = true;
var _jobParams = _toArray(jobParams);
var params = _jobParams[0];
var remainingParams = _jobParams.slice(1);
var style = generateFontPreviewStyle({
owner: owner,
name: params.name,
text: params.text,
color: params.color
});
renderMap.setStyle(style);
renderMap.once("idle", function () {
trimWebglCanvas(renderMap.getCanvas(), function (cropped) {
var dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(params.name, params.color, params.text, dataURL);
renderMapBusy = false;
console.log(params.name + ": " + (Date.now() - jobStart) + "ms");
// TODO: instead of adding the image here, we'd i guess have
// the consuming components do something when store state is altered
// (via selector) due to saveRenderedFontPreviewData
addImage(params.name, params.color, params.text);
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](function () {
return processRenderJobs(remainingParams);
});
} else {
console.log("Total time: " + (Date.now() - start) + "ms");
}
});
});
}
var start = Date.now();
window[windowMethod](function () {
return processRenderJobs(initialJobParams);
});
</script>
<script id="jsbin-source-javascript" type="text/javascript">mapboxgl.accessToken =
"pk.eyJ1IjoianNlcHBpbWJ4IiwiYSI6ImNqbGU1ODdtMzBpZjUzcG1pMWJnaHB2aHgifQ.xGVwKUpyJ-S5iyaLq7GFLA";
const owner = "jseppimbx";
// These would be rendered in workspace upon application mount
const renderMap = new mapboxgl.Map({
container: "render_map_1",
center: [0.525, -0.025],
zoom: 9,
pitch: 0,
bearing: 0,
fadeDuration: 0,
preserveDrawingBuffer: true,
localIdeographFontFamily: false
});
let renderMapBusy = false;
const renderCanvas = document.createElement('canvas');
const renderCtx = renderCanvas.getContext('2d');
// -----
function generateFontPreviewStyle({ owner, name, text, color }) {
return {
version: 8,
glyphs: `mapbox://fonts/${owner}/{fontstack}/{range}.pbf`,
sources: {
font: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "Point",
coordinates: [0, 0]
},
properties: {}
}
]
}
}
},
layers: [
{
id: "preview",
source: "font",
type: "symbol",
layout: {
"text-justify": "left",
"text-anchor": "left",
"text-field": text,
"text-font": [name],
"text-max-width": 800,
"text-size": 44
},
paint: {
"text-color": color,
"text-halo-color": color,
"text-halo-width": 0.3,
"text-halo-blur": 0
}
}
]
};
}
// ***** redux-like stuff
const state = {};
state.fontPreviewsToRender = new Set();
state.renderedFontPreviews = {};
function makeFontKey(name, color, text) {
return `${name}^${color}^${text}`;
}
function fontKeyToParams(key) {
const parts = key.split("^");
return {
name: parts[0],
color: parts[1],
text: parts[2]
};
}
function addFontPreviewToRender(name, color, text) {
const key = makeFontKey(name, color, text);
if (!state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender.add(key);
}
}
function getFontPreviewsToRender() {
const paramsToRender = [];
state.fontPreviewsToRender.forEach(key => {
const params = fontKeyToParams(key);
paramsToRender.push(params);
});
return paramsToRender;
}
function saveRenderedFontPreviewData(name, color, text, dataURL) {
const key = makeFontKey(name, color, text);
state.renderedFontPreviews[key] = dataURL;
if (state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender.delete(key);
}
}
// ***** fake output helpers
function addImage(name, color, text) {
const img = document.createElement("img");
const key = makeFontKey(name, color, text);
const dataURL = state.renderedFontPreviews[key]
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
// -------------------
function trimCanvas(canvas) {
// From https://stackoverflow.com/a/58882518
const context = canvas.getContext("2d");
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x, y) {
this.x = Math.min(this.x, x);
this.y = Math.min(this.y, y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x, y) {
this.x = Math.max(this.x, x);
this.y = Math.max(this.y, y);
}
};
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
const alpha = imageData.data[y * (canvas.width * 4) + x * 4 + 3];
if (alpha !== 0) {
topLeft.update(x, y);
bottomRight.update(x, y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(
topLeft.x,
topLeft.y,
width,
height
);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas, 0, 0);
return canvas;
}
function trimWebglCanvas(webglCanvas, callback) {
renderCanvas.width = webglCanvas.width;
renderCanvas.height = webglCanvas.height;
const img = new Image();
img.onload = () => {
renderCtx.drawImage(img, 0, 0);
const trimmed = trimCanvas(renderCanvas);
callback(trimmed);
};
img.src = webglCanvas.toDataURL();
}
// ***** render component stuff
const demoFontsToRender = [
"Komika Hand Bold Italic",
"Komika Hand Bold",
"Komika Hand Italic",
"Komika Hand Regular",
"Komika Parch Regular",
"Komika Title - Axis Regular",
"Komika Title - Kaps Regular",
"Komika Title - Paint Regular",
"Komika Title - Wide Regular",
"Komika Title Regular"
];
const color = "#fff";
demoFontsToRender.forEach(name => {
addFontPreviewToRender(name, color, name);
});
// get initial list of job params
const initialJobParams = getFontPreviewsToRender();
let i = 0;
const windowMethod = "requestAnimationFrame";
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMapBusy) {
// requeue
window[windowMethod](() => processRenderJobs(jobParams));
return;
}
// else
const jobStart = Date.now();
renderMapBusy = true;
const [params, ...remainingParams] = jobParams;
const style = generateFontPreviewStyle({
owner,
name: params.name,
text: params.text,
color: params.color
});
renderMap.setStyle(style);
renderMap.once("idle", () => {
trimWebglCanvas(renderMap.getCanvas(), cropped => {
const dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(
params.name,
params.color,
params.text,
dataURL
);
renderMapBusy = false;
console.log(`${params.name}: ${Date.now() - jobStart}ms`);
// TODO: instead of adding the image here, we'd i guess have
// the consuming components do something when store state is altered
// (via selector) due to saveRenderedFontPreviewData
addImage(params.name, params.color, params.text)
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](() => processRenderJobs(remainingParams));
} else {
console.log(`Total time: ${Date.now() - start}ms`);
}
});
});
}
const start = Date.now();
window[windowMethod](() => processRenderJobs(initialJobParams));
</script></body>
</html>
"use strict";
function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }
mapboxgl.accessToken = "pk.eyJ1IjoianNlcHBpbWJ4IiwiYSI6ImNqbGU1ODdtMzBpZjUzcG1pMWJnaHB2aHgifQ.xGVwKUpyJ-S5iyaLq7GFLA";
var owner = "jseppimbx";
// These would be rendered in workspace upon application mount
var renderMap = new mapboxgl.Map({
container: "render_map_1",
center: [0.525, -0.025],
zoom: 9,
pitch: 0,
bearing: 0,
fadeDuration: 0,
preserveDrawingBuffer: true,
localIdeographFontFamily: false
});
var renderMapBusy = false;
var renderCanvas = document.createElement('canvas');
var renderCtx = renderCanvas.getContext('2d');
// -----
function generateFontPreviewStyle(_ref) {
var owner = _ref.owner;
var name = _ref.name;
var text = _ref.text;
var color = _ref.color;
return {
version: 8,
glyphs: "mapbox://fonts/" + owner + "/{fontstack}/{range}.pbf",
sources: {
font: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [{
type: "Feature",
geometry: {
type: "Point",
coordinates: [0, 0]
},
properties: {}
}]
}
}
},
layers: [{
id: "preview",
source: "font",
type: "symbol",
layout: {
"text-justify": "left",
"text-anchor": "left",
"text-field": text,
"text-font": [name],
"text-max-width": 800,
"text-size": 44
},
paint: {
"text-color": color,
"text-halo-color": color,
"text-halo-width": 0.3,
"text-halo-blur": 0
}
}]
};
}
// ***** redux-like stuff
var state = {};
state.fontPreviewsToRender = new Set();
state.renderedFontPreviews = {};
function makeFontKey(name, color, text) {
return name + "^" + color + "^" + text;
}
function fontKeyToParams(key) {
var parts = key.split("^");
return {
name: parts[0],
color: parts[1],
text: parts[2]
};
}
function addFontPreviewToRender(name, color, text) {
var key = makeFontKey(name, color, text);
if (!state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender.add(key);
}
}
function getFontPreviewsToRender() {
var paramsToRender = [];
state.fontPreviewsToRender.forEach(function (key) {
var params = fontKeyToParams(key);
paramsToRender.push(params);
});
return paramsToRender;
}
function saveRenderedFontPreviewData(name, color, text, dataURL) {
var key = makeFontKey(name, color, text);
state.renderedFontPreviews[key] = dataURL;
if (state.fontPreviewsToRender.has(key)) {
state.fontPreviewsToRender["delete"](key);
}
}
// ***** fake output helpers
function addImage(name, color, text) {
var img = document.createElement("img");
var key = makeFontKey(name, color, text);
var dataURL = state.renderedFontPreviews[key];
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
// -------------------
function trimCanvas(canvas) {
// From https://stackoverflow.com/a/58882518
var context = canvas.getContext("2d");
var topLeft = {
x: canvas.width,
y: canvas.height,
update: function update(x, y) {
this.x = Math.min(this.x, x);
this.y = Math.min(this.y, y);
}
};
var bottomRight = {
x: 0,
y: 0,
update: function update(x, y) {
this.x = Math.max(this.x, x);
this.y = Math.max(this.y, y);
}
};
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
for (var y = 0; y < canvas.height; y++) {
var alpha = imageData.data[y * (canvas.width * 4) + x * 4 + 3];
if (alpha !== 0) {
topLeft.update(x, y);
bottomRight.update(x, y);
}
}
}
var width = bottomRight.x - topLeft.x;
var height = bottomRight.y - topLeft.y;
var croppedCanvas = context.getImageData(topLeft.x, topLeft.y, width, height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas, 0, 0);
return canvas;
}
function trimWebglCanvas(webglCanvas, callback) {
renderCanvas.width = webglCanvas.width;
renderCanvas.height = webglCanvas.height;
var img = new Image();
img.onload = function () {
renderCtx.drawImage(img, 0, 0);
var trimmed = trimCanvas(renderCanvas);
callback(trimmed);
};
img.src = webglCanvas.toDataURL();
}
// ***** render component stuff
var demoFontsToRender = ["Komika Hand Bold Italic", "Komika Hand Bold", "Komika Hand Italic", "Komika Hand Regular", "Komika Parch Regular", "Komika Title - Axis Regular", "Komika Title - Kaps Regular", "Komika Title - Paint Regular", "Komika Title - Wide Regular", "Komika Title Regular"];
var color = "#fff";
demoFontsToRender.forEach(function (name) {
addFontPreviewToRender(name, color, name);
});
// get initial list of job params
var initialJobParams = getFontPreviewsToRender();
var i = 0;
var windowMethod = "requestAnimationFrame";
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMapBusy) {
// requeue
window[windowMethod](function () {
return processRenderJobs(jobParams);
});
return;
}
// else
var jobStart = Date.now();
renderMapBusy = true;
var _jobParams = _toArray(jobParams);
var params = _jobParams[0];
var remainingParams = _jobParams.slice(1);
var style = generateFontPreviewStyle({
owner: owner,
name: params.name,
text: params.text,
color: params.color
});
renderMap.setStyle(style);
renderMap.once("idle", function () {
trimWebglCanvas(renderMap.getCanvas(), function (cropped) {
var dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(params.name, params.color, params.text, dataURL);
renderMapBusy = false;
console.log(params.name + ": " + (Date.now() - jobStart) + "ms");
// TODO: instead of adding the image here, we'd i guess have
// the consuming components do something when store state is altered
// (via selector) due to saveRenderedFontPreviewData
addImage(params.name, params.color, params.text);
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](function () {
return processRenderJobs(remainingParams);
});
} else {
console.log("Total time: " + (Date.now() - start) + "ms");
}
});
});
}
var start = Date.now();
window[windowMethod](function () {
return processRenderJobs(initialJobParams);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment