UNPKG

@thebigcrunch/sdk

Version:
407 lines (359 loc) 12.4 kB
/** * 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 }; }