package for svg renderer

This commit is contained in:
DD 2017-09-05 11:57:48 -04:00
commit 4d487c8fcd
12 changed files with 447 additions and 0 deletions

6
.babelrc Normal file
View file

@ -0,0 +1,6 @@
{
"plugins": [
"transform-object-rest-spread"
],
"presets": ["es2015"],
}

11
.editorconfig Normal file
View file

@ -0,0 +1,11 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_size = 4
trim_trailing_whitespace = true
[*.{js}]
indent_style = space

2
.eslintignore Normal file
View file

@ -0,0 +1,2 @@
node_modules/*
dist/*

10
.eslintrc.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
extends: ['scratch', 'scratch/es6', 'scratch/node'],
"globals": {
"document": true,
"window": true,
"DOMParser": true,
"Image": true,
"XMLSerializer": true,
}
};

37
.gitattributes vendored Normal file
View file

@ -0,0 +1,37 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto
# Explicitly specify line endings for as many files as possible.
# People who (for example) rsync between Windows and Linux need this.
# File types which we know are binary
*.sb2 binary
# Prefer LF for most file types
*.css text eol=lf
*.frag text eol=lf
*.htm text eol=lf
*.html text eol=lf
*.iml text eol=lf
*.js text eol=lf
*.js.map text eol=lf
*.json text eol=lf
*.md text eol=lf
*.vert text eol=lf
*.xml text eol=lf
*.yml text eol=lf
# Prefer LF for these files
.editorconfig text eol=lf
.eslintignore text eol=lf
.eslintrc text eol=lf
.gitattributes text eol=lf
.gitignore text eol=lf
.gitmodules text eol=lf
.npmignore text eol=lf
LICENSE text eol=lf
Makefile text eol=lf
README text eol=lf
TRADEMARK text eol=lf
# Use CRLF for Windows-specific file types

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Mac OS
.DS_Store
# NPM
/node_modules
npm-*
# Build
dist/*
# Editors
/#*
*~

12
LICENSE Normal file
View file

@ -0,0 +1,12 @@
Copyright (c) 2016, Massachusetts Institute of Technology
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# scratch-blobs
Blobs of Wrath

7
TRADEMARK Normal file
View file

@ -0,0 +1,7 @@
The Scratch trademarks, including the Scratch name, logo, the Scratch Cat, Gobo, Pico, Nano, Tera and Giga graphics (the "Marks"), are property of the Massachusetts Institute of Technology (MIT), and the use of the Marks is governed by this policy.
You may use the Marks to refer to Scratch in Substantially Unmodified form.
"Substantially Unmodified" means the source code provided by MIT, possibly with minor modifications including but not limited to: bug fixes (including security), changing the locations of files for better integration with the host operating system, adding documentation, and changes to the dynamic linking of libraries.
A version is not "Substantially Unmodified" if it incorporates features not present in a release of Scratch by MIT. If you do make a substantial modification, to avoid confusion with versions of Scratch produced by MIT you must remove all Marks from your version of the software and refrain from using any of the Marks to refer to your version.

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "scratch-svg-renderer",
"version": "0.1.0",
"description": "SVG renderer for Scratch",
"main": "./dist/scratch-svg-renderer.js",
"scripts": {
"build": "npm run clean && webpack --progress --colors --bail",
"clean": "rimraf ./dist",
"lint": "eslint . --ext .js",
"watch": "webpack --progress --colors --watch"
},
"author": "Massachusetts Institute of Technology",
"license": "BSD-3-Clause",
"homepage": "https://github.com/LLK/scratch-svg-renderer#readme",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-svg-renderer.git"
},
"devDependencies": {
"babel-core": "^6.23.1",
"babel-eslint": "^7.2.3",
"babel-loader": "^7.1.0",
"babel-plugin-transform-object-rest-spread": "^6.22.0",
"babel-preset-es2015": "^6.22.0",
"base64-loader": "^1.0.0",
"eslint": "^4.4.1",
"eslint-config-import": "^0.13.0",
"eslint-config-scratch": "^4.0.0",
"eslint-plugin-import": "^2.7.0",
"mkdirp": "^0.5.1",
"rimraf": "^2.6.1",
"scratch-render-fonts": "git+https://github.com/LLK/scratch-render-fonts.git",
"webpack": "^2.2.1"
}
}

280
src/svg-renderer.js Normal file
View file

@ -0,0 +1,280 @@
// 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;
// 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();
// Draw to a canvas.
this._draw();
}
/**
* @return {Array<number>} the natural size, in Scratch units, of this SVG.
*/
get size () {
return [this._measurements.width, this._measurements.height];
}
/**
* @return {Array<number>} the offset (upper left corner) of the SVG's view box.
*/
get viewOffset () {
return [this._measurements.x, this._measurements.y];
}
/**
* 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 <tspan> 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', '18');
}
// 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', '1em');
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 <link> or <style>@import</style> 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]);
}
/**
* 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;
// 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;
// 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);
// Convert the SVG text to an Image, and then draw it to the canvas.
const img = new Image();
img.onload = () => {
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
);
}
}
export default SvgRenderer;

32
webpack.config.js Normal file
View file

@ -0,0 +1,32 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.jsx?$/,
loader: 'babel-loader',
include: path.resolve(__dirname, 'src'),
options: {
plugins: ['transform-object-rest-spread'],
presets: ['es2015']
}
}]
},
entry: {
'scratch-svg-renderer': './src/svg-renderer.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
libraryTarget: 'commonjs2'
},
plugins: []
.concat(process.env.NODE_ENV === 'production' ? [
new webpack.optimize.UglifyJsPlugin({
include: /\.min\.js$/,
minimize: true
})
] : [])
};