Home | History | Annotate | Download | only in image_editor
      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.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Viewport class controls the way the image is displayed (scale, offset etc).
      9  * @constructor
     10  */
     11 function Viewport() {
     12   this.imageBounds_ = new Rect();
     13   this.screenBounds_ = new Rect();
     14 
     15   this.scale_ = 1;
     16   this.offsetX_ = 0;
     17   this.offsetY_ = 0;
     18 
     19   this.generation_ = 0;
     20 
     21   this.scaleControl_ = null;
     22   this.repaintCallbacks_ = [];
     23   this.update();
     24 }
     25 
     26 /*
     27  * Viewport modification.
     28  */
     29 
     30 /**
     31  * @param {Object} scaleControl The UI object responsible for scaling.
     32  */
     33 Viewport.prototype.setScaleControl = function(scaleControl) {
     34   this.scaleControl_ = scaleControl;
     35 };
     36 
     37 /**
     38  * @param {number} width Image width.
     39  * @param {number} height Image height.
     40  */
     41 Viewport.prototype.setImageSize = function(width, height) {
     42   this.imageBounds_ = new Rect(width, height);
     43   if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height);
     44   this.invalidateCaches();
     45 };
     46 
     47 /**
     48  * @param {number} width Screen width.
     49  * @param {number} height Screen height.
     50  */
     51 Viewport.prototype.setScreenSize = function(width, height) {
     52   this.screenBounds_ = new Rect(width, height);
     53   if (this.scaleControl_)
     54     this.scaleControl_.setMinScale(this.getFittingScale());
     55   this.invalidateCaches();
     56 };
     57 
     58 /**
     59  * Set the size by an HTML element.
     60  *
     61  * @param {HTMLElement} frame The element acting as the "screen".
     62  */
     63 Viewport.prototype.sizeByFrame = function(frame) {
     64   this.setScreenSize(frame.clientWidth, frame.clientHeight);
     65 };
     66 
     67 /**
     68  * Set the size and scale to fit an HTML element.
     69  *
     70  * @param {HTMLElement} frame The element acting as the "screen".
     71  */
     72 Viewport.prototype.sizeByFrameAndFit = function(frame) {
     73   var wasFitting = this.getScale() == this.getFittingScale();
     74   this.sizeByFrame(frame);
     75   var minScale = this.getFittingScale();
     76   if (wasFitting || (this.getScale() < minScale)) {
     77     this.setScale(minScale, true);
     78   }
     79 };
     80 
     81 /**
     82  * @return {number} Scale.
     83  */
     84 Viewport.prototype.getScale = function() { return this.scale_ };
     85 
     86 /**
     87  * @param {number} scale The new scale.
     88  * @param {boolean} notify True if the change should be reflected in the UI.
     89  */
     90 Viewport.prototype.setScale = function(scale, notify) {
     91   if (this.scale_ == scale) return;
     92   this.scale_ = scale;
     93   if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale);
     94   this.invalidateCaches();
     95 };
     96 
     97 /**
     98  * @return {number} Best scale to fit the current image into the current screen.
     99  */
    100 Viewport.prototype.getFittingScale = function() {
    101   var scaleX = this.screenBounds_.width / this.imageBounds_.width;
    102   var scaleY = this.screenBounds_.height / this.imageBounds_.height;
    103   // Scales > (1 / this.getDevicePixelRatio()) do not look good. Also they are
    104   // not really useful as we do not have any pixel-level operations.
    105   return Math.min(1 / Viewport.getDevicePixelRatio(), scaleX, scaleY);
    106 };
    107 
    108 /**
    109  * Set the scale to fit the image into the screen.
    110  */
    111 Viewport.prototype.fitImage = function() {
    112   var scale = this.getFittingScale();
    113   if (this.scaleControl_) this.scaleControl_.setMinScale(scale);
    114   this.setScale(scale, true);
    115 };
    116 
    117 /**
    118  * @return {number} X-offset of the viewport.
    119  */
    120 Viewport.prototype.getOffsetX = function() { return this.offsetX_ };
    121 
    122 /**
    123  * @return {number} Y-offset of the viewport.
    124  */
    125 Viewport.prototype.getOffsetY = function() { return this.offsetY_ };
    126 
    127 /**
    128  * Set the image offset in the viewport.
    129  * @param {number} x X-offset.
    130  * @param {number} y Y-offset.
    131  * @param {boolean} ignoreClipping True if no clipping should be applied.
    132  */
    133 Viewport.prototype.setOffset = function(x, y, ignoreClipping) {
    134   if (!ignoreClipping) {
    135     x = this.clampOffsetX_(x);
    136     y = this.clampOffsetY_(y);
    137   }
    138   if (this.offsetX_ == x && this.offsetY_ == y) return;
    139   this.offsetX_ = x;
    140   this.offsetY_ = y;
    141   this.invalidateCaches();
    142 };
    143 
    144 /**
    145  * Return a closure that can be called to pan the image.
    146  * Useful for implementing non-trivial variants of panning (overview etc).
    147  * @param {number} originalX The x coordinate on the screen canvas that
    148  *                 corresponds to zero change to offsetX.
    149  * @param {number} originalY The y coordinate on the screen canvas that
    150  *                 corresponds to zero change to offsetY.
    151  * @param {function():number} scaleFunc returns the image to screen scale.
    152  * @param {function(number,number):boolean} hitFunc returns true if (x,y) is
    153  *                                                  in the valid region.
    154  * @return {function} The closure to pan the image.
    155  */
    156 Viewport.prototype.createOffsetSetter = function(
    157     originalX, originalY, scaleFunc, hitFunc) {
    158   var originalOffsetX = this.offsetX_;
    159   var originalOffsetY = this.offsetY_;
    160   if (!hitFunc) hitFunc = function() { return true };
    161   if (!scaleFunc) scaleFunc = this.getScale.bind(this);
    162 
    163   var self = this;
    164   return function(x, y) {
    165     if (hitFunc(x, y)) {
    166       var scale = scaleFunc();
    167       self.setOffset(
    168           originalOffsetX + (x - originalX) / scale,
    169           originalOffsetY + (y - originalY) / scale);
    170       self.repaint();
    171     }
    172   };
    173 };
    174 
    175 /*
    176  * Access to the current viewport state.
    177  */
    178 
    179 /**
    180  * @return {Rect} The image bounds in image coordinates.
    181  */
    182 Viewport.prototype.getImageBounds = function() { return this.imageBounds_ };
    183 
    184 /**
    185 * @return {Rect} The screen bounds in screen coordinates.
    186 */
    187 Viewport.prototype.getScreenBounds = function() { return this.screenBounds_ };
    188 
    189 /**
    190  * @return {Rect} The visible part of the image, in image coordinates.
    191  */
    192 Viewport.prototype.getImageClipped = function() { return this.imageClipped_ };
    193 
    194 /**
    195  * @return {Rect} The visible part of the image, in screen coordinates.
    196  */
    197 Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ };
    198 
    199 /**
    200  * A counter that is incremented with each viewport state change.
    201  * Clients that cache anything that depends on the viewport state should keep
    202  * track of this counter.
    203  * @return {number} counter.
    204  */
    205 Viewport.prototype.getCacheGeneration = function() { return this.generation_ };
    206 
    207 /**
    208  * Called on event view port state change (even if repaint has not been called).
    209  */
    210 Viewport.prototype.invalidateCaches = function() { this.generation_++ };
    211 
    212 /**
    213  * @return {Rect} The image bounds in screen coordinates.
    214  */
    215 Viewport.prototype.getImageBoundsOnScreen = function() {
    216   return this.imageOnScreen_;
    217 };
    218 
    219 /*
    220  * Conversion between the screen and image coordinate spaces.
    221  */
    222 
    223 /**
    224  * @param {number} size Size in screen coordinates.
    225  * @return {number} Size in image coordinates.
    226  */
    227 Viewport.prototype.screenToImageSize = function(size) {
    228   return size / this.getScale();
    229 };
    230 
    231 /**
    232  * @param {number} x X in screen coordinates.
    233  * @return {number} X in image coordinates.
    234  */
    235 Viewport.prototype.screenToImageX = function(x) {
    236   return Math.round((x - this.imageOnScreen_.left) / this.getScale());
    237 };
    238 
    239 /**
    240  * @param {number} y Y in screen coordinates.
    241  * @return {number} Y in image coordinates.
    242  */
    243 Viewport.prototype.screenToImageY = function(y) {
    244   return Math.round((y - this.imageOnScreen_.top) / this.getScale());
    245 };
    246 
    247 /**
    248  * @param {Rect} rect Rectangle in screen coordinates.
    249  * @return {Rect} Rectangle in image coordinates.
    250  */
    251 Viewport.prototype.screenToImageRect = function(rect) {
    252   return new Rect(
    253       this.screenToImageX(rect.left),
    254       this.screenToImageY(rect.top),
    255       this.screenToImageSize(rect.width),
    256       this.screenToImageSize(rect.height));
    257 };
    258 
    259 /**
    260  * @param {number} size Size in image coordinates.
    261  * @return {number} Size in screen coordinates.
    262  */
    263 Viewport.prototype.imageToScreenSize = function(size) {
    264   return size * this.getScale();
    265 };
    266 
    267 /**
    268  * @param {number} x X in image coordinates.
    269  * @return {number} X in screen coordinates.
    270  */
    271 Viewport.prototype.imageToScreenX = function(x) {
    272   return Math.round(this.imageOnScreen_.left + x * this.getScale());
    273 };
    274 
    275 /**
    276  * @param {number} y Y in image coordinates.
    277  * @return {number} Y in screen coordinates.
    278  */
    279 Viewport.prototype.imageToScreenY = function(y) {
    280   return Math.round(this.imageOnScreen_.top + y * this.getScale());
    281 };
    282 
    283 /**
    284  * @param {Rect} rect Rectangle in image coordinates.
    285  * @return {Rect} Rectangle in screen coordinates.
    286  */
    287 Viewport.prototype.imageToScreenRect = function(rect) {
    288   return new Rect(
    289       this.imageToScreenX(rect.left),
    290       this.imageToScreenY(rect.top),
    291       Math.round(this.imageToScreenSize(rect.width)),
    292       Math.round(this.imageToScreenSize(rect.height)));
    293 };
    294 
    295 /**
    296  * @return {number} The number of physical pixels in one CSS pixel.
    297  */
    298 Viewport.getDevicePixelRatio = function() { return window.devicePixelRatio };
    299 
    300 /**
    301  * Convert a rectangle from screen coordinates to 'device' coordinates.
    302  *
    303  * This conversion enlarges the original rectangle devicePixelRatio times
    304  * with the screen center as a fixed point.
    305  *
    306  * @param {Rect} rect Rectangle in screen coordinates.
    307  * @return {Rect} Rectangle in device coordinates.
    308  */
    309 Viewport.prototype.screenToDeviceRect = function(rect) {
    310   var ratio = Viewport.getDevicePixelRatio();
    311   var screenCenterX = Math.round(
    312       this.screenBounds_.left + this.screenBounds_.width / 2);
    313   var screenCenterY = Math.round(
    314       this.screenBounds_.top + this.screenBounds_.height / 2);
    315   return new Rect(screenCenterX + (rect.left - screenCenterX) * ratio,
    316                   screenCenterY + (rect.top - screenCenterY) * ratio,
    317                   rect.width * ratio,
    318                   rect.height * ratio);
    319 };
    320 
    321 /**
    322  * @return {Rect} The visible part of the image, in device coordinates.
    323  */
    324 Viewport.prototype.getDeviceClipped = function() {
    325   return this.screenToDeviceRect(this.getScreenClipped());
    326 };
    327 
    328 /**
    329  * @return {boolean} True if some part of the image is clipped by the screen.
    330  */
    331 Viewport.prototype.isClipped = function() {
    332   return this.getMarginX_() < 0 || this.getMarginY_() < 0;
    333 };
    334 
    335 /**
    336  * @return {number} Horizontal margin.
    337  *   Negative if the image is clipped horizontally.
    338  * @private
    339  */
    340 Viewport.prototype.getMarginX_ = function() {
    341   return Math.round(
    342     (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
    343 };
    344 
    345 /**
    346  * @return {number} Vertical margin.
    347  *   Negative if the image is clipped vertically.
    348  * @private
    349  */
    350 Viewport.prototype.getMarginY_ = function() {
    351   return Math.round(
    352     (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
    353 };
    354 
    355 /**
    356  * @param {number} x X-offset.
    357  * @return {number} X-offset clamped to the valid range.
    358  * @private
    359  */
    360 Viewport.prototype.clampOffsetX_ = function(x) {
    361   var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale()));
    362   return ImageUtil.clamp(-limit, x, limit);
    363 };
    364 
    365 /**
    366  * @param {number} y Y-offset.
    367  * @return {number} Y-offset clamped to the valid range.
    368  * @private
    369  */
    370 Viewport.prototype.clampOffsetY_ = function(y) {
    371   var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale()));
    372   return ImageUtil.clamp(-limit, y, limit);
    373 };
    374 
    375 /**
    376  * Recalculate the viewport parameters.
    377  */
    378 Viewport.prototype.update = function() {
    379   var scale = this.getScale();
    380 
    381   // Image bounds in screen coordinates.
    382   this.imageOnScreen_ = new Rect(
    383       this.getMarginX_(),
    384       this.getMarginY_(),
    385       Math.round(this.imageBounds_.width * scale),
    386       Math.round(this.imageBounds_.height * scale));
    387 
    388   // A visible part of the image in image coordinates.
    389   this.imageClipped_ = new Rect(this.imageBounds_);
    390 
    391   // A visible part of the image in screen coordinates.
    392   this.screenClipped_ = new Rect(this.screenBounds_);
    393 
    394   // Adjust for the offset.
    395   if (this.imageOnScreen_.left < 0) {
    396     this.imageOnScreen_.left +=
    397         Math.round(this.clampOffsetX_(this.offsetX_) * scale);
    398     this.imageClipped_.left = Math.round(-this.imageOnScreen_.left / scale);
    399     this.imageClipped_.width = Math.round(this.screenBounds_.width / scale);
    400   } else {
    401     this.screenClipped_.left = this.imageOnScreen_.left;
    402     this.screenClipped_.width = this.imageOnScreen_.width;
    403   }
    404 
    405   if (this.imageOnScreen_.top < 0) {
    406     this.imageOnScreen_.top +=
    407         Math.round(this.clampOffsetY_(this.offsetY_) * scale);
    408     this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale);
    409     this.imageClipped_.height = Math.round(this.screenBounds_.height / scale);
    410   } else {
    411     this.screenClipped_.top = this.imageOnScreen_.top;
    412     this.screenClipped_.height = this.imageOnScreen_.height;
    413   }
    414 };
    415 
    416 /**
    417  * @param {function} callback Repaint callback.
    418  */
    419 Viewport.prototype.addRepaintCallback = function(callback) {
    420   this.repaintCallbacks_.push(callback);
    421 };
    422 
    423 /**
    424  * Repaint all clients.
    425  */
    426 Viewport.prototype.repaint = function() {
    427   this.update();
    428   for (var i = 0; i != this.repaintCallbacks_.length; i++)
    429     this.repaintCallbacks_[i]();
    430 };
    431