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       Vibrance
 * @description  Modifies the saturation of desaturated colors, leaving saturated colors unmodified.
 * @param amount -1 to 1 (-1 is minimum vibrance, 0 is no change, and 1 is maximum vibrance)
 */
export function vibrance (amount) {
    gl.vibrance = gl.vibrance || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float amount;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            float average = (color.r + color.g + color.b) / 3.0;\
            float mx = max(color.r, max(color.g, color.b));\
            float amt = (mx - average) * (-amount * 3.0);\
            color.rgb = mix(color.rgb, vec3(mx), amt);\
            gl_FragColor = color;\
        }\
    ', 'vibrance');

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

    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         Vignette
 * @description    Adds a simulated lens edge darkening effect.
 * @param size     0 to 1 (0 for center of frame, 1 for edge of frame)
 * @param amount   0 to 1 (0 for no effect, 1 for maximum lens darkening)
 */
export function vignette (size, amount) {
    gl.vignette = gl.vignette || new Shader(null, '\
        uniform sampler2D texture;\
        uniform float size;\
        uniform float amount;\
        varying vec2 texCoord;\
        void main() {\
            vec4 color = texture2D(texture, texCoord);\
            \
            float dist = distance(texCoord, vec2(0.5, 0.5));\
            color.rgb *= smoothstep(0.8, size * 0.799, dist * (amount + size));\
            \
            gl_FragColor = color;\
        }\
    ', 'vignette');

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

    return this;
}

repo_name: Doggermelon/klecks

license: mit


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         Zoom Blur
 * @description    Blurs the image away from a certain point, which looks like radial motion blur.
 * @param centerX  The x coordinate of the blur origin.
 * @param centerY  The y coordinate of the blur origin.
 * @param strength The strength of the blur. Values in the range 0 to 1 are usually sufficient,
 *                 where 0 doesn't change the image and 1 creates a highly blurred image.
 */
export function zoomBlur (centerX, centerY, strength) {
    gl.zoomBlur = gl.zoomBlur || new Shader(null, '\
        uniform sampler2D texture;\
        uniform vec2 center;\
        uniform float strength;\
        uniform vec2 texSize;\
        varying vec2 texCoord;\
        ' + randomShaderFunc + '\
        void main() {\
            vec4 color = vec4(0.0);\
            float total = 0.0;\
            vec2 toCenter = center - texCoord * texSize;\
            \
            /* 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 = 0.0; t <= 40.0; t++) {\
                float percent = (t + offset) / 40.0;\
                float weight = 4.0 * (percent - percent * percent);\
                vec4 sample = texture2D(texture, texCoord + toCenter * percent * strength / texSize);\
                \
                /* 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;\
        }\
    ', 'zoomBlur');

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

    return this;
}


code:
import {TFilterBrightnessContrast} from './filters/brightness-contrast';
import {TFilterCurves} from './filters/curves';
import {TFilterHueSaturation} from './filters/hue-saturation';
import {TFilterDistort} from './filters/distort';
import {TFilterInvert} from './filters/invert';
import {TFilterMatrixWarp} from './filters/matrix-warp';
import {TFilterMultiplyAlpha} from './filters/multiply-alpha';
import {TFilterUnmultiplyAlpha} from './filters/unmultiply-alpha';
import {TFilterNoise} from './filters/noise';
import {TFilterPerspective} from './filters/perspective';
import {TFilterTiltShift} from './filters/tilt-shift';
import {TFilterToAlpha} from './filters/to-alpha';
import {TFilterTriangleBlur} from './filters/triangle-blur';
import {TFilterUnsharpMask} from './filters/unsharp-mask';
import {FxTexture} from './core/fx-texture';
import {FxShader} from './core/fx-shader';

export type TVec3 = [number, number, number];
export type TMat2x2 = [number, number, number, number];
export type TMat3x3 = [number, number, number, number, number, number, number, number, number];
export type TMatDeep3x3 = [TVec3, TVec3, TVec3];

export type TFxSupportedElements = HTMLCanvasElement | HTMLVideoElement | HTMLImageElement;

export type TUniforms<T extends number | (number[]) | unknown = unknown> = Record<string, T>;

export type TFxFilters = {
    matrixWarp: TFilterMatrixWarp;
    multiplyAlpha: TFilterMultiplyAlpha;
    unmultiplyAlpha: TFilterUnmultiplyAlpha;

    brightnessContrast: TFilterBrightnessContrast;
    curves: TFilterCurves;
    distort: TFilterDistort;
    hueSaturation: TFilterHueSaturation;
    invert: TFilterInvert;
    noise: TFilterNoise;
    perspective: TFilterPerspective;
    tiltShift: TFilterTiltShift;
    toAlpha: TFilterToAlpha;
    triangleBlur: TFilterTriangleBlur;
    unsharpMask: TFilterUnsharpMask;
}

type TFxShaders = Record<keyof Pick<TFxFilters, keyof TFxFilters>, FxShader | undefined>;

export type TFxGl = WebGLRenderingContext & TFxShaders & {
    framebuffer: WebGLFramebuffer;
    vertexBuffer: WebGLBuffer;
    texCoordBuffer: WebGLBuffer;
    defaultShader: FxShader;
};

export type TWrappedTexture = {

    /** Pseudo private. Don't interface with this. */
    _: FxTexture;

    /**
     * Loads the image from an HTML element into the texture. This is more efficient than repeatedly
     * creating and destroying textures.
     *
     * element - The HTML element to store in the texture, either an <img>, a <canvas>, or a <video>.
     */
    loadContentsOf: (element: TFxSupportedElements) => void;

    /**
     * Textures will be garbage collected eventually when they are no longer referenced, but this method will
     * free GPU resources immediately.
     */
    destroy: () => void;
};

export type TFxCanvas = HTMLCanvasElement & TFxFilters & {

    /** Pseudo private. Don't interface with this. */
    '_': {
        gl: TFxGl;
        isInitialized: boolean;
        texture: FxTexture;
        spareTexture: FxTexture;
        extraTexture: FxTexture;
        flippedShader: FxShader;
    };

    /**
     * Texture Constructor
     * Creates a texture that initially stores the image from an HTML element. Notice that texture() is a method on a
     * canvas object, which means if you want to use the same image on two canvas objects you will need two different
     * textures, one for each canvas.
     *
     * element - The HTML element to store in the texture, either an <img>, a <canvas>, or a <video>.
     */
    texture: (element: TFxSupportedElements) => TWrappedTexture;

    /**
     * This replaces the internal contents of the canvas with the image stored in texture. All filter operations take
     * place in a chain that starts with fxCanvas.draw() and ends with fxCanvas.update().
     *
     * texture - Stores image data, the result of calling fx.texture().
     *
     * optional width and height to scale to. If no width and height are given then the original
     * texture width and height
     */
    draw: (texture: TWrappedTexture, width?: number, height?: number) => TFxCanvas;

    /**
     * Update Screen
     * This replaces the visible contents of the canvas with the internal image result. For efficiency reasons,
     * the internal image buffers are not rendered to the screen every time a filter is applied, so you will need
     * to call update() on your canvas after you have finished applying the filters to be able to see the result.
     * All filter operations take place in a chain that starts with fxCanvas.draw() and ends with fxCanvas.update().
     */
    update: () => TFxCanvas;

    /**
     * Returns new texture with contents of canvas.
     */
    contents: () => TWrappedTexture;

    /**
     * Get a Uint8 array of pixel values: [r, g, b, a, r, g, b, a, ...]
     * Length of the array will be width * height * 4.
     */
    getPixelArray: () => Uint8Array;
};


