Implement BlendMode class.

This commit is contained in:
Jonathan Puckey 2011-02-25 12:46:45 +01:00
parent d9b75a7232
commit c9d04d33f8
8 changed files with 625 additions and 91 deletions

View file

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

494
src/item/BlendMode.js Normal file
View file

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

View file

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

View file

@ -87,6 +87,20 @@ Item = Base.extend({
opacity: 1,
/**
* The blend mode of the item.
*
* Sample code:
* <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';
* </code>
*/
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

View file

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

View file

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

View file

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

View file

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