code:
export interface IWheelCleanerEvent {
    deltaY: number;
    pageX: number;
    pageY: number;
    clientX: number;
    clientY: number;
}

const SEQUENCE_TIMEOUT_MS = 200;

/**
 * Filters wheel events. removes swipe scrolling and pinch scrolling that trackpads do. (as best as it can)
 * Normalizes regular scrolls.
 *
 * Why:
 * - trackpad scrolling is different from old school mouse scrolling
 * - but there is no way to learn from the browser if it's trackpad scrolling
 * - browsers don't even give access to the raw swiping or pinching movement, but some abstraction on top, making the scrolling
 *      continue an arbitrary amount, at an arbitrary scale
 * - each browser does this differently. So you can't offer a consistent/controlled experience
 *
 * - also trackpads are painful to draw with. So supporting a trackpad-based workflow makes not much sense.
 */
export class WheelCleaner {

    private readonly knownUnitArr: number[] = [100];
    private sequenceLength: number = 0;
    private sequenceUnit: null | number = null;
    private endSequenceTimeout: undefined | ReturnType<typeof setTimeout>;
    private toEmitDelta: null | number = null;
    private position: null | {
        pageX: number;
        pageY: number;
        clientX: number;
        clientY: number;
    } = null;

    private emit (delta: number): void {
        if (this.position === null || this.sequenceUnit === null) {
            return;
        }
        this.callback({
            deltaY: Math.round(delta / this.sequenceUnit),
            pageX: this.position.pageX,
            pageY: this.position.pageY,
            clientX: this.position.clientX,
            clientY: this.position.clientY,
        });
    }

    private endSequence (): void {
        if (this.toEmitDelta !== null) {
            this.emit(this.toEmitDelta);
            this.toEmitDelta = null;
        }
        if (this.sequenceUnit !== null && !(this.knownUnitArr.includes(this.sequenceUnit))) {
            this.knownUnitArr.push(this.sequenceUnit);
        }
        this.sequenceLength = 0;
        this.sequenceUnit = null;
    }

    // ---- public ----

    constructor (private callback: (p: IWheelCleanerEvent) => void) {}

    process (event: WheelEvent): void {
        this.position = {
            pageX: event.pageX,
            pageY: event.pageY,
            clientX: event.clientX,
            clientY: event.clientY,
        };

        if (this.endSequenceTimeout) {
            clearTimeout(this.endSequenceTimeout);
        }
        this.endSequenceTimeout = setTimeout(() => this.endSequence(), SEQUENCE_TIMEOUT_MS);

        //prep delta
        let delta = event.deltaY;
        if ('deltaMode' in event && event.deltaMode === 1) {
            delta *= 100 / 3;
        }
        const absDelta = Math.abs(delta);

        //sequence begins
        if (this.sequenceLength === 0) {
            this.sequenceLength++;
            if (absDelta < 50) {
                //dirty - probably a swipe scroll or pinch scroll on trackpad
                return;
            }

            this.sequenceUnit = absDelta;
            if (this.knownUnitArr.includes(this.sequenceUnit)) {
                //we know this unit - emit right away
                this.emit(delta);
            } else {
                //unknown unit - wait until next event or sequence end, to have more certainty that it's clean
                this.toEmitDelta = delta;
            }
            return;
        }

        if (this.sequenceUnit === null) {
            //previously determined dirty sequence
            this.toEmitDelta = null;
            return;
        }

        //sequence continues
        if (absDelta === 0) {
            //ignore zero scroll
            return;
        }
        if (
            absDelta === this.sequenceUnit ||
            (absDelta / this.sequenceUnit) % 1 < 0.0001// a multiple
        ) {
            //fine
        } else if ((this.sequenceUnit / absDelta) % 1 < 0.0001) {
            //unit was actually a multiple - update it
            this.sequenceUnit = absDelta;

        } else if (absDelta !== this.sequenceUnit) {
            //not clean - delta is varying - probably a swipe scroll or pinch scroll on trackpad
            this.sequenceUnit = null;
            this.toEmitDelta = null;
            return;
        }

        if (this.toEmitDelta !== null) {
            this.emit(this.toEmitDelta);
            this.toEmitDelta = null;
        }
        this.emit(delta);
    }

}


code:
import {Vec2} from './vec2';
import {clamp, dist, mix, pointsToAngleDeg} from './math';
import {IVector2D} from '../bb-types';
import {copyObj} from '../base/base';

/**
 * project p onto line
 * @param lineStart
 * @param lineEnd
 * @param p
 */
export const projectPointOnLine = function (lineStart: IVector2D, lineEnd: IVector2D, p: IVector2D): IVector2D {
    let x, y;
    if (lineStart.x === lineEnd.x) {
        x = lineStart.x;
        y = p.y;

        return {
            x: x,
            y: y,
        };
    }
    const m = (lineEnd.y - lineStart.y) / (lineEnd.x - lineStart.x);
    const b = lineStart.y - (m * lineStart.x);

    x = (m * p.y + p.x - m * b) / (m * m + 1);
    y = (m * m * p.y + m * p.x + b) / (m * m + 1);

    return {
        x: x,
        y: y,
    };
};

/**
 * Operations on a line made up of points
 */
export class PointLine {

    private readonly segmentArr: {
        x: number;
        y: number;
        length: number; // last is 0
    }[];

    // ---- public ----
    constructor (
        p: {
            points: IVector2D[];
        }
    ) {
        this.segmentArr = [];

        for (let i = 0; i < p.points.length; i++) {
            ((i) => {
                let length = 0;
                if (i < p.points.length - 1) {
                    length = dist(p.points[i].x, p.points[i].y, p.points[i + 1].x, p.points[i + 1].y);
                }
                this.segmentArr[i] = {
                    x: p.points[i].x,
                    y: p.points[i].y,
                    length: length,
                };
            })(i);
        }
    }

    // ---- interface ----

    /**
     * returns point when traveling *dist* along the line, > 0
     * @param dist
     */
    getAtDist (dist: number): IVector2D {
        let remainder = Math.min(this.getLength(), dist);
        let i = 0;

        for (; remainder > this.segmentArr[i].length && i < this.segmentArr.length - 2; i++) {
            remainder -= this.segmentArr[i].length;
        }

        const fac = Math.min(1, Math.max(0, remainder / this.segmentArr[i].length));

        return {
            x: (this.segmentArr[i].x * (1 - fac) + this.segmentArr[i + 1].x * fac),
            y: (this.segmentArr[i].y * (1 - fac) + this.segmentArr[i + 1].y * fac),
        };
    }

    /**
     * total length of line
     */
    getLength (): number {
        let result = 0;
        for (let i = 0; i < this.segmentArr.length - 1; i++) {
            result += this.segmentArr[i].length;
        }
        return result;
    }
}

export type TBezierLineCallback = (
    v: {
        x: number;
        y: number;
        t: number; // [0, 1] - how far along
        angle?: number;
        dAngle: number;
    }
) => void;
type TBezierLineControlsCallback = (
    v: {
        p1: IVector2D;
        p2: IVector2D;
        p3: IVector2D;
        p4: IVector2D;
    }
) => void;

type TBezierLinePoint = {
    x: number;
    y: number;
    spacing: number;
    dir: IVector2D;
};

/**
 * Each instance is one line made up of bezier interpolated segments.
 * You feed it points. It calculates control points on its own, and the resulting curve.
 */
export class BezierLine {

    private readonly pointArr: TBezierLinePoint[];
    private lastDot: number = 0;
    private lastPoint: IVector2D | undefined;
    private lastCallbackPoint: IVector2D | undefined;
    private lastAngle: number | undefined;
    private lastSpacing: number | undefined;

    /**
     * creates bezier curve from control points
     * @param p1 - control point 1 {x: float, y: float}
     * @param p2 - control point 2 {x: float, y: float}
     * @param p3 - control point 3 {x: float, y: float}
     * @param p4 - control point 4 {x: float, y: float}
     * @param resolution - int
     * @returns bezier curve made up of points {x: float, y: float}
     */
    private getBezierPoints (p1: IVector2D, p2: IVector2D, p3: IVector2D, p4: IVector2D, resolution: number): IVector2D[] {
        const curvePoints = [];
        let t;
        for (let i = 0; i <= resolution; i++) {
            t = i / resolution;
            curvePoints[curvePoints.length] = {
                x: Math.pow(1 - t, 3) * p1.x + 3 * Math.pow(1 - t, 2) * t * p2.x + 3 * (1 - t) * Math.pow(t, 2) * p3.x + Math.pow(t, 3) * p4.x,
                y: Math.pow(1 - t, 3) * p1.y + 3 * Math.pow(1 - t, 2) * t * p2.y + 3 * (1 - t) * Math.pow(t, 2) * p3.y + Math.pow(t, 3) * p4.y,
            };
        }
        return curvePoints;
    }

    // ---- public ----
    constructor () {
        this.pointArr = [];
    }

    // ---- interface ----

    /**
     * Add new point to line. "Drawn" line will go until the previous point.
     *
     * @param x - coord of new point
     * @param y
     * @param spacing - space between each step
     * @param callback - calls for each step
     * @param controlsCallback - calls that callback with the bezier control points
     */
    add (
        x: number,
        y: number,
        spacing: number,
        callback?: TBezierLineCallback,
        controlsCallback?: TBezierLineControlsCallback,
    ): void {
        if (this.lastPoint && x === this.lastPoint.x && y === this.lastPoint.y) {
            return;
        }
        this.lastPoint = {x, y};
        this.pointArr[this.pointArr.length] = {
            x,
            y,
            spacing,
        } as TBezierLinePoint;

        //calculate directions
        if (this.pointArr.length === 1) {
            this.lastSpacing = spacing;
            return;
        } else if (this.pointArr.length === 2) {
            this.pointArr[0].dir = Vec2.nor(Vec2.sub(this.pointArr[1], this.pointArr[0]));
            this.lastDot = spacing;
            this.lastSpacing = spacing;
            return;
        } else {
            const pointM1 = this.pointArr[this.pointArr.length - 1];
            const pointM2 = this.pointArr[this.pointArr.length - 2];
            const pointM3 = this.pointArr[this.pointArr.length - 3];
            pointM2.dir = Vec2.nor(Vec2.sub(pointM1, pointM3));
            if (isNaN(pointM2.dir.x) || isNaN(pointM2.dir.y)) {
                //when xy -3 == -1
                pointM2.dir = copyObj(pointM3.dir);
            }
        }

        //get bezier curve
        const a = this.pointArr[this.pointArr.length - 3];
        const b = this.pointArr[this.pointArr.length - 2];
        const p1 = a;
        const p2 = Vec2.add(a, Vec2.mul(a.dir, Vec2.dist(a, b) / 4));
        const p3 = Vec2.sub(b, Vec2.mul(b.dir, Vec2.dist(a, b) / 4));
        const p4 = b;

        let pointLine: PointLine;
        if (callback) {
            const curvePoints = this.getBezierPoints(p1, p2, p3, p4, 20);
            pointLine = new PointLine({points: curvePoints});
        } else {
            pointLine = new PointLine({points: [p1, p4]});
        }

        //iterate over curve with spacing and callback
        const len = pointLine.getLength();
        let tempSpacing = mix(this.lastSpacing!, spacing, clamp(this.lastDot / len, 0, 1));
        let d = this.lastDot;
        for (; d <= len; d += tempSpacing) {
            tempSpacing = mix(this.lastSpacing!, spacing, clamp(d / len, 0, 1));
            const point = pointLine.getAtDist(d);
            const angle = this.lastCallbackPoint ? pointsToAngleDeg(this.lastCallbackPoint, point) : undefined;
            if (callback) {
                callback({
                    x: point.x,
                    y: point.y,
                    t: d / len,
                    angle: angle,
                    dAngle: this.lastCallbackPoint ? angle! - this.lastAngle! : 0,
                });
            }
            this.lastCallbackPoint = point;
            this.lastAngle = angle;
        }

        if (callback) {
            this.lastDot = d - len;
        } else {
            this.lastDot = 0;
            controlsCallback && controlsCallback({p1: p1, p2: p2, p3: p3, p4: p4});
        }

        this.lastSpacing = spacing;
    }