code:
import {brightnessContrast} from './filters/brightness-contrast';
import {FxShader} from './core/fx-shader';
import {gl, setGl} from './core/gl';
import {FxTexture} from './core/fx-texture';
import {curves} from './filters/curves';
import {hueSaturation} from './filters/hue-saturation';
import {noise} from './filters/noise';
import {triangleBlur} from './filters/triangle-blur';
import {tiltShift} from './filters/tilt-shift';
import {matrixWarp} from './filters/matrix-warp';
import {unsharpMask} from './filters/unsharp-mask';
import {toAlpha} from './filters/to-alpha';
import {invert} from './filters/invert';
import {perspective} from './filters/perspective';
import {unmultiplyAlpha} from './filters/unmultiply-alpha';
import {distort} from './filters/distort';
import {multiplyAlpha} from './filters/multiply-alpha';
import {TFxCanvas, TFxGl, TFxSupportedElements, TWrappedTexture} from './fx-canvas-types';
import {BB} from '../bb/bb';

/*
 * based on glfx.js
 * https://github.com/evanw/glfx.js/
 * Copyright 2011 Evan Wallace
 * Released under the MIT license
 */

/**
 * Before you can apply any filters you will need a canvas, which stores the result of the filters you apply.
 * Canvas creation is done through fxCanvas(), which creates and returns a new WebGL <canvas> tag with additional
 * methods specific to fxCanvas. This call will throw an error message if the browser doesn't support WebGL.
 *
 * This library provides realtime image effects using WebGL. There are three parts to it:
 * - texture - a raw source of image data (created from <img> <canvas> or <video>)
 * - filter - an image effect (represents one or more WebGL shaders)
 * - canvas - an image buffer that stores the results (a WebGL <canvas> tag)
 */
export const fxCanvas: (() => TFxCanvas) = (function () {

    function wrapTexture (texture: FxTexture): TWrappedTexture {
        return {
            _: texture,
            loadContentsOf: function (element) {
                // Make sure that we're using the correct global WebGL context
                setGl(this._.gl);
                this._.loadContentsOf(element);
            },
            destroy: function () {
                // Make sure that we're using the correct global WebGL context
                setGl(this._.gl);
                this._.destroy();
            },
        };
    }

    function texture (element: TFxSupportedElements) {
        return wrapTexture(FxTexture.fromElement(element));
    }

    function getTextureType (): GLenum {
        let textureType: GLenum = gl.UNSIGNED_BYTE;

        // Go for floating point buffer textures if we can, it'll make the bokeh
        // filter look a lot better. Note that on Windows, ANGLE does not let you
        // render to a floating-point texture when linear filtering is enabled.
        // See http://crbug.com/172278 for more information.
        if (
            gl.getExtension('WEBGL_color_buffer_float') && // firefox shows warning without this line
            gl.getExtension('OES_texture_float') &&
            gl.getExtension('OES_texture_float_linear')
        ) {
            const testTexture = new FxTexture(100, 100, gl.RGBA, gl.FLOAT);
            try {
                // Only use gl.FLOAT if we can render to it
                testTexture.drawTo(function () {
                    textureType = gl.FLOAT;
                });
            } catch (e) {
                /* empty */
            }
            testTexture.destroy();
        }
        return textureType;
    }

    function initialize (this: TFxCanvas, width: number, height: number): void {
        const textureType = getTextureType();
        if (this._.texture) {
            this._.texture.destroy();
        }
        if (this._.spareTexture) {
            this._.spareTexture.destroy();
        }
        this.width = width;
        this.height = height;
        this._.texture = new FxTexture(width, height, gl.RGBA, textureType);
        this._.spareTexture = new FxTexture(width, height, gl.RGBA, textureType);
        this._.extraTexture = this._.extraTexture || new FxTexture(0, 0, gl.RGBA, textureType);
        this._.flippedShader = this._.flippedShader || new FxShader(null, `
uniform sampler2D texture;
varying vec2 texCoord;
void main() {
    gl_FragColor = texture2D(texture, vec2(texCoord.x, 1.0 - texCoord.y));
}
        `, 'flippedShader');
        this._.isInitialized = true;
    }

    function draw (this: TFxCanvas, texture: TWrappedTexture, width?: number, height?: number): TFxCanvas {
        if (!this._.isInitialized || texture._.width != this.width || texture._.height != this.height) {
            initialize.call(this, width ? width : texture._.width, height ? height : texture._.height);
        }

        texture._.use();
        this._.texture.drawTo(function () {
            FxShader.getDefaultShader().drawRect();
        });

        return this;
    }

    function update (this: TFxCanvas): TFxCanvas {
        this._.texture.use();
        this._.flippedShader.drawRect();
        return this;
    }

    function contents (this: TFxCanvas): TWrappedTexture {
        const texture = new FxTexture(this._.texture.width, this._.texture.height, gl.RGBA, gl.UNSIGNED_BYTE);
        this._.texture.use();
        texture.drawTo(function () {
            FxShader.getDefaultShader().drawRect();
        });
        return wrapTexture(texture);
    }

    /*
       Get a Uint8 array of pixel values: [r, g, b, a, r, g, b, a, ...]
       Length of the array will be width * height * 4.
    */
    function getPixelArray (this: TFxCanvas): Uint8Array {
        const w = this._.texture.width;
        const h = this._.texture.height;
        const array = new Uint8Array(w * h * 4);
        this._.texture.drawTo(function () {
            gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, array);
        });
        return array;
    }

    function wrap<F extends (...args: any[]) => unknown>(fn: F): F {
        return <F>function (this: TFxCanvas, ...args: any[]) {
            // Make sure that we're using the correct global WebGL context
            setGl(this._.gl);
            // Now that the context has been switched, we can call the wrapped function
            return fn.apply(this, args);
        };
    }

    function getWebGlContext (canvas: HTMLCanvasElement, options?: WebGLContextAttributes): WebGLRenderingContext | null {
        const contextNames = ['webgl', 'experimental-webgl', 'webgl2'];
        let context: WebGLRenderingContext | null = null;
        contextNames.forEach(name => {
            if (context) {
                return;
            }
            // get.webgl.org does a try-catch
            try {
                context = canvas.getContext(name, options) as (typeof context);
            } catch (e) {
                /* empty */
            }
        });
        return context;
    }

    return (): TFxCanvas => {
        if (!window.WebGLRenderingContext) {
            throw 'WebGLRenderingContext not set. Browser does not support WebGL.';
        }

        const canvas: TFxCanvas = BB.canvas() as TFxCanvas;
        const context = getWebGlContext(canvas, {premultipliedAlpha: false});
        if (!context) {
            throw 'This browser does not support WebGL';
        }
        setGl(context as TFxGl);

        canvas._ = {
            gl,
            isInitialized: false,
            texture: null,
            spareTexture: null,
            flippedShader: null,
        } as any;

        // Core methods
        canvas.texture = wrap(texture);
        canvas.draw = wrap(draw);
        canvas.update = wrap(update);
        canvas.contents = wrap(contents);
        canvas.getPixelArray = wrap(getPixelArray);

        // Filter methods
        canvas.brightnessContrast = wrap(brightnessContrast);
        canvas.hueSaturation = wrap(hueSaturation);
        canvas.triangleBlur = wrap(triangleBlur);
        canvas.unsharpMask = wrap(unsharpMask);
        canvas.perspective = wrap(perspective);
        canvas.matrixWarp = wrap(matrixWarp);
        canvas.tiltShift = wrap(tiltShift);
        canvas.noise = wrap(noise);
        canvas.curves = wrap(curves);
        canvas.invert = wrap(invert);
        canvas.multiplyAlpha = wrap(multiplyAlpha);
        canvas.unmultiplyAlpha = wrap(unmultiplyAlpha);
        canvas.toAlpha = wrap(toAlpha);
        canvas.distort = wrap(distort);

        return canvas as TFxCanvas;
    };
})();


code:
export function clamp (lo: number, value: number, hi: number): number {
    return Math.max(lo, Math.min(value, hi));
}


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

// from javax.media.jai.PerspectiveTransform

/**
 * Creates a PerspectiveTransform that maps the unit square onto an arbitrary quadrilateral.
 */
