Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / image_editor / viewport.js
blob54ea128593db7dd12208a0e490feb9525ba83de1
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6 * Viewport class controls the way the image is displayed (scale, offset etc).
7 * @constructor
8 */
9 function Viewport() {
10 /**
11 * Size of the full resolution image.
12 * @type {ImageRect}
13 * @private
15 this.imageBounds_ = new ImageRect();
17 /**
18 * Size of the application window.
19 * @type {ImageRect}
20 * @private
22 this.screenBounds_ = new ImageRect();
24 /**
25 * Bounds of the image element on screen without zoom and offset.
26 * @type {ImageRect}
27 * @private
29 this.imageElementBoundsOnScreen_ = null;
31 /**
32 * Bounds of the image with zoom and offset.
33 * @type {ImageRect}
34 * @private
36 this.imageBoundsOnScreen_ = null;
38 /**
39 * Image bounds that is clipped with the screen bounds.
40 * @type {ImageRect}
41 * @private
43 this.imageBoundsOnScreenClipped_ = null;
45 /**
46 * Scale from the full resolution image to the screen displayed image. This is
47 * not zoom operated by users.
48 * @type {number}
49 * @private
51 this.scale_ = 1;
53 /**
54 * Zoom ratio specified by user operations.
55 * @type {number}
56 * @private
58 this.zoom_ = 1;
60 /**
61 * Offset specified by user operations.
62 * @type {number}
63 * @private
65 this.offsetX_ = 0;
67 /**
68 * Offset specified by user operations.
69 * @type {number}
70 * @private
72 this.offsetY_ = 0;
74 /**
75 * Integer Rotation value.
76 * The rotation angle is this.rotation_ * 90.
77 * @type {number}
78 * @private
80 this.rotation_ = 0;
82 /**
83 * Generation of the screen size image cache.
84 * This is incremented every time when the size of image cache is changed.
85 * @type {number}
86 * @private
88 this.generation_ = 0;
90 this.update_();
91 Object.seal(this);
94 /**
95 * Zoom ratios.
97 * @type {Array.<number>}
98 * @const
100 Viewport.ZOOM_RATIOS = Object.freeze([1, 1.5, 2, 3]);
103 * @param {number} width Image width.
104 * @param {number} height Image height.
106 Viewport.prototype.setImageSize = function(width, height) {
107 this.imageBounds_ = new ImageRect(width, height);
108 this.update_();
112 * @param {number} width Screen width.
113 * @param {number} height Screen height.
115 Viewport.prototype.setScreenSize = function(width, height) {
116 this.screenBounds_ = new ImageRect(width, height);
117 this.update_();
121 * Sets zoom value directly.
122 * @param {number} zoom New zoom value.
124 Viewport.prototype.setZoom = function(zoom) {
125 var zoomMin = Viewport.ZOOM_RATIOS[0];
126 var zoomMax = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
127 var adjustedZoom = Math.max(zoomMin, Math.min(zoom, zoomMax));
128 this.zoom_ = adjustedZoom;
129 this.update_();
133 * Returns the value of zoom.
134 * @return {number} Zoom value.
136 Viewport.prototype.getZoom = function() {
137 return this.zoom_;
141 * Sets the nearest larger value of ZOOM_RATIOS.
143 Viewport.prototype.zoomIn = function() {
144 var zoom = Viewport.ZOOM_RATIOS[0];
145 for (var i = 0; i < Viewport.ZOOM_RATIOS.length; i++) {
146 zoom = Viewport.ZOOM_RATIOS[i];
147 if (zoom > this.zoom_)
148 break;
150 this.setZoom(zoom);
154 * Sets the nearest smaller value of ZOOM_RATIOS.
156 Viewport.prototype.zoomOut = function() {
157 var zoom = Viewport.ZOOM_RATIOS[Viewport.ZOOM_RATIOS.length - 1];
158 for (var i = Viewport.ZOOM_RATIOS.length - 1; i >= 0; i--) {
159 zoom = Viewport.ZOOM_RATIOS[i];
160 if (zoom < this.zoom_)
161 break;
163 this.setZoom(zoom);
167 * Obtains whether the picture is zoomed or not.
168 * @return {boolean}
170 Viewport.prototype.isZoomed = function() {
171 return this.zoom_ !== 1;
175 * Sets the rotation value.
176 * @param {number} rotation New rotation value.
178 Viewport.prototype.setRotation = function(rotation) {
179 this.rotation_ = rotation;
180 this.update_();
185 * Obtains the rotation value.
186 * @return {number} Current rotation value.
188 Viewport.prototype.getRotation = function() {
189 return this.rotation_;
193 * Obtains the scale for the specified image size.
195 * @param {number} width Width of the full resolution image.
196 * @param {number} height Height of the full resolution image.
197 * @return {number} The ratio of the full resotion image size and the calculated
198 * displayed image size.
199 * @private
201 Viewport.prototype.getFittingScaleForImageSize_ = function(width, height) {
202 var scaleX = this.screenBounds_.width / width;
203 var scaleY = this.screenBounds_.height / height;
204 // Scales > (1 / devicePixelRatio) do not look good. Also they are
205 // not really useful as we do not have any pixel-level operations.
206 return Math.min(1 / window.devicePixelRatio, scaleX, scaleY);
210 * @return {number} X-offset of the viewport.
212 Viewport.prototype.getOffsetX = function() { return this.offsetX_; };
215 * @return {number} Y-offset of the viewport.
217 Viewport.prototype.getOffsetY = function() { return this.offsetY_; };
220 * Set the image offset in the viewport.
221 * @param {number} x X-offset.
222 * @param {number} y Y-offset.
224 Viewport.prototype.setOffset = function(x, y) {
225 if (this.offsetX_ == x && this.offsetY_ == y)
226 return;
227 this.offsetX_ = x;
228 this.offsetY_ = y;
229 this.update_();
233 * @return {ImageRect} The image bounds in image coordinates.
235 Viewport.prototype.getImageBounds = function() { return this.imageBounds_; };
238 * @return {ImageRect} The screen bounds in screen coordinates.
240 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_; };
243 * @return {ImageRect} The size of screen cache canvas.
245 Viewport.prototype.getDeviceBounds = function() {
246 var size = this.getImageElementBoundsOnScreen();
247 return new ImageRect(
248 size.width * window.devicePixelRatio,
249 size.height * window.devicePixelRatio);
253 * A counter that is incremented with each viewport state change.
254 * Clients that cache anything that depends on the viewport state should keep
255 * track of this counter.
256 * @return {number} counter.
258 Viewport.prototype.getCacheGeneration = function() { return this.generation_; };
261 * @return {ImageRect} The image bounds in screen coordinates.
263 Viewport.prototype.getImageBoundsOnScreen = function() {
264 return this.imageBoundsOnScreen_;
268 * The image bounds in screen coordinates.
269 * This returns the bounds of element before applying zoom and offset.
270 * @return {ImageRect}
272 Viewport.prototype.getImageElementBoundsOnScreen = function() {
273 return this.imageElementBoundsOnScreen_;
277 * The image bounds on screen, which is clipped with the screen size.
278 * @return {ImageRect}
280 Viewport.prototype.getImageBoundsOnScreenClipped = function() {
281 return this.imageBoundsOnScreenClipped_;
285 * @param {number} size Size in screen coordinates.
286 * @return {number} Size in image coordinates.
288 Viewport.prototype.screenToImageSize = function(size) {
289 return size / this.scale_;
293 * @param {number} x X in screen coordinates.
294 * @return {number} X in image coordinates.
296 Viewport.prototype.screenToImageX = function(x) {
297 return Math.round((x - this.imageBoundsOnScreen_.left) / this.scale_);
301 * @param {number} y Y in screen coordinates.
302 * @return {number} Y in image coordinates.
304 Viewport.prototype.screenToImageY = function(y) {
305 return Math.round((y - this.imageBoundsOnScreen_.top) / this.scale_);
309 * @param {ImageRect} rect Rectangle in screen coordinates.
310 * @return {ImageRect} Rectangle in image coordinates.
312 Viewport.prototype.screenToImageRect = function(rect) {
313 return new ImageRect(
314 this.screenToImageX(rect.left),
315 this.screenToImageY(rect.top),
316 this.screenToImageSize(rect.width),
317 this.screenToImageSize(rect.height));
321 * @param {number} size Size in image coordinates.
322 * @return {number} Size in screen coordinates.
324 Viewport.prototype.imageToScreenSize = function(size) {
325 return size * this.scale_;
329 * @param {number} x X in image coordinates.
330 * @return {number} X in screen coordinates.
332 Viewport.prototype.imageToScreenX = function(x) {
333 return Math.round(this.imageBoundsOnScreen_.left + x * this.scale_);
337 * @param {number} y Y in image coordinates.
338 * @return {number} Y in screen coordinates.
340 Viewport.prototype.imageToScreenY = function(y) {
341 return Math.round(this.imageBoundsOnScreen_.top + y * this.scale_);
345 * @param {ImageRect} rect Rectangle in image coordinates.
346 * @return {ImageRect} Rectangle in screen coordinates.
348 Viewport.prototype.imageToScreenRect = function(rect) {
349 return new ImageRect(
350 this.imageToScreenX(rect.left),
351 this.imageToScreenY(rect.top),
352 Math.round(this.imageToScreenSize(rect.width)),
353 Math.round(this.imageToScreenSize(rect.height)));
357 * @param {number} width Width of the rectangle.
358 * @param {number} height Height of the rectangle.
359 * @param {number} offsetX X-offset of center position of the rectangle.
360 * @param {number} offsetY Y-offset of center position of the rectangle.
361 * @return {ImageRect} Rectangle with given geometry.
362 * @private
364 Viewport.prototype.getCenteredRect_ = function(
365 width, height, offsetX, offsetY) {
366 return new ImageRect(
367 ~~((this.screenBounds_.width - width) / 2) + offsetX,
368 ~~((this.screenBounds_.height - height) / 2) + offsetY,
369 width,
370 height);
374 * Resets zoom and offset.
376 Viewport.prototype.resetView = function() {
377 this.zoom_ = 1;
378 this.offsetX_ = 0;
379 this.offsetY_ = 0;
380 this.rotation_ = 0;
381 this.update_();
385 * Recalculate the viewport parameters.
386 * @private
388 Viewport.prototype.update_ = function() {
389 // Update scale.
390 this.scale_ = this.getFittingScaleForImageSize_(
391 this.imageBounds_.width, this.imageBounds_.height);
393 // Limit offset values.
394 var zoomedWidht;
395 var zoomedHeight;
396 if (this.rotation_ % 2 == 0) {
397 zoomedWidht = ~~(this.imageBounds_.width * this.scale_ * this.zoom_);
398 zoomedHeight = ~~(this.imageBounds_.height * this.scale_ * this.zoom_);
399 } else {
400 var scale = this.getFittingScaleForImageSize_(
401 this.imageBounds_.height, this.imageBounds_.width);
402 zoomedWidht = ~~(this.imageBounds_.height * scale * this.zoom_);
403 zoomedHeight = ~~(this.imageBounds_.width * scale * this.zoom_);
405 var dx = Math.max(zoomedWidht - this.screenBounds_.width, 0) / 2;
406 var dy = Math.max(zoomedHeight - this.screenBounds_.height, 0) /2;
407 this.offsetX_ = ImageUtil.clamp(-dx, this.offsetX_, dx);
408 this.offsetY_ = ImageUtil.clamp(-dy, this.offsetY_, dy);
410 // Image bounds on screen.
411 this.imageBoundsOnScreen_ = this.getCenteredRect_(
412 zoomedWidht, zoomedHeight, this.offsetX_, this.offsetY_);
414 // Image bounds of element (that is not applied zoom and offset) on screen.
415 var oldBounds = this.imageElementBoundsOnScreen_;
416 this.imageElementBoundsOnScreen_ = this.getCenteredRect_(
417 ~~(this.imageBounds_.width * this.scale_),
418 ~~(this.imageBounds_.height * this.scale_),
421 if (!oldBounds ||
422 this.imageElementBoundsOnScreen_.width != oldBounds.width ||
423 this.imageElementBoundsOnScreen_.height != oldBounds.height) {
424 this.generation_++;
427 // Image bounds on screen clipped with the screen bounds.
428 var left = Math.max(this.imageBoundsOnScreen_.left, 0);
429 var top = Math.max(this.imageBoundsOnScreen_.top, 0);
430 var right = Math.min(
431 this.imageBoundsOnScreen_.right, this.screenBounds_.width);
432 var bottom = Math.min(
433 this.imageBoundsOnScreen_.bottom, this.screenBounds_.height);
434 this.imageBoundsOnScreenClipped_ = new ImageRect(
435 left, top, right - left, bottom - top);
439 * Clones the viewport.
440 * @return {Viewport} New instance.
442 Viewport.prototype.clone = function() {
443 var viewport = new Viewport();
444 viewport.imageBounds_ = new ImageRect(this.imageBounds_);
445 viewport.screenBounds_ = new ImageRect(this.screenBounds_);
446 viewport.scale_ = this.scale_;
447 viewport.zoom_ = this.zoom_;
448 viewport.offsetX_ = this.offsetX_;
449 viewport.offsetY_ = this.offsetY_;
450 viewport.rotation_ = this.rotation_;
451 viewport.generation_ = this.generation_;
452 viewport.update_();
453 return viewport;
457 * Obtains CSS transformation for the screen image.
458 * @return {string} Transformation description.
460 Viewport.prototype.getTransformation = function() {
461 var rotationScaleAdjustment;
462 if (this.rotation_ % 2) {
463 rotationScaleAdjustment = this.getFittingScaleForImageSize_(
464 this.imageBounds_.height, this.imageBounds_.width) / this.scale_;
465 } else {
466 rotationScaleAdjustment = 1;
468 return [
469 'translate(' + this.offsetX_ + 'px, ' + this.offsetY_ + 'px) ',
470 'rotate(' + (this.rotation_ * 90) + 'deg)',
471 'scale(' + (this.zoom_ * rotationScaleAdjustment) + ')'
472 ].join(' ');
476 * Obtains shift CSS transformation for the screen image.
477 * @param {number} dx Amount of shift.
478 * @return {string} Transformation description.
480 Viewport.prototype.getShiftTransformation = function(dx) {
481 return 'translateX(' + dx + 'px) ' + this.getTransformation();
485 * Obtains CSS transformation that makes the rotated image fit the original
486 * image. The new rotated image that the transformation is applied to looks the
487 * same with original image.
489 * @param {boolean} orientation Orientation of the rotation from the original
490 * image to the rotated image. True is for clockwise and false is for
491 * counterclockwise.
492 * @return {string} Transformation description.
494 Viewport.prototype.getInverseTransformForRotatedImage = function(orientation) {
495 var previousImageWidth = this.imageBounds_.height;
496 var previousImageHeight = this.imageBounds_.width;
497 var oldScale = this.getFittingScaleForImageSize_(
498 previousImageWidth, previousImageHeight);
499 var scaleRatio = oldScale / this.scale_;
500 var degree = orientation ? '-90deg' : '90deg';
501 return [
502 'scale(' + scaleRatio + ')',
503 'rotate(' + degree + ')',
504 this.getTransformation()
505 ].join(' ');
509 * Obtains CSS transformation that makes the cropped image fit the original
510 * image. The new cropped image that the transformation is applied to fits to
511 * the cropped rectangle in the original image.
513 * @param {number} imageWidth Width of the original image.
514 * @param {number} imageHeight Height of the original image.
515 * @param {ImageRect} imageCropRect Crop rectangle in the image's coordinate
516 * system.
517 * @return {string} Transformation description.
519 Viewport.prototype.getInverseTransformForCroppedImage =
520 function(imageWidth, imageHeight, imageCropRect) {
521 var wholeScale = this.getFittingScaleForImageSize_(
522 imageWidth, imageHeight);
523 var croppedScale = this.getFittingScaleForImageSize_(
524 imageCropRect.width, imageCropRect.height);
525 var dx =
526 (imageCropRect.left + imageCropRect.width / 2 - imageWidth / 2) *
527 wholeScale;
528 var dy =
529 (imageCropRect.top + imageCropRect.height / 2 - imageHeight / 2) *
530 wholeScale;
531 return [
532 'translate(' + dx + 'px,' + dy + 'px)',
533 'scale(' + wholeScale / croppedScale + ')',
534 this.getTransformation()
535 ].join(' ');
539 * Obtains CSS transformation that makes the image fit to the screen rectangle.
541 * @param {ImageRect} screenRect Screen rectangle.
542 * @return {string} Transformation description.
544 Viewport.prototype.getScreenRectTransformForImage = function(screenRect) {
545 var imageBounds = this.getImageElementBoundsOnScreen();
546 var scaleX = screenRect.width / imageBounds.width;
547 var scaleY = screenRect.height / imageBounds.height;
548 var screenWidth = this.screenBounds_.width;
549 var screenHeight = this.screenBounds_.height;
550 var dx = screenRect.left + screenRect.width / 2 - screenWidth / 2;
551 var dy = screenRect.top + screenRect.height / 2 - screenHeight / 2;
552 return [
553 'translate(' + dx + 'px,' + dy + 'px)',
554 'scale(' + scaleX + ',' + scaleY + ')',
555 this.getTransformation()
556 ].join(' ');