code:
import {BB} from '../../bb/bb';
import {IRGB, IRGBA} from '../kl-types';
import {IBounds, IPressureInput} from '../../bb/bb-types';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {clamp} from '../../bb/math/math';
import {BezierLine, TBezierLineCallback} from '../../bb/math/line';

export interface IBlendBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'BlendBrush'];
    actions: THistoryInnerActions<BlendBrush>[];
}

const cellSize = 256;

interface IDrawBufferItem {
    x: number;
    y: number;
    size: number;
    opacity: number;
    x1: number;
    y1: number;
    x2: number;
    y2: number;
    r: number;
    g: number;
    b: number;
}

export class BlendBrush {

    private isTesting: boolean = false; // testing mode - context only gets updated when line is finished

    private context: CanvasRenderingContext2D = {} as CanvasRenderingContext2D;
    private color: IRGB = {} as IRGB;
    private size: number = 29; // radius - 0.5 - 99999
    private opacity: number = 0.6; // 0-1
    private blending: number = 0.65; // 0-1

    private settingLockLayerAlpha: boolean = false;
    private settingSizePressure: boolean = true;
    private settingOpacityPressure: boolean = false;

    private blendCol: IRGBA = {r: 0, g: 0, b: 0, a: 1}; // todo docs
    private blendMix: number = 0.45; // todo docs
    private mixCol: IRGB = {r: 0, g: 0, b: 0}; // todo docs
    private localColOld: IRGBA = {} as IRGBA; // todo docs

    private isDrawing: boolean = false;
    private lastInput: IPressureInput = {x: 0, y: 0, pressure: 0}; // todo docs
    private lastInput2: IPressureInput = {x: 0, y: 0, pressure: 0}; // todo docs
    private bezierLine: undefined | BezierLine;

    private history: KlHistoryInterface | undefined;
    private historyEntry: IBlendBrushHistoryEntry | undefined;
    private redrawBounds: IBounds | undefined;
    private cells: (ImageData | undefined)[] = [];
    private drawBuffer: IDrawBufferItem[] = [];

    private updateRedrawBounds (bounds: IBounds): void {
        this.redrawBounds = BB.updateBounds(this.redrawBounds, bounds);
    }

    private getCellsWidth (): number {
        return Math.ceil(this.context.canvas.width / cellSize);
    }

    /**
     * draw changed cells (changed by brushstroke) onto context
     * @private
     */
    private drawChangedCells (): void {
        if (!this.redrawBounds) {
            return;
        }

        const cells: typeof this.cells = this.cells.map(() => undefined);
        const touchedCells = this.getTouchedCells(this.redrawBounds);
        touchedCells.forEach((isTouched, index) => {
            if (isTouched) {
                cells[index] = this.cells[index];
            }
        });
        this.drawCells(cells);
        this.redrawBounds = undefined;
    }

    private getTouchedCells (bounds: IBounds): boolean[] {
        const touchedCells = this.cells.map(() => false);
        const cellsW = this.getCellsWidth();
        bounds = {
            x1: Math.floor(bounds.x1 / cellSize),
            y1: Math.floor(bounds.y1 / cellSize),
            x2: Math.floor(bounds.x2 / cellSize),
            y2: Math.floor(bounds.y2 / cellSize),
        };
        for (let i = bounds.x1; i <= bounds.x2; i++) {
            for (let e = bounds.y1; e <= bounds.y2; e++) {
                touchedCells[e * cellsW + i] = true;
            }
        }
        return touchedCells;
    }

    /**
     * Slice up bounds according to cells
     * @param bounds
     * @private
     */
    private sliceBounds (bounds: IBounds): {index: number; bounds: IBounds}[] {
        const cellsW = this.getCellsWidth();
        const result: {index: number; bounds: IBounds}[] = [];
        const touchedCells = this.getTouchedCells(bounds);

        touchedCells.forEach((cell, i) => {
            if (!cell) {
                return;
            }

            const cellOffsetX = (i % cellsW) * cellSize;
            const cellOffsetY = Math.floor(i / cellsW) * cellSize;
            const cellWidth = this.cells[i]!.width;
            const cellHeight = this.cells[i]!.height;

            const inCellBounds = {
                x1: Math.max(0, bounds.x1 - cellOffsetX),
                y1: Math.max(0, bounds.y1 - cellOffsetY),
                x2: Math.min(cellWidth - 1, bounds.x2 - cellOffsetX),
                y2: Math.min(cellHeight - 1, bounds.y2 - cellOffsetY),
            };
            if (inCellBounds.x1 > inCellBounds.x2 || inCellBounds.y1 > inCellBounds.y2) {
                return;
            }
            result.push({
                index: i,
                bounds: inCellBounds,
            });
        });

        return result;
    }

    /**
     * update copyImageData. copy over new regions if needed
     */
    private copyFromCanvas (bounds: IBounds | undefined): void {
        if (!bounds) {
            return;
        }

        const touchedCells = this.getTouchedCells(bounds);
        const cellsW = this.getCellsWidth();

        touchedCells.forEach((item, i) => {
            if (!item || this.cells[i]) {
                // not touched, or already copied
                return;
            }
            const x = i % cellsW;
            const y = Math.floor(i / cellsW);
            const w = (Math.min(x * cellSize + cellSize, this.context.canvas.width) - 1) % cellSize + 1;
            const h = (Math.min(y * cellSize + cellSize, this.context.canvas.height) - 1) % cellSize + 1;

            // temp canvas to prevent main canvas from getting slowed down in chrome
            const tmpCanvas = BB.canvas(w, h);
            const tmpCtx = BB.ctx(tmpCanvas);
            tmpCtx.drawImage(this.context.canvas, -x * cellSize, -y * cellSize);

            this.cells[i] = tmpCtx.getImageData(0, 0, w, h);
        });
    }

    private getAverage (x: number, y: number, size: number): IRGBA {

        size = Math.max(0.5, size * 0.75);
        const x1 = Math.max(0, Math.floor(x - size));
        const y1 = Math.max(0, Math.floor(y - size));
        const x2 = Math.min(this.context.canvas.width - 1, Math.ceil(x + size));
        const y2 = Math.min(this.context.canvas.height - 1, Math.ceil(y + size));
        if (x1 > x2 || y1 > y2) {
            return {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            };
        }

        let ar = 0, ag = 0, ab = 0, aa = 0, alpha;

        const slicedBounds = this.sliceBounds({ x1, y1, x2, y2 });
        slicedBounds.forEach(slice => {
            const width = this.cells[slice.index]!.width;
            const data = this.cells[slice.index]!.data;
            const bounds = slice.bounds;

            for (let i = bounds.y1; i <= bounds.y2; i += 4) {
                for (let e = bounds.x1, e2 = i * width * 4 + bounds.x1 * 4; e <= bounds.x2; e += 4, e2 += 4 * 4) {
                    alpha = data[e2 + 3];
                    ar += data[e2] * alpha;
                    ag += data[e2 + 1] * alpha;
                    ab += data[e2 + 2] * alpha;
                    aa += alpha;
                }
            }
        });

        if (aa !== 0) {
            ar /= aa;
            ag /= aa;
            ab /= aa;
            aa = Math.min(1, aa);
        }
        return {
            r: ar,
            g: ag,
            b: ab,
            a: aa,
        };
    }

    private prepDot (x: number, y: number, size: number): IBounds | undefined {
        size = Math.max(0.5, size);
        const x1 = Math.max(0, Math.floor(x - size));
        const y1 = Math.max(0, Math.floor(y - size));
        const x2 = Math.min(this.context.canvas.width - 1, Math.ceil(x + size));
        const y2 = Math.min(this.context.canvas.height - 1, Math.ceil(y + size));
        if (x1 > x2 || y1 > y2) {
            return undefined;
        }
        return { x1, y1, x2, y2 };
    }

    private drawDot (params: IDrawBufferItem): void {
        // array with random numbers. faster than Math.random()
        let randI = 0;
        const randLen = params.size > 30 ? 1024 : 512; // lower lengths lead to noticeable patterns
        const randArr: number[] = [];
        for (let i = 0; i < randLen; i++) {
            randArr[i] = (Math.random() - 0.5) / 1.001 + 0.5;
        }

        // thin lines take more than just 1 sample
        const sampleArr = [8,4,4,4,2,2,2,2,2,2]; // <0.5, 0.5, 1, 1.5, etc.
        const samples = sampleArr[Math.floor(params.size * 2)];
        const samplesSquared: number = samples ? samples * samples : 0;
        const sampleOffsets: number[] = [];
        if (samples) {
            let i = 0;
            for (let n = 0; n < samples; n++) {
                for (let m = 0; m < samples; m++, i += 2) {
                    sampleOffsets[i] = (n + 1) / samples; // x offset
                    sampleOffsets[i + 1] = (m + 1) / samples; // y offset
                }
            }
        }

        const sharpness = Math.pow(params.opacity, 2) * 0.8;
        // to optimize calculations
        const invSharpness = 1 - sharpness;
        const sharpnessSubtrahend = sharpness / invSharpness;
        const sizeSquared = params.size * params.size;
        const distDivisor = sizeSquared * invSharpness / params.opacity;
        const alphaMinuend = (1 + sharpnessSubtrahend) * params.opacity;

        const slicedBounds = this.sliceBounds({
            x1: params.x1,
            y1: params.y1,
            x2: params.x2,
            y2: params.y2,
        });

        const cellsW = this.getCellsWidth();
        slicedBounds.forEach(slice => {
            const cellOffsetX = (slice.index % cellsW) * cellSize;
            const cellOffsetY = Math.floor(slice.index / cellsW) * cellSize;
            const cellWidth = this.cells[slice.index]!.width;
            const data = this.cells[slice.index]!.data;

            // i - y index within cell
            // e - x index within cell

            // e2 - index in image data

            // ri - y index within image relative to dot-center
            // re - x index within image relative to dot-center

            for (let i = slice.bounds.y1, ri = i + cellOffsetY - params.y; i <= slice.bounds.y2; i++, ri++) {
                for (
                    let e = slice.bounds.x1,
                        e2 = i * cellWidth * 4 + slice.bounds.x1 * 4,
                        re = e + cellOffsetX - params.x;
                    e <= slice.bounds.x2;
                    e++,
                        e2 += 4,
                        re++
                ) {
                    // O = over -> brush-dot
                    // U = under -> image

                    let alphaO = 0;
                    if (samplesSquared) {
                        for (let f = 0; f < sampleOffsets.length; f += 2) {
                            const dist = BB.lenSquared(re + sampleOffsets[f], ri + sampleOffsets[f + 1]);
                            if (dist >= sizeSquared) {
                                continue;
                            }
                            alphaO += clamp(alphaMinuend - dist / distDivisor, 0, params.opacity);
                        }
                        if (!alphaO) {
                            continue;
                        }
                        alphaO /= samplesSquared;
                    } else {
                        // technically needs + 0.5 offset, but not noticeable with large brush
                        const dist = Math.pow(re, 2) + Math.pow(ri, 2);
                        if (dist >= sizeSquared) {
                            continue;
                        }
                        alphaO = clamp(alphaMinuend - dist / distDivisor, 0, params.opacity);
                    }

                    const invAlphaO = 1 - alphaO;
                    const alphaU =data[e2 + 3] / 255;

                    if (this.settingLockLayerAlpha) {
                        const underR = (params.r * alphaO) + ((data[e2]) * invAlphaO);
                        const underG = (params.g * alphaO) + ((data[e2 + 1]) * invAlphaO);
                        const underB = (params.b * alphaO) + ((data[e2 + 2]) * invAlphaO);
                        if (alphaU) {
                            data[e2] = Math.floor(underR + randArr[randI]);
                            data[e2 + 1] = Math.floor(underG + randArr[randI]);
                            data[e2 + 2] = Math.floor(underB + randArr[randI]);
                        }
                    } else {
                        const underR = (params.r * alphaO) + ((data[e2] * alphaU) * invAlphaO);
                        const underG = (params.g * alphaO) + ((data[e2 + 1] * alphaU) * invAlphaO);
                        const underB = (params.b * alphaO) + ((data[e2 + 2] * alphaU) * invAlphaO);

                        const newAlpha = 1 - invAlphaO * (1 - alphaU);
                        data[e2 + 3] = Math.floor(Math.min(255, newAlpha * 255) + 0.5);
                        if (newAlpha) {
                            data[e2] = Math.floor(underR / (newAlpha) + randArr[randI]);
                            data[e2 + 1] = Math.floor(underG / (newAlpha) + randArr[randI]);
                            data[e2 + 2] = Math.floor(underB / (newAlpha) + randArr[randI]);
                        }
                    }
                    randI = (randI + 1) % randLen;

                }
            }

        });
    }