export function getSquareToQuad (
    x0: number,
    y0: number,
    x1: number,
    y1: number,
    x2: number,
    y2: number,
    x3: number,
    y3: number
): TMat3x3 {
    const dx1 = x1 - x2;
    const dy1 = y1 - y2;
    const dx2 = x3 - x2;
    const dy2 = y3 - y2;
    const dx3 = x0 - x1 + x2 - x3;
    const dy3 = y0 - y1 + y2 - y3;
    const det = dx1 * dy2 - dx2 * dy1;
    const a = (dx3 * dy2 - dx2 * dy3) / det;
    const b = (dx1 * dy3 - dx3 * dy1) / det;
    return [
        x1 - x0 + a * x1, y1 - y0 + a * y1, a,
        x3 - x0 + b * x3, y3 - y0 + b * y3, b,
        x0, y0, 1,
    ];
}

export function getInverse (m: TMat3x3): TMat3x3 {
    const a = m[0], b = m[1], c = m[2];
    const d = m[3], e = m[4], f = m[5];
    const g = m[6], h = m[7], i = m[8];
    const det = a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g;
    return [
        (e * i - f * h) / det, (c * h - b * i) / det, (b * f - c * e) / det,
        (f * g - d * i) / det, (a * i - c * g) / det, (c * d - a * f) / det,
        (d * h - e * g) / det, (b * g - a * h) / det, (a * e - b * d) / det,
    ];
}

export function multiply (a: TMat3x3, b: TMat3x3): TMat3x3 {
    return [
        a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
        a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
        a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
        a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
        a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
        a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
        a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
        a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
        a[6] * b[2] + a[7] * b[5] + a[8] * b[8],
    ];
}


code:
import {BB} from '../../bb/bb';

export function splineInterpolate (points: [number, number][]): number[] {
    const interpolator = new BB.SplineInterpolator(points);
    const array = [];
    for (let i = 0; i < 256; i++) {
        array.push(
            BB.clamp(
                Math.floor(interpolator.interpolate(i / 255) * 256),
                0,
                255
            )
        );
    }
    return array;
}


code:
// returns a random number between 0 and 1
export const randomShaderFunc = '\
    float random(vec3 scale, float seed) {\
        /* use the fragment position for a different seed per-pixel */\
        return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed);\
    }\
';


code:
uniform sampler2D texture;
varying vec2 texCoord;
uniform vec2 texSize;

uniform float seed;
uniform float type;
uniform vec2 scale;
uniform vec2 offset;
uniform float octaves;
uniform float samples;

uniform float peaks;
uniform float contrast;
uniform float brightness;
uniform float isReversed;

uniform vec3 colA;
uniform vec3 colB;

uniform float channels; // 0 - rgb, 1 - alpha

// fbm, cellular_noise based on
// https://github.com/Gonkee/Gonkees-Shaders
// MIT, Copyright © Gonkee

// hash & simplex based on
// https://www.shadertoy.com/view/Msf3WH
// MIT, Copyright © 2013 Inigo Quilez
// https://www.youtube.com/c/InigoQuilez
// https://iquilezles.org

float rand(vec2 co) {
    return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

float value_noise(vec2 coord){
    return rand(floor(coord) / 100.0);
}

float cellular_noise(vec2 coord) {
    vec2 i = floor(coord);
    vec2 f = fract(coord);

    float min_dist = 99999.0;
    for (float x = -1.0; x <= 1.0; x++) {
        for (float y = -1.0; y <= 1.0; y++) {
            vec2 node = rand(i + vec2(x, y)) + vec2(x, y);
            float dist = sqrt((f - node).x * (f - node).x + (f - node).y * (f - node).y);
            min_dist = min(min_dist, dist);
        }
    }
    return min_dist;
}

// ---------- Simplex Alternative ---------------------

vec3 hash(vec3 p)
{ p=vec3(dot(p, vec3(127.1, 311.7, 74.7))
, dot(p, vec3(269.5, 183.3, 246.1))
, dot(p, vec3(113.5, 271.9, 124.6)))
;return fract(sin(p)*43758.5453123)*2.-1.; }

mat3 hash(mat3 p)
{ return mat3(hash(p[0]), hash(p[1]), hash(p[2])); }

vec3 dots(mat3 a, vec3 w, mat3 b){ return vec3
(dot(a[0], w-b[0]), dot(a[1], w-b[1]), dot(a[2], w-b[2])); }

//return noiseGra13dx as .x, and its derivatives as .yzw
// https://www.shadertoy.com/view/llByD1
vec3 noiseGra13dx(in vec3 x) {
    vec3 p=floor(x), w=fract(x)
    #if 1
    , u=w*w*w*(w*(w*6.-15.)+10.), v=30.*w*w*(w*(w-2.)+1.)//quintic hermite
    #else
    , u=w*w*(3.-2.*w), v=6.*w*(1.-w)//cubic hermite
    #endif
    //gradients
    , G=hash(p+vec3(0)), F=hash(p+vec3(1))
    ;mat3 D=hash(mat3(p, p, p)+mat3(1)), E=hash(mat3(p, p, p)+1.-mat3(1));
    //projections
    vec3 d=dots(D, w, mat3(1)), e=dots(E, w, 1.-mat3(1));
    //interpolations
    float g=dot(G, w), f=dot(F, w-vec3(1));
    vec3 h=u.yzx*(g-d.xyx-d.yzz+e.zxy)+d-g, U=u*h, a=d-e;
    mat3 S=D-mat3(G, G, G), W=D-E;
    a.x=(a.x+a.y+a.z)+f-g;
    ;float b=u.x*u.y*u.z;

    vec4 result = vec4(g+U.x+U.y+U.z+a.x*b// value
    , G*(1.-b)+b*(W[0]+W[1]+W[2]+F)//https://www.shadertoy.com/view/llByD1
    +u.x*(S[0]+u.y*(G-D[0]-D[1]+E[2]))// derivatives
    +u.y*(S[1]+u.z*(G-D[1]-D[2]+E[0]))
    +u.z*(S[2]+u.x*(G-D[0]-D[2]+E[1]))
    +v*(u.zxy*(g-d.xxy-d.zyz+e.yzx)+h+u.yzx*u.zxy*a.x));
    return 0.5 + 0.5 * result.xxx;
}

float fbm(vec2 coord) {
    float normalize_factor = 0.0;
    float value = 0.0;
    float scale = 0.5;

    for (float i = 0.0; i < 7.0; i++){
        if (i < octaves) {
            if (type == 0.0) {
                value += value_noise(coord) * scale;
            } else if (type == 1.0) {
                value += noiseGra13dx(vec3(coord.x, coord.y, 0.0)).r * scale;
            } else if (type == 2.0) {
                value += cellular_noise(coord) * scale;
            }
            normalize_factor += scale;
            coord *= 2.0;
            scale *= 0.5;
        }
    }
    return value / normalize_factor;
}

float render (vec2 pos) {
    float result = fbm(pos);
    //

    if (peaks > 0.0) {
        result = abs(mod(result * peaks - 0.5, 1.0) - 0.5) * 2.0;// triangle
        //result = mod(result * (peaks + 1.0), 1.0); // sawtooth
    }

    result += brightness;
    if (contrast > 0.0) {
        result = clamp((result - 0.5) / (1.0 - contrast) + 0.5, 0.0, 1.0);
    } else if (contrast < 0.0) {
        result = clamp((result - 0.5) * (1.0 + contrast) + 0.5, 0.0, 1.0);
    }

    if (isReversed == 1.0) {
        result = 1.0 - result;
    }
    return result;
}

void main() {
    vec4 color;

    vec2 seedOffset;
    seedOffset.x = (rand(vec2(seed, 0.0)) * 100.0 - 50.0);
    seedOffset.y = (rand(vec2(0.0, seed)) * 100.0 - 50.0);

    float val = 0.0;
    vec2 basePos = texCoord * texSize / scale - offset / scale + seedOffset;

    if (samples == 1.0) {
        val = render(basePos);

    }/* else if (samples == 4.0) {
        for (float i = 0.0; i < 2.0; i++) {
            for (float e = 0.0; e < 2.0; e++) {
                val += render(basePos + (vec2(i, e) - 0.5) * 0.5 / scale);
            }
        }
        val /= 4.0;

    }*/ else if (samples == 16.0) {
        for (float i = 0.0; i < 4.0; i++) {
            for (float e = 0.0; e < 4.0; e++) {
                val += render(basePos + (vec2(i, e) - 0.25) * 0.25 / scale);
            }
        }
        val /= 16.0;
    }

    if (channels == 0.0) {
        gl_FragColor = vec4(vec3(mix(colA, colB, val)), 1.0);
    } else {
        gl_FragColor = vec4(vec3(1.0), val);
    }
}


code:
import {FxShader} from '../core/fx-shader';

export function warpShader (uniformsStr: string, warpStr: string): FxShader {
    return new FxShader(null, uniformsStr + '\
    uniform sampler2D texture;\
    uniform vec2 texSize;\
    varying vec2 texCoord;\
    void main() {\
        vec2 coord = texCoord * texSize;\
        ' + warpStr + '\
        gl_FragColor = texture2D(texture, coord / texSize);\
        vec2 clampedCoord = clamp(coord, vec2(0.0), texSize);\
        if (coord != clampedCoord) {\
            /* fade to transparent if we are outside the image */\
            gl_FragColor.a *= max(0.0, 1.0 - length(coord - clampedCoord));\
        }\
    }', 'warp');
}


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

let failed = false;
let fx: TFxCanvas | null = null;

export function getSharedFx (): TFxCanvas | null {
    // failed previously, don't need to try again.
    if (failed) {
        return fx;
    }

    if (!fx || fx._.gl.isContextLost()) {
        try {
            fx = fxCanvas();
        } catch (e) {
            failed = true;
            fx = null;
            setTimeout(() => {
                throw e;
            });
        }
    }
    return fx;
}


code:
import {BB} from '../../bb/bb';
import {createPenPressureToggle} from '../ui/components/create-pen-pressure-toggle';
import {eventResMs} from './brushes-consts';
import {Checkbox} from '../ui/components/checkbox';
import {brushes} from '../brushes/brushes';
import {klHistory} from '../history/kl-history';
import {KlSlider} from '../ui/components/kl-slider';
import brushIconImg from '/src/app/img/ui/brush-blend.svg';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {BlendBrush} from '../brushes/blend-brush';

export const blendBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-blend'),
        sizeSlider: {
            min: 0.5,
            max: 100,
            curve: BB.quadraticSplineInput(0.5, 100, 0.1),
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
        },
    } as IBrushUi<BlendBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-blend');
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.BlendBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());

        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        function setSize (size: number): void {
            brush.setSize(size);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 225,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: 58,
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 225,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brush.getOpacity(),
                curve: brushInterface.opacitySlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });
            const blendingSlider = new KlSlider({
                label: LANG('brush-blending'),
                width: 225,
                height: 30,
                min: 0,
                max: 1,
                value: brush.getBlending(),
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: function (val) {
                    brush.setBlending(val);
                },
            });
            blendingSlider.getElement().style.marginTop = '10px';

            const pressureSizeToggle = createPenPressureToggle(true, function (b) {
                brush.setSizePressure(b);
            });
            const pressureOpacityToggle = createPenPressureToggle(false, function (b) {
                brush.setOpacityPressure(b);
            });

            const lockAlphaToggle = new Checkbox({
                init: brush.getLockAlpha(),
                label: LANG('lock-alpha'),
                callback: function (b) {
                    brush.setLockAlpha(b);
                },
                doHighlight: true,
                title: LANG('lock-alpha-title'),
                css: {
                    marginTop: '10px',
                    display: 'inline-block',
                },
            });

            div.append(
                BB.el({
                    content: [
                        sizeSlider.getElement(),
                        pressureSizeToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                        marginBottom: '10px',
                    },
                }),
                BB.el({
                    content: [
                        opacitySlider.getElement(),
                        pressureOpacityToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                    },
                }),
                blendingSlider.getElement(),
                lockAlphaToggle.getElement()
            );
        }

        init();

        this.increaseSize = function (f) {
            if (!brush.getIsDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.getIsDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };

        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };

        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y, p);
        };
        this.goLine = function (x, y, p, isCoalesced) {
            brush.goLine(x, y, p, isCoalesced);
        };
        this.endLine = function () {
            brush.endLine();
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.getIsDrawing();
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<BlendBrush>['Ui'];

    return brushInterface;
})();