    addFinal (
        spacing: number,
        callback?: TBezierLineCallback,
        controlsCallback?: TBezierLineControlsCallback,
    ): void {
        if (this.pointArr.length < 2) {
            return;
        }

        const p1 = this.pointArr[this.pointArr.length - 2];
        const p2 = this.pointArr[this.pointArr.length - 1];

        const newP = Vec2.add(p2, Vec2.sub(p2, p1));

        this.add(newP.x, newP.y, spacing, callback, controlsCallback);
    }
}

/**
 * from SplineInterpolator.cs in the Paint.NET source code
 */
export class SplineInterpolator {

    private readonly xa: number[];
    private readonly ya: number[];
    private readonly u: number[];
    private readonly y2: number[];
    private readonly first: number;
    private readonly last: number;

    // ---- public ----
    constructor (
        points: [number, number][],
    ) {
        const n = points.length;
        this.xa = [];
        this.ya = [];
        this.u = [];
        this.y2 = [];
        let i;

        this.first = points[0][0];
        this.last = points[points.length - 1][0];

        points.sort(function (a, b) {
            return a[0] - b[0];
        });
        for (i = 0; i < n; i++) {
            this.xa.push(points[i][0]);
            this.ya.push(points[i][1]);
        }

        this.u[0] = 0;
        this.y2[0] = 0;

        for (i = 1; i < n - 1; ++i) {
            // This is the decomposition loop of the tridiagonal algorithm.
            // y2 and u are used for temporary storage of the decomposed factors.
            const wx = this.xa[i + 1] - this.xa[i - 1];
            const sig = (this.xa[i] - this.xa[i - 1]) / wx;
            const p = sig * this.y2[i - 1] + 2.0;

            this.y2[i] = (sig - 1.0) / p;

            const ddydx =
                (this.ya[i + 1] - this.ya[i]) / (this.xa[i + 1] - this.xa[i]) -
                (this.ya[i] - this.ya[i - 1]) / (this.xa[i] - this.xa[i - 1]);

            this.u[i] = (6.0 * ddydx / wx - sig * this.u[i - 1]) / p;
        }

        this.y2[n - 1] = 0;

        // This is the backsubstitution loop of the tridiagonal algorithm
        for (i = n - 2; i >= 0; --i) {
            this.y2[i] = this.y2[i] * this.y2[i + 1] + this.u[i];
        }
    }

    // ---- interface ----

    getFirstX (): number {
        return this.first;
    }

    getLastX (): number {
        return this.last;
    }

    interpolate (x: number): number {
        const n = this.ya.length;
        let klo = 0;
        let khi = n - 1;

        // We will find the right place in the table by means of
        // bisection. This is optimal if sequential calls to this
        // routine are at random values of x. If sequential calls
        // are in order, and closely spaced, one would do better
        // to store previous values of klo and khi.
        while (khi - klo > 1) {
            const k = (khi + klo) >> 1;

            if (this.xa[k] > x) {
                khi = k;
            } else {
                klo = k;
            }
        }

        const h = this.xa[khi] - this.xa[klo];
        const a = (this.xa[khi] - x) / h;
        const b = (x - this.xa[klo]) / h;

        // Cubic spline polynomial is now evaluated.
        return a * this.ya[klo] + b * this.ya[khi] +
            ((a * a * a - a) * this.y2[klo] + (b * b * b - b) * this.y2[khi]) * (h * h) / 6.0;
    }

    /**
     * find x to y. simply by stepping through. suboptimal, so don't call often.
     * searches in x 0-1 range
     */
    findX (y: number, resolution: number): number | undefined {
        let x;
        let dist: number;
        for (let i = 0; i <= resolution; i++) {
            const tempX = i / resolution;
            const tempY = this.interpolate(tempX);
            if (x === undefined) {
                x = tempX;
                dist = Math.abs(tempY - y);
                continue;
            }

            const tempDist = Math.abs(tempY - y);

            if (tempDist < dist!) {
                x = tempX;
                dist = tempDist;
            } else {
                //distance increasing
                break;
            }
        }
        return x;
    }
}

/**
 * input for a spline, following curve of quadratic function x^2 [0 - 1]
 * returns [[0, startVal], ..., [1, endVal]]
 * @param startVal
 * @param endVal
 * @param stepSize
 */
export function quadraticSplineInput (
    startVal: number,
    endVal: number,
    stepSize: number,
): [number, number][] {
    function round (v: number, dec: number): number {
        return Math.round(v * Math.pow(10, dec)) / Math.pow(10, dec);
    }

    const resultArr: [number, number][] = [];
    for (let i = 0; i <= 1; i += stepSize) {
        resultArr.push([round(i, 4), round(startVal + Math.pow(i, 2) * (endVal - startVal), 4)]);
    }
    return resultArr;
}

repo_name: Doggermelon/klecks

license: mit


code:
import {IBounds, IRect, IVector2D} from '../bb-types';

export function mix (a: number, b: number, f: number): number {
    return a * (1 - f) + b * f;
}

export function dist (ax: number, ay: number, bx: number, by: number): number {
    return Math.sqrt(Math.pow(ax - bx, 2) + Math.pow(ay - by, 2));
}

export function distSquared (ax: number, ay: number, bx: number, by: number): number {
    // faster because no square-root
    return Math.pow(ax - bx, 2) + Math.pow(ay - by, 2);
}

export function lenSquared (x: number, y: number): number {
    // faster because no square-root
    return x * x + y * y;
}

export function pointsToAngleRad (p1: IVector2D, p2: IVector2D): number {
    return Math.atan2(p2.y - p1.y, p2.x - p1.x);
}

export function pointsToAngleDeg (p1: IVector2D, p2: IVector2D): number {
    return pointsToAngleRad(p1, p2) * 180 / Math.PI;
}

export function isInsideRect (p: IVector2D, rect: IRect): boolean {
    return rect.x <= p.x && p.x <= rect.x + rect.width &&
        rect.y <= p.y && p.y <= rect.y + rect.height;
}

export function clamp (num: number, min: number, max: number): number {
    return num < min
        ? min
        : num > max
            ? max
            : num;
}

export function rotate (x: number, y: number, deg: number): IVector2D {
    const theta = deg * (Math.PI / 180);
    const cs = Math.cos(theta);
    const sn = Math.sin(theta);

    return {
        x: x * cs - y * sn,
        y: x * sn + y * cs,
    };
}

export function rotateAround (center: IVector2D, point: IVector2D, deg: number): IVector2D {
    const rot = rotate(point.x - center.x, point.y - center.y, deg);
    rot.x += center.x;
    rot.y += center.y;
    return rot;
}

export function intDxy (remainder: IVector2D, fDx: number, fDy: number): {dX: number; dY: number} {
    remainder.x += fDx;
    remainder.y += fDy;
    const dX = Math.round(remainder.x);
    const dY = Math.round(remainder.y);
    remainder.x -= dX;
    remainder.y -= dY;
    return {
        dX,
        dY,
    };
}

/**
 * return closest even number
 * @param f
 */
export function roundEven (f: number): number {
    if (f % 1 === 0) {
        if (f % 2 === 0) {
            return f;
        }
        return f + 1;
    }
    const above = Math.ceil(f);
    const below = Math.floor(f);
    if (above % 2 === 0) {
        return above;
    } else {
        return below;
    }
}

/**
 * return closest uneven number
 * @param f
 */
export function roundUneven (f: number): number {
    if (f % 1 === 0) {
        if (f % 2 === 0) {
            return f + 1;
        }
        return f;
    }
    const above = Math.ceil(f);
    const below = Math.floor(f);
    if (above % 2 === 1) {
        return above;
    } else {
        return below;
    }
}

/**
 * round number to certain precision.
 * round(1.2345, 2) = 1.23
 * round(1.2345, 0) = 0
 * round(123, -1) = 120
 *
 * @param f
 * @param digits
 */
export function round (f: number, digits: number): number {
    const digitMult = Math.pow(10, digits);
    return Math.round((f/* + Number.EPSILON*/) * digitMult) / digitMult;
}

/**
 * update target so it includes bounds
 * @param target
 * @param bounds
 */
export function updateBounds (target: IBounds | undefined, bounds: IBounds | undefined): IBounds {
    if (!bounds && !target) {
        throw new Error('at least one param needs to be defined');
    }
    if (!bounds) {
        return target!;
    }
    if (!target) {
        target = { x1: bounds.x1, y1: bounds.y1, x2: bounds.x2, y2: bounds.y2 };
    } else {
        target.x1 = Math.min(target.x1, bounds.x1);
        target.y1 = Math.min(target.y1, bounds.y1);
        target.x2 = Math.max(target.x2, bounds.x2);
        target.y2 = Math.max(target.y2, bounds.y2);
    }
    return target;
}

/**
 * determine overlap of bounds with width&height
 */
export function boundsInArea (bounds: IBounds | undefined, width: number, height: number): IBounds | undefined {
    if (!bounds) {
        return undefined;
    }
    const x1 = Math.max(0, bounds.x1);
    const y1 = Math.max(0, bounds.y1);
    const x2 = Math.min(width - 1, bounds.x2);
    const y2 = Math.min(height - 1, bounds.y2);
    if (x1 > x2 || y1 > y2) {
        return undefined;
    }
    return { x1, y1, x2, y2 };
}


code:
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web
// not optimized

type TMatrix4x4 = [
    number, number, number, number,
    number, number, number, number,
    number, number, number, number,
    number, number, number, number,
];

export type TVec4 = [number, number, number, number];

function getIdentity (): TMatrix4x4 {
    return [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1,
    ];
}

// point • matrix
function multiplyMatrixAndPoint (matrix: TMatrix4x4, point: TVec4): TVec4 {
    return [
        (point[0] * matrix[0]) + (point[1] * matrix[4]) + (point[2] * matrix[8]) + (point[3] * matrix[12]), // x
        (point[0] * matrix[1]) + (point[1] * matrix[5]) + (point[2] * matrix[9]) + (point[3] * matrix[13]), // y
        (point[0] * matrix[2]) + (point[1] * matrix[6]) + (point[2] * matrix[10]) + (point[3] * matrix[14]), // z
        (point[0] * matrix[3]) + (point[1] * matrix[7]) + (point[2] * matrix[11]) + (point[3] * matrix[15]), // w
    ];
}

