From 413c1d80273d18a41777012e68ca81fdda03aa44 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 9 Jan 2018 14:52:12 -0800 Subject: [PATCH] Extract svg-renderer out into a separate module --- package.json | 2 +- src/SVGSkin.js | 6 +- src/svg-quirks-mode/svg-renderer.js | 337 ---------------------------- src/util/svg-text-bubble.js | 3 +- 4 files changed, 6 insertions(+), 342 deletions(-) delete mode 100644 src/svg-quirks-mode/svg-renderer.js diff --git a/package.json b/package.json index 9adba24..1681d09 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "json": "^9.0.4", "linebreak": "0.3.0", "raw-loader": "^0.5.1", - "scratch-render-fonts": "git+https://github.com/LLK/scratch-render-fonts.git", + "scratch-svg-renderer": "git+https://github.com/LLK/scratch-svg-renderer.git", "tap": "^10.3.0", "travis-after-all": "^1.4.4", "twgl.js": "3.7.0", diff --git a/src/SVGSkin.js b/src/SVGSkin.js index 186eb16..787b2b5 100644 --- a/src/SVGSkin.js +++ b/src/SVGSkin.js @@ -1,9 +1,9 @@ const twgl = require('twgl.js'); -const Skin = require('./Skin'); -const SvgRenderer = require('./svg-quirks-mode/svg-renderer'); - const Silhouette = require('./Silhouette'); +const Skin = require('./Skin'); + +import SvgRenderer from 'scratch-svg-renderer'; class SVGSkin extends Skin { /** diff --git a/src/svg-quirks-mode/svg-renderer.js b/src/svg-quirks-mode/svg-renderer.js deleted file mode 100644 index ccf09e3..0000000 --- a/src/svg-quirks-mode/svg-renderer.js +++ /dev/null @@ -1,337 +0,0 @@ -// Synchronously load TTF fonts. -// First, have Webpack load their data as Base 64 strings. -/* eslint-disable global-require */ -const FONTS = { - Donegal: require('base64-loader!scratch-render-fonts/DonegalOne-Regular.ttf'), - Gloria: require('base64-loader!scratch-render-fonts/GloriaHallelujah.ttf'), - Mystery: require('base64-loader!scratch-render-fonts/MysteryQuest-Regular.ttf'), - Marker: require('base64-loader!scratch-render-fonts/PermanentMarker.ttf'), - Scratch: require('base64-loader!scratch-render-fonts/Scratch.ttf') -}; -/* eslint-enable global-require */ - -// For each Base 64 string, -// 1. Replace each with a usable @font-face tag that points to a Data URI. -// 2. Inject the font into a style on `document.body`, so measurements -// can be accurately taken in SvgRenderer._transformMeasurements. -const documentStyleTag = document.createElement('style'); -documentStyleTag.id = 'scratch-font-styles'; -for (const fontName in FONTS) { - const fontData = FONTS[fontName]; - FONTS[fontName] = '@font-face {' + - `font-family: "${fontName}";src: url("data:application/x-font-ttf;charset=utf-8;base64,${fontData}");}`; - documentStyleTag.textContent += FONTS[fontName]; -} -document.body.insertBefore(documentStyleTag, document.body.firstChild); - -/** - * Main quirks-mode SVG rendering code. - */ -class SvgRenderer { - /** - * Create a quirks-mode SVG renderer for a particular canvas. - * @param {HTMLCanvasElement} [canvas] An optional canvas element to draw to. If this is not provided, the renderer - * will create a new canvas. - * @constructor - */ - constructor (canvas) { - this._canvas = canvas || document.createElement('canvas'); - this._context = this._canvas.getContext('2d'); - this._measurements = {x: 0, y: 0, width: 0, height: 0}; - } - - /** - * @returns {!HTMLCanvasElement} this renderer's target canvas. - */ - get canvas () { - return this._canvas; - } - - /** - * Load an SVG from a string and draw it. - * This will be parsed and transformed, and finally drawn. - * When drawing is finished, the `onFinish` callback is called. - * @param {string} svgString String of SVG data to draw in quirks-mode. - * @param {Function} [onFinish] Optional callback for when drawing finished. - */ - fromString (svgString, onFinish) { - // Store the callback for later. - this._onFinish = onFinish; - this._loadString(svgString); - // Draw to a canvas. - this._draw(); - } - - /** - * Load an SVG from a string and measure it. - * @param {string} svgString String of SVG data to draw in quirks-mode. - * @return {object} the natural size, in Scratch units, of this SVG. - */ - measure (svgString) { - this._loadString(svgString); - return this._measurements; - } - - /** - * @return {Array} the natural size, in Scratch units, of this SVG. - */ - get size () { - return [this._measurements.width, this._measurements.height]; - } - - /** - * @return {Array} the offset (upper left corner) of the SVG's view box. - */ - get viewOffset () { - return [this._measurements.x, this._measurements.y]; - } - - /** - * Load an SVG string and normalize it. All the steps before drawing/measuring. - * @param {string} svgString String of SVG data to draw in quirks-mode. - */ - _loadString (svgString) { - // Parse string into SVG XML. - const parser = new DOMParser(); - this._svgDom = parser.parseFromString(svgString, 'text/xml'); - if (this._svgDom.childNodes.length < 1 || - this._svgDom.documentElement.localName !== 'svg') { - throw new Error('Document does not appear to be SVG.'); - } - this._svgTag = this._svgDom.documentElement; - // Transform all text elements. - this._transformText(); - // Transform measurements. - this._transformMeasurements(); - } - - /** - * Transforms an SVG's text elements for Scratch 2.0 quirks. - * These quirks include: - * 1. `x` and `y` properties are removed/ignored. - * 2. Alignment is set to `text-before-edge`. - * 3. Line-breaks are converted to explicit elements. - * 4. Any required fonts are injected. - */ - _transformText () { - // Collect all text elements into a list. - const textElements = []; - const collectText = domElement => { - if (domElement.localName === 'text') { - textElements.push(domElement); - } - for (let i = 0; i < domElement.childNodes.length; i++) { - collectText(domElement.childNodes[i]); - } - }; - collectText(this._svgTag); - // For each text element, apply quirks. - const fontsNeeded = {}; - for (const textElement of textElements) { - // Remove x and y attributes - they are not used in Scratch. - textElement.removeAttribute('x'); - textElement.removeAttribute('y'); - // Set text-before-edge alignment: - // Scratch renders all text like this. - textElement.setAttribute('alignment-baseline', 'text-before-edge'); - // If there's no font size provided, provide one. - if (!textElement.getAttribute('font-size')) { - textElement.setAttribute('font-size', '14'); - } - // If there's no font-family provided, provide one. - if (!textElement.getAttribute('font-family')) { - textElement.setAttribute('font-family', 'Helvetica'); - } - // Collect fonts that need injection. - const font = textElement.getAttribute('font-family'); - fontsNeeded[font] = true; - // Fix line breaks in text, which are not natively supported by SVG. - let text = textElement.textContent; - if (text) { - textElement.textContent = ''; - const lines = text.split('\n'); - text = ''; - for (const line of lines) { - const tspanNode = this._createSVGElement('tspan'); - tspanNode.setAttribute('x', '0'); - tspanNode.setAttribute('dy', '1.2em'); - tspanNode.textContent = line; - textElement.appendChild(tspanNode); - } - } - } - // Inject fonts that are needed. - // It would be nice if there were another way to get the SVG-in-canvas - // to render the correct font family, but I couldn't find any other way. - // Other things I tried: - // Just injecting the font-family into the document: no effect. - // External stylesheet linked to by SVG: no effect. - // Using a or to link to font-family - // injected into the document: no effect. - const newDefs = this._createSVGElement('defs'); - const newStyle = this._createSVGElement('style'); - const allFonts = Object.keys(fontsNeeded); - for (const font of allFonts) { - if (FONTS.hasOwnProperty(font)) { - newStyle.textContent += FONTS[font]; - } - } - newDefs.appendChild(newStyle); - this._svgTag.insertBefore(newDefs, this._svgTag.childNodes[0]); - } - - /** - * Find the largest stroke width in the svg. If a shape has no - * `stroke` property, it has a stroke-width of 0. If it has a `stroke`, - * it is by default a stroke-width of 1. - * This is used to enlarge the computed bounding box, which doesn't take - * stroke width into account. - * @param {SVGSVGElement} rootNode The root SVG node to traverse. - * @return {number} The largest stroke width in the SVG. - */ - _findLargestStrokeWidth (rootNode) { - let largestStrokeWidth = 0; - const collectStrokeWidths = domElement => { - if (domElement.getAttribute) { - if (domElement.getAttribute('stroke')) { - largestStrokeWidth = Math.max(largestStrokeWidth, 1); - } - if (domElement.getAttribute('stroke-width')) { - largestStrokeWidth = Math.max( - largestStrokeWidth, - Number(domElement.getAttribute('stroke-width')) || 0 - ); - } - } - for (let i = 0; i < domElement.childNodes.length; i++) { - collectStrokeWidths(domElement.childNodes[i]); - } - }; - collectStrokeWidths(rootNode); - return largestStrokeWidth; - } - - /** - * Transform the measurements of the SVG. - * In Scratch 2.0, SVGs are drawn without respect to the width, - * height, and viewBox attribute on the tag. The exporter - * does output these properties - but they appear to be incorrect often. - * To address the incorrect measurements, we append the DOM to the - * document, and then use SVG's native `getBBox` to find the real - * drawn dimensions. This ensures things drawn in negative dimensions, - * outside the given viewBox, etc., are all eventually drawn to the canvas. - * I tried to do this several other ways: stripping the width/height/viewBox - * attributes and then drawing (Firefox won't draw anything), - * or inflating them and then measuring a canvas. But this seems to be - * a natural and performant way. - */ - _transformMeasurements () { - // Save `svgText` for later re-parsing. - const svgText = this._toString(); - - // Append the SVG dom to the document. - // This allows us to use `getBBox` on the page, - // which returns the full bounding-box of all drawn SVG - // elements, similar to how Scratch 2.0 did measurement. - const svgSpot = document.createElement('span'); - let bbox; - try { - document.body.appendChild(svgSpot); - svgSpot.appendChild(this._svgTag); - // Take the bounding box. - bbox = this._svgTag.getBBox(); - } finally { - // Always destroy the element, even if, for example, getBBox throws. - document.body.removeChild(svgSpot); - } - - // Re-parse the SVG from `svgText`. The above DOM becomes - // unusable/undrawable in browsers once it's appended to the page, - // perhaps for security reasons? - const parser = new DOMParser(); - this._svgDom = parser.parseFromString(svgText, 'text/xml'); - this._svgTag = this._svgDom.documentElement; - - // Enlarge the bbox from the largest found stroke width - // This may have false-positives, but at least the bbox will always - // contain the full graphic including strokes. - const halfStrokeWidth = this._findLargestStrokeWidth(this._svgTag) / 2; - bbox.width += halfStrokeWidth * 2; - bbox.height += halfStrokeWidth * 2; - bbox.x -= halfStrokeWidth; - bbox.y -= halfStrokeWidth; - - // Set the correct measurements on the SVG tag, and save them. - this._svgTag.setAttribute('width', bbox.width); - this._svgTag.setAttribute('height', bbox.height); - this._svgTag.setAttribute('viewBox', - `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`); - this._measurements = bbox; - } - - /** - * Serialize the active SVG DOM to a string. - * @returns {string} String representing current SVG data. - */ - _toString () { - const serializer = new XMLSerializer(); - return serializer.serializeToString(this._svgDom); - } - - /** - * Get the drawing ratio, adjusted for HiDPI screens. - * @return {number} Scale ratio to draw to canvases with. - */ - getDrawRatio () { - const devicePixelRatio = window.devicePixelRatio || 1; - const backingStoreRatio = this._context.webkitBackingStorePixelRatio || - this._context.mozBackingStorePixelRatio || - this._context.msBackingStorePixelRatio || - this._context.oBackingStorePixelRatio || - this._context.backingStorePixelRatio || 1; - return devicePixelRatio / backingStoreRatio; - } - - /** - * Draw the SVG to a canvas. - */ - _draw () { - const ratio = this.getDrawRatio(); - const bbox = this._measurements; - - // Convert the SVG text to an Image, and then draw it to the canvas. - const img = new Image(); - img.onload = () => { - // Set up the canvas for drawing. - this._canvas.width = bbox.width * ratio; - this._canvas.height = bbox.height * ratio; - this._context.clearRect(0, 0, this._canvas.width, this._canvas.height); - this._context.scale(ratio, ratio); - this._context.drawImage(img, 0, 0); - // Reset the canvas transform after drawing. - this._context.setTransform(1, 0, 0, 1, 0, 0); - // Set the CSS style of the canvas to the actual measurements. - this._canvas.style.width = bbox.width; - this._canvas.style.height = bbox.height; - // All finished - call the callback if provided. - if (this._onFinish) { - this._onFinish(); - } - }; - const svgText = this._toString(); - img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; - } - - /** - * Helper to create an SVG element with the correct NS. - * @param {string} tagName Tag name for the element. - * @return {!DOMElement} Element created. - */ - _createSVGElement (tagName) { - return document.createElementNS( - 'http://www.w3.org/2000/svg', tagName - ); - } -} - -module.exports = SvgRenderer; diff --git a/src/util/svg-text-bubble.js b/src/util/svg-text-bubble.js index 1458e3d..5af8f20 100644 --- a/src/util/svg-text-bubble.js +++ b/src/util/svg-text-bubble.js @@ -1,5 +1,6 @@ const SVGTextWrapper = require('./svg-text-wrapper'); -const SVGRenderer = require('../svg-quirks-mode/svg-renderer'); + +import SVGRenderer from 'scratch-svg-renderer'; const MAX_LINE_LENGTH = 170; const MIN_WIDTH = 50;