feat(ui): aspect ratio preview is regional prompts canvas

This commit is contained in:
psychedelicious 2024-04-23 10:37:28 +10:00
parent bb37e25ed0
commit c915220965
4 changed files with 114 additions and 149 deletions

View File

@ -1,73 +1,10 @@
import { useSize } from '@chakra-ui/react-use-size';
import { Flex, Icon } from '@invoke-ai/ui-library';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { AnimatePresence, motion } from 'framer-motion';
import { useMemo, useRef } from 'react';
import { PiFrameCorners } from 'react-icons/pi';
import {
BOX_SIZE_CSS_CALC,
ICON_CONTAINER_STYLES,
ICON_HIGH_CUTOFF,
ICON_LOW_CUTOFF,
MOTION_ICON_ANIMATE,
MOTION_ICON_EXIT,
MOTION_ICON_INITIAL,
} from './constants';
import { Flex } from '@invoke-ai/ui-library';
import { StageComponent } from 'features/regionalPrompts/components/StageComponent';
export const AspectRatioPreview = () => {
const ctx = useImageSizeContext();
const containerRef = useRef<HTMLDivElement>(null);
const containerSize = useSize(containerRef);
const shouldShowIcon = useMemo(
() => ctx.aspectRatioState.value < ICON_HIGH_CUTOFF && ctx.aspectRatioState.value > ICON_LOW_CUTOFF,
[ctx.aspectRatioState.value]
);
const { width, height } = useMemo(() => {
if (!containerSize) {
return { width: 0, height: 0 };
}
let width = ctx.width;
let height = ctx.height;
if (ctx.width > ctx.height) {
width = containerSize.width;
height = width / ctx.aspectRatioState.value;
} else {
height = containerSize.height;
width = height * ctx.aspectRatioState.value;
}
return { width, height };
}, [containerSize, ctx.width, ctx.height, ctx.aspectRatioState.value]);
return (
<Flex w="full" h="full" alignItems="center" justifyContent="center" ref={containerRef}>
<Flex
bg="blackAlpha.400"
borderRadius="base"
width={`${width}px`}
height={`${height}px`}
alignItems="center"
justifyContent="center"
>
<AnimatePresence>
{shouldShowIcon && (
<Flex
as={motion.div}
initial={MOTION_ICON_INITIAL}
animate={MOTION_ICON_ANIMATE}
exit={MOTION_ICON_EXIT}
style={ICON_CONTAINER_STYLES}
>
<Icon as={PiFrameCorners} color="base.700" boxSize={BOX_SIZE_CSS_CALC} />
</Flex>
)}
</AnimatePresence>
</Flex>
<Flex w="full" h="full" alignItems="center" justifyContent="center">
<StageComponent asPreview />
</Flex>
);
};

View File