    private calcSpacing (size: number): number {
        return BB.mix(
            (size * 2) / 2, // until size 5.3
            (size * 2) / 9, // at size 24
            clamp((size - 2.7) / (12 - 2.7), 0, 1)
        );
    }

    private continueLine (x: number | undefined, y: number | undefined, p: number, isCoalesced: boolean): void {

        this.drawBuffer = [];

        let localPressure;
        let localOpacity;
        let localSize = (this.settingSizePressure) ? Math.max(1, p * this.size) : Math.max(1, this.size);

        const bDist = this.calcSpacing(localSize);

        const avgX = (x === undefined) ? this.lastInput.x : x;
        const avgY = (y === undefined) ? this.lastInput.y : y;

        let localColNew: IRGBA;

        if (this.blending === 0) {
            this.mixCol.r = this.color.r;
            this.mixCol.g = this.color.g;
            this.mixCol.b = this.color.b;
        } else {
            let average;
            if (isCoalesced) {
                average = {r: this.localColOld.r, g: this.localColOld.g, b: this.localColOld.b, a: 0};
            } else {
                const avgParams = [avgX, avgY, ((this.settingSizePressure) ? Math.max(0.5, p * this.size) : Math.max(0.5, this.size))];
                const bounds = this.prepDot(avgParams[0], avgParams[1], avgParams[2]);
                if (bounds) {
                    this.copyFromCanvas(bounds);
                }
                average = this.getAverage(avgParams[0], avgParams[1], avgParams[2]);
            }
            localColNew = {r: 0, g: 0, b: 0, a: 0};

            if (average.a > 0 && this.blendCol.a === 0) {
                this.blendCol.r = average.r;
                this.blendCol.g = average.g;
                this.blendCol.b = average.b;
                this.blendCol.a = average.a;
                localColNew.r = this.blendCol.r;
                localColNew.g = this.blendCol.g;
                localColNew.b = this.blendCol.b;
                localColNew.a = this.blendCol.a;

            } else {
                if (average.a === 0) {
                    average.r = this.color.r;
                    average.g = this.color.g;
                    average.b = this.color.b;
                    average.a = 1 - this.blending;
                }

                this.blendCol.r = BB.mix(this.blendCol.r, BB.mix(this.blendCol.r, average.r, this.blendMix), average.a);
                this.blendCol.g = BB.mix(this.blendCol.g, BB.mix(this.blendCol.g, average.g, this.blendMix), average.a);
                this.blendCol.b = BB.mix(this.blendCol.b, BB.mix(this.blendCol.b, average.b, this.blendMix), average.a);
                this.blendCol.a = Math.min(1, this.blendCol.a + average.a);
                localColNew.r = this.blendCol.r;
                localColNew.g = this.blendCol.g;
                localColNew.b = this.blendCol.b;
                localColNew.a = this.blendCol.a;
            }

        }

        const bezierCallback: TBezierLineCallback  = (val) => {
            if (this.blending >= 1 && this.blendCol.a <= 0) {
                return;
            }
            const factor = val.t;
            localPressure = this.lastInput2.pressure * (1 - factor) + p * factor;
            localOpacity = (this.settingOpacityPressure) ? (this.opacity * localPressure * localPressure) : this.opacity;
            localSize = (this.settingSizePressure) ? Math.max(0.1, localPressure * this.size) : Math.max(0.1, this.size);
            if (this.blending != 0) {
                this.mixCol.r = BB.mix(this.localColOld.r, localColNew.r, factor);
                this.mixCol.g = BB.mix(this.localColOld.g, localColNew.g, factor);
                this.mixCol.b = BB.mix(this.localColOld.b, localColNew.b, factor);
            }
            if (this.blending === 1 && this.localColOld.a === 0) {
                this.mixCol.r = localColNew.r;
                this.mixCol.g = localColNew.g;
                this.mixCol.b = localColNew.b;
            }
            const bounds = this.prepDot(val.x, val.y, localSize);
            if (bounds) {
                this.updateRedrawBounds(bounds);
                this.drawBuffer.push({
                    x: val.x,
                    y: val.y,
                    size: localSize,
                    opacity: localOpacity,
                    x1: bounds.x1,
                    y1: bounds.y1,
                    x2: bounds.x2,
                    y2: bounds.y2,
                    r: BB.mix(this.color.r, this.mixCol.r, this.blending),
                    g: BB.mix(this.color.g, this.mixCol.g, this.blending),
                    b: BB.mix(this.color.b, this.mixCol.b, this.blending),
                });
            }
        };

        if (x === undefined || y === undefined) {
            this.bezierLine!.addFinal(bDist, bezierCallback);
        } else {
            this.bezierLine!.add(x, y, bDist, bezierCallback);
        }

        this.copyFromCanvas(this.redrawBounds);
        this.drawBuffer.forEach(item => {
            this.drawDot(item);
        });
        this.drawBuffer = [];

        this.localColOld = localColNew!;
    }

    // --- public ----
    constructor () {
    }

    setHistory (h: KlHistoryInterface): void {
        this.history = h;
    }

    getSize (): number {
        return this.size;
    }

    setSize (s: number): void {
        this.size = s;
    }

    getOpacity (): number {
        return this.opacity;
    }

    setOpacity (o: number): void {
        this.opacity = o;
    }

    getBlending (): number {
        return this.blending;
    }

    setBlending (b: number): void {
        this.blending = b;
    }

    setColor (c: IRGB): void {
        this.color = BB.copyObj(c);
    }

    setContext (c: CanvasRenderingContext2D): void {
        this.context = c;
    }

    setSizePressure (b: boolean): void {
        this.settingSizePressure = b;
    }

    setOpacityPressure (b: boolean): void {
        this.settingOpacityPressure = b;
    }

    getLockAlpha (): boolean {
        return this.settingLockLayerAlpha;
    }

    setLockAlpha (b: boolean): void {
        this.settingLockLayerAlpha = b;
    }

    getIsDrawing (): boolean {
        return this.isDrawing;
    }

    setIsTesting (b: boolean): void {
        this.isTesting = b;
    }

    startLine (x: number, y: number, p: number): void {
        this.historyEntry = {
            tool: ['brush', 'BlendBrush'],
            actions: [],
        };

        const totalCells = Math.ceil(this.context.canvas.width / cellSize) * Math.ceil(this.context.canvas.height / cellSize);
        this.cells = '0'.repeat(totalCells).split('').map(() => undefined);

        this.isDrawing = true;

        p = Math.max(0, Math.min(1, p));
        const localOpacity = (this.settingOpacityPressure) ? (this.opacity * p * p) : this.opacity;
        const localSize = (this.settingSizePressure) ? Math.max(0.1, p * this.size) : Math.max(0.1, this.size);
        if (this.blending === 0) {
            this.mixCol.r = this.color.r;
            this.mixCol.g = this.color.g;
            this.mixCol.b = this.color.b;
        } else {
            this.copyFromCanvas(this.prepDot(x, y, localSize));

            const average = this.getAverage(
                x,
                y,
                ((this.settingSizePressure) ? Math.max(0.1, p * this.size) : Math.max(0.1, this.size))
            );
            if (average.a === 0) {
                this.blendCol = {
                    r: this.color.r,
                    g: this.color.g,
                    b: this.color.b,
                    a: 1 - this.blending,
                };
            } else {
                this.blendCol = {
                    r: average.r,
                    g: average.g,
                    b: average.b,
                    a: average.a,
                };
            }

            this.mixCol.r = this.blendCol.r;
            this.mixCol.g = this.blendCol.g;
            this.mixCol.b = this.blendCol.b;
        }

        this.localColOld = {r: this.mixCol.r, g: this.mixCol.g, b: this.mixCol.b, a: this.blendCol.a};

        this.redrawBounds = undefined;
        this.drawBuffer = [];

        if (this.blending < 1 || this.blendCol.a > 0) {
            const bounds = this.prepDot(x, y, localSize);
            if (bounds) {
                this.updateRedrawBounds(bounds);
                this.drawBuffer.push({
                    x: x,
                    y: y,
                    size: localSize,
                    opacity: localOpacity,
                    x1: bounds.x1,
                    y1: bounds.y1,
                    x2: bounds.x2,
                    y2: bounds.y2,
                    r: BB.mix(this.color.r, this.mixCol.r, this.blending),
                    g: BB.mix(this.color.g, this.mixCol.g, this.blending),
                    b: BB.mix(this.color.b, this.mixCol.b, this.blending),
                });
            }
        }

        this.copyFromCanvas(this.redrawBounds);
        this.drawBuffer.forEach(item => {
            this.drawDot(item);
        });
        this.drawBuffer = [];

        this.bezierLine = new BB.BezierLine();
        this.bezierLine.add(x, y, 0, function () {
        });

        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;
        this.lastInput2 = BB.copyObj(this.lastInput);

        if (!this.isTesting) {
            this.drawChangedCells();
        }
    }