code:
import {IRGB, ISliderConfig} from '../kl-types';

export type TBrushSettingEmit = { type: 'color'; value: IRGB } |
    { type: 'opacity'; value: number } |
    { type: 'size'; value: number } |
    { type: 'sliderConfig'; value: ISliderConfig };

export type TBrushSettingSubscriber = (p : TBrushSettingEmit) => void;

/**
 * Central place to update brush settings, and to subscribe to changes.
 */
export class BrushSettingService {

    private static instance: BrushSettingService;
    subscriberArr: TBrushSettingSubscriber[] = [];

    private emit (obj: TBrushSettingEmit, skipSubscriber?: TBrushSettingSubscriber): void {
        for (let i = 0; i < this.subscriberArr.length; i++) {
            if (this.subscriberArr[i] === skipSubscriber) {
                continue;
            }
            this.subscriberArr[i](obj);
        }
    }

    // --- public ---

    constructor (
        private onSetColor: (rgb: IRGB) => void,
        private onSetSize: (size: number) => void,
        private onSetOpacity: (opacity: number) => void,
        private onGetColor: () => IRGB,
        private onGetSize: () => number,
        private onGetOpacity: () => number,
        private onGetSliderConfig: () => ISliderConfig,
    ) {
        if (BrushSettingService.instance) {
            throw new Error('BrushSettingService already instantiated');
        }
        BrushSettingService.instance = this;
    }

    emitColor (color: IRGB, skipSubscriber?: TBrushSettingSubscriber): void {
        this.emit(
            {
                type: 'color',
                value: color,
            },
            skipSubscriber
        );
    }

    emitSize (size: number, skipSubscriber?: TBrushSettingSubscriber): void {
        this.emit(
            {
                type: 'size',
                value: size,
            },
            skipSubscriber
        );
    }

    emitOpacity (opacity: number, skipSubscriber?: TBrushSettingSubscriber): void {
        this.emit(
            {
                type: 'opacity',
                value: opacity,
            },
            skipSubscriber
        );
    }

    emitSliderConfig (sliderConfig: ISliderConfig, skipSubscriber?: TBrushSettingSubscriber) {
        this.emit(
            {
                type: 'sliderConfig',
                value: sliderConfig,
            },
            skipSubscriber
        );
    }

    /**
     * set current brush color
     * @param color
     * @param skipSubscriber
     */
    setColor (color: IRGB, skipSubscriber?: TBrushSettingSubscriber) {
        this.onSetColor(color);
        this.emitColor(color, skipSubscriber);
    }

    /**
     * set current brush size
     * @param size
     * @param skipSubscriber
     */
    setSize (size: number, skipSubscriber?: TBrushSettingSubscriber) {
        this.onSetSize(size);
        // why not emitting?
    }

    /**
     * set current opacity
     * @param opacity
     * @param skipSubscriber
     */
    setOpacity (opacity: number, skipSubscriber?: TBrushSettingSubscriber) {
        this.onSetOpacity(opacity);
        // why not emitting?
    }

    /**
     * get current brush color
     */
    getColor (): IRGB {
        return this.onGetColor();
    }

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

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

    getSliderConfig (): ISliderConfig {
        return this.onGetSliderConfig();
    }

