diff --git a/src/basic/Point.js b/src/basic/Point.js index 5cf1a07c..b5f5d7cf 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -1,3 +1,8 @@ +/** + * The Point object represents a point in the two dimensional space of the + * Paper.js document. It is also used to represent two dimensional vector + * objects. + */ var Point = Base.extend({ beans: true, @@ -28,6 +33,21 @@ var Point = Base.extend({ } }, + /** + * Returns a copy of the point. + * This is useful as the following code only generates a flat copy: + * + * + * var point1 = new Point(); + * var point2 = point1; + * point2.x = 1; // also changes point1.x + * + * var point2 = point1.clone(); + * point2.x = 1; // doesn't change point1.x + * + * + * @return the cloned point + */ clone: function() { return Point.create(this.x, this.y); }, @@ -66,6 +86,26 @@ var Point = Base.extend({ return this.x == point.x && this.y == point.y; }, + transform: function(matrix) { + return matrix.transform(this); + }, + + /** + * Returns the distance between the point and another point. + * + * Sample code: + * + * var firstPoint = new Point(5, 10); + * var secondPoint = new Point(5, 20); + * + * var distance = firstPoint.getDistance(secondPoint); + * + * print(distance); // 10 + * + * + * @param px + * @param py + */ getDistance: function() { var point = Point.read(arguments); var px = point.x - this.x; @@ -80,6 +120,12 @@ var Point = Base.extend({ return px * px + py * py; }, + /** + * The length of the vector that is represented by this point's coordinates. + * Each point can be interpreted as a vector that points from the origin + * ({@code x = 0},{@code y = 0}) to the point's location. + * Setting the length changes the location but keeps the vector's angle. + */ getLength: function() { var point = Point.read(arguments); return Math.sqrt(this.x * this.x + this.y * this.y); @@ -131,7 +177,6 @@ var Point = Base.extend({ return Math.atan2(this.y, this.x) * 180 / Math.PI; }, - getQuadrant: function() { if (this.x >= 0) { if (this.y >= 0) { @@ -148,15 +193,20 @@ var Point = Base.extend({ } }, - setAngle: function(angle) { - angle = this._angle = angle * Math.PI / 180; - if (!this.isZero()) { - var length = this.length; - this.x = Math.cos(angle) * length; - this.y = Math.sin(angle) * length; - } - }, - + /** + * {@grouptitle Angle & Rotation} + * + * The vector's angle, measured from the x-axis to the vector. + * + * When supplied with a point, returns the smaller angle between two + * vectors. The angle is unsigned, no information about rotational + * direction is given. + * + * Read more about angle units and orientation in the description of the + * {@link #getAngle()} property. + * + * @param point + */ getAngle: function() { var angle; if (arguments.length) { @@ -175,6 +225,24 @@ var Point = Base.extend({ return angle * 180 / Math.PI; }, + setAngle: function(angle) { + angle = this._angle = angle * Math.PI / 180; + if (!this.isZero()) { + var length = this.length; + this.x = Math.cos(angle) * length; + this.y = Math.sin(angle) * length; + } + }, + + /** + * Returns the angle between two vectors. The angle is directional and + * signed, giving information about the rotational direction. + * + * Read more about angle units and orientation in the description of the + * {@link #getAngle()} property. + * + * @param point + */ getDirectedAngle: function() { var point = Point.read(arguments); var angle = this.angle - point.angle; @@ -188,7 +256,17 @@ var Point = Base.extend({ } }, - // TODO: Add center parameter support back to Scriptographer + /** + * Rotates the point by the given angle around an optional center point. + * The object itself is not modified. + * + * Read more about angle units and orientation in the description of the + * {@link #getAngle()} property. + * + * @param angle the rotation angle + * @param center the center point of the rotation + * @return the rotated point + */ rotate: function(angle, center) { var point = center ? this.subtract(center) : this; angle = angle * Math.PI / 180; @@ -201,6 +279,16 @@ var Point = Base.extend({ return center ? point.add(center) : point; }, + /** + * Returns the interpolation point between the point and another point. + * The object itself is not modified! + * + * @param point + * @param t the position between the two points as a value between 0 and 1 + * @return the interpolation point + * + * @jshide + */ interpolate: function(point, t) { return Point.create( this.x * (1 - t) + point.x * t, @@ -208,52 +296,152 @@ var Point = Base.extend({ ); }, + /** + * {@grouptitle Tests} + * + * Checks whether the point is inside the boundaries of the rectangle. + * + * @param rect the rectangle to check against + * @return {@true if the point is inside the rectangle} + */ isInside: function(rect) { return rect.contains(this); }, + /** + * Checks if the point is within a given distance of another point. + * + * @param point the point to check against + * @param tolerance the maximum distance allowed + * @return {@true if it is within the given distance} + */ isClose: function(point, tolerance) { return this.getDistance(point) < tolerance; }, + /** + * Checks if the vector represented by this point is parallel (collinear) to + * another vector. + * + * @param point the vector to check against + * @return {@true if it is parallel} + */ isParallel: function(point) { return Math.abs(this.x / point.x - this.y / point.y) < 0.00001; }, + /** + * Checks if this point has both the x and y coordinate set to 0. + * + * @return {@true if both x and y are 0} + */ isZero: function() { return this.x == 0 && this.y == 0; }, + /** + * Checks if this point has an undefined value for at least one of its + * coordinates. + * + * @return {@true if either x or y are not a number} + */ isNaN: function() { return isNaN(this.x) || isNaN(this.y); }, + /** + * {@grouptitle Math Functions} + * + * Returns a new point with rounded {@link #x} and {@link #y} values. The + * object itself is not modified! + * + * Sample code: + * + * var point = new Point(10.2, 10.9); + * var roundPoint = point.round(); + * print(roundPoint); // { x: 10.0, y: 11.0 } + * + */ round: function() { return Point.create(Math.round(this.x), Math.round(this.y)); }, + /** + * Returns a new point with the nearest greater non-fractional values to the + * specified {@link #x} and {@link #y} values. The object itself is not + * modified! + * + * Sample code: + * + * var point = new Point(10.2, 10.9); + * var ceilPoint = point.ceil(); + * print(ceilPoint); // { x: 11.0, y: 11.0 } + * + */ ceil: function() { return Point.create(Math.ceil(this.x), Math.ceil(this.y)); }, + /** + * Returns a new point with the nearest smaller non-fractional values to the + * specified {@link #x} and {@link #y} values. The object itself is not + * modified! + * + * Sample code: + * + * var point = new Point(10.2, 10.9); + * var floorPoint = point.floor(); + * print(floorPoint); // { x: 10.0, y: 10.0 } + * + */ floor: function() { return Point.create(Math.floor(this.x), Math.floor(this.y)); }, + /** + * Returns a new point with the absolute values of the specified {@link #x} + * and {@link #y} values. The object itself is not modified! + * + * Sample code: + * + * var point = new Point(-5, 10); + * var absPoint = point.abs(); + * print(absPoint); // { x: 5.0, y: 10.0 } + * + */ abs: function() { return Point.create(Math.abs(this.x), Math.abs(this.y)); }, + /** + * {@grouptitle Vectorial Math Functions} + * + * Returns the dot product of the point and another point. + * @param point + * @return the dot product of the two points + */ dot: function() { var point = Point.read(arguments); return this.x * point.x + this.y * point.y; }, + /** + * Returns the cross product of the point and another point. + * @param point + * @return the cross product of the two points + */ cross: function() { var point = Point.read(arguments); return this.x * point.y - this.y - point.x; }, + /** + * Returns the projection of the point on another point. + * Both points are interpreted as vectors. + * + * @param point + * @return the project of the point on another point + */ project: function() { var point = Point.read(arguments); if (point.isZero()) { @@ -297,18 +485,63 @@ var Point = Base.extend({ return null; }, + /** + * Returns a new point object with the smallest {@link #x} and + * {@link #y} of the supplied points. + * + * Sample code: + * + * var point1 = new Point(10, 100); + * var point2 = new Point(200, 5); + * var minPoint = Point.min(point1, point2); + * print(minPoint); // { x: 10.0, y: 5.0 } + * + * + * @param point1 + * @param point2 + * @return The newly created point object + */ min: function(point1, point2) { return Point.create( Math.min(point1.x, point2.x), Math.min(point1.y, point2.y)); }, + /** + * Returns a new point object with the largest {@link #x} and + * {@link #y} of the supplied points. + * + * Sample code: + * + * var point1 = new Point(10, 100); + * var point2 = new Point(200, 5); + * var maxPoint = Point.max(point1, point2); + * print(maxPoint); // { x: 200.0, y: 100.0 } + * + * + * @param point1 + * @param point2 + * @return The newly created point object + */ max: function(point1, point2) { return Point.create( Math.max(point1.x, point2.x), Math.max(point1.y, point2.y)); }, + /** + * Returns a point object with random {@link #x} and {@link #y} values + * between {@code 0} and {@code 1}. + * + * Sample code: + * + * var maxPoint = new Point(100, 100); + * var randomPoint = Point.random(); + * + * // A point between {x:0, y:0} and {x:100, y:100}: + * var point = maxPoint * randomPoint; + * + */ random: function() { return Point.create(Math.random(), Math.random()); } diff --git a/src/color/RGBColor.js b/src/color/RGBColor.js index ff6ace2b..f995f95b 100644 --- a/src/color/RGBColor.js +++ b/src/color/RGBColor.js @@ -157,7 +157,7 @@ RGBColor = Color.extend(new function() { setGray: function(gray) { this._cssString = null; - this._red = this._green = this._blue = gray; + this._red = this._green = this._blue = 1 - gray; }, /** diff --git a/src/document/Doc.js b/src/document/Doc.js index 8b5136da..c62667a0 100644 --- a/src/document/Doc.js +++ b/src/document/Doc.js @@ -29,11 +29,12 @@ Doc = Base.extend({ redraw: function() { if (this.canvas) { - // TODO: clearing the canvas by setting - // this.canvas.width = this.canvas.width might be faster.. - this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height); + // Initial tests conclude that clearing the canvas is always + // faster than using clearRect: + // http://jsperf.com/clearrect-vs-setting-width/7 + this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1); for (var i = 0, l = this.layers.length; i < l; i++) { - this.layers[i].draw(this.ctx); + this.layers[i].draw(this.ctx, {}); } } } diff --git a/src/item/BlendMode.js b/src/item/BlendMode.js new file mode 100644 index 00000000..37f9fa3e --- /dev/null +++ b/src/item/BlendMode.js @@ -0,0 +1,215 @@ +/* + * BlendMode code ported from Context Blender JavaScript Library + * + * Copyright © 2010 Gavin Kistner + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +BlendMode = { + // TODO: Should we remove the blend modes that are not in Scriptographer? + // TODO: Add missing blendmodes like hue / saturation / color / luminosity + // TODO: Clean up codespacing of original code, or keep it as is, so + // we can easily encorporate changes? + process: function(documentContext, item, param) { + // TODO: use strokeBounds + var itemBounds = item.bounds; + var top = Math.floor(itemBounds.top); + var left = Math.floor(itemBounds.left); + var size = itemBounds.size.ceil(); + var width = size.width; + var height = size.height; + + var itemCanvas = CanvasProvider.getCanvas(size); + var itemContext = itemCanvas.getContext('2d'); + if(item.matrix) { + var matrix = item.matrix.clone(); + var transMatrix = Matrix.getTranslateInstance(-left, -top); + matrix.preConcatenate(transMatrix); + // TODO: Profiling shows this as a hotspot + matrix.applyToContext(itemContext); + } else { + itemContext.translate(-itemBounds.left, -itemBounds.top); + } + param.ignoreBlendMode = true; + item.draw(itemContext, param); + + var dstD = documentContext.getImageData( + left, top, + width, height + ); + + var srcD = itemContext.getImageData( + 0, 0, + width, height + ); + + var src = srcD.data; + var dst = dstD.data; + var sA, dA, len=dst.length; + var sRA, sGA, sBA, dRA, dGA, dBA, dA2; + var demultiply; + + for (var px=0;pxdRA ? dRA : sRA) * demultiply; + dst[px+1] = (sGA>dGA ? dGA : sGA) * demultiply; + dst[px+2] = (sBA>dBA ? dBA : sBA) * demultiply; + break; + + case 'lighten': + case 'lighter': + dst[px ] = (sRA + * var circle = new Path.Circle(new Point(50, 50), 10); + * print(circle.blendMode); // normal + * + * // Change the blend mode of the path item: + * circle.blendMode = 'multiply'; + * + */ + blendMode: 'normal', + /** * Specifies whether the item is hidden. * @@ -137,6 +154,18 @@ Item = Base.extend({ } }, + // TODO: getIsolated / setIsolated (print specific feature) + // TODO: get/setKnockout (print specific feature) + // TODO get/setAlphaIsShape + // TODO: get/setData + + /** + * Reverses the order of this item's children + */ + reverseChildren: function() { + this.children.reverse(); + }, + /** * The first item contained within this item. */ @@ -228,6 +257,21 @@ Item = Base.extend({ return true; }, + /** + * Checks whether the item is valid, i.e. it hasn't been removed. + * + * Sample code: + * + * var path = new Path(); + * print(path.isValid()); // true + * path.remove(); + * print(path.isValid()); // false + * + * + * @return {@true if the item is valid} + */ + // TODO: isValid / checkValid + /** * {@grouptitle Hierarchy Operations} * @@ -331,6 +375,40 @@ Item = Base.extend({ return true; }, + /** + * {@grouptitle Hierarchy Tests} + * + * Checks if this item is above the specified item in the stacking order of + * the document. + * + * Sample code: + * + * var firstPath = new Path(); + * var secondPath = new Path(); + * print(secondPath.isAbove(firstPath)); // true + * + * + * @param item The item to check against + * @return {@true if it is above the specified item} + */ + // TODO: isAbove + + /** + * Checks if the item is below the specified item in the stacking order of + * the document. + * + * Sample code: + * + * var firstPath = new Path(); + * var secondPath = new Path(); + * print(firstPath.isBelow(secondPath)); // true + * + * + * @param item The item to check against + * @return {@true if it is below the specified item} + */ + // TODO: isBelow + // TODO: this is confusing the beans // isParent: function(item) { // return this.parent == item; @@ -355,7 +433,7 @@ Item = Base.extend({ * @return {@true if it is inside the specified item} */ isDescendant: function(item) { - var parent = this; + var parent = this.parent; while(parent) { if (parent == item) return true; @@ -380,7 +458,7 @@ Item = Base.extend({ * @return {@true if the item is an ancestor of the specified item} */ isAncestor: function(item) { - var parent = item; + var parent = item.parent; while(parent) { if (parent == this) return true; @@ -388,7 +466,28 @@ Item = Base.extend({ } return false; }, - + + /** + * Checks whether the item is grouped with the specified item. + * + * @param item + * @return {@true if the items are grouped together} + */ + isGroupedWith: function(item) { + var parent = this.parent; + while(parent) { + // Find group parents. Check for parent.parent, since don't want + // top level layers, because they also inherit from Group + if(parent.parent + && (parent instanceof Group || parent instanceof CompoundPath) + && item.isDescendant(parent)) + return true; + // Keep walking up otherwise + parent = parent.parent + } + return false; + }, + getBounds: function() { // TODO: Implement for items other than paths return new Rectangle(); @@ -416,7 +515,45 @@ Item = Base.extend({ // Now execute the transformation: this.transform(matrix); }, + + /** + * The bounding rectangle of the item including stroke width. + */ + // TODO: getStrokeBounds + /** + * The bounding rectangle of the item including stroke width and controls. + */ + // TODO: getControlBounds + + /** + * Rasterizes the item into a newly created Raster object. The item itself + * is not removed after rasterization. + * + * @param resolution the resolution of the raster in dpi {@default 72} + * @return the newly created Raster item + */ + rasterize: function(resolution) { + // TODO: why would we want to pass a size to rasterize? Seems to produce + // weird results on Scriptographer. Also we can't use antialiasing, since + // Canvas doesn't support it yet. Document colorMode is also out of the + // question for now. + if(!resolution) + resolution = 72; + // TODO: use strokebounds for this: + var bounds = this.bounds; + var scale = resolution / 72; + var canvas = CanvasProvider.getCanvas(bounds.size.multiply(scale)); + var context = canvas.getContext('2d'); + var matrix = new Matrix().scale(scale).translate(-bounds.x, -bounds.y); + matrix.applyToContext(context); + this.draw(context); + var raster = new Raster(canvas); + raster.position = this.bounds.center; + raster.scale(1 / scale); + return raster; + }, + /** * The item's position within the art board. This is the * {@link Rectangle#getCenter()} of the {@link Item#getBounds()} rectangle. @@ -569,4 +706,6 @@ Item = Base.extend({ setStyle: function(style) { this._style = new PathStyle(this, style); } + + // TODO: toString }); \ No newline at end of file diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 2c38bcb7..788d0e91 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -19,31 +19,58 @@ PlacedSymbol = Item.extend({ } else { this.matrix = new Matrix(); } + // TODO: this should use strokeBounds: this._bounds = this.symbol.definition.bounds.clone(); + // TODO: should size be cached here, or on Symbol? + this._size = this._bounds.size; }, transformContent: function(matrix, flags) { - var bounds = this.bounds; - var coords = [bounds.x, bounds.y, - bounds.x + bounds.width, bounds.y + bounds.height]; - matrix.transform(coords, 0, coords, 0, 2); + var width = this._size.width; + var height = this._size.height; + var x = width * -0.5; + var y = height * -0.5; + var coords = [ + x, y, + x + width, y, + x + width, y + height, + x, y + height]; this.matrix.preConcatenate(matrix); - bounds.x = coords[0]; - bounds.y = coords[1]; - bounds.width = coords[2] - coords[0]; - bounds.height = coords[3] - coords[1]; + this.matrix.transform(coords, 0, coords, 0, 4); + + var xMin = coords[0], xMax = coords[0]; + var yMin = coords[1], yMax = coords[1]; + for(var i = 2; i < 8; i += 2) { + var x = coords[i]; + var y = coords[i + 1]; + xMin = Math.min(x, xMin); + xMax = Math.max(x, xMax); + yMin = Math.min(y, yMin); + yMax = Math.max(y, yMax); + }; + var bounds = this._bounds; + bounds.x = xMin; + bounds.y = yMin; + bounds.width = xMax - xMin; + bounds.height = yMax - yMin; }, getBounds: function() { return this._bounds; }, - draw: function(ctx) { - // TODO: we need to preserve strokewidth, but still transform the fill - ctx.save(); - this.matrix.applyToContext(ctx); - this.symbol.definition.draw(ctx); - ctx.restore(); + draw: function(ctx, param) { + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + // TODO: we need to preserve strokewidth, but still transform the fill + ctx.save(); + if(param.ignoreBlendMode !== true) + this.matrix.applyToContext(ctx); + param.ignoreBlendMode = false; + this.symbol.definition.draw(ctx, param); + ctx.restore(); + } } // TODO: // embed() diff --git a/src/item/Raster.js b/src/item/Raster.js index 1c484aff..78c84bd1 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -3,80 +3,122 @@ Raster = Item.extend({ // TODO: implement url / type, width, height // TODO: have PlacedSymbol & Raster inherit from a shared class? - initialize: function(image) { + initialize: function(object) { + var width, height; this.base(); - if (image) { - this.image = image; - var width = image.width; - var height = image.height; - this.size = new Size(width, height); - this._bounds = new Rectangle(-width / 2, -height / 2, width, height); - this.matrix = new Matrix(); + if (object.getContext) { + this.canvas = object; + width = this.canvas.width; + height = this.canvas.height; + } else { + this._image = object; + // TODO: cross browser compatible? + width = object.naturalWidth; + height = object.naturalHeight; } + this._size = new Size(width, height); + this._bounds = new Rectangle(-width / 2, -height / 2, width, height); + this.matrix = new Matrix(); + }, + + /** + * The size of the raster in pixels. + */ + getSize: function() { + return this._size; }, - // TODO: getSize / setSize + setSize: function() { + var size = Size.read(arguments); + var canvas = CanvasProvider.getCanvas(size); + var context = canvas.getContext('2d'); + context.drawImage(this._canvas ? this._canvas : this._image, + 0, 0, size.width, size.height); + // If we already had a canvas, return it to be reused. + if (this._canvas) + CanvasProvider.returnCanvas(this._canvas); + this._size = size; + this._context = null; + this._canvas = canvas; + }, /** * The width of the raster in pixels. */ getWidth: function() { - return this.size.width; + return this._size.width; }, /** * The height of the raster in pixels. */ getHeight: function() { - return this.size.height; + return this._size.height; }, - // TODO: getPpi - // TODO: getSubImage - // TODO: getImage - // TODO: drawImage - - // TODO: support getAverageColor paramaters: point, rect, path - // TODO: Idea for getAverageColor(path): set globalCompositeOperation = 'xor', - // then fillRect with black, then draw the path, then draw the image, then - // resize and count values. - getAverageColor: function() { - var size = 32; - var tempCanvas = CanvasProvider.getCanvas(size, size); - var ctx = tempCanvas.getContext('2d'); - ctx.drawImage(this.image, 0, 0, size, size); - var pixels = ctx.getImageData(0.5, 0.5, size, size).data; - var channels = [0, 0, 0]; - - for (var i = 0; i < size; i++) { - var offset = i * size; - var alpha = pixels[offset + 3] / 255; - channels[0] += pixels[offset] * alpha; - channels[1] += pixels[offset + 1] * alpha; - channels[2] += pixels[offset + 2] * alpha; - } - - for (var i = 0; i < 3; i++) - channels[i] /= size * 255; - - CanvasProvider.returnCanvas(tempCanvas); - return Color.read(channels); + /** + * Pixels per inch of the raster at it's current size. + */ + getPpi: function() { + var matrix = this.matrix; + var orig = new Point(0, 0).transform(matrix); + var u = new Point(1, 0).transform(matrix).subtract(orig); + var v = new Point(0, 1).transform(matrix).subtract(orig); + return new Size( + 72 / u.length, + 72 / v.length + ); }, - // TODO: getPixel(point) - // TODO: test this - getPixel: function(x, y) { - var pixels = this.context.getImageData(x + 0.5, y + 0.5, 1, 1).data; + getSubImage: function(/* rectangle */) { + var rectangle = Rectangle.read(arguments); + var canvas = CanvasProvider.getCanvas(rectangle.size); + var context = canvas.getContext('2d'); + context.drawImage(this.canvas, rectangle.x, rectangle.y, + canvas.width, canvas.height, 0, 0, canvas.width, canvas.height); + return canvas; + }, + + getImage: function() { + return this._image || this.canvas; + }, + + // TODO: setImage + + // TODO: drawImage(image, point) + drawImage: function(image, x, y) { + var point = center = Point.read(arguments, 1); + this.context.drawImage(image, x, y); + }, + + /** + * {@grouptitle Pixels} + * + * Gets the color of a pixel in the raster. + * @param x + * @param y + */ + getPixel: function() { + var point = Point.read(arguments); + var ctx = this.context; + var pixels = ctx.getImageData(point.x + 0.5, point.y + 0.5, 1, 1).data; var channels = []; - for(var i = 0; i < 4; i++) + for (var i = 0; i < 4; i++) channels.push(pixels[i] / 255); return Color.read(channels); }, // TODO: setPixel(point, color) - // setPixel: function(x, y, color) { - // - // } + setPixel: function(x, y, color) { + color = Color.read(arguments, 2); + var ctx = this.context; + var imageData = ctx.getImageData(x, y, 1, 1); + imageData.data[0] = color.red * 255; + imageData.data[1] = color.green * 255; + imageData.data[2] = color.blue * 255; + imageData.data[3] = color.alpha != -1 ? color.alpha * 255 : 255; + ctx.putImageData(imageData, x, y); + }, getContext: function() { if (!this._context) @@ -90,40 +132,144 @@ Raster = Item.extend({ getCanvas: function() { if (!this._canvas) { - this._canvas = CanvasProvider.getCanvas(this.size.width, this.size.height); + this._canvas = CanvasProvider.getCanvas(this.size); this.ctx = this._canvas.getContext('2d'); - this.ctx.drawImage(this.image, 0, 0); + this.ctx.drawImage(this._image, 0, 0); } return this._canvas; }, setCanvas: function(canvas) { - CanvasProvider.returnCanvas(this._canvas); + if (this._canvas) + CanvasProvider.returnCanvas(this._canvas); + // TODO: should the width / height of the bounds be reset too? + this._size = new Size(canvas.width, canvas.height); + this._image = null; this._ctx = null; this._canvas = canvas; }, transformContent: function(matrix, flags) { - var bounds = this.bounds; - var coords = [bounds.x, bounds.y, - bounds.x + bounds.width, bounds.y + bounds.height]; - matrix.transform(coords, 0, coords, 0, 2); + var width = this._size.width; + var height = this._size.height; + var x = width * -0.5; + var y = height * -0.5; + var coords = [ + x, y, + x + width, y, + x + width, y + height, + x, y + height]; this.matrix.preConcatenate(matrix); - bounds.x = coords[0]; - bounds.y = coords[1]; - bounds.width = coords[2] - coords[0]; - bounds.height = coords[3] - coords[1]; + this.matrix.transform(coords, 0, coords, 0, 4); + + var xMin = coords[0], xMax = coords[0]; + var yMin = coords[1], yMax = coords[1]; + for(var i = 2; i < 8; i += 2) { + var x = coords[i]; + var y = coords[i + 1]; + xMin = Math.min(x, xMin); + xMax = Math.max(x, xMax); + yMin = Math.min(y, yMin); + yMax = Math.max(y, yMax); + }; + var bounds = this._bounds; + bounds.x = xMin; + bounds.y = yMin; + bounds.width = xMax - xMin; + bounds.height = yMax - yMin; }, getBounds: function() { return this._bounds; }, - draw: function(ctx) { - ctx.save(); - this.matrix.applyToContext(ctx); - ctx.drawImage(this._canvas || this.image, - -this.size.width / 2, -this.size.height / 2); - ctx.restore(); + draw: function(ctx, param) { + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + ctx.save(); + if(param.ignoreBlendMode !== true) + this.matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this.size.width / 2, -this.size.height / 2); + ctx.restore(); + param.ignoreBlendMode = false; + } + } +}, new function() { + function getAverageColor(pixels) { + var channels = [0, 0, 0]; + var total = 0; + for (var i = 0, l = pixels.length / 4; i < l; i++) { + var offset = i * 4; + var alpha = pixels[offset + 3] / 255; + total += alpha; + channels[0] += pixels[offset] * alpha; + channels[1] += pixels[offset + 1] * alpha; + channels[2] += pixels[offset + 2] * alpha; + } + for (var i = 0; i < 3; i++) + channels[i] /= total * 255; + return total ? Color.read(channels) : null; + } + + return { + /** + * {@grouptitle Average Color} + * Calculates the average color of the image within the given path, + * rectangle or point. This can be used for creating raster image + * effects. + * + * @param object + * @return the average color contained in the area covered by the + * specified path, rectangle or point. + */ + getAverageColor: function(object) { + var image; + if (object) { + var bounds, path; + if (object instanceof Path) { + // TODO: what if the path is smaller than 1 px? + // TODO: how about rounding of bounds.size? + // TODO: test with compound paths. + path = object; + bounds = object.bounds; + } else if (object.width) { + bounds = new Rectangle(object); + } else if (object.x) { + bounds = new Rectangle(object.x - 0.5, object.y - 0.5, 1, 1); + } + + var canvas = CanvasProvider.getCanvas(bounds.size); + var ctx = canvas.getContext('2d'); + var delta = bounds.topLeft.multiply(-1); + ctx.translate(delta.x, delta.y); + if (path) { + var style = object.style; + path.draw(ctx); + ctx.clip(); + path.style = style; + } + var matrix = this.matrix.clone(); + var transMatrix = Matrix.getTranslateInstance(delta); + matrix.preConcatenate(transMatrix); + matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this.size.width / 2, -this.size.height / 2); + image = canvas; + } else { + image = this.image; + } + var size = new Size(32); + var sampleCanvas = CanvasProvider.getCanvas(size); + var ctx = sampleCanvas.getContext('2d'); + ctx.drawImage(image, 0, 0, size.width, size.height); + var pixels = ctx.getImageData(0.5, 0.5, size.width, size.height).data; + var color = getAverageColor(pixels); + CanvasProvider.returnCanvas(sampleCanvas); + if (image instanceof HTMLCanvasElement) + CanvasProvider.returnCanvas(image); + return color; + } } }); \ No newline at end of file diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 6df06394..1833e942 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -19,29 +19,53 @@ CompoundPath = PathItem.extend(new function() { } }, - draw: function(ctx) { + draw: function(ctx, param) { if(!this.visible) return; if (this.children.length) { - var firstChild = this.children[0]; - ctx.beginPath(); - for (var i = 0, l = this.children.length; i < l; i++) { - var child = this.children[i]; - child.draw(ctx, true); - } - firstChild.setCtxStyles(ctx); - if (firstChild.fillColor) { - ctx.fillStyle = firstChild.fillColor.getCssString(); - ctx.fill(); - } - if (firstChild.strokeColor) { - ctx.strokeStyle = firstChild.strokeColor.getCssString(); - ctx.stroke(); + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + var firstChild = this.children[0]; + ctx.beginPath(); + param.compound = true; + for (var i = 0, l = this.children.length; i < l; i++) { + var child = this.children[i]; + child.draw(ctx, param); + } + param.compound = false; + firstChild.setCtxStyles(ctx); + if (firstChild.fillColor) { + ctx.fillStyle = firstChild.fillColor.getCssString(); + ctx.fill(); + } + if (firstChild.strokeColor) { + ctx.strokeStyle = firstChild.strokeColor.getCssString(); + ctx.stroke(); + } } } }, - // TODO: add getBounds + // TODO: have getBounds of Group / Layer / CompoundPath use the same + // code (from a utility script?) + getBounds: function() { + if (this.children.length) { + var rect = this.children[0].bounds; + var x1 = rect.x; + var y1 = rect.y; + var x2 = rect.x + rect.width; + var y2 = rect.y + rect.height; + for (var i = 1, l = this.children.length; i < l; i++) { + var rect2 = this.children[i].bounds; + x1 = Math.min(rect2.x, x1); + y1 = Math.min(rect2.y, y1); + x2 = Math.max(rect2.x + rect2.width, x1 + x2 - x1); + y2 = Math.max(rect2.y + rect2.height, y1 + y2 - y1); + } + } + return new Rectangle(x1, y1, x2 - x1, y2 - y1); + }, /** * If this is a compound path with only one path inside, @@ -73,17 +97,10 @@ CompoundPath = PathItem.extend(new function() { }, moveBy: function() { - if (!arguments.length) { - // TODO: Shouldn't this be relative to the previous position - // in lack of an argument? This should then be corrected in - // Scriptographer too. - this.moveTo(0, 0); - } else { - var point = Point.read(arguments); - var path = getCurrentPath(this); - var current = path.segments[path.segments.length - 1].point; - this.moveTo(current.add(point)); - } + var point = arguments.length ? Point.read(arguments) : new Point(); + var path = getCurrentPath(this); + var current = path.segments[path.segments.length - 1].point; + this.moveTo(current.add(point)); }, closePath: function() { diff --git a/src/path/Path.js b/src/path/Path.js index f14ad3ac..fcfb06a8 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -228,7 +228,7 @@ Path = PathItem.extend({ lineTo: function() { var segment = Segment.read(arguments); - if (segment && this._segments.length) + if (segment) this.addSegment(segment); }, @@ -403,57 +403,63 @@ Path = PathItem.extend({ this.closed = ture; }, - draw: function(ctx, compound) { + draw: function(ctx, param) { if (!this.visible) return; - if (!compound) - ctx.beginPath(); - - var segments = this._segments; - var length = segments.length; - for (var i = 0; i < length; i++) { - var segment = segments[i]; - var x = segment.point.x; - var y = segment.point.y; - var handleIn = segment.handleIn; - if (i == 0) { - ctx.moveTo(x, y); - } else { - if (handleOut.isZero() && handleIn.isZero()) { - ctx.lineTo(x, y); + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + param.ignoreBlendMode = false; + if (!param.compound) + ctx.beginPath(); + + var segments = this._segments; + var length = segments.length; + for (var i = 0; i < length; i++) { + var segment = segments[i]; + var x = segment.point.x; + var y = segment.point.y; + var handleIn = segment.handleIn; + if (i == 0) { + ctx.moveTo(x, y); } else { - ctx.bezierCurveTo( - outX, outY, - handleIn.x + x, handleIn.y + y, - x, y - ); + if (handleOut.isZero() && handleIn.isZero()) { + ctx.lineTo(x, y); + } else { + ctx.bezierCurveTo( + outX, outY, + handleIn.x + x, handleIn.y + y, + x, y + ); + } } + var handleOut = segment.handleOut; + var outX = handleOut.x + x; + var outY = handleOut.y + y; } - var handleOut = segment.handleOut; - var outX = handleOut.x + x; - var outY = handleOut.y + y; - } - if (this.closed && length > 1) { - var segment = segments[0]; - var x = segment.point.x; - var y = segment.point.y; - var handleIn = segment.handleIn; - ctx.bezierCurveTo(outX, outY, handleIn.x + x, handleIn.y + y, x, y); - ctx.closePath(); - } - if (!compound) { - this.setCtxStyles(ctx); - ctx.save(); - ctx.globalAlpha = this.opacity; - if (this.fillColor) { - ctx.fillStyle = this.fillColor.getCanvasStyle(ctx); - ctx.fill(); + if (this.closed && length > 1) { + var segment = segments[0]; + var x = segment.point.x; + var y = segment.point.y; + var handleIn = segment.handleIn; + ctx.bezierCurveTo(outX, outY, handleIn.x + x, handleIn.y + y, x, y); + ctx.closePath(); } - if (this.strokeColor) { - ctx.strokeStyle = this.strokeColor.getCanvasStyle(ctx); - ctx.stroke(); + if (!param.compound) { + this.setCtxStyles(ctx); + ctx.save(); + ctx.globalAlpha = this.opacity; + if (this.fillColor) { + ctx.fillStyle = this.fillColor.getCanvasStyle(ctx); + ctx.fill(); + } + if (this.strokeColor) { + ctx.strokeStyle = this.strokeColor.getCanvasStyle(ctx); + ctx.stroke(); + } + ctx.restore(); } - ctx.restore(); } + } }, new function() { // inject methods that require scoped privates /** diff --git a/src/path/Segment.js b/src/path/Segment.js index 79c2d4c7..8039c75e 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -34,15 +34,6 @@ Segment = Base.extend({ this.handleOut = new Point(); }, - // TODO: - // insert: function() { - // if (this._segments && this._segments.path) { - // var path = this._segments.path; - // path.checkValid(); - // - // } - // }, - getPoint: function() { return this.point; }, diff --git a/src/tool/Tool.js b/src/tool/Tool.js index 3808c21a..44dfeb2a 100644 --- a/src/tool/Tool.js +++ b/src/tool/Tool.js @@ -12,6 +12,7 @@ Tool = ToolHandler.extend({ $(this._document.canvas).removeEvents(); this._document = doc || Paper.document; var that = this, curPoint; + var dragging = false; var events = { dragstart: function(e) { curPoint = new Point(e.offset); @@ -20,6 +21,7 @@ Tool = ToolHandler.extend({ that._document.redraw(); if (that.eventInterval != -1) this.intervalId = setInterval(events.drag, that.eventInterval); + dragging = true; }, drag: function(e) { if (e) curPoint = new Point(e.offset); @@ -36,12 +38,15 @@ Tool = ToolHandler.extend({ that.onHandleEvent('MOUSE_UP', new Point(e.offset), null, null); if (that.onMouseUp) that._document.redraw(); + dragging = false; + }, + mousemove: function(e) { + if(!dragging) { + that.onHandleEvent('MOUSE_MOVE', new Point(e.offset), null, null); + if (that.onMouseMove) + that._document.redraw(); + } } - // TODO: This is currently interfering with the drag code, needs fixing: - // mousemove: function(e) { - // that.onHandleEvent('MOUSE_MOVE', new Point(e.offset), null, null); - // that._document.redraw(); - // } }; $(doc.canvas).addEvents(events); }, diff --git a/src/util/CanvasProvider.js b/src/util/CanvasProvider.js index e2f139d1..0622bfbd 100644 --- a/src/util/CanvasProvider.js +++ b/src/util/CanvasProvider.js @@ -1,15 +1,32 @@ +// TODO: it might be better to make a ContextProvider class, since you +// can always find the canvas through context.canvas. This saves code and +// speed by not having to do canvas.getContext('2d') +// TODO: Run through the canvas array to find a canvas with the requested +// width / height, so we don't need to resize it? CanvasProvider = { canvases: [], - getCanvas: function(width, height) { - var canvas = this.canvases.length - ? this.canvases.pop() - : document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - return canvas; + getCanvas: function(size) { + if(this.canvases.length) { + var canvas = this.canvases.pop(); + // If they are not the same size, we don't need to clear them + // using clearRect and visa versa. + if((canvas.width != size.width) || (canvas.height != size.height)) { + canvas.width = size.width; + canvas.height = size.height; + } else { + var context = canvas.getContext('2d'); + context.clearRect(0, 0, size.width + 1, size.height + 1); + } + return canvas; + } else { + var canvas = document.createElement('canvas'); + canvas.width = size.width; + canvas.height = size.height; + return canvas; + } }, returnCanvas: function(canvas) { this.canvases.push(canvas); } -}; +}; \ No newline at end of file diff --git a/test/index.html b/test/index.html index 3b6774ef..f6bbbba3 100644 --- a/test/index.html +++ b/test/index.html @@ -14,9 +14,11 @@ + + @@ -48,6 +50,7 @@ +

QUnit Test Suite

diff --git a/test/tests/Color.js b/test/tests/Color.js index 92c2f6aa..f4a561cb 100644 --- a/test/tests/Color.js +++ b/test/tests/Color.js @@ -68,5 +68,11 @@ test('Converting Colors', function() { var color = new GrayColor(0.2); var rgbColor = new RGBColor(color); - compareRGBColors(rgbColor, [ 0.8, 0.8, 0.8, 1 ]); + compareRGBColors(rgbColor, [ 0.8, 0.8, 0.8, 1]); +}); + +test('Setting RGBColor#gray', function() { + var color = new RGBColor(1, 0.5, 0.2); + color.gray = 0.1; + compareRGBColors(color, [ 0.9, 0.9, 0.9, 1]); }); \ No newline at end of file diff --git a/test/tests/Path.js b/test/tests/Path.js index cb55ce9b..5295434b 100644 --- a/test/tests/Path.js +++ b/test/tests/Path.js @@ -1,6 +1,7 @@ module('Path'); test('path.currentSegment', function() { + var doc = new Doc(); var path = new Path(); path.moveTo([50, 50]); path.lineTo([100, 100]); diff --git a/test/tests/Path_Bounds.js b/test/tests/Path_Bounds.js index 43db98c2..44d02a1f 100644 --- a/test/tests/Path_Bounds.js +++ b/test/tests/Path_Bounds.js @@ -28,4 +28,8 @@ test('path.bounds', function() { // Set new bounds and check segment list as result of resizing / positioning path.bounds = { x: 100, y: 100, width: 200, height: 200 }; compareSegmentLists(path.segments, [{ point: { x: 107.93066, y: 179.56982 }, handleIn: { x: -24.41211, y: 51.30664 }, handleOut: { x: 39.52734, y: -83.08447 } }, { point: { x: 271.10107, y: 160.66553 }, handleIn: { x: -53.96289, y: -99.9126 }, handleOut: { x: 53.96143, y: 99.91406 } }, { point: { x: 215.85303, y: 296.96045 }, handleIn: { x: 85.81299, y: -17.18555 }, handleOut: { x: -101.49854, y: 20.32861 } }]) -}); + + path.rotate(40); + compareRectangles(path.bounds, { x: 92.38155, y: 106.78981, width: 191.48048, height: 203.66789 }); + compareSegmentLists(path.segments, [{ point: { x: 142.604, y: 125.16748 }, handleIn: { x: -51.6792, y: 23.61182 }, handleOut: { x: 83.68457, y: -38.23438 } }, { point: { x: 279.75, y: 215.57129 }, handleIn: { x: 22.88525, y: -111.22363 }, handleOut: { x: -22.88623, y: 111.22363 } }, { point: { x: 149.81982, y: 284.46729 }, handleIn: { x: 76.78223, y: 41.99219 }, handleOut: { x: -90.81885, y: -49.67139 } }]); +}); \ No newline at end of file diff --git a/test/tests/PlacedSymbol.js b/test/tests/PlacedSymbol.js new file mode 100644 index 00000000..48be51ae --- /dev/null +++ b/test/tests/PlacedSymbol.js @@ -0,0 +1,24 @@ +module('Placed Symbol'); + +test('placedSymbol bounds', function() { + var doc = new Doc(); + var path = new Path.Circle([50, 50], 50); + var symbol = new Symbol(path); + var placedSymbol = new PlacedSymbol(symbol); + + // These tests currently fail because we haven't implemented + // Item#strokeBounds yet. + compareRectangles(placedSymbol.bounds, + new Rectangle(-50.5, -50.5, 101, 101), + 'PlacedSymbol initial bounds.'); + + placedSymbol.scale(0.5); + compareRectangles(placedSymbol.bounds, + { x: -25.5, y: -25.5, width: 51, height: 51 }, + 'Bounds after scale'); + + placedSymbol.rotate(40); + compareRectangles(placedSymbol.bounds, + { x: -25.50049, y: -25.50049, width: 51.00098, height: 51.00098 }, + 'Bounds after rotation'); +}); \ No newline at end of file diff --git a/test/tests/item.js b/test/tests/item.js index 7262a714..5185864b 100644 --- a/test/tests/item.js +++ b/test/tests/item.js @@ -89,6 +89,33 @@ test('isDescendant(item) / isAncestor(item)', function() { equals(path.isAncestor(doc.activeLayer), false); equals(doc.activeLayer.isAncestor(path), true); + + // an item can't be its own descendant: + equals(doc.activeLayer.isDescendant(doc.activeLayer), false); + + // an item can't be its own ancestor: + equals(doc.activeLayer.isAncestor(doc.activeLayer), false); +}); + +test('isGroupedWith', function() { + var doc = new Doc(); + var path = new Path(); + var secondPath = new Path(); + var group = new Group([path]); + var secondGroup = new Group([secondPath]); + + equals(path.isGroupedWith(secondPath), false); + secondGroup.appendTop(path); + equals(path.isGroupedWith(secondPath), true); + equals(path.isGroupedWith(group), false); + equals(path.isDescendant(secondGroup), true); + equals(secondGroup.isDescendant(path), false); + equals(secondGroup.isDescendant(secondGroup), false); + equals(path.isGroupedWith(secondGroup), false); + Paper.document.activeLayer.appendTop(path); + equals(path.isGroupedWith(secondPath), false); + Paper.document.activeLayer.appendTop(secondPath); + equals(path.isGroupedWith(secondPath), false); }); test('getPreviousSibling() / getNextSibling()', function() { @@ -105,4 +132,16 @@ test('hidden', function() { var firstPath = new Path(); firstPath.visible = false; equals(firstPath.hidden, true); -}); \ No newline at end of file +}); + +test('reverseChildren()', function() { + var doc = new Doc(); + var path = new Path(); + var secondPath = new Path(); + var thirdPath = new Path(); + equals(doc.activeLayer.firstChild == path, true); + doc.activeLayer.reverseChildren(); + equals(doc.activeLayer.firstChild == path, false); + equals(doc.activeLayer.firstChild == thirdPath, true); + equals(doc.activeLayer.lastChild == path, true); +}) \ No newline at end of file