    goLine (x: number, y: number, p: number, isCoalesced: boolean): void {
        if (!this.isDrawing) {
            return;
        }
        this.continueLine(x, y, this.lastInput.pressure, isCoalesced);

        this.lastInput2 = BB.copyObj(this.lastInput);
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;

        if (!this.isTesting) {
            this.drawChangedCells();
        }
    }

    endLine (): void {
        if (this.bezierLine) {
            this.continueLine(undefined, undefined, this.lastInput.pressure, false);
        }

        this.isDrawing = false;
        this.bezierLine = undefined;

        this.drawChangedCells();

        if (this.historyEntry && this.history && this.cells.find(item => !!item)) {
            this.historyEntry.actions.push({
                action: 'drawCells',
                params: [this.cells],
            });
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }
        this.cells = [];
    }

    /**
     * draw cells onto context
     * @param cells
     */
    drawCells (cells: (ImageData | undefined)[]): void {
        const cellsW = this.getCellsWidth();
        cells.forEach((imageData, index) => {
            if (!imageData) {
                return;
            }
            const cellOffsetX = (index % cellsW) * cellSize;
            const cellOffsetY = Math.floor(index / cellsW) * cellSize;
            this.context.putImageData(imageData, cellOffsetX, cellOffsetY);
        });
    }

    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {
        this.lastInput.x = x2;
        this.lastInput.y = y2;

        if (this.isDrawing || x1 === undefined) {
            return;
        }

        const totalCells = Math.ceil(this.context.canvas.width / cellSize) * Math.ceil(this.context.canvas.height / cellSize);
        this.cells = '0'.repeat(totalCells).split('').map(() => undefined);
        this.redrawBounds = undefined;
        this.drawBuffer = [];

        this.copyFromCanvas(this.prepDot(x1, y1, Math.max(0.1, this.size)));
        const average = this.getAverage(x1, y1, Math.max(0.1, this.size));

        if (average.a === 0) {
            this.blendCol = {
                r: this.color.r,
                g: this.color.g,
                b: this.color.b,
                a: 1 - this.blending,
            };
        } else {
            this.blendCol = {
                r: average.r,
                g: average.g,
                b: average.b,
                a: average.a,
            };
        }

        this.mixCol.r = this.color.r * (1 - this.blendCol.a) + (this.blending * this.blendCol.r + this.color.r * (1 - this.blending)) * this.blendCol.a;
        this.mixCol.g = this.color.g * (1 - this.blendCol.a) + (this.blending * this.blendCol.g + this.color.g * (1 - this.blending)) * this.blendCol.a;
        this.mixCol.b = this.color.b * (1 - this.blendCol.a) + (this.blending * this.blendCol.b + this.color.b * (1 - this.blending)) * this.blendCol.a;

        const p = 1;
        const localOpacity = (this.settingOpacityPressure) ? (this.opacity * p * p) : this.opacity;
        const localSize = (this.settingSizePressure) ? Math.max(0.1, p * this.size) : Math.max(0.1, this.size);

        const mouseDist = Math.sqrt(Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0));
        const eX = (x2 - x1) / mouseDist;
        const eY = (y2 - y1) / mouseDist;
        const bDist = this.calcSpacing(localSize);
        for (let loopDist = 0; loopDist <= mouseDist; loopDist += bDist) {
            const bounds = this.prepDot(x1 + eX * loopDist, y1 + eY * loopDist, localSize);
            if (bounds) {
                this.copyFromCanvas(bounds);
                this.drawBuffer.push({
                    x: x1 + eX * loopDist,
                    y: y1 + eY * loopDist,
                    size: localSize,
                    opacity: localOpacity,
                    x1: bounds.x1,
                    y1: bounds.y1,
                    x2: bounds.x2,
                    y2: bounds.y2,
                    r: BB.mix(this.color.r, this.mixCol.r, this.blending),
                    g: BB.mix(this.color.g, this.mixCol.g, this.blending),
                    b: BB.mix(this.color.b, this.mixCol.b, this.blending),
                });
            }
        }
        this.drawBuffer.forEach(item => {
            this.drawDot(item);
        });
        this.drawBuffer = [];

        if (this.history && this.cells.find(item => !!item)) {
            this.drawCells(this.cells);
            this.historyEntry = {
                tool: ['brush', 'BlendBrush'],
                actions: [{
                    action: 'drawCells',
                    params: [this.cells],
                }],
            };
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }
        this.cells = [];
    }
}


code:
import {genBrushAlpha01, genBrushAlpha02} from './alphas/brush-alphas';

export const alphaImArr: HTMLCanvasElement[] = []; //used by default brush
alphaImArr[1] = genBrushAlpha01(128);
alphaImArr[2] = genBrushAlpha02(128);

repo_name: Doggermelon/klecks

license: mit


code:
import {PenBrush} from './pen-brush';
import {BlendBrush} from './blend-brush';
import {SketchyBrush} from './sketchy-brush';
import {PixelBrush} from './pixel-brush';
import {EraserBrush} from './eraser-brush';
import {SmudgeBrush} from './smudge-brush';
import {ChemyBrush} from './chemy-brush';

export type TBrush = |
    PenBrush |
    BlendBrush |
    SketchyBrush |
    PixelBrush |
    ChemyBrush |
    SmudgeBrush |
    EraserBrush;

export const brushes = {
    PenBrush,
    BlendBrush,
    SketchyBrush,
    PixelBrush,
    ChemyBrush,
    SmudgeBrush,
    EraserBrush,
};


code:
import {BB} from '../../bb/bb';
import {IRGB} from '../kl-types';
import {IBounds} from '../../bb/bb-types';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {ERASE_COLOR} from './erase-color';
import {KL} from '../kl';

export interface IChemyBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'ChemyBrush'];
    actions: THistoryInnerActions<ChemyBrush>[];
}

type TChemyMode = 'fill' | 'stroke';

export class ChemyBrush {

    private context: CanvasRenderingContext2D = {} as CanvasRenderingContext2D;
    private settingColor: IRGB = {} as IRGB;
    private settingSize: number = 0.25; // radius - 0.5 - 99999
    private settingOpacity: number = 1; // 0-1
    private settingLockLayerAlpha: boolean = false;
    private settingIsEraser: boolean = false;
    private settingMode: TChemyMode = 'fill';
    private settingDistort: number = 0; // 0 - 1
    private settingXSymmetry: boolean = false;
    private settingYSymmetry: boolean = false;
    private settingGradient: boolean = false;

    private isDrawing: boolean = false;

    private history: KlHistoryInterface = new KL.DecoyKlHistory();

    private copyCanvas: HTMLCanvasElement = {} as HTMLCanvasElement;
    private path: {x: number; y: number}[] = [];
    private minY: number = 0;
    private maxY: number = 0;
    private completeRedrawBounds: IBounds | undefined;

    private updateCompleteRedrawBounds (x: number, y: number): void {
        let bounds = { x1: x, y1: y, x2: x, y2: y};
        if (this.settingXSymmetry) {
            bounds = BB.updateBounds(
                bounds,
                {
                    x1: -x + this.copyCanvas.width,
                    y1: y,
                    x2: -x + this.copyCanvas.width,
                    y2: y,
                }
                );
        }
        if (this.settingYSymmetry) {
            bounds = BB.updateBounds(
                bounds,
                {
                    x1: x,
                    y1: -y + this.copyCanvas.height,
                    x2: x,
                    y2: -y + this.copyCanvas.height,
                }
            );
        }
        const buffer = this.settingMode === 'stroke' ? this.settingSize + 1 : 1;
        bounds.x1 = Math.floor(bounds.x1 - buffer);
        bounds.y1 = Math.floor(bounds.y1 - buffer);
        bounds.x2 = Math.ceil(bounds.x2 + buffer);
        bounds.y2 = Math.ceil(bounds.y2 + buffer);

        this.completeRedrawBounds = BB.updateBounds(this.completeRedrawBounds, bounds);
    }

    private drawShape (): void {
        this.context.save();
        this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height);
        this.context.drawImage(this.copyCanvas, 0, 0);

