Merge code changes from scratch-render, CWF

The code in this repository was slightly out-of-date relative to
`scratch-render`. I also merged in some minor API changes from my work.
This commit is contained in:
Christopher Willis-Ford 2018-01-04 11:50:33 -08:00
parent 0bbc5d865a
commit b6e61ed1be
2 changed files with 99 additions and 42 deletions

View file

@ -47,6 +47,20 @@ class SvgRenderer {
return this._canvas;
}
/**
* @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];
}
/**
* Load an SVG from a string and draw it.
* This will be parsed and transformed, and finally drawn.
@ -57,6 +71,16 @@ class SvgRenderer {
fromString (svgString, onFinish) {
// Store the callback for later.
this._onFinish = onFinish;
this.loadString(svgString);
// Draw to a canvas.
this._draw();
}
/**
* 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');
@ -69,22 +93,39 @@ class SvgRenderer {
this._transformText();
// Transform measurements.
this._transformMeasurements();
// Draw to a canvas.
this._draw();
}
/**
* @return {Array<number>} the natural size, in Scratch units, of this SVG.
* Serialize the active SVG DOM to a string.
* @returns {string} String representing current SVG data.
*/
get size () {
return [this._measurements.width, this._measurements.height];
toString () {
const serializer = new XMLSerializer();
return serializer.serializeToString(this._svgDom);
}
/**
* @return {Array<number>} the offset (upper left corner) of the SVG's view box.
* 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.
*/
get viewOffset () {
return [this._measurements.x, this._measurements.y];
measure (svgString) {
this.loadString(svgString);
return this._measurements;
}
/**
* 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;
}
/**
@ -118,7 +159,7 @@ class SvgRenderer {
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');
textElement.setAttribute('font-size', '14');
}
// If there's no font-family provided, provide one.
if (!textElement.getAttribute('font-family')) {
@ -136,7 +177,7 @@ class SvgRenderer {
for (const line of lines) {
const tspanNode = this._createSVGElement('tspan');
tspanNode.setAttribute('x', '0');
tspanNode.setAttribute('dy', '1em');
tspanNode.setAttribute('dy', '1.2em');
tspanNode.textContent = line;
textElement.appendChild(tspanNode);
}
@ -162,6 +203,37 @@ class SvgRenderer {
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,
@ -178,7 +250,7 @@ class SvgRenderer {
*/
_transformMeasurements () {
// Save `svgText` for later re-parsing.
const svgText = this._toString();
const svgText = this.toString();
// Append the SVG dom to the document.
// This allows us to use `getBBox` on the page,
@ -203,6 +275,15 @@ class SvgRenderer {
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);
@ -211,29 +292,6 @@ class SvgRenderer {
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.
*/
@ -241,15 +299,14 @@ class SvgRenderer {
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 = () => {
// 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);
@ -261,7 +318,7 @@ class SvgRenderer {
this._onFinish();
}
};
const svgText = this._toString();
const svgText = this.toString();
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
}

View file

@ -39,7 +39,7 @@ module.exports = [
filename: '[name].js'
}
}),
// For testing only: rendering will fail outside a browser
// For testing only: many features will fail outside a browser
makeExport({node: true}, {
output: {
library: 'ScratchSVGRenderer',