paper.js/src/svg/SVGExport.js

538 lines
16 KiB
JavaScript
Raw Normal View History

2012-11-02 20:47:14 -04:00
/*
* Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
2012-09-30 17:51:50 -04:00
* http://paperjs.org/
*
* Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
2012-09-30 17:51:50 -04:00
* http://lehni.org/ & http://jonathanpuckey.com/
*
* Distributed under the MIT license. See LICENSE file for details.
*
* All rights reserved.
*/
2012-11-04 02:04:15 -05:00
/**
* A function scope holding all the functionality needed to convert a
* Paper.js DOM to a SVG DOM.
2012-11-04 02:04:15 -05:00
*/
new function() {
var formatter;
2013-02-10 22:02:53 -05:00
function setAttributes(node, attrs) {
for (var key in attrs) {
var val = attrs[key],
namespace = SVGNamespaces[key];
if (typeof val === 'number')
val = formatter.number(val);
if (namespace) {
node.setAttributeNS(namespace, key, val);
} else {
2013-02-10 22:02:53 -05:00
node.setAttribute(key, val);
}
}
2013-02-10 22:02:53 -05:00
return node;
2012-11-05 23:31:45 -05:00
}
function createElement(tag, attrs) {
return setAttributes(
document.createElementNS('http://www.w3.org/2000/svg', tag), attrs);
}
function getDistance(segments, index1, index2) {
return segments[index1]._point.getDistance(segments[index2]._point);
}
function getTransform(item, coordinates) {
var matrix = item._matrix,
trans = matrix.getTranslation(),
attrs = {};
if (coordinates) {
// If the item suppports x- and y- coordinates, we're taking out the
// translation part of the matrix and move it to x, y attributes, to
// produce more readable markup, and not have to use center points
// in rotate(). To do so, SVG requries us to inverse transform the
// translation point by the matrix itself, since they are provided
// in local coordinates.
matrix = matrix.shiftless();
var point = matrix._inverseTransform(trans);
attrs.x = point.x;
attrs.y = point.y;
trans = null;
}
if (matrix.isIdentity())
return attrs;
// See if we can decompose the matrix and can formulate it as a simple
// translate/scale/rotate command sequence.
var decomposed = matrix.decompose();
if (decomposed && !decomposed.shearing) {
var parts = [],
angle = decomposed.rotation,
scale = decomposed.scaling;
if (trans && !trans.isZero())
parts.push('translate(' + formatter.point(trans) + ')');
if (!Numerical.isZero(scale.x - 1) || !Numerical.isZero(scale.y - 1))
parts.push('scale(' + formatter.point(scale) +')');
if (angle)
parts.push('rotate(' + formatter.number(angle) + ')');
attrs.transform = parts.join(' ');
} else {
attrs.transform = 'matrix(' + matrix.getValues().join(',') + ')';
}
return attrs;
}
2012-11-06 14:00:58 -05:00
function determineAngle(path, segments, type, center) {
// If the object is a circle, ellipse, rectangle, or rounded rectangle,
// see if it is placed at an angle, by figuring out its topCenter point
// and measuring the angle to its center.
2012-11-06 14:00:58 -05:00
var topCenter = type === 'rect'
? segments[1]._point.add(segments[2]._point).divide(2)
: type === 'roundrect'
? segments[3]._point.add(segments[4]._point).divide(2)
: type === 'circle' || type === 'ellipse'
? segments[1]._point
: null;
var angle = topCenter && topCenter.subtract(center).getAngle() + 90;
return Numerical.isZero(angle || 0) ? 0 : angle;
2012-11-06 14:00:58 -05:00
}
function determineType(path, segments) {
// Returns true if the the two segment indices are the beggining of two
// lines and if the wto lines are parallel.
function isColinear(i, j) {
var seg1 = segments[i],
seg2 = seg1.getNext(),
seg3 = segments[j],
seg4 = seg3.getNext();
return seg1._handleOut.isZero() && seg2._handleIn.isZero()
&& seg3._handleOut.isZero() && seg4._handleIn.isZero()
&& seg2._point.subtract(seg1._point).isColinear(
seg4._point.subtract(seg3._point));
}
// Returns true if the segment at the given index is the beginning of
// a orthogonal arc segment. The code is looking at the length of the
// handles and their relation to the distance to the imaginary corner
// point. If the relation is kappa, then it's an arc.
2012-11-06 14:00:58 -05:00
function isArc(i) {
var segment = segments[i],
next = segment.getNext(),
handle1 = segment._handleOut,
2013-04-19 21:57:31 -04:00
handle2 = next._handleIn,
kappa = Numerical.KAPPA;
2012-11-06 14:00:58 -05:00
if (handle1.isOrthogonal(handle2)) {
var from = segment._point,
to = next._point,
// Find the corner point by intersecting the lines described
2012-11-06 14:00:58 -05:00
// by both handles:
corner = new Line(from, handle1, true).intersect(
new Line(to, handle2, true), true);
2012-11-06 14:00:58 -05:00
return corner && Numerical.isZero(handle1.getLength() /
2013-04-19 21:57:31 -04:00
corner.subtract(from).getLength() - kappa)
2012-11-06 14:00:58 -05:00
&& Numerical.isZero(handle2.getLength() /
2013-04-19 21:57:31 -04:00
corner.subtract(to).getLength() - kappa);
2012-11-06 14:00:58 -05:00
}
}
// See if actually have any curves in the path. Differentiate
// between straight objects (line, polyline, rect, and polygon) and
// objects with curves(circle, ellipse, roundedRectangle).
if (path.isPolygon()) {
return segments.length === 4 && path._closed
&& isColinear(0, 2) && isColinear(1, 3)
? 'rect'
2012-12-09 21:04:56 -05:00
: segments.length === 0
? 'empty'
: segments.length >= 3
? path._closed ? 'polygon' : 'polyline'
: 'line';
2012-11-06 14:00:58 -05:00
} else if (path._closed) {
if (segments.length === 8
&& isArc(0) && isArc(2) && isArc(4) && isArc(6)
&& isColinear(1, 5) && isColinear(3, 7)) {
return 'roundrect';
} else if (segments.length === 4
&& isArc(0) && isArc(1) && isArc(2) && isArc(3)) {
// If the distance between (point0 and point2) and (point1
// and point3) are equal, then it is a circle
return Numerical.isZero(getDistance(segments, 0, 2)
- getDistance(segments, 1, 3))
? 'circle'
: 'ellipse';
}
}
return 'path';
}
2013-02-10 22:02:53 -05:00
function exportGroup(item) {
var attrs = getTransform(item),
children = item._children;
var node = createElement('g', attrs);
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var childNode = exportSVG(child);
if (childNode) {
if (child.isClipMask()) {
var clip = createElement('clipPath');
clip.appendChild(childNode);
setDefinition(child, clip, 'clip');
setAttributes(node, {
'clip-path': 'url(#' + clip.id + ')'
});
} else {
node.appendChild(childNode);
}
}
}
2013-02-10 22:02:53 -05:00
return node;
}
2013-02-09 12:44:25 -05:00
function exportRaster(item) {
var attrs = getTransform(item, true),
size = item.getSize();
// Take into account that rasters are centered:
2013-02-09 12:44:25 -05:00
attrs.x -= size.width / 2;
attrs.y -= size.height / 2;
2013-02-10 13:23:49 -05:00
attrs.width = size.width;
attrs.height = size.height;
attrs.href = item.toDataURL();
return createElement('image', attrs);
2013-02-09 12:44:25 -05:00
}
function exportPath(item) {
var segments = item._segments,
center = item.getPosition(true),
type = determineType(item, segments),
angle = determineAngle(item, segments, type, center),
attrs;
2012-09-30 17:51:50 -04:00
switch (type) {
2012-12-09 21:04:56 -05:00
case 'empty':
return null;
2012-11-06 02:45:23 -05:00
case 'path':
var data = item.getPathData();
attrs = data && { d: data };
2012-11-06 02:45:23 -05:00
break;
case 'polyline':
case 'polygon':
var parts = [];
2012-11-06 14:28:50 -05:00
for(i = 0, l = segments.length; i < l; i++)
parts.push(formatter.point(segments[i]._point));
2012-11-06 02:45:23 -05:00
attrs = {
points: parts.join(' ')
};
break;
case 'rect':
2012-11-05 23:31:45 -05:00
var width = getDistance(segments, 0, 3),
height = getDistance(segments, 0, 1),
2012-11-06 11:28:54 -05:00
// Counter-compensate the determined rotation angle
point = segments[1]._point.rotate(-angle, center);
attrs = {
x: point.x,
y: point.y,
2012-11-05 23:31:45 -05:00
width: width,
height: height
};
break;
case 'roundrect':
type = 'rect';
// d-variables and point are used to determine the rounded corners
// for the rounded rectangle
var width = getDistance(segments, 1, 6),
height = getDistance(segments, 0, 3),
// Subtract side lengths from total width and divide by 2 to get
// corner radius size
rx = (width - getDistance(segments, 0, 7)) / 2,
ry = (height - getDistance(segments, 1, 2)) / 2,
// Calculate topLeft corner point, by using sides vectors and
// subtracting normalized rx vector to calculate arc corner.
left = segments[3]._point, // top-left side point
right = segments[4]._point, // top-right side point
point = left.subtract(right.subtract(left).normalize(rx))
2012-11-06 11:28:54 -05:00
// Counter-compensate the determined rotation angle
.rotate(-angle, center);
attrs = {
x: point.x,
y: point.y,
2012-11-05 23:31:45 -05:00
width: width,
height: height,
rx: rx,
ry: ry
};
break;
case'line':
2012-11-05 23:31:45 -05:00
var first = segments[0]._point,
last = segments[segments.length - 1]._point;
attrs = {
x1: first.x,
y1: first.y,
x2: last.x,
y2: last.y
};
break;
case 'circle':
var radius = getDistance(segments, 0, 2) / 2;
attrs = {
cx: center.x,
cy: center.y,
2012-11-05 23:31:45 -05:00
r: radius
};
break;
case 'ellipse':
2012-11-06 00:19:53 -05:00
var rx = getDistance(segments, 2, 0) / 2,
ry = getDistance(segments, 3, 1) / 2;
attrs = {
cx: center.x,
cy: center.y,
2012-11-06 00:19:53 -05:00
rx: rx,
ry: ry
};
break;
}
2012-11-06 02:45:23 -05:00
if (angle) {
attrs.transform = 'rotate(' + formatter.number(angle) + ','
+ formatter.point(center) + ')';
// Tell applyStyle() that to transform the gradient the other way
item._gradientMatrix = new Matrix().rotate(-angle, center);
2012-09-30 17:51:50 -04:00
}
2013-02-09 16:38:22 -05:00
return createElement(type, attrs);
}
2013-02-10 13:23:49 -05:00
function exportCompoundPath(item) {
var attrs = getTransform(item, true);
var data = item.getPathData();
if (data)
attrs.d = data;
2013-02-10 13:23:49 -05:00
return createElement('path', attrs);
}
function exportPlacedSymbol(item) {
var attrs = getTransform(item, true),
symbol = item.getSymbol(),
2013-10-16 10:14:37 -04:00
symbolNode = getDefinition(symbol, 'symbol'),
2013-02-10 13:23:49 -05:00
definition = symbol.getDefinition(),
bounds = definition.getBounds();
if (!symbolNode) {
symbolNode = createElement('symbol', {
viewBox: formatter.rectangle(bounds)
2013-02-10 13:23:49 -05:00
});
2013-04-23 10:19:08 -04:00
symbolNode.appendChild(exportSVG(definition));
setDefinition(symbol, symbolNode, 'symbol');
2013-02-10 13:23:49 -05:00
}
attrs.href = '#' + symbolNode.id;
2013-02-10 13:23:49 -05:00
attrs.x += bounds.x;
attrs.y += bounds.y;
attrs.width = formatter.number(bounds.width);
attrs.height = formatter.number(bounds.height);
2013-02-10 13:23:49 -05:00
return createElement('use', attrs);
}
function exportGradient(color, item) {
// NOTE: As long as the fillTransform attribute is not implemented,
// we need to create a separate gradient object for each gradient,
// even when they share the same gradient defintion.
// http://www.svgopen.org/2011/papers/20-Separating_gradients_from_geometry/
2013-04-23 10:19:08 -04:00
// TODO: Implement gradient merging in SVGImport
var gradientNode = getDefinition(color, 'color');
if (!gradientNode) {
var gradient = color.getGradient(),
radial = gradient._radial,
2013-02-12 20:23:56 -05:00
matrix = item._gradientMatrix,
origin = color.getOrigin().transform(matrix),
destination = color.getDestination().transform(matrix),
attrs;
2013-05-08 23:29:37 -04:00
if (radial) {
attrs = {
cx: origin.x,
cy: origin.y,
r: origin.getDistance(destination)
};
var highlight = color.getHighlight();
if (highlight) {
highlight = highlight.transform(matrix);
attrs.fx = highlight.x;
attrs.fy = highlight.y;
}
2013-05-08 23:29:37 -04:00
} else {
attrs = {
x1: origin.x,
y1: origin.y,
x2: destination.x,
y2: destination.y
};
}
attrs.gradientUnits = 'userSpaceOnUse';
gradientNode = createElement(
(radial ? 'radial' : 'linear') + 'Gradient', attrs);
var stops = gradient._stops;
for (var i = 0, l = stops.length; i < l; i++) {
var stop = stops[i],
stopColor = stop._color,
alpha = stopColor.getAlpha();
2013-02-10 22:40:44 -05:00
attrs = {
offset: stop._rampPoint,
2013-06-12 17:04:59 -04:00
'stop-color': stopColor.toCSS(true)
2013-02-10 22:40:44 -05:00
};
// See applyStyle for an explanation of why there are separated
// opacity / color attributes.
if (alpha < 1)
attrs['stop-opacity'] = alpha;
gradientNode.appendChild(createElement('stop', attrs));
}
setDefinition(color, gradientNode, 'color');
2013-02-10 22:02:53 -05:00
}
return 'url(#' + gradientNode.id + ')';
2013-02-10 22:02:53 -05:00
}
2013-06-18 19:57:09 -04:00
function exportText(item) {
var node = createElement('text', getTransform(item, true));
node.textContent = item._content;
return node;
}
2012-11-06 14:00:58 -05:00
var exporters = {
group: exportGroup,
layer: exportGroup,
raster: exportRaster,
path: exportPath,
2013-06-18 19:57:09 -04:00
'compound-path': exportCompoundPath,
'placed-symbol': exportPlacedSymbol,
2013-06-18 19:57:09 -04:00
'point-text': exportText
2012-11-06 14:00:58 -05:00
};
2013-02-10 22:02:53 -05:00
function applyStyle(item, node) {
var attrs = {},
parent = item.getParent();
if (item._name != null)
2012-11-05 22:26:54 -05:00
attrs.id = item._name;
2013-04-23 10:19:08 -04:00
Base.each(SVGStyles, function(entry) {
// Get a given style only if it differs from the value on the parent
// (A layer or group which can have style values in SVG).
2013-06-18 19:18:13 -04:00
var get = entry.get,
type = entry.type,
value = item[get]();
if (!parent || !Base.equals(parent[get](), value)) {
2013-06-18 19:18:13 -04:00
if (type === 'color' && value != null) {
// Support for css-style rgba() values is not in SVG 1.1, so
// separate the alpha value of colors with alpha into the
// separate fill- / stroke-opacity attribute:
var alpha = value.getAlpha();
if (alpha < 1)
attrs[entry.attribute + '-opacity'] = alpha;
}
attrs[entry.attribute] = value == null
? 'none'
2013-06-18 19:18:13 -04:00
: type === 'number'
? formatter.number(value)
2013-06-18 19:18:13 -04:00
: type === 'color'
? value.gradient
? exportGradient(value, item)
// true for noAlpha, see above
: value.toCSS(true)
2013-06-18 19:18:13 -04:00
: type === 'array'
? value.join(',')
2013-06-18 19:18:13 -04:00
: type === 'lookup'
? entry.toSVG[value]
: value;
}
});
if (attrs.opacity === 1)
delete attrs.opacity;
if (item._visibility != null && !item._visibility)
attrs.visibility = 'hidden';
delete item._gradientMatrix; // see exportPath()
2013-02-10 22:02:53 -05:00
return setAttributes(node, attrs);
}
2013-02-10 13:23:49 -05:00
var definitions;
function getDefinition(item, type) {
2013-02-10 13:23:49 -05:00
if (!definitions)
definitions = { ids: {}, svgs: {} };
return item && definitions.svgs[type + '-' + item._id];
2013-02-10 13:23:49 -05:00
}
function setDefinition(item, node, type) {
// Make sure the definitions lookup is created before we use it.
// This is required by 'clip', where getDefinition() is not called.
if (!definitions)
getDefinition();
// Have different id ranges per type
var id = definitions.ids[type] = (definitions.ids[type] || 0) + 1;
2013-06-18 20:29:00 -04:00
// Give the svg node an id, and link to it from the item id.
2013-02-10 22:02:53 -05:00
node.id = type + '-' + id;
2013-06-18 20:29:00 -04:00
definitions.svgs[type + '-' + item._id] = node;
2013-02-10 13:23:49 -05:00
}
function exportDefinitions(node, options) {
2013-02-10 13:23:49 -05:00
if (!definitions)
2013-02-10 22:02:53 -05:00
return node;
// We can only use svg nodes as defintion containers. Have the loop
2013-02-10 13:23:49 -05:00
// produce one if it's a single item of another type (when calling
2013-04-23 10:19:08 -04:00
// #exportSVG() on an item rather than a whole project)
// jsdom in Node.js uses uppercase values for nodeName...
var svg = node.nodeName.toLowerCase() === 'svg' && node,
defs = null;
2013-02-10 13:23:49 -05:00
for (var i in definitions.svgs) {
// This code is inside the loop so we only create a container if we
// actually have svgs.
if (!defs) {
if (!svg) {
svg = createElement('svg');
svg.appendChild(node);
}
defs = svg.insertBefore(createElement('defs'), svg.firstChild);
2013-02-10 13:23:49 -05:00
}
defs.appendChild(definitions.svgs[i]);
2013-02-10 13:23:49 -05:00
}
// Clear definitions at the end of export
definitions = null;
return options && options.asString
? new XMLSerializer().serializeToString(svg)
: svg;
2013-02-10 13:23:49 -05:00
}
2013-04-23 10:19:08 -04:00
function exportSVG(item) {
var exporter = exporters[item._type],
node = exporter && exporter(item, item._type);
if (node && item._data)
node.setAttribute('data-paper-data', JSON.stringify(item._data));
2013-02-10 22:02:53 -05:00
return node && applyStyle(item, node);
2013-02-10 13:23:49 -05:00
}
function setOptions(options) {
formatter = options && options.precision
? new Formatter(options.precision)
: Formatter.instance;
}
Item.inject({
exportSVG: function(options) {
setOptions(options);
return exportDefinitions(exportSVG(this), options);
}
});
Project.inject({
exportSVG: function(options) {
setOptions(options);
2013-06-11 17:15:54 -04:00
var layers = this.layers,
size = this.view.getSize(),
node = createElement('svg', {
x: 0,
y: 0,
width: size.width,
height: size.height,
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
2013-06-19 11:22:20 -04:00
'xmlns:xlink': 'http://www.w3.org/1999/xlink'
2013-06-11 17:15:54 -04:00
});
for (var i = 0, l = layers.length; i < l; i++)
2013-04-23 10:19:08 -04:00
node.appendChild(exportSVG(layers[i]));
return exportDefinitions(node, options);
}
});
};