//matrixB • matrixA
function multiplyMatrices (matrixA: TMatrix4x4, matrixB: TMatrix4x4): TMatrix4x4 {

    // Slice the second matrix up into rows
    const row0: TVec4 = [matrixB[0], matrixB[1], matrixB[2], matrixB[3]];
    const row1: TVec4 = [matrixB[4], matrixB[5], matrixB[6], matrixB[7]];
    const row2: TVec4 = [matrixB[8], matrixB[9], matrixB[10], matrixB[11]];
    const row3: TVec4 = [matrixB[12], matrixB[13], matrixB[14], matrixB[15]];

    // Multiply each row by matrixA
    const result0 = multiplyMatrixAndPoint(matrixA, row0);
    const result1 = multiplyMatrixAndPoint(matrixA, row1);
    const result2 = multiplyMatrixAndPoint(matrixA, row2);
    const result3 = multiplyMatrixAndPoint(matrixA, row3);

    // Turn the result rows back into a single matrix
    return [
        result0[0], result0[1], result0[2], result0[3],
        result1[0], result1[1], result1[2], result1[3],
        result2[0], result2[1], result2[2], result2[3],
        result3[0], result3[1], result3[2], result3[3],
    ];
}

function createTranslationMatrix (x: number, y: number): TMatrix4x4 {
    return [
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        x, y, 0, 1,
    ];
}

function createRotationMatrix (angleRad: number): TMatrix4x4 {
    //let angleRad = angleDeg / 360 * 2 * Math.PI;
    return [
        Math.cos(-angleRad), -Math.sin(-angleRad), 0, 0,
        Math.sin(-angleRad), Math.cos(-angleRad), 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1,
    ];
}

function createScaleMatrix (f: number): TMatrix4x4 {
    return [
        f, 0, 0, 0,
        0, f, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, 1,
    ];
}

export const Matrix = Object.freeze({
    getIdentity,
    multiplyMatrixAndPoint,
    multiplyMatrices,
    createTranslationMatrix,
    createRotationMatrix,
    createScaleMatrix,
});


code:
import {IVector2D} from '../bb-types';

export const Vec2 = {
    add: function (p1: IVector2D, p2: IVector2D): IVector2D {
        return {x: p1.x + p2.x, y: p1.y + p2.y};
    },
    sub: function (p1: IVector2D, p2: IVector2D): IVector2D {
        return {x: p1.x - p2.x, y: p1.y - p2.y};
    },
    nor: function (p: IVector2D): IVector2D {
        const len = Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2));
        return {x: p.x / len, y: p.y / len};
    },
    len: function (p: IVector2D): number {
        return Math.sqrt(Math.pow(p.x, 2) + Math.pow(p.y, 2));
    },
    dist: function (p1: IVector2D, p2: IVector2D): number {
        return Vec2.len(Vec2.sub(p1, p2));
    },
    mul: function (p: IVector2D, s: number): IVector2D {
        return {x: p.x * s, y: p.y * s};
    },
    angle: function (p1: IVector2D, p2: IVector2D): number {
        return Math.atan2(p2.y - p1.y, p2.x - p1.x);
    },
    dot: function (a: IVector2D, b: IVector2D): number {
        const aArr = [a.x, a.y];
        const bArr = [b.x, b.y];
        return aArr.map((x, i) => aArr[i] * bArr[i]).reduce((m, n) => m + n);
    },
};


code:
import {Embed, IEmbedParams, IReadPSD} from '../../main-embed';
import {IKlProject} from '../../klecks/kl-types';
import logoImg from '/src/app/img/klecks-logo.png';
import {getEmbedUrl} from './get-embed-url';
import {initLANG, LANG} from '../../language/language';
import {theme} from '../../theme/theme';
import {loadAgPsd} from '../../klecks/storage/load-ag-psd';

// only one instance can exist
let wrapperCreated = false;

// add missing props. modifies project object
function processProject (project: IKlProject | undefined): void {
    if (!project) {
        return;
    }
    project.layers.forEach(layer => {
        layer.isVisible = layer.isVisible === undefined ? true : layer.isVisible;
    });
}

/**
 * Starting point for the Embed. Quickly available due to lazy loading.
 * (Which is why it had to be separated from main-embed.ts > Embed)
 *
 * Lazy loads rest of library, shows a loading screen, exposes Embed interface
 */
export class EmbedWrapper {

    private project: IKlProject | undefined;
    private errorStr: string | undefined;
    private psds: IReadPSD[] = [];// if instance not loaded yet, these are psds to be read

    private instance: Embed | undefined; // instance of loaded Embed

    // ------ public ------------------
    constructor (p: IEmbedParams) {
        if (wrapperCreated) {
            throw new Error('Already created an embed');
        }
        wrapperCreated = true;

        processProject(p.project);
        p = {
            ...p,
            embedUrl: p.embedUrl ? p.embedUrl : getEmbedUrl(),
        };

        (async () => {
            await initLANG();

            // loading screen
            const loadingScreen = document.createElement('div');
            const loadingStyleArr = [
                ['position', 'fixed'],
                ['left', '0'],
                ['top', '0'],
                ['width', '100vw'],
                ['height', '100vh'],

                ['display', 'flex'],
                ['alignItems', 'center'],
                ['justifyContent', 'center'],
                ['flexDirection', 'column'],

                ['background', theme.isDark() ? 'rgb(33, 33, 33)' : 'rgb(158,158,158)'],

                ['fontFamily', 'Arial, sans-serif'],
                ['fontSize', '30px'],
                ['color', '#e3e3e3'],
            ];
            for (let i = 0; i < loadingStyleArr.length; i++) {
                loadingScreen.style[loadingStyleArr[i][0] as any] = loadingStyleArr[i][1];
            }
            loadingScreen.id = 'loading-screen';
            const logoStyle = theme.isDark() && !p.logoImg ? ' style="filter: invert(1)"' : '';
            loadingScreen.innerHTML = '\n' +
                '
\n' +
                '

\n' +
                '' + LANG('embed-init-loading') + '' +
                '
';
            document.body.appendChild(loadingScreen);

            const mainEmbed = await import('../../main-embed');
            this.instance = new mainEmbed.Embed(p);

            this.getPNG = () => this.instance!.getPNG();
            this.getPSD = () => this.instance!.getPSD();

            if (this.project) {
                this.instance.openProject(this.project);
            }
            if (this.errorStr) {
                this.instance.initError(this.errorStr);
            }
            if (this.psds.length) {
                this.instance.readPSDs(this.psds);
            }
        })();

        // needed for uploading. load here to prevent possible timeouts due to server cold-start
        loadAgPsd();
    }

    openProject (project: IKlProject) {
        processProject(project);
        if (this.instance) {
            this.instance.openProject(project);
        } else {
            if (this.project) {
                throw new Error('Already called openProject');
            }
            this.project = project;
        }
    }

    initError (error: string) {
        if (this.instance) {
            this.instance.initError(error);
        } else {
            this.errorStr = error;
        }
    }

    async readPSD (blob: Blob)  {
        return new Promise((resolve, reject) => {
            const item: IReadPSD = {
                blob,
                callback: (loadedProject: IKlProject | null) => {
                    this.psds.splice(this.psds.indexOf(item), 1);
                    if (loadedProject) {
                        resolve(loadedProject);
                    } else {
                        reject();
                    }
                },
            };
            if (this.instance) {
                this.instance.readPSDs([item]);
            } else {
                this.psds.push(item);
            }
        });
    }

    getPNG: (() => Blob) | undefined = undefined;
    getPSD: (() => Promise<Blob>) | undefined = undefined;
}


code:
let embedUrl: string;

function getBaseURL (url: string): string {
    return ('' + url).replace(/^((?:https?|file|ftp):\/\/.+)\/[^/]+$/, '$1') + '/';
}

export function getEmbedUrl (): string {
    if (embedUrl) {
        return embedUrl;
    }

    let match: string[];
    try {
        throw new Error();
    } catch (e) {
        if (e instanceof Error) {
            match = ('' + e.stack).match(/(https?|file|ftp):\/\/[^)\n]+/g)!;
        }
    }
    let index = 0;
    match!.forEach((item, i) => {
        if (item.indexOf('embed.js') !== -1) {
            index = i;
        }
    });
    embedUrl = getBaseURL(match![index]);
    return embedUrl;
}


code:
import {BB} from '../bb/bb';
import uiSwapImg from '/src/app/img/ui/ui-swap-lr.svg';
import helpImg from '/src/app/img/ui/help.svg';
import {LANG} from '../language/language';
import {PointerListener} from '../bb/input/pointer-listener';

/**
 * Topmost row of buttons in toolspace. (embed)
 *
 * @param p
 * @constructor
 */
export class EmbedToolspaceTopRow {

    private readonly rootEl: HTMLElement;

    // ---- public ----

    constructor (p: {onSubmit: () => void; onLeftRight: () => void; onHelp: () => void}) {
        this.rootEl = BB.el({
            className: 'kl-toolspace-row',
            css: {
                height: '36px',
                display: 'flex',
            },
        });

        function createButton (p: {
            title: string;
            content?: HTMLElement;
            image?: string;
            onClick: () => void;
            extraPadding?: number;
            contain: boolean;
        }): {
            el: HTMLElement;
            pointerListener: PointerListener;
        } {
            const padding = 6 + (p.extraPadding ? p.extraPadding : 0);
            const el = BB.el({
                className: 'toolspace-row-button nohighlight',
                title: p.title,
                onClick: p.onClick,
                css: {
                    padding: p.content ? '' : (p.contain ? padding + 'px 0' : ''),
                },
            });
            if (p.content) {
                el.append(p.content);
            } else {
                const im = BB.el({
                    className: 'dark-invert',
                    css: {
                        backgroundImage: 'url(\'' + p.image + '\')',
                        backgroundRepeat: 'no-repeat',
                        backgroundPosition: 'center',
                        backgroundSize: p.contain ? 'contain' : '',
                        //filter: 'grayscale(1)',
                        height: '100%',
                    },
                });
                im.style.pointerEvents = 'none';
                el.append(im);
            }
            const pointerListener = new BB.PointerListener({ // because :hover causes problems w touch
                target: el,
                onEnterLeave: function (isOver): void {
                    el.classList.toggle('toolspace-row-button-hover', isOver);
                },
            });

            return {
                el,
                pointerListener,
            };
        }

        const submitButton = createButton({
            onClick: p.onSubmit,
            title: LANG('submit-title'),
            content: BB.el({
                content: LANG('submit'),
                className: 'toolspace-row-button__submit',
                css: {
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    width: '100%',
                    height: '100%',
                },
            }),
            contain: true,
        });
        submitButton.el.style.width = '45px';

        const helpButton = createButton({
            onClick: p.onHelp,
            title: LANG('help'),
            image: helpImg,
            contain: true,
        });

        const leftRightButton = createButton({
            onClick: p.onLeftRight,
            title: LANG('switch-ui-left-right'),
            image: uiSwapImg,
            contain: true,
        });

        this.rootEl.append(submitButton.el, leftRightButton.el, helpButton.el);
    }

