Adding simple filters to ChromeOS Image Editor.

Added Autofix, Exposure, Blur, Sharpen.
Also improved appearance a bit by avoiding non-integer coordinates
and fixed W3C compatibility (a standalone demo now works on Opera
and Firefox allowing for performance comparisons).

BUG=
TEST=

Review URL: http://codereview.chromium.org/7552035

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@96384 0039d316-1c4b-4281-b951-d872f2087c98
parent d30db2de
......@@ -145,7 +145,7 @@ ImageBuffer.prototype.onClick = function (x, y) {
/**
* Searches for a drag handler in the descending Z-order.
* @return {Function} A closure to be called on mouse drag.
* @return {function(number,number)} A function to be called on mouse drag.
*/
ImageBuffer.prototype.getDragHandler = function (x, y) {
for (var i = this.overlays_.length - 1; i >= 0; i--) {
......@@ -191,7 +191,8 @@ ImageBuffer.Margin.prototype.draw = function(context) {
Rect.fillBetween(context,
this.viewport_.getImageBoundsOnScreen(),
this.viewport_.getScreenBounds());
Rect.stroke(context, this.viewport_.getImageBoundsOnScreen());
Rect.outline(context, this.viewport_.getImageBoundsOnScreen());
context.restore();
};
......@@ -252,8 +253,8 @@ ImageBuffer.Content.prototype.getCanvas = function() { return this.canvas_ };
* If the logical width/height are supplied they override the canvas dimensions
* and the canvas contents is scaled when displayed.
* @param {HTMLCanvasElement} canvas
* @param {Number} opt_width Logical width (=canvas.width by default)
* @param {Number} opt_height Logical height (=canvas.height by default)
* @param {number} opt_width Logical width (=canvas.width by default)
* @param {number} opt_height Logical height (=canvas.height by default)
*/
ImageBuffer.Content.prototype.setCanvas = function(
canvas, opt_width, opt_height) {
......@@ -275,8 +276,8 @@ ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) {
};
/**
* @param {Number} opt_width Width of the copy, original width by default.
* @param {Number} opt_height Height of the copy, original height by default.
* @param {number} opt_width Width of the copy, original width by default.
* @param {number} opt_height Height of the copy, original height by default.
* @return {HTMLCanvasElement} A new canvas with a copy of the content.
*/
ImageBuffer.Content.prototype.copyCanvas = function (opt_width, opt_height) {
......@@ -342,27 +343,23 @@ ImageBuffer.Overview.prototype.update = function() {
this.contentGeneration_ = this.content_.getCacheGeneration();
var aspect = imageBounds.width / imageBounds.height;
if (aspect > 1) {
this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE,
ImageBuffer.Overview.MAX_SIZE / aspect);
} else {
this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect,
ImageBuffer.Overview.MAX_SIZE);
}
this.canvas_ =
this.content_.copyCanvas(this.bounds_.width, this.bounds_.height);
this.canvas_ = this.content_.copyCanvas(
ImageBuffer.Overview.MAX_SIZE * Math.min(aspect, 1),
ImageBuffer.Overview.MAX_SIZE / Math.max(aspect, 1));
}
this.bounds_ = null;
this.clipped_ = null;
if (this.viewport_.isClipped()) {
var screenBounds = this.viewport_.getScreenBounds();
this.bounds_ = this.bounds_.moveTo(
screenBounds.width - ImageBuffer.Overview.RIGHT - this.bounds_.width,
screenBounds.height - ImageBuffer.Overview.BOTTOM -
this.bounds_.height);
this.bounds_ = new Rect(
screenBounds.width - ImageBuffer.Overview.RIGHT - this.canvas_.width,
screenBounds.height - ImageBuffer.Overview.BOTTOM - this.canvas_.height,
this.canvas_.width,
this.canvas_.height);
this.scale_ = this.bounds_.width / imageBounds.width;
......@@ -380,10 +377,6 @@ ImageBuffer.Overview.prototype.draw = function(context) {
// Draw the thumbnail.
Rect.drawImage(context, this.canvas_, this.bounds_);
// Draw the thumbnail border.
context.strokeStyle = '#000000';
Rect.stroke(context, this.bounds_);
// Draw the shadow over the off-screen part of the thumbnail.
context.globalAlpha = 0.3;
context.fillStyle = '#000000';
......@@ -391,14 +384,19 @@ ImageBuffer.Overview.prototype.draw = function(context) {
// Outline the on-screen part of the thumbnail.
context.strokeStyle = '#FFFFFF';
Rect.stroke(context, this.clipped_);
Rect.outline(context, this.clipped_);
context.globalAlpha = 1;
// Draw the thumbnail border.
context.strokeStyle = '#000000';
Rect.outline(context, this.bounds_);
};
ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) {
if (!this.bounds_ || !this.bounds_.inside(x, y)) return null;
// Indicate that the on-screen part is draggable.
if (this.clipped_.inside(x, y)) return 'move';
if (this.clipped_ && this.clipped_.inside(x, y)) return 'move';
// Indicate that the rest of the thumbnail is clickable.
return 'crosshair';
......
......@@ -43,7 +43,7 @@
/* Scaling controls */
.scale-tool {
position: absolute;
width: 275px;
width: 305px;
height: 43px;
right: 0;
bottom: 0;
......@@ -89,6 +89,11 @@
}
.scale-div .scale-up{
right: 63px;
}
.scale-div .scale-1to1{
width: 32px;
right: 29px;
}
......
......@@ -20,6 +20,7 @@
<script type="text/javascript" src="image_editor.js"></script>
<script type="text/javascript" src="image_transform.js"></script>
<script type="text/javascript" src="image_adjust.js"></script>
<script type="text/javascript" src="filter.js"></script>
<script type="text/javascript" src="image_encoder.js"></script>
<script type="text/javascript" src="exif_encoder.js"></script>
......
......@@ -6,8 +6,8 @@
* ImageEditor is the top level object that holds together and connects
* everything needed for image editing.
* @param {HTMLElement} container
* @param {Function} saveCallback
* @param {Function} closeCallback
* @param {function(Blob)} saveCallback
* @param {function()} closeCallback
*/
function ImageEditor(container, saveCallback, closeCallback) {
this.container_ = container;
......@@ -44,8 +44,8 @@ function ImageEditor(container, saveCallback, closeCallback) {
*
* Use this method when image_editor.html is loaded into an iframe.
*
* @param {Function} saveCallback
* @param {Function} closeCallback
* @param {function(Blob)} saveCallback
* @param {function()} closeCallback
* @param {HTMLCanvasElement|HTMLImageElement|String} source
* @param {Object} opt_metadata
* @return {ImageEditor}
......@@ -264,13 +264,14 @@ ImageEditor.ScaleControl = function(parent, viewport) {
var scaleDown = parent.ownerDocument.createElement('button');
scaleDown.className = 'scale-down';
scaleDiv.appendChild(scaleDown);
scaleDown.addEventListener('click', this.onDownButton.bind(this));
scaleDown.addEventListener('click', this.onDownButton.bind(this), false);
scaleDown.textContent = '-';
this.scaleRange_ = parent.ownerDocument.createElement('input');
this.scaleRange_.type = 'range';
this.scaleRange_.max = ImageEditor.ScaleControl.MAX_SCALE;
this.scaleRange_.addEventListener('change', this.onSliderChange.bind(this));
this.scaleRange_.addEventListener(
'change', this.onSliderChange.bind(this), false);
scaleDiv.appendChild(this.scaleRange_);
this.scaleLabel_ = parent.ownerDocument.createElement('span');
......@@ -279,13 +280,19 @@ ImageEditor.ScaleControl = function(parent, viewport) {
var scaleUp = parent.ownerDocument.createElement('button');
scaleUp.className = 'scale-up';
scaleUp.textContent = '+';
scaleUp.addEventListener('click', this.onUpButton.bind(this));
scaleUp.addEventListener('click', this.onUpButton.bind(this), false);
scaleDiv.appendChild(scaleUp);
var scale1to1 = parent.ownerDocument.createElement('button');
scale1to1.className = 'scale-1to1';
scale1to1.textContent = '1:1';
scale1to1.addEventListener('click', this.on1to1Button.bind(this), false);
scaleDiv.appendChild(scale1to1);
var scaleFit = parent.ownerDocument.createElement('button');
scaleFit.className = 'scale-fit';
scaleFit.textContent = '\u2610';
scaleFit.addEventListener('click', this.onFitButton.bind(this));
scaleFit.addEventListener('click', this.onFitButton.bind(this), false);
scaleDiv.appendChild(scaleFit);
};
......@@ -306,7 +313,7 @@ ImageEditor.ScaleControl.FACTOR = 100;
*/
ImageEditor.ScaleControl.prototype.setMinScale = function(scale) {
this.scaleRange_.min = Math.min(
Math.round(scale * ImageEditor.ScaleControl.FACTOR),
Math.round(Math.min(1, scale) * ImageEditor.ScaleControl.FACTOR),
ImageEditor.ScaleControl.MAX_SCALE);
};
......@@ -378,23 +385,28 @@ ImageEditor.ScaleControl.prototype.onFitButton = function () {
this.viewport_.repaint();
};
ImageEditor.ScaleControl.prototype.on1to1Button = function () {
this.viewport_.setScale(1);
this.viewport_.repaint();
};
/**
* A helper object for panning the ImageBuffer.
* @constructor
*/
ImageEditor.MouseControl = function(canvas, buffer) {
this.canvas_ = canvas;
this.buffer_ = buffer;
canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
canvas.addEventListener('mousedown', this.onMouseDown.bind(this), false);
canvas.addEventListener('mouseup', this.onMouseUp.bind(this), false);
canvas.addEventListener('mousemove', this.onMouseMove.bind(this), false);
};
ImageEditor.MouseControl.getPosition = function(e) {
var clientRect = e.target.getBoundingClientRect();
return {
x: e.x - clientRect.left,
y: e.y - clientRect.top
x: e.clientX - clientRect.left,
y: e.clientY - clientRect.top
};
};
......@@ -433,8 +445,8 @@ ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
/**
* A toolbar for the ImageEditor.
* @constructor
*/
ImageEditor.Toolbar = function (parent, updateCallback) {
this.wrapper_ = parent.ownerDocument.createElement('div');
this.wrapper_.className = 'toolbar';
......@@ -464,16 +476,16 @@ ImageEditor.Toolbar.prototype.addLabel = function(text) {
ImageEditor.Toolbar.prototype.addButton = function(text, handler) {
var button = this.create_('button');
button.textContent = text;
button.addEventListener('click', handler);
button.addEventListener('click', handler, false);
return this.add(button);
};
/**
* @param {String} name An option name.
* @param {Number} min Min value of the option.
* @param {Number} value Default value of the option.
* @param {Number} max Max value of the options.
* @param {Number} scale A number to multiply by when setting
* @param {string} name An option name.
* @param {number} min Min value of the option.
* @param {number} value Default value of the option.
* @param {number} max Max value of the options.
* @param {number} scale A number to multiply by when setting
* min/value/max in DOM.
*/
ImageEditor.Toolbar.prototype.addRange = function(
......@@ -507,13 +519,18 @@ ImageEditor.Toolbar.prototype.addRange = function(
range.setValue(value);
};
range.addEventListener('change', function() {
mirror();
self.updateCallback_(self.getOptions());
});
range.addEventListener('change',
function() {
mirror();
self.updateCallback_(self.getOptions());
},
false);
range.setValue(value);
var descr = this.create_('span');
descr.textContent = name;
this.add(descr);
this.add(range);
this.add(label);
......
......@@ -68,8 +68,10 @@ ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) {
this.tiltRange_ =
toolbar.addRange('angle', -maxTilt, 0, maxTilt, 10);
this.tiltRange_.addEventListener('mousedown', this.onTiltStart.bind(this));
this.tiltRange_.addEventListener('mouseup', this.onTiltStop.bind(this));
this.tiltRange_.
addEventListener('mousedown', this.onTiltStart.bind(this), false);
this.tiltRange_.
addEventListener('mouseup', this.onTiltStop.bind(this), false);
};
ImageEditor.Mode.Rotate.prototype.getOriginal = function() {
......@@ -161,23 +163,23 @@ ImageEditor.Mode.Rotate.prototype.draw = function(context) {
context.globalAlpha = 0.4;
context.strokeStyle = "#C0C0C0";
for(var x = Math.floor(screenClipped.left / STEP) * STEP;
context.beginPath();
var top = screenClipped.top + 0.5;
var left = screenClipped.left + 0.5;
for(var x = Math.ceil(screenClipped.left / STEP) * STEP;
x < screenClipped.left + screenClipped.width;
x += STEP) {
var left = Math.max(screenClipped.left, x);
var right = Math.min(screenClipped.left + screenClipped.width, x + STEP);
context.strokeRect(
left, screenClipped.top, right - left, screenClipped.height);
context.moveTo(x + 0.5, top);
context.lineTo(x + 0.5, top + screenClipped.height);
}
for(var y = Math.floor(screenClipped.top / STEP) * STEP;
for(var y = Math.ceil(screenClipped.top / STEP) * STEP;
y < screenClipped.top + screenClipped.height;
y += STEP) {
var top = Math.max(screenClipped.top, y);
var bottom = Math.min(screenClipped.top + screenClipped.height, y + STEP);
context.strokeRect(
screenClipped.left, top, screenClipped.width, bottom - top);
context.moveTo(left, y + 0.5);
context.lineTo(left + screenClipped.width, y + 0.5);
}
context.closePath();
context.stroke();
context.restore();
};
......@@ -351,7 +353,8 @@ ImageEditor.Mode.Crop.prototype.rollback = function() {
ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
var rect = new Rect(this.getViewport().getImageClipped());
rect = rect.inflate (-rect.width / 6, -rect.height / 6);
rect = rect.inflate (
-Math.round(rect.width / 6), -Math.round(rect.height / 6));
this.cropRect_ = new DraggableRect(
rect, this.getViewport(), ImageEditor.Mode.Crop.GRAB_RADIUS);
};
......@@ -368,7 +371,6 @@ ImageEditor.Mode.Crop.prototype.draw = function(context) {
context.globalAlpha = 0.25;
context.fillStyle = '#000000';
Rect.fillBetween(context, inner, outer);
Rect.stroke(context, inner);
context.fillStyle = '#FFFFFF';
context.beginPath();
......@@ -380,15 +382,24 @@ ImageEditor.Mode.Crop.prototype.draw = function(context) {
context.arc(inner_right, inner.top, R, 0, Math.PI * 2);
context.moveTo(inner_right, inner_bottom);
context.arc(inner_right, inner_bottom, R, 0, Math.PI * 2);
context.closePath();
context.fill();
context.globalAlpha = 1;
context.strokeStyle = '#808080';
context.strokeRect(inner.left, inner.top, inner.width, inner.height);
context.strokeRect(
inner.left + inner.width / 3, inner.top, inner.width / 3, inner.height);
context.strokeRect(
inner.left, inner.top + inner.height / 3, inner.width, inner.height / 3);
context.globalAlpha = 0.5;
context.strokeStyle = '#FFFFFF';
context.beginPath();
context.closePath();
for (var i = 0; i <= 3; i++) {
var y = inner.top - 0.5 + Math.round((inner.height + 1) * i / 3);
context.moveTo(inner.left, y);
context.lineTo(inner.left + inner.width, y);
var x = inner.left - 0.5 + Math.round((inner.width + 1) * i / 3);
context.moveTo(x, inner.top);
context.lineTo(x, inner.top + inner.height);
}
context.stroke();
};
ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
......
......@@ -47,23 +47,6 @@ ImageUtil.between = function(min, value, max) {
return (value - min) * (value - max) <= 0;
};
/**
* Computes the function for every integer value between 0 and max and stores
* the results. Rounds and clips the results to fit the [0..255] range.
* Used to speed up pixel manipulations.
* @param {Function} func Function returning a number.
* @param {Number} max Maximum argument value (inclusive).
* @return {Array<Number>} Computed results
*/
ImageUtil.precomputeByteFunction = function(func, max) {
var results = [];
for (var arg = 0; arg <= max; arg ++) {
results.push(Math.max(0, Math.min(0xFF, Math.round(func(arg)))));
}
return results;
}
/**
* Rectangle class.
*/
......@@ -97,10 +80,10 @@ function Rect(args) {
case 1: {
var source = arguments[0];
if (source.hasOwnProperty('left') && source.hasOwnProperty('top')) {
if ('left' in source && 'top' in source) {
this.left = source.left;
this.top = source.top;
if (source.hasOwnProperty('right') && source.hasOwnProperty('bottom')) {
if ('right' in source && 'bottom' in source) {
this.width = source.right - source.left;
this.height = source.bottom - source.top;
return;
......@@ -109,7 +92,7 @@ function Rect(args) {
this.left = 0;
this.top = 0;
}
if (source.hasOwnProperty('width') && source.hasOwnProperty('height')) {
if ('width' in source && 'height' in source) {
this.width = source.width;
this.height = source.height;
return;
......@@ -204,7 +187,7 @@ Rect.prototype.clamp = function(bounds) {
*/
/**
* Draws the image in context with appropriate scaling.
* Draw the image in context with appropriate scaling.
*/
Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
opt_dstRect = opt_dstRect || new Rect(context.canvas);
......@@ -215,10 +198,18 @@ Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
};
/**
* Strokes the rectangle.
* Draw a box around the rectangle.
*/
Rect.outline = function(context, rect) {
context.strokeRect(
rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1);
};
/**
* Fill the rectangle.
*/
Rect.stroke = function(context, rect) {
context.strokeRect(rect.left, rect.top, rect.width, rect.height);
Rect.fill = function(context, rect) {
context.fillRect(rect.left, rect.top, rect.width, rect.height);
};
/**
......
......@@ -21,8 +21,8 @@
.debug-output {
position: absolute;
top: 34px;
right: 1px;
text-align: right;
left: 1px;
text-align: left;
}
.debug-buttons {
......
......@@ -89,12 +89,13 @@ Viewport.prototype.setCenter = function(x, y, ignoreClipping) {
/**
* Return a closure that can be called to pan the image.
* Useful for implementing non-trivial variants of panning (overview etc).
* @param {Number} originalX The x coordinate on the screen canvas that
* @param {number} originalX The x coordinate on the screen canvas that
* corresponds to zero change to offsetX.
* @param {Number} originalY The y coordinate on the screen canvas that
* @param {number} originalY The y coordinate on the screen canvas that
* corresponds to zero change to offsetY.
* @param {Function} scaleFunc returns the current image to screen scale.
* @param {Function} hitFunc returns true if (x,y) is in the valid region.
* @param {function():number} scaleFunc returns the image to screen scale.
* @param {function(number,number):boolean} hitFunc returns true if (x,y) is
* in the valid region.
*/
Viewport.prototype.createOffsetSetter = function (
originalX, originalY, scaleFunc, hitFunc) {
......@@ -213,7 +214,7 @@ Viewport.prototype.isClipped = function () {
* Horizontal margin. Negative if the image is clipped horizontally.
*/
Viewport.prototype.getMarginX_ = function() {
return Math.floor(
return Math.round(
(this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
};
......@@ -221,17 +222,17 @@ Viewport.prototype.getMarginX_ = function() {
* Vertical margin. Negative if the image is clipped vertically.
*/
Viewport.prototype.getMarginY_ = function() {
return Math.floor(
return Math.round(
(this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
};
Viewport.prototype.clampOffsetX_ = function(x) {
var limit = Math.max(0, -this.getMarginX_() / this.getScale());
var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale()));
return ImageUtil.clamp(-limit, x, limit);
};
Viewport.prototype.clampOffsetY_ = function(y) {
var limit = Math.max(0, -this.getMarginY_() / this.getScale());
var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale()));
return ImageUtil.clamp(-limit, y, limit);
};
......@@ -245,8 +246,8 @@ Viewport.prototype.update = function() {
this.imageOnScreen_ = new Rect(
this.getMarginX_(),
this.getMarginY_(),
Math.floor(this.imageBounds_.width * scale),
Math.floor(this.imageBounds_.height * scale));
Math.round(this.imageBounds_.width * scale),
Math.round(this.imageBounds_.height * scale));
// A visible part of the image in image coordinates.
this.imageClipped_ = new Rect(this.imageBounds_);
......@@ -256,18 +257,20 @@ Viewport.prototype.update = function() {
// Adjust for the offset.
if (this.imageOnScreen_.left < 0) {
this.imageOnScreen_.left += this.clampOffsetX_(this.offsetX_) * scale;
this.imageClipped_.left = -this.imageOnScreen_.left / scale;
this.imageClipped_.width = this.screenBounds_.width / scale;
this.imageOnScreen_.left +=
Math.round(this.clampOffsetX_(this.offsetX_) * scale);
this.imageClipped_.left = Math.round(-this.imageOnScreen_.left / scale);
this.imageClipped_.width = Math.round(this.screenBounds_.width / scale);
} else {
this.screenClipped_.left = this.imageOnScreen_.left;
this.screenClipped_.width = this.imageOnScreen_.width;
}
if (this.imageOnScreen_.top < 0) {
this.imageOnScreen_.top += this.clampOffsetY_(this.offsetY_) * scale;
this.imageClipped_.top = -this.imageOnScreen_.top / scale;
this.imageClipped_.height = this.screenBounds_.height / scale;
this.imageOnScreen_.top +=
Math.round(this.clampOffsetY_(this.offsetY_) * scale);
this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale);
this.imageClipped_.height = Math.round(this.screenBounds_.height / scale);
} else {
this.screenClipped_.top = this.imageOnScreen_.top;
this.screenClipped_.height = this.imageOnScreen_.height;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment