Skip to content

Instantly share code, notes, and snippets.

@jseppi
Created December 18, 2019 01:12
Show Gist options
  • Save jseppi/f4849acacb3de34ee976d3a19ff967b0 to your computer and use it in GitHub Desktop.
Save jseppi/f4849acacb3de34ee976d3a19ff967b0 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";
var renderMap1 = 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 renderMap1Busy = false;
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,
"text-line-height": 8 // TODO: does this do anything?
},
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(dataURL) {
var img = document.createElement("img");
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
function doneGeneratingPreviews() {
for (key in state.renderedFontPreviews) {
addImage(state.renderedFontPreviews[key]);
}
}
function blob2canvas(canvas, blob) {
var img = new Img();
var ctx = canvas.getContext("2d");
img.onload = function () {
ctx.drawImage(img, 0, 0);
};
img.src = blob;
}
function convertURIToImageData(URI) {
return new Promise(function (resolve, reject) {
if (URI == null) return reject();
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d"),
image = new Image();
image.addEventListener("load", function () {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
resolve(context.getImageData(0, 0, canvas.width, canvas.height));
}, false);
image.src = URI;
});
}
function trimCanvas(canvas) {
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) {
// TODO: Eli says there is a webgl canvas method
// we could use to get its pixels instead of this
// (what I asumme is) hacky method of making a
// new img and setting its src.
// Probably context.readPixels
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels
// const glCtx = webglCanvas.getContext('webgl');
// const pixels = new Uint8Array(webglCanvas.width * webglCanvas.height * 4);
// glCtx.readPixels(0, 0, webglCanvas.width, webglCanvas.height, glCtx.RGBA, glCtx.UNSIGNED_BYTE, pixels);
// console.log('---> pixels')
// console.log(pixels);
// const trimmed = trimPixels(pixels, webglCanvas.width, webglCanvas.height);
// callback(trimmed);
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = webglCanvas.width;
tmpCanvas.height = webglCanvas.height;
var tmpCtx = tmpCanvas.getContext("2d");
var img = new Image();
img.onload = function () {
tmpCtx.drawImage(img, 0, 0);
var trimmed = trimCanvas(tmpCanvas);
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"; // requestAnimationFrame
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMap1Busy) {
// requeue
window[windowMethod](function () {
return processRenderJobs(jobParams);
});
return;
}
// else
renderMap1Busy = 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
});
renderMap1.setStyle(style);
renderMap1.once("idle", function () {
trimWebglCanvas(renderMap1.getCanvas(), function (cropped) {
var dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(params.name, params.text, params.color, dataURL);
renderMap1Busy = false;
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](function () {
return processRenderJobs(remainingParams);
});
} else {
doneGeneratingPreviews();
}
});
});
}
window[windowMethod](function () {
return processRenderJobs(initialJobParams);
});
</script>
<script id="jsbin-source-javascript" type="text/javascript">mapboxgl.accessToken =
"pk.eyJ1IjoianNlcHBpbWJ4IiwiYSI6ImNqbGU1ODdtMzBpZjUzcG1pMWJnaHB2aHgifQ.xGVwKUpyJ-S5iyaLq7GFLA";
const owner = "jseppimbx";
const renderMap1 = 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 renderMap1Busy = false;
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,
"text-line-height": 8 // TODO: does this do anything?
},
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(dataURL) {
const img = document.createElement("img");
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
function doneGeneratingPreviews() {
for (key in state.renderedFontPreviews) {
addImage(state.renderedFontPreviews[key]);
}
}
function blob2canvas(canvas, blob) {
var img = new Img();
var ctx = canvas.getContext("2d");
img.onload = function() {
ctx.drawImage(img, 0, 0);
};
img.src = blob;
}
function convertURIToImageData(URI) {
return new Promise(function(resolve, reject) {
if (URI == null) return reject();
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d"),
image = new Image();
image.addEventListener(
"load",
function() {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
resolve(context.getImageData(0, 0, canvas.width, canvas.height));
},
false
);
image.src = URI;
});
}
function trimCanvas(canvas) {
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) {
// TODO: Eli says there is a webgl canvas method
// we could use to get its pixels instead of this
// (what I asumme is) hacky method of making a
// new img and setting its src.
// Probably context.readPixels
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels
// const glCtx = webglCanvas.getContext('webgl');
// const pixels = new Uint8Array(webglCanvas.width * webglCanvas.height * 4);
// glCtx.readPixels(0, 0, webglCanvas.width, webglCanvas.height, glCtx.RGBA, glCtx.UNSIGNED_BYTE, pixels);
// console.log('---> pixels')
// console.log(pixels);
// const trimmed = trimPixels(pixels, webglCanvas.width, webglCanvas.height);
// callback(trimmed);
const tmpCanvas = document.createElement("canvas");
tmpCanvas.width = webglCanvas.width;
tmpCanvas.height = webglCanvas.height;
const tmpCtx = tmpCanvas.getContext("2d");
const img = new Image();
img.onload = () => {
tmpCtx.drawImage(img, 0, 0);
const trimmed = trimCanvas(tmpCanvas);
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"; // requestAnimationFrame
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMap1Busy) {
// requeue
window[windowMethod](() => processRenderJobs(jobParams));
return;
}
// else
renderMap1Busy = true;
const [params, ...remainingParams] = jobParams;
const style = generateFontPreviewStyle({
owner,
name: params.name,
text: params.text,
color: params.color
});
renderMap1.setStyle(style);
renderMap1.once("idle", () => {
trimWebglCanvas(renderMap1.getCanvas(), cropped => {
const dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(
params.name,
params.text,
params.color,
dataURL
);
renderMap1Busy = false;
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](() => processRenderJobs(remainingParams));
} else {
doneGeneratingPreviews();
}
});
});
}
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";
var renderMap1 = 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 renderMap1Busy = false;
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,
"text-line-height": 8 // TODO: does this do anything?
},
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(dataURL) {
var img = document.createElement("img");
img.src = dataURL;
img.classList.add("preview-image");
document.body.appendChild(img);
}
function doneGeneratingPreviews() {
for (key in state.renderedFontPreviews) {
addImage(state.renderedFontPreviews[key]);
}
}
function blob2canvas(canvas, blob) {
var img = new Img();
var ctx = canvas.getContext("2d");
img.onload = function () {
ctx.drawImage(img, 0, 0);
};
img.src = blob;
}
function convertURIToImageData(URI) {
return new Promise(function (resolve, reject) {
if (URI == null) return reject();
var canvas = document.createElement("canvas"),
context = canvas.getContext("2d"),
image = new Image();
image.addEventListener("load", function () {
canvas.width = image.width;
canvas.height = image.height;
context.drawImage(image, 0, 0, canvas.width, canvas.height);
resolve(context.getImageData(0, 0, canvas.width, canvas.height));
}, false);
image.src = URI;
});
}
function trimCanvas(canvas) {
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) {
// TODO: Eli says there is a webgl canvas method
// we could use to get its pixels instead of this
// (what I asumme is) hacky method of making a
// new img and setting its src.
// Probably context.readPixels
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/readPixels
// const glCtx = webglCanvas.getContext('webgl');
// const pixels = new Uint8Array(webglCanvas.width * webglCanvas.height * 4);
// glCtx.readPixels(0, 0, webglCanvas.width, webglCanvas.height, glCtx.RGBA, glCtx.UNSIGNED_BYTE, pixels);
// console.log('---> pixels')
// console.log(pixels);
// const trimmed = trimPixels(pixels, webglCanvas.width, webglCanvas.height);
// callback(trimmed);
var tmpCanvas = document.createElement("canvas");
tmpCanvas.width = webglCanvas.width;
tmpCanvas.height = webglCanvas.height;
var tmpCtx = tmpCanvas.getContext("2d");
var img = new Image();
img.onload = function () {
tmpCtx.drawImage(img, 0, 0);
var trimmed = trimCanvas(tmpCanvas);
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"; // requestAnimationFrame
function processRenderJobs(jobParams) {
if (i > 100) {
console.log("emergency brake");
return;
}
if (renderMap1Busy) {
// requeue
window[windowMethod](function () {
return processRenderJobs(jobParams);
});
return;
}
// else
renderMap1Busy = 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
});
renderMap1.setStyle(style);
renderMap1.once("idle", function () {
trimWebglCanvas(renderMap1.getCanvas(), function (cropped) {
var dataURL = cropped.toDataURL();
// Save the rendered image data into state
saveRenderedFontPreviewData(params.name, params.text, params.color, dataURL);
renderMap1Busy = false;
// we have more to process, so queue up the remaining
if (remainingParams.length) {
window[windowMethod](function () {
return processRenderJobs(remainingParams);
});
} else {
doneGeneratingPreviews();
}
});
});
}
window[windowMethod](function () {
return processRenderJobs(initialJobParams);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment