diff --git a/src/components/options-panel/tool-options-brush/tool-options-brush.vue b/src/components/options-panel/tool-options-brush/tool-options-brush.vue index 4f789df..0f30d5b 100644 --- a/src/components/options-panel/tool-options-brush/tool-options-brush.vue +++ b/src/components/options-panel/tool-options-brush/tool-options-brush.vue @@ -79,7 +79,7 @@ export default { return [ { text: this.$t( "line" ), value: BrushTypes.LINE }, { text: this.$t( "pen" ), value: BrushTypes.PEN }, - // { text: this.$t( "curvedPen" ), value: BrushTypes.CURVED_PEN }, + { text: this.$t( "curvedPen" ), value: BrushTypes.CURVED_PEN }, { text: this.$t( "calligraphic" ), value: BrushTypes.CALLIGRAPHIC }, { text: this.$t( "paintBrush" ), value: BrushTypes.PAINT_BRUSH }, { text: this.$t( "sprayCan" ), value: BrushTypes.SPRAY } diff --git a/src/components/ui/zcanvas/interaction-pane.js b/src/components/ui/zcanvas/interaction-pane.js index b1f698b..2510edc 100644 --- a/src/components/ui/zcanvas/interaction-pane.js +++ b/src/components/ui/zcanvas/interaction-pane.js @@ -76,7 +76,7 @@ class InteractionPane extends sprite { zCanvas.getHeight() / zCanvas.zoomFactor ); - if ( mode === MODE_SELECTION ) { + if ( document && mode === MODE_SELECTION ) { if ( !document.selection ) { this.setSelection( [] ); } diff --git a/src/components/ui/zcanvas/layer-sprite.js b/src/components/ui/zcanvas/layer-sprite.js index 2569bff..656a5d4 100644 --- a/src/components/ui/zcanvas/layer-sprite.js +++ b/src/components/ui/zcanvas/layer-sprite.js @@ -94,14 +94,15 @@ class LayerSprite extends sprite { cacheBrush( color = "rgba(255,0,0,1)", toolOptions = { radius: 5, strokes: 1 } ) { this._brush = BrushFactory.create({ color, - radius: toolOptions.size, - pointer: this._brush.pointer, - options: toolOptions + radius : toolOptions.size, + pointers : this._brush.pointers, + options : toolOptions }); } storeBrushPointer( x, y ) { - this._brush.pointer = { x: x - this._bounds.left, y: y - this._bounds.top }; + this._brush.down = true; + this._brush.pointers.push({ x: x - this._bounds.left, y: y - this._bounds.top }); } cacheEffects() { @@ -128,6 +129,10 @@ class LayerSprite extends sprite { this.setInteractive( this.layer === activeLayer ); } + resetSelection() { + this._selection = null; + } + handleActiveTool( tool, toolOptions, activeDocument ) { this.isDragging = false; this._isPaintMode = false; @@ -194,10 +199,11 @@ class LayerSprite extends sprite { this.preparePendingPaintState(); } + const { left, top } = this._bounds; // translate pointer to translated space, when layer is rotated or mirrored const { mirrorX, mirrorY, rotation } = this.layer.effects; - const rotCenterX = this._bounds.left + this._bounds.width / 2; - const rotCenterY = this._bounds.top + this._bounds.height / 2; + const rotCenterX = left + this._bounds.width / 2; + const rotCenterY = top + this._bounds.height / 2; if ( this.isRotated() ) { ({ x, y } = translatePointerRotation( x, y, rotCenterX, rotCenterY, rotation )); @@ -208,7 +214,7 @@ class LayerSprite extends sprite { const isFillMode = this._toolType === ToolTypes.FILL; // get the drawing context - const ctx = ( drawOnMask ? this.layer.mask : this.layer.source ).getContext( "2d" ); + let ctx = ( drawOnMask ? this.layer.mask : this.layer.source ).getContext( "2d" ); const { width, height } = ctx.canvas; ctx.save(); @@ -228,11 +234,11 @@ class LayerSprite extends sprite { y -= this.layer.y; // if there is an active selection, painting will be constrained within - - if ( this._selection ) { - let selectionPoints = this._selection; - let sX = this._bounds.left; - let sY = this._bounds.top; + let selectionPoints = this._selection; + let sX, sY; + if ( selectionPoints ) { + sX = left; + sY = top; if ( this.isRotated() ) { selectionPoints = rotatePoints( selectionPoints, rotCenterX, rotCenterY, rotation ); const rect = getRectangleForSelection( selectionPoints ); @@ -242,13 +248,7 @@ class LayerSprite extends sprite { sX = 0;//pts.x; sY = 0;//pts.y; } - ctx.beginPath(); - selectionPoints.forEach(( point, index ) => { - ctx[ index === 0 ? "moveTo" : "lineTo" ]( point.x - sX, point.y - sY ); - }); - if ( !isFillMode ) { - ctx.clip(); - } + clipContextToSelection( ctx, selectionPoints, isFillMode, sX, sY ); } // transform destination context in case the current layer is rotated or mirrored @@ -257,7 +257,8 @@ class LayerSprite extends sprite { ctx.rotate( rotation ); ctx.translate( -x, -y ); - if ( isFillMode ) { + if ( isFillMode ) + { ctx.fillStyle = this.canvas?.store.getters.activeColor; if ( this._selection ) { ctx.fill(); @@ -265,22 +266,54 @@ class LayerSprite extends sprite { } else { ctx.fillRect( 0, 0, width, height ); } - } else { + } + else { // TODO: when rotated and mirrored, x and y are now in right coordinate space, but not at right point - const destination = { x, y }; + + // get the enqueued pointers which are to be rendered in this paint cycle + let { pointers, last } = this._brush; + pointers = JSON.parse( JSON.stringify( pointers.slice( pointers.length - ( pointers.length - last ) - 1 ))); if ( isCloneStamp ) { - renderClonedStroke( - ctx, this, - getSpriteForLayer({ id: this._toolOptions.sourceLayerId }), // TODO: fugly!! - this._brush, destination, - ); + if ( this._brush.down ) { + renderClonedStroke( + ctx, this._brush, this, getSpriteForLayer({ id: this._toolOptions.sourceLayerId }), pointers + ); + // clone operation is direct-to-Layer-source + this.setBitmap( ctx.canvas ); + } } else { - renderBrushStroke( this, this._brush, ctx, destination ); + // brush operations are done on a lower resolution canvas during live update + // upon release, this will be rendered to the Layer source (see handleRelease()) + let overrides = null; + if ( this._brush.down ) { + // live update on lower resolution canvas + this.tempCanvas = this.tempCanvas || createCanvas( + this.canvas.getWidth(), this.canvas.getHeight() + ); + overrides = { + scale : 1 / this.canvas.documentScale, + zoom : this.canvas.zoomFactor, + x : left, + y : top, + pointers, + }; + ctx.restore(); // restore previous context before switching to temp context + ctx = this.tempCanvas.ctx; + + if ( selectionPoints && this.tempCanvas ) { + clipContextToSelection( ctx, selectionPoints, isFillMode, sX - left, sY - top, overrides.scale ); + } + } + renderBrushStroke( ctx, this._brush, this, overrides ); } } ctx.restore(); - this.resetFilterAndRecache(); + + // when brushing, defer recache of filters to handleRelease() + if ( !this._brush.down ) { + this.resetFilterAndRecache(); + } } /** @@ -423,20 +456,36 @@ class LayerSprite extends sprite { } } - // brush tool active (either draws/erases onto IMAGE_GRAPHIC layer source or on the mask bitmap) - if ( !!this._brush.pointer ) { - this.paint( x, y ); - this.storeBrushPointer( x, y ); // update so next stroke starts from current position + // brush mode and brushing is active + if ( this._brush.down ) { + // enqueue current pointer position, painting of all enqueued pointers will be deferred + // to the update()-hook, this prevents multiple renders on each move event + this.storeBrushPointer( x, y ); } } handleRelease( x, y ) { - this._brush.pointer = null; + if ( this._brush.down ) { + // brushing was active, deactivate brushing and render + // high resolution version of the brushed path onto the Layer source + this.tempCanvas = null; + this._brush.down = false; + this._brush.last = 0; + this.paint( x, y ); + this._brush.pointers = []; // pointers have been rendered, reset + } if ( this._isPaintMode ) { this.forceMoveListener(); // keeps the move listener active } } + update() { + if ( this._brush.down ) { + this.paint( this._pointerX, this._pointerY ); + this._brush.last = this._brush.pointers.length; + } + } + draw( documentContext, viewport ) { const scaleDocument = this.isScaled(); @@ -457,6 +506,16 @@ class LayerSprite extends sprite { // invoke base class behaviour to render bitmap super.draw( documentContext, viewport ); + // sprite is currently brushing, render low resolution temp contents onto screen + if ( this.tempCanvas ) { + const { cvs } = this.tempCanvas; + const scale = this.canvas.documentScale; + documentContext.drawImage( + cvs, 0, 0, cvs.width, cvs.height, + -viewport.left, -viewport.top, cvs.width * scale, cvs.height * scale + ); + } + // render brush outline at pointer position if ( this._isPaintMode ) { const drawBrushOutline = this._toolType !== ToolTypes.CLONE || !!this._toolOptions.coords; @@ -470,7 +529,7 @@ class LayerSprite extends sprite { ty = ( coords.y - viewport.top ) + ( this._pointerY - relSource.y ); } // when no source coordinate is set, or when applying the clone stamp, we show a cross to mark the origin - if ( !coords || this._brush.pointer ) { + if ( !coords || this._brush.down ) { renderCross( documentContext, tx, ty, this._brush.radius / this.canvas.zoomFactor ); } } @@ -531,6 +590,17 @@ function scaleViewport( viewport, scale ) { return scaled; } +function clipContextToSelection( ctx, selectionPoints, isFillMode, offsetX, offsetY, scale = 1 ) { + ctx.save(); + ctx.beginPath(); + selectionPoints.forEach(( point, index ) => { + ctx[ index === 0 ? "moveTo" : "lineTo" ]( ( point.x - offsetX ) * scale, ( point.y - offsetY ) * scale ); + }); + if ( !isFillMode ) { + ctx.clip(); + } +} + // NOTE we use getSpriteForLayer() instead of passing the Sprite by reference // as it is possible the Sprite originally rendering the Layer has been disposed // and a new one has been created while traversing the change history diff --git a/src/factories/brush-factory.js b/src/factories/brush-factory.js index 564bc2c..53693c5 100644 --- a/src/factories/brush-factory.js +++ b/src/factories/brush-factory.js @@ -21,7 +21,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ const BrushFactory = { - create({ radius = 10, color = "rgba(255,0,0,1)", pointer = null, options = {} } = {} ) { + create({ radius = 10, color = "rgba(255,0,0,1)", pointers = [], options = {} } = {} ) { const [r, g, b, a] = color.split( "," ); const colors = [ color, @@ -31,17 +31,18 @@ const BrushFactory = { return { radius, colors, - pointer, + pointers, options, // provided by tool-module halfRadius : radius * 0.5, - doubleRadius : radius * 2 + doubleRadius : radius * 2, + down : false }; } }; export default BrushFactory; -export const createDrawable = ( brush, ctx, x, y ) => { - const gradient = ctx.createRadialGradient( x, y, brush.halfRadius, x, y, brush.radius ); +export const createDrawable = ( brush, ctx, x, y, scale = 1 ) => { + const gradient = ctx.createRadialGradient( x, y, brush.halfRadius * scale, x, y, brush.radius * scale ); gradient.addColorStop( 0, brush.colors[ 0 ]); gradient.addColorStop( 0.5, brush.colors[ 1 ]); diff --git a/src/store/index.js b/src/store/index.js index ae5d46f..6bcd1f9 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -197,8 +197,9 @@ export default { commit( "showNotification", { message: translate( "selectionCopied" ) }); dispatch( "clearSelection" ); }, - clearSelection() { + clearSelection({ getters }) { getCanvasInstance()?.interactionPane.resetSelection(); + getSpriteForLayer( getters.activeLayer )?.resetSelection(); }, pasteSelection({ commit, getters, dispatch, state }) { const selection = state.selectionContent; diff --git a/src/styles/form.scss b/src/styles/form.scss index ed1c377..76d2855 100644 --- a/src/styles/form.scss +++ b/src/styles/form.scss @@ -10,10 +10,9 @@ .select, .vue-slider { display: inline-block; - width: 50%; + width: 50% !important; } .vue-slider { - width: 50% !important; vertical-align: middle; } input, textarea { diff --git a/src/utils/render-util.js b/src/utils/render-util.js index 6131953..2f4e33b 100644 --- a/src/utils/render-util.js +++ b/src/utils/render-util.js @@ -43,110 +43,150 @@ export const renderCross = ( ctx, x, y, size ) => { ctx.restore(); }; -export const renderBrushStroke = ( sprite, brush, ctx, destinationPoint ) => { - const { pointer, radius, halfRadius, doubleRadius, options } = brush; +/** + * Render a series of registered pointer offset into a single brush stroke + * @param {CanvasRenderingContext2D} ctx to render on + * @param {Object} brush properties + * @param {zCanvas.sprite} sprite defining relative (on-screen) Layer coordinates + * @param {Object=} optional override to use (defines alternate pointers and coordinate scaling) + */ +export const renderBrushStroke = ( ctx, brush, sprite, optOverride ) => { + let { pointers, radius, halfRadius, doubleRadius, options } = brush; + let scale = 1; const { type } = options; - // effects complicate proceedings https://github.com/igorski/bitmappery/issues/2 - const { rotation, mirrorX, mirrorY } = sprite.layer.effects; - if ( rotation || mirrorX || mirrorY ) { - ctx.fillStyle = createDrawable( brush, ctx, destinationPoint.x, destinationPoint.y ) - ctx.fillRect( destinationPoint.x - radius, destinationPoint.y - radius, doubleRadius, doubleRadius ); + if ( optOverride ) { + pointers = optOverride.pointers; + scale = optOverride.zoom; + radius *= scale; + halfRadius *= scale; + doubleRadius *= scale; + } + + if ( pointers.length < 2 ) { return; } ctx.save(); ctx.lineJoin = ctx.lineCap = "round"; - // paint brush types + for ( let i = 1; i < pointers.length; ++i ) { + const isFirst = i === 1; + const prevPoint = pointers[ i - 1 ]; + const point = pointers[ i ]; - if ( type === BrushTypes.PAINT_BRUSH ) { - const dist = distanceBetween( pointer, destinationPoint ); - const angle = angleBetween( pointer, destinationPoint ); - - const incr = brush.radius * 0.25; - const sin = Math.sin( angle ); - const cos = Math.cos( angle ); - - let x, y, size, doubleSize; - for ( let i = 0; i < dist; i += incr ) { - x = pointer.x + ( sin * i ); - y = pointer.y + ( cos * i ); - ctx.fillStyle = createDrawable( brush, ctx, x, y ); - ctx.fillRect( x - radius, y - radius, doubleRadius, doubleRadius ); + if ( optOverride ) { + if ( isFirst ) { + prevPoint.x = ( prevPoint.x + optOverride.x ) * optOverride.scale; + prevPoint.y = ( prevPoint.y + optOverride.y ) * optOverride.scale; + } + point.x = ( point.x + optOverride.x ) * optOverride.scale; + point.y = ( point.y + optOverride.y ) * optOverride.scale; } - return ctx.restore(); - } - if ( type === BrushTypes.SPRAY ) { - ctx.fillStyle = brush.colors[ 0 ]; - for ( let i = doubleRadius; i--; ) { - const angle = randomInRange( 0, TWO_PI ); - const size = randomInRange( 1, 3 ); - ctx.fillRect( - destinationPoint.x + randomInRange( -halfRadius, halfRadius ) * cos( angle ), - destinationPoint.y + randomInRange( -halfRadius, halfRadius ) * sin( angle ), - size, size - ); + // paint brush types + + if ( type === BrushTypes.PAINT_BRUSH ) { + const dist = distanceBetween( prevPoint, point ); + const angle = angleBetween( prevPoint, point ); + + const incr = radius * 0.25; + const sin = Math.sin( angle ); + const cos = Math.cos( angle ); + + let x, y, size, doubleSize; + for ( let j = 0; j < dist; j += incr ) { + x = prevPoint.x + ( sin * j ); + y = prevPoint.y + ( cos * j ); + ctx.fillStyle = createDrawable( brush, ctx, x, y, scale ); + ctx.fillRect( x - radius, y - radius, doubleRadius, doubleRadius ); + } + continue; } - return ctx.restore(); - } - // line types + if ( type === BrushTypes.SPRAY ) { + ctx.fillStyle = brush.colors[ 0 ]; + let j = doubleRadius; + while ( j-- > 0 ) { + const angle = randomInRange( 0, TWO_PI ); + const size = randomInRange( 1, 3 ); + ctx.fillRect( + point.x + randomInRange( -halfRadius, halfRadius ) * cos( angle ), + point.y + randomInRange( -halfRadius, halfRadius ) * sin( angle ), + size, size + ); + } + continue; + } - ctx.lineWidth = brush.radius; - ctx.strokeStyle = brush.colors[ 0 ]; + // line types - if ( type === BrushTypes.LINE ) { - ctx.beginPath(); - ctx.moveTo( pointer.x, pointer.y ); - ctx.lineTo( destinationPoint.x, destinationPoint.y ); - ctx.stroke(); - return ctx.restore(); - } + ctx.strokeStyle = brush.colors[ 0 ]; - if ( type === BrushTypes.CALLIGRAPHIC ) { - ctx.beginPath(); - - const min = ( brush.radius * 0.25 ) * 0.66666; - const max = ( brush.radius * 0.25 ) * 1.33333; - - [ -max, -min, 0, min, max ].forEach( offset => { - ctx.moveTo( pointer.x + offset, pointer.y + offset ); - ctx.lineTo( destinationPoint.x + offset, destinationPoint.y + offset ); + if ( type === BrushTypes.LINE ) { + if ( isFirst ) { + ctx.lineWidth = radius; + ctx.beginPath(); + ctx.moveTo( prevPoint.x, prevPoint.y ); + } + ctx.lineTo( point.x, point.y ); ctx.stroke(); - }); - return ctx.restore(); - } - - // multi stroke line types - - let dX = 0; - let dY = 0; - - for ( let i = 0; i < options.strokes; ++i ) { - switch ( type ) { - default: - case BrushTypes.PEN: - ctx.beginPath(); - ctx.lineWidth = ( brush.radius * 0.2 ) * randomInRange( 0.5, 1 ); - ctx.moveTo( pointer.x - dX, pointer.y - dY ); - ctx.lineTo( destinationPoint.x - dX, destinationPoint.y - dY ); - ctx.stroke(); - break; - - // TODO: this one benefits from working with a large point queue - case BrushTypes.CURVED_PEN: - ctx.beginPath(); - ctx.moveTo( pointer.x, pointer.y ); - const midPoint = pointBetween( pointer, destinationPoint ); - ctx.quadraticCurveTo( pointer.x, pointer.y, midPoint.x, midPoint.y ); - ctx.lineTo( destinationPoint.x, destinationPoint.y ); - ctx.stroke(); - break; + continue; + } + + if ( type === BrushTypes.CALLIGRAPHIC ) { + if ( isFirst ) { + ctx.lineWidth = halfRadius; + ctx.beginPath(); + } + const min = ( radius * 0.25 ) * 0.66666; + const max = ( radius * 0.25 ) * 1.33333; + + [ -max, -min, 0, min, max ].forEach( offset => { + ctx.moveTo( prevPoint.x + offset, prevPoint.y + offset ); + ctx.lineTo( point.x + offset, point.y + offset ); + ctx.stroke(); + }); + continue; + } + + // this one benefits from working with a large point queue + + if ( type === BrushTypes.CURVED_PEN ) { + if ( isFirst ) { + ctx.lineWidth = radius; + ctx.beginPath(); + ctx.moveTo( prevPoint.x, prevPoint.y ); + } + const midPoint = pointBetween( prevPoint, point ); + ctx.quadraticCurveTo( prevPoint.x, prevPoint.y, midPoint.x, midPoint.y ); + if ( i === pointers.length - 1 ) { + ctx.lineTo( point.x, point.y ); + ctx.stroke(); + } + continue; + } + + // multi stroke line type + + let dX = 0 + let dY = 0; + for ( let j = 0; j < options.strokes; ++j ) { + switch ( type ) { + default: + case BrushTypes.PEN: + if ( isFirst && j === 0 ) { + ctx.beginPath(); + ctx.lineWidth = ( radius * 0.2 ) * randomInRange( 0.5, 1 ); + } + ctx.moveTo( prevPoint.x - dX, prevPoint.y - dY ); + ctx.lineTo( point.x - dX, point.y - dY ); + ctx.stroke(); + break; + } + dX += randomInRange( 0, ctx.lineWidth ); + dY += randomInRange( 0, ctx.lineWidth ); } - dX += randomInRange( 0, ctx.lineWidth ); - dY += randomInRange( 0, ctx.lineWidth ); } ctx.restore(); }; @@ -155,47 +195,59 @@ export const renderBrushStroke = ( sprite, brush, ctx, destinationPoint ) => { * Masks the contents of given source using given brushCvs, and renders the result onto given destContext. * Used by clone stamp tool. * - * @param {CanvasRenderingContext2D} destContext - * @param {zCanvas.sprite} sprite - * @param {zCanvas.sprite} sourceSprite containing the bitmap to mask (see getBitmap()) + * @param {CanvasRenderingContext2D} destContext context to render on * @param {Object} brush operation to use - * @param {{ x: number, y:number }} destinationPoint coordinate relative to given destContext + * @param {zCanvas.sprite} sprite containg the relative (on-screen) Layer coordinates + * @param {zCanvas.sprite} sourceSprite containing the bitmap to mask (see getBitmap()) + * @param {Array<{{ x: Number, y: Number }}>=} optPointers optional Array of alternative coordinates */ -export const renderClonedStroke = ( destContext, sprite, sourceSprite, brush, destinationPoint ) => { - const maskRadius = brush.radius; - +export const renderClonedStroke = ( destContext, brush, sprite, sourceSprite, optPointers ) => { + if ( !sourceSprite ) { + return; + } const { coords, opacity } = sprite._toolOptions; - const source = sourceSprite.getBitmap(); - const relSource = sprite._cloneStartCoords ?? sprite._dragStartEventCoordinates; + const { radius, doubleRadius, options } = brush; + const { type } = options; + const pointers = optPointers || brush.pointers; - const sourceX = ( coords.x - sourceSprite.getX()) - maskRadius; - const sourceY = ( coords.y - sourceSprite.getY()) - maskRadius; - const xDelta = sprite._dragStartOffset.x + (( destinationPoint.x - sprite._bounds.left ) - relSource.x ); - const yDelta = sprite._dragStartOffset.y + (( destinationPoint.y - sprite._bounds.top ) - relSource.y ); + const source = sourceSprite.getBitmap(); + const sourceX = ( coords.x - sourceSprite.getX()) - radius; + const sourceY = ( coords.y - sourceSprite.getY()) - radius; + + const relSource = sprite._cloneStartCoords || sprite._dragStartEventCoordinates; // prepare temporary canvas (match size with brush) const { cvs, ctx } = tempCanvas; - cvs.width = brush.doubleRadius; - cvs.height = brush.doubleRadius; + cvs.width = doubleRadius; + cvs.height = doubleRadius; - ctx.globalAlpha = opacity; + for ( let i = 0; i < pointers.length; ++i ) { + const destinationPoint = pointers[ i ]; - // draw source bitmap data onto temporary canvas - ctx.drawImage( - source, sourceX + xDelta, sourceY + yDelta, maskRadius, maskRadius, - 0, 0, maskRadius, maskRadius - ); + const xDelta = sprite._dragStartOffset.x + (( destinationPoint.x - sprite._bounds.left ) - relSource.x ); + const yDelta = sprite._dragStartOffset.y + (( destinationPoint.y - sprite._bounds.top ) - relSource.y ); - // draw the brush above the bitmap, keeping only the overlapping area - ctx.globalCompositeOperation = "destination-in"; - // note we use the brush.pointer as the destination too - renderBrushStroke( sprite, brush, ctx, brush.pointer ); + // draw source bitmap data onto temporary canvas + ctx.globalCompositeOperation = "source-over"; + ctx.globalAlpha = opacity; - // draw the masked result onto the destination canvas - destContext.drawImage( - cvs, 0, 0, maskRadius, maskRadius, - destinationPoint.x - maskRadius, destinationPoint.y - maskRadius, maskRadius, maskRadius - ); + ctx.clearRect( 0, 0, cvs.width, cvs.height ); + ctx.drawImage( + source, sourceX + xDelta, sourceY + yDelta, radius, radius, 0, 0, radius, radius + ); + + // draw the brush above the bitmap, keeping only the overlapping area + ctx.globalCompositeOperation = "destination-in"; + + ctx.fillStyle = createDrawable( brush, ctx, 0, 0 ); + ctx.fillRect( 0, 0, radius, radius );//point.x - radius, point.y - radius, doubleRadius, doubleRadius ); + + // draw the masked result onto the destination canvas + destContext.drawImage( + cvs, 0, 0, radius, radius, + destinationPoint.x - radius, destinationPoint.y - radius, radius, radius + ); + } }; export const resizeLayerContent = async ( layer, ratioX, ratioY ) => { diff --git a/tests/unit/factories/brush-factory.spec.js b/tests/unit/factories/brush-factory.spec.js index ffe618b..51131a3 100644 --- a/tests/unit/factories/brush-factory.spec.js +++ b/tests/unit/factories/brush-factory.spec.js @@ -12,7 +12,8 @@ describe( "Brush factory", () => { "rgba(255,0,0,0)" ], options: {}, - pointer: null, + pointers: [], + down: false, }); }); @@ -20,7 +21,7 @@ describe( "Brush factory", () => { expect( BrushFactory.create({ radius: 20, color: "rgba(128,123,686,.75)", - pointer: { x: 10, y: 20 }, + pointers: [{ x: 10, y: 20 }], options: { size: 10 } })).toEqual({ radius: 20, @@ -31,8 +32,9 @@ describe( "Brush factory", () => { "rgba(128,123,686,0.375)", "rgba(128,123,686,0)" ], - pointer: { x: 10, y: 20 }, - options: { size: 10 } + pointers: [{ x: 10, y: 20 }], + options: { size: 10 }, + down: false, }); }); });