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};
ImageEditor.Mode.Adjust.prototype.rollback = function() {
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.repaint();
};
ImageEditor.Mode.Adjust.prototype.update = function(options) {
if (!this.backup_) {
this.backup_ = this.getBuffer().copyImageData();
this.scratch_ = this.getBuffer().copyImageData();
this.backup_ = this.getContent().copyImageData();
this.scratch_ = this.getContent().copyImageData();
}
ImageUtil.trace.resetTimer('filter');
this.filterFunc_(this.scratch_, this.backup_, options);
ImageUtil.trace.reportTimer('filter');
this.getBuffer().drawImageData(this.scratch_);
this.getContent().drawImageData(this.scratch_, 0, 0);
this.repaint();
};
......
......@@ -10,224 +10,22 @@
*/
function ImageBuffer(screenCanvas) {
this.screenCanvas_ = screenCanvas;
this.screenContext_ = this.screenCanvas_.getContext("2d");
this.scale_ = 1;
this.offsetX_ = 0;
this.offsetY_ = 0;
this.viewport_ = new Viewport(this.repaint.bind(this));
this.viewport_.setScreenSize(screenCanvas.width, screenCanvas.height);
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_));
}
/*
* Viewport manipulation.
*/
ImageBuffer.prototype.setScaleControl = function(scaleControl) {
this.scaleControl_ = scaleControl;
};
ImageBuffer.prototype.getViewport = function() { return this.viewport_ };
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));
};
/*
* Content manipulation.
*/
ImageBuffer.prototype.getContent = function() { return this.content_ };
/**
* Loads the new content.
......@@ -241,123 +39,43 @@ ImageBuffer.prototype.load = function(source) {
image.onload = function(e) { self.load(e.target); };
image.src = source;
} else {
this.imageCanvas_.width = source.width,
this.imageCanvas_.height = source.height;
this.drawImage(source);
if (this.scaleControl_)
this.scaleControl_.displayImageSize(
this.imageCanvas_.width, this.imageCanvas_.height);
this.fitImage();
this.content_.load(source);
this.repaint();
}
};
ImageBuffer.prototype.getImageCanvas = function() { return this.imageCanvas_; };
/**
* 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;
};
ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) {
this.screenCanvas_.width = width;
this.screenCanvas_.height = height;
/**
* @return {ImageData} A new ImageData object with a copy of the content.
*/
ImageBuffer.prototype.copyImageData = function () {
return this.imageContext_.getImageData(
0, 0, this.imageCanvas_.width, this.imageCanvas_.height);
};
var wasFitting =
this.viewport_.getScale() == this.viewport_.getFittingScale();
/**
* @param {HTMLImageElement|HTMLCanvasElement} image
*/
ImageBuffer.prototype.drawImage = function(image) {
ImageUtil.trace.resetTimer('drawImage');
this.imageContext_.drawImage(image, 0, 0);
ImageUtil.trace.reportTimer('drawImage');
};
this.viewport_.setScreenSize(width, height);
/**
* @param {ImageData} imageData
*/
ImageBuffer.prototype.drawImageData = function (imageData) {
ImageUtil.trace.resetTimer('putImageData');
this.imageContext_.putImageData(imageData, 0, 0);
ImageUtil.trace.reportTimer('putImageData');
var minScale = this.viewport_.getFittingScale();
if ((wasFitting && keepFitting) || this.viewport_.getScale() < minScale) {
this.viewport_.setScale(minScale, true);
}
this.repaint();
};
/**
* Paints the content on the screen canvas taking the current scale and offset
* into account.
*/
ImageBuffer.prototype.repaint = function () {
ImageUtil.trace.resetTimer('repaint');
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.repaint = function (opt_fromOverlay) {
this.viewport_.update();
this.drawOverlays(this.screenCanvas_.getContext("2d"), opt_fromOverlay);
};
/**
* 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.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 };
ImageBuffer.prototype.repaintScreenRect = function (screenRect, imageRect) {
Rect.drawImage(
this.screenCanvas_.getContext('2d'),
this.getContent().getCanvas(),
screenRect || this.getViewport().imageToScreenRect(screenRect),
imageRect || this.getViewport().screenToImageRect(screenRect));
};
/**
* @param {ImageBuffer.Overlay} overlay
......@@ -385,22 +103,19 @@ ImageBuffer.prototype.removeOverlay = function (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.
* 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++) {
var overlay = this.overlays_[i];
if (!opt_fromOverlay || opt_fromOverlay == overlay) skip = false;
if (skip) continue;
context.save();
this.overlays_[i].draw(context);
overlay.draw(context);
context.restore();
}
};
......@@ -414,11 +129,6 @@ ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) {
var style = this.overlays_[i].getCursorStyle(x, y, mouseDown);
if (style) return style;
}
// Indicate that the image is draggable.
if (!this.isFullyVisible() && this.screenVisible_.inside(x, y))
return 'move';
return 'default';
};
......@@ -442,116 +152,279 @@ ImageBuffer.prototype.getDragHandler = function (x, y) {
var handler = this.overlays_[i].getDragHandler(x, y);
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';
if (!this.isFullyVisible() && this.screenVisible_.inside(x, y)) {
return null;
};
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 this.createOffsetSetter_(x, y, this.getScale.bind(this));
return this.viewport_.createOffsetSetter(x, y);
}
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.
* Indicates the currently visible part.
* Supports panning by dragging.
* Replaces the off-screen canvas.
* To be used when the editor modifies the image dimensions.
* 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;
ImageBuffer.Overview.BOTTOM = 50;
/**
* @return {HTMLCanvasElement} A new blank canvas of the required size.
*/
ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) {
var canvas = this.document_.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype};
/**
* @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.getZIndex = function() { return 100; }
ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype};
ImageBuffer.Overview.prototype.updateViewPort = function(buffer) {
this.buffer_ = buffer;
// Draw above everything.
ImageBuffer.Overview.prototype.getZIndex = function() { return 100 };
this.whole_ = null;
this.visible_ = null;
ImageBuffer.Overview.MAX_SIZE = 150;
ImageBuffer.Overview.RIGHT = 7;
ImageBuffer.Overview.BOTTOM = 50;
if (this.buffer_.isFullyVisible()) return;
ImageBuffer.Overview.prototype.update = function() {
var imageBounds = this.viewport_.getImageBounds();
var screenWhole = this.buffer_.screenWhole_;
var imageWhole = this.buffer_.imageWhole_;
var imageVisible = this.buffer_.imageVisible_;
if (this.contentGeneration_ != this.content_.getCacheGeneration()) {
this.contentGeneration_ = this.content_.getCacheGeneration();
var aspect = imageWhole.width / imageWhole.height;
var aspect = imageBounds.width / imageBounds.height;
if (aspect > 1) {
this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE,
this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE,
ImageBuffer.Overview.MAX_SIZE / aspect);
} else {
this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect,
this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect,
ImageBuffer.Overview.MAX_SIZE);
}
this.whole_ = this.whole_.moveTo(
screenWhole.width - ImageBuffer.Overview.RIGHT - this.whole_.width,
screenWhole.height - ImageBuffer.Overview.BOTTOM - this.whole_.height);
this.canvas_ =
this.content_.copyCanvas(this.bounds_.width, this.bounds_.height);
}
this.clipped_ = null;
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;
this.clipped_ = this.viewport_.getImageClipped().
scale(this.scale_).
shift(this.whole_.left, this.whole_.top);
shift(this.bounds_.left, this.bounds_.top);
}
};
ImageBuffer.Overview.prototype.draw = function(context) {
if (!this.visible_) return;
this.update();
if (!this.clipped_) return;
// Draw the thumbnail.
Rect.drawImage(context, this.buffer_.imageCanvas_,
this.whole_, this.buffer_.imageWhole_);
Rect.drawImage(context, this.canvas_, this.bounds_);
// Draw the thumbnail border.
context.strokeStyle = '#000000';
Rect.stroke(context, this.whole_);
Rect.stroke(context, this.bounds_);
// Draw the shadow over the off-screen part of the thumbnail.
context.globalAlpha = 0.3;
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.
context.strokeStyle = '#FFFFFF';
Rect.stroke(context, this.visible_);
Rect.stroke(context, this.clipped_);
};
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.
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';
};
ImageBuffer.Overview.prototype.onClick = function(x, y) {
if (!this.whole_ || !this.whole_.inside(x, y)) return false;
if (this.visible_.inside(x, y)) return false;
this.buffer_.setCenter(
(x - this.whole_.left) / this.scale_,
(y - this.whole_.top) / this.scale_);
this.buffer_.repaint();
if (this.getCursorStyle(x, y) != 'crosshair') return false;
this.viewport_.setCenter(
(x - this.bounds_.left) / this.scale_,
(y - this.bounds_.top) / this.scale_);
this.viewport_.repaint();
return true;
};
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;
function scale() { return -self.scale_;}
function hit(x, y) { return self.whole_ && self.whole_.inside(x, y); }
return this.buffer_.createOffsetSetter_(x, y, scale, hit);
} else {
function hit(x, y) { return self.bounds_ && self.bounds_.inside(x, y); }
return this.viewport_.createOffsetSetter(x, y, scale, hit);
} else if (cursor == 'crosshair') {
// Force non-draggable behavior.
return function() {};
} else {
return null;
}
};
......@@ -15,6 +15,7 @@
<link rel="stylesheet" type="text/css" href="image_editor.css"/>
<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_editor.js"></script>
<script type="text/javascript" src="image_transform.js"></script>
......
......@@ -28,12 +28,11 @@ function ImageEditor(container, saveCallback, closeCallback) {
canvas.height = this.canvasWrapper_.clientHeight;
this.buffer_ = new ImageBuffer(canvas);
this.buffer_.addOverlay(new ImageBuffer.Overview());
this.scaleControl_ =
new ImageEditor.ScaleControl(this.canvasWrapper_, this.buffer_);
this.scaleControl_ = new ImageEditor.ScaleControl(
this.canvasWrapper_, this.getBuffer().getViewport());
this.panControl_ = new ImageEditor.MouseControl(canvas, this.buffer_);
this.panControl_ = new ImageEditor.MouseControl(canvas, this.getBuffer());
this.toolbar_ =
new ImageEditor.Toolbar(container, this.onOptionsChange.bind(this));
......@@ -57,7 +56,7 @@ ImageEditor.open = function(saveCallback, closeCallback, source, opt_metadata) {
window.addEventListener('resize', editor.resizeFrame.bind(editor), false);
editor.load(source, opt_metadata);
return editor;
}
};
/**
* Loads a new image and its metadata.
......@@ -98,7 +97,7 @@ ImageEditor.prototype.close = function() {
*/
ImageEditor.prototype.save = function() {
this.saveCallback_(ImageEncoder.getBlob(
this.getBuffer().getImageCanvas(), 'image/jpeg', this.metadata_));
this.getBuffer().getContent().getCanvas(), 'image/jpeg', this.metadata_));
};
ImageEditor.prototype.onOptionsChange = function(options) {
......@@ -124,7 +123,7 @@ ImageEditor.prototype.initToolbar = function() {
ImageEditor.Mode = function(displayName) {
this.displayName = displayName;
}
};
ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
......@@ -132,8 +131,16 @@ ImageEditor.Mode.prototype.getBuffer = function() {
return this.buffer_;
};
ImageEditor.Mode.prototype.repaint = function() {
return this.getBuffer().repaint();
ImageEditor.Mode.prototype.repaint = function(opt_fromOverlay) {
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() {
*/
ImageEditor.Mode.prototype.setUp = function(buffer) {
this.buffer_ = buffer;
this.viewport_ = buffer.getViewport();
this.content_ = buffer.getContent();
this.buffer_.addOverlay(this);
};
......@@ -176,7 +185,7 @@ ImageEditor.Mode.constructors = [];
ImageEditor.Mode.register = function(constructor) {
ImageEditor.Mode.constructors.push(constructor);
}
};
ImageEditor.prototype.createModeButtons = function() {
for (var i = 0; i != ImageEditor.Mode.constructors.length; i++) {
......@@ -236,9 +245,9 @@ ImageEditor.prototype.onModeReset = function() {
/**
* Scale control for an ImageBuffer.
*/
ImageEditor.ScaleControl = function(parent, buffer) {
this.buffer_ = buffer;
this.buffer_.setScaleControl(this);
ImageEditor.ScaleControl = function(parent, viewport) {
this.viewport_ = viewport;
this.viewport_.setScaleControl(this);
var div = parent.ownerDocument.createElement('div');
div.className = 'scale-tool';
......@@ -319,10 +328,10 @@ ImageEditor.ScaleControl.prototype.displayScale = function(scale) {
* Called when the user changes the scale via the controls.
*/
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.buffer_.setScale(scale / ImageEditor.ScaleControl.FACTOR, false);
this.buffer_.repaint();
this.viewport_.setScale(scale / ImageEditor.ScaleControl.FACTOR, false);
this.viewport_.repaint();
};
ImageEditor.ScaleControl.prototype.updateSlider = function(scale) {
......@@ -365,8 +374,8 @@ ImageEditor.ScaleControl.prototype.onUpButton = function () {
};
ImageEditor.ScaleControl.prototype.onFitButton = function () {
this.buffer_.fitImage();
this.buffer_.repaint();
this.viewport_.fitImage();
this.viewport_.repaint();
};
/**
......@@ -431,7 +440,7 @@ ImageEditor.Toolbar = function (parent, updateCallback) {
this.wrapper_.className = 'toolbar';
parent.appendChild(this.wrapper_);
this.updateCallback_ = updateCallback;
}
};
ImageEditor.Toolbar.prototype.clear = function() {
this.wrapper_.innerHTML = '';
......
......@@ -15,7 +15,7 @@ ImageEditor.Mode.Resize.prototype = {__proto__: ImageEditor.Mode.prototype};
ImageEditor.Mode.register(ImageEditor.Mode.Resize);
ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) {
var canvas = this.getBuffer().getImageCanvas();
var canvas = this.getContent().getCanvas();
this.widthRange_ =
toolbar.addRange('width', 0, canvas.width, canvas.width * 2);
this.heightRange_ =
......@@ -23,19 +23,12 @@ ImageEditor.Mode.Resize.prototype.createTools = function(toolbar) {
};
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');
Rect.drawImage(
context, srcCanvas, new Rect(newCanvas), new Rect(srcCanvas));
var newCanvas = this.getContent().copyCanvas(
this.widthRange_.getValue(), this.heightRange_.getValue());
ImageUtil.trace.reportTimer('transform');
this.getBuffer().setImageCanvas(newCanvas);
this.getBuffer().fitImage();
this.getContent().setCanvas(newCanvas);
this.getViewport().fitImage();
};
/**
......@@ -50,25 +43,23 @@ ImageEditor.Mode.Rotate.prototype = {__proto__: ImageEditor.Mode.prototype};
ImageEditor.Mode.register(ImageEditor.Mode.Rotate);
ImageEditor.Mode.Rotate.prototype.commit = function() {
this.getBuffer().fitImage();
};
ImageEditor.Mode.Rotate.prototype.commit = function() {};
ImageEditor.Mode.Rotate.prototype.rollback = function() {
if (this.backup_) {
this.getBuffer().setImageCanvas(this.backup_);
this.getContent().setCanvas(this.backup_);
this.backup_ = null;
this.transform_ = null;
}
this.transform_ = null;
};
ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) {
toolbar.addButton("Left", this.doTransform.bind(this, 1, 1, 3));
toolbar.addButton("Right", this.doTransform.bind(this, 1, 1, 1));
toolbar.addButton("Flip V", this.doTransform.bind(this, 1, -1, 0));
toolbar.addButton("Flip H", this.doTransform.bind(this, -1, 1, 0));
toolbar.addButton("Left", this.modifyTransform.bind(this, 1, 1, 3));
toolbar.addButton("Right", this.modifyTransform.bind(this, 1, 1, 1));
toolbar.addButton("Flip V", this.modifyTransform.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 height = srcCanvas.height;
......@@ -81,79 +72,165 @@ ImageEditor.Mode.Rotate.prototype.createTools = function(toolbar) {
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() {
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();
};
ImageEditor.Mode.Rotate.prototype.onTiltStop = function() {
this.tiltDrag_ = false;
if (this.preScaledOriginal_) {
this.preScaledOriginal_ = false;
this.preClippedOriginal_ = false;
this.applyTransform();
} else {
this.repaint();
}
};
ImageEditor.Mode.Rotate.prototype.draw = function(context) {
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;
context.save();
context.globalAlpha = 0.4;
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();
};
ImageEditor.Mode.Rotate.prototype.doTransform =
ImageEditor.Mode.Rotate.prototype.modifyTransform =
function(scaleX, scaleY, turn90) {
if (!this.backup_) {
this.backup_ = this.getBuffer().getImageCanvas();
this.transform_ = new ImageEditor.Mode.Rotate.Transform();
}
var baselineOffset = this.transform_.transformOffsetToBaseline(
this.getBuffer().getOffsetX(), this.getBuffer().getOffsetY());
var transform = this.getTransform();
var viewport = this.getViewport();
this.transform_.modify(scaleX, scaleY, turn90, this.tiltRange_.getValue());
var baselineOffset = transform.transformOffsetToBaseline(
viewport.getOffsetX(), viewport.getOffsetY());
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) {
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(
srcCanvas.width, srcCanvas.height);
var dstCanvas =
this.getBuffer().createBlankCanvas(newSize.width, newSize.height);
var scale = 1;
var context = dstCanvas.getContext("2d");
context.save();
context.translate(dstCanvas.width / 2, dstCanvas.height / 2);
context.rotate(this.transform_.getAngle());
context.scale(this.transform_.scaleX, this.transform_.scaleY);
if (this.preScaledOriginal_) {
scale = this.preScaledOriginal_.width / srcCanvas.width;
srcCanvas = this.preScaledOriginal_;
}
var dstCanvas = this.getContent().createBlankCanvas(
newSize.width * scale, newSize.height * scale);
ImageUtil.trace.resetTimer('transform');
context.drawImage(srcCanvas, -srcCanvas.width / 2, -srcCanvas.height / 2);
this.transform_.apply(dstCanvas.getContext('2d'), srcCanvas);
ImageUtil.trace.reportTimer('transform');
context.restore();
this.getContent().setCanvas(dstCanvas, newSize.width, newSize.height);
var newOffset = this.transform_.transformOffsetFromBaseline(
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();
this.repaint();
};
ImageEditor.Mode.Rotate.prototype.update = function(values) {
this.doTransform(1, 1, 0);
this.modifyTransform(1, 1, 0);
};
ImageEditor.Mode.Rotate.Transform = function() {
......@@ -161,7 +238,7 @@ ImageEditor.Mode.Rotate.Transform = function() {
this.scaleY = 1;
this.turn90 = 0;
this.tilt = 0;
}
};
ImageEditor.Mode.Rotate.Transform.prototype.modify =
function(scaleX, scaleY, turn90, tilt) {
......@@ -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.
......@@ -246,17 +332,17 @@ ImageEditor.Mode.Crop.GRAB_RADIUS = 5;
ImageEditor.Mode.Crop.prototype.commit = function() {
var cropImageRect = this.cropRect_.getRect();
var newCanvas = this.getBuffer().
var newCanvas = this.getContent().
createBlankCanvas(cropImageRect.width, cropImageRect.height);
var newContext = newCanvas.getContext("2d");
ImageUtil.trace.resetTimer('transform');
Rect.drawImage(newContext, this.getBuffer().getImageCanvas(),
Rect.drawImage(newContext, this.getContent().getCanvas(),
new Rect(newCanvas), cropImageRect);
ImageUtil.trace.reportTimer('transform');
this.getBuffer().setImageCanvas(newCanvas);
this.getBuffer().fitImage();
this.getContent().setCanvas(newCanvas);
this.getViewport().fitImage();
};
ImageEditor.Mode.Crop.prototype.rollback = function() {
......@@ -264,17 +350,17 @@ ImageEditor.Mode.Crop.prototype.rollback = 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);
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) {
var R = ImageEditor.Mode.Crop.GRAB_RADIUS;
var inner = this.getBuffer().imageToScreenRect(this.cropRect_.getRect());
var outer = this.getBuffer().getClippedScreen();
var inner = this.getViewport().imageToScreenRect(this.cropRect_.getRect());
var outer = this.getViewport().getScreenClipped();
var inner_bottom = inner.top + inner.height;
var inner_right = inner.left + inner.width;
......@@ -324,7 +410,7 @@ ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y) {
* 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).
// left/top/right/bottom held instead for convenience.
......@@ -334,7 +420,7 @@ function DraggableRect(rect, buffer, sensitivity) {
this.bounds_[DraggableRect.TOP] = rect.top;
this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height;
this.buffer_ = buffer;
this.viewport_ = viewport;
this.sensitivity_ = sensitivity;
this.oppositeSide_ = {};
......@@ -350,10 +436,10 @@ function DraggableRect(rect, buffer, sensitivity) {
this.cssSide_[DraggableRect.RIGHT] = 'e';
this.cssSide_[DraggableRect.BOTTOM] = 's';
this.cssSide_[DraggableRect.NONE] = '';
};
}
// Static members to simplify reflective access to the bounds.
DraggableRect.LEFT = 'left'
DraggableRect.LEFT = 'left';
DraggableRect.RIGHT = 'right';
DraggableRect.TOP = 'top';
DraggableRect.BOTTOM = 'bottom';
......@@ -368,7 +454,7 @@ DraggableRect.prototype.getDragMode = function(x, y) {
};
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);
......@@ -410,7 +496,7 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
mode = this.dragMode_;
} else {
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.outside) return 'crosshair';
......@@ -418,10 +504,10 @@ DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
};
DraggableRect.prototype.getDragHandler = function(x, y) {
x = this.buffer_.screenToImageX(x);
y = this.buffer_.screenToImageY(y);
x = this.viewport_.screenToImageX(x);
y = this.viewport_.screenToImageY(y);
var clipRect = this.buffer_.getClippedImage();
var clipRect = this.viewport_.getImageClipped();
if (!clipRect.inside(x, y)) return null;
this.dragMode_ = this.getDragMode(x, y);
......@@ -487,16 +573,16 @@ DraggableRect.prototype.getDragHandler = function(x, y) {
}
function convertX(x) {
return ImageUtil.clip(
return ImageUtil.clamp(
clipRect.left,
self.buffer_.screenToImageX(x) + mouseBiasX,
self.viewport_.screenToImageX(x) + mouseBiasX,
clipRect.left + clipRect.width - fixedWidth);
}
function convertY(y) {
return ImageUtil.clip(
return ImageUtil.clamp(
clipRect.top,
self.buffer_.screenToImageY(y) + mouseBiasY,
self.viewport_.screenToImageY(y) + mouseBiasY,
clipRect.top + clipRect.height - fixedHeight);
}
......
......@@ -18,7 +18,7 @@ ImageUtil.trace = (function() {
this.container_ = container;
};
PerformanceTrace.prototype.report_ = function(key, value) {
PerformanceTrace.prototype.report = function(key, value) {
if (!this.container_) return;
if (!(key in this.lines_)) {
var div = this.lines_[key] = document.createElement('div');
......@@ -29,17 +29,17 @@ ImageUtil.trace = (function() {
PerformanceTrace.prototype.resetTimer = function(key) {
this.timers_[key] = Date.now();
}
};
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();
})();
ImageUtil.clip = function(min, value, max) {
ImageUtil.clamp = function(min, value, max) {
return Math.max(min, Math.min(max, value));
};
......@@ -169,6 +169,36 @@ Rect.prototype.inside = function(x, y) {
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).
*/
......@@ -176,10 +206,12 @@ Rect.prototype.inside = function(x, y) {
/**
* 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,
srcRect.left, srcRect.top, srcRect.width, srcRect.height,
dstRect.left, dstRect.top, dstRect.width, dstRect.height);
opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height,
opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height);
};
/**
......@@ -223,7 +255,7 @@ function Circle(x, y, R) {
this.x = x;
this.y = y;
this.squaredR = R * R;
};
}
Circle.prototype.inside = function(x, y) {
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