code:
import {BB} from '../../bb/bb';
import {Checkbox} from '../ui/components/checkbox';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer, TMixMode} from '../kl-types';
import {LANG} from '../../language/language';
import {throwIfNull} from '../../bb/base/base';
import {TFilterHistoryEntry} from './filters';
import {Options} from '../ui/components/options';

export type TFilterFlipInput = {
    horizontal: boolean;
    vertical: boolean;
    flipCanvas: boolean;
};

export type TFilterFlipHistoryEntry = TFilterHistoryEntry<
    'flip',
    TFilterFlipInput>;

export const filterFlip = {

    getDialog (params: IFilterGetDialogParam) {
        const context = params.context;
        const klCanvas = params.klCanvas;
        if (!context || !klCanvas) {
            return false;
        }

        const layers = klCanvas.getLayers();
        const selectedLayerIndex = klCanvas.getLayerIndex(context.canvas);

        const fit = BB.fitInto(context.canvas.width, context.canvas.height, 280, 200, 1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterFlipInput> = {
            element: div,
        };
        let isHorizontal = true;
        let isVertical = false;
        let doFlipCanvas = true;
        div.innerHTML = LANG('filter-flip-description') + '

';

        const horizontalCheckbox = new Checkbox({
            init: isHorizontal,
            label: LANG('filter-flip-horizontal') + ' ⟷',
            allowTab: true,
            callback: function (v) {
                isHorizontal = v;
                updatePreview();
            },
            css: {
                marginBottom: '10px',
            },
        });
        const verticalCheckbox = new Checkbox({
            init: isVertical,
            label: LANG('filter-flip-vertical') + ' ↕',
            allowTab: true,
            callback: function (v) {
                isVertical = v;
                updatePreview();
            },
            css: {
                marginBottom: '10px',
            },
        });
        div.append(horizontalCheckbox.getElement());
        div.append(verticalCheckbox.getElement());

        const targetOptions = new Options<boolean>({
            optionArr: [
                {
                    id: true,
                    label: LANG('filter-flip-image'),
                },
                {
                    id: false,
                    label: LANG('filter-flip-layer'),
                },
            ],
            onChange: (val) => {
                doFlipCanvas = val;
                updatePreview();
            },
        });

        div.append(targetOptions.getElement());

        const previewWrapper = BB.el({
            className: 'kl-preview-wrapper',
            css: {
                width: '340px',
                height: '220px',
            },
        });

        const previewLayer: IKlBasicLayer = {
            image: BB.canvas(Math.round(w), Math.round(h)),
            isVisible: true,
            opacity: 1,
            mixModeStr: 'source-over' as TMixMode,
        };
        const klCanvasPreview = new KlCanvasPreview({
            width: Math.round(w),
            height: Math.round(h),
            layers: [previewLayer],
        });

        const previewInnerWrapper = BB.el({
            className: 'kl-preview-wrapper__canvas',
            css: {
                width: parseInt('' + w) + 'px',
                height: parseInt('' + h) + 'px',
            },
        });
        previewInnerWrapper.append(klCanvasPreview.getElement());
        previewWrapper.append(previewInnerWrapper);

        function updatePreview (): void {
            const ctx = BB.ctx((previewLayer.image as HTMLCanvasElement));
            ctx.save();
            ctx.clearRect(0, 0, previewLayer.image.width, previewLayer.image.height);

            if (doFlipCanvas) {
                if (isHorizontal) {
                    ctx.translate(previewLayer.image.width, 0);
                    ctx.scale(-1, 1);
                }
                if (isVertical) {
                    ctx.translate(0, previewLayer.image.height);
                    ctx.scale(1, -1);
                }
            }

            for (let i = 0; i < layers.length; i++) {
                if (!layers[i].isVisible) {
                    continue;
                }

                ctx.save();
                if (!doFlipCanvas && selectedLayerIndex === i) {
                    if (isHorizontal) {
                        ctx.translate(previewLayer.image.width, 0);
                        ctx.scale(-1, 1);
                    }
                    if (isVertical) {
                        ctx.translate(0, previewLayer.image.height);
                        ctx.scale(1, -1);
                    }
                }
                if (ctx.canvas.width > layers[i].context.canvas.width) {
                    ctx.imageSmoothingEnabled = false;
                }
                ctx.globalAlpha = layers[i].opacity;
                ctx.globalCompositeOperation = layers[i].mixModeStr;
                ctx.drawImage(layers[i].context.canvas, 0, 0, previewLayer.image.width, previewLayer.image.height);
                ctx.restore();
            }
            klCanvasPreview.render();
            ctx.restore();
        }
        setTimeout(updatePreview, 0);

        div.append(previewWrapper);
        result.destroy = (): void => {
            horizontalCheckbox.destroy();
            verticalCheckbox.destroy();
            targetOptions.destroy();
            klCanvasPreview.destroy();
        };
        result.getInput = function (): TFilterFlipInput {
            result.destroy!();
            return {
                horizontal: isHorizontal,
                vertical: isVertical,
                flipCanvas: doFlipCanvas,
            };
        };
        return result;
    },

    apply (params: IFilterApply<TFilterFlipInput>): boolean {
        const context = params.context;
        const klCanvas = params.klCanvas;
        const history = params.history;
        const horizontal = params.input.horizontal;
        const vertical = params.input.vertical;
        const flipCanvas = params.input.flipCanvas;
        if (!context || !klCanvas || !history) {
            return false;
        }

        history.pause(true);
        klCanvas.flip(
            horizontal,
            vertical,
            flipCanvas ? undefined : throwIfNull(klCanvas.getLayerIndex(context.canvas)),
        );
        history.pause(false);

        history.push({
            tool: ['filter', 'flip'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterFlipHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer} from '../kl-types';
import {LANG} from '../../language/language';
import {input} from '../ui/components/input';
import {ColorOptions} from '../ui/components/color-options';
import {drawGrid} from '../image-operations/draw-grid';
import {TFilterHistoryEntry} from './filters';
import {throwIfNull} from '../../bb/base/base';

export type TFilterGridInput = {
    x: number;
    y: number;
    thickness: number;
    color: string;
    opacity: number;
};

export type TFilterGridHistoryEntry = TFilterHistoryEntry<
    'grid',
    TFilterGridInput>;

export const filterGrid = {

    getDialog (params: IFilterGetDialogParam) {
        const context = params.context;
        const klCanvas = params.klCanvas;
        if (!context || !klCanvas) {
            return false;
        }

        const layers = klCanvas.getLayers();
        const selectedLayerIndex = throwIfNull(klCanvas.getLayerIndex(context.canvas));

        const fit = BB.fitInto(context.canvas.width, context.canvas.height, 280, 200, 1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);
        const renderW = Math.min(w, context.canvas.width);
        const renderH = Math.min(h, context.canvas.height);
        const renderFactor = renderW / context.canvas.width;

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterGridInput> = {
            element: div,
        };
        div.innerHTML = LANG('filter-grid-description') + '

';

        const settingsObj: TFilterGridInput = {
            x: 2,
            y: 2,
            thickness: 2,
            color: '#000',
            opacity: 1,
        };

        const line1 = BB.el({
            parent: div,
            css: {
                display: 'flex',
                alignItems: 'center',
            },
        });
        const line2 = BB.el({
            parent: div,
            css: {
                display: 'flex',
                alignItems: 'center',
                marginTop: '10px',
            },
        });
        const xInput = input({
            init: 2,
            type: 'number',
            min: 1,
            css: {width: '75px', marginRight: '20px'},
            callback: function (v) {
                settingsObj.x = parseFloat(v);
                updatePreview();
            },
        });
        const yInput = input({
            init: 2,
            type: 'number',
            min: 1,
            css: {width: '75px', marginRight: '20px'},
            callback: function (v) {
                settingsObj.y = parseFloat(v);
                updatePreview();
            },
        });
        const thicknessInput = input({
            init: 2,
            type: 'number',
            min: 1,
            css: {width: '75px', marginRight: '20px'},
            callback: function (v) {
                settingsObj.thickness = parseFloat(v);
                updatePreview();
            },
        });

        let selectedRgbaObj = {r: 0, g: 0, b: 0, a: 1};
        const colorOptionsArr = [
            {r: 0, g: 0, b: 0, a: 1},
            {r: 255, g: 255, b: 255, a: 1},
        ];
        colorOptionsArr.push({
            r: params.currentColorRgb.r,
            g: params.currentColorRgb.g,
            b: params.currentColorRgb.b,
            a: 1,
        });
        colorOptionsArr.push({
            r: params.secondaryColorRgb.r,
            g: params.secondaryColorRgb.g,
            b: params.secondaryColorRgb.b,
            a: 1,
        });
        settingsObj.color = BB.ColorConverter.toRgbStr(selectedRgbaObj);

        const colorOptions = new ColorOptions({
            label: LANG('shape-stroke'),
            colorArr: colorOptionsArr,
            onChange: function (rgbaObj) {
                selectedRgbaObj = rgbaObj!;
                settingsObj.color = BB.ColorConverter.toRgbStr(selectedRgbaObj);
                updatePreview();
            },
        });

        const labelStyle = {
            display: 'inline-block',
            marginRight: '5px',
        };
        line1.append(
            BB.el({content: 'X:', css: labelStyle}),
            xInput,
            BB.el({content: 'Y:', css: labelStyle}),
            yInput,
        );
        line2.append(
            BB.el({content: LANG('shape-line-width') + ':', css: labelStyle}),
            thicknessInput,
            BB.el({css:{flexGrow: '1'}}),
            colorOptions.getElement(),
        );

        const previewWrapper = BB.el({
            className: 'kl-preview-wrapper',
            css: {
                width: '340px',
                height: '220px',
            },
        });

        const previewLayer: IKlBasicLayer = {
            image: BB.canvas(renderW, renderH),
            isVisible: layers[selectedLayerIndex].isVisible,
            opacity: layers[selectedLayerIndex].opacity,
            mixModeStr: layers[selectedLayerIndex].mixModeStr,
        };
        const klCanvasPreview = new KlCanvasPreview({
            width: Math.round(w),
            height: Math.round(h),
            layers: layers.map((item, i) => {
                if (i === selectedLayerIndex) {
                    return previewLayer;
                } else {
                    return {
                        image: item.context.canvas,
                        isVisible: item.isVisible,
                        opacity: item.opacity,
                        mixModeStr: item.mixModeStr,
                    };
                }
            }),
        });

        const previewInnerWrapper = BB.el({
            className: 'kl-preview-wrapper__canvas',
            css: {
                width: parseInt('' + w) + 'px',
                height: parseInt('' + h) + 'px',
            },
        });
        previewInnerWrapper.append(klCanvasPreview.getElement());
        previewWrapper.append(previewInnerWrapper);

        function updatePreview (): void {
            const ctx = BB.ctx((previewLayer.image as HTMLCanvasElement));
            ctx.save();
            ctx.clearRect(0, 0, renderW, renderH);
            ctx.drawImage(context.canvas, 0, 0, renderW, renderH);
            drawGrid(ctx, settingsObj.x, settingsObj.y, Math.max(settingsObj.thickness * renderFactor, 1), settingsObj.color, settingsObj.opacity);

            ctx.restore();
            klCanvasPreview.render();
        }
        setTimeout(updatePreview, 0);

        div.append(previewWrapper);
        result.destroy = (): void => {
            klCanvasPreview.destroy();
            colorOptions.destroy();
        };
        result.getInput = function (): TFilterGridInput {
            result.destroy!();
            return BB.copyObj(settingsObj);
        };
        return result;
    },

    apply (params: IFilterApply<TFilterGridInput>): boolean {
        const context = params.context;
        const klCanvas = params.klCanvas;
        const history = params.history;
        if (!context || !klCanvas || !history) {
            return false;
        }

        history.pause(true);
        drawGrid(
            context,
            params.input.x,
            params.input.y,
            params.input.thickness,
            params.input.color,
            params.input.opacity,
        );
        history.pause(false);

        history.push({
            tool: ['filter', 'grid'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterGridHistoryEntry);
        return true;
    },

};

repo_name: Doggermelon/klecks

license: mit


code:
import {BB} from '../../bb/bb';
import {eventResMs} from './filters-consts';
import {KlSlider} from '../ui/components/kl-slider';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {getSharedFx} from '../../fx-canvas/shared-fx';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer} from '../kl-types';
import {LANG} from '../../language/language';
import {TFilterHistoryEntry} from './filters';

export type TFilterHueSaturationInput = {
    hue: number;
    saturation: number;
};

export type TFilterHueSaturationHistoryEntry = TFilterHistoryEntry<
    'hueSaturation',
    TFilterHueSaturationInput>;

export const filterHueSaturation = {

    getDialog (params: IFilterGetDialogParam) {

        const context = params.context;
        const klCanvas = params.klCanvas;
        if (!context || !klCanvas) {
            return false;
        }

        const layers = klCanvas.getLayers();
        const selectedLayerIndex = klCanvas.getLayerIndex(context.canvas);

        const fit = BB.fitInto(context.canvas.width, context.canvas.height,280, 200,  1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);

        const tempCanvas = BB.canvas(w, h);
        {
            const ctx = BB.ctx(tempCanvas);
            ctx.save();
            if (tempCanvas.width > context.canvas.width) {
                ctx.imageSmoothingEnabled = false;
            }
            ctx.drawImage(context.canvas, 0, 0, w, h);
            ctx.restore();
        }

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterHueSaturationInput> = {
            element: div,
        };

        function finishInit (): void {
            let hue = 0, saturation = 0;
            div.innerHTML = LANG('filter-hue-sat-description') + '

';

            const fxCanvas = getSharedFx();
            if (!fxCanvas) {
                return; // todo throw?
            }
            const texture = fxCanvas.texture(tempCanvas);
            fxCanvas.draw(texture).update(); // update fxCanvas size

            const hueSlider = new KlSlider({
                label: LANG('filter-hue-sat-hue'),
                width: 300,
                height: 30,
                min: -100,
                max: 100,
                value: hue * 100,
                eventResMs: eventResMs,
                onChange: function (val) {
                    hue = val / 100;
                    fxCanvas.draw(texture).hueSaturation(hue, saturation).update();
                    klCanvasPreview.render();
                },
            });
            const saturationSlider = new KlSlider({
                label: LANG('filter-hue-sat-saturation'),
                width: 300,
                height: 30,
                min: 0,
                max: 100,
                value: (saturation + 1) * 50,
                eventResMs: eventResMs,
                onChange: function (val) {
                    saturation = val / 50 - 1;
                    fxCanvas.draw(texture).hueSaturation(hue, saturation).update();
                    klCanvasPreview.render();
                },
            });
            hueSlider.getElement().style.marginBottom = '10px';
            div.append(hueSlider.getElement(), saturationSlider.getElement());

            const previewWrapper = BB.el({
                className: 'kl-preview-wrapper',
                css: {
                    width: '340px',
                    height: '220px',
                },
            });

            const previewLayerArr: IKlBasicLayer[] = [];
            {
                for (let i = 0; i < layers.length; i++) {
                    previewLayerArr.push({
                        image: i === selectedLayerIndex ? fxCanvas : layers[i].context.canvas,
                        isVisible: layers[i].isVisible,
                        opacity: layers[i].opacity,
                        mixModeStr: layers[i].mixModeStr,
                    });
                }
            }
            const klCanvasPreview = new KlCanvasPreview({
                width: parseInt('' + w),
                height: parseInt('' + h),
                layers: previewLayerArr,
            });

            const previewInnerWrapper = BB.el({
                className: 'kl-preview-wrapper__canvas',
                css: {
                    width: parseInt('' + w) + 'px',
                    height: parseInt('' + h) + 'px',
                },
            });
            previewInnerWrapper.append(klCanvasPreview.getElement());
            previewWrapper.append(previewInnerWrapper);

            div.append(previewWrapper);

            try {
                fxCanvas.draw(texture).hueSaturation(hue, saturation).update();
                klCanvasPreview.render();
            } catch(e) {
                (div as any).errorCallback(e);
            }

            result.destroy = (): void => {
                hueSlider.destroy();
                saturationSlider.destroy();
                texture.destroy();
                klCanvasPreview.destroy();
            };
            result.getInput = function (): TFilterHueSaturationInput {
                result.destroy!();
                return {
                    hue: hue,
                    saturation: saturation,
                };
            };
        }

        setTimeout(finishInit, 1);
        return result;
    },

    apply (params: IFilterApply<TFilterHueSaturationInput>): boolean {
        const context = params.context;
        const hue = params.input.hue;
        const history = params.history;
        const saturation = params.input.saturation;
        if (!context || hue === null || saturation === null || !history) {
            return false;
        }
        history.pause(true);
        const fxCanvas = getSharedFx();
        if (!fxCanvas) {
            return false; // todo more specific error?
        }
        const texture = fxCanvas.texture(context.canvas);
        fxCanvas.draw(texture).hueSaturation(hue, saturation).update();
        context.clearRect(0, 0, context.canvas.width, context.canvas.height);
        context.drawImage(fxCanvas, 0, 0);
        texture.destroy();
        history.pause(false);
        history.push({
            tool: ['filter', 'hueSaturation'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterHueSaturationHistoryEntry);
        return true;
    },

};


code:
import {getSharedFx} from '../../fx-canvas/shared-fx';
import {IFilterApply} from '../kl-types';
import {TFilterHistoryEntry} from './filters';

export type TFilterInvertInput = null;

export type TFilterInvertHistoryEntry = TFilterHistoryEntry<
    'invert',
    TFilterInvertInput>;

export const filterInvert = {

    apply (params: IFilterApply<TFilterInvertInput>): boolean {
        const context = params.context;
        const history = params.history;
        if (!context || !history) {
            return false;
        }

        const fxCanvas = getSharedFx();
        if (!fxCanvas) {
            return false;
        }

        history.pause(true);

        const texture = fxCanvas.texture(context.canvas);
        fxCanvas.draw(texture).invert().update();
        context.clearRect(0, 0, context.canvas.width, context.canvas.height);
        context.drawImage(fxCanvas, 0, 0);
        texture.destroy();

        history.pause(false);
        history.push({
            tool: ['filter', 'invert'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterInvertHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer, IRGB, TMixMode} from '../kl-types';
import {LANG} from '../../language/language';
import {KlSlider} from '../ui/components/kl-slider';
import {getSharedFx} from '../../fx-canvas/shared-fx';
import {Options} from '../ui/components/options';
import {eventResMs} from './filters-consts';
import {Select} from '../ui/components/select';
import {translateBlending} from '../canvas/translate-blending';
import {KL} from '../kl';
import {ColorConverter} from '../../bb/color/color';
import {Checkbox} from '../ui/components/checkbox';
import {TFxCanvas, TWrappedTexture} from '../../fx-canvas/fx-canvas-types';
import {TKlCanvasHistoryEntry} from '../canvas/kl-canvas';
import {throwIfNull} from '../../bb/base/base';

// see noise(...) in fx-canvas
interface INoisePreset {
    type: number;
    scaleX: number;
    scaleY: number;
    offsetX: number;
    offsetY: number;
    octaves: number;
    samples: number;
    peaks: number;
    brightness: number;
    contrast: number;
    isReversed: boolean;
}

type TNoiseChannels = 'rgb' | 'alpha';

interface INoiseSettings extends INoisePreset {
    seed?: number;
    colA?: IRGB;
    colB?: IRGB;
    channels?: TNoiseChannels; // default rgb
}

export type TFilterNoiseInput = {
    seed: number;
    presetIndex: number; // which preset from presetArr
    scale: number; // applies to both scaleX and scaleX
    opacity: number; // aka strength
    isReversed: boolean; // reverse direction of color gradient
    channels: TNoiseChannels;

    // only for channels = rgb
    mixModeStr: GlobalCompositeOperation; // how mixed with image
    colA: IRGB;
    colB: IRGB;
}

/*export type TFilterNoiseHistoryEntry = TFilterHistoryEntry<
    'replaceLayer',
    TFilterNoiseInput>;*/

const presetArr: INoisePreset[] = [
    // each pixel random value
    {type: 0, scaleX: 1, scaleY: 1, offsetX: 0, offsetY: 0, octaves: 1, samples: 1, peaks: 0, brightness: 0, contrast: 0, isReversed: true},

    // cloud
    {type: 1, scaleX: 166, scaleY: 164, offsetX: 105, offsetY: 30, octaves: 6, samples: 1, peaks: 0, brightness: 0.055, contrast: 0.23, isReversed: true},

    // thin lines
    {type: 1, scaleX: 235, scaleY: 190, offsetX: 3227, offsetY: 2156, octaves: 4, samples: 16, peaks: 22, brightness: -0.375, contrast: 1, isReversed: false},

    // soft large simplex, only 1 octave
    {type: 1, scaleX: 40, scaleY: 40, offsetX: 0, offsetY: 0, octaves: 1, samples: 1, peaks: 0, brightness: 0, contrast: 0, isReversed: false},

    // two value large pixels
    {type: 0, scaleX: 26, scaleY: 26, offsetX: 557, offsetY: 365, octaves: 1, samples: 1, peaks: 0, brightness: 0.02, contrast: 1, isReversed: true},

    // zebra
    {type: 1, scaleX: 1500, scaleY: 1500, offsetX: 745, offsetY: 2871, octaves: 5, samples: 16, peaks: 156.02, brightness: 0.03, contrast: 1, isReversed: true},

    // sparse dots / stars
    {type: 1, scaleX: 11, scaleY: 11, offsetX: 2940, offsetY: 2045, octaves: 1, samples: 16, peaks: 1, brightness: -0.045, contrast: 1, isReversed: true},

    // pseudo marble
    {type: 2, scaleX: 74, scaleY: 74, offsetX: 4816, offsetY: 1304, octaves: 3, samples: 1, peaks: 2.78, brightness: 0, contrast: 0, isReversed: false},
];

function drawNoise (fxCanvas: TFxCanvas, settings: INoiseSettings): void {
    fxCanvas.noise(
        settings.seed,
        settings.type,
        [settings.scaleX, settings.scaleY],
        [fxCanvas.width / 2, fxCanvas.height / 2],
        settings.octaves,
        settings.samples,
        settings.peaks,
        settings.brightness,
        settings.contrast,
        settings.isReversed,
        settings.colA,
        settings.colB,
        settings.channels ? settings.channels : 'rgb',
    ).update();
}

export const filterNoise = {

    getDialog (params: IFilterGetDialogParam) {
        const context = params.context;
        const klCanvas = params.klCanvas;
        if (!context || !klCanvas) {
            return false;
        }

        const layers = klCanvas.getLayers();
        const selectedLayerIndex = throwIfNull(klCanvas.getLayerIndex(context.canvas));

        const fit = BB.fitInto(context.canvas.width, context.canvas.height, 280, 200, 1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);
        const renderW = Math.min(w, context.canvas.width);
        const renderH = Math.min(h, context.canvas.height);
        const renderFactor = renderW / context.canvas.width;

        const fxCanvas = throwIfNull(getSharedFx());
        let texture: TWrappedTexture;

        const thumbImgArr: HTMLImageElement[] = [];
        const thumbSize = 32;
        {
            const canvas = BB.canvas(thumbSize, thumbSize);
            const ctx = BB.ctx(canvas);
            texture = fxCanvas.texture(canvas);
            fxCanvas.draw(texture).update(); // update fxCanvas size
            texture.destroy();

            presetArr.forEach((preset) => {
                const thumbImg = new Image();
                const presetCopy = BB.copyObj(preset) as INoiseSettings;
                presetCopy.scaleX /= 10;
                presetCopy.scaleY /= 10;
                drawNoise(fxCanvas, presetCopy);
                ctx.drawImage(fxCanvas, 0, 0);
                thumbImg.src = canvas.toDataURL('image/png');
                thumbImgArr.push(thumbImg);
            });

        }

        {
            const tempCanvas = BB.canvas(renderW, renderH);
            texture = fxCanvas.texture(tempCanvas);
            fxCanvas.draw(texture).update(); // update fxCanvas size
            BB.freeCanvas(tempCanvas);
        }

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterNoiseInput> = {
            element: div,
        };

        div.innerHTML = LANG('filter-noise-description') + '

';

        const settingsObj: TFilterNoiseInput = {
            seed: Math.random() * 300,
            presetIndex: 0,
            scale: 50,
            opacity: 0.5,
            isReversed: false,
            channels: 'rgb',
            mixModeStr: 'source-over' as GlobalCompositeOperation,
            colA: {r: 0, g: 0, b: 0},
            colB: {r: 255, g: 255, b: 255},
        };

        const presetOptions = new Options({
            optionArr: thumbImgArr.map((img, index) => {
                BB.css(img, {
                    margin: '1px',
                    borderRadius: '3px',
                    transition: 'all 0.1s ease-in-out',
                });
                return {
                    id: '' + index,
                    label: img,
                };
            }),
            initId: '0',
            onChange: (id) => {
                settingsObj.presetIndex = Number(id);
                updatePreview();
            },
        });
        presetOptions.getElement().style.marginBottom = '10px';
        div.append(presetOptions.getElement());

        const scaleSlider = new KlSlider({
            label: LANG('filter-noise-scale'),
            width: 300,
            height: 30,
            min: 1,
            max: 1000,
            value: settingsObj.scale,
            eventResMs: eventResMs,
            curve: BB.quadraticSplineInput(1, 1000, 0.1),
            onChange: (value) => {
                settingsObj.scale = value;
                updatePreview();
            },
        });
        scaleSlider.getElement().style.marginBottom = '10px';

        const opacitySlider = new KlSlider({
            label: LANG('opacity'),
            width: 300,
            height: 30,
            min: 1 / 100,
            max: 1,
            value: settingsObj.opacity,
            eventResMs: eventResMs,
            toValue: (displayValue) => displayValue / 100,
            toDisplayValue: (value) => value * 100,
            onChange: (value) => {
                settingsObj.opacity = value;
                updatePreview();
            },
        });
        opacitySlider.getElement().style.marginBottom = '10px';

        const row1El = BB.el({
            css: {
                display: 'flex',
                alignItems: 'center',
                marginBottom: '10px',
            },
        });

        const row2El = BB.el({
            css: {
                display: 'flex',
            },
        });

        const channelsOptions = new Options<TNoiseChannels>({
            optionArr: [
                {id: 'rgb', label: 'RGB'},
                {id: 'alpha', label: LANG('filter-noise-alpha')},
            ],
            initId: 'rgb',
            onChange: (id: TNoiseChannels) => {
                settingsObj.channels = id;
                if (id === 'rgb') {
                    row2El.style.visibility = '';
                } else {
                    row2El.style.visibility = 'hidden';
                }
                updatePreview();
            },
        });

        const reverseToggle = new Checkbox({
            label: LANG('reverse'),
            callback: (val) => {
                settingsObj.isReversed = val;
                updatePreview();
            },
            allowTab: true,
        });

        const mixModes: (TMixMode | undefined)[] = [
            'source-over',
            undefined,
            'darken',
            'multiply',
            'color-burn',
            undefined,
            'lighten',
            'screen',
            'color-dodge',
            undefined,
            'overlay',
            'soft-light',
            'hard-light',
            undefined,
            'difference',
            'exclusion',
            undefined,
            'hue',
            'saturation',
            'color',
            'luminosity',
        ];

        const blendSelect = new Select({
            isFocusable: true,
            optionArr: mixModes.map(item => {
                return item ? ([item, translateBlending(item)] as [TMixMode, string]) : undefined;
            }),
            initValue: settingsObj.mixModeStr,
            onChange: (val: GlobalCompositeOperation) => {
                settingsObj.mixModeStr = val;
                updatePreview();
            },
        });
        blendSelect.getElement().title = LANG('layers-blending');

        const colorWrapper = BB.el({
            css: {
                display: 'flex',
            },
        });

        const colInputStyle = {
            width: '34px',
            height: '34px',
            marginRight: '5px',
        };
        const colAInput = KL.input({
            type: 'color',
            init: '#' + ColorConverter.toHexString(settingsObj.colA),
            callback: (val) => {
                const newColor = ColorConverter.hexToRGB(val);
                if (newColor) {
                    settingsObj.colA = newColor;
                    updatePreview();
                }
            },
            css: colInputStyle,
        });

        const colBInput = KL.input({
            type: 'color',
            init: '#' + ColorConverter.toHexString(settingsObj.colB),
            callback: (val) => {
                const newColor = ColorConverter.hexToRGB(val);
                if (newColor) {
                    settingsObj.colB = newColor;
                    updatePreview();
                }
            },
            css: colInputStyle,
        });

        colorWrapper.append(
            colAInput,
            colBInput,
        );

        row1El.append(
            channelsOptions.getElement(),
            BB.el({css:{flexGrow: '1'}}),
            reverseToggle.getElement(),
        );

        row2El.append(
            blendSelect.getElement(),
            BB.el({css:{flexGrow: '1'}}),
            colorWrapper,
        );

        div.append(
            scaleSlider.getElement(),
            opacitySlider.getElement(),
            row1El,
            row2El,
        );

        const previewWrapper = BB.el({
            className: 'kl-preview-wrapper',
            css: {
                width: '340px',
                height: '220px',
            },
        });

        const previewLayer: IKlBasicLayer = {
            image: BB.canvas(renderW, renderH),
            isVisible: layers[selectedLayerIndex].isVisible,
            opacity: layers[selectedLayerIndex].opacity,
            mixModeStr: layers[selectedLayerIndex].mixModeStr,
        };
        const klCanvasPreview = new KlCanvasPreview({
            width: Math.round(w),
            height: Math.round(h),
            layers: layers.map((item, i) => {
                if (i === selectedLayerIndex) {
                    return previewLayer;
                } else {
                    return {
                        image: item.context.canvas,
                        isVisible: item.isVisible,
                        opacity: item.opacity,
                        mixModeStr: item.mixModeStr,
                    };
                }
            }),
        });

        const previewInnerWrapper = BB.el({
            className: 'kl-preview-wrapper__canvas',
            css: {
                width: parseInt('' + w) + 'px',
                height: parseInt('' + h) + 'px',
            },
        });
        previewInnerWrapper.append(klCanvasPreview.getElement());
        previewWrapper.append(previewInnerWrapper);

        function updatePreview (): void {
            const ctx = BB.ctx((previewLayer.image as HTMLCanvasElement));
            ctx.save();
            ctx.clearRect(0, 0, renderW, renderH);
            ctx.drawImage(context.canvas, 0, 0, renderW, renderH);

            ctx.globalAlpha = settingsObj.opacity;

            const presetCopy = BB.copyObj(presetArr[settingsObj.presetIndex]) as INoiseSettings;
            presetCopy.seed = settingsObj.seed;
            presetCopy.scaleX = presetCopy.scaleX  * settingsObj.scale / 50 * renderFactor;
            presetCopy.scaleY = presetCopy.scaleY  * settingsObj.scale / 50 * renderFactor;
            presetCopy.colA = settingsObj.colA;
            presetCopy.colB = settingsObj.colB;
            presetCopy.isReversed = settingsObj.isReversed ? !presetCopy.isReversed : presetCopy.isReversed;
            presetCopy.channels  = settingsObj.channels;
            drawNoise(fxCanvas, presetCopy);

            if (settingsObj.channels === 'alpha') {
                ctx.globalCompositeOperation = 'destination-out';
            } else {
                ctx.globalCompositeOperation = settingsObj.mixModeStr;
            }

            ctx.drawImage(fxCanvas, 0, 0);

            ctx.restore();
            klCanvasPreview.render();
        }
        setTimeout(updatePreview, 0);

        div.append(previewWrapper);
        result.destroy = (): void => {
            presetOptions.destroy();
            scaleSlider.destroy();
            opacitySlider.destroy();
            reverseToggle.destroy();
            channelsOptions.destroy();
            texture.destroy();
            blendSelect.destroy();
            klCanvasPreview.destroy();
        };
        result.getInput = function (): TFilterNoiseInput {
            result.destroy!();
            return BB.copyObj(settingsObj);
        };

        return result;
    },

    apply (params: IFilterApply<TFilterNoiseInput>): boolean {
        const context = params.context;
        const klCanvas = params.klCanvas;
        const history = params.history;
        if (!context || !klCanvas || !history) {
            return false;
        }

        history.pause(true);

        const fxCanvas = getSharedFx();
        if (!fxCanvas) {
            return false; // todo more specific error?
        }
        const texture = fxCanvas.texture(context.canvas);
        fxCanvas.draw(texture).update();
        texture.destroy();

        const input = params.input;

        const presetCopy: INoiseSettings = BB.copyObj(presetArr[input.presetIndex]) as INoiseSettings;
        presetCopy.seed = input.seed;
        presetCopy.scaleX = presetCopy.scaleX  * input.scale / 50;
        presetCopy.scaleY = presetCopy.scaleY  * input.scale / 50;
        presetCopy.colA = input.colA;
        presetCopy.colB = input.colB;
        presetCopy.isReversed = input.isReversed ? !presetCopy.isReversed : presetCopy.isReversed;
        presetCopy.channels  = input.channels;
        drawNoise(fxCanvas, presetCopy);

        context.save();
        context.globalAlpha = input.opacity;
        if (input.channels === 'alpha') {
            context.globalCompositeOperation = 'destination-out';
        } else {
            context.globalCompositeOperation = input.mixModeStr;
        }
        context.drawImage(fxCanvas, 0, 0);
        context.restore();

        history.pause(false);

        history.push({
            tool: ['canvas'],
            action: 'replaceLayer',
            params: [
                klCanvas.getLayerIndex(context.canvas),
                context.getImageData(0, 0, context.canvas.width, context.canvas.height),
            ],
        } as TKlCanvasHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer} from '../kl-types';
import {input} from '../ui/components/input';
import {KlSlider} from '../ui/components/kl-slider';
import {LANG} from '../../language/language';
import {eventResMs} from './filters-consts';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {TwoTabs} from '../ui/components/two-tabs';
import {IVector2D} from '../../bb/bb-types';
import {TFilterHistoryEntry} from './filters';
import {throwIfNull} from '../../bb/base/base';

export type TFilterPatternInput = {
    x: number;
    y: number;
    width: number;
    height: number;
    blend: number; // [0, 1] - 1 will blend linearly across whole length
    offsetX: number;
    offsetY: number;
};

export type TFilterPatternHistoryEntry = TFilterHistoryEntry<
    'pattern',
    TFilterPatternInput>;

/**
 * Draws pattern onto context. Pattern generated from context.
 * Can use blending for smoother transition. Will use area outside of bounds for blending.
 *
 * @param context
 * @param settings
 */
function drawPattern (context: CanvasRenderingContext2D, settings: TFilterPatternInput): void {

    // keep bounds in center via offset
    // because blending done towards bottom right
    const blendOffsetX = settings.blend ? Math.round(settings.blend * settings.width / 2) : 0;
    const blendOffsetY = settings.blend ? Math.round(settings.blend * settings.height / 2) : 0;

    const finalPatternCanvas = BB.canvas(settings.width, settings.height);

    if (settings.blend) {
        // construct pattern via linear blending

        const blendCanvas = BB.canvas(settings.width * 2, settings.height * 2);
        const blendCtx = BB.ctx(blendCanvas);
        const colTransparent = '#0000';
        const colOpaque = '#000';

        // transfer source to blendCanvas
        blendCtx.drawImage(
            context.canvas,
            -settings.x + blendOffsetX,
            -settings.y + blendOffsetY,
        );

        // --- 1 cross-fade vertical ------------------------

        // erase vertical gradient bottom half
        blendCtx.save();
        let blendGradient = blendCtx.createLinearGradient(0, settings.height, 0, settings.height * 2);
        blendGradient.addColorStop(0, colOpaque);
        blendGradient.addColorStop(settings.blend, colTransparent);
        blendCtx.globalCompositeOperation = 'destination-in';
        blendCtx.fillStyle = blendGradient;
        blendCtx.fillRect(0, 0, blendCanvas.width, blendCanvas.height);
        blendCtx.restore();

        // erase vertical gradient top half
        blendCtx.save();
        blendGradient = blendCtx.createLinearGradient(0, 0, 0, settings.height);
        blendGradient.addColorStop(0, colTransparent);
        blendGradient.addColorStop(settings.blend, colOpaque);
        blendCtx.globalCompositeOperation = 'destination-in';
        blendCtx.fillStyle = blendGradient;
        blendCtx.fillRect(0, 0, settings.width * 2, settings.height * 2);
        blendCtx.restore();

        // draw bottom half over top half
        blendCtx.save();
        // lighter needed for accurate cross-fade
        blendCtx.globalCompositeOperation = 'lighter';
        blendCtx.drawImage(blendCanvas, 0, -settings.height);
        blendCtx.restore();

        // --- 2 cross-fade horizontal ------------------------

        // erase horizontal gradient right half
        blendCtx.save();
        blendGradient = blendCtx.createLinearGradient(settings.width, 0, settings.width * 2, 0);
        blendGradient.addColorStop(0, colOpaque);
        blendGradient.addColorStop(settings.blend, colTransparent);
        blendCtx.globalCompositeOperation = 'destination-in';
        blendCtx.fillStyle = blendGradient;
        blendCtx.fillRect(0, 0, blendCanvas.width, blendCanvas.height);
        blendCtx.restore();

        // erase horizontal gradient left half
        blendCtx.save();
        blendGradient = blendCtx.createLinearGradient(0, 0, settings.width, 0);
        blendGradient.addColorStop(0, colTransparent);
        blendGradient.addColorStop(settings.blend, colOpaque);
        blendCtx.globalCompositeOperation = 'destination-in';
        blendCtx.fillStyle = blendGradient;
        blendCtx.fillRect(0, 0, settings.width * 2, settings.height * 2);
        blendCtx.restore();

        // draw right half over left half
        blendCtx.save();
        // lighter needed for accurate cross-fade
        blendCtx.globalCompositeOperation = 'lighter';
        blendCtx.drawImage(blendCanvas, -settings.width, 0);
        blendCtx.restore();

        // transfer to pattern canvas
        BB.ctx(finalPatternCanvas).drawImage(blendCanvas, 0, 0);

    } else {
        BB.ctx(finalPatternCanvas).drawImage(
            context.canvas,
            -settings.x,
            -settings.y,
        );
    }

    context.save();

    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
    context.translate(settings.offsetX - blendOffsetX, settings.offsetY - blendOffsetY);
    context.fillStyle = throwIfNull(context.createPattern(finalPatternCanvas, 'repeat'));
    context.fillRect(-settings.offsetX + blendOffsetX, -settings.offsetY + blendOffsetY, context.canvas.width, context.canvas.height);

    context.restore();
}

export const filterPattern = {

    getDialog (params: IFilterGetDialogParam) {

        const isSmall = window.innerWidth < 550;
        const maxSize = 1024;
        const rootEl = BB.el({
            content: LANG('filter-pattern-description') + '

',
        });
        const context = params.context;
        const width = context.canvas.width;
        const height = context.canvas.height;

        let settings: TFilterPatternInput = {
            x: 0,
            y: 0,
            width: width <= 250 ? Math.round(width / 4) : 200,
            height: height <= 250 ? Math.round(height / 4) : 200,
            blend: 0,
            offsetX: 0,
            offsetY: 0,
        };
        let lastDrawnSettings: TFilterPatternInput | undefined;

        // determine bounds
        const bounds = BB.canvasBounds(context);
        // adjust settings according to bounds
        if ( // use layer bounds if:
            bounds &&
            (bounds.width <= maxSize && bounds.height <= maxSize) && // don't exceed max size
            (bounds.width < width * 0.75 || bounds.height < height * 0.75) // aren't too large (heuristic)
        ) {
            settings.x = bounds.x;
            settings.y = bounds.y;
            settings.width = bounds.width;
            settings.height = bounds.height;
        } else { // otherwise use default size in center
            settings.x = Math.round(width / 2 - settings.width / 2);
            settings.y = Math.round(height / 2 - settings.height / 2);
        }

        // ---- controls ----

        const xInput = input({
            init: settings.x,
            type: 'number',
            min: 0,
            max: width,
            css: {width: '100%'},
            callback: function (v) {
                settings.x = Number(v);
                updatePreview();
            },
        });
        const yInput = input({
            init: settings.y,
            type: 'number',
            min: 0,
            max: height,
            css: {width: '100%'},
            callback: function (v) {
                settings.y = Number(v);
                updatePreview();
            },
        });
        const widthInput = input({
            init: settings.width,
            type: 'number',
            min: 1,
            max: Math.min(maxSize, width),
            css: {width: '100%'},
            callback: function (v) {
                settings.width = Number(v);
                updatePreview();
            },
        });
        const heightInput = input({
            init: settings.height,
            type: 'number',
            min: 1,
            max: Math.min(maxSize, height),
            css: {width: '100%'},
            callback: function (v) {
                settings.height = Number(v);
                updatePreview();
            },
        });

        const inputStyle = {
            marginLeft: '5px',
            flex: '1',
        };
        rootEl.append(
            BB.el({
               content: [

                   BB.el({
                       tagName: 'label',
                       content: [
                           'X:',
                           xInput,
                       ],
                       css: inputStyle,
                   }),

                   BB.el({
                       tagName: 'label',
                       content: [
                           'Y:',
                           yInput,
                       ],
                       css: inputStyle,
                   }),

                   BB.el({
                       tagName: 'label',
                       content: [
                           LANG('width') + ':',
                           widthInput,
                       ],
                       css: inputStyle,
                   }),

                   BB.el({
                       tagName: 'label',
                       content: [
                           LANG('height') + ':',
                           heightInput,
                       ],
                       css: inputStyle,
                   }),
               ],
               css: {
                   display: 'flex',
                   marginLeft: '-5px',
               },
            }),
        );

        const blendSlider = new KlSlider({
            label: LANG('brush-blending'),
            width: 300,
            height: 30,
            min: 0,
            max: 1,
            value: settings.blend,
            eventResMs: eventResMs,
            onChange: function (val) {
                settings.blend = val;
                updatePreview();
            },
            formatFunc: (val) => {
                return BB.round(val, 2);
            },
            manualInputRoundDigits: 2,
        });
        BB.css(blendSlider.getElement(), {
            margin: '10px 0',
        });

        rootEl.append(
            blendSlider.getElement(),
        );

        // ---- preview tabs ----
        let previewMode = 1; // 0 before, 1 after
        const beforeAfterTabs = new TwoTabs({
            left: LANG('compare-before'),
            right: LANG('compare-after'),
            init: previewMode,
            onChange: (val) => {
                previewMode = val;
                overlayCanvas.style.display = val === 0 ? 'block' : 'none';
                updatePreview(true);
            },
        });
        rootEl.append(beforeAfterTabs.getElement());

        // ---- previews ----

        const klCanvas = params.klCanvas;
        const layers = klCanvas.getLayers();
        const selectedLayerIndex = throwIfNull(klCanvas.getLayerIndex(context.canvas));

        const fit = BB.fitInto(context.canvas.width, context.canvas.height, isSmall ? 280 : 490, isSmall ? 200 : 240, 1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);
        const renderW = Math.min(w, context.canvas.width);
        const renderH = Math.min(h, context.canvas.height);
        // const renderFactor = renderW / context.canvas.width;
        const previewFactor = w / context.canvas.width;

        const previewWrapper = BB.el({
            className: 'kl-preview-wrapper',
            css: {
                width: isSmall ? '340px' : '540px',
                height: isSmall ? '260px' : '300px',
                marginTop: '0',
            },
        });

        const previewLayer: IKlBasicLayer = {
            image: BB.canvas(renderW, renderH),
            isVisible: layers[selectedLayerIndex].isVisible,
            opacity: layers[selectedLayerIndex].opacity,
            mixModeStr: layers[selectedLayerIndex].mixModeStr,
        };
        const previewLayerArr = layers.map((item, i) => {
            if (i === selectedLayerIndex) {
                return previewLayer;
            } else {
                return {
                    image: item.context.canvas,
                    isVisible: item.isVisible,
                    opacity: item.opacity,
                    mixModeStr: item.mixModeStr,
                };
            }
        });
        const klCanvasPreview = new KlCanvasPreview({
            width: Math.round(w),
            height: Math.round(h),
            layers: previewLayerArr,
        });

        const overlayCanvas = BB.canvas(w, h);
        BB.css(overlayCanvas, {
            position: 'absolute',
            left: '0',
            top: '0',
            mixBlendMode: 'difference',
            imageRendering: 'pixelated',
        });

        const previewInnerWrapper = BB.el({
            className: 'kl-preview-wrapper__canvas',
            css: {
                width: parseInt('' + w) + 'px',
                height: parseInt('' + h) + 'px',
            },
        });
        previewInnerWrapper.append(
            klCanvasPreview.getElement(),
            overlayCanvas
        );
        previewWrapper.append(previewInnerWrapper);
        rootEl.append(previewWrapper);

        // ---- preview input processing ----
        const inputs = {} as {
            start: IVector2D;
            end: IVector2D | null;
            oldSettings: TFilterPatternInput;
            state: null | 'move' | 'select';
        };

        function syncInputs (): void {
            xInput.value = '' + settings.x;
            yInput.value = '' + settings.y;
            widthInput.value = '' + settings.width;
            heightInput.value = '' + settings.height;
        }

        const keyListener = new BB.KeyListener({
        });

        previewWrapper.oncontextmenu = function () {
            return false;
        };
        previewInnerWrapper.style.touchAction = 'none';
        const pointerListener = new BB.PointerListener({
            target: previewInnerWrapper,
            onPointer: (event) => {
                if (previewMode === 0) {
                    if (event.type === 'pointerdown') {
                        if (!inputs.state) {
                            inputs.oldSettings = BB.copyObj(settings);

                            const x = event.relX / previewFactor;
                            const y = event.relY / previewFactor;

                            if (BB.isInsideRect(
                                {x, y},
                                {x: settings.x, y: settings.y, width: settings.width, height: settings.height},
                            )) {
                                inputs.state = 'move';
                            } else {
                                inputs.state = 'select';
                            }
                            inputs.start = {x, y};
                        }
                    } else if (event.type === 'pointermove') {

                        const x = event.relX / previewFactor;
                        const y = event.relY / previewFactor;

                        if (inputs.state === 'select') {
                            inputs.end = {x, y};

                            const x1 = Math.max(0, Math.min(inputs.start.x, inputs.end.x));
                            const y1 = Math.max(0, Math.min(inputs.start.y, inputs.end.y));
                            const x2 = Math.min(width, Math.max(inputs.start.x, inputs.end.x));
                            const y2 = Math.min(height, Math.max(inputs.start.y, inputs.end.y));

                            settings.x = Math.floor(x1);
                            settings.y = Math.floor(y1);
                            settings.width = Math.min(maxSize, Math.ceil(x2 - settings.x));
                            settings.height = Math.min(maxSize, Math.ceil(y2 - settings.y));

                            if (keyListener.isPressed('shift')) {
                                settings.width = Math.min(settings.width, settings.height);
                                settings.height = Math.min(settings.width, settings.height);
                            }

                            if (settings.width === 0 || settings.height === 0) {
                                settings = BB.copyObj(inputs.oldSettings);
                            }

                            syncInputs();
                            updatePreview();
                        } else if (inputs.state === 'move') {
                            const dX = Math.round(x - inputs.start.x);
                            const dY = Math.round(y - inputs.start.y);

                            settings.x = BB.clamp(inputs.oldSettings.x + dX, 0, width - settings.width);
                            settings.y = BB.clamp(inputs.oldSettings.y + dY, 0, height - settings.height);

                            syncInputs();
                            updatePreview();
                        }
                    } else if (event.type === 'pointerup') {
                        if (inputs.state) {
                            inputs.state = null;
                        }
                    }
                } else {
                    if (event.type === 'pointerdown') {
                        if (!inputs.state) {
                            inputs.state = 'move';
                            inputs.oldSettings = BB.copyObj(settings);
                            inputs.start = {x: event.relX / previewFactor, y: event.relY / previewFactor};
                            inputs.end = null;
                        }
                    } else if (event.type === 'pointermove') {
                        if (inputs.state) {
                            inputs.end = {x: event.relX / previewFactor, y: event.relY / previewFactor};

                            settings.offsetX = Math.round(inputs.end.x - inputs.start.x) + inputs.oldSettings.offsetX;
                            settings.offsetY = Math.round(inputs.end.y - inputs.start.y) + inputs.oldSettings.offsetY;

                            updatePreview();
                        }
                    } else if (event.type === 'pointerup') {
                        if (inputs.state) {
                            if (!inputs.end) {
                                settings.offsetX = 0;
                                settings.offsetY = 0;
                                updatePreview();
                            }
                            inputs.state = null;
                        }
                    }
                }
            },
        });

        // ---------- rendering ---------------------
        const fullSizeCanvas = BB.canvas(width, height);
        const fullSizeCtx = BB.ctx(fullSizeCanvas);

        function sharpStrokeRect (
            context: CanvasRenderingContext2D,
            x: number,
            y: number,
            width: number,
            height: number
        ): void {
            const drawX = Math.round(x + 0.5) - 0.5;
            const drawY = Math.round(y + 0.5) - 0.5;
            const drawWidth = Math.round(x + width - drawX);
            const drawHeight = Math.round(y + height - drawY);
            context.strokeRect(drawX, drawY, drawWidth, drawHeight);
        }

        function updatePreview (doForce?: boolean) {
            if (!doForce && lastDrawnSettings && JSON.stringify(lastDrawnSettings) === JSON.stringify(settings)) {
                return;
            }

            if (previewMode === 0) { // before
                fullSizeCtx.clearRect(0, 0, width, height);
                fullSizeCtx.drawImage(context.canvas, 0, 0);

                const previewCanvas = previewLayer.image as HTMLCanvasElement;
                const previewCtx = BB.ctx(previewCanvas);
                previewCtx.save();
                previewCtx.clearRect(0, 0, renderW, renderH);
                previewCtx.drawImage(fullSizeCanvas, 0, 0, renderW, renderH);
                previewCtx.restore();

                const pW = settings.width * previewFactor;
                const pH = settings.height * previewFactor;

                const overlayCtx = BB.ctx(overlayCanvas);
                overlayCtx.save();
                overlayCtx.clearRect(0, 0, w, h);
                overlayCtx.strokeStyle = '#fff';
                sharpStrokeRect(
                    overlayCtx,
                    settings.x * previewFactor,
                    settings.y * previewFactor,
                    pW,
                    pH
                );
                if (settings.blend > 0.05) {
                    overlayCtx.strokeStyle = '#f0f';
                    sharpStrokeRect(
                        overlayCtx,
                        settings.x * previewFactor - pW / 2 * settings.blend,
                        settings.y * previewFactor - pH / 2 * settings.blend,
                        pW * (1 + settings.blend),
                        pH * (1 + settings.blend),
                    );
                }
                overlayCtx.restore();

            } else { // after
                fullSizeCtx.clearRect(0, 0, width, height);
                fullSizeCtx.drawImage(context.canvas, 0, 0);
                drawPattern(fullSizeCtx, settings);

                const previewCanvas = previewLayer.image as HTMLCanvasElement;
                const previewCtx = BB.ctx(previewCanvas);
                previewCtx.clearRect(0, 0, renderW, renderH);
                previewCtx.drawImage(fullSizeCanvas, 0, 0, renderW, renderH);
            }

            klCanvasPreview.render();
            lastDrawnSettings = BB.copyObj(settings);
        }

        updatePreview();

        // ----- result -------------------
        const destroy = () => {
            blendSlider.destroy();
            keyListener.destroy();
            pointerListener.destroy();
            klCanvasPreview.destroy();
        };
        const result: IFilterGetDialogResult<TFilterPatternInput> = {
            element: rootEl,
            destroy,
            getInput: () => {
                destroy();
                return BB.copyObj(settings);
            },
        };
        if (!isSmall) {
            result.width = 500;
        }
        return result;
    },

    apply (params: IFilterApply<TFilterPatternInput>): boolean {
        const klCanvas = params.klCanvas;
        const ctx = params.context;
        const history = params.history;
        if (!klCanvas || !history) {
            return false;
        }
        history.pause(true);

        drawPattern(ctx, params.input);

        history.pause(false);
        history.push({
            tool: ['filter', 'pattern'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterPatternHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {KlCanvasPreview} from '../canvas-ui/canvas-preview';
import {getSharedFx} from '../../fx-canvas/shared-fx';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult, IKlBasicLayer} from '../kl-types';
import {LANG} from '../../language/language';
import {TwoTabs} from '../ui/components/two-tabs';
import {TRectanglePoints} from '../../fx-canvas/filters/perspective';
import {TFilterHistoryEntry} from './filters';
import {PointerListener} from '../../bb/input/pointer-listener';
import {IVector2D} from '../../bb/bb-types';
import {throwIfNull} from '../../bb/base/base';

export type TFilterPerspectiveInput = {
    before: TRectanglePoints;
    after: TRectanglePoints;
};

export type TFilterPerspectiveHistoryEntry = TFilterHistoryEntry<
    'perspective',
    TFilterPerspectiveInput>;

type TDraggable = HTMLDivElement & {
    x: number;
    y: number;
    copy: (p: IVector2D) => void;
};

export const filterPerspective = {

    getDialog (params: IFilterGetDialogParam) {
        const context = params.context;
        const klCanvas = params.klCanvas;
        if (!context || !klCanvas) {
            return false;
        }

        const isSmall = window.innerWidth < 550;
        const layers = klCanvas.getLayers();
        const selectedLayerIndex = klCanvas.getLayerIndex(context.canvas);

        const fit = BB.fitInto(context.canvas.width, context.canvas.height, isSmall ? 280 : 490, isSmall ? 200 : 240, 1);
        const displayW = parseInt('' + fit.width), displayH = parseInt('' + fit.height);
        const w = Math.min(displayW, context.canvas.width);
        const h = Math.min(displayH, context.canvas.height);

        const tempCanvas = BB.canvas(w, h);
        {
            const ctx = BB.ctx(tempCanvas);
            ctx.save();
            if (w > context.canvas.width) {
                ctx.imageSmoothingEnabled = false;
            }
            ctx.drawImage(context.canvas, 0, 0, w, h);
            ctx.restore();
        }
        const displayPreviewFactor = displayW / context.canvas.width;

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterPerspectiveInput> = {
            element: div,
        };
        if (!isSmall) {
            result.width = 500;
        }

        const pointerListenerArr: PointerListener[] = [];

        function finishInit (): void {
            div.innerHTML = LANG('filter-perspective-description') + '

';

            const fxCanvas = throwIfNull(getSharedFx());
            const texture = fxCanvas.texture(tempCanvas);
            fxCanvas.draw(texture).update(); // update fxCanvas size
            function update (): void {
                try {
                    fxCanvas.draw(texture).multiplyAlpha().perspective(
                        [ba.x, ba.y, bb.x, bb.y, bc.x, bc.y, bd.x, bd.y].map((item, i) => {
                            if (i % 2 === 0) {
                                return item / displayW * w;
                            } else {
                                return item / displayH * h;
                            }
                        }) as TRectanglePoints,
                        [aa.x, aa.y, ab.x, ab.y, ac.x, ac.y, ad.x, ad.y].map((item, i) => {
                            if (i % 2 === 0) {
                                return item / displayW * w;
                            } else {
                                return item / displayH * h;
                            }
                        }) as TRectanglePoints
                    ).unmultiplyAlpha().update();
                    klCanvasPreview.render();
                } catch(e) {
                    (div as any).errorCallback(e);
                }
            }

            function createDraggable (x: number, y: number, callback?: () => void): TDraggable {
                const size = 14;
                const div = document.createElement('div') as TDraggable;
                div.x = x;
                div.y = y;
                BB.css(div, {
                    width: size + 'px',
                    height: size + 'px',
                    backgroundColor: '#fff',
                    boxShadow: 'inset 0 0 0 2px #000',
                    borderRadius: size + 'px',
                    position: 'absolute',
                    cursor: 'move',
                    left: (div.x - size / 2) + 'px',
                    top: (div.y - size / 2) + 'px',
                    userSelect: 'none',
                    touchAction: 'none',
                });

                const pointerListener = new BB.PointerListener({
                    target: div,
                    onPointer: function (event) {
                        event.eventPreventDefault();
                        if (event.button === 'left' && event.type === 'pointermove') {
                            div.x += event.dX;
                            div.y += event.dY;
                            div.style.left = (div.x - size / 2) + 'px';
                            div.style.top = (div.y - size / 2) + 'px';
                            if (callback) {
                                callback();
                            }
                            update();
                        }
                    },
                });
                div.copy = function (p) {
                    div.x = p.x;
                    div.y = p.y;
                    div.style.left = (div.x - size / 2) + 'px';
                    div.style.top = (div.y - size / 2) + 'px';
                };
                pointerListenerArr.push(pointerListener);
                return div;
            }

            function updateAfter () {
                aa.copy(ba);
                ab.copy(bb);
                ac.copy(bc);
                ad.copy(bd);
            }

            // b - before
            const ba = createDraggable(0, 0, updateAfter);
            const bb = createDraggable(displayW, 0, updateAfter);
            const bc = createDraggable(displayW, displayH, updateAfter);
            const bd = createDraggable(0, displayH, updateAfter);
            // a - after
            const aa = createDraggable(0, 0);
            const ab = createDraggable(displayW, 0);
            const ac = createDraggable(displayW, displayH);
            const ad = createDraggable(0, displayH);

            let before = false;

            const beforeAfterTabs = new TwoTabs({
                left: LANG('compare-before'),
                right: LANG('compare-after'),
                init: 1,
                onChange: (val: number) => {
                    before = val === 0;
                    if (before) {
                        aa.style.display = 'none';
                        ab.style.display = 'none';
                        ac.style.display = 'none';
                        ad.style.display = 'none';

                        ba.style.display = 'block';
                        bb.style.display = 'block';
                        bc.style.display = 'block';
                        bd.style.display = 'block';
                        ba.copy(aa);
                        bb.copy(ab);
                        bc.copy(ac);
                        bd.copy(ad);
                    } else {
                        ba.style.display = 'none';
                        bb.style.display = 'none';
                        bc.style.display = 'none';
                        bd.style.display = 'none';

                        aa.style.display = 'block';
                        ab.style.display = 'block';
                        ac.style.display = 'block';
                        ad.style.display = 'block';
                        aa.copy(ba);
                        ab.copy(bb);
                        ac.copy(bc);
                        ad.copy(bd);
                    }
                    update();
                },
            });
            div.append(beforeAfterTabs.getElement());

            const previewWrapper = BB.el({
                className: 'kl-preview-wrapper',
                css: {
                    width: isSmall ? '340px' : '540px',
                    height: isSmall ? '260px' : '300px',
                    marginTop: '0',
                },
            });
            previewWrapper.oncontextmenu = function () {
                return false;
            };

            const previewLayerArr: IKlBasicLayer[] = [];
            {
                for (let i = 0; i < layers.length; i++) {
                    const canvas = i === selectedLayerIndex ? fxCanvas : layers[i].context.canvas;
                    previewLayerArr.push({
                        image: canvas,
                        isVisible: layers[i].isVisible,
                        opacity: layers[i].opacity,
                        mixModeStr: layers[i].mixModeStr,
                    });
                }
            }
            const klCanvasPreview = new KlCanvasPreview({
                width: parseInt('' + displayW),
                height: parseInt('' + displayH),
                layers: previewLayerArr,
            });

            const previewInnerWrapper = BB.el({
                className: 'kl-preview-wrapper__canvas',
                css: {
                    width: parseInt('' + displayW) + 'px',
                    height: parseInt('' + displayH) + 'px',
                },
            });
            previewInnerWrapper.append(klCanvasPreview.getElement());
            previewWrapper.append(previewInnerWrapper);

            previewInnerWrapper.append(aa, ab, ac, ad);

            ba.style.display = 'none';
            bb.style.display = 'none';
            bc.style.display = 'none';
            bd.style.display = 'none';
            previewInnerWrapper.append(ba, bb, bc, bd);

            div.append(previewWrapper);
            update();
            result.destroy = (): void => {
                for (let i = 0; i < pointerListenerArr.length; i++) {
                    pointerListenerArr[i].destroy();
                }
                texture.destroy();
                klCanvasPreview.destroy();
            };
            result.getInput = function (): TFilterPerspectiveInput {
                result.destroy!();
                return {
                    before: [
                        ba.x / displayPreviewFactor,
                        ba.y / displayPreviewFactor,

                        bb.x / displayPreviewFactor,
                        bb.y / displayPreviewFactor,

                        bc.x / displayPreviewFactor,
                        bc.y / displayPreviewFactor,

                        bd.x / displayPreviewFactor,
                        bd.y / displayPreviewFactor,
                    ],
                    after: [
                        aa.x / displayPreviewFactor,
                        aa.y / displayPreviewFactor,

                        ab.x / displayPreviewFactor,
                        ab.y / displayPreviewFactor,

                        ac.x / displayPreviewFactor,
                        ac.y / displayPreviewFactor,

                        ad.x / displayPreviewFactor,
                        ad.y / displayPreviewFactor,
                    ],
                };
            };
        }

        setTimeout(finishInit, 1);

        return result;
    },

    apply (params: IFilterApply<TFilterPerspectiveInput>): boolean {
        const context = params.context;
        const history = params.history;
        const before = params.input.before;
        const after = params.input.after;
        if (!context || !before || !after || !history) {
            return false;
        }
        history.pause(true);
        const fxCanvas = getSharedFx();
        if (!fxCanvas) {
            return false; // todo more specific error?
        }
        const texture = fxCanvas.texture(context.canvas);
        fxCanvas.draw(texture).multiplyAlpha().perspective(before, after).unmultiplyAlpha().update();
        context.clearRect(0, 0, context.canvas.width, context.canvas.height);
        context.drawImage(fxCanvas, 0, 0);
        texture.destroy();
        history.pause(false);
        history.push({
            tool: ['filter', 'perspective'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterPerspectiveHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {Checkbox} from '../ui/components/checkbox';
import {Select} from '../ui/components/select';
import constrainImg from '/src/app/img/ui/constrain.svg';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult} from '../kl-types';
import {LANG} from '../../language/language';
import {TFilterHistoryEntry} from './filters';
import {table} from '../ui/components/table';
import {theme} from '../../theme/theme';

export type TFilterResizeInput = {
    width: number;
    height: number;
    algorithm: 'smooth' | 'pixelated';
};

export type TFilterResizeHistoryEntry = TFilterHistoryEntry<
    'resize',
    TFilterResizeInput>;

export const filterResize = {

    getDialog (params: IFilterGetDialogParam) {
        //BB.centerWithin
        const klCanvas = params.klCanvas;
        if (!klCanvas) {
            return false;
        }

        const fit = BB.fitInto( klCanvas.getWidth(), klCanvas.getHeight(), 280, 200,1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);

        let previewFactor = w / klCanvas.getWidth();
        const tempCanvas = klCanvas.getCompleteCanvas(1);

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterResizeInput> = {
            element: div,
        };
        let newWidth = klCanvas.getWidth(), newHeight = klCanvas.getHeight();

        div.innerHTML = LANG('filter-resize-description') + '

';

        const maxWidth = params.maxWidth, maxHeight = params.maxHeight;

        const widthWrapper = BB.el({
            css: {
                width: '150px',
                height: '35px',
                lineHeight: '30px',
            },
        });
        const heightWrapper = BB.el({
            css: {
                width: '150px',
                height: '35px',
                lineHeight: '30px',
            },
        });
        const widthInput = BB.el({
            tagName: 'input',
            css: {
                cssFloat: 'right',
                width: '90px',
            },
            custom: {
                type: 'number',
                min: '1',
                max: '' + maxWidth,
                value: '' + klCanvas.getWidth(),
            },
        });
        const heightInput = BB.el({
            tagName: 'input',
            css: {
                cssFloat: 'right',
                width: '90px',
            },
            custom: {
                type: 'number',
                min: '1',
                max: '' + maxHeight,
                value: '' + klCanvas.getHeight(),
            },
        });
        widthInput.onclick = function () {
            (this as any).focus();
            widthChanged = true;
            update();
        };
        heightInput.onclick = function () {
            (this as any).focus();
            heightChanged = true;
            update();
        };
        widthInput.onchange = function () {
            widthChanged = true;
            update();
        };
        heightInput.onchange = function () {
            heightChanged = true;
            update();
        };
        widthWrapper.append(LANG('width') + ': ', widthInput);
        heightWrapper.append(LANG('height') + ': ', heightInput);
        const inputWrapper = BB.el({
            css: {
                background: 'url(' + constrainImg + ') no-repeat 140px 5px',
                backgroundSize: '50px 52px',
            },
        });
        inputWrapper.append(widthWrapper, heightWrapper);
        const constrainIm = new Image();
        constrainIm.src = constrainImg;
        constrainIm.height = 40;

        const sizeTable = table([
            [
                LANG('width') + ': ',
                widthInput,
                constrainIm,
            ],
            [
                BB.el({css: {height: '5px'}}),
                '',
                '',
            ],
            [
                LANG('height') + ': ',
                heightInput,
            ],
        ], {
            '0.2': {rowspan: 3},
        });
        BB.css(sizeTable, {
            marginBottom: '10px',
        });

        div.append(sizeTable);

        //contrain checkbox
        let heightChanged = false, widthChanged = false;
        const ratio = klCanvas.getWidth() / klCanvas.getHeight();

        function updateConstrain (): void {
            constrainIm.style.display = isConstrained ? '' : 'none';
            if (isConstrained) {
                widthInput.value = '' + klCanvas.getWidth();
                heightInput.value = '' + klCanvas.getHeight();
                update();
            }
        }

        let isConstrained = true;
        const constrainCheckbox = new Checkbox({
            init: true,
            label: LANG('constrain-proportions'),
            allowTab: true,
            callback: function (b) {
                isConstrained = b;
                updateConstrain();
            },
        });
        div.append(BB.el({
            css: {
                clear: 'both',
            },
        }));

        const algorithmSelect = new Select({
            isFocusable: true,
            optionArr: [
                ['smooth', LANG('algorithm-smooth')],
                ['pixelated', LANG('algorithm-pixelated')],
            ],
            title: LANG('scaling-algorithm'),
            initValue: 'smooth',
            onChange: (): void => {
                update();
            },
        });

        const secondRowElement = BB.el({
            parent: div,
            css: {
                display: 'flex',
                justifyContent: 'space-between',
                'alignItems': 'center',
            },
        });
        secondRowElement.append(constrainCheckbox.getElement(), algorithmSelect.getElement());

        const previewCanvas = BB.canvas(w, h);
        previewCanvas.style.imageRendering = 'pixelated';

        const previewCtx = BB.ctx(previewCanvas);

        function draw (): void {
            if (algorithmSelect.getValue() === 'smooth') {
                previewCanvas.style.imageRendering = previewFactor > 1 ? 'pixelated' : '';

                previewCanvas.width = klCanvas.getWidth();
                previewCanvas.height = klCanvas.getHeight();

                previewCtx.save();
                previewCtx.imageSmoothingQuality = 'high';
                previewCtx.drawImage(tempCanvas, 0, 0);
                BB.resizeCanvas(previewCanvas, newWidth, newHeight);
                previewCtx.restore();

            } else {
                previewCanvas.style.imageRendering = 'pixelated';

                previewCanvas.width = newWidth;
                previewCanvas.height = newHeight;
                previewCtx.save();
                previewCtx.imageSmoothingEnabled = false;
                previewCtx.drawImage(tempCanvas, 0, 0, previewCanvas.width, previewCanvas.height);
                previewCtx.restore();
            }

        }

        function update (): void {
            if ((widthInput.value.length === 0 && widthChanged) || (heightInput.value.length === 0 && heightChanged)) {
                heightChanged = false;
                widthChanged = false;
                return;
            }
            widthInput.value = '' + Math.max(1, parseInt(widthInput.value));
            heightInput.value = '' + Math.max(1, parseInt(heightInput.value));
            if (isConstrained) {
                if (heightChanged) {
                    widthInput.value = '' + parseInt('' + (parseInt(heightInput.value) * ratio));
                }
                if (widthChanged) {
                    heightInput.value = '' + parseInt('' + (parseInt(widthInput.value) / ratio));
                }

                if (parseInt(widthInput.value) > maxWidth || parseInt(heightInput.value) > maxHeight) {
                    const fit = BB.fitInto(parseInt(widthInput.value), parseInt(heightInput.value), maxWidth, maxHeight, 1);
                    widthInput.value = '' + parseInt('' + fit.width);
                    heightInput.value = '' + parseInt('' + fit.height);
                }
            }

            if (parseInt(widthInput.value) > maxWidth) {
                widthInput.value = '' + maxWidth;
            }
            if (parseInt(heightInput.value) > maxHeight) {
                heightInput.value = '' + maxHeight;
            }

            heightChanged = false;
            widthChanged = false;

            newWidth = parseInt(widthInput.value);
            newHeight = parseInt(heightInput.value);

            const preview = BB.fitInto(newWidth, newHeight, 280, 200, 1);
            const previewW = parseInt('' + preview.width), previewH = parseInt('' + preview.height);
            previewFactor = previewW / newWidth;

            const offset = BB.centerWithin(340, 220, previewW, previewH);

            draw();

            previewCanvas.style.width = Math.max(1, previewW) + 'px';
            previewCanvas.style.height = Math.max(1, previewH) + 'px';
            canvasWrapper.style.left = offset.x + 'px';
            canvasWrapper.style.top = offset.y + 'px';
            canvasWrapper.style.width = Math.max(1, previewW) + 'px';
            canvasWrapper.style.height = Math.max(1, previewH) + 'px';
        }

        const previewWrapper = BB.el({
            className: 'kl-transparent-preview',
            css: {
                width: '340px',
                marginLeft: '-20px',
                height: '220px',
                display: 'table',
                marginTop: '10px',
                position: 'relative',
                userSelect: 'none',
            },
        });

        const canvasWrapper = BB.el({
            parent: previewWrapper,
            content: previewCanvas,
            className: 'kl-transparent-preview__canvas',
            css: {
                width: w + 'px',
                height: h + 'px',
                position: 'absolute',
                overflow: 'hidden',
            },
        });

        function updateCheckerboard (): void {
            BB.createCheckerDataUrl(8, function (url) {
                previewWrapper.style.background = 'url(' + url + ')';
            }, theme.isDark());
        }
        theme.addIsDarkListener(updateCheckerboard);
        updateCheckerboard();

        div.append(previewWrapper);
        update();

        result.destroy = (): void => {
            constrainCheckbox.destroy();
            theme.removeIsDarkListener(updateCheckerboard);
        };
        result.getInput = function (): TFilterResizeInput {
            result.destroy!();
            return {
                width: newWidth,
                height: newHeight,
                algorithm: algorithmSelect.getValue(),
            };
        };
        return result;
    },

    apply (params: IFilterApply<TFilterResizeInput>): boolean {
        const klCanvas = params.klCanvas;
        const history = params.history;
        const width = params.input.width;
        const height = params.input.height;
        const algorithm = params.input.algorithm;
        if (!klCanvas || !history) {
            return false;
        }
        history.pause(true);
        klCanvas.resize(width, height, algorithm);
        history.pause(false);
        history.push({
            tool: ['filter', 'resize'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterResizeHistoryEntry);
        return true;
    },

};


code:
import {BB} from '../../bb/bb';
import {IFilterApply, IFilterGetDialogParam, IFilterGetDialogResult} from '../kl-types';
import {LANG} from '../../language/language';
import {TFilterHistoryEntry} from './filters';
import {theme} from '../../theme/theme';

export type TFilterRotateInput = {
    deg: number;
}

export type TFilterRotateHistoryEntry = TFilterHistoryEntry<
    'rotate',
    TFilterRotateInput>;

export const filterRotate = {

    getDialog (params: IFilterGetDialogParam) {
        const klCanvas = params.klCanvas;
        if (!klCanvas) {
            return false;
        }

        const fit = BB.fitInto(klCanvas.getWidth(), klCanvas.getHeight(), 280, 200, 1);
        const w = parseInt('' + fit.width), h = parseInt('' + fit.height);

        const previewFactor = w / klCanvas.getWidth();
        const tempCanvas = BB.canvas(w, h);
        tempCanvas.style.display = 'block';
        BB.ctx(tempCanvas).drawImage(klCanvas.getCompleteCanvas(previewFactor), 0, 0, w, h);

        const div = document.createElement('div');
        const result: IFilterGetDialogResult<TFilterRotateInput> = {
            element: div,
        };
        let deg = 0;
        div.innerHTML = LANG('filter-rotate-description') + '

';

        function update (): void {
            canvasWrapper.style.transform = 'rotate(' + deg + 'deg)';
            if (Math.abs(deg % 180) === 90) {
                //height has to fit width because of rotation
                const fit = BB.fitInto(h, w, 280, 200, 1);
                const scale = parseInt('' + fit.height) / w;
                canvasWrapper.style.transform = 'rotate(' + deg + 'deg) scale(' + scale + ')';
            }
        }

        const btnPlus = document.createElement('button');
        btnPlus.innerHTML = "⟳ 90°";
        const btnMinus = document.createElement('button');
        btnMinus.innerHTML = "⟲ 90°";
        btnMinus.style.marginRight = '5px';

        btnPlus.onclick = function () {
            deg += 90;
            update();
        };
        btnMinus.onclick = function () {
            deg -= 90;
            update();
        };

        div.append(btnMinus, btnPlus);

        const previewWrapper = BB.el({
            className: 'kl-preview-wrapper',
            css: {
                width: '340px',
                height: '220px',
                display: 'table',
            },
        });

        const previewcell = BB.el({
            parent: previewWrapper,
            css: {
                display: 'table-cell',
                verticalAlign: 'middle',
            },
        });
        const canvasWrapper = BB.el({
            parent: previewcell,
            content: tempCanvas,
            className: 'kl-preview-wrapper__canvas',
            css: {
                width: w + 'px',
                height: h + 'px',
                marginLeft: 'auto',
                marginRight: 'auto',
                overflow: 'hidden',
            },
        });

        function updateCheckerboard (): void {
            BB.createCheckerDataUrl(8, function (url) {
                canvasWrapper.style.background = 'url(' + url + ')';
            }, theme.isDark());
        }
        theme.addIsDarkListener(updateCheckerboard);
        updateCheckerboard();

        canvasWrapper.style.transition = 'all 0.2s ease-out';

        div.append(previewWrapper);
        update();

        result.destroy = (): void => {
            theme.removeIsDarkListener(updateCheckerboard);
        };
        result.getInput = function (): TFilterRotateInput {
            result.destroy!();
            return {
                deg: deg,
            };
        };
        return result;
    },

    apply (params: IFilterApply<TFilterRotateInput>): boolean {
        const klCanvas = params.klCanvas;
        const history = params.history;
        if (!klCanvas || !history) {
            return false;
        }
        history.pause(true);
        klCanvas.rotate(params.input.deg);
        history.pause(false);
        history.push({
            tool: ['filter', 'rotate'],
            action: 'apply',
            params: [{
                input: params.input,
            }],
        } as TFilterRotateHistoryEntry);
        return true;
    },

};