        const color = {...this.settingColor};
        if (this.settingIsEraser) {
            color.r = ERASE_COLOR;
            color.g = ERASE_COLOR;
            color.b = ERASE_COLOR;
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            } else {
                this.context.globalCompositeOperation = 'destination-out';
            }
        } else {
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            }
        }

        if (this.path.length > 1) {

            // path
            const path = new Path2D();
            this.path.forEach((item, index) => {
                if (index === 0) {
                    path.moveTo(item.x, item.y);
                } else {
                    path.lineTo(item.x, item.y);
                }
            });

            let style: string | CanvasGradient = BB.ColorConverter.toRgbaStr({
                r: color.r,
                g: color.g,
                b: color.b,
                a: this.settingOpacity,
            });
            if (this.settingGradient) {
                const startAtTop = this.path[0].x > this.path[this.path.length - 1].x;
                const gradient = this.context.createLinearGradient(
                    0, startAtTop ? this.minY : this.maxY,
                    0, startAtTop ? this.maxY : this.minY,
                );
                gradient.addColorStop(
                    0,
                    BB.ColorConverter.toRgbaStr({
                        r: color.r,
                        g: color.g,
                        b: color.b,
                        a: this.settingOpacity,
                    })
                );
                gradient.addColorStop(
                    1,
                    BB.ColorConverter.toRgbaStr({
                        r: color.r,
                        g: color.g,
                        b: color.b,
                        a: 0,
                    })
                );
                style = gradient;
            }

            // setup params
            if (this.settingMode === 'fill') {
                this.context.fillStyle = style;

            } else {
                this.context.lineWidth = this.settingSize * 2;
                this.context.lineJoin = 'bevel';
                this.context.strokeStyle = style;
            }

            // draw
            const draw = () => {
                if (this.settingMode === 'fill') {
                    this.context.fill(path);
                } else {
                    this.context.stroke(path);
                }
            };

            draw();
            if (this.settingXSymmetry) {
                this.context.save();
                this.context.translate(this.context.canvas.width / 2, 0);
                this.context.scale(-1, 1);
                this.context.translate(-this.context.canvas.width / 2, 0);
                draw();
                this.context.restore();
            }
            if (this.settingYSymmetry) {
                this.context.save();
                this.context.translate(0, this.context.canvas.height / 2);
                this.context.scale(1, -1);
                this.context.translate(0, -this.context.canvas.height / 2);
                draw();
                this.context.restore();
                if (this.settingXSymmetry) {
                    this.context.save();
                    this.context.translate(this.context.canvas.width / 2, this.context.canvas.height / 2);
                    this.context.scale(-1, -1);
                    this.context.translate(-this.context.canvas.width / 2, -this.context.canvas.height / 2);
                    draw();
                    this.context.restore();
                }
            }
        }

        this.context.restore();
    }

    // --- public ----
    constructor () {
    }

    setHistory (h: KlHistoryInterface): void {
        this.history = h;
    }

    getSize (): number {
        return this.settingMode === 'stroke' ? this.settingSize : 0;
    }

    setSize (s: number): void {
        this.settingSize = s;
    }

    getOpacity (): number {
        return this.settingOpacity;
    }

    setOpacity (o: number): void {
        this.settingOpacity = o;
    }

    setColor (c: IRGB): void {
        this.settingColor = BB.copyObj(c);
    }

    setContext (c: CanvasRenderingContext2D) {
        this.context = c;
    }

    setMode (mode: TChemyMode): void {
        this.settingMode = mode;
    }

    getMode (): TChemyMode {
        return this.settingMode;
    }

    setDistort (distort: number): void {
        this.settingDistort = BB.clamp(distort, 0, 1);
    }

    getDistort (): number {
        return this.settingDistort;
    }

    setXSymmetry (b: boolean): void {
        this.settingXSymmetry = b;
    }

    getXSymmetry (): boolean {
        return this.settingXSymmetry;
    }

    setYSymmetry (b: boolean): void {
        this.settingYSymmetry = b;
    }

    getYSymmetry (): boolean {
        return this.settingYSymmetry;
    }

    setGradient (b: boolean): void {
        this.settingGradient = b;
    }

    getGradient (): boolean {
        return this.settingGradient;
    }

    getLockAlpha (): boolean {
        return this.settingLockLayerAlpha;
    }

    setLockAlpha (b: boolean): void {
        this.settingLockLayerAlpha = b;
    }

    getIsEraser (): boolean {
        return this.settingIsEraser;
    }

    setIsEraser (b: boolean): void {
        this.settingIsEraser = b;
    }

    getIsDrawing (): boolean {
        return this.isDrawing;
    }

    startLine (x: number, y: number): void {
        this.isDrawing = true;
        this.path = [{x, y}];
        this.minY = y;
        this.maxY = y;
        this.copyCanvas = BB.canvas(this.context.canvas.width, this.context.canvas.height);
        BB.ctx(this.copyCanvas).drawImage(this.context.canvas, 0, 0);
        this.completeRedrawBounds = undefined;
        this.updateCompleteRedrawBounds(x, y);
    }

    goLine (x: number, y: number): void {
        if (!this.isDrawing) {
            return;
        }

        const pos = { x, y };
        if (this.settingDistort > 0) {
            pos.x += (Math.random() - 0.5) * this.settingDistort * 80;
            pos.y += (Math.random() - 0.5) * this.settingDistort * 80;
        }

        this.minY = Math.min(this.minY, pos.y);
        this.maxY = Math.max(this.maxY, pos.y);
        this.path.push(pos);
        this.updateCompleteRedrawBounds(x, y);
        this.drawShape();
    }

    endLine (): void {
        this.isDrawing = false;
        this.completeRedrawBounds = BB.boundsInArea(this.completeRedrawBounds, this.copyCanvas.width, this.copyCanvas.height);
        if (this.path.length > 1 && this.completeRedrawBounds) {
            const historyCanvas = BB.canvas(
                this.completeRedrawBounds.x2 - this.completeRedrawBounds.x1 + 1,
                this.completeRedrawBounds.y2 - this.completeRedrawBounds.y1 + 1
            );
            const historyCtx = BB.ctx(historyCanvas);
            historyCtx.drawImage(this.context.canvas, -this.completeRedrawBounds.x1, -this.completeRedrawBounds.y1);

            this.history.push({
                tool: ['brush', 'ChemyBrush'],
                actions: [
                    {
                        action: 'drawImage',
                        params: [
                            historyCanvas, // faster than getting image data (measured on 2018 lenovo chromebook)
                            this.completeRedrawBounds.x1,
                            this.completeRedrawBounds.y1,
                        ],
                    },
                ],
            } as IChemyBrushHistoryEntry);
        }
        this.path = [];
        this.copyCanvas = {} as HTMLCanvasElement;
    }

    drawImage (im: HTMLCanvasElement, x: number, y: number): void {
        this.context.save();
        this.context.clearRect(x, y, im.width, im.height);
        this.context.drawImage(im, x, y);
        this.context.restore();
    }

    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {
        // might make sense for stroke
    }
}


code:
import {theme} from '../../theme/theme';

export const ERASE_COLOR = theme.isDark() ? 255 : 255;


code:
import {BB} from '../../bb/bb';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {KL} from '../kl';
import {TPressureInput} from '../kl-types';
import {BezierLine} from '../../bb/math/line';
import {ERASE_COLOR} from './erase-color';
import {KlCanvasContext} from '../canvas/kl-canvas';

export interface IEraserBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'EraserBrush'];
    actions: THistoryInnerActions<EraserBrush>[];
}

export class EraserBrush {

    private size: number = 30;
    private spacing: number = 0.4;
    private opacity: number = 1;
    private useSizePressure: boolean = true;
    private useOpacityPressure: boolean = false;
    private isTransparentBG: boolean = false;

    private history: KlHistoryInterface = new KL.DecoyKlHistory();
    private historyEntry: IEraserBrushHistoryEntry | undefined;
    private isBaseLayer: boolean = false;
    private context: KlCanvasContext = {} as KlCanvasContext;

    private started: boolean = false;
    private lastDot: number | undefined;
    private lastInput: TPressureInput = {x: 0, y: 0, pressure: 0};
    private lastInput2: TPressureInput = {x: 0, y: 0, pressure: 0};
    private bezierLine: BezierLine | undefined;

    private drawDot (x: number, y: number, size: number, opacity: number): void {
        this.context.save();
        if (this.isBaseLayer) {
            if (this.isTransparentBG) {
                this.context.globalCompositeOperation = 'destination-out';
            } else {
                this.context.globalCompositeOperation = 'source-atop';
            }
        } else {
            this.context.globalCompositeOperation = 'destination-out';
        }
        const radgrad = this.context.createRadialGradient(size, size, 0, size, size, size);
        let sharpness = Math.pow(opacity, 2);
        sharpness = Math.max(0, Math.min((size - 1) / size, sharpness));
        const oFac = Math.max(0, Math.min(1, opacity));
        const localOpacity = 2 * oFac - oFac * oFac;
        radgrad.addColorStop(sharpness, `rgba(${ERASE_COLOR}, ${ERASE_COLOR}, ${ERASE_COLOR}, ` + localOpacity + ')');
        radgrad.addColorStop(1, `rgba(${ERASE_COLOR}, ${ERASE_COLOR}, ${ERASE_COLOR}, 0)`);
        this.context.fillStyle = radgrad;
        this.context.translate(x - size, y - size);
        this.context.fillRect(0, 0, size * 2, size * 2);
        this.context.restore();
    }

    private continueLine (x: number | undefined, y: number | undefined, p: number): void {
        p = Math.max(0, Math.min(1, p));
        let localPressure;
        let localOpacity;
        let localSize = (this.useSizePressure) ? Math.max(0.1, p * this.size) : Math.max(0.1, this.size);

        const bdist = Math.max(1, Math.max(0.5, 1 - this.opacity) * localSize * this.spacing);

        const bezierCallback = (val: {
            x: number;
            y: number;
            t: number;
            angle?: number;
            dAngle: number;
        }): void => {
            const factor = val.t;
            localPressure = this.lastInput2.pressure * (1 - factor) + p * factor;
            localOpacity = (this.useOpacityPressure) ? (this.opacity * localPressure * localPressure) : this.opacity;
            localSize = (this.useSizePressure) ? Math.max(0.1, localPressure * this.size) : Math.max(0.1, this.size);

            this.drawDot(val.x, val.y, localSize, localOpacity);
        };

        if (x === undefined || y === undefined) {
            this.bezierLine!.addFinal(bdist, bezierCallback);
        } else {
            this.bezierLine!.add(x, y, bdist, bezierCallback);
        }
    }

    // ---- public -----
    constructor () {}

    // ---- interface ----
    startLine (x: number, y: number, p: number): void {
        this.historyEntry = {
            tool: ['brush', 'EraserBrush'],
            actions: [
                {
                    action: 'opacityPressure',
                    params: [this.useOpacityPressure],
                },
                {
                    action: 'sizePressure',
                    params: [this.useSizePressure],
                },
                {
                    action: 'setSize',
                    params: [this.size],
                },
                {
                    action: 'setOpacity',
                    params: [this.opacity],
                },
                {
                    action: 'setTransparentBG',
                    params: [this.isTransparentBG],
                },
                {
                    action: 'startLine',
                    params: [x, y, p],
                },
            ],
        };

        this.isBaseLayer = 0 === this.context.canvas.index;

        p = Math.max(0, Math.min(1, p));
        const localOpacity = (this.useOpacityPressure) ? (this.opacity * p * p) : this.opacity;
        const localSize = (this.useSizePressure) ? Math.max(0.1, p * this.size) : Math.max(0.1, this.size);

        this.started = true;
        if (localSize > 1) {
            this.drawDot(x, y, localSize, localOpacity);
        }
        this.lastDot = localSize * this.spacing;
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;
        this.lastInput2 = BB.copyObj(this.lastInput);

        this.bezierLine = new BB.BezierLine();
        this.bezierLine.add(x, y, 0, () => undefined);
    }

    goLine (x: number, y: number, p: number): void {
        if (!this.started || !this.historyEntry) {
            return;
        }
        this.historyEntry.actions?.push({
            action: 'goLine',
            params: [x, y, p],
        });

        this.continueLine(x, y, this.lastInput.pressure);

        this.lastInput2 = BB.copyObj(this.lastInput);
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;
    }

    endLine (): void {

        if (this.bezierLine) {
            this.continueLine(undefined, undefined, this.lastInput.pressure);
        }

        this.started = false;
        this.bezierLine = undefined;

        if (this.historyEntry) {
            this.historyEntry.actions?.push({
                action: 'endLine',
                params: [],
            });
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }
    }

    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {

        this.isBaseLayer = 0 === this.context.canvas.index;

        this.lastInput.x = x2;
        this.lastInput.y = y2;

        if (this.started || x1 === undefined) {
            return;
        }

        const mouseDist = Math.sqrt(Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0));
        const eX = (x2 - x1) / mouseDist;
        const eY = (y2 - y1) / mouseDist;
        let loopDist;
        const bdist = Math.max(1, Math.max(0.5, 1 - this.opacity) * this.size * this.spacing);
        this.lastDot = 0;
        for (loopDist = this.lastDot; loopDist <= mouseDist; loopDist += bdist) {
            this.drawDot(x1 + eX * loopDist, y1 + eY * loopDist, this.size, this.opacity);
        }

