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();
};
......
......@@ -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 = '';
......
......@@ -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