@ -1,3 +1,4 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { logger } from 'app/logging/logger';
@ -14,18 +15,18 @@ import {
layerTranslated,
selectRegionalPromptsSlice,
} from 'features/regionalPrompts/store/regionalPromptsSlice';
import { renderBackground, renderBbox, renderLayers, renderToolPreview } from 'features/regionalPrompts/util/renderers';
import { renderers } from 'features/regionalPrompts/util/renderers';
import Konva from 'konva';
import type { IRect } from 'konva/lib/types';
import { atom } from 'nanostores';
import { memo, useCallback, useLayoutEffect } from 'react';
import type { MutableRefObject } from 'react';
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { assert } from 'tsafe';
// This will log warnings when layers > 5 - maybe use `import.meta.env.MODE === 'development'` instead?
Konva.showWarnings = false;
const log = logger('regionalPrompts');
const $stage = atom<Konva.Stage | null>(null);
const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSlice, (regionalPrompts) => {
const layer = regionalPrompts.present.layers.find((l) => l.id === regionalPrompts.present.selectedLayerId);
if (!layer) {
@ -35,43 +36,52 @@ const selectSelectedLayerColor = createMemoizedSelector(selectRegionalPromptsSli
return layer.previewColor;
});
const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElement | null, asPreview: boolean) => {
const useStageRenderer = (
stageRef: MutableRefObject<Konva.Stage>,
container: HTMLDivElement | null,
wrapper: HTMLDivElement | null,
asPreview: boolean
) => {
const dispatch = useAppDispatch();
const width = useAppSelector((s) => s.generation.width);
const height = useAppSelector((s) => s.generation.height);
const state = useAppSelector((s) => s.regionalPrompts.present);
const stage = useStore($stage);
const tool = useStore($tool);
const { onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave } = useMouseEvents();
const cursorPosition = useStore($cursorPosition);
const lastMouseDownPos = useStore($lastMouseDownPos);
const selectedLayerIdColor = useAppSelector(selectSelectedLayerColor);
const renderLayers = useMemo(() => (asPreview ? renderers.layersDebounced : renderers.layers), [asPreview]);
const renderToolPreview = useMemo(
() => (asPreview ? renderers.toolPreviewDebounced : renderers.toolPreview),
[asPreview]
);
const renderBbox = useMemo(() => (asPreview ? renderers.bboxDebounced : renderers.bbox), [asPreview]);
const renderBackground = useMemo(
() => (asPreview ? renderers.backgroundDebounced : renderers.background),
[asPreview]
);
const onLayerPosChanged = useCallback(
(layerId: string, x: number, y: number) => {
if (asPreview) {
dispatch(layerTranslated({ layerId, x, y }));
}
},
[dispatch, asPreview]
[dispatch]
);
const onBboxChanged = useCallback(
(layerId: string, bbox: IRect | null) => {
if (asPreview) {
dispatch(layerBboxChanged({ layerId, bbox }));
}
},
[dispatch, asPreview]
[dispatch]
);
const onBboxMouseDown = useCallback(
(layerId: string) => {
if (asPreview) {
dispatch(layerSelected(layerId));
}
},
[dispatch, asPreview]
[dispatch]
);
useLayoutEffect(() => {
@ -79,27 +89,24 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
if (!container) {
return;
}
$stage.set(
new Konva.Stage({
container,
})
);
const stage = stageRef.current.container(container);
return () => {
log.trace('Cleaning up stage');
$stage.get()?.destroy();
stage.destroy();
};
}, [container]);
}, [container, stageRef]);
useLayoutEffect(() => {
log.trace('Adding stage listeners');
if (!stage || asPreview) {
if (asPreview) {
return;
}
stage.on('mousedown', onMouseDown);
stage.on('mouseup', onMouseUp);
stage.on('mousemove', onMouseMove);
stage.on('mouseenter', onMouseEnter);
stage.on('mouseleave', onMouseLeave);
stageRef.current.on('mousedown', onMouseDown);
stageRef.current.on('mouseup', onMouseUp);
stageRef.current.on('mousemove', onMouseMove);
stageRef.current.on('mouseenter', onMouseEnter);
stageRef.current.on('mouseleave', onMouseLeave);
const stage = stageRef.current;
return () => {
log.trace('Cleaning up stage listeners');
@ -109,14 +116,16 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
stage.off('mouseenter', onMouseEnter);
stage.off('mouseleave', onMouseLeave);
};
}, [stage, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
}, [stageRef, asPreview, onMouseDown, onMouseUp, onMouseMove, onMouseEnter, onMouseLeave]);
useLayoutEffect(() => {
log.trace('Updating stage dimensions');
if (!stage || !wrapper) {
if (!wrapper) {
return;
}
const stage = stageRef.current;
const fitStageToContainer = () => {
const newXScale = wrapper.offsetWidth / width;
const newYScale = wrapper.offsetHeight / height;
@ -134,15 +143,15 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
return () => {
resizeObserver.disconnect();
};
}, [stage, width, height, wrapper]);
}, [stageRef, width, height, wrapper]);
useLayoutEffect(() => {
log.trace('Rendering brush preview');
if (!stage || asPreview) {
if (asPreview) {
return;
}
renderToolPreview(
stage,
stageRef.current,
tool,
selectedLayerIdColor,
state.globalMaskLayerOpacity,
@ -152,47 +161,36 @@ const useStageRenderer = (container: HTMLDivElement | null, wrapper: HTMLDivElem
);
}, [
asPreview,
stage,
stageRef,
tool,
selectedLayerIdColor,
state.globalMaskLayerOpacity,
cursorPosition,
lastMouseDownPos,
state.brushSize,
renderToolPreview,
]);
useLayoutEffect(() => {
log.trace('Rendering layers');
if (!stage) {
return;
}
renderLayers(stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged);
}, [stage, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged]);
renderLayers(stageRef.current, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged);
}, [stageRef, state.layers, state.globalMaskLayerOpacity, tool, onLayerPosChanged, renderLayers]);
useLayoutEffect(() => {
log.trace('Rendering bbox');
if (!stage || asPreview) {
if (asPreview) {
return;
}
renderBbox(stage, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown);
}, [stage, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown]);
renderBbox(stageRef.current, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown);
}, [stageRef, asPreview, state.layers, state.selectedLayerId, tool, onBboxChanged, onBboxMouseDown, renderBbox]);
useLayoutEffect(() => {
log.trace('Rendering background');
if (!stage || asPreview) {
if (asPreview) {
return;
}
renderBackground(stage, width, height);
}, [stage, asPreview, width, height]);
};
const $container = atom<HTMLDivElement | null>(null);
const containerRef = (el: HTMLDivElement | null) => {
$container.set(el);
};
const $wrapper = atom<HTMLDivElement | null>(null);
const wrapperRef = (el: HTMLDivElement | null) => {
$wrapper.set(el);
renderBackground(stageRef.current, width, height);
}, [stageRef, asPreview, width, height, renderBackground]);
};
type Props = {
@ -200,24 +198,39 @@ type Props = {
};
export const StageComponent = memo(({ asPreview = false }: Props) => {
const container = useStore($container);
const wrapper = useStore($wrapper);
useStageRenderer(container, wrapper, asPreview);
const stageRef = useRef<Konva.Stage>(
new Konva.Stage({
container: document.createElement('div'), // We will overwrite this shortly...
})
);
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null);
const containerRef = useCallback((el: HTMLDivElement | null) => {
setContainer(el);
}, []);
const wrapperRef = useCallback((el: HTMLDivElement | null) => {
setWrapper(el);
}, []);
useStageRenderer(stageRef, container, wrapper, asPreview);
const sx = useMemo<SystemStyleObject>(
() => ({
bg: 'base.850',
p: asPreview ? 0 : 2,
borderRadius: asPreview ? 0 : 'base',
borderWidth: asPreview ? 0 : 1,
w: 'min-content',
h: 'min-content',
}),
[asPreview]
);
return (
<Flex overflow="hidden" w="full" h="full">
<Flex ref={wrapperRef} w="full" h="full" alignItems="center" justifyContent="center">
<Flex
ref={containerRef}
tabIndex={-1}
bg="base.850"
p={2}
borderRadius="base"
borderWidth={1}
w="min-content"
h="min-content"
minW={64}
minH={64}
/>
<Flex ref={containerRef} tabIndex={-1} sx={sx} />
</Flex>
</Flex>
);

View File

@ -2,7 +2,7 @@ import { getStore } from 'app/store/nanostores/store';
import openBase64ImageInTab from 'common/util/openBase64ImageInTab';
import { blobToDataURL } from 'features/canvas/util/blobToDataURL';
import { VECTOR_MASK_LAYER_NAME } from 'features/regionalPrompts/store/regionalPromptsSlice';
import { renderLayers } from 'features/regionalPrompts/util/renderers';
import { renderers } from 'features/regionalPrompts/util/renderers';
import Konva from 'konva';
import { assert } from 'tsafe';
@ -20,7 +20,7 @@ export const getRegionalPromptLayerBlobs = async (
const reduxLayers = state.regionalPrompts.present.layers;
const container = document.createElement('div');
const stage = new Konva.Stage({ container, width: state.generation.width, height: state.generation.height });
renderLayers(stage, reduxLayers, 1, 'brush');
renderers.layers(stage, reduxLayers, 1, 'brush');
const konvaLayers = stage.find<Konva.Layer>(`.${VECTOR_MASK_LAYER_NAME}`);
const blobs: Record<string, Blob> = {};

View File

@ -25,6 +25,7 @@ import {
import { getLayerBboxFast, getLayerBboxPixels } from 'features/regionalPrompts/util/bbox';
import Konva from 'konva';
import type { IRect, Vector2d } from 'konva/lib/types';
import { debounce } from 'lodash-es';
import type { RgbColor } from 'react-colorful';
import { assert } from 'tsafe';
import { v4 as uuidv4 } from 'uuid';
@ -34,6 +35,8 @@ const BBOX_NOT_SELECTED_STROKE = 'rgba(255, 255, 255, 0.353)';
const BBOX_NOT_SELECTED_MOUSEOVER_STROKE = 'rgba(255, 255, 255, 0.661)';
const BRUSH_BORDER_INNER_COLOR = 'rgba(0,0,0,1)';
const BRUSH_BORDER_OUTER_COLOR = 'rgba(255,255,255,0.8)';
const STAGE_BG_DATAURL =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII=';
const mapId = (object: { id: string }) => object.id;
@ -57,7 +60,7 @@ const selectVectorMaskObjects = (node: Konva.Node) => {
* @param lastMouseDownPos The position of the last mouse down event - used for the rect tool.
* @param brushSize The brush size.
*/
export const renderToolPreview = (
const toolPreview = (
stage: Konva.Stage,
tool: Tool,
color: RgbColor | null,
@ -197,7 +200,7 @@ export const renderToolPreview = (
}
};
const renderVectorMaskLayer = (
const vectorMaskLayer = (
stage: Konva.Stage,
vmLayer: VectorMaskLayer,
vmLayerIndex: number,
@ -369,7 +372,7 @@ const renderVectorMaskLayer = (
* @param onLayerPosChanged Callback for when the layer's position changes. This is optional to allow for offscreen rendering.
* @returns
*/
export const renderLayers = (
const layers = (
stage: Konva.Stage,
reduxLayers: Layer[],
globalMaskLayerOpacity: number,
@ -389,7 +392,7 @@ export const renderLayers = (
const reduxLayer = reduxLayers[layerIndex];
assert(reduxLayer, `Layer at index ${layerIndex} is undefined`);
if (isVectorMaskLayer(reduxLayer)) {
renderVectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged);
vectorMaskLayer(stage, reduxLayer, layerIndex, globalMaskLayerOpacity, tool, onLayerPosChanged);
}
}
};
@ -402,7 +405,7 @@ export const renderLayers = (
* @param onBboxChanged A callback to be called when the bounding box changes.
* @returns
*/
export const renderBbox = (
const bbox = (
stage: Konva.Stage,
reduxLayers: Layer[],
selectedLayerId: string | null,
@ -478,7 +481,7 @@ export const renderBbox = (
}
};
export const renderBackground = (stage: Konva.Stage, width: number, height: number) => {
const background = (stage: Konva.Stage, width: number, height: number) => {
let layer = stage.findOne<Konva.Layer>(`#${BACKGROUND_LAYER_ID}`);
if (!layer) {
@ -501,8 +504,7 @@ export const renderBackground = (stage: Konva.Stage, width: number, height: numb
background.fillPatternImage(image);
};
// This is invokeai/frontend/web/public/assets/images/transparent_bg.png as a dataURL
image.src =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAEsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIKICAgIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgZXhpZjpQaXhlbFhEaW1lbnNpb249IjIwIgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iMjAiCiAgIGV4aWY6Q29sb3JTcGFjZT0iMSIKICAgdGlmZjpJbWFnZVdpZHRoPSIyMCIKICAgdGlmZjpJbWFnZUxlbmd0aD0iMjAiCiAgIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiCiAgIHRpZmY6WFJlc29sdXRpb249IjMwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIzMDAvMSIKICAgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIKICAgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNC0wNC0yM1QwODoyMDo0NysxMDowMCI+CiAgIDx4bXBNTTpIaXN0b3J5PgogICAgPHJkZjpTZXE+CiAgICAgPHJkZjpsaQogICAgICBzdEV2dDphY3Rpb249InByb2R1Y2VkIgogICAgICBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZmZpbml0eSBQaG90byAxLjEwLjgiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDQtMjNUMDg6MjA6NDcrMTA6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/Pn9pdVgAAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWR3yuDURjHP5uJmKghFy6WxpVpqMWNMgm1tGbKr5vt3S+1d3t73y3JrXKrKHHj1wV/AbfKtVJESq53TdywXs9rakv2nJ7zfM73nOfpnOeAPZJRVMPhAzWb18NTAffC4pK7oYiDTjpw4YgqhjYeCgWpaR8P2Kx457Vq1T73rzXHE4YCtkbhMUXT88LTwsG1vGbxrnC7ko7Ghc+F+3W5oPC9pcfKXLQ4VeYvi/VIeALsbcLuVBXHqlhJ66qwvByPmikov/exXuJMZOfnJPaId2MQZooAbmaYZAI/g4zK7MfLEAOyoka+7yd/lpzkKjJrrKOzSoo0efpFLUj1hMSk6AkZGdat/v/tq5EcHipXdwag/sU033qhYQdK26b5eWyapROoe4arbCU/dwQj76JvVzTPIbRuwsV1RYvtweUWdD1pUT36I9WJ25NJeD2DlkVw3ULTcrlnv/ucPkJkQ77qBvYPoE/Ot658AxagZ8FoS/a7AAAACXBIWXMAAC4jAAAuIwF4pT92AAAAL0lEQVQ4jWM8ffo0A25gYmKCR5YJjxxBMKp5ZGhm/P//Px7pM2fO0MrmUc0jQzMAB2EIhZC3pUYAAAAASUVORK5CYII=';
image.src = STAGE_BG_DATAURL;
}
const background = layer.findOne<Konva.Rect>(`#${BACKGROUND_RECT_ID}`);
@ -525,3 +527,16 @@ export const renderBackground = (stage: Konva.Stage, width: number, height: numb
// Apply that movement to the fill pattern
background.fillPatternOffset(stagePos);
};
const DEBOUNCE_MS = 100;
export const renderers = {
toolPreview,
toolPreviewDebounced: debounce(toolPreview, DEBOUNCE_MS),
layers,
layersDebounced: debounce(layers, DEBOUNCE_MS),
bbox,
bboxDebounced: debounce(bbox, DEBOUNCE_MS),
background,
backgroundDebounced: debounce(background, DEBOUNCE_MS),
};