    /**
     * subscribe to changes
     * @param func
     */
    subscribe (func: TBrushSettingSubscriber): void {
        if (this.subscriberArr.includes(func)) {
            return;
        }
        this.subscriberArr.push(func);
    }

    unsubscribe (func: TBrushSettingSubscriber): void {
        for (let i = 0; i < this.subscriberArr.length; i++) {
            if (func === this.subscriberArr[i]) {
                this.subscriberArr.splice(i, 1);
                i--;
            }
        }
    }
}


code: export const eventResMs = 20;


code:
import {penBrushUi} from './pen-brush-ui';
import {blendBrushUi} from './blend-brush-ui';
import {sketchyBrushUi} from './sketchy-brush-ui';
import {pixelBrushUi} from './pixel-brush-ui';
import {eraserBrushUi} from './eraser-brush-ui';
import {smudgeBrushUi} from './smudge-brush-ui';
import {chemyBrushUi} from './chemy-brush-ui';
import {IBrushUi} from '../kl-types';

/**
 * UI for brushes.
 * Each brush ui carries the brush with it.
 * So if you want to draw, you do it through the UI. should be changed sometime.
 */

export const brushesUI: {
    [key: string]: IBrushUi<any>;
} = {
    penBrush: penBrushUi,
    blendBrush: blendBrushUi,
    sketchyBrush: sketchyBrushUi,
    pixelBrush: pixelBrushUi,
    chemyBrush: chemyBrushUi,
    smudgeBrush: smudgeBrushUi,
    eraserBrush: eraserBrushUi,
};


code:
import {BB} from '../../bb/bb';
import {eventResMs} from './brushes-consts';
import {Checkbox} from '../ui/components/checkbox';
import {brushes} from '../brushes/brushes';
import {klHistory} from '../history/kl-history';
import {KlSlider} from '../ui/components/kl-slider';
import brushIconImg from '/src/app/img/ui/brush-chemy.svg';
import {IBrushUi} from '../kl-types';
import {Options} from '../ui/components/options';
import {BoxToggle} from '../ui/components/box-toggle';
import {LANG, languageStrings} from '../../language/language';
import {ChemyBrush} from '../brushes/chemy-brush';

export const chemyBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-chemy'),
        sizeSlider: {
            min: 0.25,
            max: 25,
            curve: BB.quadraticSplineInput(0.25, 25, 0.1),
            isDisabled: true,
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
        },
    } as IBrushUi<ChemyBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-chemy');
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.ChemyBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());

        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        function setSize (size: number) {
            brush.setSize(size);
        }

        let eraserToggle: Checkbox;

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 250,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: brush.getSize(),
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                isEnabled: brush.getMode() === 'stroke',
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
                formatFunc: (displayValue) => {
                    if (displayValue < 5) {
                        return BB.round(displayValue, 1);
                    } else {
                        return Math.round(displayValue);
                    }
                },
                manualInputRoundDigits: 1,
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 250,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brush.getOpacity(),
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });

            BB.css(opacitySlider.getElement(), {
                marginTop: '10px',
            });

            eraserToggle = new Checkbox({
                init: brush.getIsEraser(),
                label: LANG('eraser'),
                callback: function (b) {
                    brush.setIsEraser(b);
                },
                css: {
                    marginTop: '10px',
                    marginLeft: '10px',
                },
            });

            const lockAlphaToggle = new Checkbox({
                init: brush.getLockAlpha(),
                label: LANG('lock-alpha'),
                callback: function (b) {
                    brush.setLockAlpha(b);
                },
                doHighlight: true,
                title: LANG('lock-alpha-title'),
                css: {
                    marginTop: '10px',
                },
            });

            const toggleRow = BB.el({
                css: {
                    display: 'flex',
                    marginTop: '10px',
                },
            });

            const iconSize = 35;
            const padding = 8;
            const actualIconSize = iconSize - padding * 2;
            const halfSize = actualIconSize / 2;

            const modeOptions = new Options({
                optionArr: [
                    {
                        id: 'fill',
                        label: BB.createSvg({
                            class: 'dark-invert',
                            elementType: 'svg',
                            width: iconSize + '',
                            height: iconSize + '',
                            childrenArr: [
                                {
                                    elementType: 'path',
                                    fill: '#000',
                                    style: `transform-origin: 0 0; transform: translate(-0.5px, -0.5px) scale(${actualIconSize}, ${actualIconSize}) translate(0.5px, 0.5px)`,
                                    d: 'M 0,0 C 1.5,0 -0.5,1 1,1',
                                },
                            ],
                        }),
                        title: LANG('brush-chemy-fill'),
                    },
                    {
                        id: 'stroke',
                        label: BB.createSvg({
                            class: 'dark-invert',
                            elementType: 'svg',
                            width: iconSize + '',
                            height: iconSize + '',
                            childrenArr: [
                                {
                                    elementType: 'path',
                                    fill: 'none',
                                    stroke: '#000',
                                    style: `stroke-width: 0.12px; transform-origin: 0 0; transform: translate(-0.5px, -0.5px) scale(${actualIconSize}, ${actualIconSize}) translate(0.5px, 0.5px)`,
                                    d: 'M 0,0 C 1.5,0 -0.5,1 1,1',
                                },
                            ],
                        }),
                        title: LANG('brush-chemy-stroke'),
                    },
                ],
                initId: brush.getMode(),
                onChange: (id: string): void => {
                    brush.setMode(id as 'stroke' | 'fill');
                    brushInterface.sizeSlider.isDisabled = brush.getMode() === 'fill';
                    sizeSlider.setIsEnabled(!brushInterface.sizeSlider.isDisabled);

                    const brushSize = brush.getSize();
                    sizeSlider.setValue(brushSize);
                    p.onSizeChange(brushSize);
                    p.onConfigChange();
                },
            });

            const mirrorXToggle = new BoxToggle({
                label: BB.createSvg({
                    class: 'dark-invert',
                    elementType: 'svg',
                    width: iconSize + '',
                    height: iconSize + '',
                    childrenArr: [
                        {
                            elementType: 'path',
                            fill: 'none',
                            stroke: '#000',
                            style: 'stroke-width: 1px',
                            d: `M ${halfSize + padding},${padding} ${halfSize + padding},${actualIconSize + padding}`,
                        },
                    ],
                }),
                title: LANG('brush-chemy-mirror-x'),
                init: brush.getXSymmetry(),
                onChange: (b) => {
                    brush.setXSymmetry(b);
                },
            });

            const mirrorYToggle = new BoxToggle({
                label: BB.createSvg({
                    class: 'dark-invert',
                    elementType: 'svg',
                    width: iconSize + '',
                    height: iconSize + '',
                    childrenArr: [
                        {
                            elementType: 'path',
                            fill: 'none',
                            stroke: '#000',
                            style: 'stroke-width: 1px',
                            d: `M ${padding},${halfSize + padding} ${actualIconSize + padding},${halfSize + padding}`,
                        },
                    ],
                }),
                title: LANG('brush-chemy-mirror-y'),
                init: brush.getYSymmetry(),
                onChange: (b) => {
                    brush.setYSymmetry(b);
                },
            });

            const gradientToggle = new BoxToggle({
                label: BB.createSvg({
                    class: 'dark-invert',
                    elementType: 'svg',
                    width: iconSize + '',
                    height: iconSize + '',
                    childrenArr: [
                        {
                            elementType: 'defs',
                            childrenArr: [
                                {
                                    elementType: 'linearGradient',
                                    id: 'gradient',
                                    x1: '0',
                                    y1: '0',
                                    x2: '0',
                                    y2: '1',
                                    childrenArr: [
                                        {
                                            elementType: 'stop',
                                            offset: '0%',
                                            'stop-color': 'rgba(0,0,0,0)',
                                        },
                                        {
                                            elementType: 'stop',
                                            offset: '100%',
                                            'stop-color': 'rgba(0,0,0,1)',
                                        },
                                    ],
                                },
                            ],
                        },
                        {
                            elementType: 'rect',
                            fill: "url('#gradient')",
                            x: '' + padding,
                            y: '' + padding,
                            width: '' + actualIconSize,
                            height: '' + actualIconSize,
                        },
                    ],
                }),
                title: LANG('brush-chemy-gradient'),
                init: brush.getGradient(),
                onChange: (b) => {
                    brush.setGradient(b);
                },
            });

            BB.css(mirrorXToggle.getElement(), {
                marginLeft: '10px',
            });
            {
                const margin = {
                    marginLeft: '4px',
                };
                BB.css(mirrorYToggle.getElement(), margin);
                BB.css(gradientToggle.getElement(), margin);
            }

            toggleRow.append(
                modeOptions.getElement(),
                mirrorXToggle.getElement(),
                mirrorYToggle.getElement(),
                gradientToggle.getElement(),
            );

            div.append(
                sizeSlider.getElement(),
                opacitySlider.getElement(),
                toggleRow,
                BB.el({
                    content: [
                        lockAlphaToggle.getElement(),
                        eraserToggle.getElement(),
                    ],
                    css: {
                        display: 'flex',
                    },
                })
            );
        }

        init();

        this.increaseSize = function (f) {
            if (!brush.getIsDrawing() && brush.getMode() === 'stroke') {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.getIsDrawing() && brush.getMode() === 'stroke') {
                sizeSlider.changeSliderValue(-f);
            }
        };

        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };

        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y);
        };
        this.goLine = function (x, y, p, isCoalesced) {
            brush.goLine(x, y);
        };
        this.endLine = function () {
            brush.endLine();
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.getIsDrawing();
        };
        this.toggleEraser = () => {
            eraserToggle.setValue(!eraserToggle.getValue());
            brush.setIsEraser(eraserToggle.getValue());
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<ChemyBrush>['Ui'];

    return brushInterface;
})();


