diff --git a/src/document/Doc.js b/src/document/Doc.js index 62a6a864..8528fc3f 100644 --- a/src/document/Doc.js +++ b/src/document/Doc.js @@ -36,19 +36,6 @@ Doc = Base.extend({ }, redraw: function() { - if (this.canvas) { - // Initial tests conclude that clearing the canvas using clearRect - // is always faster than setting canvas.width = canvas.width - // http://jsperf.com/clearrect-vs-setting-width/7 - var view = this.activeView; - var bounds = view.bounds; - this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1); - this.ctx.save(); - view.matrix.applyToContext(this.ctx, true); - for (var i = 0, l = this.layers.length; i < l; i++) { - this.layers[i].draw(this.ctx, {}); - } - this.ctx.restore(); - } + this._draw(); } }); diff --git a/src/item/BlendMode.js b/src/item/BlendMode.js index 508f910a..336f39e9 100644 --- a/src/item/BlendMode.js +++ b/src/item/BlendMode.js @@ -27,40 +27,16 @@ BlendMode = { // 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; - if (!itemBounds) - return; - var top = Math.floor(itemBounds.top); - var left = Math.floor(itemBounds.left); - var size = itemBounds.size.ceil().add(1, 1); - var width = size.width; - var height = size.height; - - var itemCanvas = CanvasProvider.getCanvas(size); - var itemContext = itemCanvas.getContext('2d'); - itemContext.save(); - if (item.matrix) { - var matrix = item.matrix.clone(); - var transMatrix = Matrix.getTranslateInstance(-left, -top); - matrix.preConcatenate(transMatrix); - // TODO: Profiling shows this as a hotspot - matrix.applyToContext(itemContext); - } else { - itemContext.translate(-itemBounds.left, -itemBounds.top); - } - param.ignoreBlendMode = true; - item.draw(itemContext, param); - - var dstD = documentContext.getImageData( - left, top, - width, height + process: function(blendMode, sourceContext, destContext, opacity, offset) { + var sourceCanvas = sourceContext.canvas; + var dstD = destContext.getImageData( + offset.x, offset.y, + sourceCanvas.width, sourceCanvas.height ); - - var srcD = itemContext.getImageData( + + var srcD = sourceContext.getImageData( 0, 0, - width, height + sourceCanvas.width, sourceCanvas.height ); var src = srcD.data; @@ -70,7 +46,7 @@ BlendMode = { var demultiply; for (var px=0;pxdRA ? dRA : sRA) * demultiply; dst[px+1] = (sGA>dGA ? dGA : sGA) * demultiply; dst[px+2] = (sBA>dBA ? dBA : sBA) * demultiply; break; - + case 'lighten': case 'lighter': dst[px ] = (sRA 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 (!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(); - } - } - } }, new function() { // Inject methods that require scoped privates diff --git a/src/util/CanvasDraw.js b/src/util/CanvasDraw.js new file mode 100644 index 00000000..391c483e --- /dev/null +++ b/src/util/CanvasDraw.js @@ -0,0 +1,203 @@ +new function() { + // TODO: Implement DocumentView into the drawing + // TODO: Optimize temporary canvas drawing to ignore parts that are + // outside of the visible view. + function draw(context, item, param) { + if (!item.visible || item.opacity == 0) + return; + + var tempCanvas, parentContext; + // If the item has a blendMode or is defining an opacity, draw it on + // a temporary canvas first and composite the canvas afterwards. + // Paths with an opacity < 1 that both define a fillColor + // and strokeColor also need to be drawn on a temporary canvas first, + // since otherwise their stroke is drawn half transparent over their + // fill. + if (item.blendMode !== 'normal' + || item.opacity < 1 + && !(item.segments && (!item.fillColor || !item.strokeColor)) + ) { + var bounds = item.strokeBounds; + if (!item.bounds.width || !item.bounds.height) + return; + + // Floor the offset and ceil the size, so we don't cut off any + // antialiased pixels when drawing onto the temporary canvas. + var itemOffset = bounds.topLeft.floor(); + var size = bounds.size.ceil().add(1, 1); + tempCanvas = CanvasProvider.getCanvas(size); + + // Save the parent context, so we can draw onto it later + parentContext = context; + + // Set context to the context of the temporary canvas, + // so we draw onto it, instead of the parentContext + context = tempCanvas.getContext('2d'); + context.save(); + + // Translate the context so the topLeft of the item is at (0, 0) + // on the temporary canvas. + context.translate(-itemOffset.x, -itemOffset.y); + } + + item._draw(context, { + offset: itemOffset || param.offset, + compound: param.compound + }); + + // If we created a temporary canvas before, composite it onto the + // parent canvas: + if (tempCanvas) { + + // Restore the temporary canvas to its state before the translation + // matrix was applied above. + context.restore(); + + // If the item has a blendMode, use BlendMode#process to composite + // its canvas on the parentCanvas. + if (item.blendMode != 'normal') { + // The pixel offset of the temporary canvas to the parent + // canvas. + var pixelOffset = itemOffset.subtract(param.offset); + BlendMode.process(item.blendMode, context, parentContext, + item.opacity, pixelOffset); + } else { + // Otherwise we just need to set the globalAlpha before drawing + // the temporary canvas on the parent canvas. + parentContext.save(); + parentContext.globalAlpha = item.opacity; + parentContext.drawImage(tempCanvas, itemOffset.x, itemOffset.y); + parentContext.restore(); + } + + // Return the temporary canvas, so it can be reused + CanvasProvider.returnCanvas(tempCanvas); + } + } + + Doc.inject({ + _draw: function() { + if (this.canvas) { + // Initial tests conclude that clearing the canvas using clearRect + // is always faster than setting canvas.width = canvas.width + // http://jsperf.com/clearrect-vs-setting-width/7 + this.ctx.clearRect(0, 0, this.size.width + 1, this.size.height + 1); + this.ctx.save(); + + for (var i = 0, l = this.layers.length; i < l; i++) { + draw(this.ctx, this.layers[i], { offset: new Point(0, 0)}); + } + this.ctx.restore(); + } + } + }); + + Group.inject({ + _draw: function(ctx, param) { + for (var i = 0, l = this.children.length; i < l; i++) { + draw(ctx, this.children[i], param); + if (this.clipped && i == 0) + ctx.clip(); + } + } + }); + + PlacedSymbol.inject({ + _draw: function(ctx, param) { + // TODO: we need to preserve strokewidth + ctx.save(); + this.matrix.applyToContext(ctx); + draw(ctx, this.symbol.definition, param); + ctx.restore(); + } + }); + + Raster.inject({ + _draw: function(ctx, param) { + ctx.save(); + this.matrix.applyToContext(ctx); + ctx.drawImage(this._canvas || this._image, + -this.size.width / 2, -this.size.height / 2); + ctx.restore(); + } + }); + + Path.inject({ + _draw: function(ctx, param) { + 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 { + 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; + } + 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 the path is part of a compound path or doesn't have a fill or + // stroke, there is no need to continue. + if (!param.compound && (this.fillColor || this.strokeColor)) { + this.setCtxStyles(ctx); + ctx.save(); + // If the path only defines a strokeColor or a fillColor, + // draw it directly with the globalAlpha set, otherwise + // we will do it later when we composite the temporary canvas. + if (!this.fillColor || !this.strokeColor) + 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(); + } + } + }); + + CompoundPath.inject({ + _draw: function(ctx, param) { + var firstChild = this.children[0]; + ctx.beginPath(); + param.compound = true; + for (var i = 0, l = this.children.length; i < l; i++) { + draw(ctx, this.children[i], param); + } + firstChild.setCtxStyles(ctx); + if (firstChild.fillColor) { + ctx.fillStyle = firstChild.fillColor.getCssString(); + ctx.fill(); + } + if (firstChild.strokeColor) { + ctx.strokeStyle = firstChild.strokeColor.getCssString(); + ctx.stroke(); + } + } + }); +}; \ No newline at end of file