mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add canvas background style
This commit is contained in:
parent
cb293fd7ac
commit
2950775fa7
@ -1716,6 +1716,12 @@
|
|||||||
"view": "View",
|
"view": "View",
|
||||||
"transform": "Transform",
|
"transform": "Transform",
|
||||||
"eyeDropper": "Eye Dropper"
|
"eyeDropper": "Eye Dropper"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"backgroundStyle": "Background Style",
|
||||||
|
"solid": "Solid",
|
||||||
|
"checkerboard": "Checkerboard",
|
||||||
|
"dynamicGrid": "Dynamic Grid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"upscaling": {
|
"upscaling": {
|
||||||
|
@ -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';
|
@ -12,6 +12,7 @@ import {
|
|||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { CanvasSettingsBackgroundStyle } from 'features/controlLayers/components/CanvasSettingsBackgroundStyle';
|
||||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import {
|
import {
|
||||||
clipToBboxChanged,
|
clipToBboxChanged,
|
||||||
@ -71,6 +72,7 @@ const ControlLayersSettingsPopover = () => {
|
|||||||
<FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel>
|
<FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel>
|
||||||
<Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} />
|
<Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<CanvasSettingsBackgroundStyle />
|
||||||
<Button onClick={invalidateRasterizationCaches} size="sm">
|
<Button onClick={invalidateRasterizationCaches} size="sm">
|
||||||
Invalidate Rasterization Caches
|
Invalidate Rasterization Caches
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -3,8 +3,10 @@ import { useStore } from '@nanostores/react';
|
|||||||
import { $socket } from 'app/hooks/useSocketIO';
|
import { $socket } from 'app/hooks/useSocketIO';
|
||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
import { useAppStore } from 'app/store/nanostores/store';
|
import { useAppStore } from 'app/store/nanostores/store';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||||
import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { $canvasManager, CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
|
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||||
import Konva from 'konva';
|
import Konva from 'konva';
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||||
@ -52,6 +54,8 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||||
|
const canvasBackgroundStyle = useAppSelector((s) => s.canvasV2.settings.canvasBackgroundStyle);
|
||||||
|
|
||||||
const [stage] = useState(
|
const [stage] = useState(
|
||||||
() =>
|
() =>
|
||||||
new Konva.Stage({
|
new Konva.Stage({
|
||||||
@ -77,6 +81,17 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full">
|
<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
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
top={0}
|
top={0}
|
||||||
|
@ -34,6 +34,15 @@ export class CanvasBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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);
|
this.konva.layer.zIndex(0);
|
||||||
const scale = this.manager.stage.scaleX();
|
const scale = this.manager.stage.scaleX();
|
||||||
const gridSpacing = CanvasBackground.getGridSpacing(scale);
|
const gridSpacing = CanvasBackground.getGridSpacing(scale);
|
||||||
|
@ -270,6 +270,13 @@ export class CanvasManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._isFirstRender ||
|
||||||
|
state.settings.canvasBackgroundStyle !== this._prevState.settings.canvasBackgroundStyle
|
||||||
|
) {
|
||||||
|
this.background.render();
|
||||||
|
}
|
||||||
|
|
||||||
if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) {
|
if (this._isFirstRender || state.rasterLayers.entities !== this._prevState.rasterLayers.entities) {
|
||||||
this.log.debug('Rendering raster layers');
|
this.log.debug('Rendering raster layers');
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ const initialState: CanvasV2State = {
|
|||||||
showHUD: true,
|
showHUD: true,
|
||||||
clipToBbox: false,
|
clipToBbox: false,
|
||||||
cropToBboxOnSave: false,
|
cropToBboxOnSave: false,
|
||||||
|
canvasBackgroundStyle: 'dynamicGrid',
|
||||||
},
|
},
|
||||||
compositing: {
|
compositing: {
|
||||||
maskBlur: 16,
|
maskBlur: 16,
|
||||||
@ -474,6 +475,7 @@ export const {
|
|||||||
clipToBboxChanged,
|
clipToBboxChanged,
|
||||||
canvasReset,
|
canvasReset,
|
||||||
rasterizationCachesInvalidated,
|
rasterizationCachesInvalidated,
|
||||||
|
canvasBackgroundStyleChanged,
|
||||||
// All entities
|
// All entities
|
||||||
entitySelected,
|
entitySelected,
|
||||||
entityNameChanged,
|
entityNameChanged,
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import type { PayloadAction, SliceCaseReducers } from '@reduxjs/toolkit';
|
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 = {
|
export const settingsReducers = {
|
||||||
clipToBboxChanged: (state, action: PayloadAction<boolean>) => {
|
clipToBboxChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.settings.clipToBbox = action.payload;
|
state.settings.clipToBbox = action.payload;
|
||||||
},
|
},
|
||||||
|
canvasBackgroundStyleChanged: (state, action: PayloadAction<CanvasBackgroundStyle>) => {
|
||||||
|
state.settings.canvasBackgroundStyle = action.payload;
|
||||||
|
},
|
||||||
} satisfies SliceCaseReducers<CanvasV2State>;
|
} satisfies SliceCaseReducers<CanvasV2State>;
|
||||||
|
@ -839,6 +839,11 @@ export type StagingAreaImage = {
|
|||||||
offsetY: number;
|
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 = {
|
export type CanvasV2State = {
|
||||||
_version: 3;
|
_version: 3;
|
||||||
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
selectedEntityIdentifier: CanvasEntityIdentifier | null;
|
||||||
@ -863,6 +868,7 @@ export type CanvasV2State = {
|
|||||||
preserveMaskedArea: boolean;
|
preserveMaskedArea: boolean;
|
||||||
cropToBboxOnSave: boolean;
|
cropToBboxOnSave: boolean;
|
||||||
clipToBbox: boolean;
|
clipToBbox: boolean;
|
||||||
|
canvasBackgroundStyle: CanvasBackgroundStyle;
|
||||||
};
|
};
|
||||||
bbox: {
|
bbox: {
|
||||||
rect: {
|
rect: {
|
||||||
|
Loading…
Reference in New Issue
Block a user