    getElement (): HTMLElement {
        return this.rootEl;
    }
}


code:
import {gl} from './gl';
import {TUniforms} from '../fx-canvas-types';
import {BB} from '../../bb/bb';

// VERTEX_SHADER | FRAGMENT_SHADER
type TShaderType = GLenum;

const defaultVertexSource = '\
    attribute vec2 vertex;\
    attribute vec2 _texCoord;\
    varying vec2 texCoord;\
    void main() {\
        texCoord = _texCoord;\
        gl_Position = vec4(vertex * 2.0 - 1.0, 0.0, 1.0);\
    }';

const defaultFragmentSource = '\
    uniform sampler2D texture;\
    varying vec2 texCoord;\
    void main() {\
        gl_FragColor = texture2D(texture, texCoord);\
    }';

function isArray (obj: unknown): obj is unknown[] {
    return Object.prototype.toString.call(obj) == '[object Array]';
}

function isNumber (obj: unknown): obj is number {
    return Object.prototype.toString.call(obj) == '[object Number]';
}

let floatPrecision: 'lowp' | 'mediump' | 'highp' | undefined;

export class FxShader {

    // ---- static ----
    static getDefaultShader (): FxShader {
        gl.defaultShader = gl.defaultShader || new FxShader();
        return gl.defaultShader;
    }

    // ---- private ----

    private compileSource (type: TShaderType, source: string, nameStr: string): WebGLShader {
        const shader = BB.throwIfNull(gl.createShader(type));
        gl.shaderSource(shader, source.replace(/#define.*/, '')); // glslify adds a line add the beginning that breaks it
        gl.compileShader(shader);
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            throw 'compile error: ' + nameStr + ' - ' + gl.getShaderInfoLog(shader);
        }
        return shader;
    }

    /**
     * HIGH_FLOAT | MEDIUM_FLOAT
     */
    private testPrecisionSupport (precisionType: GLenum): boolean {
        const format = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, precisionType);
        return format !== null && format.precision !== 0;
    }

    // ---- public ----
    constructor (
        vertexSource?: string | null,
        fragmentSource?: string | null,
        nameStr?: string,
    ) {
        this.vertexAttribute = null;
        this.texCoordAttribute = null;
        this.program = BB.throwIfNull(gl.createProgram());
        vertexSource = vertexSource || defaultVertexSource;
        fragmentSource = fragmentSource || defaultFragmentSource;
        if (!floatPrecision) {
            floatPrecision = this.testPrecisionSupport(gl.HIGH_FLOAT) ? 'highp' : (this.testPrecisionSupport(gl.MEDIUM_FLOAT) ? 'mediump' : 'lowp');
        }
        fragmentSource = 'precision ' + floatPrecision + ' float;' + fragmentSource; // annoying requirement is annoying
        gl.attachShader(this.program, this.compileSource(gl.VERTEX_SHADER, vertexSource, nameStr + '(vertex)'));
        gl.attachShader(this.program, this.compileSource(gl.FRAGMENT_SHADER, fragmentSource, nameStr + '(fragment)'));
        gl.linkProgram(this.program);
        if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
            throw 'link error: ' + gl.getProgramInfoLog(this.program);
        }
    }

    // ---- interface ----

    vertexAttribute: null | number;
    texCoordAttribute: null | number;
    program: WebGLProgram | null; // null = destroyed

    destroy (): void {
        gl.deleteProgram(this.program);
        this.program = null;
    }

    uniforms (uniforms: TUniforms<number | number[]>): FxShader {
        gl.useProgram(this.program);
        Object.entries(uniforms).forEach(([name, value]) => {

            const location = gl.getUniformLocation(this.program!, name);
            if (location === null) {
                // will be null if the uniform isn't used in the shader
                return;
            }

            if (isArray(value)) {
                switch (value.length) {
                    case 1:
                        gl.uniform1fv(location, new Float32Array(value));
                        break;
                    case 2:
                        gl.uniform2fv(location, new Float32Array(value));
                        break;
                    case 3:
                        gl.uniform3fv(location, new Float32Array(value));
                        break;
                    case 4:
                        gl.uniform4fv(location, new Float32Array(value));
                        break;
                    case 9:
                        gl.uniformMatrix3fv(location, false, new Float32Array(value));
                        break;
                    case 16:
                        gl.uniformMatrix4fv(location, false, new Float32Array(value));
                        break;
                    default:
                        throw 'dont\'t know how to load uniform "' + name + '" of length ' + value.length;
                }
            } else if (isNumber(value)) {
                gl.uniform1f(location, value);
            } else {
                throw 'attempted to set uniform "' + name + '" to invalid value ' + ((value as any) || 'undefined').toString();
            }
        });
        // allow chaining
        return this;
    }

    //
    /**
     * textures are uniforms too but for some reason can't be specified by gl.uniform1f,
     * even though floating point numbers represent the integers 0 through 7 exactly
     */
    textures (textures: Record<string, GLint>): FxShader {
        gl.useProgram(this.program);

        Object.entries(textures).forEach(([name, value]) => {
            gl.uniform1i(gl.getUniformLocation(this.program!, name), value);
        });

        // allow chaining
        return this;
    }

   drawRect (left?: number, top?: number, right?: number, bottom?: number): void {
        let undefined;
        const viewport = gl.getParameter(gl.VIEWPORT);
        top = top !== undefined ? (top - viewport[1]) / viewport[3] : 0;
        left = left !== undefined ? (left - viewport[0]) / viewport[2] : 0;
        right = right !== undefined ? (right - viewport[0]) / viewport[2] : 1;
        bottom = bottom !== undefined ? (bottom - viewport[1]) / viewport[3] : 1;
        if (gl.vertexBuffer === undefined || gl.vertexBuffer === null) {
            gl.vertexBuffer = BB.throwIfNull(gl.createBuffer());
        }
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.vertexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([left, top, left, bottom, right, top, right, bottom]), gl.STATIC_DRAW);
        if (gl.texCoordBuffer == null) {
            gl.texCoordBuffer = BB.throwIfNull(gl.createBuffer());
            gl.bindBuffer(gl.ARRAY_BUFFER, gl.texCoordBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 0, 1, 1, 0, 1, 1]), gl.STATIC_DRAW);
        }
        if (this.vertexAttribute == null) {
            this.vertexAttribute = gl.getAttribLocation(this.program!, 'vertex');
            gl.enableVertexAttribArray(this.vertexAttribute);
        }
        if (this.texCoordAttribute == null) {
            this.texCoordAttribute = gl.getAttribLocation(this.program!, '_texCoord');
            gl.enableVertexAttribArray(this.texCoordAttribute);
        }
        gl.useProgram(this.program);
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.vertexBuffer);
        gl.vertexAttribPointer(this.vertexAttribute, 2, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, gl.texCoordBuffer);
        gl.vertexAttribPointer(this.texCoordAttribute, 2, gl.FLOAT, false, 0, 0);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

}


code:
import {gl} from './gl';
import {BB} from '../../bb/bb';
import {TFxGl, TFxSupportedElements} from '../fx-canvas-types';

/**
 * Format           Type                    Channels    Bytes per pixel
 * RGBA             UNSIGNED_BYTE           4           4
 * RGB              UNSIGNED_BYTE           3           3
 * RGBA             UNSIGNED_SHORT_4_4_4_4  4           2
 * RGBA             UNSIGNED_SHORT_5_5_5_1  4           2
 * RGB              UNSIGNED_SHORT_5_6_5    3           2
 * LUMINANCE_ALPHA  UNSIGNED_BYTE           2           2
 * LUMINANCE        UNSIGNED_BYTE           1           1
 * ALPHA            UNSIGNED_BYTE           1           1
 *
 * https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html
 */
export type TTextureFormat = GLenum;
export type TTextureType = GLenum;

export class FxTexture {

    // ---- static ----
    static fromElement (element: TFxSupportedElements): FxTexture {
        const texture = new FxTexture(0, 0, gl.RGBA, gl.UNSIGNED_BYTE);
        texture.loadContentsOf(element);
        return texture;
    }

    // ---- private ----
    private canvas: HTMLCanvasElement | null;
    private type: TTextureType;

    /*
    // never seen this being used
    private getCanvas(texture: Texture): CanvasRenderingContext2D {
        if (this.canvas == null) {
            this.canvas = BB.canvas(texture.width, texture.height);
        }
        this.canvas.width = texture.width;
        this.canvas.height = texture.height;
        const c = BB.ctx(this.canvas);
        c.clearRect(0, 0, this.canvas.width, this.canvas.height);
        return c;
    }*/

    // ---- public ----
    constructor (
        width: number,
        height: number,
        format: TTextureFormat,
        type: TTextureType,
    ) {
        this.gl = gl;
        this.id = BB.throwIfNull(gl.createTexture());
        this.width = width;
        this.height = height;
        this.format = format;
        this.type = type;
        this.canvas = null;

        gl.bindTexture(gl.TEXTURE_2D, this.id);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        if (width && height) {
            gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, this.type, null);
        }
    }

    // ---- interface ----

    gl: TFxGl;
    width: number;
    height: number;
    id: WebGLTexture | null; // null -> destroyed
    format: TTextureFormat;

    loadContentsOf (element: TFxSupportedElements): void {
        this.width = element.width || (element as HTMLVideoElement).videoWidth;
        this.height = element.height || (element as HTMLVideoElement).videoHeight!;
        gl.bindTexture(gl.TEXTURE_2D, this.id);
        gl.texImage2D(gl.TEXTURE_2D, 0, this.format, this.format, this.type, element);
    }

    initFromBytes (width: number, height: number, data: number[]): void {
        this.width = width;
        this.height = height;
        this.format = gl.RGBA;
        this.type = gl.UNSIGNED_BYTE;
        gl.bindTexture(gl.TEXTURE_2D, this.id);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, this.type, new Uint8Array(data));
    }

    destroy (): void {
        gl.deleteTexture(this.id);
        this.id = null;
    }

    use (unit?: number): void {
        gl.activeTexture(gl.TEXTURE0 + (unit || 0));
        gl.bindTexture(gl.TEXTURE_2D, this.id);
    }

    unuse (unit: number): void {
        gl.activeTexture(gl.TEXTURE0 + (unit || 0));
        gl.bindTexture(gl.TEXTURE_2D, null);
    }

    ensureFormat (width: number, height: number, format: TTextureFormat, type: TTextureType): void {
        // change the format only if required
        if (width != this.width || height != this.height || format != this.format || type != this.type) {
            this.width = width;
            this.height = height;
            this.format = format;
            this.type = type;
            gl.bindTexture(gl.TEXTURE_2D, this.id);
            gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, this.type, null);
        }
    }

    ensureFormatViaTexture (texture: FxTexture): void {
        this.ensureFormat(texture.width, texture.height, texture.format, texture.type);
    }

    drawTo (callback: () => void): void {
        // start rendering to this texture
        gl.framebuffer = gl.framebuffer || gl.createFramebuffer();
        gl.bindFramebuffer(gl.FRAMEBUFFER, gl.framebuffer);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.id, 0);
        if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
            throw new Error('incomplete framebuffer');
        }
        gl.viewport(0, 0, this.width, this.height);

        // do the drawing
        callback();

        // stop rendering to this texture
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
    }

    /*
    // never seen this being used
    fillUsingCanvas (callback: (canvas: CanvasRenderingContext2D) => void): Texture {
        callback(this.getCanvas(this));
        this.format = gl.RGBA;
        this.type = gl.UNSIGNED_BYTE;
        gl.bindTexture(gl.TEXTURE_2D, this.id);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas);
        return this;
    }

    // never seen this being used
    toImage (image: HTMLImageElement): void {
        this.use();
        Shader.getDefaultShader().drawRect();
        const size = this.width * this.height * 4;
        const pixels = new Uint8Array(size);
        const c = this.getCanvas(this);
        const data = c.createImageData(this.width, this.height);
        gl.readPixels(0, 0, this.width, this.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
        for (let i = 0; i < size; i++) {
            data.data[i] = pixels[i];
        }
        c.putImageData(data, 0, 0);
        image.src = this.canvas.toDataURL();
    }
     */

    swapWith (other: FxTexture): void {
        let temp;

        temp = other.id;
        other.id = this.id;
        this.id = temp;

        temp = other.width;
        other.width = this.width;
        this.width = temp;

        temp = other.height;
        other.height = this.height;
        this.height = temp;

        temp = other.format;
        other.format = this.format;
        this.format = temp;

        // type?
    }
}


