prototypo-library
Version:
This is prototypo parametric font library
504 lines (429 loc) • 10.9 kB
JavaScript
import _forOwn from 'lodash/forOwn';
import LocalClient from '../../stores/local-client.stores';
import FontPrecursor from '../precursor/FontPrecursor';
import WorkerPool from '../../worker/worker-pool';
import {fontToSfntTable} from '../../opentype/font';
const MERGE_URL = process.env.MERGE
? 'http://localhost:3000'
: 'https://merge.prototypo.io/v1';
const GLYPHR_URL = 'http://www.glyphrstudio.com/online';
const oldFont = {};
let localClient;
let mergeTimeoutRef;
let instance;
const getfsSelection = (weight, italic) =>
(weight > 500
? 0b0000000000100000
: 0b0000000001000000)
| (italic ? 0b0000000000000001 : 0b0000000000000000);
window.addEventListener('fluxServer.setup', () => {
localClient = LocalClient.instance();
});
window.fontResult = undefined;
window.glyph = undefined;
function getUuid(email, familyName, styleName) {
const stringForId = `${new Date().getTime()}${familyName}${email}${styleName}`;
let id = '';
for (let i = 0; i < 16; i++) {
if (i < stringForId.length) {
id += (stringForId.charCodeAt(i) * Math.random() * 32)
.toFixed(0)
.toString(16)
.padStart(2, '0');
}
else {
id += (Math.random() * 100)
.toFixed(0)
.toString(16)
.padStart(2, '0');
}
if (i === 3 || i === 5 || i === 7 || i === 9) {
id += '-';
}
}
return id;
}
function getComponentIdAndGlyphPerClass(typedata) {
const fontComponentIdAndGlyphPerClass = {};
Object.keys(typedata.json.glyphs).forEach((key) => {
const glyph = typedata.json.glyphs[key];
if (glyph.outline.component) {
glyph.outline.component.forEach((component) => {
if (component.class) {
fontComponentIdAndGlyphPerClass[component.class] = [
...(fontComponentIdAndGlyphPerClass[component.class] || []),
[glyph.name, component.id],
];
}
});
}
});
return fontComponentIdAndGlyphPerClass;
}
async function mergeFont(url, action, params, arrayBuffer, mime = 'otf') {
const response = await fetch([url, action, ...params].join('/'), {
method: 'POST',
headers: {'Content-Type': `application/${mime}`},
body: arrayBuffer,
});
return response.arrayBuffer();
}
export default class FontMediator {
static async init(typedatas, workerPoolSize, noCanvas) {
instance = new FontMediator(workerPoolSize, noCanvas);
await instance.workerPool.workerReady;
if (typedatas) {
return instance
.addTemplate(typedatas)
.then((componentIdAndGlyphPerClass) => {
if (!process.env.LIBRARY) {
localClient.dispatchAction('/store-value-font', {
componentIdAndGlyphPerClass,
});
}
});
}
return Promise.resolve();
}
static instance() {
if (!instance) {
throw new Error('cannot return an instance before init');
}
return instance;
}
constructor(workerPoolSize, noCanvas = false) {
this.noCanvas = noCanvas;
this.workerPool = new WorkerPool(workerPoolSize);
}
addTemplate(typedatas) {
return new Promise((resolve) => {
this.workerPool.eachJob({
action: {
type: 'createFont',
data: typedatas,
},
callback: () => {
this.initValues = {};
this.glyphList = {};
this.fontMakers = {};
const componentIdAndGlyphPerClass = {};
if (!this.noCanvas) {
typedatas.forEach((typedata) => {
if (!process.env.LIBRARY) {
const font = new FontPrecursor(typedata.json);
this.fontMakers[typedata.name] = font;
}
this.glyphList[typedata.name] = typedata.json.glyphs;
componentIdAndGlyphPerClass[
typedata.name
] = getComponentIdAndGlyphPerClass(typedata);
const initValues = {};
typedata.json.controls.forEach((group) => {
group.parameters.forEach((param) => {
initValues[param.name] = param.init;
});
});
this.initValues[typedata.name] = initValues;
});
resolve(componentIdAndGlyphPerClass);
}
else {
resolve();
}
},
});
});
}
setupInfo({family, style, email, template}) {
if (!instance) {
throw new Error('cannot return an instance before init');
}
this.family = family || this.family;
this.style = style || this.style;
this.email = email || this.email;
this.template = template || this.template;
}
reset(fontName, template, subset, glyphCanvasUnicode) {
return this.getFont(
fontName,
template,
this.initValues[template],
subset,
glyphCanvasUnicode,
);
}
reloadFont(fontName, json) {
return new Promise((resolve) => {
this.workerPool.eachJob({
action: {
type: 'reloadFont',
data: {
name: fontName,
json,
},
},
callback: () => {
this.fontMakers[fontName] = new FontPrecursor(json);
resolve(this);
},
});
});
}
addToFont(buffer, fontName) {
const fontFace = new FontFace(fontName, buffer);
if (fontFace.status === 'error') {
console.warn(`error in fontface ${fontName}`); // eslint-disable-line no-console
return;
}
if (oldFont[fontName]) {
document.fonts.delete(oldFont[fontName]);
}
document.fonts.add(fontFace);
oldFont[fontName] = fontFace;
}
getFontFile(
fontName,
template,
params,
subset,
designer,
designerUrl,
foundry,
foundryUrl,
weight,
width,
italic,
merged = true,
) {
if (!this.workerPool) {
return undefined;
}
const familyName = fontName.family;
const styleName = fontName.style || 'REGULAR';
return new Promise((resolve) => {
const job = {
action: {
type: 'constructFont',
data: {
name: template,
familyName,
styleName,
params: {
...params,
},
subset,
forExport: true,
designer,
designerUrl,
foundry,
foundryUrl,
weight,
width,
italic,
},
},
callback: async (arrayBuffer) => {
const id = getUuid(this.email, familyName, styleName);
const mergedFont = await mergeFont(
MERGE_URL,
'fontfile',
[id, familyName, styleName, true],
arrayBuffer,
);
await mergeFont(
MERGE_URL,
'fontinfo',
[id],
JSON.stringify({
template,
family: familyName,
style: styleName,
date: new Date().getTime(),
email: this.email,
params,
}),
'json',
);
resolve(merged ? mergedFont : arrayBuffer);
},
};
this.workerPool.doJob(job, fontName);
});
}
async mergeFont(arrayBuffer) {
const buffer = await mergeFont(
MERGE_URL,
'mergefont',
[this.email],
arrayBuffer,
);
return buffer;
}
mergeFontWithoutTimeout(arrayBuffer) {
return this.mergeFont(arrayBuffer);
}
mergeFontWithTimeout(arrayBuffer) {
clearTimeout(mergeTimeoutRef);
return new Promise((resolve) => {
const timeout = setTimeout(() => {
resolve(this.mergeFont(arrayBuffer));
}, 300);
mergeTimeoutRef = timeout;
});
}
openInGlyphr(fontName, template, params, subset) {
return new Promise((resolve) => {
this.getFontObject(
fontName.family,
fontName.style,
template,
params,
subset,
).then(({fontBuffer}) => {
window.open(GLYPHR_URL);
window.addEventListener(
'message',
(e) => {
e.source.postMessage(fontBuffer, e.origin, [fontBuffer]);
resolve();
},
{once: true},
);
});
});
}
getFontObjectNoWorker(
familyName,
styleName = 'Regular',
template,
params,
subset,
) {
const constructedGlyph = this.fontMakers[template].constructFont(
{
...params,
},
subset,
);
const arrayBuffer = fontToSfntTable({
...font,
fontFamily: {en: e.data.data.familyName || 'Prototypo web font'},
fontSubfamily: {en: e.data.data.styleName || 'Regular'},
postScriptName: {},
unitsPerEm: 1024,
usWeightClass: weight,
usWidthClass: width,
manufacturer: foundry,
manufacturerURL: foundryUrl,
designer,
designerURL: designerUrl,
fsSelection: getfsSelection(e.data.data.weight, e.data.data.italic),
});
return arrayBuffer;
}
getFontObject(familyName, styleName = 'Regular', template, params, subset) {
if (!this.workerPool) {
return false;
}
return new Promise((resolve) => {
const job = {
action: {
type: 'constructFont',
data: {
name: template,
familyName,
styleName,
params: {
...params,
},
subset,
},
},
callback: (arrayBuffer) => {
const glyphsListLengthView = new DataView(arrayBuffer, 0, 4);
const glyphsListLength = glyphsListLengthView.getInt32(0, true);
const glyphListArray = new Int32Array(
arrayBuffer,
4,
glyphsListLength * 6, // each value is 32bits and there is 4
);
const glyphValues = [];
for (let i = 0; i < glyphsListLength * 6; i += 6) {
const unicode = glyphListArray[i];
const advanceWidth = glyphListArray[i + 1];
const spacingLeft = glyphListArray[i + 2];
const baseSpacingLeft = glyphListArray[i + 3];
const spacingRight = glyphListArray[i + 4];
const baseSpacingRight = glyphListArray[i + 5];
glyphValues.push({
unicode,
advanceWidth,
spacingLeft,
spacingRight,
baseSpacingLeft,
baseSpacingRight,
});
}
const fontBuffer = arrayBuffer.slice(
4 + glyphsListLength * 4 * 6,
arrayBuffer.length,
);
resolve({
glyphValues,
fontBuffer,
});
},
};
this.workerPool.doJob(job, familyName + styleName);
});
}
getFont(fontName, template, params, subset, glyphCanvasUnicode) {
if (glyphCanvasUnicode && !this.noCanvas) {
const glyphForCanvas = this.fontMakers[template].constructFont(
{
...params,
},
[glyphCanvasUnicode],
);
[window.glyph] = glyphForCanvas.glyphs;
localClient.dispatchAction('/store-value-font', {
glyph: Math.random(),
});
}
return this.getFontObject(fontName, 'Regular', template, params, subset)
.then(({glyphValues, fontBuffer}) => {
this.addToFont(fontBuffer, fontName);
window.fontResult = {glyphs: glyphValues};
localClient.dispatchAction('/store-value-font', {
font: Math.random(),
});
return this.mergeFontWithTimeout(fontBuffer, fontName);
})
.then((mergedBuffer) => {
this.addToFont(mergedBuffer, fontName);
});
}
getAllGlyphForCanvas(template, params = this.initValues[template]) {
const glyphArray = [];
_forOwn(this.glyphList[template], (glyph) => {
if (glyph.unicode && !this.noCanvas) {
try {
const constructedGlyph = this.fontMakers[template].constructFont(
{
...params,
},
[glyph.unicode],
).glyphs[0];
glyphArray.push(constructedGlyph);
}
catch (error) {
glyphArray.push({error, unicode: glyph.unicode});
}
}
});
window.glyphArray = glyphArray;
localClient.dispatchAction('/store-value-font', {
glyphArray: Math.random(),
});
}
}