From ab64078b7684fa815349c77c7e1c2f2241509ff6 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 16 Aug 2024 20:59:32 +1000 Subject: [PATCH] feat(ui): add canvas background style --- invokeai/frontend/web/public/locales/en.json | 6 +++ .../CanvasSettingsBackgroundStyle.tsx | 50 +++++++++++++++++++ .../ControlLayersSettingsPopover.tsx | 2 + .../components/StageComponent.tsx | 15 ++++++ .../controlLayers/konva/CanvasBackground.ts | 9 ++++ .../controlLayers/konva/CanvasManager.ts | 7 +++ .../controlLayers/store/canvasV2Slice.ts | 2 + .../controlLayers/store/settingsReducers.ts | 5 +- .../src/features/controlLayers/store/types.ts | 6 +++ 9 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 9b743b4bd9..7a8c3f2f31 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1717,6 +1717,12 @@ "view": "View", "transform": "Transform", "eyeDropper": "Eye Dropper" + }, + "background": { + "backgroundStyle": "Background Style", + "solid": "Solid", + "checkerboard": "Checkerboard", + "dynamicGrid": "Dynamic Grid" } }, "upscaling": { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx new file mode 100644 index 0000000000..20ea323680 --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSettingsBackgroundStyle.tsx @@ -0,0 +1,50 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { canvasBackgroundStyleChanged } from 'features/controlLayers/store/canvasV2Slice'; +import { isCanvasBackgroundStyle } from 'features/controlLayers/store/types'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const CanvasSettingsBackgroundStyle = memo(() => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const canvasBackgroundStyle = useAppSelector((s) => s.canvasV2.settings.canvasBackgroundStyle); + const onChange = useCallback<ComboboxOnChange>( + (v) => { + if (!isCanvasBackgroundStyle(v?.value)) { + return; + } + dispatch(canvasBackgroundStyleChanged(v.value)); + }, + [dispatch] + ); + + const options = useMemo<ComboboxOption[]>(() => { + return [ + { + value: 'solid', + label: t('controlLayers.background.solid'), + }, + { + value: 'checkerboard', + label: t('controlLayers.background.checkerboard'), + }, + { + value: 'dynamicGrid', + label: t('controlLayers.background.dynamicGrid'), + }, + ]; + }, [t]); + + const value = useMemo(() => options.find((o) => o.value === canvasBackgroundStyle), [options, canvasBackgroundStyle]); + + return ( + <FormControl orientation="vertical"> + <FormLabel m={0}>{t('controlLayers.background.backgroundStyle')}</FormLabel> + <Combobox value={value} options={options} onChange={onChange} isSearchable={false} /> + </FormControl> + ); +}); + +CanvasSettingsBackgroundStyle.displayName = 'CanvasSettingsBackgroundStyle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx index 8a7eb0b98e..a6dca5eb90 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayersSettingsPopover.tsx @@ -12,6 +12,7 @@ import { } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { CanvasSettingsBackgroundStyle } from 'features/controlLayers/components/CanvasSettingsBackgroundStyle'; import { $canvasManager } from 'features/controlLayers/konva/CanvasManager'; import { clipToBboxChanged, @@ -71,6 +72,7 @@ const ControlLayersSettingsPopover = () => { <FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel> <Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} /> </FormControl> + <CanvasSettingsBackgroundStyle /> <Button onClick={invalidateRasterizationCaches} size="sm"> Invalidate Rasterization Caches </Button> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index ae8e217df8..9870e3c72d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -3,8 +3,10 @@ import { useStore } from '@nanostores/react'; import { $socket } from 'app/hooks/useSocketIO'; import { logger } from 'app/logging/logger'; import { useAppStore } from 'app/store/nanostores/store'; +import { useAppSelector } from 'app/store/storeHooks'; import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay'; import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager'; +import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants'; import Konva from 'konva'; import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useDevicePixelRatio } from 'use-device-pixel-ratio'; @@ -52,6 +54,8 @@ type Props = { }; export const StageComponent = memo(({ asPreview = false }: Props) => { + const canvasBackgroundStyle = useAppSelector((s) => s.canvasV2.settings.canvasBackgroundStyle); + const [stage] = useState( () => new Konva.Stage({ @@ -77,6 +81,17 @@ export const StageComponent = memo(({ asPreview = false }: Props) => { return ( <Flex position="relative" w="full" h="full"> + {canvasBackgroundStyle === 'checkerboard' && ( + <Flex + position="absolute" + bgImage={TRANSPARENCY_CHECKER_PATTERN} + top={0} + right={0} + bottom={0} + left={0} + opacity={0.1} + /> + )} <Flex position="absolute" top={0} diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts index 6dc5462ff9..7e268a64f8 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasBackground.ts @@ -34,6 +34,15 @@ export class CanvasBackground { } render() { + const settings = this.manager.stateApi.getSettings(); + + if (settings.canvasBackgroundStyle !== 'dynamicGrid') { + this.konva.layer.visible(false); + return; + } + + this.konva.layer.visible(true); + this.konva.layer.zIndex(0); const scale = this.manager.stage.scaleX(); const gridSpacing = CanvasBackground.getGridSpacing(scale); diff --git a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts index ef0e8fe781..4adc5c80e3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts +++ b/invokeai/frontend/web/src/features/controlLayers/konva/CanvasManager.ts @@ -270,6 +270,13 @@ export class CanvasManager { return; } + if ( + this._isFirstRender || + state.settings.canvasBackgroundStyle !== this._prevState.settings.canvasBackgroundStyle + ) { + this.background.render(); + } + if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) { this.log.debug('Rendering raster layers'); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts index 55a6fdb2ca..bc939f0894 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/canvasV2Slice.ts @@ -94,6 +94,7 @@ const initialState: CanvasV2State = { showHUD: true, clipToBbox: false, cropToBboxOnSave: false, + canvasBackgroundStyle: 'dynamicGrid', }, compositing: { maskBlur: 16, @@ -474,6 +475,7 @@ export const { clipToBboxChanged, canvasReset, rasterizationCachesInvalidated, + canvasBackgroundStyleChanged, // All entities entitySelected, entityNameChanged, diff --git a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts index cecdd38135..50870bfa4f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/settingsReducers.ts @@ -1,8 +1,11 @@ import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit'; -import type { CanvasV2State } from 'features/controlLayers/store/types'; +import type { CanvasBackgroundStyle, CanvasV2State } from 'features/controlLayers/store/types'; export const settingsReducers = { clipToBboxChanged: (state, action: PayloadAction<boolean>) => { state.settings.clipToBbox = action.payload; }, + canvasBackgroundStyleChanged: (state, action: PayloadAction<CanvasBackgroundStyle>) => { + state.settings.canvasBackgroundStyle = action.payload; + }, } satisfies SliceCaseReducers<CanvasV2State>; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/types.ts b/invokeai/frontend/web/src/features/controlLayers/store/types.ts index 3d37342a07..ea010ff06b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/types.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/types.ts @@ -839,6 +839,11 @@ export type StagingAreaImage = { offsetY: number; }; +const zCanvasBackgroundStyle = z.enum(['checkerboard', 'dynamicGrid', 'solid']); +export type CanvasBackgroundStyle = z.infer<typeof zCanvasBackgroundStyle>; +export const isCanvasBackgroundStyle = (v: unknown): v is CanvasBackgroundStyle => + zCanvasBackgroundStyle.safeParse(v).success; + export type CanvasV2State = { _version: 3; selectedEntityIdentifier: CanvasEntityIdentifier | null; @@ -863,6 +868,7 @@ export type CanvasV2State = { preserveMaskedArea: boolean; cropToBboxOnSave: boolean; clipToBbox: boolean; + canvasBackgroundStyle: CanvasBackgroundStyle; }; bbox: { rect: {