@thebigcrunch/sdk
Version:
The big crunch SDK library
407 lines (359 loc) • 12.4 kB
JavaScript
/**
* Renderers are generic components without any dependencies.
* They can be called from the SDK, universe and fullscreen mode
* At somepoint this could be split out into an npm package
* We will also write a block post on drawing text into a box.
*/
const LEFTRIGHTPADDING = 4; // 5px of padding
// const LEFTRIGHTPADDING = 0.05; // 5px of the space padding
const MAX_FONT_SIZE = 400;
const MIN_FONT_SIZE = 0;
const defaultProperties = {
alignment: "left",
"font-weight": "100",
"font-style": [],
"font-multiplier": 1,
"font-family": "Roboto Slab",
color: "#000000"
};
const isNumeric = function isNumeric(text) {
let t = text;
if (typeof t === "string") {
t = Number(t.split(",").join(""));
}
return !Number.isNaN(t);
};
/**
*
* @param {*} alignment The alignment of the text
* @param {*} x The leftmost x position for the canvas coords
* @param {*} width the width of the box
*/
const findXStart = function findXStart(alignment, x, width) {
switch (alignment) {
case "center":
return x + width / 2;
case "left":
return x + LEFTRIGHTPADDING;
case "right":
return x + width - LEFTRIGHTPADDING;
default:
console.error(
`[TextRenderer] Incorrect alignment value ${alignment}`
);
}
};
const findYStart = function findYStart(y, height) {
return y + height / 2;
};
export const getFontStyleFromProperties = function getFontStyleFromProperties(
properties,
size
) {
return `${properties["font-weight"]} ${
properties["font-style"]
} ${size}px ${properties["font-family"]}`;
};
const setDefaultAlignmentRightIfNumeric = function setDefaultAlignmentRightIfNumeric(
properties,
isNumber,
currentAlignment
) {
if ((!properties || !properties.alignment) && isNumber) {
return "right";
}
return currentAlignment;
};
export const getFontProperties = function getFontProperties(properties) {
return !properties
? { ...defaultProperties }
: Object.assign({}, defaultProperties, properties);
};
const setContextFontProperties = function setContextFontProperties(
ctx,
properties,
size,
width,
height
) {
if (properties && properties.backgroundColor) {
ctx.fillStyle = properties.backgroundColor;
ctx.fillRect(0, 0, width, height);
}
ctx.font = getFontStyleFromProperties(properties, size);
ctx.fillStyle = properties.color;
ctx.textBaseline = "middle"; // Vertical alignment to middle.
ctx.textAlign = properties.alignment;
};
const calculateRelativeSizeFont = function calculateRelativeSizeFont(
props,
fontSize
) {
return fontSize * props["font-multiplier"];
};
const setContextForNativeDraw = function setContextForNativeDraw(
ctx,
properties,
fontSize,
isNumber,
width,
height
) {
const props = getFontProperties(properties);
// Set default alignment, if it's a number right align, otherwise center
props.alignment = setDefaultAlignmentRightIfNumeric(
properties,
isNumber,
props.alignment
);
const relativeFontSize = calculateRelativeSizeFont(props, fontSize);
setContextFontProperties(ctx, props, relativeFontSize, width, height);
return props;
};
const createMiniCanvas = function createMiniCanvas(width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, width, height);
return canvas;
};
const truncateWideText = function truncateWideText(
text,
properties,
width,
height,
fontSize
) {
const textCanvas = createMiniCanvas(width, height);
const ctx = textCanvas.getContext("2d");
const props = setContextForNativeDraw(
ctx,
properties,
fontSize,
isNumeric(text),
width,
height
);
ctx.fillText(
text,
findXStart(props.alignment, 0, width),
findYStart(0, height)
);
return textCanvas;
};
const drawFittedTextOnMiniCanvas = function drawFittedTextOnMiniCanvas(
text,
properties,
width,
height,
fontSize
) {
const textCanvas = createMiniCanvas(width, height);
const ctx = textCanvas.getContext("2d");
setContextFontProperties(ctx, properties, fontSize, width, height);
ctx.fillText(
text,
findXStart(properties.alignment, 0, width),
findYStart(0, height)
);
return textCanvas;
};
/**
* https://www.goodboydigital.com/pixijs/docs/files/src_pixi_Text.js.html determineFontHeight
* There is no easy way to measure the height of text on a canvas. We could consider hard coding the
* height of fonts. But there isn't always a great calculate for each font type.
* This might be slow and we should consider caching results.
* @param {*} fontStyle
*/
export const getFontHeight = function getFontHeight(fontStyle) {
const div = document.createElement("div");
// Put an M in the div this is a high character.
// Once the text is appended to the DOM we can inspect it's height.
div.appendChild(document.createTextNode("M"));
div.style.font = fontStyle;
div.style.alignSelf = "flex-start";
document.body.appendChild(div);
const result = div.offsetHeight;
// Once we're done remove the div leaving no trace.
document.body.removeChild(div);
return result + 1; // Add one for a bit of extra space between lines
};
const getLinePosition = function getLinePosition(y, lineCount, fontHeight) {
return y + fontHeight * lineCount;
};
const shortenWordToFitWidth = function shortenWordToFitWidth(
ctx,
currentWord,
width
) {
let text = currentWord;
// If the current word is too long for the space
// Consider a binary search here
while (ctx.measureText(text).width > width) {
// Shorten it by one character until it fits
text = text.substring(0, text.length - 1);
// If none of the letters fit write the first one.
if (text.length === 0) {
return currentWord[0];
}
}
return text;
};
/**
* @param ctx The canvas context to draw on
* @param text The text to draw
* @param x The location on the canvas to draw the text
* @param y The location on the canvas to draw the text
* @param width The width of the text to draw
* @param height The height of the text to draw
* @param fontProperties The font properties, font style, weight, bold, italic etc.
*/
export function drawNativeText(text, width, height, fontProperties) {
// Find fontsize
const fontSize = height / 2;
// Set the context to the properties
// Test drawing the text
// Draw on a mini canvas
const truncatedTextImage = truncateWideText(
text,
fontProperties,
width,
height,
fontSize
);
return truncatedTextImage;
}
/**
* Draws text that fits within the bounding box.
* https://github.com/STRML/textFit/blob/master/textFit.js
* Algorithm one -
* 1. place text into a div, with the width and height set to the same as the space
* 2. measure the scroll distance on the span
* 3. if the span scroll distance is longer than the size of the div make text smaller.
* 4. if the scroll distance is the same, make the text bigger.
*
* https://gist.githubusercontent.com/videlais/9588989/raw/f2c60f3421308806a4842e884461ec432ed16422/fontHeight.js
* Algorithm two -
* 1. draw the text on a hidden canvas
* 2. place the letter M (highest letter) into a span
* 3. use context2d measure text to determine the width
* 4. measure the height of the span to determine the height of the text
* 5. if the text is within the bounds, make it bigger. If it's outside make it smaller
* @param {*} ctx
* @param {*} text
* @param {*} x
* @param {*} y
* @param {*} width
* @param {*} height
* @param {*} fontProperties
*/
export function drawFittedText(text, w, h, fontProperties) {
// Flooring the width and height is required because the span.scrollwidth/height is rounded to nearest whole number.
const width = Math.floor(w);
const height = Math.floor(h);
let low = MIN_FONT_SIZE + 1;
let high = MAX_FONT_SIZE + 1;
let proposedSize = (low + high) / 2;
const actualProperties = getFontProperties(fontProperties);
/*
Alternate span implementation
*/
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.width = `${width}px`;
span.style.height = `${height}px`;
span.style.textAlign = "center";
span.style.whiteSpace = "nowrap";
span.innerHTML = text;
document.body.appendChild(span);
while (low <= high) {
proposedSize = (low + high) / 2;
const fontStyle = getFontStyleFromProperties(
actualProperties,
proposedSize
);
span.style.font = fontStyle;
// ctx.font = fontStyle;
// We have two implementations here at the moment. One that use's measure text and
// a span for line height, which has it
// An alternative implementation would be to make a span with the right width / height
// Keep changing the font properties checking the scroll width and height like text fit does.
// If we are under sized
// if (ctx.measureText(text).width < width && getFontHeight(fontStyle) < height) {
if (span.scrollWidth <= width && span.scrollHeight <= height) {
low = proposedSize + 1;
} else {
// If we are oversized
high = proposedSize - 1;
}
}
// When exiting the loop we have 1 px more font size than we need.
document.body.removeChild(span);
const image = drawFittedTextOnMiniCanvas(
text,
actualProperties,
w,
h,
proposedSize - 1
);
return image;
}
export function drawWrappedText(text, width, height, fontProperties) {
const actualProperties = getFontProperties(fontProperties);
const miniCanvas = createMiniCanvas(width, height);
const ctx = miniCanvas.getContext("2d");
// X, ys for drawing on the mini canvas
const localX = findXStart(actualProperties.alignment, 0, width);
const localY = 0;
const fontSize = calculateRelativeSizeFont(actualProperties, height / 2);
setContextFontProperties(ctx, actualProperties, fontSize, width, height);
ctx.textBaseline = "top";
const words = text.split(" "); // Split on - and _, . as well
const fontHeight = getFontHeight(
getFontStyleFromProperties(actualProperties, fontSize)
);
let lineAccumulator = "";
let linesWritten = 0;
for (let i = 0; i < words.length; i += 1) {
const currentWord = words[i];
const shortenedWord = shortenWordToFitWidth(ctx, currentWord, width);
// if we have had to chop the word
if (currentWord !== shortenedWord) {
// splice the remaining part of the word into the array at the next index for processing later
words.splice(i + 1, 0, words[i].substr(shortenedWord.length));
}
const currentLine = `${lineAccumulator}${shortenedWord} `;
// If this word has send us over the edge
if (ctx.measureText(currentLine).width > width && i !== 0) {
// Write to the canvas
ctx.fillText(
lineAccumulator.trim(),
localX,
getLinePosition(localY, linesWritten, fontHeight)
);
// Add this word to the next line
lineAccumulator = `${shortenedWord} `;
linesWritten += 1;
} else {
lineAccumulator = currentLine;
}
}
ctx.fillText(
lineAccumulator,
localX,
getLinePosition(localY, linesWritten, fontHeight)
);
return miniCanvas;
}
export function drawText(text, w, h, properties) {
if (properties && properties.content === "fit") {
return drawFittedText(text, w, h, properties);
} else if (properties && properties.content === "wrap") {
return drawWrappedText(text.toString(), w, h, properties);
}
return drawNativeText(text, w, h, properties);
}
export function getDefaultFontProperties() {
return { ...defaultProperties };
}