From c915220965e71bd47c4916fd9827f2e0be5123c2 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:37:28 +1000 Subject: [PATCH] feat(ui): aspect ratio preview is regional prompts canvas --- .../ImageSize/AspectRatioPreview.tsx | 71 +------- .../components/StageComponent.tsx | 157 ++++++++++-------- .../regionalPrompts/util/getLayerBlobs.ts | 4 +- .../regionalPrompts/util/renderers.ts | 31 +++- 4 files changed, 114 insertions(+), 149 deletions(-) diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx index e662acae7d..dcef435c39 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageSize/AspectRatioPreview.tsx @@ -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(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 ( - - - - {shouldShowIcon && ( - - - - )} - - + + ); }; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx index fe53951cde..1f69b37bbe 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/regionalPrompts/components/StageComponent.tsx @@ -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(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, + 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(layerTranslated({ layerId, x, y })); }, - [dispatch, asPreview] + [dispatch] ); const onBboxChanged = useCallback( (layerId: string, bbox: IRect | null) => { - if (asPreview) { - dispatch(layerBboxChanged({ layerId, bbox })); - } + dispatch(layerBboxChanged({ layerId, bbox })); }, - [dispatch, asPreview] + [dispatch] ); const onBboxMouseDown = useCallback( (layerId: string) => { - if (asPreview) { - dispatch(layerSelected(layerId)); - } + 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(null); -const containerRef = (el: HTMLDivElement | null) => { - $container.set(el); -}; -const $wrapper = atom(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( + new Konva.Stage({ + container: document.createElement('div'), // We will overwrite this shortly... + }) + ); + const [container, setContainer] = useState(null); + const [wrapper, setWrapper] = useState(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( + () => ({ + bg: 'base.850', + p: asPreview ? 0 : 2, + borderRadius: asPreview ? 0 : 'base', + borderWidth: asPreview ? 0 : 1, + w: 'min-content', + h: 'min-content', + }), + [asPreview] + ); return ( - + ); diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts index aa61a9a406..183042bb43 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/getLayerBlobs.ts @@ -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(`.${VECTOR_MASK_LAYER_NAME}`); const blobs: Record = {}; diff --git a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts index 4e4b275073..ae1c2a9d38 100644 --- a/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts +++ b/invokeai/frontend/web/src/features/regionalPrompts/util/renderers.ts @@ -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(`#${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(`#${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), +};