Implement non-scaling strokes through Style#strokeScaling.

Closes #418.
This commit is contained in:
Jürg Lehni 2014-05-13 13:38:51 +02:00
parent 68db4f9b59
commit 846c806034
12 changed files with 277 additions and 126 deletions

View file

@ -6,24 +6,39 @@
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper-full.js"></script>
<script type="text/paperscript" canvas="canvas1">
var ellipse = new Shape.Ellipse({
from: [10, 10],
to: [200, 100],
var circle = new Shape.Circle({
center: [100, 100],
radius: 50,
fillColor: 'red'
});
var circle = new Shape.Circle({
center: [50, 150],
radius: 25,
fillColor: 'blue'
var ellipse = new Shape.Ellipse({
center: [100, 200],
radius: [50, 25],
fillColor: 'blue',
strokeColor: 'black',
strokeWidth: 4,
rotation: 20
});
var rectangle = new Shape.Rectangle({
from: [25, 200],
to: [100, 225],
fillColor: 'green'
var rect = new Shape.Rectangle({
center: [100, 300],
size: [100, 50],
fillColor: 'green',
strokeColor: 'black',
strokeWidth: 4,
rotation: -20
});
var roundRect = new Shape.Rectangle({
center: [100, 400],
size: [50, 100],
radius: [15, 20],
fillColor: 'orange',
strokeColor: 'black',
strokeWidth: 4,
rotation: 20
});
rectangle.rotate(30);
window._json = project.exportJSON();
console.log(window._json);

View file

@ -6,55 +6,46 @@
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper-full.js"></script>
<script type="text/paperscript" canvas="canvas">
var path = new Path.Circle({
var circle = new Shape.Circle({
center: [100, 100],
radius: 50,
fillColor: 'red'
});
var shape = path.toShape();
shape.position += [200, 0];
var path = shape.toPath();
path.position += [200, 0];
var path = new Path.Ellipse({
var ellipse = new Shape.Ellipse({
center: [100, 200],
radius: [50, 25],
fillColor: 'blue',
strokeColor: 'black',
strokeWidth: 10
strokeWidth: 10,
rotation: 20
});
path.rotate(20);
var shape = path.toShape();
shape.position += [200, 0];
var path = shape.toPath();
path.position += [200, 0];
var path = new Path.Rectangle({
var rect = new Shape.Rectangle({
center: [100, 300],
size: [100, 50],
fillColor: 'green',
strokeColor: 'black',
strokeWidth: 10
strokeWidth: 10,
rotation: -20
});
path.rotate(-20);
var shape = path.toShape();
shape.position += [200, 0];
var path = shape.toPath();
path.position += [200, 0];
var path = new Path.Rectangle({
var roundRect = new Shape.Rectangle({
center: [100, 400],
size: [50, 100],
radius: [5, 10],
radius: [15, 20],
fillColor: 'orange',
strokeColor: 'black',
strokeWidth: 10
strokeWidth: 10,
rotation: 20
});
path.rotate(20);
var shape = path.toShape();
shape.position += [200, 0];
var path = shape.toPath();
path.position += [200, 0];
[circle, ellipse, rect, roundRect].forEach(function(shape) {
var path = shape.toPath();
path.position += [200, 0];
var shape2 = path.toShape();
shape2.position += [200, 0];
})
</script>
</head>
<body>

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Shapes</title>
<link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../../dist/paper-full.js"></script>
<script type="text/paperscript" canvas="canvas">
// view._context = new ProxyContext(view._context);
// view.zoom = 1.25;
settings.applyMatrix = false;
var path = new Path.Circle({
center: view.center - [0, 140],
radius: 50,
fillColor: 'red',
strokeColor: 'black',
strokeWidth: 10,
strokeScaling: false,
opacity: 0.5,
selected: true
});
path.scale(2, 1);
var shape = new Shape.Circle({
center: view.center,
radius: 50,
fillColor: 'red',
strokeColor: 'black',
strokeWidth: 10,
strokeScaling: false,
opacity: 0.5,
selected: true
});
shape.scale(2, 1);
var hole;
var compound = new CompoundPath({
children: [
new Path.Rectangle({
point: [0, 0],
size: [100, 100]
}),
hole = new Path.Circle({
center: [50, 50],
radius: 25
})
],
fillColor: 'red',
strokeColor: 'black',
strokeWidth: 10,
position: view.center + [0, 140],
strokeScaling: false,
opacity: 0.5,
selected: true
});
hole.position += 15;
compound.scale(2, 1);
document.getElementById('svg').appendChild(project.exportSVG());
</script>
</head>
<body>
<canvas id="canvas" width="300" height="500"></canvas>
<svg id="svg" width="300" height="500"></svg>
</body>
</html>

View file

@ -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) {

View file

@ -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.

View file

@ -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();

View file

@ -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;
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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.
*

View file

@ -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:

View file

@ -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
};