diff --git a/src/rendering/canvas-elements/layer-sprite.js b/src/rendering/canvas-elements/layer-sprite.js index 46075f9..f116c58 100644 --- a/src/rendering/canvas-elements/layer-sprite.js +++ b/src/rendering/canvas-elements/layer-sprite.js @@ -21,12 +21,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import Vue from "vue"; -import { sprite } from "zcanvas" +import ZoomableSprite from "./zoomable-sprite"; import { createCanvas, canvasToBlob, resizeImage, globalToLocal } from "@/utils/canvas-util"; import { renderCross } from "@/utils/render-util"; import { blobToResource } from "@/utils/resource-manager"; import { LAYER_GRAPHIC, LAYER_MASK, LAYER_TEXT } from "@/definitions/layer-types"; import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math"; +import { scaleRectangle } from "@/math/image-math"; import { translatePointerRotation } from "@/math/point-math"; import { renderEffectsForLayer } from "@/services/render-service"; import { clipContextToSelection } from "@/rendering/clipping"; @@ -41,12 +42,14 @@ import { getSpriteForLayer } from "@/factories/sprite-factory"; import { enqueueState } from "@/factories/history-state-factory"; import ToolTypes, { canDrawOnSelection } from "@/definitions/tool-types"; +const HALF = 0.5; + /** * A LayerSprite is the renderer for a Documents Layer. * It handles all tool interactions with the layer and also provides interaction with the Layers Mask. * It inherits from the zCanvas Sprite to be an interactive Canvas drawable. */ -class LayerSprite extends sprite { +class LayerSprite extends ZoomableSprite { constructor( layer ) { const { bitmap, x, y, width, height } = layer; super({ bitmap, x, y, width, height }); // zCanvas inheritance @@ -481,26 +484,46 @@ class LayerSprite extends sprite { } } + drawCropped( canvasContext, size ) { + if ( !this.isScaled() ) { + return super.drawCropped( canvasContext, size ); + } + const scale = 1 / this.layer.effects.scale; + const { src, dest } = size; + canvasContext.drawImage( + this._bitmap, + ( HALF + src.left * scale ) << 0, + ( HALF + src.top * scale ) << 0, + ( HALF + src.width * scale ) << 0, + ( HALF + src.height * scale ) << 0, + ( HALF + dest.left ) << 0, + ( HALF + dest.top ) << 0, + ( HALF + dest.width ) << 0, + ( HALF + dest.height ) << 0 + ); + } + draw( documentContext, viewport, omitOutlines = false ) { - const scaleDocument = this.isScaled(); + let orgBounds; // in case Layer has scale effect, apply it here (we don't resample the // actual Layer source to make this behaviour non-destructive, it's // merely a visualization and thus renderer affair) - if ( scaleDocument ) { + if ( this.isScaled() ) { const { scale } = this.layer.effects; - const { left, top, width, height } = this._bounds; - const xTranslation = ( left + width * 0.5 ) - viewport.left; - const yTranslation = ( top + height * 0.5 ) - viewport.top; - documentContext.save(); - documentContext.translate( xTranslation, yTranslation ); - documentContext.scale( scale, scale ); - documentContext.translate( -xTranslation, -yTranslation ); + // we could scale the canvas context instead, but scaling the bounds means + // that viewport pan logic will work "out of the box" + orgBounds = this._bounds; + this._bounds = scaleRectangle( orgBounds, scale ); } // invoke base class behaviour to render bitmap super.draw( documentContext, viewport ); + if ( orgBounds ) { + this._bounds = orgBounds; + } + // sprite is currently brushing, render low resolution temp contents onto screen if ( this.tempCanvas ) { documentContext.save(); @@ -567,10 +590,6 @@ class LayerSprite extends sprite { documentContext.restore(); } } - - if ( scaleDocument ) { - documentContext.restore(); - } } dispose() { diff --git a/src/rendering/canvas-elements/zoomable-sprite.js b/src/rendering/canvas-elements/zoomable-sprite.js new file mode 100644 index 0000000..27fb165 --- /dev/null +++ b/src/rendering/canvas-elements/zoomable-sprite.js @@ -0,0 +1,121 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 2021 - https://www.igorski.nl + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { sprite } from "zcanvas"; + +const { min } = Math; +const HALF = 0.5; + +class ZoomableSprite extends sprite { + constructor( opts ) { + super( opts ); + } + + drawCropped( canvasContext, { src, dest }) { + canvasContext.drawImage( + this._bitmap, + ( HALF + src.left ) << 0, + ( HALF + src.top ) << 0, + ( HALF + src.width ) << 0, + ( HALF + src.height ) << 0, + ( HALF + dest.left ) << 0, + ( HALF + dest.top ) << 0, + ( HALF + dest.width ) << 0, + ( HALF + dest.height ) << 0 + ); + } + + /* zCanvas overrides */ + + // ZoomableSprites dont function as masks, don't support tile sheets and have no children + + draw( canvasContext, viewport = null ) { + const bounds = this._bounds; + let render = this._bitmapReady; + if ( render && viewport ) { + render = isInsideViewport( bounds, viewport ); + } + if ( !render ) { + return; + } + if ( viewport ) { + this.drawCropped( canvasContext, calculateDrawRectangle( bounds, viewport )); + } else { + const { left, top, width, height } = bounds; + canvasContext.drawImage( + this._bitmap, + ( HALF + left ) << 0, + ( HALF + top ) << 0, + ( HALF + width ) << 0, + ( HALF + height ) << 0 + ); + } + } +}; +export default ZoomableSprite; + +/* internal methods */ + +export const isInsideViewport = ({ left, top, width, height }, viewport ) => { + return ( left + width ) >= viewport.left && left <= viewport.right && + ( top + height ) >= viewport.top && top <= viewport.bottom; +}; + +/** + * If the full zCanvas "document" is represented inside a smaller, pannable viewport + * we can omit drawing a Sprites unseen pixels by calculating the visible area from both + * the source drawable and destination canvas context. + */ +export const calculateDrawRectangle = ({ left, top, width, height }, viewport ) => { + const { + left : viewportX, + top : viewportY, + width : viewportWidth, + height : viewportHeight + } = viewport; + + if ( left > viewportX ) { + width = min( width, viewportWidth - ( left - viewportX )); + } else { + width = min( viewportWidth, width - ( viewportX - left )); + } + if ( top > viewportY ) { + height = min( height, viewportHeight - ( top - viewportY )); + } else { + height = min( viewportHeight, height - ( viewportY - top )); + } + + return { + src: { + left : left > viewportX ? 0 : viewportX - left, + top : top > viewportY ? 0 : viewportY - top, + width, + height + }, + dest: { + left : left > viewportX ? left - viewportX : 0, + top : top > viewportY ? top - viewportY : 0, + width, + height + } + }; +};