        const historyEntry: IEraserBrushHistoryEntry = {
            tool: ['brush', 'EraserBrush'],
            actions: [
                {
                    action: 'opacityPressure',
                    params: [this.useOpacityPressure],
                },
                {
                    action: 'sizePressure',
                    params: [this.useSizePressure],
                },
                {
                    action: 'setSize',
                    params: [this.size],
                },
                {
                    action: 'setOpacity',
                    params: [this.opacity],
                },
                {
                    action: 'setTransparentBG',
                    params: [this.isTransparentBG],
                },
                {
                    action: 'drawLineSegment',
                    params: [x1, y1, x2, y2],
                },
            ],
        };
        this.history.push(historyEntry);
    }

    //IS
    isDrawing (): boolean {
        return this.started;
    }

    //SET
    setContext (c: KlCanvasContext): void {
        this.context = c;
    }

    setHistory (l: KlHistoryInterface): void {
        this.history = l;
    }

    setSize (s: number): void {
        this.size = s;
    }

    setOpacity (o: number): void {
        this.opacity = o;
    }

    sizePressure (b: boolean): void {
        this.useSizePressure = b;
    }

    opacityPressure (b: boolean): void {
        this.useOpacityPressure = b;
    }

    setTransparentBG (b: boolean): void {
        this.isTransparentBG = b;
    }

    //GET
    getSize (): number {
        return this.size;
    }

    getOpacity (): number {
        return this.opacity;
    }
}


code:
import {BB} from '../../bb/bb';
import {alphaImArr} from './brushes-common';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {KL} from '../kl';
import {IRGB, TPressureInput} from '../kl-types';
import {BezierLine} from '../../bb/math/line';

export interface IPenBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'PenBrush'];
    actions: THistoryInnerActions<PenBrush>[];
}

const ALPHA_CIRCLE = 0;
const ALPHA_CHALK = 1;
const ALPHA_CAL = 2; // calligraphy
const ALPHA_SQUARE = 3;

const TWO_PI = 2 * Math.PI;

export class PenBrush {

    private context: CanvasRenderingContext2D = {} as CanvasRenderingContext2D;
    private history: KlHistoryInterface = new KL.DecoyKlHistory();
    private historyEntry: IPenBrushHistoryEntry | undefined;

    private settingHasOpacityPressure: boolean = false;
    private settingHasSizePressure: boolean = true;
    private settingSize: number = 2;
    private settingSpacing: number = 0.8489;
    private settingOpacity: number = 1;
    private settingColor: IRGB = {} as IRGB;
    private settingColorStr: string = '';
    private settingAlphaId: number = ALPHA_CIRCLE;
    private settingLockLayerAlpha: boolean = false;

    private hasDrawnDot: boolean = false;
    private lineToolLastDot: number = 0;
    private lastInput: TPressureInput = {x: 0, y: 0, pressure: 0};
    private lastInput2: TPressureInput = {x: 0, y: 0, pressure: 0};
    private inputArr: TPressureInput[] = [];
    private inputIsDrawing: boolean = false;
    private bezierLine: BezierLine | null = null;

    // mipmapping
    private readonly alphaCanvas128: HTMLCanvasElement = BB.canvas(128, 128);
    private readonly alphaCanvas64: HTMLCanvasElement = BB.canvas(64, 64);
    private readonly alphaCanvas32: HTMLCanvasElement = BB.canvas(32, 32);
    private readonly alphaOpacityArr: number[] = [1, 0.9, 1, 1];

    private updateAlphaCanvas () {
        if (this.settingAlphaId === ALPHA_CIRCLE || this.settingAlphaId === ALPHA_SQUARE) {
            return;
        }

        const instructionArr: [HTMLCanvasElement, number][] = [
            [this.alphaCanvas128, 128],
            [this.alphaCanvas64, 64],
            [this.alphaCanvas32, 32],
        ];

        let ctx;

        for (let i = 0; i < instructionArr.length; i++) {
            ctx = BB.ctx((instructionArr[i][0] as any));

            ctx.save();
            ctx.clearRect(0, 0, instructionArr[i][1], instructionArr[i][1]);

            ctx.fillStyle = 'rgba(' + this.settingColor.r
                + ', ' + this.settingColor.g
                + ', ' + this.settingColor.b
                + ', ' + this.alphaOpacityArr[this.settingAlphaId] + ')';
            ctx.fillRect(0, 0, instructionArr[i][1], instructionArr[i][1]);

            ctx.globalCompositeOperation = 'destination-in';
            ctx.imageSmoothingQuality = 'high';
            ctx.drawImage(alphaImArr[this.settingAlphaId], 0, 0, instructionArr[i][1], instructionArr[i][1]);

            ctx.restore();
        }
    }

    private calcOpacity (pressure: number): number {
        return this.settingOpacity * (this.settingHasOpacityPressure ? pressure * pressure : 1);
    }

    /**
     * @param x
     * @param y
     * @param size
     * @param opacity
     * @param angle
     * @param before - [x, y, size, opacity, angle] the drawDot call before
     */
    private drawDot (
        x: number,
        y:number,
        size: number,
        opacity: number,
        angle?: number,
        before?: [number, number, number, number, (number | undefined)]
    ): void {
        if (size <= 0) {
            return;
        }

        if (this.settingLockLayerAlpha) {
            this.context.globalCompositeOperation = 'source-atop';
        }

        if (!before || before[3] !== opacity) {
            this.context.globalAlpha = opacity;
        }

        if (!before && (this.settingAlphaId === ALPHA_CIRCLE || this.settingAlphaId === ALPHA_SQUARE)) {
            this.context.fillStyle = this.settingColorStr;
        }

        if (this.settingAlphaId === ALPHA_CIRCLE) {
            this.context.beginPath();
            this.context.arc(x, y, size, 0, TWO_PI);
            this.context.closePath();
            this.context.fill();
            this.hasDrawnDot = true;

        } else if (this.settingAlphaId === ALPHA_SQUARE) {
            if (angle !== undefined) {
                this.context.save();
                this.context.translate(x, y);
                this.context.rotate(angle / 180 * Math.PI);
                this.context.fillRect(-size, -size, size * 2, size * 2);
                this.context.restore();
                this.hasDrawnDot = true;
            }

        } else { // other brush alphas
            this.context.save();
            this.context.translate(x, y);
            let targetMipmap = this.alphaCanvas128;
            if (size <= 32 && size > 16) {
                targetMipmap = this.alphaCanvas64;
            } else if (size <= 16) {
                targetMipmap = this.alphaCanvas32;
            }
            this.context.scale(size, size);
            if (this.settingAlphaId === ALPHA_CHALK) {
                this.context.rotate(((x + y) * 53123) % TWO_PI); // without mod it sometimes looks different
            }
            this.context.drawImage(targetMipmap, -1, -1, 2, 2);

            this.context.restore();
            this.hasDrawnDot = true;
        }
    }

    // continueLine
    private continueLine (
        x: number | null,
        y: number | null,
        size: number,
        pressure: number,
    ): void {
        if (this.bezierLine === null) {
            this.bezierLine = new BB.BezierLine();
            this.bezierLine.add(this.lastInput.x, this.lastInput.y, 0, () => {});
        }

        const drawArr: [number, number, number, number, (number | undefined)][] = []; //draw instructions. will be all drawn at once

        const dotCallback = (val: {
            x: number;
            y: number;
            t: number;
            angle?: number;
            dAngle: number;
        }): void => {
            const localPressure = BB.mix(this.lastInput2.pressure, pressure, val.t);
            const localOpacity = this.calcOpacity(localPressure);
            const localSize = Math.max(0.1, this.settingSize * (this.settingHasSizePressure ? localPressure : 1));
            drawArr.push([val.x, val.y, localSize, localOpacity, val.angle]);
        };

        const localSpacing = size * this.settingSpacing;
        if (x === null || y === null) {
            this.bezierLine.addFinal(localSpacing, dotCallback);
        } else {
            this.bezierLine.add(x, y, localSpacing, dotCallback);
        }

        // execute draw instructions
        this.context.save();
        let before: typeof drawArr[number] | undefined = undefined;
        for (let i = 0; i < drawArr.length; i++) {
            const item = drawArr[i];
            this.drawDot(item[0], item[1], item[2], item[3], item[4], before);
            before = item;
        }
        this.context.restore();
    }

    // ---- public ----
    constructor () {}

    // ---- interface ----

    startLine (x: number, y: number, p: number): void {
        this.historyEntry = {
            tool: ['brush', 'PenBrush'],
            actions: [
                {
                    action: 'opacityPressure',
                    params: [this.settingHasOpacityPressure],
                },
                {
                    action: 'sizePressure',
                    params: [this.settingHasSizePressure],
                },
                {
                    action: 'setSize',
                    params: [this.settingSize],
                },
                {
                    action: 'setSpacing',
                    params: [this.settingSpacing],
                },
                {
                    action: 'setOpacity',
                    params: [this.settingOpacity],
                },
                {
                    action: 'setColor',
                    params: [this.settingColor],
                },
                {
                    action: 'setAlpha',
                    params: [this.settingAlphaId],
                },
                {
                    action: 'setLockAlpha',
                    params: [this.settingLockLayerAlpha],
                },
            ],
        };

        p = BB.clamp(p, 0, 1);
        const localOpacity = this.calcOpacity(p);
        const localSize = this.settingHasSizePressure ? Math.max(0.1, p * this.settingSize) : Math.max(0.1, this.settingSize);

        this.hasDrawnDot = false;

        this.inputIsDrawing = true;
        this.context.save();
        this.drawDot(x, y, localSize, localOpacity);
        this.context.restore();

        this.lineToolLastDot = localSize * this.settingSpacing;
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;
        this.lastInput2.pressure = p;

        this.inputArr = [{
            x,
            y,
            pressure: p,
        }];

        this.historyEntry.actions!.push({
            action: 'startLine',
            params: [x, y, p],
        });
    }

    goLine (x: number, y: number, p: number): void {
        if (!this.inputIsDrawing) {
            return;
        }
        this.historyEntry!.actions!.push({
            action: 'goLine',
            params: [x, y, p],
        });

        const pressure = BB.clamp(p, 0, 1);
        const localSize = this.settingHasSizePressure ? Math.max(0.1, this.lastInput.pressure * this.settingSize) : Math.max(0.1, this.settingSize);

        this.context.save();
        this.continueLine(x, y, localSize, this.lastInput.pressure);

        /*context.fillStyle = 'red';
        context.fillRect(Math.floor(x), Math.floor(y - 10), 1, 20);
        context.fillRect(Math.floor(x - 10), Math.floor(y), 20, 1);*/

        this.context.restore();

        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput2.pressure = this.lastInput.pressure;
        this.lastInput.pressure = pressure;

        this.inputArr.push({
            x,
            y,
            pressure: p,
        });
    }