code:
import {TFxGl} from '../fx-canvas-types';

export let gl: TFxGl;

export function setGl (newGl: TFxGl | null): void {
    // more problems setting type to null
    gl = newGl as TFxGl;
}


code:
import {TFxCanvas, TUniforms} from '../fx-canvas-types';
import {FxTexture} from './fx-texture';
import {FxShader} from './fx-shader';

export function simpleShader (
    this: TFxCanvas,
    shader: FxShader,
    uniforms: TUniforms<number | number[]>,
    textureIn?: FxTexture,
    textureOut?: FxTexture
): void {
    (textureIn || this._.texture).use();
    this._.spareTexture.drawTo(function () {
        shader.uniforms(uniforms).drawRect();
    });
    this._.spareTexture.swapWith(textureOut || this._.texture);
}


code:
import {BB} from '../../bb/bb';
import {simpleShader} from '../core/simple-shader';
import {FxShader} from '../core/fx-shader';
import {gl} from '../core/gl';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * @type Brightness / Contrast
 * Provides additive brightness and multiplicative contrast control.
 * @param brightness -1 to 1 (-1 is solid black, 0 is no change, and 1 is solid white)
 * @param contrast   -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
 */
export type TFilterBrightnessContrast = (
    this: TFxCanvas,
    brightness: number,
    contrast: number
) => TFxCanvas;

export const brightnessContrast: TFilterBrightnessContrast = function (brightness, contrast) {
    gl.brightnessContrast = gl.brightnessContrast || new FxShader(null, '\
        uniform sampler2D texture;\
        uniform float brightness;\
        uniform float contrast;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            color.rgb += brightness;\
            if (contrast > 0.0) {\
                color.rgb = (color.rgb - 0.5) / (1.0 - contrast) + 0.5;\
            } else {\
                color.rgb = (color.rgb - 0.5) * (1.0 + contrast) + 0.5;\
            }\
            gl_FragColor = color;\
        }\
    ', 'brightnessContrast');

    simpleShader.call(this, gl.brightnessContrast, {
        brightness: BB.clamp(brightness, -1, 1),
        contrast: BB.clamp(contrast, -1, 1),
    });

    return this;
};


code:
import {splineInterpolate} from '../math/spline-interpolate';
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Curves
 * @description A powerful mapping tool that transforms the colors in the image
 *              by an arbitrary function. The function is interpolated between
 *              a set of 2D points using splines. The curves filter can take
 *              either one or three arguments which will apply the mapping to
 *              either luminance or RGB values, respectively.
 * @param red   A list of points that define the function for the red channel.
 *              Each point is a list of two values: the value before the mapping
 *              and the value after the mapping, both in the range 0 to 1. For
 *              example, [[0,1], [1,0]] would invert the red channel while
 *              [[0,0], [1,1]] would leave the red channel unchanged.
 * @param green A list of points that define the function for the green
 *              channel (just like for red).
 * @param blue  list of points that define the function for the blue
 *              channel (just like for red).
 */
export type TFilterCurves = (
    this: TFxCanvas,
    red: [number, number][],
    green: [number, number][],
    blue: [number, number][],
) => TFxCanvas;

export const curves: TFilterCurves = function (red, green, blue) {
    // Create the ramp texture
    const redRamp = splineInterpolate(red);
    const greenRamp = splineInterpolate(green);
    const blueRamp = splineInterpolate(blue);
    const array: number[] = [];
    for (let i = 0; i < 256; i++) {
        array.splice(array.length, 0, redRamp[i], greenRamp[i], blueRamp[i], 255);
    }
    this._.extraTexture.initFromBytes(256, 1, array);
    this._.extraTexture.use(1);

    gl.curves = gl.curves || new FxShader(null, '\
        uniform sampler2D texture;\
        uniform sampler2D map;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            color.r = texture2D(map, vec2(color.r)).r;\
            color.g = texture2D(map, vec2(color.g)).g;\
            color.b = texture2D(map, vec2(color.b)).b;\
            gl_FragColor = color;\
        }\
    ', 'curves');

    gl.curves.textures({
        map: 1,
    });
    simpleShader.call(this, gl.curves, {});

    return this;
};


code:
import {gl} from '../core/gl';
import {warpShader} from '../shaders/warp-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

export type TFilterDistortSettings = {
    stepSize: number; // [1, inf]
    distortType: 0 | 1 | 2;
    scale: { x: number; y: number };
    strength: { x: number; y: number };
    phase: { x: number; y: number };
};

/**
 * Distort
 * Distorts image (moves pixels around)
 */
export type TFilterDistort = (
    this: TFxCanvas,
    settings: TFilterDistortSettings,
) => TFxCanvas;

/**
 * @filter        Distort
 * @description   Distorts image (moves pixels around)
 */
export const distort: TFilterDistort = function (settings) {
    gl.distort = gl.distort || warpShader('\
        uniform float stepSize;\
        uniform vec2 scale;\
        uniform vec2 strength;\
        uniform vec2 phase;\
        uniform float type;\
        ', '\
        const float PI = 3.14159265;\
        float x = coord.x;\
        float y = coord.y;\
        if (stepSize > 1.0) {\
            x = floor(x / stepSize) * stepSize;\
            y = floor(y / stepSize) * stepSize;\
        }\
        float distortX = sin((x/scale.x + phase.x) * PI * 2.0) * strength.x;\
        float distortY = sin((y/scale.y + phase.y) * PI * 2.0) * strength.y;\
        if (type == 0.0) {\
            coord.y += distortX;\
            coord.x += distortY;\
        } else if (type == 1.0) {\
            coord.x += distortX;\
            coord.y += distortY;\
        } else if (type == 2.0) {\
            gl_FragColor = texture2D(texture, vec2(x, y) / texSize);\
            coord.y += sin(gl_FragColor.r/scale.x*200.0 + phase.x * PI * 2.0) * strength.x;\
            coord.x += cos(gl_FragColor.g/scale.y*200.0 + phase.y * PI * 2.0) * strength.y;\
        }\
        coord.x = mod(coord.x, texSize.x);\
        coord.y = mod(coord.y, texSize.y);\
        \
        ');

    simpleShader.call(this, gl.distort, {
        stepSize: settings.stepSize,
        type: settings.distortType,
        scale: [settings.scale.x, settings.scale.y],
        strength: [settings.strength.x, settings.strength.y],
        phase: [settings.phase.x, settings.phase.y],
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {BB} from '../../bb/bb';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Hue / Saturation
 * @description      Provides rotational hue and multiplicative saturation control. RGB color space
 *                   can be imagined as a cube where the axes are the red, green, and blue color
 *                   values. Hue changing works by rotating the color vector around the grayscale
 *                   line, which is the straight line from black (0, 0, 0) to white (1, 1, 1).
 *                   Saturation is implemented by scaling all color channel values either toward
 *                   or away from the average color channel value.
 * @param hue        -1 to 1 (-1 is 180 degree rotation in the negative direction, 0 is no change,
 *                   and 1 is 180 degree rotation in the positive direction)
 * @param saturation -1 to 1 (-1 is solid gray, 0 is no change, and 1 is maximum contrast)
 */
export type TFilterHueSaturation = (
    this: TFxCanvas,
    hue: number,
    saturation: number,
) => TFxCanvas;

export const hueSaturation: TFilterHueSaturation = function (hue, saturation) {
    gl.hueSaturation = gl.hueSaturation || new FxShader(null, '\
        uniform sampler2D texture;\
        uniform float hue;\
        uniform float saturation;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            \
            /* hue adjustment, wolfram alpha: RotationTransform[angle, {1, 1, 1}][{x, y, z}] */\
            float angle = hue * 3.14159265;\
            float s = sin(angle), c = cos(angle);\
            vec3 weights = (vec3(2.0 * c, -sqrt(3.0) * s - c, sqrt(3.0) * s - c) + 1.0) / 3.0;\
            float len = length(color.rgb);\
            color.rgb = vec3(\
                dot(color.rgb, weights.xyz),\
                dot(color.rgb, weights.zxy),\
                dot(color.rgb, weights.yzx)\
            );\
            \
            /* saturation adjustment */\
            float average = (color.r + color.g + color.b) / 3.0;\
            if (saturation > 0.0) {\
                color.rgb += (average - color.rgb) * (1.0 - 1.0 / (1.001 - saturation));\
            } else {\
                color.rgb += (average - color.rgb) * (-saturation);\
            }\
            \
            gl_FragColor = color;\
        }\
    ', 'hueSaturation');

    simpleShader.call(this, gl.hueSaturation, {
        hue: BB.clamp(hue, -1, 1),
        saturation: BB.clamp(saturation, -1, 1),
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Invert
 * @description   Inverts color of each pixel
 */
export type TFilterInvert = (
    this: TFxCanvas,
) => TFxCanvas;

export const invert: TFilterInvert = function () {
    gl.invert = gl.invert || new FxShader(null, '\
    uniform sampler2D texture;\
    uniform vec2 texSize;\
    varying vec2 texCoord;\
    \
    void main() {\
        vec4 color = texture2D(texture, texCoord);\
        color.rgb = 1.0 - color.rgb;\
        gl_FragColor = color;\
    }\
', 'invert');

    simpleShader.call(this, gl.invert, {
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {warpShader} from '../shaders/warp-shader';
import {simpleShader} from '../core/simple-shader';
import {getInverse} from '../math/matrix';
import {TFxCanvas, TMat2x2, TMat3x3, TMatDeep3x3} from '../fx-canvas-types';

/**
 * Matrix Warp
 * @description           Transforms an image by a 2x2 or 3x3 matrix. The coordinates used in
 *                        the transformation are (x, y) for a 2x2 matrix or (x, y, 1) for a
 *                        3x3 matrix, where x and y are in units of pixels.
 * @param matrix          A 2x2 or 3x3 matrix represented as either a list or a list of lists.
 *                        For example, the 3x3 matrix [[2,0,0],[0,3,0],[0,0,1]] can also be
 *                        represented as [2,0,0,0,3,0,0,0,1] or just [2,0,0,3].
 * @param inverse         A boolean value that, when true, applies the inverse transformation
 *                        instead. (optional, defaults to false)
 * @param useTextureSpace A boolean value that, when true, uses texture-space coordinates
 *                        instead of screen-space coordinates. Texture-space coordinates range
 *                        from -1 to 1 instead of 0 to width - 1 or height - 1, and are easier
 *                        to use for simple operations like flipping and rotating.
 */
export type TFilterMatrixWarp = (
    this: TFxCanvas,
    matrix: TMat2x2 | TMat3x3 | TMatDeep3x3,
    inverse?: boolean,
    useTextureSpace?: boolean,
) => TFxCanvas;

export const matrixWarp: TFilterMatrixWarp = function (matrix, inverse, useTextureSpace) {
    gl.matrixWarp = gl.matrixWarp || warpShader('\
        uniform mat3 matrix;\
        uniform bool useTextureSpace;\
    ', '\
        if (useTextureSpace) coord = coord / texSize * 2.0 - 1.0;\
        vec3 warp = matrix * vec3(coord, 1.0);\
        coord = warp.xy / warp.z;\
        if (useTextureSpace) coord = (coord * 0.5 + 0.5) * texSize;\
    ');

    // Flatten all members of matrix into one big list
    matrix = matrix.flat() as (TMat2x2 | TMat3x3);

    // Extract a 3x3 matrix out of the arguments
    if (matrix.length == 4) {
        matrix = [
            matrix[0], matrix[1], 0,
            matrix[2], matrix[3], 0,
            0, 0, 1,
        ];
    } else if (matrix.length != 9) {
        throw 'can only warp with 2x2 or 3x3 matrix';
    }

    simpleShader.call(this, gl.matrixWarp, {
        matrix: inverse ? getInverse(matrix) : matrix,
        texSize: [this.width, this.height],
        useTextureSpace: useTextureSpace ? 1 : 0,
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * multiplyAlpha
 * applies alpha multiply
 */
export type TFilterMultiplyAlpha = (
    this: TFxCanvas,
) => TFxCanvas;

export const multiplyAlpha: TFilterMultiplyAlpha = function () {
    gl.multiplyAlpha = gl.multiplyAlpha || new FxShader(null, '\
    uniform sampler2D texture;\
    uniform vec2 texSize;\
    varying vec2 texCoord;\
    \
    void main() {\
        vec4 color = texture2D(texture, texCoord);\
        color.rgb *= color.a;\
        gl_FragColor = color;\
    }\
', 'multiplyAlpha');

    simpleShader.call(this, gl.multiplyAlpha, {
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import shaderNoise from '../shaders/shader-noise.glsl';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Noise
 * draws noise, completely replaces original image
 */
export type TFilterNoise = (
    this: TFxCanvas,
    seed: number | undefined, // float, any value okay - default 0
    type: number, // 1 value, 2 simplex, 3 cellular
    scale: [number, number], // float, small scale will show repeating patterns
    offset: [number, number], // offset x, y - large offsets will show repeating patterns
    octaves: number, // int 0 - 4
    samples: number, // 1 | 4 | 8 -> super-sampling
    peaks: number, // how often it will ramp *additionally* from 0 to 1
    brightness: number, // range [-1, 1]
    contrast: number, // range [-1, 1]
    isReversed: boolean, // reverse 0-1 range
    colA: {r: number; g: number; b: number} | undefined, // which color at 0, if rgb - default black
    colB: {r: number; g: number; b: number} | undefined, // which color at 1, if rgb - default white
    channels: 'rgb' | 'alpha', // which channel to target
) => TFxCanvas;

export const noise: TFilterNoise = function (
    seed,
    type,
    scale,
    offset,
    octaves,
    samples,
    peaks,
    brightness,
    contrast,
    isReversed,
    colA,
    colB,
    channels,
) {
    gl.noise = gl.noise || new FxShader(null, shaderNoise.replace(/#define.*/, ''), 'noise');
    simpleShader.call(this, gl.noise, {
        seed: seed || 0,
        type,
        scale: [scale[0], scale[1]],
        offset,
        octaves,
        samples,
        texSize: [this.width, this.height],
        peaks,
        brightness,
        contrast,
        isReversed: isReversed ? 1.0 : 0.0,
        colA: colA ? [colA.r / 255, colA.g / 255, colA.b / 255] : [0, 0, 0],
        colB: colB ? [colB.r / 255, colB.g / 255, colB.b / 255] : [1, 1, 1],
        channels: channels === 'rgb' ? 0 : 1,
    });

    return this;
};


code:
import {getInverse, getSquareToQuad, multiply} from '../math/matrix';
import {TFxCanvas} from '../fx-canvas-types';

export type TRectanglePoints = [
    number, number, // point a
    number, number, // point b
    number, number, // point c
    number, number, // point d
];

/**
 * Perspective
 * @description  Warps one quadrangle to another with a perspective transform. This can be used to
 *               make a 2D image look 3D or to recover a 2D image captured in a 3D environment.
 * @param before The x and y coordinates of four points before the transform in a flat list. This
 *               would look like [ax, ay, bx, by, cx, cy, dx, dy] for four points (ax, ay), (bx, by),
 *               (cx, cy), and (dx, dy).
 * @param after  The x and y coordinates of four points after the transform in a flat list, just
 *               like the other argument.
 */
export type TFilterPerspective = (
    this: TFxCanvas,
    before: TRectanglePoints,
    after: TRectanglePoints,
) => TFxCanvas;

export const perspective: TFilterPerspective = function (before, after) {
    const a = getSquareToQuad(...after);
    const b = getSquareToQuad(...before);
    const c = multiply(getInverse(a), b);
    return this.matrixWarp(c);
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {randomShaderFunc} from '../shaders/random-shader-func';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Tilt Shift
 * @description          Simulates the shallow depth of field normally encountered in close-up
 *                       photography, which makes the scene seem much smaller than it actually
 *                       is. This filter assumes the scene is relatively planar, in which case
 *                       the part of the scene that is completely in focus can be described by
 *                       a line (the intersection of the focal plane and the scene). An example
 *                       of a planar scene might be looking at a road from above at a downward
 *                       angle. The image is then blurred with a blur radius that starts at zero
 *                       on the line and increases further from the line.
 * @param startX         The x coordinate of the start of the line segment.
 * @param startY         The y coordinate of the start of the line segment.
 * @param endX           The x coordinate of the end of the line segment.
 * @param endY           The y coordinate of the end of the line segment.
 * @param blurRadius     The maximum radius of the pyramid blur.
 * @param gradientRadius The distance from the line at which the maximum blur radius is reached.
 */
export type TFilterTiltShift = (
    this: TFxCanvas,
    startX: number,
    startY: number,
    endX: number,
    endY: number,
    blurRadius: number,
    gradientRadius: number,
) => TFxCanvas;

export const tiltShift: TFilterTiltShift = function (
    startX,
    startY,
    endX,
    endY,
    blurRadius,
    gradientRadius,
) {
    gl.tiltShift = gl.tiltShift || new FxShader(null, '\
        uniform sampler2D texture;\
        uniform float blurRadius;\
        uniform float gradientRadius;\
        uniform vec2 start;\
        uniform vec2 end;\
        uniform vec2 delta;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        void main() {\
            vec4 color = vec4(0.0);\
            float total = 0.0;\
            \
            /* randomize the lookup values to hide the fixed number of samples */\
            float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0);\
            \
            vec2 normal = normalize(vec2(start.y - end.y, end.x - start.x));\
            float radius = smoothstep(0.0, 1.0, abs(dot(texCoord * texSize - start, normal)) / gradientRadius) * blurRadius;\
            for (float t = -30.0; t <= 30.0; t++) {\
                float percent = (t + offset - 0.5) / 30.0;\
                float weight = 1.0 - abs(percent);\
                vec4 sample = texture2D(texture, texCoord + delta / texSize * percent * radius);\
                \
                /* switch to pre-multiplied alpha to correctly blur transparent images */\
                sample.rgb *= sample.a;\
                \
                color += sample * weight;\
                total += weight;\
            }\
            \
            gl_FragColor = color / total;\
            \
            /* switch back from pre-multiplied alpha */\
            gl_FragColor.rgb /= gl_FragColor.a + 0.00001;\
        }\
    ', 'tiltShift');

    const dx = endX - startX;
    const dy = endY - startY;
    const d = Math.sqrt(dx * dx + dy * dy);
    simpleShader.call(this, gl.tiltShift, {
        blurRadius: blurRadius,
        gradientRadius: gradientRadius,
        start: [startX, startY],
        end: [endX, endY],
        delta: [dx / d, dy / d],
        texSize: [this.width, this.height],
    });
    simpleShader.call(this, gl.tiltShift, {
        blurRadius: blurRadius,
        gradientRadius: gradientRadius,
        start: [startX, startY],
        end: [endX, endY],
        delta: [-dy / d, dx / d],
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';
import {IRGBA} from '../../klecks/kl-types';

/**
 * To Alpha
 * @description   Generates alpha channel from luminance (r+g+b)
 * @param isInverted    boolean - invert luminance
 * @param replaceRGBA    rgba - color to replace rgb with. if a = 0 -> keep orig color
 */
export type TFilterToAlpha = (
    this: TFxCanvas,
    isInverted: boolean,
    replaceRGBA: IRGBA | null,
) => TFxCanvas;

export const toAlpha: TFilterToAlpha = function (isInverted, replaceRGBA) {
    gl.toAlpha = gl.toAlpha || new FxShader(null, '\
    uniform bool isInverted;\
    uniform vec4 replace;\
    uniform sampler2D texture;\
    uniform vec2 texSize;\
    varying vec2 texCoord;\
    \
    void main() {\
        vec4 color = texture2D(texture, texCoord);\
        float alpha = (color.r + color.g + color.b) / 3.0;\
        if (isInverted) alpha = 1.0 - alpha;\
        alpha = min(color.a, alpha);\
        if (replace.a > 0.0) color = replace;\
        gl_FragColor = vec4(color.r, color.g, color.b, alpha);\
    }\
', 'toAlpha');

    simpleShader.call(this, gl.toAlpha, {
        isInverted: isInverted ? 1 : 0,
        replace: replaceRGBA ? [replaceRGBA.r / 255, replaceRGBA.g / 255, replaceRGBA.b / 255, replaceRGBA.a] : [0, 0, 0, 0],
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {randomShaderFunc} from '../shaders/random-shader-func';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Triangle Blur
 * @description  This is the most basic blur filter, which convolves the image with a
 *               pyramid filter. The pyramid filter is separable and is applied as two
 *               perpendicular triangle filters.
 * @param radius The radius of the pyramid convolved with the image.
 */
export type TFilterTriangleBlur = (
    this: TFxCanvas,
    radius: number,
) => TFxCanvas;

export const triangleBlur: TFilterTriangleBlur = function (radius) {
    gl.triangleBlur = gl.triangleBlur || new FxShader(null, '\
        uniform sampler2D texture;\
        uniform vec2 delta;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        void main() {\
            vec4 color = vec4(0.0);\
            float total = 0.0;\
            \
            /* randomize the lookup values to hide the fixed number of samples */\
            float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0);\
            \
            for (float t = -30.0; t <= 30.0; t++) {\
                float percent = (t + offset - 0.5) / 30.0;\
                float weight = 1.0 - abs(percent);\
                vec4 sample = texture2D(texture, texCoord + delta * percent);\
                \
                /* switch to pre-multiplied alpha to correctly blur transparent images */\
                sample.rgb *= sample.a;\
                \
                color += sample * weight;\
                total += weight;\
            }\
            \
            gl_FragColor = color / total;\
            \
            /* switch back from pre-multiplied alpha */\
            gl_FragColor.rgb /= gl_FragColor.a + 0.00001;\
        }\
    ', 'triangleBlur');

    simpleShader.call(this, gl.triangleBlur, {
        delta: [radius / this.width, 0],
    });
    simpleShader.call(this, gl.triangleBlur, {
        delta: [0, radius / this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * unmultiplyAlpha
 * reverses alpha multiply
 */
export type TFilterUnmultiplyAlpha = (
    this: TFxCanvas,
) => TFxCanvas;

export const unmultiplyAlpha: TFilterUnmultiplyAlpha = function () {
    gl.unmultiplyAlpha = gl.unmultiplyAlpha || new FxShader(null, '\
    uniform sampler2D texture;\
    uniform vec2 texSize;\
    varying vec2 texCoord;\
    \
    void main() {\
        vec4 color = texture2D(texture, texCoord);\
        if(color.a > 0.0) {\
            color.rgb /= color.a;\
        }\
        gl_FragColor = color;\
    }\
', 'unmultiplyAlpha');

    simpleShader.call(this, gl.unmultiplyAlpha, {
        texSize: [this.width, this.height],
    });

    return this;
};


code:
import {gl} from '../core/gl';
import {FxShader} from '../core/fx-shader';
import {simpleShader} from '../core/simple-shader';
import {TFxCanvas} from '../fx-canvas-types';

/**
 * Unsharp Mask
 * @description    A form of image sharpening that amplifies high-frequencies in the image. It
 *                 is implemented by scaling pixels away from the average of their neighbors.
 * @param radius   The blur radius that calculates the average of the neighboring pixels.
 * @param strength A scale factor where 0 is no effect and higher values cause a stronger effect.
 */
export type TFilterUnsharpMask = (
    this: TFxCanvas,
    radius: number,
    strength: number,
) => TFxCanvas;

export const unsharpMask: TFilterUnsharpMask = function (radius, strength) {
    gl.unsharpMask = gl.unsharpMask || new FxShader(null, '\
        uniform sampler2D blurredTexture;\
        uniform sampler2D originalTexture;\
        uniform float strength;\
        uniform float threshold;\
        varying vec2 texCoord;\
        void main() {\
            vec4 blurred = texture2D(blurredTexture, texCoord);\
            vec4 original = texture2D(originalTexture, texCoord);\
            gl_FragColor = mix(blurred, original, 1.0 + strength);\
        }\
    ', 'unsharpMask');

    // Store a copy of the current texture in the second texture unit
    this._.extraTexture.ensureFormatViaTexture(this._.texture);
    this._.texture.use();
    this._.extraTexture.drawTo(function () {
        FxShader.getDefaultShader().drawRect();
    });

    // Blur the current texture, then use the stored texture to detect edges
    this._.extraTexture.use(1);
    this.triangleBlur(radius);
    gl.unsharpMask.textures({
        originalTexture: 1,
    });
    simpleShader.call(this, gl.unsharpMask, {
        strength: strength,
    });
    this._.extraTexture.unuse(1);

    return this;
};


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {warpShader} from '../shaders/warp-shader';
import {simpleShader} from '../core/simple-shader';
import {BB} from '../../bb/bb';

/**
 * @filter         Bulge / Pinch
 * @description    Bulges or pinches the image in a circle.
 * @param centerX  The x coordinate of the center of the circle of effect.
 * @param centerY  The y coordinate of the center of the circle of effect.
 * @param radius   The radius of the circle of effect.
 * @param strength -1 to 1 (-1 is strong pinch, 0 is no effect, 1 is strong bulge)
 */
export function bulgePinch (centerX, centerY, radius, strength) {
    gl.bulgePinch = gl.bulgePinch || warpShader('\
        uniform float radius;\
        uniform float strength;\
        uniform vec2 center;\
    ', '\
        coord -= center;\
        float distance = length(coord);\
        if (distance < radius) {\
            float percent = distance / radius;\
            if (strength > 0.0) {\
                coord *= mix(1.0, smoothstep(0.0, radius / distance, percent), strength * 0.75);\
            } else {\
                coord *= mix(1.0, pow(percent, 1.0 + strength * 0.75) * radius / distance, 1.0 - percent);\
            }\
        }\
        coord += center;\
    ');

    simpleShader.call(this, gl.bulgePinch, {
        radius: radius,
        strength: BB.clamp(strength, -1, 1),
        center: [centerX, centerY],
        texSize: [this.width, this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter        Color Halftone
 * @description   Simulates a CMYK halftone rendering of the image by multiplying pixel values
 *                with a four rotated 2D sine wave patterns, one each for cyan, magenta, yellow,
 *                and black.
 * @param centerX The x coordinate of the pattern origin.
 * @param centerY The y coordinate of the pattern origin.
 * @param angle   The rotation of the pattern in radians.
 * @param size    The diameter of a dot in pixels.
 */
export function colorHalftone (centerX, centerY, angle, size) {
    gl.colorHalftone = gl.colorHalftone || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 center;\
        uniform float angle;\
        uniform float scale;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        \
        float pattern(float angle) {\
            float s = sin(angle), c = cos(angle);\
            vec2 tex = texCoord * texSize - center;\
            vec2 point = vec2(\
                c * tex.x - s * tex.y,\
                s * tex.x + c * tex.y\
            ) * scale;\
            return (sin(point.x) * sin(point.y)) * 4.0;\
        }\
        \
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            vec3 cmy = 1.0 - color.rgb;\
            float k = min(cmy.x, min(cmy.y, cmy.z));\
            cmy = (cmy - k) / (1.0 - k);\
            cmy = clamp(cmy * 10.0 - 3.0 + vec3(pattern(angle + 0.26179), pattern(angle + 1.30899), pattern(angle)), 0.0, 1.0);\
            k = clamp(k * 10.0 - 5.0 + pattern(angle + 0.78539), 0.0, 1.0);\
            gl_FragColor = vec4(1.0 - cmy - k, color.a);\
        }\
    ', 'colorHalftone');

    simpleShader.call(this, gl.colorHalftone, {
        center: [centerX, centerY],
        angle: angle,
        scale: Math.PI / size,
        texSize: [this.width, this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter         Denoise
 * @description    Smooths over grainy noise in dark images using an 9x9 box filter
 *                 weighted by color intensity, similar to a bilateral filter.
 * @param exponent The exponent of the color intensity difference, should be greater
 *                 than zero. A value of zero just gives an 9x9 box blur and high values
 *                 give the original image, but ideal values are usually around 10-20.
 */
export function denoise (exponent) {
    // Do a 9x9 bilateral box filter
    gl.denoise = gl.denoise || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float exponent;\
        uniform float strength;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        void main() {\
            vec4 center = texture2D(texture, texCoord);\
            vec4 color = vec4(0.0);\
            float total = 0.0;\
            for (float x = -4.0; x <= 4.0; x += 1.0) {\
                for (float y = -4.0; y <= 4.0; y += 1.0) {\
                    vec4 sample = texture2D(texture, texCoord + vec2(x, y) / texSize);\
                    float weight = 1.0 - abs(dot(sample.rgb - center.rgb, vec3(0.25)));\
                    weight = pow(weight, exponent);\
                    color += sample * weight;\
                    total += weight;\
                }\
            }\
            gl_FragColor = color / total;\
        }\
    ', 'denoise');

    // Perform two iterations for stronger results
    for (let i = 0; i < 2; i++) {
        simpleShader.call(this, gl.denoise, {
            exponent: Math.max(0, exponent),
            texSize: [this.width, this.height],
        });
    }

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter        Dot Screen
 * @description   Simulates a black and white halftone rendering of the image by multiplying
 *                pixel values with a rotated 2D sine wave pattern.
 * @param centerX The x coordinate of the pattern origin.
 * @param centerY The y coordinate of the pattern origin.
 * @param angle   The rotation of the pattern in radians.
 * @param size    The diameter of a dot in pixels.
 */
export function dotScreen (centerX, centerY, angle, size) {
    gl.dotScreen = gl.dotScreen || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 center;\
        uniform float angle;\
        uniform float scale;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        \
        float pattern() {\
            float s = sin(angle), c = cos(angle);\
            vec2 tex = texCoord * texSize - center;\
            vec2 point = vec2(\
                c * tex.x - s * tex.y,\
                s * tex.x + c * tex.y\
            ) * scale;\
            return (sin(point.x) * sin(point.y)) * 4.0;\
        }\
        \
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            float average = (color.r + color.g + color.b) / 3.0;\
            gl_FragColor = vec4(vec3(average * 10.0 - 5.0 + pattern()), color.a);\
        }\
    ', 'dotScreen');

    simpleShader.call(this, gl.dotScreen, {
        center: [centerX, centerY],
        angle: angle,
        scale: Math.PI / size,
        texSize: [this.width, this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {randomShaderFunc} from '../shaders/random-shader-func';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter       Edge Work
 * @description  Picks out different frequencies in the image by subtracting two
 *               copies of the image blurred with different radii.
 * @param radius The radius of the effect in pixels.
 */
export function edgeWork (radius) {
    gl.edgeWork1 = gl.edgeWork1 || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 delta;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        void main() {\
            vec2 color = vec2(0.0);\
            vec2 total = vec2(0.0);\
            \
            /* randomize the lookup values to hide the fixed number of samples */\
            float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0);\
            \
            for (float t = -30.0; t <= 30.0; t++) {\
                float percent = (t + offset - 0.5) / 30.0;\
                float weight = 1.0 - abs(percent);\
                vec3 sample = texture2D(texture, texCoord + delta * percent).rgb;\
                float average = (sample.r + sample.g + sample.b) / 3.0;\
                color.x += average * weight;\
                total.x += weight;\
                if (abs(t) < 15.0) {\
                    weight = weight * 2.0 - 1.0;\
                    color.y += average * weight;\
                    total.y += weight;\
                }\
            }\
            gl_FragColor = vec4(color / total, 0.0, 1.0);\
        }\
    ', 'edgeWork1');
    gl.edgeWork2 = gl.edgeWork2 || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 delta;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        void main() {\
            vec2 color = vec2(0.0);\
            vec2 total = vec2(0.0);\
            \
            /* randomize the lookup values to hide the fixed number of samples */\
            float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0);\
            \
            for (float t = -30.0; t <= 30.0; t++) {\
                float percent = (t + offset - 0.5) / 30.0;\
                float weight = 1.0 - abs(percent);\
                vec2 sample = texture2D(texture, texCoord + delta * percent).xy;\
                color.x += sample.x * weight;\
                total.x += weight;\
                if (abs(t) < 15.0) {\
                    weight = weight * 2.0 - 1.0;\
                    color.y += sample.y * weight;\
                    total.y += weight;\
                }\
            }\
            float c = clamp(10000.0 * (color.y / total.y - color.x / total.x) + 0.5, 0.0, 1.0);\
            gl_FragColor = vec4(c, c, c, 1.0);\
        }\
    ', 'edgeWork2');

    simpleShader.call(this, gl.edgeWork1, {
        delta: [radius / this.width, 0],
    });
    simpleShader.call(this, gl.edgeWork2, {
        delta: [0, radius / this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter        Hexagonal Pixelate
 * @description   Renders the image using a pattern of hexagonal tiles. Tile colors
 *                are nearest-neighbor sampled from the centers of the tiles.
 * @param centerX The x coordinate of the pattern center.
 * @param centerY The y coordinate of the pattern center.
 * @param scale   The width of an individual tile, in pixels.
 */
export function hexagonalPixelate (centerX, centerY, scale) {
    gl.hexagonalPixelate = gl.hexagonalPixelate || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 center;\
        uniform float scale;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        void main() {\
            vec2 tex = (texCoord * texSize - center) / scale;\
            tex.y /= 0.866025404;\
            tex.x -= tex.y * 0.5;\
            \
            vec2 a;\
            if (tex.x + tex.y - floor(tex.x) - floor(tex.y) < 1.0) a = vec2(floor(tex.x), floor(tex.y));\
            else a = vec2(ceil(tex.x), ceil(tex.y));\
            vec2 b = vec2(ceil(tex.x), floor(tex.y));\
            vec2 c = vec2(floor(tex.x), ceil(tex.y));\
            \
            vec3 TEX = vec3(tex.x, tex.y, 1.0 - tex.x - tex.y);\
            vec3 A = vec3(a.x, a.y, 1.0 - a.x - a.y);\
            vec3 B = vec3(b.x, b.y, 1.0 - b.x - b.y);\
            vec3 C = vec3(c.x, c.y, 1.0 - c.x - c.y);\
            \
            float alen = length(TEX - A);\
            float blen = length(TEX - B);\
            float clen = length(TEX - C);\
            \
            vec2 choice;\
            if (alen < blen) {\
                if (alen < clen) choice = a;\
                else choice = c;\
            } else {\
                if (blen < clen) choice = b;\
                else choice = c;\
            }\
            \
            choice.x += choice.y * 0.5;\
            choice.y *= 0.866025404;\
            choice *= scale / texSize;\
            gl_FragColor = texture2D(texture, choice + center / texSize);\
        }\
    ', 'hexagonalPixelate');

    simpleShader.call(this, gl.hexagonalPixelate, {
        center: [centerX, centerY],
        scale: scale,
        texSize: [this.width, this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter         Ink
 * @description    Simulates outlining the image in ink by darkening edges stronger than a
 *                 certain threshold. The edge detection value is the difference of two
 *                 copies of the image, each blurred using a blur of a different radius.
 * @param strength The multiplicative scale of the ink edges. Values in the range 0 to 1
 *                 are usually sufficient, where 0 doesn't change the image and 1 adds lots
 *                 of black edges. Negative strength values will create white ink edges
 *                 instead of black ones.
 */
export function ink (strength) {
    gl.ink = gl.ink || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float strength;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        void main() {\
            vec2 dx = vec2(1.0 / texSize.x, 0.0);\
            vec2 dy = vec2(0.0, 1.0 / texSize.y);\
            vec4 color = texture2D(texture, texCoord);\
            float bigTotal = 0.0;\
            float smallTotal = 0.0;\
            vec3 bigAverage = vec3(0.0);\
            vec3 smallAverage = vec3(0.0);\
            for (float x = -2.0; x <= 2.0; x += 1.0) {\
                for (float y = -2.0; y <= 2.0; y += 1.0) {\
                    vec3 sample = texture2D(texture, texCoord + dx * x + dy * y).rgb;\
                    bigAverage += sample;\
                    bigTotal += 1.0;\
                    if (abs(x) + abs(y) < 2.0) {\
                        smallAverage += sample;\
                        smallTotal += 1.0;\
                    }\
                }\
            }\
            vec3 edge = max(vec3(0.0), bigAverage / bigTotal - smallAverage / smallTotal);\
            gl_FragColor = vec4(color.rgb - dot(edge, edge) * strength * 100000.0, color.a);\
        }\
    ', 'ink');

    simpleShader.call(this, gl.ink, {
        strength: strength * strength * strength * strength * strength,
        texSize: [this.width, this.height],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {randomShaderFunc} from '../shaders/random-shader-func';
import {simpleShader} from '../core/simple-shader';
import {BB} from '../../bb/bb';

/**
 * @filter           Lens Blur
 * @description      Imitates a camera capturing the image out of focus by using a blur that generates
 *                   the large shapes known as bokeh. The polygonal shape of real bokeh is due to the
 *                   blades of the aperture diaphragm when it isn't fully open. This blur renders
 *                   bokeh from a 6-bladed diaphragm because the computation is more efficient. It
 *                   can be separated into three rhombi, each of which is just a skewed box blur.
 *                   This filter makes use of the floating point texture WebGL extension to implement
 *                   the brightness parameter, so there will be severe visual artifacts if brightness
 *                   is non-zero and the floating point texture extension is not available. The
 *                   idea was from John White's SIGGRAPH 2011 talk but this effect has an additional
 *                   brightness parameter that fakes what would otherwise come from a HDR source.
 * @param radius     the radius of the hexagonal disk convolved with the image
 * @param brightness -1 to 1 (the brightness of the bokeh, negative values will create dark bokeh)
 * @param angle      the rotation of the bokeh in radians
 */
export function lensBlur (radius, brightness, angle) {
    // All averaging is done on values raised to a power to make more obvious bokeh
    // (we will raise the average to the inverse power at the end to compensate).
    // Without this the image looks almost like a normal blurred image. This hack is
    // obviously not realistic, but to accurately simulate this we would need a high
    // dynamic range source photograph which we don't have.
    gl.lensBlurPrePass = gl.lensBlurPrePass || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float power;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            color = pow(color, vec4(power));\
            gl_FragColor = vec4(color);\
        }\
    ', 'lensBlurPrePass');

    const common = '\
        uniform sampler2D texture0;\
        uniform sampler2D texture1;\
        uniform vec2 delta0;\
        uniform vec2 delta1;\
        uniform float power;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        vec4 sample(vec2 delta) {\
            /* randomize the lookup values to hide the fixed number of samples */\
            float offset = random(vec3(delta, 151.7182), 0.0);\
            \
            vec4 color = vec4(0.0);\
            float total = 0.0;\
            for (float t = 0.0; t <= 30.0; t++) {\
                float percent = (t + offset) / 30.0;\
                color += texture2D(texture0, texCoord + delta * percent);\
                total += 1.0;\
            }\
            return color / total;\
        }\
    ';

    gl.lensBlur0 = gl.lensBlur0 || new Shader(null, common + '\
        void main() {\
            gl_FragColor = sample(delta0);\
        }\
    ', 'lensBlur0');
    gl.lensBlur1 = gl.lensBlur1 || new Shader(null, common + '\
        void main() {\
            gl_FragColor = (sample(delta0) + sample(delta1)) * 0.5;\
        }\
    ', 'lensBlur1');
    gl.lensBlur2 = gl.lensBlur2 || new Shader(null, common + '\
        void main() {\
            vec4 color = (sample(delta0) + 2.0 * texture2D(texture1, texCoord)) / 3.0;\
            gl_FragColor = pow(color, vec4(power));\
        }\
    ', 'lensBlur2').textures({texture1: 1});

    // Generate
    const dir = [];
    for (let i = 0; i < 3; i++) {
        const a = angle + i * Math.PI * 2 / 3;
        dir.push([radius * Math.sin(a) / this.width, radius * Math.cos(a) / this.height]);
    }
    const power = Math.pow(10, BB.clamp(brightness, -1, 1));

    // Remap the texture values, which will help make the bokeh effect
    simpleShader.call(this, gl.lensBlurPrePass, {
        power: power,
    });

    // Blur two rhombi in parallel into extraTexture
    this._.extraTexture.ensureFormatViaTexture(this._.texture);
    simpleShader.call(this, gl.lensBlur0, {
        delta0: dir[0],
    }, this._.texture, this._.extraTexture);
    simpleShader.call(this, gl.lensBlur1, {
        delta0: dir[1],
        delta1: dir[2],
    }, this._.extraTexture, this._.extraTexture);

    // Blur the last rhombus and combine with extraTexture
    simpleShader.call(this, gl.lensBlur0, {
        delta0: dir[1],
    });
    this._.extraTexture.use(1);
    simpleShader.call(this, gl.lensBlur2, {
        power: 1 / power,
        delta0: dir[2],
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {Shader} from '../core/shader';
import {simpleShader} from '../core/simple-shader';
import {BB} from '../../bb/bb';

/**
 * @filter         Sepia
 * @description    Gives the image a reddish-brown monochrome tint that imitates an old photograph.
 * @param amount   0 to 1 (0 for no effect, 1 for full sepia coloring)
 */
export function sepia (amount) {
    gl.sepia = gl.sepia || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float amount;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            float r = color.r;\
            float g = color.g;\
            float b = color.b;\
            \
            color.r = min(1.0, (r * (1.0 - (0.607 * amount))) + (g * (0.769 * amount)) + (b * (0.189 * amount)));\
            color.g = min(1.0, (r * 0.349 * amount) + (g * (1.0 - (0.314 * amount))) + (b * 0.168 * amount));\
            color.b = min(1.0, (r * 0.272 * amount) + (g * 0.534 * amount) + (b * (1.0 - (0.869 * amount))));\
            \
            gl_FragColor = color;\
        }\
    ', 'sepia');

    simpleShader.call(this, gl.sepia, {
        amount: BB.clamp(amount, 0, 1),
    });

    return this;
}


code:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import {gl} from '../core/gl';
import {warpShader} from '../shaders/warp-shader';
import {simpleShader} from '../core/simple-shader';

/**
 * @filter        Swirl
 * @description   Warps a circular region of the image in a swirl.
 * @param centerX The x coordinate of the center of the circular region.
 * @param centerY The y coordinate of the center of the circular region.
 * @param radius  The radius of the circular region.
 * @param angle   The angle in radians that the pixels in the center of
 *                the circular region will be rotated by.
 */
export function swirl (centerX, centerY, radius, angle) {
    gl.swirl = gl.swirl || warpShader('\
        uniform float radius;\
        uniform float angle;\
        uniform vec2 center;\
    ', '\
        coord -= center;\
        float distance = length(coord);\
        if (distance < radius) {\
            float percent = (radius - distance) / radius;\
            float theta = percent * percent * angle;\
            float s = sin(theta);\
            float c = cos(theta);\
            coord = vec2(\
                coord.x * c - coord.y * s,\
                coord.x * s + coord.y * c\
            );\
        }\
        coord += center;\
    ');

    simpleShader.call(this, gl.swirl, {
        radius: radius,
        center: [centerX, centerY],
        angle: angle,
        texSize: [this.width, this.height],
    });

    return this;
}