code:
import {BB} from '../../bb/bb';
import {brushes} from '../brushes/brushes';
import {eventResMs} from './brushes-consts';
import {klHistory} from '../history/kl-history';
import {KlSlider} from '../ui/components/kl-slider';
import {createPenPressureToggle} from '../ui/components/create-pen-pressure-toggle';
import {Checkbox} from '../ui/components/checkbox';
import brushIconImg from '/src/app/img/ui/brush-eraser.svg';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {EraserBrush} from '../brushes/eraser-brush';

export const eraserBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('eraser') + ' [E]',
        sizeSlider: {
            min: 0.5,
            max: 200,
            curve: BB.quadraticSplineInput(0.5, 200, 0.1),
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
        },
    } as IBrushUi<EraserBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('eraser') + ' [E]';
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.EraserBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());

        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;
        let isTransparentBg = false;

        function setSize (size: number) {
            brush.setSize(size);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 225,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: 30,
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
                formatFunc: (displayValue) => {
                    if (displayValue < 10) {
                        return BB.round(displayValue, 1);
                    } else {
                        return Math.round(displayValue);
                    }
                },
                manualInputRoundDigits: 1,
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 225,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brushInterface.opacitySlider.max,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });

            const pressureSizeToggle = createPenPressureToggle(true, function (b) {
                brush.sizePressure(b);
            });
            const pressureOpacityToggle = createPenPressureToggle(false, function (b) {
                brush.opacityPressure(b);
            });

            div.append(
                BB.el({
                    content: [
                        sizeSlider.getElement(),
                        pressureSizeToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                        marginBottom: '10px',
                    },
                }),
                BB.el({
                    content: [
                        opacitySlider.getElement(),
                        pressureOpacityToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                    },
                })
            );

            const transparencyToggle = new Checkbox({
                init: false,
                label: LANG('brush-eraser-transparent-bg'),
                callback: function (b) {
                    isTransparentBg = b;
                    brush.setTransparentBG(b);
                },
                css: {
                    marginTop: '10px',
                },
            });
            div.append(transparencyToggle.getElement());
        }

        init();

        this.increaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };
        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };
        this.setColor = function () {
        };
        this.setContext = function (c) {
            brush.setContext(c as Parameters<EraserBrush['setContext']>[0]);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y, p);
        };
        this.goLine = function (x, y, p) {
            brush.goLine(x, y, p);
        };
        this.endLine = function () {
            brush.endLine();
        };
        this.getBrush = function () {
            return brush;
        };
        this.getIsTransparentBg = function () {
            return isTransparentBg;
        };
        this.isDrawing = function () {
            return brush.isDrawing();
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<EraserBrush>['Ui'];

    return brushInterface;
})();


code:
import {BB} from '../../bb/bb';
import {brushes} from '../brushes/brushes';
import {eventResMs} from './brushes-consts';
import {klHistory} from '../history/kl-history';
import {Checkbox} from '../ui/components/checkbox';
import {KlSlider} from '../ui/components/kl-slider';
import {createPenPressureToggle} from '../ui/components/create-pen-pressure-toggle';
import brushIconImg from '/src/app/img/ui/brush-pen.svg';
import {genBrushAlpha01, genBrushAlpha02} from '../brushes/alphas/brush-alphas';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {Options} from '../ui/components/options';
import {PenBrush} from '../brushes/pen-brush';

export const penBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-pen'),
        sizeSlider: {
            min: 0.5,
            max: 100,
            curve: BB.quadraticSplineInput(0.5, 100, 0.1),
        },
        opacitySlider: {
            min: 1/100,
            max: 1,
            curve: [[0, 1/100], [0.5, 30/100], [1, 1]],
        },
    } as IBrushUi<PenBrush>;

    let alphaNames = [
        LANG('brush-pen-circle'),
        LANG('brush-pen-chalk'),
        LANG('brush-pen-calligraphy'),
        LANG('brush-pen-square'),
    ];
    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-pen');
        alphaNames = [
            LANG('brush-pen-circle'),
            LANG('brush-pen-chalk'),
            LANG('brush-pen-calligraphy'),
            LANG('brush-pen-square'),
        ];
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.PenBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());
        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        const alphaOptions = new Options({
            optionArr: [0, 1, 2, 3].map(id => {

                const alpha = BB.el({
                    className: 'dark-invert',
                    css: {
                        width: '31px',
                        height: '31px',
                        backgroundSize: 'contain',
                        margin: '2px',
                    },
                });
                const canvas = BB.canvas(70, 70);
                const ctx = BB.ctx(canvas);
                if (id === 0 || id === 3) {
                    if (id === 0) {
                        ctx.beginPath();
                        ctx.arc(35, 35, 30, 0, 2 * Math.PI);
                        ctx.closePath();
                        ctx.fill();
                    } else {
                        ctx.fillRect(5, 5, 60, 60);
                    }
                } else if (id === 1) {
                    ctx.drawImage(genBrushAlpha01(60), 5, 5);
                } else if (id === 2) {
                    ctx.drawImage(genBrushAlpha02(60), 5, 5);
                }
                alpha.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')';

                return {
                    id: id,
                    label: alpha,
                    title: alphaNames[id],
                };
            }),
            initId: 0,
            onChange: (id) => {
                brush.setAlpha(id);
            },
        });

        const lockAlphaToggle = new Checkbox({
            init: brush.getLockAlpha(),
            label: LANG('lock-alpha'),
            callback: function (b) {
                brush.setLockAlpha(b);
            },
            doHighlight: true,
            title: LANG('lock-alpha-title'),
            css: {
                display: 'inline-block',
            },
        });

        const spacingSpline = new BB.SplineInterpolator([[0, 15], [8, 7], [14, 4], [30, 3], [50, 2.7], [100, 2]]);

        function setSize (size: number) {
            brush.setSize(size);
            brush.setSpacing(Math.max(2, spacingSpline.interpolate(size)) / 15);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 225,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: brush.getSize(),
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
                formatFunc: (displayValue) => {
                    if (displayValue < 10) {
                        return BB.round(displayValue, 1);
                    } else {
                        return Math.round(displayValue);
                    }
                },
                manualInputRoundDigits: 1,
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 225,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brushInterface.opacitySlider.max,
                curve: brushInterface.opacitySlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });

            const pressureSizeToggle = createPenPressureToggle(true, function (b) {
                brush.sizePressure(b);
            });
            const pressureOpacityToggle = createPenPressureToggle(false, function (b) {
                brush.opacityPressure(b);
            });

            div.append(
                BB.el({
                    content: [
                        sizeSlider.getElement(),
                        pressureSizeToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                        marginBottom: '10px',
                    },
                }),
                BB.el({
                    content: [
                        opacitySlider.getElement(),
                        pressureOpacityToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                    },
                }),
                BB.el({
                    content: alphaOptions.getElement(),
                    css: {
                        marginTop: '10px',
                    },
                }),
                BB.el({
                    content: lockAlphaToggle.getElement(),
                    css: {
                        marginTop: '10px',
                    },
                }),
            );

        }

        init();

        this.increaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };

        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };

        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y, p);
        };
        this.goLine = function (x, y, p) {
            brush.goLine(x, y, p);
        };
        this.endLine = function (x, y) {
            brush.endLine(x, y);
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.isDrawing();
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<PenBrush>['Ui'];
    return brushInterface;
})();


