mirror of
https://github.com/scratchfoundation/scratch-svg-renderer.git
synced 2024-11-14 19:25:41 -05:00
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:
parent
0bbc5d865a
commit
b6e61ed1be
2 changed files with 99 additions and 42 deletions
|
@ -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)}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue