Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tools/pen): add option to smooth line #28

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 76 additions & 21 deletions src/tools/pen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { DrawLineCommand } from '../commands'
import DrawingContext from '../drawing_context'
import Layering from '../core/layering'

const TENSION_TO_SMOOTHNESS_RATION = 1 / 100
const THRESHOLD_TO_SMOOTHNESS_RATIO = 10 / 1

export class PenTool extends Tool {
private line?: Konva.Line
private layering!: Layering
Expand All @@ -18,6 +21,8 @@ export class PenTool extends Tool {
// line following the user's mouse movement.
private mode: 'free' | 'line' | 'none' = 'none'

private smoothness = 0

activate(context: DrawingContext): void {
super.activate(context)
this.layering = context.layering
Expand All @@ -31,11 +36,7 @@ export class PenTool extends Tool {
onMouseClick(_: KonvaEventObject<MouseEvent>): boolean {
if (this.mode === 'none') {
const { x, y } = this.context.getRelativePointerPosition()
this.line = new Konva.Line({
...this.context.overlayProperties,
globalCompositeOperation: 'source-over',
points: [x, y, x, y],
})
this.line = this.startLine(x, y, x, y)
this.layering.overlay.add(this.line).draw()

this.mode = 'line'
Expand All @@ -52,11 +53,7 @@ export class PenTool extends Tool {
if (this.line || this.mode === 'line') return false

const { x, y } = this.context.getRelativePointerPosition()
this.line = new Konva.Line({
...this.context.overlayProperties,
globalCompositeOperation: 'source-over',
points: [x, y],
})
this.line = this.startLine(x, y)
this.layering.overlay.add(this.line).draw()

this.mode = 'free'
Expand Down Expand Up @@ -88,30 +85,88 @@ export class PenTool extends Tool {
return true
}

onKeyDown(event: KeyboardEvent): boolean {
if (event.code === 'Escape' && this.mode === 'line') {
this.endLine()
return true
} else if (event.code === 'KeyS') {
this.smoothness = this.smoothness === 0 ? 50 : 0
return true
}
return false
}

private startLine(...points: number[]): Konva.Line {
return new Konva.Line({
...this.context.overlayProperties,
globalCompositeOperation: 'source-over',
points: points,
})
}

private endLine(): void {
const line = this.line!

if (this.mode === 'line') {
line.points().splice(-2, 2)
}

line.remove()
this.layering.overlay.draw()

if (line.points().length > 2) {
line.setAttrs({ ...this.context.properties })
const command = new DrawLineCommand(this.layering.currentLayer, line)
this.context.executor.execute(command)
}
this.executeCommand()

this.line = undefined
this.mode = 'none'
}

onKeyDown(event: KeyboardEvent): boolean {
if (event.key === 'Escape' && this.mode === 'line') {
this.endLine()
return true
private executeCommand(): void {
const line = this.line!
if (line.points().length <= 2) return

const extra: { [key: string]: unknown } = {}

if (this.smoothness !== 0) {
// smoothing using tension works nicely with line mode, but in free mode using high values of tension
// gives unwanted results.
if (this.mode === 'free') {
const threshold = this.smoothness * THRESHOLD_TO_SMOOTHNESS_RATIO
line.points(collapsePoints(line.points(), threshold))
extra['bezier'] = true

if (line.points().length < 8) {
// bezier lines need at least 4 points, otherwise its an empty line
return
}
} else {
extra['tension'] = this.smoothness * TENSION_TO_SMOOTHNESS_RATION
}
}
return false

line.setAttrs({ ...this.context.properties, ...extra })
const command = new DrawLineCommand(this.layering.currentLayer, line)
this.context.executor.execute(command)
}
}

const collapsePoints = (points: number[], threshold: number): number[] => {
if (points.length <= 4) {
return points
}

const ret = points.slice(0, 4)
let [lastX, lastY] = [points[0], points[1]]

for (let i = 2; i < points.length - 2; i += 2) {
const [x, y] = [points[i], points[i + 1]]
const distance = (x - lastX) ** 2 + (y - lastY) ** 2

if (distance > threshold) {
ret.push(x, y)
lastX = x
lastY = y
}
}

ret.push(...points.slice(-2))
return ret
}