code:
import {BB} from '../../bb/bb';
import {brushes} from '../brushes/brushes';
import {eventResMs} from './brushes-consts';
import {klHistory} from '../history/kl-history';
import {Checkbox} from '../ui/components/checkbox';
import {KlSlider} from '../ui/components/kl-slider';
import {createPenPressureToggle} from '../ui/components/create-pen-pressure-toggle';
import brushIconImg from '/src/app/img/ui/brush-pixel.svg';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {PixelBrush} from '../brushes/pixel-brush';

export const pixelBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-pixel'),
        sizeSlider: {
            min: 0.5,
            max: 100,
            curve: BB.quadraticSplineInput(0.5, 100, 0.1),
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
            curve: [[0, 1 / 100], [0.5, 0.3], [1, 1]],
        },
    } as IBrushUi<PixelBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-pixel');
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.PixelBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());
        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        const lockAlphaToggle = new Checkbox({
            init: brush.getLockAlpha(),
            label: LANG('lock-alpha'),
            callback: function (b) {
                brush.setLockAlpha(b);
            },
            doHighlight: true,
            title: LANG('lock-alpha-title'),
            css: {
                marginRight: '10px',
            },
        });

        const eraserToggle = new Checkbox({
            init: brush.getIsEraser(),
            label: LANG('eraser'),
            callback: function (b) {
                brush.setIsEraser(b);
            },
            css: {
                marginRight: '10px',
            },
        });

        const ditherToggle = new Checkbox({
            init: brush.getUseDither(),
            label: LANG('brush-pixel-dither'),
            callback: function (b) {
                brush.setUseDither(b);
            },
        });

        const spacingSpline = new BB.SplineInterpolator([[0.5, 0.45], [100, 4]]);

        function setSize (size: number) {
            brush.setSize(size);
            brush.setSpacing(spacingSpline.interpolate(size) / size);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 225,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: brush.getSize(),
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 225,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brushInterface.opacitySlider.max,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });

            const pressureSizeToggle = createPenPressureToggle(true, function (b) {
                brush.sizePressure(b);
            });

            div.append(
                BB.el({
                    content: [
                        sizeSlider.getElement(),
                        pressureSizeToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                        marginBottom: '10px',
                    },
                }),
                opacitySlider.getElement()
            );

            const toggleRow = BB.el({
                parent: div,
                css: {
                    display: 'flex',
                    marginTop: '10px',
                },
            });

            toggleRow.append(
                lockAlphaToggle.getElement(),
                eraserToggle.getElement(),
                ditherToggle.getElement(),
            );
        }

        init();

        this.increaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };

        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size * 2);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity * 100);
        };

        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y, p);
        };
        this.goLine = function (x, y, p) {
            brush.goLine(x, y, p);
        };
        this.endLine = function (x, y) {
            brush.endLine(x, y);
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.isDrawing();
        };
        this.toggleEraser = () => {
            eraserToggle.setValue(!eraserToggle.getValue());
            brush.setIsEraser(eraserToggle.getValue());
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<PixelBrush>['Ui'];
    return brushInterface;
})();


code:
import {brushes} from '../brushes/brushes';
import {eventResMs} from './brushes-consts';
import {klHistory} from '../history/kl-history';
import {KlSlider} from '../ui/components/kl-slider';
import brushIconImg from '/src/app/img/ui/brush-sketchy.png';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {BB} from '../../bb/bb';
import {SketchyBrush} from '../brushes/sketchy-brush';

export const sketchyBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-sketchy'),
        sizeSlider: {
            min: 0.5,
            max: 10,
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
        },
    } as IBrushUi<SketchyBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-sketchy');
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.SketchyBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());
        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        function setSize (size: number) {
            brush.setSize(size);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 250,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: brush.getSize() * 2,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: function (val) {
                    brush.setSize(val);
                    p.onSizeChange(val);
                },
                formatFunc: (displayValue) => {
                    if (displayValue < 10) {
                        return BB.round(displayValue, 1);
                    } else {
                        return Math.round(displayValue);
                    }
                },
                manualInputRoundDigits: 1,
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 250,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brush.getOpacity(),
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });
            const blendSlider = new KlSlider({
                label: LANG('brush-blending'),
                width: 250,
                height: 30,
                min: 0,
                max: 1,
                value: brush.getBlending(),
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: function (val) {
                    brush.setBlending(val);
                },
            });
            const scaleSlider = new KlSlider({
                label: LANG('brush-sketchy-scale'),
                width: 250,
                height: 30,
                min: 1,
                max: 20,
                value: brush.getScale(),
                eventResMs: eventResMs,
                onChange: function (val) {
                    brush.setScale(val);
                },
            });
            opacitySlider.getElement().style.marginTop = '10px';
            blendSlider.getElement().style.marginTop = '10px';
            scaleSlider.getElement().style.marginTop = '10px';
            div.append(
                sizeSlider.getElement(),
                opacitySlider.getElement(),
                blendSlider.getElement(),
                scaleSlider.getElement(),
            );
        }

        init();

        this.increaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.isDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };
        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };
        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, pressure) {
            brush.startLine(x, y, pressure);
        };
        this.goLine = function (x, y, pressure) {
            brush.goLine(x, y, pressure, undefined);
        };
        this.endLine = function () {
            brush.endLine();
        };
        this.getSeed = function () {
            return parseInt('' + brush.getSeed());
        };
        this.setSeed = function (s) {
            brush.setSeed(parseInt('' + s));
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.isDrawing();
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<SketchyBrush>['Ui'];

    return brushInterface;
})();


code:
import {BB} from '../../bb/bb';
import {brushes} from '../brushes/brushes';
import {eventResMs} from './brushes-consts';
import {klHistory} from '../history/kl-history';
import {Checkbox} from '../ui/components/checkbox';
import {KlSlider} from '../ui/components/kl-slider';
import {createPenPressureToggle} from '../ui/components/create-pen-pressure-toggle';
import brushIconImg from '/src/app/img/ui/brush-smudge.svg';
import {IBrushUi} from '../kl-types';
import {LANG, languageStrings} from '../../language/language';
import {SmudgeBrush} from '../brushes/smudge-brush';

