Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(155)

Side by Side Diff: chrome/browser/resources/file_manager/js/image_editor/image_buffer.js

Issue 7541075: Pre-scaling images to speed up feedback in Image Editor. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Addressed comments Created 9 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * The ImageBuffer object holds an offscreen canvas object and 6 * The ImageBuffer object holds an offscreen canvas object and
7 * draws its content on the screen canvas applying scale and offset. 7 * draws its content on the screen canvas applying scale and offset.
8 * Supports pluggable overlays that modify the image appearance and behavior. 8 * Supports pluggable overlays that modify the image appearance and behavior.
9 * @constructor 9 * @constructor
10 */ 10 */
11 function ImageBuffer(screenCanvas) { 11 function ImageBuffer(screenCanvas) {
12 this.screenCanvas_ = screenCanvas; 12 this.screenCanvas_ = screenCanvas;
13 this.screenContext_ = this.screenCanvas_.getContext("2d");
14 13
15 this.scale_ = 1; 14 this.viewport_ = new Viewport(this.repaint.bind(this));
16 this.offsetX_ = 0; 15 this.viewport_.setScreenSize(screenCanvas.width, screenCanvas.height);
17 this.offsetY_ = 0; 16
17 this.content_ = new ImageBuffer.Content(
18 this.viewport_, screenCanvas.ownerDocument);
18 19
19 this.overlays_ = []; 20 this.overlays_ = [];
20 21 this.addOverlay(new ImageBuffer.Margin(this.viewport_));
21 this.setImageCanvas(this.createBlankCanvas(0, 0)); 22 this.addOverlay(this.content_);
23 this.addOverlay(new ImageBuffer.Overview(this.viewport_, this.content_));
22 } 24 }
23 25
24 /* 26 ImageBuffer.prototype.getViewport = function() { return this.viewport_ };
25 * Viewport manipulation.
26 */
27 27
28 ImageBuffer.prototype.setScaleControl = function(scaleControl) { 28 ImageBuffer.prototype.getContent = function() { return this.content_ };
29 this.scaleControl_ = scaleControl;
30 };
31
32 ImageBuffer.prototype.getScale = function () { return this.scale_ };
33
34 ImageBuffer.prototype.setScale = function (scale, notify) {
35 if (this.scale_ == scale) return;
36 this.scale_ = scale;
37 if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale);
38 };
39
40 ImageBuffer.prototype.getFittingScale = function() {
41 var scaleX = this.screenCanvas_.width / this.imageCanvas_.width;
42 var scaleY = this.screenCanvas_.height / this.imageCanvas_.height;
43 return Math.min(scaleX, scaleY) * 0.85;
44 };
45
46 ImageBuffer.prototype.fitImage = function() {
47 var scale = this.getFittingScale();
48 if (this.scaleControl_) this.scaleControl_.setMinScale(scale);
49 this.setScale(scale, true);
50 };
51
52 ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) {
53 var wasFitting = this.getScale() == this.getFittingScale();
54
55 this.screenCanvas_.width = width;
56 this.screenCanvas_.height = height;
57
58 var minScale = this.getFittingScale();
59 if (this.scaleControl_) this.scaleControl_.setMinScale(minScale);
60 if ((wasFitting && keepFitting) || this.getScale() < minScale) {
61 this.setScale(minScale, true);
62 }
63 this.repaint();
64 };
65
66 ImageBuffer.prototype.getOffsetX = function () { return this.offsetX_; };
67
68 ImageBuffer.prototype.getOffsetY = function () { return this.offsetY_; };
69
70 ImageBuffer.prototype.setOffset = function(x, y, ignoreClipping) {
71 if (!ignoreClipping) {
72 var limitX = Math.max(0, -this.marginX_ / this.getScale());
73 var limitY = Math.max(0, -this.marginY_ / this.getScale());
74 x = ImageUtil.clip(-limitX, x, limitX);
75 y = ImageUtil.clip(-limitY, y, limitY);
76 }
77 if (this.offsetX_ == x && this.offsetY_ == y) return;
78 this.offsetX_ = x;
79 this.offsetY_ = y;
80 };
81
82 ImageBuffer.prototype.setCenter = function(x, y, ignoreClipping) {
83 this.setOffset(
84 this.imageWhole_.width / 2 - x,
85 this.imageWhole_.height / 2 - y,
86 ignoreClipping);
87 };
88
89 /**
90 * @return {Rect} The visible part of the image, in image coordinates.
91 */
92 ImageBuffer.prototype.getClippedImage = function() {
93 return this.imageVisible_;
94 };
95
96 /**
97 * @return {Rect} The visible part of the image, in screen coordinates.
98 */
99 ImageBuffer.prototype.getClippedScreen = function() {
100 return this.screenVisible_;
101 };
102
103 /**
104 * Returns a closure that can be called to pan the image.
105 * Useful for implementing non-trivial variants of panning (overview etc).
106 * @param {Number} originalX The x coordinate on the screen canvas that
107 * corresponds to zero change to offsetX.
108 * @param {Number} originalY The y coordinate on the screen canvas that
109 * corresponds to zero change to offsetY.
110 * @param {Function} scaleFunc returns the current image to screen scale.
111 * @param {Function} hitFunc returns true if (x,y) is in the valid region.
112 */
113 ImageBuffer.prototype.createOffsetSetter_ = function (
114 originalX, originalY, scaleFunc, hitFunc) {
115 var self = this;
116 var originalOffsetX = this.offsetX_;
117 var originalOffsetY = this.offsetY_;
118 if (!hitFunc) hitFunc = function() { return true; }
119 if (!scaleFunc) scaleFunc = this.getScale.bind(this);
120 return function(x, y) {
121 if (hitFunc(x, y)) {
122 var scale = scaleFunc();
123 self.setOffset(
124 originalOffsetX + (x - originalX) / scale,
125 originalOffsetY + (y - originalY) / scale);
126 self.repaint();
127 }
128 };
129 };
130
131 /**
132 * @return {Boolean} True if the entire image is visible on the screen canvas.
133 */
134 ImageBuffer.prototype.isFullyVisible = function () {
135 return this.marginX_ >= 0 && this.marginY_ >= 0;
136 };
137
138 ImageBuffer.prototype.updateViewPort = function () {
139 var scale = this.getScale();
140
141 this.screenWhole_ = new Rect(this.screenCanvas_);
142 this.imageWhole_ = new Rect(this.imageCanvas_);
143
144 // Image bounds in screen coordinates.
145 this.imageOnScreen_ = {};
146
147 this.imageOnScreen_.width = Math.floor(this.imageWhole_.width * scale);
148 this.imageOnScreen_.height = Math.floor(this.imageWhole_.height * scale);
149
150 this.marginX_ = Math.floor(
151 (this.screenCanvas_.width - this.imageOnScreen_.width) / 2);
152 this.marginY_ = Math.floor(
153 (this.screenCanvas_.height - this.imageOnScreen_.height) / 2);
154
155 this.imageOnScreen_.left = this.marginX_;
156 this.imageOnScreen_.top = this.marginY_;
157
158 this.imageVisible_ = new Rect(this.imageWhole_);
159 this.screenVisible_ = new Rect(this.screenWhole_);
160
161 if (this.marginX_ < 0) {
162 this.imageOnScreen_.left +=
163 ImageUtil.clip(this.marginX_, this.offsetX_ * scale, -this.marginX_);
164 this.imageVisible_.left = -this.imageOnScreen_.left / scale;
165 this.imageVisible_.width = this.screenCanvas_.width / scale;
166 } else {
167 this.screenVisible_.left = this.imageOnScreen_.left;
168 this.screenVisible_.width = this.imageOnScreen_.width;
169 }
170
171 if (this.marginY_ < 0) {
172 this.imageOnScreen_.top +=
173 ImageUtil.clip(this.marginY_, this.offsetY_ * scale, -this.marginY_);
174 this.imageVisible_.top = -this.imageOnScreen_.top / scale;
175 this.imageVisible_.height = this.screenCanvas_.height / scale;
176 } else {
177 this.screenVisible_.top = this.imageOnScreen_.top;
178 this.screenVisible_.height = this.imageOnScreen_.height;
179 }
180
181 this.updateOverlays();
182 };
183
184 /*
185 * Coordinate conversion between the screen canvas and the image.
186 */
187
188 ImageBuffer.prototype.screenToImageSize = function(size) {
189 return size / this.getScale();
190 };
191
192 ImageBuffer.prototype.screenToImageX = function(x) {
193 return Math.round((x - this.imageOnScreen_.left) / this.getScale());
194 };
195
196 ImageBuffer.prototype.screenToImageY = function(y) {
197 return Math.round((y - this.imageOnScreen_.top) / this.getScale());
198 };
199
200 ImageBuffer.prototype.screenToImageRect = function(rect) {
201 return new Rect(
202 this.screenToImageX(rect.left),
203 this.screenToImageY(rect.top),
204 this.screenToImageSize(rect.width),
205 this.screenToImageSize(rect.height));
206 };
207
208 ImageBuffer.prototype.imageToScreenSize = function(size) {
209 return size * this.getScale();
210 };
211
212 ImageBuffer.prototype.imageToScreenX = function(x) {
213 return Math.round(this.imageOnScreen_.left + x * this.getScale());
214 };
215
216 ImageBuffer.prototype.imageToScreenY = function(y) {
217 return Math.round(this.imageOnScreen_.top + y * this.getScale());
218 };
219
220 ImageBuffer.prototype.imageToScreenRect = function(rect) {
221 return new Rect(
222 this.imageToScreenX(rect.left),
223 this.imageToScreenY(rect.top),
224 this.imageToScreenSize(rect.width),
225 this.imageToScreenSize(rect.height));
226 };
227
228 /*
229 * Content manipulation.
230 */
231 29
232 /** 30 /**
233 * Loads the new content. 31 * Loads the new content.
234 * A string parameter is treated as an image url. 32 * A string parameter is treated as an image url.
235 * @param {String|HTMLImageElement|HTMLCanvasElement} source 33 * @param {String|HTMLImageElement|HTMLCanvasElement} source
236 */ 34 */
237 ImageBuffer.prototype.load = function(source) { 35 ImageBuffer.prototype.load = function(source) {
238 if (typeof source == 'string') { 36 if (typeof source == 'string') {
239 var self = this; 37 var self = this;
240 var image = new Image(); 38 var image = new Image();
241 image.onload = function(e) { self.load(e.target); }; 39 image.onload = function(e) { self.load(e.target); };
242 image.src = source; 40 image.src = source;
243 } else { 41 } else {
244 this.imageCanvas_.width = source.width, 42 this.content_.load(source);
245 this.imageCanvas_.height = source.height;
246 this.drawImage(source);
247
248 if (this.scaleControl_)
249 this.scaleControl_.displayImageSize(
250 this.imageCanvas_.width, this.imageCanvas_.height);
251 this.fitImage();
252 this.repaint(); 43 this.repaint();
253 } 44 }
254 }; 45 };
255 46
256 ImageBuffer.prototype.getImageCanvas = function() { return this.imageCanvas_; }; 47 ImageBuffer.prototype.resizeScreen = function(width, height, keepFitting) {
48 this.screenCanvas_.width = width;
49 this.screenCanvas_.height = height;
257 50
258 /** 51 var wasFitting =
259 * Replaces the off-screen canvas. 52 this.viewport_.getScale() == this.viewport_.getFittingScale();
260 * To be used when the editor modifies the image dimensions.
261 * @param {HTMLCanvasElement} canvas
262 */
263 ImageBuffer.prototype.setImageCanvas = function(canvas) {
264 this.imageCanvas_ = canvas;
265 this.imageContext_ = canvas.getContext("2d");
266 if (this.scaleControl_)
267 this.scaleControl_.displayImageSize(
268 this.imageCanvas_.width, this.imageCanvas_.height);
269 };
270 53
271 /** 54 this.viewport_.setScreenSize(width, height);
272 * @return {HTMLCanvasElement} A new blank canvas of the required size.
273 */
274 ImageBuffer.prototype.createBlankCanvas = function (width, height) {
275 var canvas = this.screenCanvas_.ownerDocument.createElement('canvas');
276 canvas.width = width;
277 canvas.height = height;
278 return canvas;
279 };
280 55
281 /** 56 var minScale = this.viewport_.getFittingScale();
282 * @return {HTMLCanvasElement} A new canvas with a copy of the content. 57 if ((wasFitting && keepFitting) || this.viewport_.getScale() < minScale) {
283 */ 58 this.viewport_.setScale(minScale, true);
284 ImageBuffer.prototype.copyImageCanvas = function () { 59 }
285 var canvas = this.createBlankCanvas( 60 this.repaint();
286 this.imageCanvas_.width, this.imageCanvas_.height);
287 canvas.getContext('2d').drawImage(this.imageCanvas_, 0, 0);
288 return canvas;
289 };
290
291 /**
292 * @return {ImageData} A new ImageData object with a copy of the content.
293 */
294 ImageBuffer.prototype.copyImageData = function () {
295 return this.imageContext_.getImageData(
296 0, 0, this.imageCanvas_.width, this.imageCanvas_.height);
297 };
298
299 /**
300 * @param {HTMLImageElement|HTMLCanvasElement} image
301 */
302 ImageBuffer.prototype.drawImage = function(image) {
303 ImageUtil.trace.resetTimer('drawImage');
304 this.imageContext_.drawImage(image, 0, 0);
305 ImageUtil.trace.reportTimer('drawImage');
306 };
307
308 /**
309 * @param {ImageData} imageData
310 */
311 ImageBuffer.prototype.drawImageData = function (imageData) {
312 ImageUtil.trace.resetTimer('putImageData');
313 this.imageContext_.putImageData(imageData, 0, 0);
314 ImageUtil.trace.reportTimer('putImageData');
315 }; 61 };
316 62
317 /** 63 /**
318 * Paints the content on the screen canvas taking the current scale and offset 64 * Paints the content on the screen canvas taking the current scale and offset
319 * into account. 65 * into account.
320 */ 66 */
321 ImageBuffer.prototype.repaint = function () { 67 ImageBuffer.prototype.repaint = function (opt_fromOverlay) {
322 ImageUtil.trace.resetTimer('repaint'); 68 this.viewport_.update();
69 this.drawOverlays(this.screenCanvas_.getContext("2d"), opt_fromOverlay);
70 };
323 71
324 this.updateViewPort(); 72 ImageBuffer.prototype.repaintScreenRect = function (screenRect, imageRect) {
325 73 Rect.drawImage(
326 this.screenContext_.save(); 74 this.screenCanvas_.getContext('2d'),
327 75 this.getContent().getCanvas(),
328 this.screenContext_.fillStyle = '#F0F0F0'; 76 screenRect || this.getViewport().imageToScreenRect(screenRect),
329 this.screenContext_.strokeStyle = '#000000'; 77 imageRect || this.getViewport().screenToImageRect(screenRect));
330
331 Rect.drawImage(this.screenContext_, this.imageCanvas_,
332 this.imageOnScreen_, this.imageWhole_);
333 Rect.fillBetween(this.screenContext_, this.imageOnScreen_,
334 this.screenWhole_);
335 Rect.stroke(this.screenContext_, this.imageOnScreen_);
336
337 this.screenContext_.restore();
338
339 this.drawOverlays(this.screenContext_);
340
341 ImageUtil.trace.reportTimer('repaint');
342 }; 78 };
343 79
344 /** 80 /**
345 * ImageBuffer.Overlay is a pluggable extension that modifies the outlook
346 * and the behavior of the ImageBuffer instance.
347 */
348 ImageBuffer.Overlay = function() {};
349
350 ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 };
351
352 ImageBuffer.Overlay.prototype.updateViewPort = function() {}
353
354 ImageBuffer.Overlay.prototype.draw = function() {}
355
356 ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null };
357
358 ImageBuffer.Overlay.prototype.onClick = function() { return false };
359
360 ImageBuffer.Overlay.prototype.getDragHandler = function() { return null };
361
362 /**
363 * @param {ImageBuffer.Overlay} overlay 81 * @param {ImageBuffer.Overlay} overlay
364 */ 82 */
365 ImageBuffer.prototype.addOverlay = function (overlay) { 83 ImageBuffer.prototype.addOverlay = function (overlay) {
366 var zIndex = overlay.getZIndex(); 84 var zIndex = overlay.getZIndex();
367 // Store the overlays in the ascending Z-order. 85 // Store the overlays in the ascending Z-order.
368 var i; 86 var i;
369 for (i = 0; i != this.overlays_.length; i++) { 87 for (i = 0; i != this.overlays_.length; i++) {
370 if (zIndex < this.overlays_[i].getZIndex()) break; 88 if (zIndex < this.overlays_[i].getZIndex()) break;
371 } 89 }
372 this.overlays_.splice(i, 0, overlay); 90 this.overlays_.splice(i, 0, overlay);
373 }; 91 };
374 92
375 /** 93 /**
376 * @param {ImageBuffer.Overlay} overlay 94 * @param {ImageBuffer.Overlay} overlay
377 */ 95 */
378 ImageBuffer.prototype.removeOverlay = function (overlay) { 96 ImageBuffer.prototype.removeOverlay = function (overlay) {
379 for (var i = 0; i != this.overlays_.length; i++) { 97 for (var i = 0; i != this.overlays_.length; i++) {
380 if (this.overlays_[i] == overlay) { 98 if (this.overlays_[i] == overlay) {
381 this.overlays_.splice(i, 1); 99 this.overlays_.splice(i, 1);
382 return; 100 return;
383 } 101 }
384 } 102 }
385 throw new Error('Cannot remove overlay ' + overlay); 103 throw new Error('Cannot remove overlay ' + overlay);
386 }; 104 };
387 105
388 /** 106 /**
389 * Updates viewport configuration on all overlays. 107 * Draws overlays in the ascending Z-order.
108 * Skips overlays below opt_startFrom.
390 */ 109 */
391 ImageBuffer.prototype.updateOverlays = function (context) { 110 ImageBuffer.prototype.drawOverlays = function (context, opt_fromOverlay) {
111 var skip = true;
392 for (var i = 0; i != this.overlays_.length; i++) { 112 for (var i = 0; i != this.overlays_.length; i++) {
393 this.overlays_[i].updateViewPort(this); 113 var overlay = this.overlays_[i];
394 } 114 if (!opt_fromOverlay || opt_fromOverlay == overlay) skip = false;
395 } 115 if (skip) continue;
396 116
397 /**
398 * Draws overlays in the ascending Z-order.
399 */
400 ImageBuffer.prototype.drawOverlays = function (context) {
401 for (var i = 0; i != this.overlays_.length; i++) {
402 context.save(); 117 context.save();
403 this.overlays_[i].draw(context); 118 overlay.draw(context);
404 context.restore(); 119 context.restore();
405 } 120 }
406 }; 121 };
407 122
408 /** 123 /**
409 * Searches for a cursor style in the descending Z-order. 124 * Searches for a cursor style in the descending Z-order.
410 * @return {String} A value for style.cursor CSS property. 125 * @return {String} A value for style.cursor CSS property.
411 */ 126 */
412 ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) { 127 ImageBuffer.prototype.getCursorStyle = function (x, y, mouseDown) {
413 for (var i = this.overlays_.length - 1; i >= 0; i--) { 128 for (var i = this.overlays_.length - 1; i >= 0; i--) {
414 var style = this.overlays_[i].getCursorStyle(x, y, mouseDown); 129 var style = this.overlays_[i].getCursorStyle(x, y, mouseDown);
415 if (style) return style; 130 if (style) return style;
416 } 131 }
417
418 // Indicate that the image is draggable.
419 if (!this.isFullyVisible() && this.screenVisible_.inside(x, y))
420 return 'move';
421
422 return 'default'; 132 return 'default';
423 }; 133 };
424 134
425 /** 135 /**
426 * Searches for a click handler in the descending Z-order. 136 * Searches for a click handler in the descending Z-order.
427 * @return {Boolean} True if handled. 137 * @return {Boolean} True if handled.
428 */ 138 */
429 ImageBuffer.prototype.onClick = function (x, y) { 139 ImageBuffer.prototype.onClick = function (x, y) {
430 for (var i = this.overlays_.length - 1; i >= 0; i--) { 140 for (var i = this.overlays_.length - 1; i >= 0; i--) {
431 if (this.overlays_[i].onClick(x, y)) return true; 141 if (this.overlays_[i].onClick(x, y)) return true;
432 } 142 }
433 return false; 143 return false;
434 }; 144 };
435 145
436 /** 146 /**
437 * Searches for a drag handler in the descending Z-order. 147 * Searches for a drag handler in the descending Z-order.
438 * @return {Function} A closure to be called on mouse drag. 148 * @return {Function} A closure to be called on mouse drag.
439 */ 149 */
440 ImageBuffer.prototype.getDragHandler = function (x, y) { 150 ImageBuffer.prototype.getDragHandler = function (x, y) {
441 for (var i = this.overlays_.length - 1; i >= 0; i--) { 151 for (var i = this.overlays_.length - 1; i >= 0; i--) {
442 var handler = this.overlays_[i].getDragHandler(x, y); 152 var handler = this.overlays_[i].getDragHandler(x, y);
443 if (handler) return handler; 153 if (handler) return handler;
444 } 154 }
445 155 return null;
446 if (!this.isFullyVisible() && this.screenVisible_.inside(x, y)) { 156 };
157
158 /**
159 * ImageBuffer.Overlay is a pluggable extension that modifies the outlook
160 * and the behavior of the ImageBuffer instance.
161 */
162 ImageBuffer.Overlay = function() {};
163
164 ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 };
165
166 ImageBuffer.Overlay.prototype.draw = function() {};
167
168 ImageBuffer.Overlay.prototype.getCursorStyle = function() { return null };
169
170 ImageBuffer.Overlay.prototype.onClick = function() { return false };
171
172 ImageBuffer.Overlay.prototype.getDragHandler = function() { return null };
173
174
175 /**
176 * The margin overlay draws the image outline and paints the margins.
177 */
178 ImageBuffer.Margin = function(viewport) {
179 this.viewport_ = viewport;
180 };
181
182 ImageBuffer.Margin.prototype = {__proto__: ImageBuffer.Overlay.prototype};
183
184 // Draw below everything including the content.
185 ImageBuffer.Margin.prototype.getZIndex = function() { return -2 };
186
187 ImageBuffer.Margin.prototype.draw = function(context) {
188 context.save();
189 context.fillStyle = '#F0F0F0';
190 context.strokeStyle = '#000000';
191 Rect.fillBetween(context,
192 this.viewport_.getImageBoundsOnScreen(),
193 this.viewport_.getScreenBounds());
194 Rect.stroke(context, this.viewport_.getImageBoundsOnScreen());
195 context.restore();
196 };
197
198 /**
199 * The overlay containing the image.
200 */
201 ImageBuffer.Content = function(viewport, document) {
202 this.viewport_ = viewport;
203 this.document_ = document;
204
205 this.generation_ = 0;
206
207 this.setCanvas(this.createBlankCanvas(0, 0));
208 };
209
210 ImageBuffer.Content.prototype = {__proto__: ImageBuffer.Overlay.prototype};
211
212 // Draw below overlays with the default zIndex.
213 ImageBuffer.Content.prototype.getZIndex = function() { return -1 };
214
215 ImageBuffer.Content.prototype.draw = function(context) {
216 Rect.drawImage(
217 context, this.canvas_, this.viewport_.getImageBoundsOnScreen());
218 };
219
220 ImageBuffer.Content.prototype.getCursorStyle = function (x, y, mouseDown) {
221 // Indicate that the image is draggable.
222 if (this.viewport_.isClipped() &&
223 this.viewport_.getScreenClipped().inside(x, y))
224 return 'move';
225
226 return null;
227 };
228
229 ImageBuffer.Content.prototype.getDragHandler = function (x, y) {
230 var cursor = this.getCursorStyle(x, y);
231 if (cursor == 'move') {
447 // Return the handler that drags the entire image. 232 // Return the handler that drags the entire image.
448 return this.createOffsetSetter_(x, y, this.getScale.bind(this)); 233 return this.viewport_.createOffsetSetter(x, y);
449 } 234 }
450 235
451 return null; 236 return null;
452 }; 237 };
453 238
454 /** 239 ImageBuffer.Content.prototype.getCacheGeneration = function() {
455 * Overview overlay draws the image thumbnail in the bottom right corner. 240 return this.generation_;
456 * Indicates the currently visible part. 241 };
457 * Supports panning by dragging. 242
458 */ 243 ImageBuffer.Content.prototype.invalidateCaches = function() {
459 244 this.generation_++;
460 ImageBuffer.Overview = function() {}; 245 };
246
247 ImageBuffer.Content.prototype.getCanvas = function() { return this.canvas_ };
248
249 /**
250 * Replaces the off-screen canvas.
251 * To be used when the editor modifies the image dimensions.
252 * If the logical width/height are supplied they override the canvas dimensions
253 * and the canvas contents is scaled when displayed.
254 * @param {HTMLCanvasElement} canvas
255 * @param {Number} opt_width Logical width (=canvas.width by default)
256 * @param {Number} opt_height Logical height (=canvas.height by default)
257 */
258 ImageBuffer.Content.prototype.setCanvas = function(
259 canvas, opt_width, opt_height) {
260 this.canvas_ = canvas;
261 this.viewport_.setImageSize(opt_width || canvas.width,
262 opt_height || canvas.height);
263
264 this.invalidateCaches();
265 };
266
267 /**
268 * @return {HTMLCanvasElement} A new blank canvas of the required size.
269 */
270 ImageBuffer.Content.prototype.createBlankCanvas = function (width, height) {
271 var canvas = this.document_.createElement('canvas');
272 canvas.width = width;
273 canvas.height = height;
274 return canvas;
275 };
276
277 /**
278 * @param {Number} opt_width Width of the copy, original width by default.
279 * @param {Number} opt_height Height of the copy, original height by default.
280 * @return {HTMLCanvasElement} A new canvas with a copy of the content.
281 */
282 ImageBuffer.Content.prototype.copyCanvas = function (opt_width, opt_height) {
283 var canvas = this.createBlankCanvas(opt_width || this.canvas_.width,
284 opt_height || this.canvas_.height);
285 Rect.drawImage(canvas.getContext('2d'), this.canvas_);
286 return canvas;
287 };
288
289 /**
290 * @return {ImageData} A new ImageData object with a copy of the content.
291 */
292 ImageBuffer.Content.prototype.copyImageData = function (opt_width, opt_height) {
293 return this.canvas_.getContext("2d").getImageData(
294 0, 0, opt_width || this.canvas_.width, opt_height || this.canvas_.height);
295 };
296
297 /**
298 * @param {HTMLImageElement|HTMLCanvasElement} image
299 */
300 ImageBuffer.Content.prototype.load = function(image) {
301 this.canvas_.width = image.width;
302 this.canvas_.height = image.height;
303
304 Rect.drawImage(this.canvas_.getContext("2d"), image);
305 this.invalidateCaches();
306
307 this.viewport_.setImageSize(image.width, image.height);
308 this.viewport_.fitImage();
309 };
310
311 /**
312 * @param {ImageData} imageData
313 */
314 ImageBuffer.Content.prototype.drawImageData = function (imageData, x, y) {
315 this.canvas_.getContext("2d").putImageData(imageData, x, y);
316 this.invalidateCaches();
317 };
318
319 /**
320 * The overview overlay draws the image thumbnail in the bottom right corner.
321 * Indicates the currently visible part. Supports panning by dragging.
322 */
323 ImageBuffer.Overview = function(viewport, content) {
324 this.viewport_ = viewport;
325 this.content_ = content;
326 this.contentGeneration_ = 0;
327 };
328
329 ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype};
330
331 // Draw above everything.
332 ImageBuffer.Overview.prototype.getZIndex = function() { return 100 };
461 333
462 ImageBuffer.Overview.MAX_SIZE = 150; 334 ImageBuffer.Overview.MAX_SIZE = 150;
463 ImageBuffer.Overview.RIGHT = 7; 335 ImageBuffer.Overview.RIGHT = 7;
464 ImageBuffer.Overview.BOTTOM = 50; 336 ImageBuffer.Overview.BOTTOM = 50;
465 337
466 ImageBuffer.Overview.prototype = {__proto__: ImageBuffer.Overlay.prototype}; 338 ImageBuffer.Overview.prototype.update = function() {
467 339 var imageBounds = this.viewport_.getImageBounds();
468 ImageBuffer.Overview.prototype.getZIndex = function() { return 100; } 340
469 341 if (this.contentGeneration_ != this.content_.getCacheGeneration()) {
470 ImageBuffer.Overview.prototype.updateViewPort = function(buffer) { 342 this.contentGeneration_ = this.content_.getCacheGeneration();
471 this.buffer_ = buffer; 343
472 344 var aspect = imageBounds.width / imageBounds.height;
473 this.whole_ = null; 345 if (aspect > 1) {
474 this.visible_ = null; 346 this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE,
475 347 ImageBuffer.Overview.MAX_SIZE / aspect);
476 if (this.buffer_.isFullyVisible()) return; 348 } else {
477 349 this.bounds_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect,
478 var screenWhole = this.buffer_.screenWhole_; 350 ImageBuffer.Overview.MAX_SIZE);
479 var imageWhole = this.buffer_.imageWhole_; 351 }
480 var imageVisible = this.buffer_.imageVisible_; 352
481 353 this.canvas_ =
482 var aspect = imageWhole.width / imageWhole.height; 354 this.content_.copyCanvas(this.bounds_.width, this.bounds_.height);
483 if (aspect > 1) { 355 }
484 this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE, 356
485 ImageBuffer.Overview.MAX_SIZE / aspect); 357 this.clipped_ = null;
486 } else { 358
487 this.whole_ = new Rect(ImageBuffer.Overview.MAX_SIZE * aspect, 359 if (this.viewport_.isClipped()) {
488 ImageBuffer.Overview.MAX_SIZE); 360 var screenBounds = this.viewport_.getScreenBounds();
489 } 361
490 362 this.bounds_ = this.bounds_.moveTo(
491 this.whole_ = this.whole_.moveTo( 363 screenBounds.width - ImageBuffer.Overview.RIGHT - this.bounds_.width,
492 screenWhole.width - ImageBuffer.Overview.RIGHT - this.whole_.width, 364 screenBounds.height - ImageBuffer.Overview.BOTTOM -
493 screenWhole.height - ImageBuffer.Overview.BOTTOM - this.whole_.height); 365 this.bounds_.height);
494 366
495 this.scale_ = this.whole_.width / imageWhole.width; 367 this.scale_ = this.bounds_.width / imageBounds.width;
496 368
497 this.visible_ = imageVisible. 369 this.clipped_ = this.viewport_.getImageClipped().
498 scale(this.scale_). 370 scale(this.scale_).
499 shift(this.whole_.left, this.whole_.top); 371 shift(this.bounds_.left, this.bounds_.top);
372 }
500 }; 373 };
501 374
502 ImageBuffer.Overview.prototype.draw = function(context) { 375 ImageBuffer.Overview.prototype.draw = function(context) {
503 if (!this.visible_) return; 376 this.update();
377
378 if (!this.clipped_) return;
504 379
505 // Draw the thumbnail. 380 // Draw the thumbnail.
506 Rect.drawImage(context, this.buffer_.imageCanvas_, 381 Rect.drawImage(context, this.canvas_, this.bounds_);
507 this.whole_, this.buffer_.imageWhole_);
508 382
509 // Draw the thumbnail border. 383 // Draw the thumbnail border.
510 context.strokeStyle = '#000000'; 384 context.strokeStyle = '#000000';
511 Rect.stroke(context, this.whole_); 385 Rect.stroke(context, this.bounds_);
512 386
513 // Draw the shadow over the off-screen part of the thumbnail. 387 // Draw the shadow over the off-screen part of the thumbnail.
514 context.globalAlpha = 0.3; 388 context.globalAlpha = 0.3;
515 context.fillStyle = '#000000'; 389 context.fillStyle = '#000000';
516 Rect.fillBetween(context, this.visible_, this.whole_); 390 Rect.fillBetween(context, this.clipped_, this.bounds_);
517 391
518 // Outline the on-screen part of the thumbnail. 392 // Outline the on-screen part of the thumbnail.
519 context.strokeStyle = '#FFFFFF'; 393 context.strokeStyle = '#FFFFFF';
520 Rect.stroke(context, this.visible_); 394 Rect.stroke(context, this.clipped_);
521 }; 395 };
522 396
523 ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) { 397 ImageBuffer.Overview.prototype.getCursorStyle = function(x, y) {
524 if (!this.whole_ || !this.whole_.inside(x, y)) return null; 398 if (!this.bounds_ || !this.bounds_.inside(x, y)) return null;
525 399
526 // Indicate that the on-screen part is draggable. 400 // Indicate that the on-screen part is draggable.
527 if (this.visible_.inside(x, y)) return 'move'; 401 if (this.clipped_.inside(x, y)) return 'move';
528 402
529 // Indicathe that the rest of the thumbnail is clickable. 403 // Indicate that the rest of the thumbnail is clickable.
530 return 'crosshair'; 404 return 'crosshair';
531 }; 405 };
532 406
533 ImageBuffer.Overview.prototype.onClick = function(x, y) { 407 ImageBuffer.Overview.prototype.onClick = function(x, y) {
534 if (!this.whole_ || !this.whole_.inside(x, y)) return false; 408 if (this.getCursorStyle(x, y) != 'crosshair') return false;
535 409 this.viewport_.setCenter(
536 if (this.visible_.inside(x, y)) return false; 410 (x - this.bounds_.left) / this.scale_,
537 411 (y - this.bounds_.top) / this.scale_);
538 this.buffer_.setCenter( 412 this.viewport_.repaint();
539 (x - this.whole_.left) / this.scale_,
540 (y - this.whole_.top) / this.scale_);
541 this.buffer_.repaint();
542 return true; 413 return true;
543 }; 414 };
544 415
545 ImageBuffer.Overview.prototype.getDragHandler = function(x, y) { 416 ImageBuffer.Overview.prototype.getDragHandler = function(x, y) {
546 if (!this.whole_ || !this.whole_.inside(x, y)) return null; 417 var cursor = this.getCursorStyle(x, y);
547 418
548 if (this.visible_.inside(x, y)) { 419 if (cursor == 'move') {
549 var self = this; 420 var self = this;
550 function scale() { return -self.scale_;} 421 function scale() { return -self.scale_;}
551 function hit(x, y) { return self.whole_ && self.whole_.inside(x, y); } 422 function hit(x, y) { return self.bounds_ && self.bounds_.inside(x, y); }
552 return this.buffer_.createOffsetSetter_(x, y, scale, hit); 423 return this.viewport_.createOffsetSetter(x, y, scale, hit);
553 } else { 424 } else if (cursor == 'crosshair') {
554 // Force non-draggable behavior. 425 // Force non-draggable behavior.
555 return function() {}; 426 return function() {};
556 } 427 } else {
557 }; 428 return null;
429 }
430 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698