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.
6 * Viewport class controls the way the image is displayed (scale, offset etc).
11 * Size of the full resolution image.
15 this.imageBounds_
= new ImageRect();
18 * Size of the application window.
22 this.screenBounds_
= new ImageRect();
25 * Bounds of the image element on screen without zoom and offset.
29 this.imageElementBoundsOnScreen_
= null;
32 * Bounds of the image with zoom and offset.
36 this.imageBoundsOnScreen_
= null;
39 * Image bounds that is clipped with the screen bounds.
43 this.imageBoundsOnScreenClipped_
= null;
46 * Scale from the full resolution image to the screen displayed image. This is
47 * not zoom operated by users.
54 * Zoom ratio specified by user operations.
61 * Offset specified by user operations.
68 * Offset specified by user operations.
75 * Integer Rotation value.
76 * The rotation angle is this.rotation_ * 90.
83 * Generation of the screen size image cache.
84 * This is incremented every time when the size of image cache is changed.
97 * @type {Array.<number>}
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
);
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
);
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
;
133 * Returns the value of zoom.
134 * @return {number} Zoom value.
136 Viewport
.prototype.getZoom = function() {
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_
)
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_
)
167 * Obtains whether the picture is zoomed or not.
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
;
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.
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
)
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.
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
,
374 * Resets zoom and offset.
376 Viewport
.prototype.resetView = function() {
385 * Recalculate the viewport parameters.
388 Viewport
.prototype.update_ = function() {
390 this.scale_
= this.getFittingScaleForImageSize_(
391 this.imageBounds_
.width
, this.imageBounds_
.height
);
393 // Limit offset values.
396 if (this.rotation_
% 2 == 0) {
397 zoomedWidht
= ~~(this.imageBounds_
.width
* this.scale_
* this.zoom_
);
398 zoomedHeight
= ~~(this.imageBounds_
.height
* this.scale_
* this.zoom_
);
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_
),
422 this.imageElementBoundsOnScreen_
.width
!= oldBounds
.width
||
423 this.imageElementBoundsOnScreen_
.height
!= oldBounds
.height
) {
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_
;
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_
;
466 rotationScaleAdjustment
= 1;
469 'translate(' + this.offsetX_
+ 'px, ' + this.offsetY_
+ 'px) ',
470 'rotate(' + (this.rotation_
* 90) + 'deg)',
471 'scale(' + (this.zoom_
* rotationScaleAdjustment
) + ')'
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
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';
502 'scale(' + scaleRatio
+ ')',
503 'rotate(' + degree
+ ')',
504 this.getTransformation()
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
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
);
526 (imageCropRect
.left
+ imageCropRect
.width
/ 2 - imageWidth
/ 2) *
529 (imageCropRect
.top
+ imageCropRect
.height
/ 2 - imageHeight
/ 2) *
532 'translate(' + dx
+ 'px,' + dy
+ 'px)',
533 'scale(' + wholeScale
/ croppedScale
+ ')',
534 this.getTransformation()
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;
553 'translate(' + dx
+ 'px,' + dy
+ 'px)',
554 'scale(' + scaleX
+ ',' + scaleY
+ ')',
555 this.getTransformation()