export const smudgeBrushUi = (function () {
    const brushInterface = {
        image: brushIconImg,
        tooltip: LANG('brush-smudge'),
        sizeSlider: {
            min: 0.5,
            max: 100,
            curve: BB.quadraticSplineInput(0.5, 100, 0.1),
        },
        opacitySlider: {
            min: 1 / 100,
            max: 1,
            curve: [[0, 1 / 100], [0.5, 0.3], [1, 1]],
        },
    } as IBrushUi<SmudgeBrush>;

    languageStrings.subscribe(() => {
        brushInterface.tooltip = LANG('brush-smudge');
    });

    brushInterface.Ui = function (p) {
        const div = document.createElement('div'); // the gui
        const brush = new brushes.SmudgeBrush();
        brush.setHistory(klHistory);
        p.onSizeChange(brush.getSize());
        let sizeSlider: KlSlider;
        let opacitySlider: KlSlider;

        const lockAlphaToggle = new Checkbox({
            init: brush.getLockAlpha(),
            label: LANG('lock-alpha'),
            callback: function (b) {
                brush.setLockAlpha(b);
            },
            doHighlight: true,
            title: LANG('lock-alpha-title'),
        });

        const spacingSpline = new BB.SplineInterpolator([[0, 15], [8, 7], [14, 4], [30, 3], [50, 2.7], [100, 2]]);

        function setSize (size: number) {
            brush.setSize(size);
            brush.setSpacing(Math.max(2, spacingSpline.interpolate(size)) / 15);
        }

        function init () {
            sizeSlider = new KlSlider({
                label: LANG('brush-size'),
                width: 225,
                height: 30,
                min: brushInterface.sizeSlider.min,
                max: brushInterface.sizeSlider.max,
                value: brush.getSize(),
                curve: brushInterface.sizeSlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 2,
                toValue: (displayValue) => displayValue / 2,
                onChange: (val) => {
                    setSize(val);
                    p.onSizeChange(val);
                },
                formatFunc: (displayValue) => {
                    if (displayValue < 10) {
                        return BB.round(displayValue, 1);
                    } else {
                        return Math.round(displayValue);
                    }
                },
                manualInputRoundDigits: 1,
            });
            opacitySlider = new KlSlider({
                label: LANG('opacity'),
                width: 225,
                height: 30,
                min: brushInterface.opacitySlider.min,
                max: brushInterface.opacitySlider.max,
                value: brush.getOpacity(),
                curve: brushInterface.opacitySlider.curve,
                eventResMs: eventResMs,
                toDisplayValue: (val) => val * 100,
                toValue: (displayValue) => displayValue / 100,
                onChange: (val) => {
                    brush.setOpacity(val);
                    p.onOpacityChange(val);
                },
            });

            const pressureSizeToggle = createPenPressureToggle(false, function (b) {
                brush.sizePressure(b);
            });
            const pressureOpacityToggle = createPenPressureToggle(false, function (b) {
                brush.opacityPressure(b);
            });

            div.append(
                BB.el({
                    content: [
                        sizeSlider.getElement(),
                        pressureSizeToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                        marginBottom: '10px',
                    },
                }),
                BB.el({
                    content: [
                        opacitySlider.getElement(),
                        pressureOpacityToggle,
                    ],
                    css: {
                        display: 'flex',
                        justifyContent: 'space-between',
                        alignItems: 'center',
                    },
                })
            );

            const bottomRow = BB.el({
                parent: div,
                css: {
                    marginTop: '10px',
                },
            });
            bottomRow.append(lockAlphaToggle.getElement());

        }

        init();

        this.increaseSize = function (f) {
            if (!brush.getIsDrawing()) {
                sizeSlider.changeSliderValue(f);
            }
        };
        this.decreaseSize = function (f) {
            if (!brush.getIsDrawing()) {
                sizeSlider.changeSliderValue(-f);
            }
        };

        this.getSize = function () {
            return brush.getSize();
        };
        this.setSize = function (size) {
            setSize(size);
            sizeSlider.setValue(size);
        };
        this.getOpacity = function () {
            return brush.getOpacity();
        };
        this.setOpacity = function (opacity) {
            brush.setOpacity(opacity);
            opacitySlider.setValue(opacity);
        };

        this.setColor = function (c) {
            brush.setColor(c);
        };
        this.setContext = function (c) {
            brush.setContext(c);
        };
        this.startLine = function (x, y, p) {
            brush.startLine(x, y, p);
        };
        this.goLine = function (x, y, p) {
            brush.goLine(x, y, p);
        };
        this.endLine = function () {
            brush.endLine();
        };
        this.getBrush = function () {
            return brush;
        };
        this.isDrawing = function () {
            return brush.getIsDrawing();
        };
        this.getElement = function () {
            return div;
        };
    } as IBrushUi<SmudgeBrush>['Ui'];
    return brushInterface;
})();


code:
import {BB} from '../../../bb/bb';
import {noise} from '../../../bb/math/perlin';
import {IVector2D} from '../../../bb/bb-types';

// chalk
export function genBrushAlpha01 (w: number): HTMLCanvasElement {

    const scaleFac = w / 500;
    const h = w;
    const canvas = BB.canvas(w, h);
    const ctx = BB.ctx(canvas);
    const imData = ctx.createImageData(w, h);
    for (let x = 0; x < w; x++) {
        for (let y = 0; y < h; y++) {
            const i = (y * w + x) * 4;

            // base noise
            const sFac2 = scaleFac + noise.simplex2(x / 50 / scaleFac, y / 50 / scaleFac) * 0.04;
            let noisePattern = 100 + (noise.simplex2(x / 50 / sFac2, y / 50 / sFac2) + 1) / 2 * 100;
            noisePattern -= (noise.simplex2(x / 10 / scaleFac, y / 10 / scaleFac) + 1) / 2 * 100;

            // fade out in circular shape
            const centerDist = BB.dist(w / 2, h / 2, x, y);
            const falloff = BB.clamp(
                1 - ((centerDist - (w / 2.5)) / (w / 14) + noise.simplex2(x / 22 / sFac2, y / 22 / sFac2)),
                0,
                1);
            noisePattern = noisePattern * falloff;

            // make the middle darker
            const falloff2 = BB.clamp(1 - centerDist / w, 0, 1) * 2;
            noisePattern = noisePattern * falloff2;

            imData.data[i] = 0;
            imData.data[i + 1] = 0;
            imData.data[i + 2] = 0;
            imData.data[i + 3] = BB.clamp(noisePattern, 0, 255);
        }
    }

    ctx.putImageData(imData, 0, 0);
    return canvas;
}

// https://www.shadertoy.com/view/3tdSDj
function udSegment ( p: IVector2D, a: IVector2D, b: IVector2D ): number {
    const ba = BB.Vec2.sub(b, a);
    const pa = BB.Vec2.sub(p, a);
    const h = BB.clamp( BB.Vec2.dot(pa,ba) / BB.Vec2.dot(ba,ba), 0.0, 1.0 );
    return BB.Vec2.len(BB.Vec2.sub(pa, BB.Vec2.mul(ba, h)));
}

// calligraphy
export function genBrushAlpha02 (w: number): HTMLCanvasElement {

    const pDist = 1 / 4;
    let centerSize = 2 / 3;
    let transitionSize = 1 / 3;
    centerSize *= pDist;
    transitionSize *= pDist;
    const p1 = {x: pDist * w, y: w - (w * pDist)};
    const p2 = {x: w - (w * pDist), y: pDist * w};

    const h = w;
    const canvas = BB.canvas(w, h);
    const ctx = BB.ctx(canvas);
    const imData = ctx.createImageData(w, h);
    for (let x = 0; x < w; x++) {
        for (let y = 0; y < h; y++) {
            const i = (y * w + x) * 4;

            let col = udSegment({x: x, y: y}, p1, p2);

            col = BB.clamp(
                255 - ((col - (w * centerSize)) / (w * transitionSize) * 255),
                0,
                255
            );

            imData.data[i] = 0;
            imData.data[i + 1] = 0;
            imData.data[i + 2] = 0;
            imData.data[i + 3] = col;
        }
    }

    ctx.putImageData(imData, 0, 0);
    return canvas;
}