mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Implement continuous brushing (#6)
Implementation of continuous brushing added
This commit is contained in:
@@ -145,7 +145,6 @@ further benchmarking and tweaking.
|
||||
|
||||
* Implement layer sorting and opacity
|
||||
* Layer source and mask must not be stored as Vue observables
|
||||
* Implement action queue when drawing, only execute drawing on zCanvas.sprite.update()-hook
|
||||
* Maintain cache for source images at the display destination size (invalidate on window resize / zoom), this prevents processing large images that are never displayed at their full scale
|
||||
* Dragging of masks on rotated/mirror content is kinda broken
|
||||
* Restored base64 images should be treated as binary once more (see layer-factory)
|
||||
|
||||
@@ -83,9 +83,9 @@ export default {
|
||||
"cloneOptions",
|
||||
]),
|
||||
selectableLayers() {
|
||||
return this.layers.filter(({ visible }) => visible )
|
||||
.map( layer => ({ text: layer.name, value: layer.id }))
|
||||
.reverse();
|
||||
return this.layers?.filter(({ visible }) => visible )
|
||||
.map( layer => ({ text: layer.name, value: layer.id }))
|
||||
.reverse();
|
||||
},
|
||||
sourceLayer: {
|
||||
get() {
|
||||
|
||||
@@ -25,7 +25,8 @@ import { sprite } from "zcanvas";
|
||||
import { isInsideTransparentArea } from "@/utils/canvas-util";
|
||||
import { enqueueState } from "@/factories/history-state-factory";
|
||||
import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory";
|
||||
import { isPointInRange, rectangleToCoordinates } from "@/math/image-math";
|
||||
import { isPointInRange } from "@/math/point-math";
|
||||
import { rectangleToCoordinates } from "@/math/image-math";
|
||||
import { isSelectionClosed } from "@/math/selection-math";
|
||||
import ToolTypes from "@/definitions/tool-types";
|
||||
import LayerSprite from "@/components/ui/zcanvas/layer-sprite";
|
||||
|
||||
@@ -23,12 +23,14 @@
|
||||
import Vue from "vue";
|
||||
import { sprite } from "zcanvas"
|
||||
import { createCanvas, cloneCanvas, resizeImage, globalToLocal } from "@/utils/canvas-util";
|
||||
import { renderCross, renderMasked } from "@/utils/render-util";
|
||||
import { renderCross, renderBrushStroke, renderClonedStroke } from "@/utils/render-util";
|
||||
import { LAYER_GRAPHIC, LAYER_MASK, LAYER_TEXT } from "@/definitions/layer-types";
|
||||
import { translatePointerRotation, rotatePoints, scaleRectangle } from "@/math/image-math";
|
||||
import { scaleRectangle } from "@/math/image-math";
|
||||
import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math";
|
||||
import { rotatePoints, translatePointerRotation } from "@/math/point-math";
|
||||
import { renderEffectsForLayer } from "@/services/render-service";
|
||||
import { flushLayerCache, clearCacheProperty } from "@/services/caches/bitmap-cache";
|
||||
import BrushFactory from "@/factories/brush-factory";
|
||||
import { getSpriteForLayer } from "@/factories/sprite-factory";
|
||||
import { enqueueState } from "@/factories/history-state-factory";
|
||||
import ToolTypes, { canDrawOnSelection } from "@/definitions/tool-types";
|
||||
@@ -51,13 +53,11 @@ class LayerSprite extends sprite {
|
||||
layer.source = cvs;
|
||||
}
|
||||
|
||||
// create brush (used for both drawing on LAYER_GRAPHIC types and to create layer masks)
|
||||
const brushCanvas = createCanvas();
|
||||
this._brushCvs = brushCanvas.cvs;
|
||||
this._brushCtx = brushCanvas.ctx;
|
||||
this._halfRadius = 0;
|
||||
this._pointerX = 0;
|
||||
this._pointerY = 0;
|
||||
this._pointerX = 0;
|
||||
this._pointerY = 0;
|
||||
|
||||
// brush properties (used for both drawing on LAYER_GRAPHIC types and to create layer masks)
|
||||
this._brush = BrushFactory.create();
|
||||
|
||||
this.setActionTarget();
|
||||
|
||||
@@ -91,41 +91,12 @@ class LayerSprite extends sprite {
|
||||
return this.layer.effects.scale !== 1;
|
||||
}
|
||||
|
||||
cacheBrush( color, radius = 30 ) {
|
||||
this._radius = radius;
|
||||
cacheBrush( color = "rgba(255,0,0,1)", radius = 30 ) {
|
||||
this._brush = BrushFactory.create({ color, radius, pointer: this._brush.pointer });
|
||||
}
|
||||
|
||||
const innerRadius = radius / 10;
|
||||
const outerRadius = radius * 2;
|
||||
|
||||
const x = radius;
|
||||
const y = radius;
|
||||
|
||||
// update brush Canvas size
|
||||
this._brushCvs.width = outerRadius;
|
||||
this._brushCvs.height = outerRadius;
|
||||
|
||||
if ( !this._brushCtx ) {
|
||||
return; // TODO: this is because in Jenkins on Linux there is no canvas mock available
|
||||
}
|
||||
|
||||
const gradient = this._brushCtx.createRadialGradient( x, y, innerRadius, x, y, outerRadius );
|
||||
gradient.addColorStop( 0, color );
|
||||
|
||||
let color2 = "rgba(255,255,255,0)";
|
||||
/*
|
||||
if ( color.startsWith( "rgba" )) {
|
||||
const [r, g, b, a] = color.split( "," );
|
||||
color2 = `${r},${g},${b},0)`;
|
||||
}
|
||||
*/
|
||||
gradient.addColorStop( 1, color2 );
|
||||
|
||||
this._brushCtx.clearRect( 0, 0, this._brushCvs.width, this._brushCvs.height );
|
||||
this._brushCtx.arc( x, y, radius, 0, 2 * Math.PI );
|
||||
this._brushCtx.fillStyle = gradient;
|
||||
this._brushCtx.fill();
|
||||
|
||||
this._halfRadius = radius / 2;
|
||||
storeBrushPointer( x, y ) {
|
||||
this._brush.pointer = { x: x - this._bounds.left, y: y - this._bounds.top };
|
||||
}
|
||||
|
||||
cacheEffects() {
|
||||
@@ -290,17 +261,17 @@ class LayerSprite extends sprite {
|
||||
ctx.fillRect( 0, 0, width, height );
|
||||
}
|
||||
} else {
|
||||
|
||||
// TODO: when rotated and mirrored, x and y are now in right coordinate space, but not at right point
|
||||
const destination = { x, y };
|
||||
|
||||
if ( isCloneStamp ) {
|
||||
renderMasked(
|
||||
ctx, this, x, y,
|
||||
renderClonedStroke(
|
||||
ctx, this,
|
||||
getSpriteForLayer({ id: this._toolOptions.sourceLayerId }), // TODO: fugly!!
|
||||
this._brushCvs, this._radius
|
||||
this._brush, destination,
|
||||
);
|
||||
} else {
|
||||
ctx.drawImage( this._brushCvs, x - this._radius, y - this._radius );
|
||||
renderBrushStroke( this, this._brush, ctx, destination );
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
@@ -416,7 +387,7 @@ class LayerSprite extends sprite {
|
||||
return;
|
||||
}
|
||||
// for any other brush mode state, set the brush application to true (will be applied in handleMove())
|
||||
this._applyPaint = true;
|
||||
this.storeBrushPointer( x, y );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,13 +414,14 @@ class LayerSprite extends sprite {
|
||||
}
|
||||
|
||||
// brush tool active (either draws/erases onto IMAGE_GRAPHIC layer source or on the mask bitmap)
|
||||
if ( this._applyPaint ) {
|
||||
if ( !!this._brush.pointer ) {
|
||||
this.paint( x, y );
|
||||
this.storeBrushPointer( x, y ); // update so next stroke starts from current position
|
||||
}
|
||||
}
|
||||
|
||||
handleRelease( x, y ) {
|
||||
this._applyPaint = false;
|
||||
this._brush.pointer = null;
|
||||
if ( this._isPaintMode ) {
|
||||
this.forceMoveListener(); // keeps the move listener active
|
||||
}
|
||||
@@ -488,8 +460,8 @@ 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._applyPaint ) {
|
||||
renderCross( documentContext, tx, ty, this._radius / this.canvas.zoomFactor );
|
||||
if ( !coords || this._brush.pointer ) {
|
||||
renderCross( documentContext, tx, ty, this._brush.radius / this.canvas.zoomFactor );
|
||||
}
|
||||
}
|
||||
documentContext.save();
|
||||
@@ -497,7 +469,7 @@ class LayerSprite extends sprite {
|
||||
|
||||
if ( drawBrushOutline ) {
|
||||
// any other brush mode state shows brush outline
|
||||
documentContext.arc( this._pointerX - viewport.left, this._pointerY - viewport.top, this._radius, 0, 2 * Math.PI );
|
||||
documentContext.arc( this._pointerX - viewport.left, this._pointerY - viewport.top, this._brush.radius, 0, 2 * Math.PI );
|
||||
}
|
||||
documentContext.stroke();
|
||||
documentContext.restore();
|
||||
@@ -536,7 +508,6 @@ class LayerSprite extends sprite {
|
||||
|
||||
this._bitmap = null;
|
||||
this._bitmapReady = false;
|
||||
this._brushCvs = null;
|
||||
}
|
||||
}
|
||||
export default LayerSprite;
|
||||
|
||||
50
src/factories/brush-factory.js
Normal file
50
src/factories/brush-factory.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const BrushFactory = {
|
||||
create({ radius = 10, color = "rgba(255,0,0,1)", pointer = null } = {} ) {
|
||||
const [r, g, b, a] = color.split( "," );
|
||||
const colors = [
|
||||
color,
|
||||
`${r},${g},${b},${parseFloat(a) * 0.5})`,
|
||||
`${r},${g},${b},0)`
|
||||
];
|
||||
return {
|
||||
radius,
|
||||
colors,
|
||||
pointer,
|
||||
halfRadius : radius * 0.5,
|
||||
doubleRadius : radius * 2
|
||||
};
|
||||
}
|
||||
};
|
||||
export default BrushFactory;
|
||||
|
||||
export const createDrawable = ( brush, ctx, x, y ) => {
|
||||
const gradient = ctx.createRadialGradient( x, y, brush.halfRadius, x, y, brush.radius );
|
||||
|
||||
gradient.addColorStop( 0, brush.colors[ 0 ]);
|
||||
gradient.addColorStop( 0.5, brush.colors[ 1 ]);
|
||||
gradient.addColorStop( 1, brush.colors[ 2 ]);
|
||||
|
||||
return gradient;
|
||||
};
|
||||
@@ -82,28 +82,6 @@ export const isPortrait = ( width, height ) => width < height;
|
||||
export const isLandscape = ( width, height ) => width > height;
|
||||
export const isSquare = ( width, height ) => width === height;
|
||||
|
||||
export const isPointInRange = ( point1x, point1y, point2x, point2y, margin = 5 ) => {
|
||||
const left = point2x - margin;
|
||||
const right = point2x + margin;
|
||||
const top = point2y - margin;
|
||||
const bottom = point2y + margin;
|
||||
|
||||
return point1x >= left && point1x <= right && point1y >= top && point1y <= bottom;
|
||||
};
|
||||
|
||||
export const translatePointerRotation = ( x, y, rotationCenterX, rotationCenterY, angleInRadians ) => {
|
||||
const x2 = x - rotationCenterX;
|
||||
const y2 = y - rotationCenterY;
|
||||
|
||||
const cos = Math.cos( -angleInRadians );
|
||||
const sin = Math.sin( -angleInRadians );
|
||||
|
||||
return {
|
||||
x : x2 * cos - y2 * sin + rotationCenterX,
|
||||
y : x2 * sin + y2 * cos + rotationCenterY
|
||||
};
|
||||
};
|
||||
|
||||
export const getRotationCenter = ({ left, top, width, height }, rounded = false ) => {
|
||||
const x = left + width * .5;
|
||||
const y = top + height * .5;
|
||||
@@ -157,19 +135,6 @@ export const rectangleToCoordinates = ( x, y, width, height ) => [
|
||||
{ x, y }, // back to TL to close selection
|
||||
];
|
||||
|
||||
export const translatePoints = ( coordinateList, xTranslation = 0, yTranslation = 0 ) => {
|
||||
return coordinateList.map(({ x, y }) => ({
|
||||
x: x + xTranslation,
|
||||
y: y + yTranslation,
|
||||
}));
|
||||
};
|
||||
|
||||
export const rotatePoints = ( coordinateList, rotationCenterX, rotationCenterY, angleInRadians ) => {
|
||||
return coordinateList.map(({ x, y }) => {
|
||||
return translatePointerRotation( x, y, rotationCenterX, rotationCenterY, angleInRadians );
|
||||
});
|
||||
};
|
||||
|
||||
export const scaleRectangle = ({ left, top, width, height }, scale = 1 ) => {
|
||||
const scaledWidth = width * scale;
|
||||
const scaledHeight = height * scale;
|
||||
|
||||
79
src/math/line-math.js
Normal file
79
src/math/line-math.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020-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.
|
||||
*/
|
||||
export const calcPathSpline = ( nodes, node, splinePts, resolution = 32 ) => {
|
||||
const prev = node - 1;
|
||||
const next = node + 1;
|
||||
const nextNext = node + 2;
|
||||
|
||||
const maxRes = resolution - 1;
|
||||
|
||||
for ( let n = 0; n < resolution; ++n ) {
|
||||
const pt = {};
|
||||
const p = n / maxRes;
|
||||
|
||||
splinePts.push({
|
||||
x : nspline(
|
||||
p,
|
||||
nodes[ prev ].x,
|
||||
nodes[ node ].x,
|
||||
nodes[ next ].x,
|
||||
nodes[ nextNext ]?.x
|
||||
),
|
||||
y : nspline(
|
||||
p,
|
||||
nodes[ prev ].y,
|
||||
nodes[ node ].y,
|
||||
nodes[ next ].y,
|
||||
nodes[ nextNext ]?.y
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* internal methods */
|
||||
|
||||
const CR00 = -0.5;
|
||||
const CR01 = 1.5;
|
||||
const CR02 = -1.5;
|
||||
const CR03 = 0.5;
|
||||
const CR10 = 1.0;
|
||||
const CR11 = -2.5;
|
||||
const CR12 = 2.0;
|
||||
const CR13 = -0.5;
|
||||
const CR20 = -0.5;
|
||||
const CR21 = 0.0;
|
||||
const CR22 = 0.5;
|
||||
const CR23 = 0.0;
|
||||
const CR30 = 0.0;
|
||||
const CR31 = 1.0;
|
||||
const CR32 = 0.0;
|
||||
const CR33 = 0.0;
|
||||
|
||||
function nspline( x, f0, f1, f2, f3 ) {
|
||||
const c3 = CR00 * f0 + CR01 * f1 + CR02 * f2 + CR03 * f3;
|
||||
const c2 = CR10 * f0 + CR11 * f1 + CR12 * f2 + CR13 * f3;
|
||||
const c1 = CR20 * f0 + CR21 * f1 + CR22 * f2 + CR23 * f3;
|
||||
const c0 = CR30 * f0 + CR31 * f1 + CR32 * f2 + CR33 * f3;
|
||||
|
||||
return (( c3 * x + c2 ) * x + c1 ) * x + c0;
|
||||
}
|
||||
66
src/math/point-math.js
Normal file
66
src/math/point-math.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2020-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.
|
||||
*/
|
||||
const { pow, sqrt, atan2 } = Math;
|
||||
|
||||
export const distanceBetween = ( point1, point2 ) => {
|
||||
return sqrt( pow( point2.x - point1.x, 2 ) + pow( point2.y - point1.y, 2 ));
|
||||
};
|
||||
|
||||
export const angleBetween = ( point1, point2 ) => {
|
||||
return atan2( point2.x - point1.x, point2.y - point1.y );
|
||||
};
|
||||
|
||||
export const isPointInRange = ( point1x, point1y, point2x, point2y, margin = 5 ) => {
|
||||
const left = point2x - margin;
|
||||
const right = point2x + margin;
|
||||
const top = point2y - margin;
|
||||
const bottom = point2y + margin;
|
||||
|
||||
return point1x >= left && point1x <= right && point1y >= top && point1y <= bottom;
|
||||
};
|
||||
|
||||
export const translatePointerRotation = ( x, y, rotationCenterX, rotationCenterY, angleInRadians ) => {
|
||||
const x2 = x - rotationCenterX;
|
||||
const y2 = y - rotationCenterY;
|
||||
|
||||
const cos = Math.cos( -angleInRadians );
|
||||
const sin = Math.sin( -angleInRadians );
|
||||
|
||||
return {
|
||||
x : x2 * cos - y2 * sin + rotationCenterX,
|
||||
y : x2 * sin + y2 * cos + rotationCenterY
|
||||
};
|
||||
};
|
||||
|
||||
export const translatePoints = ( coordinateList, xTranslation = 0, yTranslation = 0 ) => {
|
||||
return coordinateList.map(({ x, y }) => ({
|
||||
x: x + xTranslation,
|
||||
y: y + yTranslation,
|
||||
}));
|
||||
};
|
||||
|
||||
export const rotatePoints = ( coordinateList, rotationCenterX, rotationCenterY, angleInRadians ) => {
|
||||
return coordinateList.map(({ x, y }) => {
|
||||
return translatePointerRotation( x, y, rotationCenterX, rotationCenterY, angleInRadians );
|
||||
});
|
||||
};
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
CREATE_DOCUMENT, ADD_LAYER, EXPORT_DOCUMENT, DROPBOX_FILE_SELECTOR, SAVE_DROPBOX_DOCUMENT
|
||||
} from "@/definitions/modal-windows";
|
||||
import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory";
|
||||
import { translatePoints } from "@/math/image-math";
|
||||
import { translatePoints } from "@/math/point-math";
|
||||
|
||||
let state, getters, commit, dispatch, listener,
|
||||
suspended = false, blockDefaults = true, optionDown = false, shiftDown = false;
|
||||
@@ -138,7 +138,7 @@ function handleKeyDown( event ) {
|
||||
switch ( keyCode )
|
||||
{
|
||||
case 8: // backspace
|
||||
if ( getters.activeDocument?.selection.length && getters.activeLayer ) {
|
||||
if ( getters.activeDocument?.selection?.length && getters.activeLayer ) {
|
||||
dispatch( "deleteInSelection" );
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -20,8 +20,10 @@
|
||||
* 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 { createCanvas, resizeImage } from "@/utils/canvas-util";
|
||||
import { LAYER_TEXT } from "@/definitions/layer-types";
|
||||
import { createDrawable } from "@/factories/brush-factory";
|
||||
import { distanceBetween, angleBetween, translatePointerRotation } from "@/math/point-math";
|
||||
import { createCanvas, resizeImage } from "@/utils/canvas-util";
|
||||
|
||||
const tempCanvas = createCanvas();
|
||||
|
||||
@@ -36,32 +38,63 @@ export const renderCross = ( ctx, x, y, size ) => {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
export const renderBrushStroke = ( sprite, brush, ctx, destinationPoint ) => {
|
||||
const { pointer, radius, doubleRadius } = brush;
|
||||
|
||||
// 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 );
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.lineJoin = ctx.lineCap = "round";
|
||||
|
||||
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;
|
||||
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 );
|
||||
}
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {number} destX destination x-coordinate relative to given destContext
|
||||
* @param {number} destY destination y-coordinate relative to given destContext
|
||||
* @param {zCanvas.sprite} sourceSprite containing the bitmap to mask (see getBitmap())
|
||||
* @param {HTMLCanvasElement} brushCvs mask image to use
|
||||
* @param {number} maskRadius radius of the mask (determines mask size)
|
||||
* @param {Object} brush operation to use
|
||||
* @param {{ x: number, y:number }} destinationPoint coordinate relative to given destContext
|
||||
*/
|
||||
export const renderMasked = ( destContext, sprite, destX, destY, sourceSprite, brushCvs, maskRadius ) => {
|
||||
export const renderClonedStroke = ( destContext, sprite, sourceSprite, brush, destinationPoint ) => {
|
||||
const maskRadius = brush.radius;
|
||||
|
||||
const { coords, opacity } = sprite._toolOptions;
|
||||
const source = sourceSprite.getBitmap();
|
||||
const relSource = sprite._cloneStartCoords ?? sprite._dragStartEventCoordinates;
|
||||
|
||||
const sourceX = ( coords.x - sourceSprite.getX()) - maskRadius;
|
||||
const sourceY = ( coords.y - sourceSprite.getY()) - maskRadius;
|
||||
const xDelta = sprite._dragStartOffset.x + (( destX - sprite._bounds.left ) - relSource.x );
|
||||
const yDelta = sprite._dragStartOffset.y + (( destY - sprite._bounds.top ) - relSource.y );
|
||||
const xDelta = sprite._dragStartOffset.x + (( destinationPoint.x - sprite._bounds.left ) - relSource.x );
|
||||
const yDelta = sprite._dragStartOffset.y + (( destinationPoint.y - sprite._bounds.top ) - relSource.y );
|
||||
|
||||
// prepare temporary canvas (match size with brush)
|
||||
const { cvs, ctx } = tempCanvas;
|
||||
cvs.width = brushCvs.width;
|
||||
cvs.height = brushCvs.height;
|
||||
cvs.width = brush.doubleRadius;
|
||||
cvs.height = brush.doubleRadius;
|
||||
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
@@ -73,12 +106,13 @@ export const renderMasked = ( destContext, sprite, destX, destY, sourceSprite, b
|
||||
|
||||
// draw the brush above the bitmap, keeping only the overlapping area
|
||||
ctx.globalCompositeOperation = "destination-in";
|
||||
ctx.drawImage( brushCvs, 0, 0 );
|
||||
// note we use the brush.pointer as the destination too
|
||||
renderBrushStroke( sprite, brush, ctx, brush.pointer );
|
||||
|
||||
// draw the masked result onto the destination canvas
|
||||
destContext.drawImage(
|
||||
cvs, 0, 0, maskRadius, maskRadius,
|
||||
destX - maskRadius, destY - maskRadius, maskRadius, maskRadius
|
||||
destinationPoint.x - maskRadius, destinationPoint.y - maskRadius, maskRadius, maskRadius
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
35
tests/unit/factories/brush-factory.spec.js
Normal file
35
tests/unit/factories/brush-factory.spec.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import BrushFactory from "@/factories/brush-factory";
|
||||
|
||||
describe( "Brush factory", () => {
|
||||
it( "should be able to create a default brush when no arguments are given", () => {
|
||||
expect( BrushFactory.create()).toEqual({
|
||||
radius: 10,
|
||||
halfRadius: 5,
|
||||
doubleRadius: 20,
|
||||
colors: [
|
||||
"rgba(255,0,0,1)",
|
||||
"rgba(255,0,0,0.5)",
|
||||
"rgba(255,0,0,0)"
|
||||
],
|
||||
pointer: null,
|
||||
});
|
||||
});
|
||||
|
||||
it( "should be able to create a brush from given arguments", () => {
|
||||
expect( BrushFactory.create({
|
||||
radius: 20,
|
||||
color: "rgba(128,123,686,.75)",
|
||||
pointer: { x: 10, y: 20 }
|
||||
})).toEqual({
|
||||
radius: 20,
|
||||
halfRadius: 10,
|
||||
doubleRadius: 40,
|
||||
colors: [
|
||||
"rgba(128,123,686,.75)",
|
||||
"rgba(128,123,686,0.375)",
|
||||
"rgba(128,123,686,0)"
|
||||
],
|
||||
pointer: { x: 10, y: 20 }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
fastRound, scaleToRatio, constrain, isPortrait, isLandscape, isSquare, translatePoints, scaleRectangle
|
||||
fastRound, scaleToRatio, constrain, isPortrait, isLandscape, isSquare, scaleRectangle
|
||||
} from "@/math/image-math";
|
||||
|
||||
describe( "Image math utilities", () => {
|
||||
@@ -59,15 +59,6 @@ describe( "Image math utilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it( "should be able to translate the coordinates within a list", () => {
|
||||
const list = [
|
||||
{ x: 10, y: 10 }, { x: 15, y: 15 }
|
||||
];
|
||||
expect( translatePoints( list, 5, -5 )).toEqual([
|
||||
{ x: 15, y: 5 }, { x: 20, y: 10 }
|
||||
]);
|
||||
});
|
||||
|
||||
describe( "when scaling rectangles", () => {
|
||||
it( "should be able to scale a rectangle and maintain its relative offset when scaling up", () => {
|
||||
const rectangle = {
|
||||
|
||||
12
tests/unit/math/point-math.spec.js
Normal file
12
tests/unit/math/point-math.spec.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { translatePoints } from "@/math/point-math";
|
||||
|
||||
describe( "Point math", () => {
|
||||
it( "should be able to translate the coordinates within a list", () => {
|
||||
const list = [
|
||||
{ x: 10, y: 10 }, { x: 15, y: 15 }
|
||||
];
|
||||
expect( translatePoints( list, 5, -5 )).toEqual([
|
||||
{ x: 15, y: 5 }, { x: 20, y: 10 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user