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.
8 * Viewport class controls the way the image is displayed (scale, offset etc).
13 * Size of the full resolution image.
17 this.imageBounds_ = new Rect();
20 * Size of the application window.
24 this.screenBounds_ = new Rect();
27 * Bounds of the image element on screen without zoom and offset.
31 this.imageElementBoundsOnScreen_ = null;
34 * Bounds of the image with zoom and offset.
38 this.imageBoundsOnScreen_ = null;
41 * Image bounds that is clipped with the screen bounds.
45 this.imageBoundsOnScreenClipped_ = null;
48 * Scale from the full resolution image to the screen displayed image. This is
49 * not zoom operated by users.
56 * Zoom ratio specified by user operations.
63 * Offset specified by user operations.
70 * Offset specified by user operations.
77 * Integer Rotation value.
78 * The rotation angle is this.rotation_ * 90.
85 * Generation of the screen size image cache.
86 * This is incremented every time when the size of image cache is changed.
99 * @type {Array.<number>}
102 Viewport.ZOOM_RATIOS = Object.freeze([1, 1.5, 2, 3]);
105 * @param {number} width Image width.
106 * @param {number} height Image height.
108 Viewport.prototype.setImageSize = function(width, height) {
109 this.imageBounds_ = new Rect(width, height);
114 * @param {number} width Screen width.
115 * @param {number} height Screen height.
117 Viewport.prototype.setScreenSize = function(width, height) {
118 this.screenBounds_ = new Rect(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 // Scales > (1 / devicePixelRatio) do not look good. Also they are
207 // not really useful as we do not have any pixel-level operations.
208 return Math.min(1 / window.devicePixelRatio, scaleX, scaleY);
212 * @return {number} X-offset of the viewport.
214 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 * @return {Rect} The image bounds in image coordinates.
237 Viewport.prototype.getImageBounds = function() { return this.imageBounds_; };
240 * @return {Rect} The screen bounds in screen coordinates.
242 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_; };
245 * @return {Rect} The size of screen cache canvas.
247 Viewport.prototype.getDeviceBounds = function() {
248 var size = this.getImageElementBoundsOnScreen();
250 size.width * window.devicePixelRatio,
251 size.height * window.devicePixelRatio);
255 * A counter that is incremented with each viewport state change.
256 * Clients that cache anything that depends on the viewport state should keep
257 * track of this counter.
258 * @return {number} counter.
260 Viewport.prototype.getCacheGeneration = function() { return this.generation_; };
263 * @return {Rect} The image bounds in screen coordinates.
265 Viewport.prototype.getImageBoundsOnScreen = function() {
266 return this.imageBoundsOnScreen_;
270 * The image bounds in screen coordinates.
271 * This returns the bounds of element before applying zoom and offset.
274 Viewport.prototype.getImageElementBoundsOnScreen = function() {
275 return this.imageElementBoundsOnScreen_;
279 * The image bounds on screen, which is clipped with the screen size.
282 Viewport.prototype.getImageBoundsOnScreenClipped = function() {
283 return this.imageBoundsOnScreenClipped_;
287 * @param {number} size Size in screen coordinates.
288 * @return {number} Size in image coordinates.
290 Viewport.prototype.screenToImageSize = function(size) {
291 return size / this.scale_;
295 * @param {number} x X in screen coordinates.
296 * @return {number} X in image coordinates.
298 Viewport.prototype.screenToImageX = function(x) {
299 return Math.round((x - this.imageBoundsOnScreen_.left) / this.scale_);
303 * @param {number} y Y in screen coordinates.
304 * @return {number} Y in image coordinates.
306 Viewport.prototype.screenToImageY = function(y) {
307 return Math.round((y - this.imageBoundsOnScreen_.top) / this.scale_);
311 * @param {Rect} rect Rectangle in screen coordinates.
312 * @return {Rect} Rectangle in image coordinates.
314 Viewport.prototype.screenToImageRect = function(rect) {
316 this.screenToImageX(rect.left),
317 this.screenToImageY(rect.top),
318 this.screenToImageSize(rect.width),
319 this.screenToImageSize(rect.height));
323 * @param {number} size Size in image coordinates.
324 * @return {number} Size in screen coordinates.
326 Viewport.prototype.imageToScreenSize = function(size) {
327 return size * this.scale_;
331 * @param {number} x X in image coordinates.
332 * @return {number} X in screen coordinates.
334 Viewport.prototype.imageToScreenX = function(x) {
335 return Math.round(this.imageBoundsOnScreen_.left + x * this.scale_);
339 * @param {number} y Y in image coordinates.
340 * @return {number} Y in screen coordinates.
342 Viewport.prototype.imageToScreenY = function(y) {
343 return Math.round(this.imageBoundsOnScreen_.top + y * this.scale_);
347 * @param {Rect} rect Rectangle in image coordinates.
348 * @return {Rect} Rectangle in screen coordinates.
350 Viewport.prototype.imageToScreenRect = function(rect) {
352 this.imageToScreenX(rect.left),
353 this.imageToScreenY(rect.top),
354 Math.round(this.imageToScreenSize(rect.width)),
355 Math.round(this.imageToScreenSize(rect.height)));
359 * @param {number} width Width of the rectangle.
360 * @param {number} height Height of the rectangle.
361 * @param {number} offsetX X-offset of center position of the rectangle.
362 * @param {number} offsetY Y-offset of center position of the rectangle.
363 * @return {Rect} Rectangle with given geometry.
366 Viewport.prototype.getCenteredRect_ = function(
367 width, height, offsetX, offsetY) {
369 ~~((this.screenBounds_.width - width) / 2) + offsetX,
370 ~~((this.screenBounds_.height - height) / 2) + offsetY,
376 * Resets zoom and offset.
378 Viewport.prototype.resetView = function() {
387 * Recalculate the viewport parameters.
390 Viewport.prototype.update_ = function() {
392 this.scale_ = this.getFittingScaleForImageSize_(
393 this.imageBounds_.width, this.imageBounds_.height);
395 // Limit offset values.
398 if (this.rotation_ % 2 == 0) {
399 zoomedWidht = ~~(this.imageBounds_.width * this.scale_ * this.zoom_);
400 zoomedHeight = ~~(this.imageBounds_.height * this.scale_ * this.zoom_);
402 var scale = this.getFittingScaleForImageSize_(
403 this.imageBounds_.height, this.imageBounds_.width);
404 zoomedWidht = ~~(this.imageBounds_.height * scale * this.zoom_);
405 zoomedHeight = ~~(this.imageBounds_.width * scale * this.zoom_);
407 var dx = Math.max(zoomedWidht - this.screenBounds_.width, 0) / 2;
408 var dy = Math.max(zoomedHeight - this.screenBounds_.height, 0) /2;
409 this.offsetX_ = ImageUtil.clamp(-dx, this.offsetX_, dx);
410 this.offsetY_ = ImageUtil.clamp(-dy, this.offsetY_, dy);
412 // Image bounds on screen.
413 this.imageBoundsOnScreen_ = this.getCenteredRect_(
414 zoomedWidht, zoomedHeight, this.offsetX_, this.offsetY_);
416 // Image bounds of element (that is not applied zoom and offset) on screen.
417 var oldBounds = this.imageElementBoundsOnScreen_;
418 this.imageElementBoundsOnScreen_ = this.getCenteredRect_(
419 ~~(this.imageBounds_.width * this.scale_),
420 ~~(this.imageBounds_.height * this.scale_),
424 this.imageElementBoundsOnScreen_.width != oldBounds.width ||
425 this.imageElementBoundsOnScreen_.height != oldBounds.height) {
429 // Image bounds on screen clipped with the screen bounds.
430 var left = Math.max(this.imageBoundsOnScreen_.left, 0);
431 var top = Math.max(this.imageBoundsOnScreen_.top, 0);
432 var right = Math.min(
433 this.imageBoundsOnScreen_.right, this.screenBounds_.width);
434 var bottom = Math.min(
435 this.imageBoundsOnScreen_.bottom, this.screenBounds_.height);
436 this.imageBoundsOnScreenClipped_ = new Rect(
437 left, top, right - left, bottom - top);
441 * Clones the viewport.
442 * @return {Viewport} New instance.
444 Viewport.prototype.clone = function() {
445 var viewport = new Viewport();
446 viewport.imageBounds_ = new Rect(this.imageBounds_);
447 viewport.screenBounds_ = new Rect(this.screenBounds_);
448 viewport.scale_ = this.scale_;
449 viewport.zoom_ = this.zoom_;
450 viewport.offsetX_ = this.offsetX_;
451 viewport.offsetY_ = this.offsetY_;
452 viewport.rotation_ = this.rotation_;
453 viewport.generation_ = this.generation_;
459 * Obtains CSS transformation for the screen image.
460 * @return {string} Transformation description.
462 Viewport.prototype.getTransformation = function() {
463 var rotationScaleAdjustment;
464 if (this.rotation_ % 2) {
465 rotationScaleAdjustment = this.getFittingScaleForImageSize_(
466 this.imageBounds_.height, this.imageBounds_.width) / this.scale_;
468 rotationScaleAdjustment = 1;
471 'translate(' + this.offsetX_ + 'px, ' + this.offsetY_ + 'px) ',
472 'rotate(' + (this.rotation_ * 90) + 'deg)',
473 'scale(' + (this.zoom_ * rotationScaleAdjustment) + ')'
478 * Obtains shift CSS transformation for the screen image.
479 * @param {number} dx Amount of shift.
480 * @return {string} Transformation description.
482 Viewport.prototype.getShiftTransformation = function(dx) {
483 return 'translateX(' + dx + 'px) ' + this.getTransformation();
487 * Obtains CSS transformation that makes the rotated image fit the original
488 * image. The new rotated image that the transformation is applied to looks the
489 * same with original image.
491 * @param {boolean} orientation Orientation of the rotation from the original
492 * image to the rotated image. True is for clockwise and false is for
494 * @return {string} Transformation description.
496 Viewport.prototype.getInverseTransformForRotatedImage = function(orientation) {
497 var previousImageWidth = this.imageBounds_.height;
498 var previousImageHeight = this.imageBounds_.width;
499 var oldScale = this.getFittingScaleForImageSize_(
500 previousImageWidth, previousImageHeight);
501 var scaleRatio = oldScale / this.scale_;
502 var degree = orientation ? '-90deg' : '90deg';
504 'scale(' + scaleRatio + ')',
505 'rotate(' + degree + ')',
506 this.getTransformation()
511 * Obtains CSS transformation that makes the cropped image fit the original
512 * image. The new cropped image that the transformation is applied to fits to
513 * the cropped rectangle in the original image.
515 * @param {number} imageWidth Width of the original image.
516 * @param {number} imageHeight Height of the original image.
517 * @param {Rect} imageCropRect Crop rectangle in the image's coordinate system.
518 * @return {string} Transformation description.
520 Viewport.prototype.getInverseTransformForCroppedImage =
521 function(imageWidth, imageHeight, imageCropRect) {
522 var wholeScale = this.getFittingScaleForImageSize_(
523 imageWidth, imageHeight);
524 var croppedScale = this.getFittingScaleForImageSize_(
525 imageCropRect.width, imageCropRect.height);
527 (imageCropRect.left + imageCropRect.width / 2 - imageWidth / 2) *
530 (imageCropRect.top + imageCropRect.height / 2 - imageHeight / 2) *
533 'translate(' + dx + 'px,' + dy + 'px)',
534 'scale(' + wholeScale / croppedScale + ')',
535 this.getTransformation()
540 * Obtains CSS transformation that makes the image fit to the screen rectangle.
542 * @param {Rect} screenRect Screen rectangle.
543 * @return {string} Transformation description.
545 Viewport.prototype.getScreenRectTransformForImage = function(screenRect) {
546 var imageBounds = this.getImageElementBoundsOnScreen();
547 var scaleX = screenRect.width / imageBounds.width;
548 var scaleY = screenRect.height / imageBounds.height;
549 var screenWidth = this.screenBounds_.width;
550 var screenHeight = this.screenBounds_.height;
551 var dx = screenRect.left + screenRect.width / 2 - screenWidth / 2;
552 var dy = screenRect.top + screenRect.height / 2 - screenHeight / 2;
554 'translate(' + dx + 'px,' + dy + 'px)',
555 'scale(' + scaleX + ',' + scaleY + ')',
556 this.getTransformation()