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).
12 * Size of the full resolution image.
16 this.imageBounds_
= new ImageRect(0, 0, 0, 0);
19 * Size of the application window.
23 this.screenBounds_
= new ImageRect(0, 0, 0, 0);
26 * Bounds of the image element on screen without zoom and offset.
30 this.imageElementBoundsOnScreen_
= null;
33 * Bounds of the image with zoom and offset.
37 this.imageBoundsOnScreen_
= null;
40 * Image bounds that is clipped with the screen bounds.
44 this.imageBoundsOnScreenClipped_
= null;
47 * Scale from the full resolution image to the screen displayed image. This is
48 * not zoom operated by users.
55 * Zoom ratio specified by user operations.
62 * Offset specified by user operations.
69 * Offset specified by user operations.
76 * Integer Rotation value.
77 * The rotation angle is this.rotation_ * 90.
84 * Generation of the screen size image cache.
85 * This is incremented every time when the size of image cache is changed.
97 * @type {Array.<number>}
100 Viewport
.ZOOM_RATIOS
= [1, 1.5, 2, 3];
104 * @param {number} width Image width.
105 * @param {number} height Image height.
107 Viewport
.prototype.setImageSize = function(width
, height
) {
108 this.imageBounds_
= ImageRect
.createFromWidthAndHeight(width
, height
);
114 * @param {number} width Screen width.
115 * @param {number} height Screen height.
117 Viewport
.prototype.setScreenSize = function(width
, height
) {
118 this.screenBounds_
= ImageRect
.createFromWidthAndHeight(width
, height
);
123 * Sets zoom value directly.
124 * @param {number} zoom New zoom value.
126 Viewport
.prototype.setZoom = function(zoom
) {
127 var zoomMin
= Viewport
.ZOOM_RATIOS
[0];
128 var zoomMax
= Viewport
.ZOOM_RATIOS
[Viewport
.ZOOM_RATIOS
.length
- 1];
129 var adjustedZoom
= Math
.max(zoomMin
, Math
.min(zoom
, zoomMax
));
130 this.zoom_
= adjustedZoom
;
135 * Returns the value of zoom.
136 * @return {number} Zoom value.
138 Viewport
.prototype.getZoom = function() {
143 * Sets the nearest larger value of ZOOM_RATIOS.
145 Viewport
.prototype.zoomIn = function() {
146 var zoom
= Viewport
.ZOOM_RATIOS
[0];
147 for (var i
= 0; i
< Viewport
.ZOOM_RATIOS
.length
; i
++) {
148 zoom
= Viewport
.ZOOM_RATIOS
[i
];
149 if (zoom
> this.zoom_
)
156 * Sets the nearest smaller value of ZOOM_RATIOS.
158 Viewport
.prototype.zoomOut = function() {
159 var zoom
= Viewport
.ZOOM_RATIOS
[Viewport
.ZOOM_RATIOS
.length
- 1];
160 for (var i
= Viewport
.ZOOM_RATIOS
.length
- 1; i
>= 0; i
--) {
161 zoom
= Viewport
.ZOOM_RATIOS
[i
];
162 if (zoom
< this.zoom_
)
169 * Obtains whether the picture is zoomed or not.
172 Viewport
.prototype.isZoomed = function() {
173 return this.zoom_
!== 1;
177 * Sets the rotation value.
178 * @param {number} rotation New rotation value.
180 Viewport
.prototype.setRotation = function(rotation
) {
181 this.rotation_
= rotation
;
187 * Obtains the rotation value.
188 * @return {number} Current rotation value.
190 Viewport
.prototype.getRotation = function() {
191 return this.rotation_
;
195 * Obtains the scale for the specified image size.
197 * @param {number} width Width of the full resolution image.
198 * @param {number} height Height of the full resolution image.
199 * @return {number} The ratio of the full resotion image size and the calculated
200 * displayed image size.
203 Viewport
.prototype.getFittingScaleForImageSize_ = function(width
, height
) {
204 var scaleX
= this.screenBounds_
.width
/ width
;
205 var scaleY
= this.screenBounds_
.height
/ height
;
206 return Math
.min(scaleX
, scaleY
, 1);
211 * @return {number} X-offset of the viewport.
213 Viewport
.prototype.getOffsetX = function() { return this.offsetX_
; };
217 * @return {number} Y-offset of the viewport.
219 Viewport
.prototype.getOffsetY = function() { return this.offsetY_
; };
222 * Set the image offset in the viewport.
223 * @param {number} x X-offset.
224 * @param {number} y Y-offset.
226 Viewport
.prototype.setOffset = function(x
, y
) {
227 if (this.offsetX_
== x
&& this.offsetY_
== y
)
235 * Returns image bounds.
236 * @return {!ImageRect} The image bounds in image coordinates.
238 Viewport
.prototype.getImageBounds = function() { return this.imageBounds_
; };
241 * Returns screen bounds.
242 * @return {!ImageRect} The screen bounds in screen coordinates.
244 Viewport
.prototype.getScreenBounds = function() { return this.screenBounds_
; };
247 * Returns device bounds.
248 * @return {!ImageRect} The size of screen cache canvas.
250 Viewport
.prototype.getDeviceBounds = function() {
251 var size
= this.getImageElementBoundsOnScreen();
252 return ImageRect
.createFromWidthAndHeight(
253 size
.width
* window
.devicePixelRatio
,
254 size
.height
* window
.devicePixelRatio
);
258 * A counter that is incremented with each viewport state change.
259 * Clients that cache anything that depends on the viewport state should keep
260 * track of this counter.
261 * @return {number} counter.
263 Viewport
.prototype.getCacheGeneration = function() { return this.generation_
; };
266 * Returns image bounds in screen coordinates.
267 * @return {!ImageRect} The image bounds in screen coordinates.
269 Viewport
.prototype.getImageBoundsOnScreen = function() {
270 assert(this.imageBoundsOnScreen_
);
271 return this.imageBoundsOnScreen_
;
275 * The image bounds in screen coordinates.
276 * This returns the bounds of element before applying zoom and offset.
277 * @return {!ImageRect}
279 Viewport
.prototype.getImageElementBoundsOnScreen = function() {
280 assert(this.imageElementBoundsOnScreen_
);
281 return this.imageElementBoundsOnScreen_
;
285 * The image bounds on screen, which is clipped with the screen size.
286 * @return {!ImageRect}
288 Viewport
.prototype.getImageBoundsOnScreenClipped = function() {
289 assert(this.imageBoundsOnScreenClipped_
);
290 return this.imageBoundsOnScreenClipped_
;
294 * Returns size in image coordinates.
295 * @param {number} size Size in screen coordinates.
296 * @return {number} Size in image coordinates.
298 Viewport
.prototype.screenToImageSize = function(size
) {
299 return size
/ this.scale_
;
303 * Returns X in image coordinates.
304 * @param {number} x X in screen coordinates.
305 * @return {number} X in image coordinates.
307 Viewport
.prototype.screenToImageX = function(x
) {
308 return Math
.round((x
- this.imageBoundsOnScreen_
.left
) / this.scale_
);
312 * Returns Y in image coordinates.
313 * @param {number} y Y in screen coordinates.
314 * @return {number} Y in image coordinates.
316 Viewport
.prototype.screenToImageY = function(y
) {
317 return Math
.round((y
- this.imageBoundsOnScreen_
.top
) / this.scale_
);
321 * Returns a rectangle in image coordinates.
322 * @param {!ImageRect} rect Rectangle in screen coordinates.
323 * @return {!ImageRect} Rectangle in image coordinates.
325 Viewport
.prototype.screenToImageRect = function(rect
) {
326 return new ImageRect(
327 this.screenToImageX(rect
.left
),
328 this.screenToImageY(rect
.top
),
329 this.screenToImageSize(rect
.width
),
330 this.screenToImageSize(rect
.height
));
334 * Returns size in screen coordinates.
335 * @param {number} size Size in image coordinates.
336 * @return {number} Size in screen coordinates.
338 Viewport
.prototype.imageToScreenSize = function(size
) {
339 return size
* this.scale_
;
343 * Returns X in screen coordinates.
344 * @param {number} x X in image coordinates.
345 * @return {number} X in screen coordinates.
347 Viewport
.prototype.imageToScreenX = function(x
) {
348 return Math
.round(this.imageBoundsOnScreen_
.left
+ x
* this.scale_
);
352 * Returns Y in screen coordinates.
353 * @param {number} y Y in image coordinates.
354 * @return {number} Y in screen coordinates.
356 Viewport
.prototype.imageToScreenY = function(y
) {
357 return Math
.round(this.imageBoundsOnScreen_
.top
+ y
* this.scale_
);
361 * Returns a rectangle in screen coordinates.
362 * @param {!ImageRect} rect Rectangle in image coordinates.
363 * @return {!ImageRect} Rectangle in screen coordinates.
365 Viewport
.prototype.imageToScreenRect = function(rect
) {
366 return new ImageRect(
367 this.imageToScreenX(rect
.left
),
368 this.imageToScreenY(rect
.top
),
369 Math
.round(this.imageToScreenSize(rect
.width
)),
370 Math
.round(this.imageToScreenSize(rect
.height
)));
374 * Returns a rectangle with given geometry.
375 * @param {number} width Width of the rectangle.
376 * @param {number} height Height of the rectangle.
377 * @param {number} offsetX X-offset of center position of the rectangle.
378 * @param {number} offsetY Y-offset of center position of the rectangle.
379 * @return {!ImageRect} Rectangle with given geometry.
382 Viewport
.prototype.getCenteredRect_ = function(
383 width
, height
, offsetX
, offsetY
) {
384 return new ImageRect(
385 ~~((this.screenBounds_
.width
- width
) / 2) + offsetX
,
386 ~~((this.screenBounds_
.height
- height
) / 2) + offsetY
,
392 * Resets zoom and offset.
394 Viewport
.prototype.resetView = function() {
403 * Recalculate the viewport parameters.
406 Viewport
.prototype.update_ = function() {
408 this.scale_
= this.getFittingScaleForImageSize_(
409 this.imageBounds_
.width
, this.imageBounds_
.height
);
411 // Limit offset values.
414 if (this.rotation_
% 2 == 0) {
415 zoomedWidht
= ~~(this.imageBounds_
.width
* this.scale_
* this.zoom_
);
416 zoomedHeight
= ~~(this.imageBounds_
.height
* this.scale_
* this.zoom_
);
418 var scale
= this.getFittingScaleForImageSize_(
419 this.imageBounds_
.height
, this.imageBounds_
.width
);
420 zoomedWidht
= ~~(this.imageBounds_
.height
* scale
* this.zoom_
);
421 zoomedHeight
= ~~(this.imageBounds_
.width
* scale
* this.zoom_
);
423 var dx
= Math
.max(zoomedWidht
- this.screenBounds_
.width
, 0) / 2;
424 var dy
= Math
.max(zoomedHeight
- this.screenBounds_
.height
, 0) / 2;
425 this.offsetX_
= ImageUtil
.clamp(-dx
, this.offsetX_
, dx
);
426 this.offsetY_
= ImageUtil
.clamp(-dy
, this.offsetY_
, dy
);
428 // Image bounds on screen.
429 this.imageBoundsOnScreen_
= this.getCenteredRect_(
430 zoomedWidht
, zoomedHeight
, this.offsetX_
, this.offsetY_
);
432 // Image bounds of element (that is not applied zoom and offset) on screen.
433 var oldBounds
= this.imageElementBoundsOnScreen_
;
434 this.imageElementBoundsOnScreen_
= this.getCenteredRect_(
435 ~~(this.imageBounds_
.width
* this.scale_
),
436 ~~(this.imageBounds_
.height
* this.scale_
),
440 this.imageElementBoundsOnScreen_
.width
!= oldBounds
.width
||
441 this.imageElementBoundsOnScreen_
.height
!= oldBounds
.height
) {
445 // Image bounds on screen clipped with the screen bounds.
446 var left
= Math
.max(this.imageBoundsOnScreen_
.left
, 0);
447 var top
= Math
.max(this.imageBoundsOnScreen_
.top
, 0);
448 var right
= Math
.min(
449 this.imageBoundsOnScreen_
.right
, this.screenBounds_
.width
);
450 var bottom
= Math
.min(
451 this.imageBoundsOnScreen_
.bottom
, this.screenBounds_
.height
);
452 this.imageBoundsOnScreenClipped_
= new ImageRect(
453 left
, top
, right
- left
, bottom
- top
);
457 * Clones the viewport.
458 * @return {!Viewport} New instance.
460 Viewport
.prototype.clone = function() {
461 var viewport
= new Viewport();
462 viewport
.imageBounds_
= ImageRect
.createFromBounds(this.imageBounds_
);
463 viewport
.screenBounds_
= ImageRect
.createFromBounds(this.screenBounds_
);
464 viewport
.scale_
= this.scale_
;
465 viewport
.zoom_
= this.zoom_
;
466 viewport
.offsetX_
= this.offsetX_
;
467 viewport
.offsetY_
= this.offsetY_
;
468 viewport
.rotation_
= this.rotation_
;
469 viewport
.generation_
= this.generation_
;
475 * Obtains CSS transformation for the screen image.
476 * @return {string} Transformation description.
478 Viewport
.prototype.getTransformation = function() {
479 var rotationScaleAdjustment
;
480 if (this.rotation_
% 2) {
481 rotationScaleAdjustment
= this.getFittingScaleForImageSize_(
482 this.imageBounds_
.height
, this.imageBounds_
.width
) / this.scale_
;
484 rotationScaleAdjustment
= 1;
487 'translate(' + this.offsetX_
+ 'px, ' + this.offsetY_
+ 'px) ',
488 'rotate(' + (this.rotation_
* 90) + 'deg)',
489 'scale(' + (this.zoom_
* rotationScaleAdjustment
) + ')'
494 * Obtains shift CSS transformation for the screen image.
495 * @param {number} dx Amount of shift.
496 * @return {string} Transformation description.
498 Viewport
.prototype.getShiftTransformation = function(dx
) {
499 return 'translateX(' + dx
+ 'px) ' + this.getTransformation();
503 * Obtains CSS transformation that makes the rotated image fit the original
504 * image. The new rotated image that the transformation is applied to looks the
505 * same with original image.
507 * @param {boolean} orientation Orientation of the rotation from the original
508 * image to the rotated image. True is for clockwise and false is for
510 * @return {string} Transformation description.
512 Viewport
.prototype.getInverseTransformForRotatedImage = function(orientation
) {
513 var previousImageWidth
= this.imageBounds_
.height
;
514 var previousImageHeight
= this.imageBounds_
.width
;
515 var oldScale
= this.getFittingScaleForImageSize_(
516 previousImageWidth
, previousImageHeight
);
517 var scaleRatio
= oldScale
/ this.scale_
;
518 var degree
= orientation
? '-90deg' : '90deg';
520 'scale(' + scaleRatio
+ ')',
521 'rotate(' + degree
+ ')',
522 this.getTransformation()
527 * Obtains CSS transformation that makes the cropped image fit the original
528 * image. The new cropped image that the transformation is applied to fits to
529 * the cropped rectangle in the original image.
531 * @param {number} imageWidth Width of the original image.
532 * @param {number} imageHeight Height of the original image.
533 * @param {!ImageRect} imageCropRect Crop rectangle in the image's coordinate
535 * @return {string} Transformation description.
537 Viewport
.prototype.getInverseTransformForCroppedImage
=
538 function(imageWidth
, imageHeight
, imageCropRect
) {
539 var wholeScale
= this.getFittingScaleForImageSize_(
540 imageWidth
, imageHeight
);
541 var croppedScale
= this.getFittingScaleForImageSize_(
542 imageCropRect
.width
, imageCropRect
.height
);
544 (imageCropRect
.left
+ imageCropRect
.width
/ 2 - imageWidth
/ 2) *
547 (imageCropRect
.top
+ imageCropRect
.height
/ 2 - imageHeight
/ 2) *
550 'translate(' + dx
+ 'px,' + dy
+ 'px)',
551 'scale(' + wholeScale
/ croppedScale
+ ')',
552 this.getTransformation()
557 * Obtains CSS transformation that makes the image fit to the screen rectangle.
559 * @param {!ImageRect} screenRect Screen rectangle.
560 * @return {string} Transformation description.
562 Viewport
.prototype.getScreenRectTransformForImage = function(screenRect
) {
563 var imageBounds
= this.getImageElementBoundsOnScreen();
564 var scaleX
= screenRect
.width
/ imageBounds
.width
;
565 var scaleY
= screenRect
.height
/ imageBounds
.height
;
566 var screenWidth
= this.screenBounds_
.width
;
567 var screenHeight
= this.screenBounds_
.height
;
568 var dx
= screenRect
.left
+ screenRect
.width
/ 2 - screenWidth
/ 2;
569 var dy
= screenRect
.top
+ screenRect
.height
/ 2 - screenHeight
/ 2;
571 'translate(' + dx
+ 'px,' + dy
+ 'px)',
572 'scale(' + scaleX
+ ',' + scaleY
+ ')',
573 this.getTransformation()