From f1cd24eef89338fc998d104efe184f21b6489779 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 01:28:11 +0100 Subject: [PATCH 01/40] More work on Raster, including getAverageColor, get/setPixel, get/setSize. --- src/item/Raster.js | 165 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 124 insertions(+), 41 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 1c484aff..41185816 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -7,28 +7,48 @@ Raster = Item.extend({ this.base(); if (image) { this.image = image; - var width = image.width; - var height = image.height; - this.size = new Size(width, height); + // TODO: cross browser compatible? + var width = image.naturalWidth; + var height = image.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.width, size.height); + 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 @@ -36,47 +56,34 @@ Raster = Item.extend({ // 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); - }, - - // TODO: getPixel(point) - // TODO: test this - getPixel: function(x, y) { - var pixels = this.context.getImageData(x + 0.5, y + 0.5, 1, 1).data; + /** + * {@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) @@ -126,4 +133,80 @@ Raster = Item.extend({ -this.size.width / 2, -this.size.height / 2); ctx.restore(); } +}, 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 = 32; + var sampleCanvas = CanvasProvider.getCanvas(size); + var ctx = sampleCanvas.getContext('2d'); + ctx.drawImage(image, 0, 0, size, size); + var pixels = context.getImageData(0.5, 0.5, size, size).data; + var color = getAverageColor(pixels); + CanvasProvider.returnCanvas(tempCanvas); + if(image instanceof HTMLCanvasElement) + CanvasProvider.returnCanvas(image); + return color; + } + } }); \ No newline at end of file From dc51384a1a3a05bbac29c2ca169a6199d0332322 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 01:29:51 +0100 Subject: [PATCH 02/40] Allow CanvasProvider to receive sizes too. --- src/util/CanvasProvider.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/util/CanvasProvider.js b/src/util/CanvasProvider.js index e2f139d1..abcfba10 100644 --- a/src/util/CanvasProvider.js +++ b/src/util/CanvasProvider.js @@ -1,15 +1,18 @@ CanvasProvider = { canvases: [], - getCanvas: function(width, height) { + getCanvas: function() { + var size = Size.read(arguments); var canvas = this.canvases.length ? this.canvases.pop() : document.createElement('canvas'); - canvas.width = width; - canvas.height = height; + canvas.width = size.width; + canvas.height = size.height; return canvas; }, returnCanvas: function(canvas) { + // reset canvas: + canvas.width = canvas.width; this.canvases.push(canvas); } }; From c929bacde8e2c94c726ef7d33e3acc3431ace7c5 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 01:33:36 +0100 Subject: [PATCH 03/40] Clean up Group#draw and save/restore context state when drawing group with opacity. --- src/item/Group.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/item/Group.js b/src/item/Group.js index 9976cb68..552cd5c6 100644 --- a/src/item/Group.js +++ b/src/item/Group.js @@ -17,10 +17,10 @@ Group = Item.extend({ // If the group has an opacity of less then 1, draw its children on a // temporary canvas, and then draw that canvas onto ctx afterwards // with globalAlpha set. + var tempCanvas, originalCtx; if (this.opacity < 1) { var originalCtx = ctx; - var size = this.document.size; - var tempCanvas = CanvasProvider.getCanvas(size.width, size.height); + tempCanvas = CanvasProvider.getCanvas(this.document.size); ctx = tempCanvas.getContext('2d'); } for (var i = 0, l = this.children.length; i < l; i++) { @@ -29,8 +29,10 @@ Group = Item.extend({ ctx.clip(); } if (tempCanvas) { + originalCtx.save(); originalCtx.globalAlpha = this.opacity; originalCtx.drawImage(tempCanvas, 0, 0); + originalCtx.restore(); // Return the canvas, so it can be reused CanvasProvider.returnCanvas(tempCanvas); } From 83b8a3e815dd775cfdb407fdf5645fda5049c8e8 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 01:36:38 +0100 Subject: [PATCH 04/40] Optimize CanvasProvider to only expect a Size object. --- src/item/Raster.js | 4 ++-- src/util/CanvasProvider.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 41185816..6a2818dc 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -25,7 +25,7 @@ Raster = Item.extend({ setSize: function() { var size = Size.read(arguments); - var canvas = CanvasProvider.getCanvas(size.width, size.height); + var canvas = CanvasProvider.getCanvas(size); var context = canvas.getContext('2d'); context.drawImage(this._canvas ? this._canvas : this.image, 0, 0, size.width, size.height); @@ -97,7 +97,7 @@ 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); } diff --git a/src/util/CanvasProvider.js b/src/util/CanvasProvider.js index abcfba10..d7fa9793 100644 --- a/src/util/CanvasProvider.js +++ b/src/util/CanvasProvider.js @@ -1,7 +1,6 @@ CanvasProvider = { canvases: [], - getCanvas: function() { - var size = Size.read(arguments); + getCanvas: function(size) { var canvas = this.canvases.length ? this.canvases.pop() : document.createElement('canvas'); From 41f3f47dd35686874843fcf56aa2ff6e09335e91 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 01:51:49 +0100 Subject: [PATCH 05/40] Fix small mistakes in Raster. --- src/item/Raster.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 6a2818dc..2ac55065 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -201,9 +201,9 @@ Raster = Item.extend({ var sampleCanvas = CanvasProvider.getCanvas(size); var ctx = sampleCanvas.getContext('2d'); ctx.drawImage(image, 0, 0, size, size); - var pixels = context.getImageData(0.5, 0.5, size, size).data; + var pixels = ctx.getImageData(0.5, 0.5, size, size).data; var color = getAverageColor(pixels); - CanvasProvider.returnCanvas(tempCanvas); + CanvasProvider.returnCanvas(sampleCanvas); if(image instanceof HTMLCanvasElement) CanvasProvider.returnCanvas(image); return color; From 2712bd0419abfe0de43b80aa036ed6b2e4a6e7ac Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 02:39:33 +0100 Subject: [PATCH 06/40] Fix mistake in Raster. --- src/item/Raster.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 2ac55065..78edc8cc 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -197,11 +197,11 @@ Raster = Item.extend({ } else { image = this.image; } - var size = 32; + var size = new Size(32); var sampleCanvas = CanvasProvider.getCanvas(size); var ctx = sampleCanvas.getContext('2d'); - ctx.drawImage(image, 0, 0, size, size); - var pixels = ctx.getImageData(0.5, 0.5, size, size).data; + 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) From 73df06150addfb27c98ceb91b259f74acdf2ecdf Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 12:58:16 +0100 Subject: [PATCH 07/40] Fix RGBColor#setGray. --- src/color/RGBColor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; }, /** From 083822ff9d63e745d96e9701b4dce073b64235e1 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 13:00:46 +0100 Subject: [PATCH 08/40] Add test for RGBColor#setGray. --- test/tests/Color.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 From f88fcd3049fd5c36147c4fb03baf2fb3718ea8b1 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 13:48:24 +0100 Subject: [PATCH 09/40] Implement Point#transform(matrix). --- src/basic/Point.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/basic/Point.js b/src/basic/Point.js index 5cf1a07c..4259a678 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -66,6 +66,10 @@ var Point = Base.extend({ return this.x == point.x && this.y == point.y; }, + transform: function(matrix) { + return matrix.transform(this); + }, + getDistance: function() { var point = Point.read(arguments); var px = point.x - this.x; From d564be8a7ef69feb525d1aa3fe4dd45661c5bffe Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 13:52:27 +0100 Subject: [PATCH 10/40] Implement new Raster(canvas) / Raster#getSubImage / Raster.drawImage / Raster.ppi. --- src/item/Raster.js | 70 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 78edc8cc..502b2dc6 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -3,17 +3,22 @@ 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; + if (object instanceof Image) { + this._image = object; // TODO: cross browser compatible? - var width = image.naturalWidth; - var height = image.naturalHeight; - this._size = new Size(width, height); - this._bounds = new Rectangle(-width / 2, -height / 2, width, height); - this.matrix = new Matrix(); + width = object.naturalWidth; + height = object.naturalHeight; + } else if (object.getContext) { + this.canvas = object; + width = this.canvas.width; + height = this.canvas.height; } + this._size = new Size(width, height); + this._bounds = new Rectangle(-width / 2, -height / 2, width, height); + this.matrix = new Matrix(); }, /** @@ -27,7 +32,7 @@ Raster = Item.extend({ var size = Size.read(arguments); var canvas = CanvasProvider.getCanvas(size); var context = canvas.getContext('2d'); - context.drawImage(this._canvas ? this._canvas : this.image, + 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) @@ -51,10 +56,38 @@ Raster = Item.extend({ return this._size.height; }, - // TODO: getPpi - // TODO: getSubImage - // TODO: getImage - // TODO: drawImage + /** + * 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.0 / u.length, + 72.0 / v.length + ); + }, + + 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: drawImage(image, point) + drawImage: function(image, x, y) { + var point = center = Point.read(arguments, 1); + this.context.drawImage(image, x, y); + }, /** * {@grouptitle Pixels} @@ -99,13 +132,14 @@ Raster = Item.extend({ if (!this._canvas) { 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); this._ctx = null; this._canvas = canvas; }, @@ -129,7 +163,7 @@ Raster = Item.extend({ draw: function(ctx) { ctx.save(); this.matrix.applyToContext(ctx); - ctx.drawImage(this._canvas || this.image, + ctx.drawImage(this._canvas || this._image, -this.size.width / 2, -this.size.height / 2); ctx.restore(); } @@ -191,7 +225,7 @@ Raster = Item.extend({ var transMatrix = Matrix.getTranslateInstance(delta); matrix.preConcatenate(transMatrix); matrix.applyToContext(ctx); - ctx.drawImage(this._canvas || this.image, + ctx.drawImage(this._canvas || this._image, -this.size.width / 2, -this.size.height / 2); image = canvas; } else { @@ -204,7 +238,7 @@ Raster = Item.extend({ var pixels = ctx.getImageData(0.5, 0.5, size.width, size.height).data; var color = getAverageColor(pixels); CanvasProvider.returnCanvas(sampleCanvas); - if(image instanceof HTMLCanvasElement) + if (image instanceof HTMLCanvasElement) CanvasProvider.returnCanvas(image); return color; } From ff2ac484da0065e392d63ebfb287a37e1b7c84cf Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 13:56:27 +0100 Subject: [PATCH 11/40] Raster: add todo and perform small cleanup. --- src/item/Raster.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 502b2dc6..9607050f 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -65,8 +65,8 @@ Raster = Item.extend({ var u = new Point(1, 0).transform(matrix).subtract(orig); var v = new Point(0, 1).transform(matrix).subtract(orig); return new Size( - 72.0 / u.length, - 72.0 / v.length + 72 / u.length, + 72 / v.length ); }, @@ -82,6 +82,8 @@ Raster = Item.extend({ getImage: function() { return this._image || this.canvas; }, + + // TODO: setImage // TODO: drawImage(image, point) drawImage: function(image, x, y) { @@ -140,6 +142,7 @@ Raster = Item.extend({ setCanvas: function(canvas) { if (this._canvas) CanvasProvider.returnCanvas(this._canvas); + this._image = null; this._ctx = null; this._canvas = canvas; }, From d37a794b646bfe729d3e5fbde72b39f96d94e4fa Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 14:15:30 +0100 Subject: [PATCH 12/40] Simplify Raster#initialize, set _size in Raster#setCanvas and add todo. --- src/item/Raster.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/item/Raster.js b/src/item/Raster.js index 9607050f..f66468ca 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -6,15 +6,15 @@ Raster = Item.extend({ initialize: function(object) { var width, height; this.base(); - if (object instanceof Image) { + 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; - } else if (object.getContext) { - this.canvas = object; - width = this.canvas.width; - height = this.canvas.height; } this._size = new Size(width, height); this._bounds = new Rectangle(-width / 2, -height / 2, width, height); @@ -142,6 +142,8 @@ Raster = Item.extend({ setCanvas: function(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; From a3ec52472212ce55eec62f4e9c3f652c303a5193 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 14:28:44 +0100 Subject: [PATCH 13/40] Add documentation from Point.java to Point.js, remove TODO about adding back center support to Scriptographer for Point#rotate, since it is already there. --- src/basic/Point.js | 231 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 220 insertions(+), 11 deletions(-) diff --git a/src/basic/Point.js b/src/basic/Point.js index 4259a678..9fef3ae0 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -70,6 +70,22 @@ var Point = Base.extend({ 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; @@ -84,6 +100,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); @@ -135,7 +157,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) { @@ -152,15 +173,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) { @@ -179,6 +205,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; @@ -192,7 +236,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; @@ -205,6 +259,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, @@ -212,52 +276,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()) { @@ -301,18 +465,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()); } From ebbf8cf03960826efdec95b9493bc54dc3da20cd Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 14:30:06 +0100 Subject: [PATCH 14/40] Add missing introduction to Point.js --- src/basic/Point.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/basic/Point.js b/src/basic/Point.js index 9fef3ae0..2de3e6e1 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, From 95f9028091203ad37794673d834b7a8b855eebd9 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 14:31:32 +0100 Subject: [PATCH 15/40] Add Point#clone docs to Point.js. --- src/basic/Point.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/basic/Point.js b/src/basic/Point.js index 2de3e6e1..b5f5d7cf 100644 --- a/src/basic/Point.js +++ b/src/basic/Point.js @@ -33,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); }, From ad82be1bec89d7e003b75308d4ed5008c1bbaf60 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 17:11:02 +0100 Subject: [PATCH 16/40] Add CompoundPath#bounds --- src/path/CompoundPath.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 6df06394..7fe087a7 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -41,7 +41,25 @@ CompoundPath = PathItem.extend(new function() { } }, - // 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, From f65625b5912006e87caa59c6b9e5b5f14d00d8a6 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 17:13:41 +0100 Subject: [PATCH 17/40] Change CompoundPath#moveBy to be relative to the previous position in lack of an argument. --- src/path/CompoundPath.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 7fe087a7..c54a6057 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -91,17 +91,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() { From 4d8363123060186075a375199c5b76a38407d064 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 17:14:59 +0100 Subject: [PATCH 18/40] Segment: remove todo. --- src/path/Segment.js | 9 --------- 1 file changed, 9 deletions(-) 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; }, From 6cea290dd8709bfaedde8866848738b3fae3cf8b Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 17:42:32 +0100 Subject: [PATCH 19/40] Item: add todo's for missing functionalities. --- src/item/Item.js | 94 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/src/item/Item.js b/src/item/Item.js index 3dfe6851..fbe2aa66 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -37,6 +37,9 @@ Item = Base.extend({ return this.copyTo(this.parent); }, + // TODO: isSelected / setSelected + // TODO: isFullySelected / setFullySelected + /** * Specifies whether the item is locked. * @@ -134,6 +137,13 @@ Item = Base.extend({ } }, + // TODO: getBlendMode / setBlendMode + // TODO: getIsolated / setIsolated (print specific feature) + // TODO: get/setKnockout (print specific feature) + // TODO get/setAlphaIsShape + // TODO: get/setData + // TODO: reverseChildren + /** * The first item contained within this item. */ @@ -225,6 +235,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} * @@ -328,6 +353,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; @@ -385,7 +444,15 @@ 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} + */ + // TODO: isGroupedWith + getBounds: function() { // TODO: Implement for items other than paths return new Rectangle(); @@ -413,7 +480,30 @@ 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 type the color mode of the raster {@default same as document} + * @param resolution the resolution of the raster in dpi {@default 72} + * @param antialiasing the amount of anti-aliasing {@default 4} + * @param width {@default automatic} + * @param height {@default automatic} + * @return the newly created Raster item + */ + // TODO: rasterize + /** * The item's position within the art board. This is the * {@link Rectangle#getCenter()} of the {@link Item#getBounds()} rectangle. @@ -566,4 +656,6 @@ Item = Base.extend({ setStyle: function(style) { this._style = new PathStyle(this, style); } + + // TODO: toString }); \ No newline at end of file From 014d1053a765fb70ba94c9b3789a278e34edd0f0 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 18:09:48 +0100 Subject: [PATCH 20/40] Fix Item#isDescendant / Item#isAncestor, implement Item#isGroupedWith and add tests. --- src/item/Item.js | 20 +++++++++++++++++--- test/tests/item.js | 27 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index fbe2aa66..4bbcf118 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -411,7 +411,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; @@ -436,7 +436,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; @@ -451,7 +451,21 @@ Item = Base.extend({ * @param item * @return {@true if the items are grouped together} */ - // TODO: isGroupedWith + 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 + console.log(parent.parent); + 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 diff --git a/test/tests/item.js b/test/tests/item.js index 7262a714..1c141040 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() { From d77741db4fb87511311fa1559e4dc5b3cb2a40cc Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 19:22:19 +0100 Subject: [PATCH 21/40] Implement item#rasterize(resolution). --- src/item/Item.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index 4bbcf118..295e5759 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -509,14 +509,29 @@ Item = Base.extend({ * Rasterizes the item into a newly created Raster object. The item itself * is not removed after rasterization. * - * @param type the color mode of the raster {@default same as document} * @param resolution the resolution of the raster in dpi {@default 72} - * @param antialiasing the amount of anti-aliasing {@default 4} - * @param width {@default automatic} - * @param height {@default automatic} * @return the newly created Raster item */ - // TODO: rasterize + 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 From d9b75a7232cd45635ea0da818e431d60e6032795 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Thu, 24 Feb 2011 19:31:07 +0100 Subject: [PATCH 22/40] Implement Item#reverseChildren() and add tests for it. --- src/item/Item.js | 8 +++++++- test/tests/item.js | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index 295e5759..dad79723 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -142,7 +142,13 @@ Item = Base.extend({ // TODO: get/setKnockout (print specific feature) // TODO get/setAlphaIsShape // TODO: get/setData - // TODO: reverseChildren + + /** + * Reverses the order of this item's children + */ + reverseChildren: function() { + this.children.reverse(); + }, /** * The first item contained within this item. diff --git a/test/tests/item.js b/test/tests/item.js index 1c141040..5185864b 100644 --- a/test/tests/item.js +++ b/test/tests/item.js @@ -132,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 From eddbc2517100c4f743cb24f59bc561cf3b563bfa Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 12:46:45 +0100 Subject: [PATCH 23/40] Implement Item#blendMode. --- src/document/Doc.js | 2 +- src/item/Group.js | 41 ++++++++++-------- src/item/Item.js | 15 ++++++- src/item/PlacedSymbol.js | 17 +++++--- src/item/Raster.js | 17 +++++--- src/path/CompoundPath.js | 36 ++++++++------- src/path/Path.js | 94 +++++++++++++++++++++------------------- 7 files changed, 131 insertions(+), 91 deletions(-) diff --git a/src/document/Doc.js b/src/document/Doc.js index 8b5136da..83c37d33 100644 --- a/src/document/Doc.js +++ b/src/document/Doc.js @@ -33,7 +33,7 @@ Doc = Base.extend({ // this.canvas.width = this.canvas.width might be faster.. this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height); 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/Group.js b/src/item/Group.js index 552cd5c6..c5e92c5e 100644 --- a/src/item/Group.js +++ b/src/item/Group.js @@ -11,30 +11,35 @@ Group = Item.extend({ this.clipped = false; }, - draw: function(ctx) { + draw: function(ctx, param) { if (!this.visible) return; // If the group has an opacity of less then 1, draw its children on a // temporary canvas, and then draw that canvas onto ctx afterwards // with globalAlpha set. var tempCanvas, originalCtx; - if (this.opacity < 1) { - var originalCtx = ctx; - tempCanvas = CanvasProvider.getCanvas(this.document.size); - ctx = tempCanvas.getContext('2d'); - } - for (var i = 0, l = this.children.length; i < l; i++) { - this.children[i].draw(ctx); - if (this.clipped & i == 0) - ctx.clip(); - } - if (tempCanvas) { - originalCtx.save(); - originalCtx.globalAlpha = this.opacity; - originalCtx.drawImage(tempCanvas, 0, 0); - originalCtx.restore(); - // Return the canvas, so it can be reused - CanvasProvider.returnCanvas(tempCanvas); + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + param.ignoreBlendMode = false; + if (this.opacity < 1) { + var originalCtx = ctx; + tempCanvas = CanvasProvider.getCanvas(this.document.size); + ctx = tempCanvas.getContext('2d'); + } + for (var i = 0, l = this.children.length; i < l; i++) { + this.children[i].draw(ctx, param); + if (this.clipped & i == 0) + ctx.clip(); + } + if (tempCanvas) { + originalCtx.save(); + originalCtx.globalAlpha = this.opacity; + originalCtx.drawImage(tempCanvas, 0, 0); + originalCtx.restore(); + // Return the canvas, so it can be reused + CanvasProvider.returnCanvas(tempCanvas); + } } }, diff --git a/src/item/Item.js b/src/item/Item.js index dad79723..5af9a6c8 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -87,6 +87,20 @@ Item = Base.extend({ opacity: 1, + /** + * The blend mode of the item. + * + * Sample code: + * + * 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,7 +151,6 @@ Item = Base.extend({ } }, - // TODO: getBlendMode / setBlendMode // TODO: getIsolated / setIsolated (print specific feature) // TODO: get/setKnockout (print specific feature) // TODO get/setAlphaIsShape diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 2c38bcb7..92fc022c 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -38,12 +38,17 @@ PlacedSymbol = Item.extend({ 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 { + param.ignoreBlendMode = false; + // TODO: we need to preserve strokewidth, but still transform the fill + ctx.save(); + this.matrix.applyToContext(ctx); + this.symbol.definition.draw(ctx); + ctx.restore(); + } } // TODO: // embed() diff --git a/src/item/Raster.js b/src/item/Raster.js index f66468ca..ce75016b 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -165,12 +165,17 @@ Raster = Item.extend({ 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 { + param.ignoreBlendMode = false; + ctx.save(); + this.matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this.size.width / 2, -this.size.height / 2); + ctx.restore(); + } } }, new function() { function getAverageColor(pixels) { diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index c54a6057..e20a262e 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -19,24 +19,30 @@ 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 && !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(); + } } } }, diff --git a/src/path/Path.js b/src/path/Path.js index 9ebe308f..91110f62 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -352,57 +352,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 /** From c9d04d33f8d36db05493fb73c5a5c0c60a22690b Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 12:46:45 +0100 Subject: [PATCH 24/40] Implement BlendMode class. --- src/document/Doc.js | 2 +- src/item/BlendMode.js | 494 +++++++++++++++++++++++++++++++++++++++ src/item/Group.js | 41 ++-- src/item/Item.js | 15 +- src/item/PlacedSymbol.js | 17 +- src/item/Raster.js | 17 +- src/path/CompoundPath.js | 36 +-- src/path/Path.js | 94 ++++---- 8 files changed, 625 insertions(+), 91 deletions(-) create mode 100644 src/item/BlendMode.js diff --git a/src/document/Doc.js b/src/document/Doc.js index 8b5136da..83c37d33 100644 --- a/src/document/Doc.js +++ b/src/document/Doc.js @@ -33,7 +33,7 @@ Doc = Base.extend({ // this.canvas.width = this.canvas.width might be faster.. this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height); 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..472003b0 --- /dev/null +++ b/src/item/BlendMode.js @@ -0,0 +1,494 @@ +/* + * BlendMode code ported from Pixastic Lib - Blend - v0.1.1 + * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ + * License: [http://www.pixastic.com/lib/license.txt] + */ + +BlendMode = { + // TODO: Should we remove some of the blend modes? + // TODO: Add missing blendmodes like hue / saturation. + process: function(documentContext, item, param) { + 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'); + itemContext.translate(-itemBounds.left, -itemBounds.top); + param.ignoreBlendMode = true; + item.draw(itemContext, param); + + var data = documentContext.getImageData( + left, top, + width, height + ).data; + + var dataDesc2 = itemContext.getImageData( + 0, 0, + width, height + ); + var data2 = dataDesc2.data; + var p = size.width * size.height; + var pix = p * 4; + var pix1, pix2; + var r1, g1, b1; + var r2, g2, b2; + var r3, g3, b3; + var r4, g4, b4; + + switch (item.blendMode) { + case 'normal' : + break; + + case 'multiply' : + while (p--) { + data2[pix -= 4] = data[pix] * data2[pix] / 255; + data2[pix1 = pix + 1] = data[pix1] * data2[pix1] / 255; + data2[pix2 = pix + 2] = data[pix2] * data2[pix2] / 255; + } + break; + + case 'lighten' : + while (p--) { + if ((r1 = data[pix -= 4]) > data2[pix]) + data2[pix] = r1; + if ((g1 = data[pix1 = pix + 1]) > data2[pix1]) + data2[pix1] = g1; + if ((b1 = data[pix2 = pix + 2]) > data2[pix2]) + data2[pix2] = b1; + } + break; + + case 'darken' : + while (p--) { + if ((r1 = data[pix -= 4]) < data2[pix]) + data2[pix] = r1; + if ((g1 = data[pix1 = pix + 1]) < data2[pix1]) + data2[pix1] = g1; + if ((b1 = data[pix2 = pix + 2]) < data2[pix2]) + data2[pix2] = b1; + + } + break; + + case 'darker-color' : + while (p--) { + if (((r1 = data[pix -= 4]) * 0.3 + + (g1 = data[pix1 = pix + 1]) * 0.59 + + (b1 = data[pix2 = pix + 2]) * 0.11) + <= (data2[pix] * 0.3 + data2[pix1] * 0.59 + + data2[pix2] * 0.11)) { + data2[pix] = r1; + data2[pix1] = g1; + data2[pix2] = b1; + } + } + break; + + case 'lighter-color' : + while (p--) { + if (((r1 = data[pix -= 4]) * 0.3 + + (g1 = data[pix1 = pix + 1]) + * 0.59 + (b1 = data[pix2 = pix + 2]) * 0.11) + > (data2[pix] * 0.3 + data2[pix1] * 0.59 + + data2[pix2] * 0.11)) { + data2[pix] = r1; + data2[pix1] = g1; + data2[pix2] = b1; + } + } + break; + + case 'linear-dodge' : + /* + otherdocumentContext.globalCompositeOperation = 'source-over'; + otherdocumentContext.drawImage(params.canvas, 0, 0); + otherdocumentContext.globalCompositeOperation = 'lighter'; + otherdocumentContext.drawImage(image, 0, 0); + */ + + while (p--) { + if ((r3 = data[pix -= 4] + data2[pix]) > 255) + data2[pix] = 255; + else + data2[pix] = r3; + if ((g3 = data[pix1 = pix + 1] + data2[pix1]) > 255) + data2[pix1] = 255; + else + data2[pix1] = g3; + if ((b3 = data[pix2 = pix + 2] + data2[pix2]) > 255) + data2[pix2] = 255; + else + data2[pix2] = b3; + } + + break; + + case 'linear-burn' : + while (p--) { + if ((r3 = data[pix -= 4] + data2[pix]) < 255) + data2[pix] = 0; + else + data2[pix] = (r3 - 255); + if ((g3 = data[pix1 = pix + 1] + data2[pix1]) < 255) + data2[pix1] = 0; + else + data2[pix1] = (g3 - 255); + if ((b3 = data[pix2 = pix + 2] + data2[pix2]) < 255) + data2[pix2] = 0; + else + data2[pix2] = (b3 - 255); + } + break; + + case 'difference' : + while (p--) { + if ((r3 = data[pix -= 4] - data2[pix]) < 0) + data2[pix] = -r3; + else + data2[pix] = r3; + if ((g3 = data[pix1 = pix + 1] - data2[pix1]) < 0) + data2[pix1] = -g3; + else + data2[pix1] = g3; + if ((b3 = data[pix2 = pix + 2] - data2[pix2]) < 0) + data2[pix2] = -b3; + else + data2[pix2] = b3; + } + break; + + case 'screen' : + while (p--) { + data2[pix -= 4] = (255 - (((255 - data2[pix]) + * (255 - data[pix])) >> 8)); + data2[pix1 = pix + 1] = (255 - (((255 - data2[pix1]) + * (255 - data[pix1])) >> 8)); + data2[pix2 = pix + 2] = (255 - (((255 - data2[pix2]) + * (255 - data[pix2])) >> 8)); + } + break; + + case 'exclusion' : + var div_2_255 = 2 / 255; + while (p--) { + data2[pix -= 4] = (r1 = data[pix]) + - (r1 * div_2_255 - 1) * data2[pix]; + data2[pix1 = pix + 1] = (g1 = data[pix1]) + - (g1 * div_2_255 - 1) * data2[pix1]; + data2[pix2 = pix + 2] = (b1 = data[pix2]) + - (b1 * div_2_255 - 1) * data2[pix2]; + } + break; + + case 'overlay' : + var div_2_255 = 2 / 255; + while (p--) { + if ((r1 = data[pix -= 4]) < 128) + data2[pix] = data2[pix] * r1 * div_2_255; + else + data2[pix] = 255 - (255 - data2[pix]) * (255 - r1) + * div_2_255; + + if ((g1 = data[pix1 = pix + 1]) < 128) + data2[pix1] = data2[pix1] * g1 * div_2_255; + else + data2[pix1] = 255 - (255 - data2[pix1]) * (255 - g1) + * div_2_255; + + if ((b1 = data[pix2 = pix + 2]) < 128) + data2[pix2] = data2[pix2] * b1 * div_2_255; + else + data2[pix2] = 255 - (255 - data2[pix2]) * (255 - b1) + * div_2_255; + + } + break; + + case 'soft-light' : + var div_2_255 = 2 / 255; + while (p--) { + if ((r1 = data[pix -= 4]) < 128) + data2[pix] = ((data2[pix] >> 1) + 64) * r1 * div_2_255; + else + data2[pix] = 255 - (191 - (data2[pix] >> 1)) + * (255 - r1) * div_2_255; + + if ((g1 = data[pix1 = pix + 1]) < 128) + data2[pix1] = ((data2[pix1] >> 1) + 64) * g1 * div_2_255; + else + data2[pix1] = 255 - (191 - (data2[pix1] >> 1)) + * (255 - g1) * div_2_255; + + if ((b1 = data[pix2 = pix + 2]) < 128) + data2[pix2] = ((data2[pix2] >> 1) + 64) * b1 * div_2_255; + else + data2[pix2] = 255 - (191 - (data2[pix2] >> 1)) + * (255 - b1) * div_2_255; + + } + break; + + case 'hard-light' : + var div_2_255 = 2 / 255; + while (p--) { + if ((r2 = data2[pix -= 4]) < 128) + data2[pix] = data[pix] * r2 * div_2_255; + else + data2[pix] = 255 - (255 - data[pix]) * (255 - r2) + * div_2_255; + + if ((g2 = data2[pix1 = pix + 1]) < 128) + data2[pix1] = data[pix1] * g2 * div_2_255; + else + data2[pix1] = 255 - (255 - data[pix1]) * (255 - g2) + * div_2_255; + + if ((b2 = data2[pix2 = pix + 2]) < 128) + data2[pix2] = data[pix2] * b2 * div_2_255; + else + data2[pix2] = 255 - (255 - data[pix2]) * (255 - b2) + * div_2_255; + + } + break; + + case 'color-dodge' : + while (p--) { + if ((r3 = (data[pix -= 4] << 8) / (255 - (r2 = data2[pix]))) + > 255 || r2 == 255) + data2[pix] = 255; + else + data2[pix] = r3; + + if ((g3 = (data[pix1 = pix + 1] << 8) / (255 + - (g2 = data2[pix1]))) > 255 || g2 == 255) + data2[pix1] = 255; + else + data2[pix1] = g3; + + if ((b3 = (data[pix2 = pix + 2] << 8) / (255 + - (b2 = data2[pix2]))) > 255 || b2 == 255) + data2[pix2] = 255; + else + data2[pix2] = b3; + } + break; + + case 'color-burn' : + while (p--) { + if ((r3 = 255 - ((255 - data[pix -= 4]) << 8) / data2[pix]) + < 0 || data2[pix] == 0) + data2[pix] = 0; + else + data2[pix] = r3; + + if ((g3 = 255 - ((255 - data[pix1 = pix + 1]) << 8) / + data2[pix1]) < 0 || data2[pix1] == 0) + data2[pix1] = 0; + else + data2[pix1] = g3; + + if ((b3 = 255 - ((255 - data[pix2 = pix + 2]) << 8) / + data2[pix2]) < 0 || data2[pix2] == 0) + data2[pix2] = 0; + else + data2[pix2] = b3; + } + break; + + case 'linear-light' : + while (p--) { + if (((r3 = 2 * (r2 = data2[pix -= 4]) + data[pix] - 256) + < 0) || (r2 < 128 && r3 < 0)) { + data2[pix] = 0; + } else { + if (r3 > 255) + data2[pix] = 255; + else + data2[pix] = r3; + } + if (((g3 = 2 * (g2 = data2[pix1 = pix + 1]) + data[pix1] + - 256) < 0) || (g2 < 128 && g3 < 0)) { + data2[pix1] = 0; + } else { + if (g3 > 255) + data2[pix1] = 255; + else + data2[pix1] = g3; + } + if ( ((b3 = 2*(b2 = data2[pix2 = pix + 2])+ data[pix2]-256) + < 0) || (b2 < 128 && b3 < 0)) { + data2[pix2] = 0; + } else { + if (b3 > 255) + data2[pix2] = 255; + else + data2[pix2] = b3; + } + } + break; + + case 'vivid-light' : + while (p--) { + if ((r2 = data2[pix -= 4]) < 128) { + if (r2) { + if ((r3 = 255 - ((255 - data[pix]) << 8) / + (2 * r2)) < 0) + data2[pix] = 0; + else + data2[pix] = r3; + } else { + data2[pix] = 0; + } + } else if ((r3 = (r4 = 2 * r2 - 256)) < 255) { + if ((r3 = (data[pix] << 8) / (255 - r4)) > 255) + data2[pix] = 255; + else + data2[pix] = r3; + } else { + if (r3 < 0) + data2[pix] = 0; + else + data2[pix] = r3; + } + + if ((g2 = data2[pix1 = pix + 1]) < 128) { + if (g2) { + if ((g3 = 255 - ((255 - data[pix1]) << 8) / + (2 * g2)) < 0) + data2[pix1] = 0; + else + data2[pix1] = g3; + } else { + data2[pix1] = 0; + } + } else if ((g3 = (g4 = 2 * g2 - 256)) < 255) { + if ((g3 = (data[pix1] << 8) / (255 - g4)) > 255) + data2[pix1] = 255; + else + data2[pix1] = g3; + } else { + if (g3 < 0) + data2[pix1] = 0; + else + data2[pix1] = g3; + } + + if ((b2 = data2[pix2 = pix + 2]) < 128) { + if (b2) { + if ((b3 = 255 - ((255 - data[pix2]) << 8) / + (2 * b2)) < 0) + data2[pix2] = 0; + else + data2[pix2] = b3; + } else { + data2[pix2] = 0; + } + } else if ((b3 = (b4 = 2 * b2 - 256)) < 255) { + if ((b3 = (data[pix2] << 8) / (255 - b4)) > 255) + data2[pix2] = 255; + else + data2[pix2] = b3; + } else { + if (b3 < 0) + data2[pix2] = 0; + else + data2[pix2] = b3; + } + } + break; + + case 'pin-light' : + while (p--) { + if ((r2 = data2[pix -= 4]) < 128) + if ((r1 = data[pix]) < (r4 = 2 * r2)) + data2[pix] = r1; + else + data2[pix] = r4; + else + if ((r1 = data[pix]) > (r4 = 2 * r2 - 256)) + data2[pix] = r1; + else + data2[pix] = r4; + + if ((g2 = data2[pix1 = pix + 1]) < 128) + if ((g1 = data[pix1]) < (g4 = 2 * g2)) + data2[pix1] = g1; + else + data2[pix1] = g4; + else + if ((g1 = data[pix1]) > (g4 = 2 * g2 - 256)) + data2[pix1] = g1; + else + data2[pix1] = g4; + + if ((r2 = data2[pix2 = pix + 2]) < 128) + if ((r1 = data[pix2]) < (r4 = 2 * r2)) + data2[pix2] = r1; + else + data2[pix2] = r4; + else + if ((r1 = data[pix2]) > (r4 = 2 * r2 - 256)) + data2[pix2] = r1; + else + data2[pix2] = r4; + } + break; + + case 'hard-mix' : + while (p--) { + if ((r2 = data2[pix -= 4]) < 128) + if (255 - ((255 - data[pix]) << 8) / (2 * r2) < 128 + || r2 == 0) + data2[pix] = 0; + else + data2[pix] = 255; + else if ((r4 = 2 * r2 - 256) < 255 + && (data[pix] << 8) / (255 - r4) < 128) + data2[pix] = 0; + else + data2[pix] = 255; + + if ((g2 = data2[pix1 = pix + 1]) < 128) + if (255 - ((255 - data[pix1]) << 8) / (2 * g2) < 128 + || g2 == 0) + data2[pix1] = 0; + else + data2[pix1] = 255; + else if ((g4 = 2 * g2 - 256) < 255 + && (data[pix1] << 8) / (255 - g4) < 128) + data2[pix1] = 0; + else + data2[pix1] = 255; + + if ((b2 = data2[pix2 = pix + 2]) < 128) + if (255 - ((255 - data[pix2]) << 8) / (2 * b2) < 128 + || b2 == 0) + data2[pix2] = 0; + else + data2[pix2] = 255; + else if ((b4 = 2 * b2 - 256) < 255 + && (data[pix2] << 8) / (255 - b4) < 128) + data2[pix2] = 0; + else + data2[pix2] = 255; + } + break; + } + + itemContext.putImageData(dataDesc2, 0, 0); + + documentContext.drawImage( + itemCanvas, + 0, 0, + width, height, + left, top, + width, height + ); + CanvasProvider.returnCanvas(itemCanvas); + } +}; \ No newline at end of file diff --git a/src/item/Group.js b/src/item/Group.js index 552cd5c6..c5e92c5e 100644 --- a/src/item/Group.js +++ b/src/item/Group.js @@ -11,30 +11,35 @@ Group = Item.extend({ this.clipped = false; }, - draw: function(ctx) { + draw: function(ctx, param) { if (!this.visible) return; // If the group has an opacity of less then 1, draw its children on a // temporary canvas, and then draw that canvas onto ctx afterwards // with globalAlpha set. var tempCanvas, originalCtx; - if (this.opacity < 1) { - var originalCtx = ctx; - tempCanvas = CanvasProvider.getCanvas(this.document.size); - ctx = tempCanvas.getContext('2d'); - } - for (var i = 0, l = this.children.length; i < l; i++) { - this.children[i].draw(ctx); - if (this.clipped & i == 0) - ctx.clip(); - } - if (tempCanvas) { - originalCtx.save(); - originalCtx.globalAlpha = this.opacity; - originalCtx.drawImage(tempCanvas, 0, 0); - originalCtx.restore(); - // Return the canvas, so it can be reused - CanvasProvider.returnCanvas(tempCanvas); + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { + BlendMode.process(ctx, this, param); + } else { + param.ignoreBlendMode = false; + if (this.opacity < 1) { + var originalCtx = ctx; + tempCanvas = CanvasProvider.getCanvas(this.document.size); + ctx = tempCanvas.getContext('2d'); + } + for (var i = 0, l = this.children.length; i < l; i++) { + this.children[i].draw(ctx, param); + if (this.clipped & i == 0) + ctx.clip(); + } + if (tempCanvas) { + originalCtx.save(); + originalCtx.globalAlpha = this.opacity; + originalCtx.drawImage(tempCanvas, 0, 0); + originalCtx.restore(); + // Return the canvas, so it can be reused + CanvasProvider.returnCanvas(tempCanvas); + } } }, diff --git a/src/item/Item.js b/src/item/Item.js index dad79723..5af9a6c8 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -87,6 +87,20 @@ Item = Base.extend({ opacity: 1, + /** + * The blend mode of the item. + * + * Sample code: + * + * 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,7 +151,6 @@ Item = Base.extend({ } }, - // TODO: getBlendMode / setBlendMode // TODO: getIsolated / setIsolated (print specific feature) // TODO: get/setKnockout (print specific feature) // TODO get/setAlphaIsShape diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 2c38bcb7..92fc022c 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -38,12 +38,17 @@ PlacedSymbol = Item.extend({ 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 { + param.ignoreBlendMode = false; + // TODO: we need to preserve strokewidth, but still transform the fill + ctx.save(); + this.matrix.applyToContext(ctx); + this.symbol.definition.draw(ctx); + ctx.restore(); + } } // TODO: // embed() diff --git a/src/item/Raster.js b/src/item/Raster.js index f66468ca..ce75016b 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -165,12 +165,17 @@ Raster = Item.extend({ 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 { + param.ignoreBlendMode = false; + ctx.save(); + this.matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this.size.width / 2, -this.size.height / 2); + ctx.restore(); + } } }, new function() { function getAverageColor(pixels) { diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index c54a6057..e20a262e 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -19,24 +19,30 @@ 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 && !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(); + } } } }, diff --git a/src/path/Path.js b/src/path/Path.js index 9ebe308f..91110f62 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -352,57 +352,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 /** From ec0b1dec1d67c798b3dea03ccd34558b7c60f768 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 13:13:46 +0100 Subject: [PATCH 25/40] PlacedSymbol: add missing param object to PlacedSymbol#definition#draw call. --- src/item/PlacedSymbol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 92fc022c..acc49f2c 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -46,7 +46,7 @@ PlacedSymbol = Item.extend({ // TODO: we need to preserve strokewidth, but still transform the fill ctx.save(); this.matrix.applyToContext(ctx); - this.symbol.definition.draw(ctx); + this.symbol.definition.draw(ctx, param); ctx.restore(); } } From 1c364601b488689274707143bcb85de17ff9196e Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 13:28:35 +0100 Subject: [PATCH 26/40] Added todo about supporting BlendMode for PlacedSymbols. --- src/item/PlacedSymbol.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index acc49f2c..f8a5de60 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -39,6 +39,7 @@ PlacedSymbol = Item.extend({ }, draw: function(ctx, param) { + // TODO: BlendMode isn't working yet for PlacedSymbols. if(this.blendMode != 'normal' && !param.ignoreBlendMode) { BlendMode.process(ctx, this, param); } else { From c47a18ea58f3a4dc9334c8910b62028c3e956fdf Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 13:57:01 +0100 Subject: [PATCH 27/40] BlendMode: add todo about using Item#strokeBounds instead of Item#bounds. --- src/item/BlendMode.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/item/BlendMode.js b/src/item/BlendMode.js index 472003b0..6ff2ffcf 100644 --- a/src/item/BlendMode.js +++ b/src/item/BlendMode.js @@ -8,6 +8,7 @@ BlendMode = { // TODO: Should we remove some of the blend modes? // TODO: Add missing blendmodes like hue / saturation. process: function(documentContext, item, param) { + // TODO: use strokeBounds var itemBounds = item.bounds; var top = Math.floor(itemBounds.top); var left = Math.floor(itemBounds.left); From 049504a67aa8447ac12a3bd33f45db527468b84c Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 18:37:46 +0100 Subject: [PATCH 28/40] Changed BlendMode library from code ported from Pixastic to code ported from Gavin Kistner's Context Blender Javascript Library. --- src/item/BlendMode.js | 612 ++++++++++++------------------------------ 1 file changed, 166 insertions(+), 446 deletions(-) diff --git a/src/item/BlendMode.js b/src/item/BlendMode.js index 6ff2ffcf..37f9fa3e 100644 --- a/src/item/BlendMode.js +++ b/src/item/BlendMode.js @@ -1,12 +1,32 @@ /* - * BlendMode code ported from Pixastic Lib - Blend - v0.1.1 - * Copyright (c) 2008 Jacob Seidelin, jseidelin@nihilogic.dk, http://blog.nihilogic.dk/ - * License: [http://www.pixastic.com/lib/license.txt] + * 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 some of the blend modes? - // TODO: Add missing blendmodes like hue / saturation. + // 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; @@ -18,478 +38,178 @@ BlendMode = { var itemCanvas = CanvasProvider.getCanvas(size); var itemContext = itemCanvas.getContext('2d'); - itemContext.translate(-itemBounds.left, -itemBounds.top); + 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 data = documentContext.getImageData( + var dstD = documentContext.getImageData( left, top, width, height - ).data; + ); - var dataDesc2 = itemContext.getImageData( + var srcD = itemContext.getImageData( 0, 0, width, height ); - var data2 = dataDesc2.data; - var p = size.width * size.height; - var pix = p * 4; - var pix1, pix2; - var r1, g1, b1; - var r2, g2, b2; - var r3, g3, b3; - var r4, g4, b4; - switch (item.blendMode) { - case 'normal' : + 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;px data2[pix]) - data2[pix] = r1; - if ((g1 = data[pix1 = pix + 1]) > data2[pix1]) - data2[pix1] = g1; - if ((b1 = data[pix2 = pix + 2]) > data2[pix2]) - data2[pix2] = b1; - } + case 'multiply': + dst[px ] = (sRA*dRA + sRA*(1-dA) + dRA*(1-sA)) * demultiply; + dst[px+1] = (sGA*dGA + sGA*(1-dA) + dGA*(1-sA)) * demultiply; + dst[px+2] = (sBA*dBA + sBA*(1-dA) + dBA*(1-sA)) * demultiply; break; - case 'darken' : - while (p--) { - if ((r1 = data[pix -= 4]) < data2[pix]) - data2[pix] = r1; - if ((g1 = data[pix1 = pix + 1]) < data2[pix1]) - data2[pix1] = g1; - if ((b1 = data[pix2 = pix + 2]) < data2[pix2]) - data2[pix2] = b1; - - } + case 'difference': + dst[px ] = (sRA + dRA - 2 * Math.min( sRA*dA, dRA*sA )) * demultiply; + dst[px+1] = (sGA + dGA - 2 * Math.min( sGA*dA, dGA*sA )) * demultiply; + dst[px+2] = (sBA + dBA - 2 * Math.min( sBA*dA, dBA*sA )) * demultiply; break; - case 'darker-color' : - while (p--) { - if (((r1 = data[pix -= 4]) * 0.3 - + (g1 = data[pix1 = pix + 1]) * 0.59 - + (b1 = data[pix2 = pix + 2]) * 0.11) - <= (data2[pix] * 0.3 + data2[pix1] * 0.59 - + data2[pix2] * 0.11)) { - data2[pix] = r1; - data2[pix1] = g1; - data2[pix2] = b1; - } - } + // ******* Slightly different from Photoshop, where alpha is concerned + case 'src-in': + // Only differs from Photoshop in low-opacity areas + dA2 = sA*dA; + demultiply = 255 / dA2; + dst[px+3] = dA2*255; + dst[px ] = sRA*dA * demultiply; + dst[px+1] = sGA*dA * demultiply; + dst[px+2] = sBA*dA * demultiply; break; - case 'lighter-color' : - while (p--) { - if (((r1 = data[pix -= 4]) * 0.3 - + (g1 = data[pix1 = pix + 1]) - * 0.59 + (b1 = data[pix2 = pix + 2]) * 0.11) - > (data2[pix] * 0.3 + data2[pix1] * 0.59 - + data2[pix2] * 0.11)) { - data2[pix] = r1; - data2[pix1] = g1; - data2[pix2] = b1; - } - } + case 'plus': + case 'add': + // Photoshop doesn't simply add the alpha channels; this might be correct wrt SVG 1.2 + dA2 = Math.min(1,sA+dA); + dst[px+3] = dA2*255; + demultiply = 255 / dA2; + dst[px ] = Math.min(sRA + dRA,1) * demultiply; + dst[px+1] = Math.min(sGA + dGA,1) * demultiply; + dst[px+2] = Math.min(sBA + dBA,1) * demultiply; break; - case 'linear-dodge' : - /* - otherdocumentContext.globalCompositeOperation = 'source-over'; - otherdocumentContext.drawImage(params.canvas, 0, 0); - otherdocumentContext.globalCompositeOperation = 'lighter'; - otherdocumentContext.drawImage(image, 0, 0); - */ + case 'overlay': + // Correct for 100% opacity case; colors get clipped as opacity falls + dst[px ] = (dRA<=0.5) ? (2*src[px ]*dRA/dA) : 255 - (2 - 2*dRA/dA) * (255-src[px ]); + dst[px+1] = (dGA<=0.5) ? (2*src[px+1]*dGA/dA) : 255 - (2 - 2*dGA/dA) * (255-src[px+1]); + dst[px+2] = (dBA<=0.5) ? (2*src[px+2]*dBA/dA) : 255 - (2 - 2*dBA/dA) * (255-src[px+2]); - while (p--) { - if ((r3 = data[pix -= 4] + data2[pix]) > 255) - data2[pix] = 255; - else - data2[pix] = r3; - if ((g3 = data[pix1 = pix + 1] + data2[pix1]) > 255) - data2[pix1] = 255; - else - data2[pix1] = g3; - if ((b3 = data[pix2 = pix + 2] + data2[pix2]) > 255) - data2[pix2] = 255; - else - data2[pix2] = b3; - } + // http://dunnbypaul.net/blends/ + // dst[px ] = ( (dRA<=0.5) ? (2*sRA*dRA) : 1 - (1 - 2*(dRA-0.5)) * (1-sRA) ) * demultiply; + // dst[px+1] = ( (dGA<=0.5) ? (2*sGA*dGA) : 1 - (1 - 2*(dGA-0.5)) * (1-sGA) ) * demultiply; + // dst[px+2] = ( (dBA<=0.5) ? (2*sBA*dBA) : 1 - (1 - 2*(dBA-0.5)) * (1-sBA) ) * demultiply; + // http://www.barbato.us/2010/12/01/blimageblending-emulating-photoshops-blending-modes-opencv/#toc-blendoverlay + // dst[px ] = ( (sRA<=0.5) ? (sRA*dRA + sRA*(1-dA) + dRA*(1-sA)) : (sRA + dRA - sRA*dRA) ) * demultiply; + // dst[px+1] = ( (sGA<=0.5) ? (sGA*dGA + sGA*(1-dA) + dGA*(1-sA)) : (sGA + dGA - sGA*dGA) ) * demultiply; + // dst[px+2] = ( (sBA<=0.5) ? (sBA*dBA + sBA*(1-dA) + dBA*(1-sA)) : (sBA + dBA - sBA*dBA) ) * demultiply; + + // http://www.nathanm.com/photoshop-blending-math/ + // dst[px ] = ( (sRA < 0.5) ? (2 * dRA * sRA) : (1 - 2 * (1 - sRA) * (1 - dRA)) ) * demultiply; + // dst[px+1] = ( (sGA < 0.5) ? (2 * dGA * sGA) : (1 - 2 * (1 - sGA) * (1 - dGA)) ) * demultiply; + // dst[px+2] = ( (sBA < 0.5) ? (2 * dBA * sBA) : (1 - 2 * (1 - sBA) * (1 - dBA)) ) * demultiply; break; - case 'linear-burn' : - while (p--) { - if ((r3 = data[pix -= 4] + data2[pix]) < 255) - data2[pix] = 0; - else - data2[pix] = (r3 - 255); - if ((g3 = data[pix1 = pix + 1] + data2[pix1]) < 255) - data2[pix1] = 0; - else - data2[pix1] = (g3 - 255); - if ((b3 = data[pix2 = pix + 2] + data2[pix2]) < 255) - data2[pix2] = 0; - else - data2[pix2] = (b3 - 255); - } + case 'hardlight': + dst[px ] = (sRA<=0.5) ? (2*dst[px ]*sRA/dA) : 255 - (2 - 2*sRA/sA) * (255-dst[px ]); + dst[px+1] = (sGA<=0.5) ? (2*dst[px+1]*sGA/dA) : 255 - (2 - 2*sGA/sA) * (255-dst[px+1]); + dst[px+2] = (sBA<=0.5) ? (2*dst[px+2]*sBA/dA) : 255 - (2 - 2*sBA/sA) * (255-dst[px+2]); + break; + + case 'color-dodge': + case 'dodge': + if ( src[px ] == 255 && dRA==0) dst[px ] = 255; + else dst[px ] = Math.min(255, dst[px ]/(255 - src[px ])) * demultiply; + + if ( src[px+1] == 255 && dGA==0) dst[px+1] = 255; + else dst[px+1] = Math.min(255, dst[px+1]/(255 - src[px+1])) * demultiply; + + if ( src[px+2] == 255 && dBA==0) dst[px+2] = 255; + else dst[px+2] = Math.min(255, dst[px+2]/(255 - src[px+2])) * demultiply; + break; + + case 'color-burn': + case 'burn': + if ( src[px ] == 0 && dRA==0) dst[px ] = 0; + else dst[px ] = (1 - Math.min(1, (1 - dRA)/sRA)) * demultiply; + + if ( src[px+1] == 0 && dGA==0) dst[px+1] = 0; + else dst[px+1] = (1 - Math.min(1, (1 - dGA)/sGA)) * demultiply; + + if ( src[px+2] == 0 && dBA==0) dst[px+2] = 0; + else dst[px+2] = (1 - Math.min(1, (1 - dBA)/sBA)) * demultiply; + break; + + case 'darken': + case 'darker': + dst[px ] = (sRA>dRA ? 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> 8)); - data2[pix1 = pix + 1] = (255 - (((255 - data2[pix1]) - * (255 - data[pix1])) >> 8)); - data2[pix2 = pix + 2] = (255 - (((255 - data2[pix2]) - * (255 - data[pix2])) >> 8)); - } - break; - - case 'exclusion' : - var div_2_255 = 2 / 255; - while (p--) { - data2[pix -= 4] = (r1 = data[pix]) - - (r1 * div_2_255 - 1) * data2[pix]; - data2[pix1 = pix + 1] = (g1 = data[pix1]) - - (g1 * div_2_255 - 1) * data2[pix1]; - data2[pix2 = pix + 2] = (b1 = data[pix2]) - - (b1 * div_2_255 - 1) * data2[pix2]; - } - break; - - case 'overlay' : - var div_2_255 = 2 / 255; - while (p--) { - if ((r1 = data[pix -= 4]) < 128) - data2[pix] = data2[pix] * r1 * div_2_255; - else - data2[pix] = 255 - (255 - data2[pix]) * (255 - r1) - * div_2_255; - - if ((g1 = data[pix1 = pix + 1]) < 128) - data2[pix1] = data2[pix1] * g1 * div_2_255; - else - data2[pix1] = 255 - (255 - data2[pix1]) * (255 - g1) - * div_2_255; - - if ((b1 = data[pix2 = pix + 2]) < 128) - data2[pix2] = data2[pix2] * b1 * div_2_255; - else - data2[pix2] = 255 - (255 - data2[pix2]) * (255 - b1) - * div_2_255; - - } - break; - - case 'soft-light' : - var div_2_255 = 2 / 255; - while (p--) { - if ((r1 = data[pix -= 4]) < 128) - data2[pix] = ((data2[pix] >> 1) + 64) * r1 * div_2_255; - else - data2[pix] = 255 - (191 - (data2[pix] >> 1)) - * (255 - r1) * div_2_255; - - if ((g1 = data[pix1 = pix + 1]) < 128) - data2[pix1] = ((data2[pix1] >> 1) + 64) * g1 * div_2_255; - else - data2[pix1] = 255 - (191 - (data2[pix1] >> 1)) - * (255 - g1) * div_2_255; - - if ((b1 = data[pix2 = pix + 2]) < 128) - data2[pix2] = ((data2[pix2] >> 1) + 64) * b1 * div_2_255; - else - data2[pix2] = 255 - (191 - (data2[pix2] >> 1)) - * (255 - b1) * div_2_255; - - } - break; - - case 'hard-light' : - var div_2_255 = 2 / 255; - while (p--) { - if ((r2 = data2[pix -= 4]) < 128) - data2[pix] = data[pix] * r2 * div_2_255; - else - data2[pix] = 255 - (255 - data[pix]) * (255 - r2) - * div_2_255; - - if ((g2 = data2[pix1 = pix + 1]) < 128) - data2[pix1] = data[pix1] * g2 * div_2_255; - else - data2[pix1] = 255 - (255 - data[pix1]) * (255 - g2) - * div_2_255; - - if ((b2 = data2[pix2 = pix + 2]) < 128) - data2[pix2] = data[pix2] * b2 * div_2_255; - else - data2[pix2] = 255 - (255 - data[pix2]) * (255 - b2) - * div_2_255; - - } - break; - - case 'color-dodge' : - while (p--) { - if ((r3 = (data[pix -= 4] << 8) / (255 - (r2 = data2[pix]))) - > 255 || r2 == 255) - data2[pix] = 255; - else - data2[pix] = r3; - - if ((g3 = (data[pix1 = pix + 1] << 8) / (255 - - (g2 = data2[pix1]))) > 255 || g2 == 255) - data2[pix1] = 255; - else - data2[pix1] = g3; - - if ((b3 = (data[pix2 = pix + 2] << 8) / (255 - - (b2 = data2[pix2]))) > 255 || b2 == 255) - data2[pix2] = 255; - else - data2[pix2] = b3; - } - break; - - case 'color-burn' : - while (p--) { - if ((r3 = 255 - ((255 - data[pix -= 4]) << 8) / data2[pix]) - < 0 || data2[pix] == 0) - data2[pix] = 0; - else - data2[pix] = r3; - - if ((g3 = 255 - ((255 - data[pix1 = pix + 1]) << 8) / - data2[pix1]) < 0 || data2[pix1] == 0) - data2[pix1] = 0; - else - data2[pix1] = g3; - - if ((b3 = 255 - ((255 - data[pix2 = pix + 2]) << 8) / - data2[pix2]) < 0 || data2[pix2] == 0) - data2[pix2] = 0; - else - data2[pix2] = b3; - } - break; - - case 'linear-light' : - while (p--) { - if (((r3 = 2 * (r2 = data2[pix -= 4]) + data[pix] - 256) - < 0) || (r2 < 128 && r3 < 0)) { - data2[pix] = 0; - } else { - if (r3 > 255) - data2[pix] = 255; - else - data2[pix] = r3; - } - if (((g3 = 2 * (g2 = data2[pix1 = pix + 1]) + data[pix1] - - 256) < 0) || (g2 < 128 && g3 < 0)) { - data2[pix1] = 0; - } else { - if (g3 > 255) - data2[pix1] = 255; - else - data2[pix1] = g3; - } - if ( ((b3 = 2*(b2 = data2[pix2 = pix + 2])+ data[pix2]-256) - < 0) || (b2 < 128 && b3 < 0)) { - data2[pix2] = 0; - } else { - if (b3 > 255) - data2[pix2] = 255; - else - data2[pix2] = b3; - } - } - break; - - case 'vivid-light' : - while (p--) { - if ((r2 = data2[pix -= 4]) < 128) { - if (r2) { - if ((r3 = 255 - ((255 - data[pix]) << 8) / - (2 * r2)) < 0) - data2[pix] = 0; - else - data2[pix] = r3; - } else { - data2[pix] = 0; - } - } else if ((r3 = (r4 = 2 * r2 - 256)) < 255) { - if ((r3 = (data[pix] << 8) / (255 - r4)) > 255) - data2[pix] = 255; - else - data2[pix] = r3; - } else { - if (r3 < 0) - data2[pix] = 0; - else - data2[pix] = r3; - } - - if ((g2 = data2[pix1 = pix + 1]) < 128) { - if (g2) { - if ((g3 = 255 - ((255 - data[pix1]) << 8) / - (2 * g2)) < 0) - data2[pix1] = 0; - else - data2[pix1] = g3; - } else { - data2[pix1] = 0; - } - } else if ((g3 = (g4 = 2 * g2 - 256)) < 255) { - if ((g3 = (data[pix1] << 8) / (255 - g4)) > 255) - data2[pix1] = 255; - else - data2[pix1] = g3; - } else { - if (g3 < 0) - data2[pix1] = 0; - else - data2[pix1] = g3; - } - - if ((b2 = data2[pix2 = pix + 2]) < 128) { - if (b2) { - if ((b3 = 255 - ((255 - data[pix2]) << 8) / - (2 * b2)) < 0) - data2[pix2] = 0; - else - data2[pix2] = b3; - } else { - data2[pix2] = 0; - } - } else if ((b3 = (b4 = 2 * b2 - 256)) < 255) { - if ((b3 = (data[pix2] << 8) / (255 - b4)) > 255) - data2[pix2] = 255; - else - data2[pix2] = b3; - } else { - if (b3 < 0) - data2[pix2] = 0; - else - data2[pix2] = b3; - } - } - break; - - case 'pin-light' : - while (p--) { - if ((r2 = data2[pix -= 4]) < 128) - if ((r1 = data[pix]) < (r4 = 2 * r2)) - data2[pix] = r1; - else - data2[pix] = r4; - else - if ((r1 = data[pix]) > (r4 = 2 * r2 - 256)) - data2[pix] = r1; - else - data2[pix] = r4; - - if ((g2 = data2[pix1 = pix + 1]) < 128) - if ((g1 = data[pix1]) < (g4 = 2 * g2)) - data2[pix1] = g1; - else - data2[pix1] = g4; - else - if ((g1 = data[pix1]) > (g4 = 2 * g2 - 256)) - data2[pix1] = g1; - else - data2[pix1] = g4; - - if ((r2 = data2[pix2 = pix + 2]) < 128) - if ((r1 = data[pix2]) < (r4 = 2 * r2)) - data2[pix2] = r1; - else - data2[pix2] = r4; - else - if ((r1 = data[pix2]) > (r4 = 2 * r2 - 256)) - data2[pix2] = r1; - else - data2[pix2] = r4; - } - break; - - case 'hard-mix' : - while (p--) { - if ((r2 = data2[pix -= 4]) < 128) - if (255 - ((255 - data[pix]) << 8) / (2 * r2) < 128 - || r2 == 0) - data2[pix] = 0; - else - data2[pix] = 255; - else if ((r4 = 2 * r2 - 256) < 255 - && (data[pix] << 8) / (255 - r4) < 128) - data2[pix] = 0; - else - data2[pix] = 255; - - if ((g2 = data2[pix1 = pix + 1]) < 128) - if (255 - ((255 - data[pix1]) << 8) / (2 * g2) < 128 - || g2 == 0) - data2[pix1] = 0; - else - data2[pix1] = 255; - else if ((g4 = 2 * g2 - 256) < 255 - && (data[pix1] << 8) / (255 - g4) < 128) - data2[pix1] = 0; - else - data2[pix1] = 255; - - if ((b2 = data2[pix2 = pix + 2]) < 128) - if (255 - ((255 - data[pix2]) << 8) / (2 * b2) < 128 - || b2 == 0) - data2[pix2] = 0; - else - data2[pix2] = 255; - else if ((b4 = 2 * b2 - 256) < 255 - && (data[pix2] << 8) / (255 - b4) < 128) - data2[pix2] = 0; - else - data2[pix2] = 255; - } - break; + // ******* UNSUPPORTED + default: + dst[px] = dst[px+3] = 255; + dst[px+1] = px%8==0 ? 255 : 0; + dst[px+2] = px%8==0 ? 0 : 255; + } } - - itemContext.putImageData(dataDesc2, 0, 0); - - documentContext.drawImage( - itemCanvas, - 0, 0, - width, height, - left, top, - width, height - ); + documentContext.putImageData(dstD, left, top); CanvasProvider.returnCanvas(itemCanvas); } }; \ No newline at end of file From f79e6a045458c4b46d3321c960acd6607144236e Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Fri, 25 Feb 2011 19:38:55 +0100 Subject: [PATCH 29/40] PlacedSymbol & Raster: fix transformContent and support BlendMode. --- src/item/PlacedSymbol.js | 14 +++++++++++--- src/item/Raster.js | 13 +++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index f8a5de60..64962f0a 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -32,6 +32,14 @@ PlacedSymbol = Item.extend({ bounds.y = coords[1]; bounds.width = coords[2] - coords[0]; bounds.height = coords[3] - coords[1]; + if (bounds.width < 0) { + bounds.x = coords[2]; + bounds.width = -bounds.width; + } + if (bounds.height < 0) { + bounds.y = coords[3]; + bounds.height = bounds.height; + } }, getBounds: function() { @@ -39,14 +47,14 @@ PlacedSymbol = Item.extend({ }, draw: function(ctx, param) { - // TODO: BlendMode isn't working yet for PlacedSymbols. if(this.blendMode != 'normal' && !param.ignoreBlendMode) { BlendMode.process(ctx, this, param); } else { - param.ignoreBlendMode = false; // TODO: we need to preserve strokewidth, but still transform the fill ctx.save(); - this.matrix.applyToContext(ctx); + if(param.ignoreBlendMode !== true) + this.matrix.applyToContext(ctx); + param.ignoreBlendMode = false; this.symbol.definition.draw(ctx, param); ctx.restore(); } diff --git a/src/item/Raster.js b/src/item/Raster.js index ce75016b..57decad6 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -159,6 +159,14 @@ Raster = Item.extend({ bounds.y = coords[1]; bounds.width = coords[2] - coords[0]; bounds.height = coords[3] - coords[1]; + if (bounds.width < 0) { + bounds.x = coords[2]; + bounds.width = -bounds.width; + } + if (bounds.height < 0) { + bounds.y = coords[3]; + bounds.height = bounds.height; + } }, getBounds: function() { @@ -169,12 +177,13 @@ Raster = Item.extend({ if(this.blendMode != 'normal' && !param.ignoreBlendMode) { BlendMode.process(ctx, this, param); } else { - param.ignoreBlendMode = false; ctx.save(); - this.matrix.applyToContext(ctx); + 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() { From bebc8bc8cca2179f4ae8f3b39e605d0c7c979f01 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 00:58:54 +0100 Subject: [PATCH 30/40] Another attempt at fixing Item#transformContent. The bounds are correctly calculated now, but Item#rotate rotates the items in the wrong direction and rotating by a point doesn't work well. --- src/item/PlacedSymbol.js | 42 +++++++++++++++++++++++++--------------- src/item/Raster.js | 42 +++++++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 64962f0a..ceef9d18 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -23,23 +23,33 @@ PlacedSymbol = Item.extend({ }, 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]; - if (bounds.width < 0) { - bounds.x = coords[2]; - bounds.width = -bounds.width; - } - if (bounds.height < 0) { - bounds.y = coords[3]; - bounds.height = bounds.height; - } + 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() { diff --git a/src/item/Raster.js b/src/item/Raster.js index 57decad6..78c84bd1 100644 --- a/src/item/Raster.js +++ b/src/item/Raster.js @@ -150,23 +150,33 @@ Raster = Item.extend({ }, 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]; - if (bounds.width < 0) { - bounds.x = coords[2]; - bounds.width = -bounds.width; - } - if (bounds.height < 0) { - bounds.y = coords[3]; - bounds.height = bounds.height; - } + 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() { From 8828ed7e2542273da5bbcd4d17c004d7fe7c7375 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 01:02:17 +0100 Subject: [PATCH 31/40] CompoundPath - check for this.blendMode != 'normal'. --- src/path/CompoundPath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index e20a262e..1833e942 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -23,7 +23,7 @@ CompoundPath = PathItem.extend(new function() { if(!this.visible) return; if (this.children.length) { - if(this.blendMode && !param.ignoreBlendMode) { + if(this.blendMode != 'normal' && !param.ignoreBlendMode) { BlendMode.process(ctx, this, param); } else { var firstChild = this.children[0]; From 71bf8dba88607d8c8474a47baf5f26fccdb4a1ed Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 02:04:18 +0100 Subject: [PATCH 32/40] Item: remove logging statement. --- src/item/Item.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/item/Item.js b/src/item/Item.js index 5af9a6c8..987718b8 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -475,7 +475,6 @@ Item = Base.extend({ while(parent) { // Find group parents. Check for parent.parent, since don't want // top level layers, because they also inherit from Group - console.log(parent.parent); if(parent.parent && (parent instanceof Group || parent instanceof CompoundPath) && item.isDescendant(parent)) From 51f4ee718c0fc144cda1fd4a8752758243ea0a42 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 12:44:09 +0100 Subject: [PATCH 33/40] As in Scriptographer, we don't worry if there wasn't a moveTo before the first lineTo. --- src/path/Path.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path/Path.js b/src/path/Path.js index 91110f62..ce1933cb 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -177,7 +177,7 @@ Path = PathItem.extend({ lineTo: function() { var segment = Segment.read(arguments); - if (segment && this._segments.length) + if (segment) this.addSegment(segment); }, From 22bccf3a31e24b408a7a80102def9ac057db1fcb Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 13:00:55 +0100 Subject: [PATCH 34/40] Tool: Fix issue with onMouseMove being called while the user is dragging the mouse. --- src/tool/Tool.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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); }, From 422b8911b872940c6ab1bfcb152856c8fa0bd313 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 13:14:58 +0100 Subject: [PATCH 35/40] Fix PlacedSymbol#transformContent. --- src/item/PlacedSymbol.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index ceef9d18..788d0e91 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -19,7 +19,10 @@ 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) { From fd2926bc45111921effa5510fae931ddcf00a4c9 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 13:48:31 +0100 Subject: [PATCH 36/40] Implement tests for PlacedSymbol, which fail because of lack of Item#strokeBounds and problems with PlacedSymbol#rotate. --- test/index.html | 3 +++ test/tests/Path.js | 1 + test/tests/PlacedSymbol.js | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 test/tests/PlacedSymbol.js diff --git a/test/index.html b/test/index.html index 81ad76e7..e2b35128 100644 --- a/test/index.html +++ b/test/index.html @@ -14,9 +14,11 @@ + + @@ -45,6 +47,7 @@ +

QUnit Test Suite

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/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 From 8cf2f54d5f6d2b4fff8613db43083d6289a5dde1 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 13:58:00 +0100 Subject: [PATCH 37/40] Add test to Path_Bounds.js which tests bounds and segments after rotation. --- test/tests/Path_Bounds.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 1a6c5c8c6f520dc81ec7da136b7f5c0a4afcbc33 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 14:34:01 +0100 Subject: [PATCH 38/40] Doc: Change comment about using canvas.width = canvas.width instead of context.clearRect. --- src/document/Doc.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/document/Doc.js b/src/document/Doc.js index 83c37d33..c62667a0 100644 --- a/src/document/Doc.js +++ b/src/document/Doc.js @@ -29,9 +29,10 @@ 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, {}); } From 9e6fd5a7e74393bac6186fb61d2feca22c995cf8 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 15:29:51 +0100 Subject: [PATCH 39/40] Speed up CanvasProvider. --- src/util/CanvasProvider.js | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/util/CanvasProvider.js b/src/util/CanvasProvider.js index d7fa9793..0622bfbd 100644 --- a/src/util/CanvasProvider.js +++ b/src/util/CanvasProvider.js @@ -1,17 +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(size) { - var canvas = this.canvases.length - ? this.canvases.pop() - : document.createElement('canvas'); - canvas.width = size.width; - canvas.height = size.height; - return canvas; + 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) { - // reset canvas: - canvas.width = canvas.width; this.canvases.push(canvas); } -}; +}; \ No newline at end of file From 461aa87211a667be297e24d998d5fde5fc272ad8 Mon Sep 17 00:00:00 2001 From: Jonathan Puckey Date: Sat, 26 Feb 2011 15:55:58 +0100 Subject: [PATCH 40/40] Group: add todo. --- src/item/Group.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/item/Group.js b/src/item/Group.js index c5e92c5e..de88522e 100644 --- a/src/item/Group.js +++ b/src/item/Group.js @@ -24,6 +24,7 @@ Group = Item.extend({ param.ignoreBlendMode = false; if (this.opacity < 1) { var originalCtx = ctx; + // TODO: use strokeBounds for this, when implemented: tempCanvas = CanvasProvider.getCanvas(this.document.size); ctx = tempCanvas.getContext('2d'); }