    endLine (x: number, y: number): void {

        const localSize = this.settingHasSizePressure ? Math.max(0.1, this.lastInput.pressure * this.settingSize) : Math.max(0.1, this.settingSize);
        this.context.save();
        this.continueLine(null, null, localSize, this.lastInput.pressure);
        this.context.restore();

        this.inputIsDrawing = false;

        if (this.settingAlphaId === ALPHA_SQUARE && !this.hasDrawnDot) {
            // find max pressure input, use that one
            let maxInput = this.inputArr[0];
            this.inputArr.forEach(item => {
                if (item.pressure > maxInput.pressure) {
                    maxInput = item;
                }
            });

            this.context.save();
            const p = BB.clamp(maxInput.pressure, 0, 1);
            const localOpacity = this.calcOpacity(p);
            this.drawDot(maxInput.x, maxInput.y, localSize, localOpacity, 0);
            this.context.restore();
        }

        this.bezierLine = null;

        if (this.historyEntry) {
            this.historyEntry.actions!.push({
                action: 'endLine',
                params: [x, y],
            });
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }

        this.hasDrawnDot = false;
        this.inputArr = [];
    }

    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {
        this.lastInput.x = x2;
        this.lastInput.y = y2;
        this.lastInput.pressure = 1;

        if (this.inputIsDrawing || x1 === undefined) {
            return;
        }

        const angle = BB.pointsToAngleDeg({x:x1, y:y1}, {x:x2, y:y2});
        const mouseDist = Math.sqrt(Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0));
        const eX = (x2 - x1) / mouseDist;
        const eY = (y2 - y1) / mouseDist;
        let loopDist;
        const bdist = this.settingSize * this.settingSpacing;
        this.lineToolLastDot = this.settingSize * this.settingSpacing;
        this.context.save();
        for (loopDist = this.lineToolLastDot; loopDist <= mouseDist; loopDist += bdist) {
            this.drawDot(x1 + eX * loopDist, y1 + eY * loopDist, this.settingSize, this.settingOpacity, angle);
        }
        this.context.restore();

        const historyEntry: IPenBrushHistoryEntry = {
            tool: ['brush', 'PenBrush'],
            actions: [
                {
                    action: 'opacityPressure',
                    params: [this.settingHasOpacityPressure],
                },
                {
                    action: 'sizePressure',
                    params: [this.settingHasSizePressure],
                },
                {
                    action: 'setSize',
                    params: [this.settingSize],
                },
                {
                    action: 'setSpacing',
                    params: [this.settingSpacing],
                },
                {
                    action: 'setOpacity',
                    params: [this.settingOpacity],
                },
                {
                    action: 'setColor',
                    params: [this.settingColor],
                },
                {
                    action: 'setAlpha',
                    params: [this.settingAlphaId],
                },
                {
                    action: 'setLockAlpha',
                    params: [this.settingLockLayerAlpha],
                },
                {
                    action: 'drawLineSegment',
                    params: [x1, y1, x2, y2],
                },
            ],
        };
        this.history.push(historyEntry);
    }

    //IS
    isDrawing (): boolean {
        return this.inputIsDrawing;
    }

    //SET
    setAlpha (a: number): void {
        if (this.settingAlphaId === a) {
            return;
        }
        this.settingAlphaId = a;
        this.updateAlphaCanvas();
    }

    setColor (c: IRGB): void {
        if (this.settingColor === c) {
            return;
        }
        this.settingColor = {r: c.r, g: c.g, b: c.b};
        this.settingColorStr = 'rgb(' + this.settingColor.r + ',' + this.settingColor.g + ',' + this.settingColor.b + ')';
        this.updateAlphaCanvas();
    }

    setContext (c: CanvasRenderingContext2D): void {
        this.context = c;
    }

    setHistory (l: KlHistoryInterface): void {
        this.history = l;
    }

    setSize (s: number): void {
        this.settingSize = s;
    }

    setOpacity (o: number): void {
        this.settingOpacity = o;
    }

    setSpacing (s: number): void {
        this.settingSpacing = s;
    }

    sizePressure (b: boolean): void {
        this.settingHasSizePressure = b;
    }

    opacityPressure (b: boolean): void {
        this.settingHasOpacityPressure = b;
    }

    setLockAlpha (b: boolean): void {
        this.settingLockLayerAlpha = b;
    }

    //GET
    getSpacing (): number {
        return this.settingSpacing;
    }

    getSize (): number {
        return this.settingSize;
    }

    getOpacity (): number {
        return this.settingOpacity;
    }

    getLockAlpha (): boolean {
        return this.settingLockLayerAlpha;
    }

}


code:
import {BB} from '../../bb/bb';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {KL} from '../kl';
import {IRGB, TPressureInput} from '../kl-types';
import {IVector2D} from '../../bb/bb-types';
import {BezierLine} from '../../bb/math/line';
import {ERASE_COLOR} from './erase-color';
import {throwIfNull} from '../../bb/base/base';

export interface IPixelBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'PixelBrush'];
    actions: THistoryInnerActions<PixelBrush>[];
}

export class PixelBrush {

    private history: KlHistoryInterface = new KL.DecoyKlHistory();
    private historyEntry: IPixelBrushHistoryEntry | undefined;
    private context: CanvasRenderingContext2D = {} as CanvasRenderingContext2D;
    private settingHasSizePressure: boolean = true;
    private settingHasOpacityPressure: boolean = false;
    private settingSize: number = 0.5;
    private settingSpacing: number = 0.9;
    private settingOpacity: number = 1;
    private settingColor: IRGB = {} as IRGB;
    private settingColorStr: string = '';
    private settingLockLayerAlpha: boolean = false;
    private settingIsEraser: boolean = false;
    private settingUseDither: boolean = true;
    private inputIsDrawing: boolean = false;
    private lineToolLastDot: number = 0;
    private lastInput: TPressureInput = {x: 0, y: 0, pressure: 0};
    private lastInput2: TPressureInput = {x: 0, y: 0, pressure: 0};
    private bezierLine: BezierLine | null = null;
    private readonly ditherArr: [number, number][] = [
        [3, 2],
        [1, 0],
        [3, 0],
        [1, 2],
        [2, 1],
        [0, 3],
        [0, 1],
        [2, 3],

        [2, 0],
        [0, 2],
        [0, 0],
        [2, 2],
        [1, 1],
        [3, 3],
        [3, 1],
        [1, 3],
    ];
    private readonly ditherCanvas: HTMLCanvasElement;
    private readonly ditherCtx: CanvasRenderingContext2D;
    private ditherPattern: CanvasPattern = {} as CanvasPattern;

    private updateDither (): void {
        this.ditherCtx.clearRect(0, 0, 4, 4);
        this.ditherCtx.fillStyle = this.settingIsEraser ? `rgb(${ERASE_COLOR},${ERASE_COLOR},${ERASE_COLOR})` : this.settingColorStr;
        for (let i = 0; i < Math.max(1, Math.round(this.settingOpacity * this.ditherArr.length)); i++) {
            this.ditherCtx.fillRect(this.ditherArr[i][0], this.ditherArr[i][1], 1, 1);
        }
        this.ditherPattern = throwIfNull(this.context.createPattern(this.ditherCanvas, 'repeat'));
    }

    /**
     * Tests p1->p2 or p3->p4 deviate in their direction more than max, compared to p1->p4
     */
    private cubicCurveOverThreshold (p1: IVector2D, p2: IVector2D, p3: IVector2D, p4: IVector2D, maxAngleRad: number): boolean {
        const d = BB.Vec2.nor({
            x: p4.x - p1.x,
            y: p4.y - p1.y,
        });
        const d2 = BB.Vec2.nor({
            x: p2.x - p1.x,
            y: p2.y - p1.y,
        });
        const d3 = BB.Vec2.nor({
            x: p4.x - p3.x,
            y: p4.y - p3.y,
        });
        // const a2 = Math.abs(BB.Vec2.angle(d, d2) % Math.PI) / Math.PI * 180;
        // const a3 = Math.abs(BB.Vec2.angle(d, d3) % Math.PI) / Math.PI * 180;

        return Math.max(BB.Vec2.dist(d, d2), BB.Vec2.dist(d, d3)) > maxAngleRad;
    }

    private plotCubicBezierLine (p1: IVector2D, p2: IVector2D, p3: IVector2D, p4: IVector2D): void {

        const isOverThreshold = this.cubicCurveOverThreshold(p1, p2, p3, p4, 0.1);

        p1.x = Math.floor(p1.x);
        p1.y = Math.floor(p1.y);
        p4.x = Math.floor(p4.x);
        p4.y = Math.floor(p4.y);

        const dist = BB.dist(p1.x, p1.y, p4.x, p4.y);
        if (!isOverThreshold || dist < 7) {
            this.plotLine(p1.x, p1.y, p4.x, p4.y, true);
            return;
        }

        const n = Math.max(2, Math.round(dist / 4));
        const pointArr = [];
        for (let i = 0; i <= n; i++) {
            const t = i / n;
            const a = Math.pow(1 - t, 3);
            const b = 3 * t * Math.pow(1 - t, 2);
            const c = 3 * Math.pow(t, 2) * (1 - t);
            const d = Math.pow(t, 3);
            pointArr.push({
                x: a * p1.x + b * p2.x + c * p3.x + d * p4.x,
                y: a * p1.y + b * p2.y + c * p3.y + d * p4.y,
            });
        }

        for (let i = 0; i < n; i++) {
            this.plotLine(
                Math.round(pointArr[i].x),
                Math.round(pointArr[i].y),
                Math.round(pointArr[i + 1].x),
                Math.round(pointArr[i + 1].y),
                true
            );
        }
    }

