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 * Index of zoom ratio. 0 is "zoom ratio = 1".
63 * Zoom ratio specified by user operations.
70 * Offset specified by user operations.
76 * Offset specified by user operations.
82 * Generation of the screen size image cache.
83 * This is incremented every time when the size of image cache is changed.
96 * @type {Object.<string, number>}
99 Viewport.ZOOM_RATIOS = Object.freeze({
110 * @param {number} width Image width.
111 * @param {number} height Image height.
113 Viewport.prototype.setImageSize = function(width, height) {
114 this.imageBounds_ = new Rect(width, height);
116 this.invalidateCaches();
120 * @param {number} width Screen width.
121 * @param {number} height Screen height.
123 Viewport.prototype.setScreenSize = function(width, height) {
124 this.screenBounds_ = new Rect(width, height);
126 this.invalidateCaches();
130 * Sets the new zoom ratio.
131 * @param {number} zoomIndex New zoom index.
133 Viewport.prototype.setZoomIndex = function(zoomIndex) {
134 // Ignore the invalid zoomIndex.
135 if (!Viewport.ZOOM_RATIOS[zoomIndex.toString()])
137 this.zoomIndex_ = zoomIndex;
138 this.zoom_ = Viewport.ZOOM_RATIOS[zoomIndex];
143 * Returns the current zoom index.
144 * @return {number} Zoon index.
146 Viewport.prototype.getZoomIndex = function() {
147 return this.zoomIndex_;
151 * @return {number} Scale.
153 Viewport.prototype.getScale = function() { return this.scale_; };
156 * @param {number} scale The new scale.
158 Viewport.prototype.setScale = function(scale) {
159 if (this.scale_ == scale)
163 this.invalidateCaches();
167 * @return {number} Best scale to fit the current image into the current screen.
169 Viewport.prototype.getFittingScale = function() {
170 return this.getFittingScaleForImageSize_(
171 this.imageBounds_.width, this.imageBounds_.height);
175 * Obtains the scale for the specified image size.
177 * @param {number} width Width of the full resolution image.
178 * @param {number} height Height of the full resolution image.
179 * @return {number} The ratio of the fullresotion image size and the calculated
180 * displayed image size.
182 Viewport.prototype.getFittingScaleForImageSize_ = function(width, height) {
183 var scaleX = this.screenBounds_.width / width;
184 var scaleY = this.screenBounds_.height / height;
185 // Scales > (1 / devicePixelRatio) do not look good. Also they are
186 // not really useful as we do not have any pixel-level operations.
187 return Math.min(1 / window.devicePixelRatio, scaleX, scaleY);
191 * Set the scale to fit the image into the screen.
193 Viewport.prototype.fitImage = function() {
194 this.setScale(this.getFittingScale());
198 * @return {number} X-offset of the viewport.
200 Viewport.prototype.getOffsetX = function() { return this.offsetX_; };
203 * @return {number} Y-offset of the viewport.
205 Viewport.prototype.getOffsetY = function() { return this.offsetY_; };
208 * Set the image offset in the viewport.
209 * @param {number} x X-offset.
210 * @param {number} y Y-offset.
211 * @param {boolean} ignoreClipping True if no clipping should be applied.
213 Viewport.prototype.setOffset = function(x, y, ignoreClipping) {
214 if (!ignoreClipping) {
215 x = this.clampOffsetX_(x);
216 y = this.clampOffsetY_(y);
218 if (this.offsetX_ == x && this.offsetY_ == y) return;
221 this.invalidateCaches();
225 * @return {Rect} The image bounds in image coordinates.
227 Viewport.prototype.getImageBounds = function() { return this.imageBounds_; };
230 * @return {Rect} The screen bounds in screen coordinates.
232 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_; };
235 * @return {Rect} The size of screen cache canvas.
237 Viewport.prototype.getDeviceBounds = function() {
238 var size = this.getImageElementBoundsOnScreen();
240 size.width * window.devicePixelRatio,
241 size.height * window.devicePixelRatio);
245 * A counter that is incremented with each viewport state change.
246 * Clients that cache anything that depends on the viewport state should keep
247 * track of this counter.
248 * @return {number} counter.
250 Viewport.prototype.getCacheGeneration = function() { return this.generation_; };
253 * Called on event view port state change.
255 Viewport.prototype.invalidateCaches = function() { this.generation_++; };
258 * @return {Rect} The image bounds in screen coordinates.
260 Viewport.prototype.getImageBoundsOnScreen = function() {
261 return this.imageBoundsOnScreen_;
265 * The image bounds in screen coordinates.
266 * This returns the bounds of element before applying zoom and offset.
269 Viewport.prototype.getImageElementBoundsOnScreen = function() {
270 return this.imageElementBoundsOnScreen_;
274 * The image bounds on screen, which is clipped with the screen size.
277 Viewport.prototype.getImageBoundsOnScreenClipped = function() {
278 return this.imageBoundsOnScreenClipped_;
282 * @param {number} size Size in screen coordinates.
283 * @return {number} Size in image coordinates.
285 Viewport.prototype.screenToImageSize = function(size) {
286 return size / this.getScale();
290 * @param {number} x X in screen coordinates.
291 * @return {number} X in image coordinates.
293 Viewport.prototype.screenToImageX = function(x) {
294 return Math.round((x - this.imageBoundsOnScreen_.left) / this.getScale());
298 * @param {number} y Y in screen coordinates.
299 * @return {number} Y in image coordinates.
301 Viewport.prototype.screenToImageY = function(y) {
302 return Math.round((y - this.imageBoundsOnScreen_.top) / this.getScale());
306 * @param {Rect} rect Rectangle in screen coordinates.
307 * @return {Rect} Rectangle in image coordinates.
309 Viewport.prototype.screenToImageRect = function(rect) {
311 this.screenToImageX(rect.left),
312 this.screenToImageY(rect.top),
313 this.screenToImageSize(rect.width),
314 this.screenToImageSize(rect.height));
318 * @param {number} size Size in image coordinates.
319 * @return {number} Size in screen coordinates.
321 Viewport.prototype.imageToScreenSize = function(size) {
322 return size * this.getScale();
326 * @param {number} x X in image coordinates.
327 * @return {number} X in screen coordinates.
329 Viewport.prototype.imageToScreenX = function(x) {
330 return Math.round(this.imageBoundsOnScreen_.left + x * this.getScale());
334 * @param {number} y Y in image coordinates.
335 * @return {number} Y in screen coordinates.
337 Viewport.prototype.imageToScreenY = function(y) {
338 return Math.round(this.imageBoundsOnScreen_.top + y * this.getScale());
342 * @param {Rect} rect Rectangle in image coordinates.
343 * @return {Rect} Rectangle in screen coordinates.
345 Viewport.prototype.imageToScreenRect = function(rect) {
347 this.imageToScreenX(rect.left),
348 this.imageToScreenY(rect.top),
349 Math.round(this.imageToScreenSize(rect.width)),
350 Math.round(this.imageToScreenSize(rect.height)));
354 * @return {boolean} True if some part of the image is clipped by the screen.
356 Viewport.prototype.isClipped = function() {
357 return this.getMarginX_() < 0 || this.getMarginY_() < 0;
361 * @return {number} Horizontal margin.
362 * Negative if the image is clipped horizontally.
365 Viewport.prototype.getMarginX_ = function() {
367 (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
371 * @return {number} Vertical margin.
372 * Negative if the image is clipped vertically.
375 Viewport.prototype.getMarginY_ = function() {
377 (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
381 * @param {number} x X-offset.
382 * @return {number} X-offset clamped to the valid range.
385 Viewport.prototype.clampOffsetX_ = function(x) {
386 var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale()));
387 return ImageUtil.clamp(-limit, x, limit);
391 * @param {number} y Y-offset.
392 * @return {number} Y-offset clamped to the valid range.
395 Viewport.prototype.clampOffsetY_ = function(y) {
396 var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale()));
397 return ImageUtil.clamp(-limit, y, limit);
403 Viewport.prototype.getCenteredRect_ = function(
404 width, height, offsetX, offsetY) {
406 ~~((this.screenBounds_.width - width) / 2) + offsetX,
407 ~~((this.screenBounds_.height - height) / 2) + offsetY,
413 * Recalculate the viewport parameters.
415 Viewport.prototype.update = function() {
416 var scale = this.getScale();
418 // Image bounds on screen.
419 this.imageBoundsOnScreen_ = this.getCenteredRect_(
420 ~~(this.imageBounds_.width * scale * this.zoom_),
421 ~~(this.imageBounds_.height * scale * this.zoom_),
425 // Image bounds of element (that is not applied zoom and offset) on screen.
426 this.imageElementBoundsOnScreen_ = this.getCenteredRect_(
427 ~~(this.imageBounds_.width * scale),
428 ~~(this.imageBounds_.height * scale),
432 // Image bounds on screen cliped with the screen bounds.
433 var left = Math.max(this.imageBoundsOnScreen_.left, 0);
434 var top = Math.max(this.imageBoundsOnScreen_.top, 0);
435 var right = Math.min(
436 this.imageBoundsOnScreen_.right, this.screenBounds_.width);
437 var bottom = Math.min(
438 this.imageBoundsOnScreen_.bottom, this.screenBounds_.height);
439 this.imageBoundsOnScreenClipped_ = new Rect(
440 left, top, right - left, bottom - top);
444 * Obtains CSS transformation for the screen image.
445 * @return {string} Transformation description.
447 Viewport.prototype.getTransformation = function() {
448 return 'scale(' + (1 / window.devicePixelRatio * this.zoom_) + ')';
452 * Obtains shift CSS transformation for the screen image.
453 * @param {number} dx Amount of shift.
454 * @return {string} Transformation description.
456 Viewport.prototype.getShiftTransformation = function(dx) {
457 return 'translateX(' + dx + 'px) ' + this.getTransformation();
461 * Obtains CSS transformation that makes the rotated image fit the original
462 * image. The new rotated image that the transformation is applied to looks the
463 * same with original image.
465 * @param {boolean} orientation Orientation of the rotation from the original
466 * image to the rotated image. True is for clockwise and false is for
468 * @return {string} Transformation description.
470 Viewport.prototype.getInverseTransformForRotatedImage = function(orientation) {
471 var previousImageWidth = this.imageBounds_.height;
472 var previousImageHeight = this.imageBounds_.width;
473 var oldScale = this.getFittingScaleForImageSize_(
474 previousImageWidth, previousImageHeight);
475 var scaleRatio = oldScale / this.getScale();
476 var degree = orientation ? '-90deg' : '90deg';
478 'scale(' + scaleRatio + ')',
479 'rotate(' + degree + ')',
480 this.getTransformation()
485 * Obtains CSS transformation that makes the cropped image fit the original
486 * image. The new cropped image that the transformaton is applied to fits to the
487 * the cropped rectangle in the original image.
489 * @param {number} imageWidth Width of the original image.
490 * @param {number} imageHeight Height of the origianl image.
491 * @param {Rect} imageCropRect Crop rectangle in the image's coordinate system.
492 * @return {string} Transformation description.
494 Viewport.prototype.getInverseTransformForCroppedImage =
495 function(imageWidth, imageHeight, imageCropRect) {
496 var wholeScale = this.getFittingScaleForImageSize_(
497 imageWidth, imageHeight);
498 var croppedScale = this.getFittingScaleForImageSize_(
499 imageCropRect.width, imageCropRect.height);
501 (imageCropRect.left + imageCropRect.width / 2 - imageWidth / 2) *
504 (imageCropRect.top + imageCropRect.height / 2 - imageHeight / 2) *
507 'translate(' + dx + 'px,' + dy + 'px)',
508 'scale(' + wholeScale / croppedScale + ')',
509 this.getTransformation()
514 * Obtains CSS transformaton that makes the image fit to the screen rectangle.
516 * @param {Rect} screenRect Screen rectangle.
517 * @return {string} Transformation description.
519 Viewport.prototype.getScreenRectTransformForImage = function(screenRect) {
520 var imageBounds = this.getImageElementBoundsOnScreen();
521 var scaleX = screenRect.width / imageBounds.width;
522 var scaleY = screenRect.height / imageBounds.height;
523 var screenWidth = this.screenBounds_.width;
524 var screenHeight = this.screenBounds_.height;
525 var dx = screenRect.left + screenRect.width / 2 - screenWidth / 2;
526 var dy = screenRect.top + screenRect.height / 2 - screenHeight / 2;
528 'translate(' + dx + 'px,' + dy + 'px)',
529 'scale(' + scaleX + ',' + scaleY + ')',
530 this.getTransformation()