diff --git a/src/rendering/canvas-elements/layer-sprite.js b/src/rendering/canvas-elements/layer-sprite.js index 7d5d4b9..fa2cd34 100644 --- a/src/rendering/canvas-elements/layer-sprite.js +++ b/src/rendering/canvas-elements/layer-sprite.js @@ -23,7 +23,7 @@ import Vue from "vue"; import { sprite } from "zcanvas" import { createCanvas, canvasToBlob, resizeImage, globalToLocal } from "@/utils/canvas-util"; -import { renderCross, renderBrushStroke } from "@/utils/render-util"; +import { renderCross } from "@/utils/render-util"; import { LAYER_GRAPHIC, LAYER_MASK, LAYER_TEXT } from "@/definitions/layer-types"; import { scaleRectangle } from "@/math/image-math"; import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math"; @@ -31,6 +31,7 @@ import { rotatePoints, translatePointerRotation } from "@/math/point-math"; import { renderEffectsForLayer } from "@/services/render-service"; import { clipContextToSelection } from "@/rendering/clipping"; import { renderClonedStroke } from "@/rendering/cloning"; +import { renderBrushStroke } from "@/rendering/drawing"; import { flushLayerCache, clearCacheProperty } from "@/rendering/cache/bitmap-cache"; import { getTempCanvas, renderTempCanvas, disposeTempCanvas, translatePointers, createOverrideConfig diff --git a/src/rendering/drawing.js b/src/rendering/drawing.js new file mode 100644 index 0000000..c93f215 --- /dev/null +++ b/src/rendering/drawing.js @@ -0,0 +1,216 @@ +/** + * 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 BrushTypes from "@/definitions/brush-types"; +import { createDrawable } from "@/factories/brush-factory"; +import { randomInRange } from "@/math/unit-math"; +import { applyOverrideConfig } from "@/rendering/lowres"; +import { distanceBetween, angleBetween, pointBetween } from "@/math/point-math"; + +const { cos, sin } = Math; +const TWO_PI = Math.PI * 2; + +/** + * Render a series of registered pointer offset into a single brush stroke + * Adapted from studies provided by Juriy Zaytsev + * + * @param {CanvasRenderingContext2D} ctx to render on + * @param {Object} brush properties + * @param {zCanvas.sprite} sprite defining relative (on-screen) Layer coordinates + * @param {Object=} overrideConfig optional override to use (defines alternate pointers and coordinate scaling) + */ +export const renderBrushStroke = ( ctx, brush, sprite, overrideConfig ) => { + let { pointers, radius, halfRadius, doubleRadius, options } = brush; + let scale = 1; + const { type } = options; + + if ( overrideConfig ) { + pointers = overrideConfig.pointers; + scale = overrideConfig.zoom; + radius *= scale; + halfRadius *= scale; + doubleRadius *= scale; + + applyOverrideConfig( overrideConfig, pointers ); + } + + if ( pointers.length < 2 ) { + return; + } + + ctx.save(); + ctx.lineJoin = ctx.lineCap = "round"; + + for ( let i = 1; i < pointers.length; ++i ) { + const isFirst = i === 1; + const prevPoint = pointers[ i - 1 ]; + const point = pointers[ i ]; + + // 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; + } + + 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; + } + + // line types + + ctx.strokeStyle = brush.colors[ 0 ]; + + 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(); + 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 + // as such when supplying overrideConfig for live rendering, regular line is drawn instead + + if ( type === BrushTypes.CONNECTED ) { + if ( isFirst ) { + ctx.lineWidth = halfRadius * 0.25; + ctx.beginPath(); + ctx.moveTo( prevPoint.x, prevPoint.y ); + } + ctx.lineTo( point.x, point.y ); + const nearPoint = pointers[ i - 5 ]; + if ( nearPoint ) { + ctx.moveTo( nearPoint.x, nearPoint.y ); + ctx.lineTo( point.x, point.y ); + } + ctx.stroke(); + continue; + } + + if ( type === BrushTypes.NEAREST ) { + const lastPoint = pointers[ pointers.length - 1 ]; + + if ( isFirst ) { + const penultimate = pointers[ pointers.length - 2 ]; + ctx.lineWidth = halfRadius; + ctx.beginPath(); + ctx.moveTo( penultimate.x, penultimate.y ); + ctx.lineTo( lastPoint.x, lastPoint.y ); + ctx.stroke(); + } + const dx = point.x - lastPoint.x; + const dy = point.y - lastPoint.y; + const d = dx * dx + dy * dy; + + // when distance of line is in range of last point, connect to it + if ( d < 1000 ) { + const xOffset = dx * 0.2; + const yOffset = dy * 0.2; + ctx.beginPath(); + ctx.strokeStyle = brush.colors[ 1 ]; + ctx.moveTo( lastPoint.x + xOffset, lastPoint.y + yOffset ); + ctx.lineTo( point.x - xOffset, point.y - yOffset ); + ctx.stroke(); + } + continue; + } + + // multi stroke line type + + let dX = 0 + let dY = 0; + const { smooth } = options; + 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 ); + if ( smooth ) { + ctx.moveTo( prevPoint.x - dX, prevPoint.y - dY ); + } + } + if ( smooth ) { + const midPoint = pointBetween( prevPoint, point ); + ctx.quadraticCurveTo( prevPoint.x - dX, prevPoint.y - dY, midPoint.x - dX, midPoint.y - dY ); + } else { + ctx.moveTo( prevPoint.x - dX, prevPoint.y - dY ); + ctx.lineTo( point.x - dX, point.y - dY ); + ctx.stroke(); + } + if ( smooth && i === pointers.length - 1 ) { + ctx.lineTo( point.x - dX, point.y - dY ); + ctx.stroke(); + } + break; + } + dX += randomInRange( 0, ctx.lineWidth ); + dY += randomInRange( 0, ctx.lineWidth ); + } + } + ctx.restore(); +}; diff --git a/src/utils/render-util.js b/src/utils/render-util.js index c2b297d..263ccaa 100644 --- a/src/utils/render-util.js +++ b/src/utils/render-util.js @@ -21,16 +21,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { LAYER_TEXT } from "@/definitions/layer-types"; -import BrushTypes from "@/definitions/brush-types"; -import { createDrawable } from "@/factories/brush-factory"; -import { distanceBetween, angleBetween, pointBetween, translatePointerRotation } from "@/math/point-math"; -import { randomInRange } from "@/math/unit-math"; -import { applyOverrideConfig } from "@/rendering/lowres"; import { resizeImage } from "@/utils/canvas-util"; -const { cos, sin } = Math; -const TWO_PI = Math.PI * 2; - export const renderCross = ( ctx, x, y, size ) => { ctx.save(); ctx.beginPath(); @@ -42,192 +34,6 @@ export const renderCross = ( ctx, x, y, size ) => { ctx.restore(); }; -/** - * Render a series of registered pointer offset into a single brush stroke - * Adapted from studies provided by Juriy Zaytsev - * - * @param {CanvasRenderingContext2D} ctx to render on - * @param {Object} brush properties - * @param {zCanvas.sprite} sprite defining relative (on-screen) Layer coordinates - * @param {Object=} overrideConfig optional override to use (defines alternate pointers and coordinate scaling) - */ -export const renderBrushStroke = ( ctx, brush, sprite, overrideConfig ) => { - let { pointers, radius, halfRadius, doubleRadius, options } = brush; - let scale = 1; - const { type } = options; - - if ( overrideConfig ) { - pointers = overrideConfig.pointers; - scale = overrideConfig.zoom; - radius *= scale; - halfRadius *= scale; - doubleRadius *= scale; - - applyOverrideConfig( overrideConfig, pointers ); - } - - if ( pointers.length < 2 ) { - return; - } - - ctx.save(); - ctx.lineJoin = ctx.lineCap = "round"; - - for ( let i = 1; i < pointers.length; ++i ) { - const isFirst = i === 1; - const prevPoint = pointers[ i - 1 ]; - const point = pointers[ i ]; - - // 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; - } - - 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; - } - - // line types - - ctx.strokeStyle = brush.colors[ 0 ]; - - 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(); - 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 - // as such when supplying overrideConfig for live rendering, regular line is drawn instead - - if ( type === BrushTypes.CONNECTED ) { - if ( isFirst ) { - ctx.lineWidth = halfRadius * 0.25; - ctx.beginPath(); - ctx.moveTo( prevPoint.x, prevPoint.y ); - } - ctx.lineTo( point.x, point.y ); - const nearPoint = pointers[ i - 5 ]; - if ( nearPoint ) { - ctx.moveTo( nearPoint.x, nearPoint.y ); - ctx.lineTo( point.x, point.y ); - } - ctx.stroke(); - continue; - } - - if ( type === BrushTypes.NEAREST ) { - const lastPoint = pointers[ pointers.length - 1 ]; - - if ( isFirst ) { - const penultimate = pointers[ pointers.length - 2 ]; - ctx.lineWidth = halfRadius; - ctx.beginPath(); - ctx.moveTo( penultimate.x, penultimate.y ); - ctx.lineTo( lastPoint.x, lastPoint.y ); - ctx.stroke(); - } - const dx = point.x - lastPoint.x; - const dy = point.y - lastPoint.y; - const d = dx * dx + dy * dy; - - // when distance of line is in range of last point, connect to it - if ( d < 1000 ) { - const xOffset = dx * 0.2; - const yOffset = dy * 0.2; - ctx.beginPath(); - ctx.strokeStyle = brush.colors[ 1 ]; - ctx.moveTo( lastPoint.x + xOffset, lastPoint.y + yOffset ); - ctx.lineTo( point.x - xOffset, point.y - yOffset ); - ctx.stroke(); - } - continue; - } - - // multi stroke line type - - let dX = 0 - let dY = 0; - const { smooth } = options; - 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 ); - if ( smooth ) { - ctx.moveTo( prevPoint.x - dX, prevPoint.y - dY ); - } - } - if ( smooth ) { - const midPoint = pointBetween( prevPoint, point ); - ctx.quadraticCurveTo( prevPoint.x - dX, prevPoint.y - dY, midPoint.x - dX, midPoint.y - dY ); - } else { - ctx.moveTo( prevPoint.x - dX, prevPoint.y - dY ); - ctx.lineTo( point.x - dX, point.y - dY ); - ctx.stroke(); - } - if ( smooth && i === pointers.length - 1 ) { - ctx.lineTo( point.x - dX, point.y - dY ); - ctx.stroke(); - } - break; - } - dX += randomInRange( 0, ctx.lineWidth ); - dY += randomInRange( 0, ctx.lineWidth ); - } - } - ctx.restore(); -}; - export const resizeLayerContent = async ( layer, ratioX, ratioY ) => { const { source, mask } = layer;