    private drawDot (x: number, y: number, size: number, opacity: number): void {
        this.context.save();
        if (this.settingIsEraser) {
            this.context.fillStyle = this.settingUseDither ? this.ditherPattern : '#fff';
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            } else {
                this.context.globalCompositeOperation = 'destination-out';
            }
        } else {
            this.context.fillStyle = this.settingUseDither ? this.ditherPattern : this.settingColorStr;
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            }
        }
        this.context.globalAlpha = this.settingUseDither ? 1 : opacity;
        this.context.fillRect(
            Math.round(x + -size),
            Math.round(y + -size),
            Math.round(size * 2),
            Math.round(size * 2)
        );
        this.context.restore();
    }

    private continueLine (
        x: number | null,
        y: number | null,
        size: number,
        pressure: number
    ): void {
        if (this.bezierLine === null) {
            this.bezierLine = new BB.BezierLine();
            this.bezierLine.add(this.lastInput.x, this.lastInput.y, 0, () => {});
        }

        this.context.save();

        const dotCallback = (
            val: {
                x: number;
                y: number;
                t: number;
                angle?: number;
                dAngle: number;
            }
        ): void => {
            const localPressure = BB.mix(this.lastInput2.pressure, pressure, val.t);
            const localOpacity = this.settingOpacity * (this.settingHasOpacityPressure ? (localPressure * localPressure) : 1);
            const localSize = Math.max(0.5, this.settingSize * (this.settingHasSizePressure ? localPressure : 1));
            this.drawDot(val.x, val.y, localSize, localOpacity);
        };

        const controlCallback = (
            controlObj: {
                p1: IVector2D;
                p2: IVector2D;
                p3: IVector2D;
                p4: IVector2D;
            }
        ): void => {
            this.plotCubicBezierLine(controlObj.p1, controlObj.p2, controlObj.p3, controlObj.p4);
        };

        if (Math.round(this.settingSize * 2) === 1) {
            if (x === null || y === null) {
                this.bezierLine.addFinal(4, undefined, controlCallback);
            } else {
                this.bezierLine.add(x, y, 4, undefined, controlCallback);
            }

        } else {
            const localSpacing = size * this.settingSpacing;

            if (x === null || y === null) {
                this.bezierLine.addFinal(localSpacing, dotCallback);
            } else {
                this.bezierLine.add(x, y, localSpacing, dotCallback);
            }
        }

        this.context.restore();
    }

    /**
     * bresenheim line drawing
     */
    private plotLine (x0: number, y0: number, x1: number, y1: number, skipFirst: boolean): void {
        this.context.save();

        if (this.settingIsEraser) {
            this.context.fillStyle = this.settingUseDither ? this.ditherPattern : '#fff';
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            } else {
                this.context.globalCompositeOperation = 'destination-out';
            }
        } else {
            this.context.fillStyle = this.settingUseDither ? this.ditherPattern : this.settingColorStr;
            if (this.settingLockLayerAlpha) {
                this.context.globalCompositeOperation = 'source-atop';
            }
        }
        this.context.globalAlpha = this.settingUseDither ? 1 : this.settingOpacity;

        x0 = Math.floor(x0);
        y0 = Math.floor(y0);
        x1 = Math.floor(x1);
        y1 = Math.floor(y1);

        const dX = Math.abs(x1 - x0);
        const sX = x0 < x1 ? 1 : -1;
        const dY = -Math.abs(y1 - y0);
        const sY = y0 < y1 ? 1 : -1;
        let err = dX + dY;
        // eslint-disable-next-line no-constant-condition
        while (true) {
            if (skipFirst) {
                skipFirst = false;
            } else {
                this.context.fillRect(x0, y0, 1, 1);
            }
            if (x0 === x1 && y0 === y1) {
                break;
            }
            const e2 = 2 * err;
            if (e2 >= dY) {
                err += dY;
                x0 += sX;
            }
            if (e2 <= dX) {
                err += dX;
                y0 += sY;
            }
        }

        this.context.restore();
    }

    // ---- public ----
    constructor () {
        this.ditherCanvas = BB.canvas(4, 4);
        this.ditherCtx = BB.ctx(this.ditherCanvas);
    }

    // ---- interface ----

    startLine (x: number, y: number, p: number): void {
        this.historyEntry = {
            tool: ['brush', 'PixelBrush'],
            actions: [
                {
                    action: 'sizePressure',
                    params: [this.settingHasSizePressure],
                },
                {
                    action: 'opacityPressure',
                    params: [this.settingHasOpacityPressure],
                },
                {
                    action: 'setSize',
                    params: [this.settingSize],
                },
                {
                    action: 'setSpacing',
                    params: [this.settingSpacing],
                },
                {
                    action: 'setOpacity',
                    params: [this.settingOpacity],
                },
                {
                    action: 'setColor',
                    params: [this.settingColor],
                },
                {
                    action: 'setLockAlpha',
                    params: [this.settingLockLayerAlpha],
                },
                {
                    action: 'setIsEraser',
                    params: [this.settingIsEraser],
                },
                {
                    action: 'setUseDither',
                    params: [this.settingUseDither],
                },
            ],
        };

        if (this.settingUseDither) {
            this.updateDither();
        }

        p = Math.max(0, Math.min(1, p));
        const localOpacity = this.settingHasOpacityPressure ? (this.settingOpacity * p * p) : this.settingOpacity;
        const localSize = this.settingHasSizePressure ? Math.max(0.5, p * this.settingSize) : Math.max(0.5, this.settingSize);

        this.inputIsDrawing = true;
        this.drawDot(x, y, localSize, localOpacity);
        this.lineToolLastDot = localSize * this.settingSpacing;
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = p;
        this.lastInput2 = BB.copyObj(this.lastInput);

        this.historyEntry.actions!.push({
            action: 'startLine',
            params: [x, y, p],
        });
    }

    goLine (x: number, y: number, p: number): void {
        if (!this.inputIsDrawing) {
            return;
        }
        this.historyEntry!.actions!.push({
            action: 'goLine',
            params: [x, y, p],
        });

        //debug
        //drawDot(x, y, 1, 0.5);

        const pressure = BB.clamp(p, 0, 1);
        const localSize = this.settingHasSizePressure ? Math.max(0.1, this.lastInput.pressure * this.settingSize) : Math.max(0.1, this.settingSize);

        this.continueLine(x, y, localSize, this.lastInput.pressure);

        this.lastInput2 = BB.copyObj(this.lastInput);
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.lastInput.pressure = pressure;
    }

    endLine (x: number, y: number): void {

        const localSize = this.settingHasSizePressure ? Math.max(0.1, this.lastInput.pressure * this.settingSize) : Math.max(0.1, this.settingSize);
        this.continueLine(null, null, localSize, this.lastInput.pressure);

        //debug
        //drawDot(lastInput.x, lastInput.y, 3, 1);
        //drawDot(x, y, 10, 0.1);

        this.inputIsDrawing = false;

        this.bezierLine = null;

        if (this.historyEntry) {
            this.historyEntry.actions!.push({
                action: 'endLine',
                params: [x, y],
            });
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }
    }

    //cheap n' ugly
    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {
        this.lastInput.x = x2;
        this.lastInput.y = y2;
        this.lastInput.pressure = 1;

        if (this.inputIsDrawing || x1 === undefined) {
            return;
        }

        if (this.settingUseDither) {
            this.updateDither();
        }

        if (Math.round(this.settingSize * 2) === 1) {
            this.plotLine(x1, y1, x2, y2, true);
        } else {
            // const angle = BB.pointsToAngleDeg({x: x1, y: y1}, {x: x2, y: y2});
            const mouseDist = Math.sqrt(Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0));
            const eX = (x2 - x1) / mouseDist;
            const eY = (y2 - y1) / mouseDist;
            let loopDist;
            const bdist = this.settingSize * this.settingSpacing;
            this.lineToolLastDot = this.settingSize * this.settingSpacing;
            for (loopDist = this.lineToolLastDot; loopDist <= mouseDist; loopDist += bdist) {
                this.drawDot(x1 + eX * loopDist, y1 + eY * loopDist, this.settingSize, this.settingOpacity);
            }
        }

        const historyEntry: IPixelBrushHistoryEntry = {
            tool: ['brush', 'PixelBrush'],
            actions: [
                {
                    action: 'sizePressure',
                    params: [this.settingHasSizePressure],
                },
                {
                    action: 'opacityPressure',
                    params: [this.settingHasOpacityPressure],
                },
                {
                    action: 'setSize',
                    params: [this.settingSize],
                },
                {
                    action: 'setSpacing',
                    params: [this.settingSpacing],
                },
                {
                    action: 'setOpacity',
                    params: [this.settingOpacity],
                },
                {
                    action: 'setColor',
                    params: [this.settingColor],
                },
                {
                    action: 'setLockAlpha',
                    params: [this.settingLockLayerAlpha],
                },
                {
                    action: 'setIsEraser',
                    params: [this.settingIsEraser],
                },
                {
                    action: 'setUseDither',
                    params: [this.settingUseDither],
                },
                {
                    action: 'drawLineSegment',
                    params: [x1, y1, x2, y2],
                },
            ],
        };
        this.history.push(historyEntry);
    }

    //IS
    isDrawing (): boolean {
        return this.inputIsDrawing;
    }

    //SET
    setColor (c: IRGB): void {
        if (this.settingColor === c) {
            return;
        }
        this.settingColor = c;
        this.settingColorStr = 'rgb(' + this.settingColor.r + ',' + this.settingColor.g + ',' + this.settingColor.b + ')';
    }

    setContext (c: CanvasRenderingContext2D): void {
        this.context = c;
    }

    setHistory (l: KlHistoryInterface): void {
        this.history = l;
    }

    setSize (s: number): void {
        this.settingSize = Math.round(s * 2) / 2;
    }

    setOpacity (o: number): void {
        this.settingOpacity = o;
    }

    setSpacing (s: number): void {
        this.settingSpacing = s;
    }

    sizePressure (b: boolean): void {
        this.settingHasSizePressure = b;
    }

    opacityPressure (b: boolean): void {
        this.settingHasOpacityPressure = b;
    }

    setLockAlpha (b: boolean): void {
        this.settingLockLayerAlpha = b;
    }

    setIsEraser (b: boolean): void {
        this.settingIsEraser = b;
    }

    setUseDither (b: boolean): void {
        this.settingUseDither = b;
    }

    //GET
    getSpacing (): number {
        return this.settingSpacing;
    }

    getSize (): number {
        return this.settingSize;
    }

    getOpacity (): number {
        return this.settingOpacity;
    }

    getLockAlpha (): boolean {
        return this.settingLockLayerAlpha;
    }

    getIsEraser (): boolean {
        return this.settingIsEraser;
    }

    getUseDither (): boolean {
        return this.settingUseDither;
    }
}


code:
import {BB} from '../../bb/bb';
import {IHistoryEntry, KlHistoryInterface, THistoryInnerActions} from '../history/kl-history';
import {KL} from '../kl';
import {IRGB, TPressureInput} from '../kl-types';

export interface ISketchyBrushHistoryEntry extends IHistoryEntry {
    tool: ['brush', 'SketchyBrush'];
    actions: THistoryInnerActions<SketchyBrush>[];
}

const sampleCanvas = BB.canvas(32, 32);
const sampleCtx = BB.ctx(sampleCanvas);

export class SketchyBrush {

