diff --git a/examples/Scripts/StrokeScaling.html b/examples/Scripts/StrokeScaling.html
new file mode 100644
index 00000000..a7c79fa8
--- /dev/null
+++ b/examples/Scripts/StrokeScaling.html
@@ -0,0 +1,66 @@
+
+
+
+
+ Shapes
+
+
+
+
+
+
+
+
+
diff --git a/src/basic/Matrix.js b/src/basic/Matrix.js
index 0ee17058..63bf520b 100644
--- a/src/basic/Matrix.js
+++ b/src/basic/Matrix.js
@@ -437,7 +437,7 @@ var Matrix = Base.extend(/** @lends Matrix# */{
* as x, y value pairs
* @param {Number[]} dst the array into which to store the transformed
* point pairs
- * @param {Number} count the number of points to tranform
+ * @param {Number} count the number of points to transform
* @return {Number[]} the dst array, containing the transformed coordinates.
*/
transform: function(/* point | */ src, dst, count) {
diff --git a/src/item/Item.js b/src/item/Item.js
index 56ab71e4..1d97d2de 100644
--- a/src/item/Item.js
+++ b/src/item/Item.js
@@ -2546,8 +2546,8 @@ var Item = Base.extend(Callback, /** @lends Item# */{
*/
/**
- * The shape to be used at the end of open {@link Path} items, when they
- * have a stroke.
+ * The shape to be used at the beginning and end of open {@link Path} items,
+ * when they have a stroke.
*
* @name Item#strokeCap
* @property
@@ -2579,7 +2579,8 @@ var Item = Base.extend(Callback, /** @lends Item# */{
*/
/**
- * The shape to be used at the corners of paths when they have a stroke.
+ * The shape to be used at the segments and corners of {@link Path} items
+ * when they have a stroke.
*
* @name Item#strokeJoin
* @property
@@ -2615,6 +2616,17 @@ var Item = Base.extend(Callback, /** @lends Item# */{
* @type Number
*/
+ /**
+ * Specifies whether the stroke is to be drawn taking the current affine
+ * transformation into account (the default behavior), or whether it should
+ * appear as a non-scaling stroke.
+ *
+ * @name Style#strokeScaling
+ * @property
+ * @default true
+ * @type Boolean
+ */
+
/**
* Specifies an array containing the dash and gap lengths of the stroke.
*
@@ -3574,7 +3586,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{
}
},
- draw: function(ctx, param) {
+ draw: function(ctx, param, parentStrokeMatrix) {
// Each time the project gets drawn, it's _updateVersion is increased.
// Keep the _updateVersion of drawn items in sync, so we have an easy
// way to know for which selected items we need to draw selection info.
@@ -3631,7 +3643,9 @@ var Item = Base.extend(Callback, /** @lends Item# */{
// If native blending is possible, see if the item allows it
|| (nativeBlend || normalBlend && opacity < 1)
&& this._canComposite(),
+ pixelRatio = param.pixelRatio,
mainCtx, itemOffset, prevOffset;
+
if (!direct) {
// Apply the parent's global matrix to the calculation of correct
// bounds.
@@ -3648,28 +3662,48 @@ var Item = Base.extend(Callback, /** @lends Item# */{
// it, instead of the mainCtx.
mainCtx = ctx;
ctx = CanvasProvider.getContext(
- bounds.getSize().ceil().add(new Size(1, 1)),
- param.pixelRatio);
+ bounds.getSize().ceil().add(new Size(1, 1)), pixelRatio);
}
ctx.save();
+ // Get the transformation matrix for non-scaling strokes.
+ var strokeMatrix = parentStrokeMatrix
+ ? parentStrokeMatrix.clone().concatenate(matrix)
+ : !this.getStrokeScaling() && getViewMatrix(globalMatrix),
+ // If we're drawing into a separate canvas and a clipItem is defined
+ // for the current rendering loop, we need to draw the clip item
+ // again.
+ clip = !direct && param.clipItem,
+ // If we're drawing with a strokeMatrix, the CTM is reset either way
+ // so we don't need to set it, except when we also have to draw a
+ // clipItem.
+ transform = !strokeMatrix || clip;
// If drawing directly, handle opacity and native blending now,
// otherwise we will do it later when the temporary canvas is composited.
if (direct) {
ctx.globalAlpha = opacity;
if (nativeBlend)
ctx.globalCompositeOperation = blendMode;
- } else {
+ } else if (transform) {
// Translate the context so the topLeft of the item is at (0, 0)
// on the temporary canvas.
ctx.translate(-itemOffset.x, -itemOffset.y);
}
// Apply globalMatrix when drawing into temporary canvas.
- (direct ? matrix : getViewMatrix(globalMatrix)).applyToContext(ctx);
- // If we're drawing into a separate canvas and a clipItem is defined for
- // the current rendering loop, we need to draw the clip item again.
- if (!direct && param.clipItem)
+ if (transform)
+ (direct ? matrix : getViewMatrix(globalMatrix)).applyToContext(ctx);
+ if (clip)
param.clipItem.draw(ctx, param.extend({ clip: true }));
- this._draw(ctx, param);
+ if (strokeMatrix) {
+ // Reset the transformation but take HiDPI pixel ratio into account.
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
+ // Also offset again when drawing non-directly.
+ // NOTE: Don't use itemOffset since offset might be from the parent,
+ // e.g. CompoundPath
+ var offset = param.offset;
+ if (offset)
+ ctx.translate(-offset.x, -offset.y);
+ }
+ this._draw(ctx, param, strokeMatrix);
ctx.restore();
matrices.pop();
if (param.clip && !param.dontFinish)
@@ -3681,7 +3715,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{
BlendMode.process(blendMode, ctx, mainCtx, opacity,
// Calculate the pixel offset of the temporary canvas to the
// main canvas. We also need to factor in the pixel-ratio.
- itemOffset.subtract(prevOffset).multiply(param.pixelRatio));
+ itemOffset.subtract(prevOffset).multiply(pixelRatio));
// Return the temporary context, so it can be reused
CanvasProvider.release(ctx);
// Restore previous offset.
diff --git a/src/item/Shape.js b/src/item/Shape.js
index e024bc1a..357e3bbc 100644
--- a/src/item/Shape.js
+++ b/src/item/Shape.js
@@ -173,55 +173,71 @@ var Shape = Item.extend(/** @lends Shape# */{
return path;
},
- _draw: function(ctx, param) {
+ _draw: function(ctx, param, strokeMatrix) {
var style = this._style,
hasFill = style.hasFill(),
hasStroke = style.hasStroke(),
- dontPaint = param.dontFinish || param.clip;
+ dontPaint = param.dontFinish || param.clip,
+ untransformed = !strokeMatrix;
if (hasFill || hasStroke || dontPaint) {
- var radius = this._radius,
- type = this._type;
- if (!param.dontStart)
- ctx.beginPath();
- if (type === 'circle') {
+ var type = this._type,
+ radius = this._radius,
+ isCircle = type === 'circle';
+ if (untransformed && isCircle) {
ctx.arc(0, 0, radius, 0, Math.PI * 2, true);
} else {
- var rx = radius.width,
- ry = radius.height,
- kappa = /*#=*/ Numerical.KAPPA;
- if (type === 'ellipse') {
- // Approximate ellipse with four bezier curves and KAPPA.
- var cx = rx * kappa,
- cy = ry * kappa;
- ctx.moveTo(-rx, 0);
- ctx.bezierCurveTo(-rx, -cy, -cx, -ry, 0, -ry);
- ctx.bezierCurveTo(cx, -ry, rx, -cy, rx, 0);
- ctx.bezierCurveTo(rx, cy, cx, ry, 0, ry);
- ctx.bezierCurveTo(-cx, ry, -rx, cy, -rx, 0);
- } else { // rect
- var size = this._size,
- width = size.width,
- height = size.height;
- if (rx === 0 && ry === 0) {
- // straight rect
- ctx.rect(-width / 2, -height / 2, width, height);
- } else {
- // rounded rect. Use 1 - KAPPA to calculate position of
- // control points from the corners inwards.
- kappa = 1 - kappa;
- var x = width / 2,
- y = height / 2,
- cx = rx * kappa,
- cy = ry * kappa;
- ctx.moveTo(-x, -y + ry);
- ctx.bezierCurveTo(-x, -y + cy, -x + cx, -y, -x + rx, -y);
- ctx.lineTo(x - rx, -y);
- ctx.bezierCurveTo(x - cx, -y, x, -y + cy, x, -y + ry);
- ctx.lineTo(x, y - ry);
- ctx.bezierCurveTo(x, y - cy, x - cx, y, x - rx, y);
- ctx.lineTo(-x + rx, y);
- ctx.bezierCurveTo(-x + cx, y, -x, y - cy, -x, y - ry);
- }
+ var rx = isCircle ? radius : radius.width,
+ ry = isCircle ? radius : radius.height,
+ size = this._size,
+ width = size.width,
+ height = size.height;
+ if (untransformed && type === 'rect' && rx === 0 && ry === 0) {
+ // Rectangles with no rounding
+ ctx.rect(-width / 2, -height / 2, width, height);
+ } else {
+ // Round rectangles, ellipses, transformed circles
+ var x = width / 2,
+ y = height / 2,
+ // Use 1 - KAPPA to calculate position of control points
+ // from the corners inwards.
+ kappa = 1 - /*#=*/ Numerical.KAPPA,
+ cx = rx * kappa,
+ cy = ry * kappa,
+ // Build the coordinates list, so it can optionally be
+ // transformed by a matrix.
+ c = [
+ -x, -y + ry,
+ -x, -y + cy,
+ -x + cx, -y,
+ -x + rx, -y,
+ x - rx, -y,
+ x - cx, -y,
+ x, -y + cy,
+ x, -y + ry,
+ x, y - ry,
+ x, y - cy,
+ x - cx, y,
+ x - rx, y,
+ -x + rx, y,
+ -x + cx, y,
+ -x, y - cy,
+ -x, y - ry
+ ];
+ if (strokeMatrix)
+ strokeMatrix.transform(c, c, 32);
+ if (!param.dontStart)
+ ctx.beginPath();
+ ctx.moveTo(c[0], c[1]);
+ ctx.bezierCurveTo(c[2], c[3], c[4], c[5], c[6], c[7]);
+ if (x !== rx)
+ ctx.lineTo(c[8], c[9]);
+ ctx.bezierCurveTo(c[10], c[11], c[12], c[13], c[14], c[15]);
+ if (y !== ry)
+ ctx.lineTo(c[16], c[17]);
+ ctx.bezierCurveTo(c[18], c[19], c[20], c[21], c[22], c[23]);
+ if (x !== rx)
+ ctx.lineTo(c[24], c[25]);
+ ctx.bezierCurveTo(c[26], c[27], c[28], c[29], c[30], c[31]);
}
}
ctx.closePath();
diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js
index f5c8dc96..8f2289cf 100644
--- a/src/path/CompoundPath.js
+++ b/src/path/CompoundPath.js
@@ -247,7 +247,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
: new Base(options, { fill: false });
},
- _draw: function(ctx, param) {
+ _draw: function(ctx, param, strokeMatrix) {
var children = this._children;
// Return early if the compound path doesn't have any children:
if (children.length === 0)
@@ -259,7 +259,7 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
param = param.extend({ dontStart: true, dontFinish: true });
ctx.beginPath();
for (var i = 0, l = children.length; i < l; i++)
- children[i].draw(ctx, param);
+ children[i].draw(ctx, param, strokeMatrix);
this._currentPath = ctx.currentPath;
}
diff --git a/src/path/Path.js b/src/path/Path.js
index de9390be..ed1a072f 100644
--- a/src/path/Path.js
+++ b/src/path/Path.js
@@ -1634,7 +1634,7 @@ var Path = PathItem.extend(/** @lends Path# */{
/**
* Returns the curve location of the specified offset on the path.
- *
+ *
* @param {Number} offset the offset on the path, where {@code 0} is at
* the beginning of the path and {@link Path#length} at the end.
* @param {Boolean} [isParameter=false]
@@ -1997,11 +1997,11 @@ var Path = PathItem.extend(/** @lends Path# */{
inX, inY,
outX, outY;
- function drawSegment(i) {
- var segment = segments[i];
- // Optimise code when no matrix is provided by accessing semgent
+ function drawSegment(segment) {
+ // Optimise code when no matrix is provided by accessing segment
// points hand handles directly, since this is the default when
- // drawing paths. Matrix is only used for drawing selections.
+ // drawing paths. Matrix is only used for drawing selections and
+ // when #strokeScaling is false.
if (matrix) {
segment._transformCoordinates(matrix, coords, false);
curX = coords[0];
@@ -2023,7 +2023,8 @@ var Path = PathItem.extend(/** @lends Path# */{
inX = curX + handle._x;
inY = curY + handle._y;
}
- if (inX == curX && inY == curY && outX == prevX && outY == prevY) {
+ if (inX === curX && inY === curY
+ && outX === prevX && outY === prevY) {
ctx.lineTo(curX, curY);
} else {
ctx.bezierCurveTo(outX, outY, inX, inY, curX, curY);
@@ -2042,20 +2043,17 @@ var Path = PathItem.extend(/** @lends Path# */{
}
for (var i = 0; i < length; i++)
- drawSegment(i);
+ drawSegment(segments[i]);
// Close path by drawing first segment again
if (path._closed && length > 0)
- drawSegment(0);
+ drawSegment(segments[0]);
}
return {
- _draw: function(ctx, param) {
+ _draw: function(ctx, param, strokeMatrix) {
var dontStart = param.dontStart,
- dontPaint = param.dontFinish || param.clip;
- if (!dontStart)
- ctx.beginPath();
-
- var style = this.getStyle(),
+ dontPaint = param.dontFinish || param.clip,
+ style = this.getStyle(),
hasFill = style.hasFill(),
hasStroke = style.hasStroke(),
dashArray = style.getDashArray(),
@@ -2063,18 +2061,15 @@ var Path = PathItem.extend(/** @lends Path# */{
dashLength = !paper.support.nativeDash && hasStroke
&& dashArray && dashArray.length;
- function getOffset(i) {
- // Negative modulo is necessary since we're stepping back
- // in the dash sequence first.
- return dashArray[((i % dashLength) + dashLength) % dashLength];
- }
+ if (!dontStart)
+ ctx.beginPath();
if (!dontStart && this._currentPath) {
ctx.currentPath = this._currentPath;
} else if (hasFill || hasStroke && !dashLength || dontPaint) {
// Prepare the canvas path if we have any situation that
// requires it to be defined.
- drawSegments(ctx, this);
+ drawSegments(ctx, this, strokeMatrix);
if (this._closed)
ctx.closePath();
// CompoundPath collects its own _currentPath
@@ -2082,6 +2077,12 @@ var Path = PathItem.extend(/** @lends Path# */{
this._currentPath = ctx.currentPath;
}
+ function getOffset(i) {
+ // Negative modulo is necessary since we're stepping back
+ // in the dash sequence first.
+ return dashArray[((i % dashLength) + dashLength) % dashLength];
+ }
+
if (!dontPaint && (hasFill || hasStroke)) {
// If the path is part of a compound path or doesn't have a fill
// or stroke, there is no need to continue.
@@ -2102,7 +2103,7 @@ var Path = PathItem.extend(/** @lends Path# */{
// native dashes.
if (!dontStart)
ctx.beginPath();
- var flattener = new PathFlattener(this),
+ var flattener = new PathFlattener(this, strokeMatrix),
length = flattener.length,
from = -style.getDashOffset(), to,
i = 0;
diff --git a/src/path/PathFlattener.js b/src/path/PathFlattener.js
index 81b5085e..9f36a8b1 100644
--- a/src/path/PathFlattener.js
+++ b/src/path/PathFlattener.js
@@ -16,7 +16,7 @@
* @private
*/
var PathFlattener = Base.extend({
- initialize: function(path) {
+ initialize: function(path, matrix) {
this.curves = []; // The curve values as returned by getValues()
this.parts = []; // The calculated, subdivided parts of the path
this.length = 0; // The total length of the path
@@ -35,7 +35,7 @@ var PathFlattener = Base.extend({
that = this;
function addCurve(segment1, segment2) {
- var curve = Curve.getValues(segment1, segment2);
+ var curve = Curve.getValues(segment1, segment2, matrix);
that.curves.push(curve);
that._computeParts(curve, segment1._index, 0, 1);
}
diff --git a/src/style/Style.js b/src/style/Style.js
index 1af1a90d..f4d1f65c 100644
--- a/src/style/Style.js
+++ b/src/style/Style.js
@@ -77,6 +77,7 @@ var Style = Base.extend(new function() {
strokeWidth: 1,
strokeCap: 'butt',
strokeJoin: 'miter',
+ strokeScaling: true,
miterLimit: 10,
dashOffset: 0,
dashArray: [],
@@ -101,6 +102,8 @@ var Style = Base.extend(new function() {
strokeWidth: /*#=*/ Change.STROKE,
strokeCap: /*#=*/ Change.STROKE,
strokeJoin: /*#=*/ Change.STROKE,
+ // strokeScaling can change the coordinates of cached path items
+ strokeScaling: /*#=*/ Change.STROKE | Change.GEOMETRY,
miterLimit: /*#=*/ Change.STROKE,
fontFamily: /*#=*/ Change.GEOMETRY,
fontWeight: /*#=*/ Change.GEOMETRY,
@@ -369,8 +372,8 @@ var Style = Base.extend(new function() {
*/
/**
- * The shape to be used at the end of open {@link Path} items, when they
- * have a stroke.
+ * The shape to be used at the beginning and end of open {@link Path} items,
+ * when they have a stroke.
*
* @name Style#strokeCap
* @property
@@ -402,7 +405,8 @@ var Style = Base.extend(new function() {
*/
/**
- * The shape to be used at the corners of paths when they have a stroke.
+ * The shape to be used at the segments and corners of {@link Path} items
+ * when they have a stroke.
*
* @name Style#strokeJoin
* @property
@@ -430,6 +434,17 @@ var Style = Base.extend(new function() {
* path3.strokeJoin = 'bevel';
*/
+ /**
+ * Specifies whether the stroke is to be drawn taking the current affine
+ * transformation into account (the default behavior), or whether it should
+ * appear as a non-scaling stroke.
+ *
+ * @name Style#strokeScaling
+ * @property
+ * @default true
+ * @type Boolean
+ */
+
/**
* The dash offset of the stroke.
*
diff --git a/src/svg/SVGExport.js b/src/svg/SVGExport.js
index 0dc32fa1..0fb3bb6f 100644
--- a/src/svg/SVGExport.js
+++ b/src/svg/SVGExport.js
@@ -293,8 +293,10 @@ new function() {
var get = entry.get,
type = entry.type,
value = item[get]();
- if (!parent || !Base.equals(parent[get](), value)) {
- if (type === 'color' && value != null) {
+ if (entry.exportFilter
+ ? entry.exportFilter(item, value)
+ : !parent || !Base.equals(parent[get](), value)) {
+ if (type === 'color' && value !== 'none') {
// 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:
diff --git a/src/svg/SVGStyles.js b/src/svg/SVGStyles.js
index 521650fb..7e496177 100644
--- a/src/svg/SVGStyles.js
+++ b/src/svg/SVGStyles.js
@@ -18,6 +18,16 @@ var SVGStyles = Base.each({
strokeWidth: ['stroke-width', 'number'],
strokeCap: ['stroke-linecap', 'string'],
strokeJoin: ['stroke-linejoin', 'string'],
+ strokeScaling: ['vector-effect', 'lookup', {
+ true: 'none',
+ false: 'non-scaling-stroke'
+ }, function(item, value) {
+ // no inheritance, only applies to graphical elements
+ return !value // false, meaning non-scaling-stroke
+ && (item instanceof PathItem
+ || item instanceof Shape
+ || item instanceof TextItem);
+ }],
miterLimit: ['stroke-miterlimit', 'number'],
dashArray: ['stroke-dasharray', 'array'],
dashOffset: ['stroke-dashoffset', 'number'],
@@ -44,6 +54,7 @@ var SVGStyles = Base.each({
fromSVG: lookup && Base.each(lookup, function(value, name) {
this[value] = name;
}, {}),
+ exportFilter: entry[3],
get: 'get' + part,
set: 'set' + part
};