paper.js/src/item/Raster.js

415 lines
12 KiB
JavaScript
Raw Normal View History

2011-03-06 19:50:44 -05:00
/*
* Paper.js
*
2011-03-06 19:50:44 -05:00
* This file is part of Paper.js, a JavaScript Vector Graphics Library,
* based on Scriptographer.org and designed to be largely API compatible.
2011-03-07 20:41:50 -05:00
* http://paperjs.org/
2011-03-06 19:50:44 -05:00
* http://scriptographer.org/
*
2011-03-06 19:50:44 -05:00
* Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
* http://lehni.org/ & http://jonathanpuckey.com/
*
2011-07-01 06:17:45 -04:00
* Distributed under the MIT license. See LICENSE file for details.
*
2011-03-07 20:41:50 -05:00
* All rights reserved.
2011-03-06 19:50:44 -05:00
*/
/**
* @name Raster
*
* @class The Raster item represents an image in a Paper.js project.
*
* @extends PlacedItem
*/
var Raster = this.Raster = PlacedItem.extend(/** @lends Raster# */{
// Raster doesn't make the distinction between the different bounds,
// so use the same name for all of them
_boundsType: 'bounds',
2011-06-01 05:49:43 -04:00
// TODO: Implement url / type, width, height.
// TODO: Have PlacedSymbol & Raster inherit from a shared class?
// DOCS: Document Raster constructor.
2011-05-23 10:09:04 -04:00
/**
* Creates a new raster item and places it in the active layer.
*
2011-05-23 10:09:04 -04:00
* @param {HTMLImageElement|Canvas|string} [object]
*/
initialize: function(object, pointOrMatrix) {
this.base(pointOrMatrix);
if (object.getContext) {
this.setCanvas(object);
} else {
/*#*/ if (options.browser) {
// If it's a string, get the element with this id first.
if (typeof object === 'string')
object = document.getElementById(object);
2011-08-21 10:38:06 -04:00
/*#*/ } else if (options.server) {
// If we're running on the server and it's a string,
// load it from disk:
if (typeof object === 'string') {
// TODO: load images async
var data = fs.readFileSync(object);
object = new Image();
object.src = data;
}
/*#*/ } // options.server
this.setImage(object);
2011-02-21 12:43:56 -05:00
}
},
clone: function() {
2011-05-21 06:50:02 -04:00
var image = this._image;
if (!image) {
// If the Raster contains a Canvas object, we need to create
// a new one and draw this raster's canvas on it.
image = CanvasProvider.getCanvas(this._size);
image.getContext('2d').drawImage(this._canvas, 0, 0);
}
2011-05-21 12:02:09 -04:00
var copy = new Raster(image);
return this._clone(copy);
},
/**
2011-05-23 10:09:04 -04:00
* The size of the raster in pixels.
*
2011-05-23 10:09:04 -04:00
* @type Size
* @bean
*/
getSize: function() {
return this._size;
},
setSize: function() {
var size = Size.read(arguments),
// Get reference to image before changing canvas
image = this.getImage();
2011-03-04 21:40:38 -05:00
// Setting canvas internally sets _size
this.setCanvas(CanvasProvider.getCanvas(size));
2011-03-04 21:40:38 -05:00
// Draw image back onto new canvas
this.getContext(true).drawImage(image, 0, 0, size.width, size.height);
},
2011-02-21 12:43:56 -05:00
/**
* The width of the raster in pixels.
*
* @type Number
2011-05-23 10:09:04 -04:00
* @bean
2011-02-21 12:43:56 -05:00
*/
getWidth: function() {
return this._size.width;
2011-02-21 12:43:56 -05:00
},
2011-02-21 12:43:56 -05:00
/**
* The height of the raster in pixels.
*
* @type Number
2011-05-23 10:09:04 -04:00
* @bean
2011-02-21 12:43:56 -05:00
*/
getHeight: function() {
return this._size.height;
2011-02-21 12:43:56 -05:00
},
/**
2011-06-08 13:47:34 -04:00
* Pixels per inch of the raster at its current size.
*
2011-05-23 10:09:04 -04:00
* @type Size
* @bean
*/
getPpi: function() {
var matrix = this._matrix,
orig = new Point(0, 0).transform(matrix),
u = new Point(1, 0).transform(matrix).subtract(orig),
v = new Point(0, 1).transform(matrix).subtract(orig);
return Size.create(
2011-03-04 21:40:38 -05:00
72 / u.getLength(),
72 / v.getLength()
);
},
2011-05-23 10:09:04 -04:00
/**
* The Canvas 2d drawing context of the raster.
*
2011-05-23 10:09:04 -04:00
* @type Context
* @bean
*/
2011-12-18 10:56:18 -05:00
getContext: function(/* notifyChange */) {
if (!this._context)
2011-03-04 21:40:38 -05:00
this._context = this.getCanvas().getContext('2d');
// Support a hidden parameter that indicates if the context will be used
// to modify the Raster object. We can notify such changes ahead since
// they are only used afterwards for redrawing.
if (arguments[0])
this._changed(Change.PIXELS);
2011-03-04 21:40:38 -05:00
return this._context;
},
setContext: function(context) {
this._context = context;
},
getCanvas: function() {
if (!this._canvas) {
this._canvas = CanvasProvider.getCanvas(this._size);
if (this._image)
this.getContext(true).drawImage(this._image, 0, 0);
}
return this._canvas;
},
setCanvas: function(canvas) {
if (this._canvas)
CanvasProvider.returnCanvas(this._canvas);
this._canvas = canvas;
this._size = Size.create(canvas.width, canvas.height);
this._image = null;
this._context = null;
2011-12-18 10:56:18 -05:00
this._changed(Change.GEOMETRY | Change.PIXELS);
},
2011-05-23 10:09:04 -04:00
/**
* The HTMLImageElement or Canvas of the raster.
*
2011-05-23 10:09:04 -04:00
* @type HTMLImageElement|Canvas
* @bean
*/
getImage: function() {
return this._image || this.getCanvas();
},
2011-06-01 05:49:43 -04:00
// TODO: Support string id of image element.
setImage: function(image) {
if (this._canvas)
CanvasProvider.returnCanvas(this._canvas);
this._image = image;
2011-08-21 10:38:06 -04:00
/*#*/ if (options.browser) {
this._size = Size.create(image.naturalWidth, image.naturalHeight);
2011-08-21 10:38:06 -04:00
/*#*/ } else if (options.server) {
this._size = Size.create(image.width, image.height);
2011-08-21 10:38:06 -04:00
/*#*/ } // options.server
this._canvas = null;
this._context = null;
this._changed(Change.GEOMETRY);
},
2011-05-23 10:09:04 -04:00
// DOCS: document Raster#getSubImage
/**
2011-05-23 10:19:37 -04:00
* @param {Rectangle} rect the boundaries of the sub image in pixel
* coordinates
*
2011-05-23 10:09:04 -04:00
* @return {Canvas}
*/
getSubImage: function(rect) {
rect = Rectangle.read(arguments);
var canvas = CanvasProvider.getCanvas(rect.getSize());
canvas.getContext('2d').drawImage(this.getCanvas(), rect.x, rect.y,
canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
return canvas;
},
2011-05-23 10:09:04 -04:00
/**
2011-05-23 10:19:37 -04:00
* Draws an image on the raster.
*
2011-05-23 10:09:04 -04:00
* @param {HTMLImageELement|Canvas} image
2011-05-23 10:19:37 -04:00
* @param {Point} point the offset of the image as a point in pixel
* coordinates
2011-05-23 10:09:04 -04:00
*/
drawImage: function(image, point) {
point = Point.read(arguments, 1);
this.getContext(true).drawImage(image, point.x, point.y);
},
2011-05-19 09:43:23 -04:00
/**
* Calculates the average color of the image within the given path,
* rectangle or point. This can be used for creating raster image
* effects.
*
2011-05-23 10:09:04 -04:00
* @param {Path|Rectangle|Point} object
* @return {RgbColor} the average color contained in the area covered by the
2011-05-19 09:43:23 -04:00
* specified path, rectangle or point.
*/
getAverageColor: function(object) {
if (!object)
object = this.getBounds();
var bounds, path;
if (object instanceof PathItem) {
2011-06-01 05:49:43 -04:00
// TODO: What if the path is smaller than 1 px?
// TODO: How about rounding of bounds.size?
2011-05-19 09:43:23 -04:00
path = object;
bounds = object.getBounds();
} else if (object.width) {
bounds = new Rectangle(object);
} else if (object.x) {
// Create a rectangle of 1px size around the specified coordinates
2011-05-19 09:43:23 -04:00
bounds = Rectangle.create(object.x - 0.5, object.y - 0.5, 1, 1);
}
// Use a sample size of max 32 x 32 pixels, into which the path is
// scaled as a clipping path, and then the actual image is drawn in and
// sampled.
2011-05-19 13:47:49 -04:00
var sampleSize = 32,
width = Math.min(bounds.width, sampleSize),
height = Math.min(bounds.height, sampleSize);
// Reuse the same sample context for speed. Memory consumption is low
// since it's only 32 x 32 pixels.
2011-05-19 13:47:49 -04:00
var ctx = Raster._sampleContext;
if (!ctx) {
2011-05-19 09:43:23 -04:00
ctx = Raster._sampleContext = CanvasProvider.getCanvas(
sampleSize.clone()).getContext('2d');
2011-05-19 09:43:23 -04:00
} else {
// Clear the sample canvas:
ctx.clearRect(0, 0, sampleSize, sampleSize);
}
ctx.save();
// Scale the context so that the bounds ends up at the given sample size
2011-05-19 13:47:49 -04:00
ctx.scale(width / bounds.width, height / bounds.height);
ctx.translate(-bounds.x, -bounds.y);
2011-05-19 09:43:23 -04:00
// If a path was passed, draw it as a clipping mask:
2011-06-04 10:28:06 -04:00
if (path)
path.draw(ctx, { clip: true });
// Now draw the image clipped into it.
this._matrix.applyToContext(ctx);
2011-05-19 09:43:23 -04:00
ctx.drawImage(this._canvas || this._image,
-this._size.width / 2, -this._size.height / 2);
ctx.restore();
// Get pixel data from the context and calculate the average color value
// from it, taking alpha into account.
2011-05-19 09:43:23 -04:00
var pixels = ctx.getImageData(0.5, 0.5, Math.ceil(width),
Math.ceil(height)).data,
channels = [0, 0, 0],
total = 0;
for (var i = 0, l = pixels.length; i < l; i += 4) {
var alpha = pixels[i + 3];
total += alpha;
alpha /= 255;
channels[0] += pixels[i] * alpha;
channels[1] += pixels[i + 1] * alpha;
channels[2] += pixels[i + 2] * alpha;
}
for (var i = 0; i < 3; i++)
channels[i] /= total;
return total ? Color.read(channels) : null;
},
/**
* {@grouptitle Pixels}
* Gets the color of a pixel in the raster.
*
* @name Raster#getPixel
* @function
* @param x the x offset of the pixel in pixel coordinates
* @param y the y offset of the pixel in pixel coordinates
* @return {RgbColor} the color of the pixel
*/
/**
* Gets the color of a pixel in the raster.
*
* @name Raster#getPixel
* @function
* @param point the offset of the pixel as a point in pixel coordinates
* @return {RgbColor} the color of the pixel
*/
getPixel: function(point) {
point = Point.read(arguments);
var pixels = this.getContext().getImageData(point.x, point.y, 1, 1).data,
channels = new Array(4);
for (var i = 0; i < 4; i++)
channels[i] = pixels[i] / 255;
return RgbColor.read(channels);
},
/**
* Sets the color of the specified pixel to the specified color.
*
* @name Raster#setPixel
* @function
* @param x the x offset of the pixel in pixel coordinates
* @param y the y offset of the pixel in pixel coordinates
* @param color the color that the pixel will be set to
*/
/**
* Sets the color of the specified pixel to the specified color.
*
* @name Raster#setPixel
* @function
* @param point the offset of the pixel as a point in pixel coordinates
* @param color the color that the pixel will be set to
*/
setPixel: function(point, color) {
var hasPoint = arguments.length == 2;
point = Point.read(arguments, 0, hasPoint ? 1 : 2);
color = Color.read(arguments, hasPoint ? 1 : 2);
var ctx = this.getContext(true),
imageData = ctx.createImageData(1, 1),
alpha = color.getAlpha();
imageData.data[0] = color.getRed() * 255;
imageData.data[1] = color.getGreen() * 255;
imageData.data[2] = color.getBlue() * 255;
imageData.data[3] = alpha != null ? alpha * 255 : 255;
ctx.putImageData(imageData, point.x, point.y);
},
2011-05-23 10:19:37 -04:00
// DOCS: document Raster#createData
/**
* {@grouptitle Image Data}
2011-05-23 10:19:37 -04:00
* @param {Size} size
* @return {ImageData}
*/
2011-03-09 13:17:12 -05:00
createData: function(size) {
size = Size.read(arguments);
return this.getContext().createImageData(size.width, size.height);
},
// TODO: Rename to #get/setImageData, as it will conflict with Item#getData
2011-05-23 10:19:37 -04:00
// DOCS: document Raster#getData
/**
* @param {Rectangle} rect
* @return {ImageData}
*/
getData: function(rect) {
rect = Rectangle.read(arguments);
if (rect.isEmpty())
rect = new Rectangle(this.getSize());
return this.getContext().getImageData(rect.x, rect.y,
rect.width, rect.height);
2011-03-09 13:17:12 -05:00
},
2011-05-23 10:19:37 -04:00
// DOCS: document Raster#setData
/**
* @param {ImageData} data
* @param {Point} point
* @return {ImageData}
*/
setData: function(data, point) {
point = Point.read(arguments, 1);
this.getContext(true).putImageData(data, point.x, point.y);
2011-03-09 13:17:12 -05:00
},
_getBounds: function(type, matrix) {
var rect = new Rectangle(this._size).setCenter(0, 0);
return matrix ? matrix._transformBounds(rect) : rect;
},
_hitTest: function(point, options) {
point = this._matrix._inverseTransform(point);
if (point.isInside(new Rectangle(this._size).setCenter(0, 0))) {
var that = this;
return new HitResult('pixel', that, {
offset: point.add(that._size.divide(2)).round(),
// Becomes HitResult#color
getColor: function() {
return that.getPixel(this.offset);
}
});
}
},
draw: function(ctx, param) {
ctx.drawImage(this._canvas || this._image,
-this._size.width / 2, -this._size.height / 2);
},
drawSelected: function(ctx, matrix) {
Item.drawSelectedBounds(new Rectangle(this._size).setCenter(0, 0), ctx,
matrix);
2011-02-21 12:43:56 -05:00
}
2011-03-03 11:32:55 -05:00
});