Pre-scaling images to speed up feedback in Image Editor.

Also refactored image_buffer.js to split in several loosely coupled objects.

BUG=
TEST=


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

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@95824 0039d316-1c4b-4281-b951-d872f2087c98
parent c79b4a53
...@@ -15,21 +15,21 @@ ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype}; ...@@ -15,21 +15,21 @@ ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype};
ImageEditor.Mode.Adjust.prototype.rollback = function() { ImageEditor.Mode.Adjust.prototype.rollback = function() {
if (!this.backup_) return; // Did not do anything yet. if (!this.backup_) return; // Did not do anything yet.
this.getBuffer().drawImageData(this.backup_); this.getContent().drawImageData(this.backup_, 0, 0);
this.backup_ = null; this.backup_ = null;
this.repaint(); this.repaint();
}; };
ImageEditor.Mode.Adjust.prototype.update = function(options) { ImageEditor.Mode.Adjust.prototype.update = function(options) {
if (!this.backup_) { if (!this.backup_) {
this.backup_ = this.getBuffer().copyImageData(); this.backup_ = this.getContent().copyImageData();
this.scratch_ = this.getBuffer().copyImageData(); this.scratch_ = this.getContent().copyImageData();
} }
ImageUtil.trace.resetTimer('filter'); ImageUtil.trace.resetTimer('filter');
this.filterFunc_(this.scratch_, this.backup_, options); this.filterFunc_(this.scratch_, this.backup_, options);
ImageUtil.trace.reportTimer('filter'); ImageUtil.trace.reportTimer('filter');
this.getBuffer().drawImageData(this.scratch_); this.getContent().drawImageData(this.scratch_, 0, 0);
this.repaint(); this.repaint();
}; };
......
...@@ -10,224 +10,22 @@ ...@@ -10,224 +10,22 @@
*/ */
function ImageBuffer(screenCanvas) { function ImageBuffer(screenCanvas) {
this.screenCanvas_ = screenCanvas; this.screenCanvas_ = screenCanvas;
this.screenContext_ = this.screenCanvas_.getContext("2d");
this.scale_ = 1; this.viewport_ = new Viewport(this.repaint.bind(this));
this.offsetX_ = 0; this.viewport_.setScreenSize(screenCanvas.width, screenCanvas.height);
this.offsetY_ = 0;
this.overlays_ = []; this.content_ = new ImageBuffer.Content(
this.viewport_, screenCanvas.ownerDocument);
this.setImageCanvas(this.createBlankCanvas(0, 0)); this.overlays_ = [];
this.addOverlay(new ImageBuffer.Margin(this.viewport_));
this.addOverlay(this.content_);
this.addOverlay(new ImageBuffer.Overview(this.viewport_, this.content_));
} }
/* ImageBuffer.prototype.getViewport = function() { return this.viewport_ };
* Viewport manipulation.
*/
ImageBuffer.prototype.setScaleControl = function(scaleControl) {
this.scaleControl_ = scaleControl;
};
ImageBuffer.prototype.getScale = function () { return this.scale_ };
ImageBuffer.prototype.setScale = function (scale, notify) {
if (this.scale_ == scale) return;
this.scale_ = scale;
if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale);
};
ImageBuffer.prototype.getFittingScale = function() {
var scaleX = this.screenCanvas_.width / this.imageCanvas_.width;
var scaleY = this.screenCanvas_.height / this.imageCanvas_.height;
return Math.min(scaleX, scaleY) * 0.85;
};
ImageBuffer.prototype.fitImage = function() {
var scale = this.getFittingScale();
if (this.scaleControl_) this.scaleControl_.setMinScale(scale);
this.setScale(scale, true);
};
ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) {
var wasFitting = this.getScale() == this.getFittingScale();
this.screenCanvas_.width = width;
this.screenCanvas_.height = height;
var minScale = this.getFittingScale();
if (this.scaleControl_) this.scaleControl_.setMinScale(minScale);
if ((wasFitting && keepFitting) || this.getScale() < minScale) {
this.setScale(minScale, true);
}
this.repaint();
};
ImageBuffer.prototype.getOffsetX = function () { return this.offsetX_; };
ImageBuffer.prototype.getOffsetY = function () { return this.offsetY_; };
ImageBuffer.prototype.setOffset = function(x, y, ignoreClipping) {
if (!ignoreClipping) {
var limitX = Math.max(0, -this.marginX_ / this.getScale());
var limitY = Math.max(0, -this.marginY_ / this.getScale());
x = ImageUtil.clip(-limitX, x, limitX);
y = ImageUtil.clip(-limitY, y, limitY);
}
if (this.offsetX_ == x && this.offsetY_ == y) return;
this.offsetX_ = x;
this.offsetY_ = y;
};
ImageBuffer.prototype.setCenter = function(x, y, ignoreClipping) {
this.setOffset(
this.imageWhole_.width / 2 - x,
this.imageWhole_.height / 2 - y,
ignoreClipping);
};
/**
* @return {Rect} The visible part of the image, in image coordinates.
*/
ImageBuffer.prototype.getClippedImage = function() {
return this.imageVisible_;
};
/**
* @return {Rect} The visible part of the image, in screen coordinates.
*/
ImageBuffer.prototype.getClippedScreen = function() {
return this.screenVisible_;
};
/**
* Returns 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
* corresponds to zero change to offsetX.
* @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.
*/
ImageBuffer.prototype.createOffsetSetter_ = function (
originalX, originalY, scaleFunc, hitFunc) {
var self = this;
var originalOffsetX = this.offsetX_;
var originalOffsetY = this.offsetY_;
if (!hitFunc) hitFunc = function() { return true; }
if (!scaleFunc) scaleFunc = this.getScale.bind(this);
return function(x, y) {
if (hitFunc(x, y)) {
var scale = scaleFunc();
self.setOffset(
originalOffsetX + (x - originalX) / scale,
originalOffsetY + (y - originalY) / scale);
self.repaint();
}
};
};
/**
* @return {Boolean} True if the entire image is visible on the screen canvas.
*/
ImageBuffer.prototype.isFullyVisible = function () {
return this.marginX_ >= 0 && this.marginY_ >= 0;
};
ImageBuffer.prototype.updateViewPort = function () {
var scale = this.getScale();
this.screenWhole_ = new Rect(this.screenCanvas_);
this.imageWhole_ = new Rect(this.imageCanvas_);
// Image bounds in screen coordinates.
this.imageOnScreen_ = {};
this.imageOnScreen_.width = Math.floor(this.imageWhole_.width * scale);
this.imageOnScreen_.height = Math.floor(this.imageWhole_.height * scale);
this.marginX_ = Math.floor(
(this.screenCanvas_.width - this.imageOnScreen_.width) / 2);
this.marginY_ = Math.floor(
(this.screenCanvas_.height - this.imageOnScreen_.height) / 2);
this.imageOnScreen_.left = this.marginX_;
this.imageOnScreen_.top = this.marginY_;
this.imageVisible_ = new Rect(this.imageWhole_);
this.screenVisible_ = new Rect(this.screenWhole_);
if (this.marginX_ < 0) {
this.imageOnScreen_.left +=
ImageUtil.clip(this.marginX_, this.offsetX_ * scale, -this.marginX_);
this.imageVisible_.left = -this.imageOnScreen_.left / scale;
this.imageVisible_.width = this.screenCanvas_.width / scale;
} else {
this.screenVisible_.left = this.imageOnScreen_.left;
this.screenVisible_.width = this.imageOnScreen_.width;
}
if (this.marginY_ < 0) {
this.imageOnScreen_.top +=
ImageUtil.clip(this.marginY_, this.offsetY_ * scale, -this.marginY_);
this.imageVisible_.top = -this.imageOnScreen_.top / scale;
this.imageVisible_.height = this.screenCanvas_.height / scale;
} else {
this.screenVisible_.top = this.imageOnScreen_.top;
this.screenVisible_.height = this.imageOnScreen_.height;
}
this.updateOverlays();
};
/*
* Coordinate conversion between the screen canvas and the image.
*/
ImageBuffer.prototype.screenToImageSize = function(size) {
return size / this.getScale();
};
ImageBuffer.prototype.screenToImageX = function(x) {
return Math.round((x - this.imageOnScreen_.left) / this.getScale());
};
ImageBuffer.prototype.screenToImageY = function(y) {
return Math.round((y - this.imageOnScreen_.top) / this.getScale());
};
ImageBuffer.prototype.screenToImageRect = function(rect) {
return new Rect(
this.screenToImageX(rect.left),
this.screenToImageY(rect.top),
this.screenToImageSize(rect.width),
this.screenToImageSize(rect.height));
};
ImageBuffer.prototype.imageToScreenSize = function(size) {
return size * this.getScale();
};
ImageBuffer.prototype.imageToScreenX = function(x) {
return Math.round(this.imageOnScreen_.left + x * this.getScale());
};
ImageBuffer.prototype.imageToScreenY = function(y) {
return Math.round(this.imageOnScreen_.top + y * this.getScale());
};
ImageBuffer.prototype.imageToScreenRect = function(rect) {
return new Rect(
this.imageToScreenX(rect.left),
this.imageToScreenY(rect.top),
this.imageToScreenSize(rect.width),
this.imageToScreenSize(rect.height));
};
/* ImageBuffer.prototype.getContent = function() { return this.content_ };
* Content manipulation.
*/
/** /**
* Loads the new content. * Loads the new content.
...@@ -241,123 +39,43 @@ ImageBuffer.prototype.load = function(source) { ...@@ -241,123 +39,43 @@ ImageBuffer.prototype.load = function(source) {
image.onload = function(e) { self.load(e.target); }; image.onload = function(e) { self.load(e.target); };
image.src = source; image.src = source;
} else { } else {
this.imageCanvas_.width = source.width, this.content_.load(source);
this.imageCanvas_.height = source.height;
this.drawImage(source);
if (this.scaleControl_)
this.scaleControl_.displayImageSize(
this.imageCanvas_.width, this.imageCanvas_.height);
this.fitImage();
this.repaint(); this.repaint();
} }
}; };
ImageBuffer.prototype.getImageCanvas = function() { return this.imageCanvas_; }; ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) {
this.screenCanvas_.width = width;
/** this.screenCanvas_.height = height;
* Replaces the off-screen canvas.
* To be used when the editor modifies the image dimensions.
* @param {HTMLCanvasElement} canvas
*/
ImageBuffer.prototype.setImageCanvas = function(canvas) {
this.imageCanvas_ = canvas;
this.imageContext_ = canvas.getContext("2d");
if (this.scaleControl_)
this.scaleControl_.displayImageSize(
this.imageCanvas_.width, this.imageCanvas_.height);
};
/**
* @return {HTMLCanvasElement} A new blank canvas of the required size.
*/
ImageBuffer.prototype.createBlankCanvas = function (width, height) {
var canvas = this.screenCanvas_.ownerDocument.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
/**
* @return {HTMLCanvasElement} A new canvas with a copy of the content.
*/
ImageBuffer.prototype.copyImageCanvas = function () {
var canvas = this.createBlankCanvas(
this.imageCanvas_.width, this.imageCanvas_.height);
canvas.getContext('2d').drawImage(this.imageCanvas_, 0, 0);
return canvas;
};
/** var wasFitting =
* @return {ImageData} A new ImageData object with a copy of the content. this.viewport_.getScale() == this.viewport_.getFittingScale();
*/
ImageBuffer.prototype.copyImageData = function () {
return this.imageContext_.getImageData(
0, 0, this.imageCanvas_.width, this.imageCanvas_.height);
};
/** this.viewport_.setScreenSize(width, height);
* @param {HTMLImageElement|HTMLCanvasElement} image
*/
ImageBuffer.prototype.drawImage = function(image) {
ImageUtil.trace.resetTimer('drawImage');
this.imageContext_.drawImage(image, 0, 0);
ImageUtil.trace.reportTimer('drawImage');
};
/** var minScale = this.viewport_.getFittingScale();
* @param {ImageData} imageData if ((wasFitting && keepFitting) || this.viewport_.getScale() < minScale) {
*/ this.viewport_.setScale(minScale, true);
ImageBuffer.prototype.drawImageData = function (imageData) { }
ImageUtil.trace.resetTimer('putImageData'); this.repaint();
this.imageContext_.putImageData(imageData, 0, 0);
ImageUtil.trace.reportTimer('putImageData');
}; };
/** /**
* Paints the content on the screen canvas taking the current scale and offset * Paints the content on the screen canvas taking the current scale and offset
* into account. * into account.
*/ */
ImageBuffer.prototype.repaint = function () { ImageBuffer.prototype.repaint = function (opt_fromOverlay) {
ImageUtil.trace.resetTimer('repaint'); this.viewport_.update();
this.drawOverlays(this.screenCanvas_.getContext("2d"), opt_fromOverlay);
this.updateViewPort();
this.screenContext_.save();
this.screenContext_.fillStyle = '#F0F0F0';
this.screenContext_.strokeStyle = '#000000';
Rect.drawImage(this.screenContext_, this.imageCanvas_,
this.imageOnScreen_, this.imageWhole_);
Rect.fillBetween(this.screenContext_, this.imageOnScreen_,
this.screenWhole_);
Rect.stroke(this.screenContext_, this.imageOnScreen_);
this.screenContext_.restore();
this.drawOverlays(this.screenContext_);
ImageUtil.trace.reportTimer('repaint');
}; };
/** ImageBuffer.prototype.repaintScreenRect = function (screenRect, imageRect) {
* ImageBuffer.Overlay is a pluggable extension that modifies the outlook Rect.drawImage(
* and the behavior of the ImageBuffer instance. this.screenCanvas_.getContext('2d'),
*/ this.getContent().getCanvas(),
ImageBuffer.Overlay = function() {}; screenRect || this.getViewport().imageToScreenRect(screenRect),
imageRect || this.getViewport().screenToImageRect(screenRect));
ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 }; };
ImageBuffer.Overlay.prototype.updateViewPort = function() {}
ImageBuffer.Overlay.prototype.draw = function() {}
ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null };
ImageBuffer.Overlay.prototype.onClick = function() { return false };
ImageBuffer.Overlay.prototype.getDragHandler = function() { return null };
/** /**
* @param {ImageBuffer.Overlay} overlay * @param {ImageBuffer.Overlay} overlay
...@@ -385,22 +103,19 @@ ImageBuffer.prototype.removeOverlay = function (overlay) { ...@@ -385,22 +103,19 @@ ImageBuffer.prototype.removeOverlay = function (overlay) {
throw new Error('Cannot remove overlay ' + overlay); throw new Error('Cannot remove overlay ' + overlay);
}; };
/**
* Updates viewport configuration on all overlays.
*/
ImageBuffer.prototype.updateOverlays = function (context) {
for (var i = 0; i != this.overlays_.length; i++) {
this.overlays_[i].updateViewPort(this);
}
}
/** /**
* Draws overlays in the ascending Z-order. * Draws overlays in the ascending Z-order.
* Skips overlays below opt_startFrom.
*/ */
ImageBuffer.prototype.drawOverlays = function (context) { ImageBuffer.prototype.drawOverlays = function (context, opt_fromOverlay) {
var skip = true;
for (var i = 0; i != this.overlays_.length; i++) { for (var i = 0; i != this.overlays_.length; i++) {
var overlay = this.overlays_[i];
if (!opt_fromOverlay || opt_fromOverlay == overlay) skip = false;
if (skip) continue;
context.save(); context.save();
this.overlays_[i].draw(context); overlay.draw(context);
context.restore(); context.restore();
} }
}; };
...@@ -414,11 +129,6 @@ ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) { ...@@ -414,11 +129,6 @@ ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) {
var style = this.overlays_[i].getCursorStyle(x, y, mouseDown); var style = this.overlays_[i].getCursorStyle(x, y, mouseDown);
if (style) return style; if (style) return style;
} }
// Indicate that the image is draggable.
if (!this.isFullyVisible() && this.screenVisible_.inside(x, y))
return 'move';
return 'default'; return 'default';
}; };
...@@ -442,116 +152,279 @@ ImageBuffer.prototype.getDragHandler = function (x, y) { ...@@ -442,116 +152,279 @@ ImageBuffer.prototype.getDragHandler = function (x, y) {
var handler = this.overlays_[i].getDragHandler(x, y); var handler = this.overlays_[i].getDragHandler(x, y);
if (handler) return handler; if (handler) return handler;
} }
return null;
};
/**
* ImageBuffer.Overlay is a pluggable extension that modifies the outlook
* and the behavior of the ImageBuffer instance.
*/
ImageBuffer.Overlay = function() {};
ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 };
ImageBuffer.Overlay.prototype.draw = function() {};
ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null };
ImageBuffer.Overlay.prototype.onClick = function() { return false };
ImageBuffer.Overlay.prototype.getDragHandler = function() { return null };
/**
* The margin overlay draws the image outline and paints the margins.
*/
ImageBuffer.Margin = function(viewport) {
this.viewport_ = viewport;
};
ImageBuffer.Margin.prototype = {__proto__: ImageBuffer.Overlay.prototype};
// Draw below everything including the content.
ImageBuffer.Margin.prototype.getZIndex = function() { return -2 };
ImageBuffer.Margin.prototype.draw = function(context) {
context.save();
context.fillStyle = '#F0F0F0';
context.strokeStyle = '#000000';
Rect.fillBetween(context,
this.viewport_.getImageBoundsOnScreen(),
this.viewport_.getScreenBounds());
Rect.stroke(context, this.viewport_.getImageBoundsOnScreen());
context.restore();
};
/**
* The overlay containing the image.
*/
ImageBuffer.Content = function(viewport, document) {
this.viewport_ = viewport;
this.document_ = document;
this.generation_ = 0;
this.setCanvas(this.createBlankCanvas(0, 0));
};
ImageBuffer.Content.prototype = {__proto__: ImageBuffer.Overlay.prototype};
// Draw below overlays with the default zIndex.
ImageBuffer.Content.prototype.getZIndex = function() { return -1 };
ImageBuffer.Content.prototype.draw = function(context) {
Rect.drawImage(
context, this.canvas_, this.viewport_.getImageBoundsOnScreen());
};
ImageBuffer.Content.prototype.getCursorStyle = function (x, y, mouseDown) {
// Indicate that the image is draggable.
if (this.viewport_.isClipped() &&
this.viewport_.getScreenClipped().inside(x, y))
return 'move';
return null;
};
if (!this.isFullyVisible() && this.screenVisible_.inside(x, y)) { ImageBuffer.Content.prototype.getDragHandler = function (x, y) {
var cursor = this.getCursorStyle(x, y);
if (cursor == 'move') {
// Return the handler that drags the entire image. // Return the handler that drags the entire image.
return this.createOffsetSetter_(x, y, this.getScale.bind(this)); return this.viewport_.createOffsetSetter(x, y);
} }
return null; return null;
}; };
ImageBuffer.Content.prototype.getCacheGeneration = function() {
return this.generation_;
};
ImageBuffer.Content.prototype.invalidateCaches = function() {
this.generation_++;
};
ImageBuffer.Content.prototype.getCanvas = function() { return this.canvas_ };
/** /**
* Overview overlay draws the image thumbnail in the bottom right corner. * Replaces the off-screen canvas.
* Indicates the currently visible part. * To be used when the editor modifies the image dimensions.
* Supports panning by dragging. * 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)
*/ */
ImageBuffer.Content.prototype.setCanvas = function(
canvas, opt_width, opt_height) {
this.canvas_ = canvas;
this.viewport_.setImageSize(opt_width || canvas.width,
opt_height || canvas.height);
ImageBuffer.Overview = function() {}; this.invalidateCaches();
};
ImageBuffer.Overview.MAX_SIZE = 150; /**
ImageBuffer.Overview.RIGHT = 7; * @return {HTMLCanvasElement} A new blank canvas of the required size.
ImageBuffer.Overview.BOTTOM = 50; */
ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) {
var canvas = this.document_.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
/**
* @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) {
var canvas = this.createBlankCanvas(opt_width || this.canvas_.width,
opt_height || this.canvas_.height);
Rect.drawImage(canvas.getContext('2d'), this.canvas_);
return canvas;
};
/**
* @return {ImageData} A new ImageData object with a copy of the content.
*/
ImageBuffer.Content.prototype.copyImageData = function (opt_width, opt_height) {
return this.canvas_.getContext("2d").getImageData(
0, 0, opt_width || this.canvas_.width, opt_height || this.canvas_.height);
};
/**
* @param {HTMLImageElement|HTMLCanvasElement} image
*/
ImageBuffer.Content.prototype.load = function(image) {
this.canvas_.width = image.width;
this.canvas_.height = image.height;
Rect.drawImage(this.canvas_.getContext("2d"), image);
this.invalidateCaches();
this.viewport_.setImageSize(image.width, image.height);
this.viewport_.fitImage();
};
/**
* @param {ImageData} imageData
*/
ImageBuffer.Content.prototype.drawImageData = function (imageData, x, y) {
this.canvas_.getContext("2d").putImageData(imageData, x, y);
this.invalidateCaches();
};
/**
* The overview overlay draws the image thumbnail in the bottom right corner.
* Indicates the currently visible part. Supports panning by dragging.
*/
ImageBuffer.Overview = function(viewport, content) {
this.viewport_ = viewport;
this.content_ = content;
this.contentGeneration_ = 0;
};
ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype}; ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype};
ImageBuffer.Overview.prototype.getZIndex = function() { return 100; } // Draw above everything.
ImageBuffer.Overview.prototype.getZIndex = function() { return 100 };
ImageBuffer.Overview.prototype.updateViewPort = function(buffer) { ImageBuffer.Overview.MAX_SIZE = 150;
this.buffer_ = buffer; ImageBuffer.Overview.RIGHT = 7;
ImageBuffer.Overview.BOTTOM = 50;
this.whole_ = null; ImageBuffer.Overview.prototype.update = function() {
this.visible_ = null; var imageBounds = this.viewport_.getImageBounds();
if (this.buffer_.isFullyVisible()) return; if (this.contentGeneration_ != this.content_.getCacheGeneration()) {
this.contentGeneration_ = this.content_.getCacheGeneration();
var screenWhole = this.buffer_.screenWhole_; var aspect = imageBounds.width / imageBounds.height;
var imageWhole = this.buffer_.imageWhole_; if (aspect > 1) {
var imageVisible = this.buffer_.imageVisible_; 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);
}
var aspect = imageWhole.width / imageWhole.height; this.canvas_ =
if (aspect > 1) { this.content_.copyCanvas(this.bounds_.width, this.bounds_.height);
this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE,
ImageBuffer.Overview.MAX_SIZE / aspect);
} else {
this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect,
ImageBuffer.Overview.MAX_SIZE);
} }
this.whole_ = this.whole_.moveTo( this.clipped_ = null;
screenWhole.width - ImageBuffer.Overview.RIGHT - this.whole_.width,
screenWhole.height - ImageBuffer.Overview.BOTTOM - this.whole_.height); if (this.viewport_.isClipped()) {
var screenBounds = this.viewport_.getScreenBounds();
this.scale_ = this.whole_.width / imageWhole.width; this.bounds_ = this.bounds_.moveTo(
screenBounds.width - ImageBuffer.Overview.RIGHT - this.bounds_.width,
screenBounds.height - ImageBuffer.Overview.BOTTOM -
this.bounds_.height);
this.visible_ = imageVisible. this.scale_ = this.bounds_.width / imageBounds.width;
scale(this.scale_).
shift(this.whole_.left, this.whole_.top); this.clipped_ = this.viewport_.getImageClipped().
scale(this.scale_).
shift(this.bounds_.left, this.bounds_.top);
}
}; };
ImageBuffer.Overview.prototype.draw = function(context) { ImageBuffer.Overview.prototype.draw = function(context) {
if (!this.visible_) return; this.update();
if (!this.clipped_) return;
// Draw the thumbnail. // Draw the thumbnail.
Rect.drawImage(context, this.buffer_.imageCanvas_, Rect.drawImage(context, this.canvas_, this.bounds_);
this.whole_, this.buffer_.imageWhole_);
// Draw the thumbnail border. // Draw the thumbnail border.
context.strokeStyle = '#000000'; context.strokeStyle = '#000000';
Rect.stroke(context, this.whole_); Rect.stroke(context, this.bounds_);
// Draw the shadow over the off-screen part of the thumbnail. // Draw the shadow over the off-screen part of the thumbnail.
context.globalAlpha = 0.3; context.globalAlpha = 0.3;
context.fillStyle = '#000000'; context.fillStyle = '#000000';
Rect.fillBetween(context, this.visible_, this.whole_); Rect.fillBetween(context, this.clipped_, this.bounds_);
// Outline the on-screen part of the thumbnail. // Outline the on-screen part of the thumbnail.
context.strokeStyle = '#FFFFFF'; context.strokeStyle = '#FFFFFF';
Rect.stroke(context, this.visible_); Rect.stroke(context, this.clipped_);
}; };
ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) { ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) {
if (!this.whole_ || !this.whole_.inside(x, y)) return null; if (!this.bounds_ || !this.bounds_.inside(x, y)) return null;
// Indicate that the on-screen part is draggable. // Indicate that the on-screen part is draggable.
if (this.visible_.inside(x, y)) return 'move'; if (this.clipped_.inside(x, y)) return 'move';
// Indicathe that the rest of the thumbnail is clickable. // Indicate that the rest of the thumbnail is clickable.
return 'crosshair'; return 'crosshair';
}; };
ImageBuffer.Overview.prototype.onClick = function(x, y) { ImageBuffer.Overview.prototype.onClick = function(x, y) {
if (!this.whole_ || !this.whole_.inside(x, y)) return false; if (this.getCursorStyle(x, y) != 'crosshair') return false;
this.viewport_.setCenter(
if (this.visible_.inside(x, y)) return false; (x - this.bounds_.left) / this.scale_,
(y - this.bounds_.top) / this.scale_);
this.buffer_.setCenter( this.viewport_.repaint();
(x - this.whole_.left) / this.scale_,
(y - this.whole_.top) / this.scale_);
this.buffer_.repaint();
return true; return true;
}; };
ImageBuffer.Overview.prototype.getDragHandler = function(x, y) { ImageBuffer.Overview.prototype.getDragHandler = function(x, y) {
if (!this.whole_ || !this.whole_.inside(x, y)) return null; var cursor = this.getCursorStyle(x, y);
if (this.visible_.inside(x, y)) { if (cursor == 'move') {
var self = this; var self = this;
function scale() { return -self.scale_;} function scale() { return -self.scale_;}
function hit(x, y) { return self.whole_ && self.whole_.inside(x, y); } function hit(x, y) { return self.bounds_ && self.bounds_.inside(x, y); }
return this.buffer_.createOffsetSetter_(x, y, scale, hit); return this.viewport_.createOffsetSetter(x, y, scale, hit);
} else { } else if (cursor == 'crosshair') {
// Force non-draggable behavior. // Force non-draggable behavior.
return function() {}; return function() {};
} else {
return null;
} }
}; };
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
<link rel="stylesheet" type="text/css" href="image_editor.css"/> <link rel="stylesheet" type="text/css" href="image_editor.css"/>
<script type="text/javascript" src="image_util.js"></script> <script type="text/javascript" src="image_util.js"></script>
<script type="text/javascript" src="viewport.js"></script>
<script type="text/javascript" src="image_buffer.js"></script> <script type="text/javascript" src="image_buffer.js"></script>
<script type="text/javascript" src="image_editor.js"></script> <script type="text/javascript" src="image_editor.js"></script>
<script type="text/javascript" src="image_transform.js"></script> <script type="text/javascript" src="image_transform.js"></script>
......
...@@ -28,12 +28,11 @@ function ImageEditor(container, saveCallback, closeCallback) { ...@@ -28,12 +28,11 @@ function ImageEditor(container, saveCallback, closeCallback) {
canvas.height = this.canvasWrapper_.clientHeight; canvas.height = this.canvasWrapper_.clientHeight;
this.buffer_ = new ImageBuffer(canvas); this.buffer_ = new ImageBuffer(canvas);
this.buffer_.addOverlay(new ImageBuffer.Overview());
this.scaleControl_ = this.scaleControl_ = new ImageEditor.ScaleControl(
new ImageEditor.ScaleControl(this.canvasWrapper_, this.buffer_); this.canvasWrapper_, this.getBuffer().getViewport());
this.panControl_ = new ImageEditor.MouseControl(canvas, this.buffer_); this.panControl_ = new ImageEditor.MouseControl(canvas, this.getBuffer());
this.toolbar_ = this.toolbar_ =
new ImageEditor.Toolbar(container, this.onOptionsChange.bind(this)); new ImageEditor.Toolbar(container, this.onOptionsChange.bind(this));
...@@ -57,7 +56,7 @@ ImageEditor.open = function(saveCallback, closeCallback, source, opt_metadata) { ...@@ -57,7 +56,7 @@ ImageEditor.open = function(saveCallback, closeCallback, source, opt_metadata) {
window.addEventListener('resize', editor.resizeFrame.bind(editor), false); window.addEventListener('resize', editor.resizeFrame.bind(editor), false);
editor.load(source, opt_metadata); editor.load(source, opt_metadata);
return editor; return editor;
} };
/** /**
* Loads a new image and its metadata. * Loads a new image and its metadata.
...@@ -98,7 +97,7 @@ ImageEditor.prototype.close = function() { ...@@ -98,7 +97,7 @@ ImageEditor.prototype.close = function() {
*/ */
ImageEditor.prototype.save = function() { ImageEditor.prototype.save = function() {
this.saveCallback_(ImageEncoder.getBlob( this.saveCallback_(ImageEncoder.getBlob(
this.getBuffer().getImageCanvas(), 'image/jpeg', this.metadata_)); this.getBuffer().getContent().getCanvas(), 'image/jpeg', this.metadata_));
}; };
ImageEditor.prototype.onOptionsChange = function(options) { ImageEditor.prototype.onOptionsChange = function(options) {
...@@ -124,7 +123,7 @@ ImageEditor.prototype.initToolbar = function() { ...@@ -124,7 +123,7 @@ ImageEditor.prototype.initToolbar = function() {
ImageEditor.Mode = function(displayName) { ImageEditor.Mode = function(displayName) {
this.displayName = displayName; this.displayName = displayName;
} };
ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype }; ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
...@@ -132,8 +131,16 @@ ImageEditor.Mode.prototype.getBuffer = function() { ...@@ -132,8 +131,16 @@ ImageEditor.Mode.prototype.getBuffer = function() {
return this.buffer_; return this.buffer_;
}; };
ImageEditor.Mode.prototype.repaint = function() { ImageEditor.Mode.prototype.repaint = function(opt_fromOverlay) {
return this.getBuffer().repaint(); return this.buffer_.repaint(opt_fromOverlay);
};
ImageEditor.Mode.prototype.getViewport = function() {
return this.viewport_;
};
ImageEditor.Mode.prototype.getContent = function() {
return this.content_;
}; };
/** /**
...@@ -141,6 +148,8 @@ ImageEditor.Mode.prototype.repaint = function() { ...@@ -141,6 +148,8 @@ ImageEditor.Mode.prototype.repaint = function() {
*/ */
ImageEditor.Mode.prototype.setUp = function(buffer) { ImageEditor.Mode.prototype.setUp = function(buffer) {
this.buffer_ = buffer; this.buffer_ = buffer;
this.viewport_ = buffer.getViewport();
this.content_ = buffer.getContent();
this.buffer_.addOverlay(this); this.buffer_.addOverlay(this);
}; };
...@@ -176,7 +185,7 @@ ImageEditor.Mode.constructors = []; ...@@ -176,7 +185,7 @@ ImageEditor.Mode.constructors = [];
ImageEditor.Mode.register = function(constructor) { ImageEditor.Mode.register = function(constructor) {
ImageEditor.Mode.constructors.push(constructor); ImageEditor.Mode.constructors.push(constructor);
} };
ImageEditor.prototype.createModeButtons = function() { ImageEditor.prototype.createModeButtons = function() {
for (var i = 0; i != ImageEditor.Mode.constructors.length; i++) { for (var i = 0; i != ImageEditor.Mode.constructors.length; i++) {
...@@ -236,9 +245,9 @@ ImageEditor.prototype.onModeReset = function() { ...@@ -236,9 +245,9 @@ ImageEditor.prototype.onModeReset = function() {
/** /**
* Scale control for an ImageBuffer. * Scale control for an ImageBuffer.
*/ */
ImageEditor.ScaleControl = function(parent, buffer) { ImageEditor.ScaleControl = function(parent, viewport) {
this.buffer_ = buffer; this.viewport_ = viewport;
this.buffer_.setScaleControl(this); this.viewport_.setScaleControl(this);
var div = parent.ownerDocument.createElement('div'); var div = parent.ownerDocument.createElement('div');
div.className = 'scale-tool'; div.className = 'scale-tool';
...@@ -319,10 +328,10 @@ ImageEditor.ScaleControl.prototype.displayScale = function(scale) { ...@@ -319,10 +328,10 @@ ImageEditor.ScaleControl.prototype.displayScale = function(scale) {
* Called when the user changes the scale via the controls. * Called when the user changes the scale via the controls.
*/ */
ImageEditor.ScaleControl.prototype.setScale = function (scale) { ImageEditor.ScaleControl.prototype.setScale = function (scale) {
scale = ImageUtil.clip(this.scaleRange_.min, scale, this.scaleRange_.max); scale = ImageUtil.clamp(this.scaleRange_.min, scale, this.scaleRange_.max);
this.updateSlider(scale); this.updateSlider(scale);
this.buffer_.setScale(scale / ImageEditor.ScaleControl.FACTOR, false); this.viewport_.setScale(scale / ImageEditor.ScaleControl.FACTOR, false);
this.buffer_.repaint(); this.viewport_.repaint();
}; };
ImageEditor.ScaleControl.prototype.updateSlider = function(scale) { ImageEditor.ScaleControl.prototype.updateSlider = function(scale) {
...@@ -365,8 +374,8 @@ ImageEditor.ScaleControl.prototype.onUpButton = function () { ...@@ -365,8 +374,8 @@ ImageEditor.ScaleControl.prototype.onUpButton = function () {
}; };
ImageEditor.ScaleControl.prototype.onFitButton = function () { ImageEditor.ScaleControl.prototype.onFitButton = function () {
this.buffer_.fitImage(); this.viewport_.fitImage();
this.buffer_.repaint(); this.viewport_.repaint();
}; };
/** /**
...@@ -431,7 +440,7 @@ ImageEditor.Toolbar = function (parent, updateCallback) { ...@@ -431,7 +440,7 @@ ImageEditor.Toolbar = function (parent, updateCallback) {
this.wrapper_.className = 'toolbar'; this.wrapper_.className = 'toolbar';
parent.appendChild(this.wrapper_); parent.appendChild(this.wrapper_);
this.updateCallback_ = updateCallback; this.updateCallback_ = updateCallback;
} };
ImageEditor.Toolbar.prototype.clear = function() { ImageEditor.Toolbar.prototype.clear = function() {
this.wrapper_.innerHTML = ''; this.wrapper_.innerHTML = '';
......
...@@ -15,7 +15,7 @@ ImageEditor.Mode.Resize.prototype = {__proto__: ImageEditor.Mode.prototype}; ...@@ -15,7 +15,7 @@ ImageEditor.Mode.Resize.prototype = {__proto__: ImageEditor.Mode.prototype};
ImageEditor.Mode.register(ImageEditor.Mode.Resize); ImageEditor.Mode.register(ImageEditor.Mode.Resize);
ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) { ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) {
var canvas = this.getBuffer().getImageCanvas(); var canvas = this.getContent().getCanvas();
this.widthRange_ = this.widthRange_ =
toolbar.addRange('width', 0, canvas.width, canvas.width * 2); toolbar.addRange('width', 0, canvas.width, canvas.width * 2);
this.heightRange_ = this.heightRange_ =
...@@ -23,19 +23,12 @@ ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) { ...@@ -23,19 +23,12 @@ ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) {
}; };
ImageEditor.Mode.Resize.prototype.commit = function() { ImageEditor.Mode.Resize.prototype.commit = function() {
var newCanvas = this.getBuffer().createBlankCanvas(
this.widthRange_.getValue(), this.heightRange_.getValue());
var srcCanvas = this.getBuffer().getImageCanvas();
var context = newCanvas.getContext("2d");
ImageUtil.trace.resetTimer('transform'); ImageUtil.trace.resetTimer('transform');
Rect.drawImage( var newCanvas = this.getContent().copyCanvas(
context, srcCanvas, new Rect(newCanvas), new Rect(srcCanvas)); this.widthRange_.getValue(), this.heightRange_.getValue());
ImageUtil.trace.reportTimer('transform'); ImageUtil.trace.reportTimer('transform');
this.getContent().setCanvas(newCanvas);
this.getBuffer().setImageCanvas(newCanvas); this.getViewport().fitImage();
this.getBuffer().fitImage();
}; };
/** /**
...@@ -50,25 +43,23 @@ ImageEditor.Mode.Rotate.prototype = {__proto__: ImageEditor.Mode.prototype}; ...@@ -50,25 +43,23 @@ ImageEditor.Mode.Rotate.prototype = {__proto__: ImageEditor.Mode.prototype};
ImageEditor.Mode.register(ImageEditor.Mode.Rotate); ImageEditor.Mode.register(ImageEditor.Mode.Rotate);
ImageEditor.Mode.Rotate.prototype.commit = function() { ImageEditor.Mode.Rotate.prototype.commit = function() {};
this.getBuffer().fitImage();
};
ImageEditor.Mode.Rotate.prototype.rollback = function() { ImageEditor.Mode.Rotate.prototype.rollback = function() {
if (this.backup_) { if (this.backup_) {
this.getBuffer().setImageCanvas(this.backup_); this.getContent().setCanvas(this.backup_);
this.backup_ = null; this.backup_ = null;
this.transform_ = null;
} }
this.transform_ = null;
}; };
ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) { ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) {
toolbar.addButton("Left", this.doTransform.bind(this, 1, 1, 3)); toolbar.addButton("Left", this.modifyTransform.bind(this, 1, 1, 3));
toolbar.addButton("Right", this.doTransform.bind(this, 1, 1, 1)); toolbar.addButton("Right", this.modifyTransform.bind(this, 1, 1, 1));
toolbar.addButton("Flip V", this.doTransform.bind(this, 1, -1, 0)); toolbar.addButton("Flip V", this.modifyTransform.bind(this, 1, -1, 0));
toolbar.addButton("Flip H", this.doTransform.bind(this, -1, 1, 0)); toolbar.addButton("Flip H", this.modifyTransform.bind(this, -1, 1, 0));
var srcCanvas = this.getBuffer().getImageCanvas(); var srcCanvas = this.getContent().getCanvas();
var width = srcCanvas.width; var width = srcCanvas.width;
var height = srcCanvas.height; var height = srcCanvas.height;
...@@ -81,79 +72,165 @@ ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) { ...@@ -81,79 +72,165 @@ ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) {
this.tiltRange_.addEventListener('mouseup', this.onTiltStop.bind(this)); this.tiltRange_.addEventListener('mouseup', this.onTiltStop.bind(this));
}; };
ImageEditor.Mode.Rotate.prototype.getOriginal = function() {
if (!this.backup_) {
this.backup_ = this.getContent().getCanvas();
}
return this.backup_;
};
ImageEditor.Mode.Rotate.prototype.getTransform = function() {
if (!this.transform_) {
this.transform_ = new ImageEditor.Mode.Rotate.Transform();
}
return this.transform_;
};
ImageEditor.Mode.Rotate.prototype.onTiltStart = function() { ImageEditor.Mode.Rotate.prototype.onTiltStart = function() {
this.tiltDrag_ = true; this.tiltDrag_ = true;
var original = this.getOriginal();
// Downscale the original image to the overview thumbnail size.
var downScale = ImageBuffer.Overview.MAX_SIZE /
Math.max(original.width, original.height);
this.preScaledOriginal_ = this.getContent().createBlankCanvas(
original.width * downScale, original.height * downScale);
Rect.drawImage(this.preScaledOriginal_.getContext('2d'), original);
// Translate the current offset into the original image coordinate space.
var viewport = this.getViewport();
var originalOffset = this.getTransform().transformOffsetToBaseline(
viewport.getOffsetX(), viewport.getOffsetY());
// Find the part of the original image that is sufficient to pre-render
// the rotation results.
var screenClipped = viewport.getScreenClipped();
var diagonal = viewport.screenToImageSize(
Math.sqrt(screenClipped.width * screenClipped.width +
screenClipped.height * screenClipped.height));
var originalBounds = new Rect(original);
var originalPreclipped = new Rect(
originalBounds.width / 2 - originalOffset.x - diagonal / 2,
originalBounds.height / 2 - originalOffset.y - diagonal / 2,
diagonal,
diagonal).clamp(originalBounds);
// We assume that the scale is not changing during the mouse drag.
var scale = viewport.getScale();
this.preClippedOriginal_ = this.getContent().createBlankCanvas(
originalPreclipped.width * scale, originalPreclipped.height * scale);
Rect.drawImage(this.preClippedOriginal_.getContext('2d'), original, null,
originalPreclipped);
this.repaint(); this.repaint();
}; };
ImageEditor.Mode.Rotate.prototype.onTiltStop = function() { ImageEditor.Mode.Rotate.prototype.onTiltStop = function() {
this.tiltDrag_ = false; this.tiltDrag_ = false;
this.repaint(); if (this.preScaledOriginal_) {
this.preScaledOriginal_ = false;
this.preClippedOriginal_ = false;
this.applyTransform();
} else {
this.repaint();
}
}; };
ImageEditor.Mode.Rotate.prototype.draw = function(context) { ImageEditor.Mode.Rotate.prototype.draw = function(context) {
if (!this.tiltDrag_) return; if (!this.tiltDrag_) return;
var rect = this.getBuffer().getClippedScreen(); var screenClipped = this.getViewport().getScreenClipped();
if (this.preClippedOriginal_) {
ImageUtil.trace.resetTimer('preview');
var transformed = this.getContent().createBlankCanvas(
screenClipped.width, screenClipped.height);
this.getTransform().apply(
transformed.getContext('2d'), this.preClippedOriginal_);
Rect.drawImage(context, transformed, screenClipped);
ImageUtil.trace.reportTimer('preview');
}
const STEP = 50; const STEP = 50;
context.save(); context.save();
context.globalAlpha = 0.4; context.globalAlpha = 0.4;
context.strokeStyle = "#C0C0C0"; context.strokeStyle = "#C0C0C0";
for(var x = 0; x <= rect.width - STEP; x += STEP) {
context.strokeRect(rect.left + x, rect.top, STEP, rect.height); for(var x = Math.floor(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);
} }
for(var y = 0; y <= rect.height - STEP; y += STEP) {
context.strokeRect(rect.left, rect.top + y, rect.width, STEP); for(var y = Math.floor(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.restore(); context.restore();
}; };
ImageEditor.Mode.Rotate.prototype.doTransform = ImageEditor.Mode.Rotate.prototype.modifyTransform =
function(scaleX, scaleY, turn90) { function(scaleX, scaleY, turn90) {
if (!this.backup_) {
this.backup_ = this.getBuffer().getImageCanvas();
this.transform_ = new ImageEditor.Mode.Rotate.Transform();
}
var baselineOffset = this.transform_.transformOffsetToBaseline( var transform = this.getTransform();
this.getBuffer().getOffsetX(), this.getBuffer().getOffsetY()); var viewport = this.getViewport();
var baselineOffset = transform.transformOffsetToBaseline(
viewport.getOffsetX(), viewport.getOffsetY());
this.transform_.modify(scaleX, scaleY, turn90, this.tiltRange_.getValue()); transform.modify(scaleX, scaleY, turn90, this.tiltRange_.getValue());
var newOffset = transform.transformOffsetFromBaseline(
baselineOffset.x, baselineOffset.y);
// Ignoring offset clipping makes rotation behave more naturally.
viewport.setOffset(newOffset.x, newOffset.y, true /*ignore clipping*/);
if (scaleX * scaleY < 0) { if (scaleX * scaleY < 0) {
this.tiltRange_.setValue(this.transform_.tilt); this.tiltRange_.setValue(transform.tilt);
} }
var srcCanvas = this.backup_; this.applyTransform();
};
ImageEditor.Mode.Rotate.prototype.applyTransform = function() {
var srcCanvas = this.getOriginal();
var newSize = this.transform_.getTiltedRectSize( var newSize = this.transform_.getTiltedRectSize(
srcCanvas.width, srcCanvas.height); srcCanvas.width, srcCanvas.height);
var dstCanvas = var scale = 1;
this.getBuffer().createBlankCanvas(newSize.width, newSize.height);
var context = dstCanvas.getContext("2d"); if (this.preScaledOriginal_) {
context.save(); scale = this.preScaledOriginal_.width / srcCanvas.width;
context.translate(dstCanvas.width / 2, dstCanvas.height / 2); srcCanvas = this.preScaledOriginal_;
context.rotate(this.transform_.getAngle()); }
context.scale(this.transform_.scaleX, this.transform_.scaleY);
var dstCanvas = this.getContent().createBlankCanvas(
newSize.width * scale, newSize.height * scale);
ImageUtil.trace.resetTimer('transform'); ImageUtil.trace.resetTimer('transform');
context.drawImage(srcCanvas, -srcCanvas.width / 2, -srcCanvas.height / 2); this.transform_.apply(dstCanvas.getContext('2d'), srcCanvas);
ImageUtil.trace.reportTimer('transform'); ImageUtil.trace.reportTimer('transform');
context.restore(); this.getContent().setCanvas(dstCanvas, newSize.width, newSize.height);
var newOffset = this.transform_.transformOffsetFromBaseline( this.repaint();
baselineOffset.x, baselineOffset.y);
// Ignoring offset clipping make rotation behave more natural.
this.getBuffer().setOffset(
newOffset.x, newOffset.y, true /*ignore clipping*/);
this.getBuffer().setImageCanvas(dstCanvas);
this.getBuffer().repaint();
}; };
ImageEditor.Mode.Rotate.prototype.update = function(values) { ImageEditor.Mode.Rotate.prototype.update = function(values) {
this.doTransform(1, 1, 0); this.modifyTransform(1, 1, 0);
}; };
ImageEditor.Mode.Rotate.Transform = function() { ImageEditor.Mode.Rotate.Transform = function() {
...@@ -161,7 +238,7 @@ ImageEditor.Mode.Rotate.Transform = function() { ...@@ -161,7 +238,7 @@ ImageEditor.Mode.Rotate.Transform = function() {
this.scaleY = 1; this.scaleY = 1;
this.turn90 = 0; this.turn90 = 0;
this.tilt = 0; this.tilt = 0;
} };
ImageEditor.Mode.Rotate.Transform.prototype.modify = ImageEditor.Mode.Rotate.Transform.prototype.modify =
function(scaleX, scaleY, turn90, tilt) { function(scaleX, scaleY, turn90, tilt) {
...@@ -224,6 +301,15 @@ ImageEditor.Mode.Rotate.Transform.prototype.getTiltedRectSize = ...@@ -224,6 +301,15 @@ ImageEditor.Mode.Rotate.Transform.prototype.getTiltedRectSize =
} }
}; };
ImageEditor.Mode.Rotate.Transform.prototype.apply = function(
context, srcCanvas) {
context.save();
context.translate(context.canvas.width / 2, context.canvas.height / 2);
context.rotate(this.getAngle());
context.scale(this.scaleX, this.scaleY);
context.drawImage(srcCanvas, -srcCanvas.width / 2, -srcCanvas.height / 2);
context.restore();
};
/** /**
* Crop mode. * Crop mode.
...@@ -246,17 +332,17 @@ ImageEditor.Mode.Crop.GRAB_RADIUS = 5; ...@@ -246,17 +332,17 @@ ImageEditor.Mode.Crop.GRAB_RADIUS = 5;
ImageEditor.Mode.Crop.prototype.commit = function() { ImageEditor.Mode.Crop.prototype.commit = function() {
var cropImageRect = this.cropRect_.getRect(); var cropImageRect = this.cropRect_.getRect();
var newCanvas = this.getBuffer(). var newCanvas = this.getContent().
createBlankCanvas(cropImageRect.width, cropImageRect.height); createBlankCanvas(cropImageRect.width, cropImageRect.height);
var newContext = newCanvas.getContext("2d"); var newContext = newCanvas.getContext("2d");
ImageUtil.trace.resetTimer('transform'); ImageUtil.trace.resetTimer('transform');
Rect.drawImage(newContext, this.getBuffer().getImageCanvas(), Rect.drawImage(newContext, this.getContent().getCanvas(),
new Rect(newCanvas), cropImageRect); new Rect(newCanvas), cropImageRect);
ImageUtil.trace.reportTimer('transform'); ImageUtil.trace.reportTimer('transform');
this.getBuffer().setImageCanvas(newCanvas); this.getContent().setCanvas(newCanvas);
this.getBuffer().fitImage(); this.getViewport().fitImage();
}; };
ImageEditor.Mode.Crop.prototype.rollback = function() { ImageEditor.Mode.Crop.prototype.rollback = function() {
...@@ -264,17 +350,17 @@ ImageEditor.Mode.Crop.prototype.rollback = function() { ...@@ -264,17 +350,17 @@ ImageEditor.Mode.Crop.prototype.rollback = function() {
}; };
ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() { ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
var rect = new Rect(this.getBuffer().getClippedImage()); var rect = new Rect(this.getViewport().getImageClipped());
rect = rect.inflate (-rect.width / 6, -rect.height / 6); rect = rect.inflate (-rect.width / 6, -rect.height / 6);
this.cropRect_ = new DraggableRect( this.cropRect_ = new DraggableRect(
rect, this.getBuffer(), ImageEditor.Mode.Crop.GRAB_RADIUS); rect, this.getViewport(), ImageEditor.Mode.Crop.GRAB_RADIUS);
}; };
ImageEditor.Mode.Crop.prototype.draw = function(context) { ImageEditor.Mode.Crop.prototype.draw = function(context) {
var R = ImageEditor.Mode.Crop.GRAB_RADIUS; var R = ImageEditor.Mode.Crop.GRAB_RADIUS;
var inner = this.getBuffer().imageToScreenRect(this.cropRect_.getRect()); var inner = this.getViewport().imageToScreenRect(this.cropRect_.getRect());
var outer = this.getBuffer().getClippedScreen(); var outer = this.getViewport().getScreenClipped();
var inner_bottom = inner.top + inner.height; var inner_bottom = inner.top + inner.height;
var inner_right = inner.left + inner.width; var inner_right = inner.left + inner.width;
...@@ -324,7 +410,7 @@ ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y) { ...@@ -324,7 +410,7 @@ ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y) {
* A draggable rectangle over the image. * A draggable rectangle over the image.
*/ */
function DraggableRect(rect, buffer, sensitivity) { function DraggableRect(rect, viewport, sensitivity) {
// The bounds are not held in a regular rectangle (with width/height). // The bounds are not held in a regular rectangle (with width/height).
// left/top/right/bottom held instead for convenience. // left/top/right/bottom held instead for convenience.
...@@ -334,7 +420,7 @@ function DraggableRect(rect, buffer, sensitivity) { ...@@ -334,7 +420,7 @@ function DraggableRect(rect, buffer, sensitivity) {
this.bounds_[DraggableRect.TOP] = rect.top; this.bounds_[DraggableRect.TOP] = rect.top;
this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height; this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height;
this.buffer_ = buffer; this.viewport_ = viewport;
this.sensitivity_ = sensitivity; this.sensitivity_ = sensitivity;
this.oppositeSide_ = {}; this.oppositeSide_ = {};
...@@ -350,10 +436,10 @@ function DraggableRect(rect, buffer, sensitivity) { ...@@ -350,10 +436,10 @@ function DraggableRect(rect, buffer, sensitivity) {
this.cssSide_[DraggableRect.RIGHT] = 'e'; this.cssSide_[DraggableRect.RIGHT] = 'e';
this.cssSide_[DraggableRect.BOTTOM] = 's'; this.cssSide_[DraggableRect.BOTTOM] = 's';
this.cssSide_[DraggableRect.NONE] = ''; this.cssSide_[DraggableRect.NONE] = '';
}; }
// Static members to simplify reflective access to the bounds. // Static members to simplify reflective access to the bounds.
DraggableRect.LEFT = 'left' DraggableRect.LEFT = 'left';
DraggableRect.RIGHT = 'right'; DraggableRect.RIGHT = 'right';
DraggableRect.TOP = 'top'; DraggableRect.TOP = 'top';
DraggableRect.BOTTOM = 'bottom'; DraggableRect.BOTTOM = 'bottom';
...@@ -368,7 +454,7 @@ DraggableRect.prototype.getDragMode = function(x, y) { ...@@ -368,7 +454,7 @@ DraggableRect.prototype.getDragMode = function(x, y) {
}; };
var bounds = this.bounds_; var bounds = this.bounds_;
var R = this.buffer_.screenToImageSize(this.sensitivity_); var R = this.viewport_.screenToImageSize(this.sensitivity_);
var circle = new Circle(x, y, R); var circle = new Circle(x, y, R);
...@@ -410,7 +496,7 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) { ...@@ -410,7 +496,7 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
mode = this.dragMode_; mode = this.dragMode_;
} else { } else {
mode = this.getDragMode( mode = this.getDragMode(
this.buffer_.screenToImageX(x), this.buffer_.screenToImageY(y)); this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
} }
if (mode.whole) return 'hand'; if (mode.whole) return 'hand';
if (mode.outside) return 'crosshair'; if (mode.outside) return 'crosshair';
...@@ -418,10 +504,10 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) { ...@@ -418,10 +504,10 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
}; };
DraggableRect.prototype.getDragHandler = function(x, y) { DraggableRect.prototype.getDragHandler = function(x, y) {
x = this.buffer_.screenToImageX(x); x = this.viewport_.screenToImageX(x);
y = this.buffer_.screenToImageY(y); y = this.viewport_.screenToImageY(y);
var clipRect = this.buffer_.getClippedImage(); var clipRect = this.viewport_.getImageClipped();
if (!clipRect.inside(x, y)) return null; if (!clipRect.inside(x, y)) return null;
this.dragMode_ = this.getDragMode(x, y); this.dragMode_ = this.getDragMode(x, y);
...@@ -487,16 +573,16 @@ DraggableRect.prototype.getDragHandler = function(x, y) { ...@@ -487,16 +573,16 @@ DraggableRect.prototype.getDragHandler = function(x, y) {
} }
function convertX(x) { function convertX(x) {
return ImageUtil.clip( return ImageUtil.clamp(
clipRect.left, clipRect.left,
self.buffer_.screenToImageX(x) + mouseBiasX, self.viewport_.screenToImageX(x) + mouseBiasX,
clipRect.left + clipRect.width - fixedWidth); clipRect.left + clipRect.width - fixedWidth);
} }
function convertY(y) { function convertY(y) {
return ImageUtil.clip( return ImageUtil.clamp(
clipRect.top, clipRect.top,
self.buffer_.screenToImageY(y) + mouseBiasY, self.viewport_.screenToImageY(y) + mouseBiasY,
clipRect.top + clipRect.height - fixedHeight); clipRect.top + clipRect.height - fixedHeight);
} }
......
...@@ -18,7 +18,7 @@ ImageUtil.trace = (function() { ...@@ -18,7 +18,7 @@ ImageUtil.trace = (function() {
this.container_ = container; this.container_ = container;
}; };
PerformanceTrace.prototype.report_ = function(key, value) { PerformanceTrace.prototype.report = function(key, value) {
if (!this.container_) return; if (!this.container_) return;
if (!(key in this.lines_)) { if (!(key in this.lines_)) {
var div = this.lines_[key] = document.createElement('div'); var div = this.lines_[key] = document.createElement('div');
...@@ -29,17 +29,17 @@ ImageUtil.trace = (function() { ...@@ -29,17 +29,17 @@ ImageUtil.trace = (function() {
PerformanceTrace.prototype.resetTimer = function(key) { PerformanceTrace.prototype.resetTimer = function(key) {
this.timers_[key] = Date.now(); this.timers_[key] = Date.now();
} };
PerformanceTrace.prototype.reportTimer = function(key) { PerformanceTrace.prototype.reportTimer = function(key) {
this.report_(key, (Date.now() - this.timers_[key]) + 'ms'); this.report(key, (Date.now() - this.timers_[key]) + 'ms');
}; };
return new PerformanceTrace(); return new PerformanceTrace();
})(); })();
ImageUtil.clip = function(min, value, max) { ImageUtil.clamp = function(min, value, max) {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
}; };
...@@ -169,6 +169,36 @@ Rect.prototype.inside = function(x, y) { ...@@ -169,6 +169,36 @@ Rect.prototype.inside = function(x, y) {
this.top <= y && y < this.top + this.height; this.top <= y && y < this.top + this.height;
}; };
/**
* Clamp the rectangle to the bounds by moving it.
* Decrease the size only if necessary.
*/
Rect.prototype.clamp = function(bounds) {
var rect = new Rect(this);
if (rect.width > bounds.width) {
rect.left = bounds.left;
rect.width = bounds.width;
} else if (rect.left < bounds.left){
rect.left = bounds.left;
} else if (rect.left + rect.width >
bounds.left + bounds.width) {
rect.left = bounds.left + bounds.width - rect.width;
}
if (rect.height > bounds.height) {
rect.top = bounds.top;
rect.height = bounds.height;
} else if (rect.top < bounds.top){
rect.top = bounds.top;
} else if (rect.top + rect.height >
bounds.top + bounds.height) {
rect.top = bounds.top + bounds.height - rect.height;
}
return rect;
};
/* /*
* Useful shortcuts for drawing (static functions). * Useful shortcuts for drawing (static functions).
*/ */
...@@ -176,10 +206,12 @@ Rect.prototype.inside = function(x, y) { ...@@ -176,10 +206,12 @@ Rect.prototype.inside = function(x, y) {
/** /**
* Draws the image in context with appropriate scaling. * Draws the image in context with appropriate scaling.
*/ */
Rect.drawImage = function(context, image, dstRect, srcRect) { Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
opt_dstRect = opt_dstRect || new Rect(context.canvas);
opt_srcRect = opt_srcRect || new Rect(image);
context.drawImage(image, context.drawImage(image,
srcRect.left, srcRect.top, srcRect.width, srcRect.height, opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height,
dstRect.left, dstRect.top, dstRect.width, dstRect.height); opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height);
}; };
/** /**
...@@ -223,7 +255,7 @@ function Circle(x, y, R) { ...@@ -223,7 +255,7 @@ function Circle(x, y, R) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.squaredR = R * R; this.squaredR = R * R;
}; }
Circle.prototype.inside = function(x, y) { Circle.prototype.inside = function(x, y) {
x -= this.x; x -= this.x;
......
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Viewport class controls the way the image is displayed (scale, offset etc).
*/
function Viewport(repaintCallback) {
this.repaintCallback_ = repaintCallback;
this.imageBounds_ = new Rect();
this.screenBounds_ = new Rect();
this.scale_ = 1;
this.offsetX_ = 0;
this.offsetY_ = 0;
this.generation_ = 0;
this.scaleControl_ = null;
this.update();
}
/*
* Viewport modification.
*/
Viewport.prototype.setScaleControl = function(scaleControl) {
this.scaleControl_ = scaleControl;
};
Viewport.prototype.setImageSize = function(width, height) {
this.imageBounds_ = new Rect(width, height);
if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height);
this.invalidateCaches();
};
Viewport.prototype.setScreenSize = function(width, height) {
this.screenBounds_ = new Rect(width, height);
if (this.scaleControl_)
this.scaleControl_.setMinScale(this.getFittingScale());
this.invalidateCaches();
};
Viewport.prototype.getScale = function() { return this.scale_ };
Viewport.prototype.setScale = function(scale, notify) {
if (this.scale_ == scale) return;
this.scale_ = scale;
if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale);
this.invalidateCaches();
};
Viewport.prototype.getFittingScale = function() {
var scaleX = this.screenBounds_.width / this.imageBounds_.width;
var scaleY = this.screenBounds_.height / this.imageBounds_.height;
return Math.min(scaleX, scaleY) * 0.85;
};
Viewport.prototype.fitImage = function() {
var scale = this.getFittingScale();
if (this.scaleControl_) this.scaleControl_.setMinScale(scale);
this.setScale(scale, true);
};
Viewport.prototype.getOffsetX = function () { return this.offsetX_ };
Viewport.prototype.getOffsetY = function () { return this.offsetY_ };
Viewport.prototype.setOffset = function(x, y, ignoreClipping) {
if (!ignoreClipping) {
x = this.clampOffsetX_(x);
y = this.clampOffsetY_(y);
}
if (this.offsetX_ == x && this.offsetY_ == y) return;
this.offsetX_ = x;
this.offsetY_ = y;
this.invalidateCaches();
};
Viewport.prototype.setCenter = function(x, y, ignoreClipping) {
this.setOffset(
this.imageBounds_.width / 2 - x,
this.imageBounds_.height / 2 - 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
* corresponds to zero change to offsetX.
* @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.
*/
Viewport.prototype.createOffsetSetter = function (
originalX, originalY, scaleFunc, hitFunc) {
var originalOffsetX = this.offsetX_;
var originalOffsetY = this.offsetY_;
if (!hitFunc) hitFunc = function() { return true };
if (!scaleFunc) scaleFunc = this.getScale.bind(this);
var self = this;
return function(x, y) {
if (hitFunc(x, y)) {
var scale = scaleFunc();
self.setOffset(
originalOffsetX + (x - originalX) / scale,
originalOffsetY + (y - originalY) / scale);
self.repaint();
}
};
};
/*
* Access to the current viewport state.
*/
/**
* @return {Rect} The image bounds in image coordinates.
*/
Viewport.prototype.getImageBounds = function() { return this.imageBounds_ };
/**
* @return {Rect} The screen bounds in screen coordinates.
*/
Viewport.prototype.getScreenBounds = function() { return this.screenBounds_ };
/**
* @return {Rect} The visible part of the image, in image coordinates.
*/
Viewport.prototype.getImageClipped = function() { return this.imageClipped_ };
/**
* @return {Rect} The visible part of the image, in screen coordinates.
*/
Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ };
/**
* A counter that is incremented with each viewport state change.
* Clients that cache anything that depends on the viewport state should keep
* track of this counter.
*/
Viewport.prototype.getCacheGeneration = function() { return this.generation_ };
/**
* Called on evert view port state change (even if repaint has not been called).
*/
Viewport.prototype.invalidateCaches = function() { this.generation_++ };
/**
* @return {Rect} The image bounds in screen coordinates.
*/
Viewport.prototype.getImageBoundsOnScreen = function() {
return this.imageOnScreen_;
};
/*
* Conversion between the screen and image coordinate spaces.
*/
Viewport.prototype.screenToImageSize = function(size) {
return size / this.getScale();
};
Viewport.prototype.screenToImageX = function(x) {
return Math.round((x - this.imageOnScreen_.left) / this.getScale());
};
Viewport.prototype.screenToImageY = function(y) {
return Math.round((y - this.imageOnScreen_.top) / this.getScale());
};
Viewport.prototype.screenToImageRect = function(rect) {
return new Rect(
this.screenToImageX(rect.left),
this.screenToImageY(rect.top),
this.screenToImageSize(rect.width),
this.screenToImageSize(rect.height));
};
Viewport.prototype.imageToScreenSize = function(size) {
return size * this.getScale();
};
Viewport.prototype.imageToScreenX = function(x) {
return Math.round(this.imageOnScreen_.left + x * this.getScale());
};
Viewport.prototype.imageToScreenY = function(y) {
return Math.round(this.imageOnScreen_.top + y * this.getScale());
};
Viewport.prototype.imageToScreenRect = function(rect) {
return new Rect(
this.imageToScreenX(rect.left),
this.imageToScreenY(rect.top),
this.imageToScreenSize(rect.width),
this.imageToScreenSize(rect.height));
};
/**
* @return {Boolean} True if some part of the image is clipped by the screen.
*/
Viewport.prototype.isClipped = function () {
return this.getMarginX_() < 0 || this.getMarginY_() < 0;
};
/**
* Horizontal margin. Negative if the image is clipped horizontally.
*/
Viewport.prototype.getMarginX_ = function() {
return Math.floor(
(this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
};
/**
* Vertical margin. Negative if the image is clipped vertically.
*/
Viewport.prototype.getMarginY_ = function() {
return Math.floor(
(this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
};
Viewport.prototype.clampOffsetX_ = function(x) {
var limit = 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());
return ImageUtil.clamp(-limit, y, limit);
};
/**
* Recalculate the viewport parameters.
*/
Viewport.prototype.update = function() {
var scale = this.getScale();
// Image bounds in screen coordinates.
this.imageOnScreen_ = new Rect(
this.getMarginX_(),
this.getMarginY_(),
Math.floor(this.imageBounds_.width * scale),
Math.floor(this.imageBounds_.height * scale));
// A visible part of the image in image coordinates.
this.imageClipped_ = new Rect(this.imageBounds_);
// A visible part of the image in screen coordinates.
this.screenClipped_ = new Rect(this.screenBounds_);
// 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;
} 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;
} else {
this.screenClipped_.top = this.imageOnScreen_.top;
this.screenClipped_.height = this.imageOnScreen_.height;
}
};
Viewport.prototype.repaint = function () {
if (this.repaintCallback_) this.repaintCallback_();
};
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