    private context: CanvasRenderingContext2D = {} as CanvasRenderingContext2D;
    private settingColor: IRGB = {} as IRGB;
    private settingSize: number = 1;
    private settingOpacity: number = 0.2;
    private settingBlending: number = 0.5;
    private settingScale: number = 1;
    private lastX: number = 0;
    private lastY: number = 0;
    private inputIsDrawing: boolean = false;
    private lastInput: TPressureInput = {x: 0, y: 0, pressure: 0};
    private history: KlHistoryInterface = new KL.DecoyKlHistory();
    private historyEntry: ISketchyBrushHistoryEntry | undefined;
    private sketchySeed: number = 0;
    private points: [number, number][] = []; // x y
    private count: number = 0;
    private readonly mixMode = [
        (c1: IRGB): IRGB => {
            return c1;
        },
        (c1: IRGB, c2: IRGB): IRGB => { // why
            const result = new BB.RGB(c1.r, c1.g, c1.b);
            result.r *= c2.r / 255;
            result.g *= c2.g / 255;
            result.b *= c2.b / 255;
            return result;
        },
    ];

    private rand (): number {
        this.sketchySeed++;
        return Math.sin(6324634.2345 * Math.cos(this.sketchySeed * 5342.3423)) * 0.5 + 0.5;
    }

    // ---- public ----
    constructor () {}

    // ---- interface ----

    setHistory (l: KlHistoryInterface): void {
        this.history = l;
    }

    setSeed (s: number): void {
        this.sketchySeed = parseInt('' + s);
    }

    getSeed (): number {
        return parseInt('' + this.sketchySeed);
    }

    getSize (): number {
        return this.settingSize / 2;
    }

    setColor (c: IRGB): void {
        this.settingColor = c;
    }

    getOpacity (): number {
        return this.settingOpacity;
    }

    setOpacity (o: number): void {
        this.settingOpacity = o;
    }

    getBlending (): number {
        return this.settingBlending;
    }

    setBlending (b: number): void {
        this.settingBlending = b;
    }

    setSize (s: number): void {
        this.settingSize = s * 2;
    }

    getScale (): number {
        return this.settingScale;
    }

    setScale (s: number): void {
        this.settingScale = s;
    }

    setContext (c: CanvasRenderingContext2D): void {
        this.context = c;
    }

    startLine (x: number, y: number, pressure: number, shift?: boolean): void {
        if (shift && this.lastInput.x) {
            this.inputIsDrawing = true;
            this.endLine();
        } else {
            this.inputIsDrawing = true;
            this.lastX = x;
            this.lastY = y;
            this.lastInput.x = x;
            this.lastInput.y = y;
            this.historyEntry = {
                tool: ['brush', 'SketchyBrush'],
                actions: [
                    {
                        action: 'setScale',
                        params: [this.settingScale],
                    },
                    {
                        action: 'setSize',
                        params: [this.settingSize / 2],
                    },
                    {
                        action: 'setOpacity',
                        params: [this.settingOpacity],
                    },
                    {
                        action: 'setColor',
                        params: [this.settingColor],
                    },
                    {
                        action: 'setBlending',
                        params: [this.settingBlending],
                    },
                    {
                        action: 'startLine',
                        params: [x, y, pressure],
                    },
                ],
            };
        }

    }

    goLine (p_x: number, p_y: number, pressure: number, preMixedColor: IRGB | undefined): void {
        if (!this.inputIsDrawing || (p_x === this.lastInput.x && p_y === this.lastInput.y)) {
            return;
        }

        let e, b, a, g;
        const x = parseInt('' + p_x);
        const y = parseInt('' + p_y);
        this.points.push([x, y]);

        let mixr = this.settingColor.r;
        let mixg = this.settingColor.g;
        let mixb = this.settingColor.b;

        if (preMixedColor) {
            mixr = preMixedColor.r;
            mixg = preMixedColor.g;
            mixb = preMixedColor.b;
        } else {
            if (this.settingBlending !== 0) {
                if (x + 5 >= 0 && y + 5 >= 0 && x - 5 < this.context.canvas.width - 1 && y - 5 < this.context.canvas.height - 1) {
                    mixr = 0;
                    mixg = 0;
                    mixb = 0;
                    const mixX = Math.min(this.context.canvas.width - 1, Math.max(0, x - 5));
                    const mixY = Math.min(this.context.canvas.height - 1, Math.max(0, y - 5));
                    let mixW = Math.min(this.context.canvas.width - 1, Math.max(0, x + 5));
                    let mixH = Math.min(this.context.canvas.height - 1, Math.max(0, y + 5));
                    mixW -= mixX;
                    mixH -= mixY;

                    if (mixW > 0 && mixH > 0) {
                        const imDat = this.context.getImageData(mixX, mixY, mixW, mixH);
                        let countMix = 0;
                        for (let i = 0; i < imDat.data.length; i += 4) {
                            mixr += imDat.data[i + 0];
                            mixg += imDat.data[i + 1];
                            mixb += imDat.data[i + 2];
                            countMix++;
                        }
                        mixr /= countMix;
                        mixg /= countMix;
                        mixb /= countMix;
                    }

                    const mixed = this.mixMode[0](new BB.RGB(mixr, mixg, mixb), this.settingColor);
                    mixr = parseInt('' + BB.mix(this.settingColor.r, mixed.r, this.settingBlending));
                    mixg = parseInt('' + BB.mix(this.settingColor.g, mixed.g, this.settingBlending));
                    mixb = parseInt('' + BB.mix(this.settingColor.b, mixed.b, this.settingBlending));
                }
            }
        }

        this.context.save();
        this.context.strokeStyle = 'rgba(' + mixr + ', ' + mixg + ', ' + mixb + ', ' + this.settingOpacity + ')';
        this.context.lineWidth = this.settingSize;

        this.context.beginPath();
        this.context.moveTo(this.lastX, this.lastY);
        this.context.lineTo(x, y);

        for (e = 0; e < this.points.length; e++) {
            b = this.points[e][0] - this.points[this.count][0];
            a = this.points[e][1] - this.points[this.count][1];
            g = b * b + a * a;
            if (g < 4000 * this.settingScale * this.settingScale && this.rand() > g / 2000 / this.settingScale / this.settingScale) {
                this.context.moveTo(this.points[this.count][0] + (b * 0.3), this.points[this.count][1] + (a * 0.3));
                this.context.lineTo(this.points[e][0] - (b * 0.3), this.points[e][1] - (a * 0.3));
            }
        }

        this.context.stroke();
        this.context.restore();

        this.count++;
        this.lastX = x;
        this.lastY = y;
        this.lastInput.x = x;
        this.lastInput.y = y;
        this.historyEntry!.actions!.push({
            action: 'goLine',
            params: [p_x, p_y, pressure, {r: mixr, g: mixg, b: mixb}],
        });
    }

    endLine (): void {
        this.inputIsDrawing = false;
        this.count = 0;
        this.points = [];
        if (this.historyEntry) {
            this.historyEntry.actions!.push({
                action: 'endLine',
                params: [],
            });
            this.history.push(this.historyEntry);
            this.historyEntry = undefined;
        }
    }
    //cheap 'n' ugly

    drawLineSegment (x1: number, y1: number, x2: number, y2: number): void {
        this.lastInput.x = x2;
        this.lastInput.y = y2;

        if (this.inputIsDrawing || x1 === undefined) {
            return;
        }

        this.context.save();
        this.context.lineWidth = this.settingSize;

        const mixCol = {
            r: this.settingColor.r,
            g: this.settingColor.g,
            b: this.settingColor.b,
        };
        if (x1 + 5 >= 0 && y1 + 5 >= 0 && x1 - 5 < this.context.canvas.width - 1 && y1 - 5 < this.context.canvas.height - 1) {
            mixCol.r = 0;
            mixCol.g = 0;
            mixCol.b = 0;
            const mixX = Math.min(this.context.canvas.width - 1, Math.max(0, x1 - 5));
            const mixY = Math.min(this.context.canvas.height - 1, Math.max(0, y1 - 5));
            let mixW = Math.min(this.context.canvas.width - 1, Math.max(0, x1 + 5));
            let mixH = Math.min(this.context.canvas.height - 1, Math.max(0, y1 + 5));
            mixW -= mixX;
            mixH -= mixY;
            if (mixW > 0 && mixH > 0) {
                const w = Math.min(sampleCanvas.width, mixW);
                const h = Math.min(sampleCanvas.height, mixH);
                sampleCtx.save();
                sampleCtx.globalCompositeOperation = 'copy';
                sampleCtx.drawImage(this.context.canvas, mixX, mixY, mixW, mixH, 0, 0, w, h);
                sampleCtx.restore();

                const imDat = sampleCtx.getImageData(mixX, mixY, mixW, mixH);
                let countMix = 0;
                for (let i = 0; i < imDat.data.length; i += 4) {
                    mixCol.r += imDat.data[i + 0];
                    mixCol.g += imDat.data[i + 1];
                    mixCol.b += imDat.data[i + 2];
                    countMix++;
                }
                mixCol.r /= countMix;
                mixCol.g /= countMix;
                mixCol.b /= countMix;
            }
        }
        const mixed = this.mixMode[0](new BB.RGB(mixCol.r, mixCol.g, mixCol.b), this.settingColor);
        mixCol.r = parseInt('' + (this.settingBlending * mixed.r + this.settingColor.r * (1 - this.settingBlending)));
        mixCol.g = parseInt('' + (this.settingBlending * mixed.g + this.settingColor.g * (1 - this.settingBlending)));
        mixCol.b = parseInt('' + (this.settingBlending * mixed.b + this.settingColor.b * (1 - this.settingBlending)));
        this.context.strokeStyle = 'rgba(' + mixCol.r + ', ' + mixCol.g + ', ' + mixCol.b + ', ' + this.settingOpacity + ')';
        this.context.beginPath();
        this.context.moveTo(x1, y1);
        this.context.lineTo(x2, y2);
        this.context.stroke();
        this.context.strokeStyle = 'rgba(' + mixCol.r + ', ' + mixCol.g + ', ' + mixCol.b + ', ' + this.settingOpacity + ')';
        this.context.restore();

        const historyEntry: ISketchyBrushHistoryEntry = {
            tool: ['brush', 'SketchyBrush'],
            actions: [
                {
                    action: 'setScale',
                    params: [this.settingScale],
                },
                {
                    action: 'setSize',
                    params: [this.settingSize / 2],
                },
                {
                    action: 'setOpacity',
                    params: [this.settingOpacity],
                },
                {
                    action: 'setColor',
                    params: [this.settingColor],
                },
                {
                    action: 'setBlending',
                    params: [this.settingBlending],
                },
                {
                    action: 'drawLineSegment',
                    params: [x1, y1, x2, y2],
                },
            ],
        };
        this.history.push(historyEntry);
    }

    isDrawing (): boolean {
        return this.inputIsDrawing;
    }
}