mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Implement action queue for deferred paint rendering, use low resolution paint preview during paint
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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( [] );
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user