feat(ui): support grid size of 8 on canvas

- Support grid size of 8 on canvas
- Internal canvas math works on 8
- Update gridlines rendering to show 64 spaced lines and 32/16/8 when zoomed in
- Bbox manipulation defaults to grid of 64 - hold shift to get grid of 8

Besides being something we support internally, supporting 8 on canvas avoids a lot of hacky logic needed to work well with aspect ratios.
This commit is contained in:
psychedelicious 2024-01-02 13:27:19 +11:00 committed by Kent Keirsey
parent 4f43eda09b
commit cecee33bc0
9 changed files with 140 additions and 77 deletions

View File

@ -1,21 +1,36 @@
// Grid drawing adapted from https://longviewcoder.com/2021/12/08/konva-a-better-grid/
import { useToken } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks';
import { range } from 'lodash-es';
import type { ReactElement } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { Group, Line as KonvaLine } from 'react-konva';
import { getArbitraryBaseColor } from 'theme/colors';
const selector = createMemoizedSelector([stateSelector], ({ canvas }) => {
const { stageScale, stageCoordinates, stageDimensions } = canvas;
return { stageScale, stageCoordinates, stageDimensions };
});
const baseGridLineColor = getArbitraryBaseColor(27);
const fineGridLineColor = getArbitraryBaseColor(18);
const IAICanvasGrid = () => {
const { stageScale, stageCoordinates, stageDimensions } =
useAppSelector(selector);
const [gridLineColor] = useToken('colors', ['base.800']);
const gridSpacing = useMemo(() => {
if (stageScale >= 2) {
return 8;
}
if (stageScale >= 1 && stageScale < 2) {
return 16;
}
if (stageScale >= 0.5 && stageScale < 1) {
return 32;
}
return 64;
}, [stageScale]);
const unscale = useCallback(
(value: number) => {
@ -40,15 +55,15 @@ const IAICanvasGrid = () => {
};
const gridOffset = {
x: Math.ceil(unscale(x) / 64) * 64,
y: Math.ceil(unscale(y) / 64) * 64,
x: Math.ceil(unscale(x) / gridSpacing) * gridSpacing,
y: Math.ceil(unscale(y) / gridSpacing) * gridSpacing,
};
const gridRect = {
x1: -gridOffset.x,
y1: -gridOffset.y,
x2: unscale(width) - gridOffset.x + 64,
y2: unscale(height) - gridOffset.y + 64,
x2: unscale(width) - gridOffset.x + gridSpacing,
y2: unscale(height) - gridOffset.y + gridSpacing,
};
const gridFullRect = {
@ -58,40 +73,50 @@ const IAICanvasGrid = () => {
y2: Math.max(stageRect.y2, gridRect.y2),
};
const fullRect = gridFullRect;
const // find the x & y size of the grid
xSize = fullRect.x2 - fullRect.x1,
ySize = fullRect.y2 - fullRect.y1,
xSize = gridFullRect.x2 - gridFullRect.x1,
ySize = gridFullRect.y2 - gridFullRect.y1,
// compute the number of steps required on each axis.
xSteps = Math.round(xSize / 64) + 1,
ySteps = Math.round(ySize / 64) + 1;
xSteps = Math.round(xSize / gridSpacing) + 1,
ySteps = Math.round(ySize / gridSpacing) + 1;
const xLines = range(0, xSteps).map((i) => (
<KonvaLine
key={`x_${i}`}
x={fullRect.x1 + i * 64}
y={fullRect.y1}
points={[0, 0, 0, ySize]}
stroke={gridLineColor}
strokeWidth={1}
listening={false}
/>
));
const yLines = range(0, ySteps).map((i) => (
<KonvaLine
key={`y_${i}`}
x={fullRect.x1}
y={fullRect.y1 + i * 64}
points={[0, 0, xSize, 0]}
stroke={gridLineColor}
strokeWidth={1}
listening={false}
/>
));
const strokeWidth = unscale(1);
return xLines.concat(yLines);
}, [stageCoordinates, stageDimensions, unscale, gridLineColor]);
const gridLines: ReactElement[] = new Array(xSteps + ySteps);
let _x = 0;
let _y = 0;
for (let i = 0; i < xSteps; i++) {
_x = gridFullRect.x1 + i * gridSpacing;
gridLines.push(
<KonvaLine
key={`x_${i}`}
x={_x}
y={gridFullRect.y1}
points={[0, 0, 0, ySize]}
stroke={_x % 64 ? fineGridLineColor : baseGridLineColor}
strokeWidth={strokeWidth}
listening={false}
/>
);
}
for (let i = 0; i < ySteps; i++) {
_y = gridFullRect.y1 + i * gridSpacing;
gridLines.push(
<KonvaLine
key={`y_${i}`}
x={gridFullRect.x1}
y={_y}
points={[0, 0, xSize, 0]}
stroke={_y % 64 ? fineGridLineColor : baseGridLineColor}
strokeWidth={strokeWidth}
listening={false}
/>
);
}
return gridLines;
}, [stageDimensions, stageCoordinates, unscale, gridSpacing]);
return <Group listening={false}>{gridLines}</Group>;
};

View File

@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { $shift } from 'common/hooks/useGlobalModifiers';
import {
roundDownToMultiple,
roundToMultiple,
@ -15,6 +16,8 @@ import {
setIsTransformingBoundingBox,
} from 'features/canvas/store/canvasNanostore';
import {
CANVAS_GRID_SIZE_COARSE,
CANVAS_GRID_SIZE_FINE,
setBoundingBoxCoordinates,
setBoundingBoxDimensions,
setShouldSnapToGrid,
@ -23,7 +26,7 @@ import type Konva from 'konva';
import type { GroupConfig } from 'konva/lib/Group';
import type { KonvaEventObject } from 'konva/lib/Node';
import type { Vector2d } from 'konva/lib/types';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Group, Rect, Transformer } from 'react-konva';
@ -58,12 +61,10 @@ type IAICanvasBoundingBoxPreviewProps = GroupConfig;
const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const { ...rest } = props;
const dispatch = useAppDispatch();
const {
boundingBoxCoordinates,
boundingBoxDimensions,
stageScale,
shouldSnapToGrid,
tool,
@ -73,6 +74,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const transformerRef = useRef<Konva.Transformer>(null);
const shapeRef = useRef<Konva.Rect>(null);
const shift = useStore($shift);
const isDrawing = useStore($isDrawing);
const isMovingBoundingBox = useStore($isMovingBoundingBox);
const isTransformingBoundingBox = useStore($isTransformingBoundingBox);
@ -87,7 +89,14 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
transformerRef.current.getLayer()?.batchDraw();
}, []);
const scaledStep = 64 * stageScale;
const gridSize = useMemo(
() => (shift ? CANVAS_GRID_SIZE_FINE : CANVAS_GRID_SIZE_COARSE),
[shift]
);
const scaledStep = useMemo(
() => gridSize * stageScale,
[gridSize, stageScale]
);
useHotkeys('N', () => {
dispatch(setShouldSnapToGrid(!shouldSnapToGrid));
@ -108,8 +117,8 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const dragX = e.target.x();
const dragY = e.target.y();
const newX = roundToMultiple(dragX, 64);
const newY = roundToMultiple(dragY, 64);
const newX = roundToMultiple(dragX, gridSize);
const newY = roundToMultiple(dragY, gridSize);
e.target.x(newX);
e.target.y(newY);
@ -121,7 +130,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
})
);
},
[dispatch, shouldSnapToGrid]
[dispatch, gridSize, shouldSnapToGrid]
);
const handleOnTransform = useCallback(() => {
@ -147,7 +156,7 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
const y = Math.round(rect.y());
if (aspectRatio) {
const newHeight = roundToMultiple(width / aspectRatio.value, 64);
const newHeight = roundToMultiple(width / aspectRatio.value, gridSize);
dispatch(
setBoundingBoxDimensions({
width: width,
@ -165,15 +174,15 @@ const IAICanvasBoundingBox = (props: IAICanvasBoundingBoxPreviewProps) => {
dispatch(
setBoundingBoxCoordinates({
x: shouldSnapToGrid ? roundDownToMultiple(x, 64) : x,
y: shouldSnapToGrid ? roundDownToMultiple(y, 64) : y,
x: shouldSnapToGrid ? roundDownToMultiple(x, gridSize) : x,
y: shouldSnapToGrid ? roundDownToMultiple(y, gridSize) : y,
})
);
// Reset the scale now that the coords/dimensions have been un-scaled
rect.scaleX(1);
rect.scaleY(1);
}, [dispatch, shouldSnapToGrid, aspectRatio]);
}, [aspectRatio, dispatch, shouldSnapToGrid, gridSize]);
const anchorDragBoundFunc = useCallback(
(

View File

@ -9,7 +9,7 @@ import calculateScale from 'features/canvas/util/calculateScale';
import { STAGE_PADDING_PERCENTAGE } from 'features/canvas/util/constants';
import floorCoordinates from 'features/canvas/util/floorCoordinates';
import getScaledBoundingBoxDimensions from 'features/canvas/util/getScaledBoundingBoxDimensions';
import roundDimensionsTo64 from 'features/canvas/util/roundDimensionsTo64';
import roundDimensionsToMultiple from 'features/canvas/util/roundDimensionsToMultiple';
import type { AspectRatioState } from 'features/parameters/components/ImageSize/types';
import type { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
@ -40,6 +40,9 @@ import {
*/
const MAX_HISTORY = 128;
export const CANVAS_GRID_SIZE_FINE = 8;
export const CANVAS_GRID_SIZE_COARSE = 64;
export const initialLayerState: CanvasLayerState = {
objects: [],
stagingArea: {
@ -141,15 +144,24 @@ export const canvasSlice = createSlice({
const { stageDimensions } = state;
const newBoundingBoxDimensions = {
width: roundDownToMultiple(clamp(width, 64, 512), 64),
height: roundDownToMultiple(clamp(height, 64, 512), 64),
width: roundDownToMultiple(
clamp(width, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(height, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(width / 2 - newBoundingBoxDimensions.width / 2, 64),
x: roundToMultiple(
width / 2 - newBoundingBoxDimensions.width / 2,
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
height / 2 - newBoundingBoxDimensions.height / 2,
64
CANVAS_GRID_SIZE_FINE
),
};
@ -206,10 +218,13 @@ export const canvasSlice = createSlice({
state,
action: PayloadAction<Partial<Dimensions>>
) => {
const newDimensions = roundDimensionsTo64({
...state.boundingBoxDimensions,
...action.payload,
});
const newDimensions = roundDimensionsToMultiple(
{
...state.boundingBoxDimensions,
...action.payload,
},
CANVAS_GRID_SIZE_FINE
);
state.boundingBoxDimensions = newDimensions;
if (state.boundingBoxScaleMethod === 'auto') {
@ -649,18 +664,24 @@ export const canvasSlice = createSlice({
scaledStageHeight
) {
const newBoundingBoxDimensions = {
width: roundDownToMultiple(clamp(scaledStageWidth, 64, 512), 64),
height: roundDownToMultiple(clamp(scaledStageHeight, 64, 512), 64),
width: roundDownToMultiple(
clamp(scaledStageWidth, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
height: roundDownToMultiple(
clamp(scaledStageHeight, CANVAS_GRID_SIZE_FINE, 512),
CANVAS_GRID_SIZE_FINE
),
};
const newBoundingBoxCoordinates = {
x: roundToMultiple(
scaledStageWidth / 2 - newBoundingBoxDimensions.width / 2,
64
CANVAS_GRID_SIZE_FINE
),
y: roundToMultiple(
scaledStageHeight / 2 - newBoundingBoxDimensions.height / 2,
64
CANVAS_GRID_SIZE_FINE
),
};

View File

@ -1,4 +1,5 @@
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import { CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
@ -10,7 +11,7 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
let currentArea = width * height;
let maxDimension = 448;
while (currentArea < targetArea) {
maxDimension += 64;
maxDimension += CANVAS_GRID_SIZE_FINE;
if (width === height) {
scaledDimensions.width = 512;
scaledDimensions.height = 512;
@ -20,13 +21,13 @@ const getScaledBoundingBoxDimensions = (dimensions: Dimensions) => {
scaledDimensions.width = maxDimension;
scaledDimensions.height = roundToMultiple(
maxDimension / aspectRatio,
64
CANVAS_GRID_SIZE_FINE
);
} else if (aspectRatio < 1) {
scaledDimensions.height = maxDimension;
scaledDimensions.width = roundToMultiple(
maxDimension * aspectRatio,
64
CANVAS_GRID_SIZE_FINE
);
}
currentArea = scaledDimensions.width * scaledDimensions.height;

View File

@ -1,11 +0,0 @@
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
const roundDimensionsTo64 = (dimensions: Dimensions): Dimensions => {
return {
width: roundToMultiple(dimensions.width, 64),
height: roundToMultiple(dimensions.height, 64),
};
};
export default roundDimensionsTo64;

View File

@ -0,0 +1,11 @@
import { roundToMultiple } from 'common/util/roundDownToMultiple';
import type { Dimensions } from 'features/canvas/store/canvasTypes';
const roundDimensionsToMultiple = (dimensions: Dimensions, multiple: number): Dimensions => {
return {
width: roundToMultiple(dimensions.width, multiple),
height: roundToMultiple(dimensions.height, multiple),
};
};
export default roundDimensionsToMultiple;

View File

@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -46,7 +47,8 @@ const ParamBoundingBoxWidth = () => {
<InvSlider
min={64}
max={1536}
step={64}
step={CANVAS_GRID_SIZE_COARSE}
fineStep={CANVAS_GRID_SIZE_FINE}
value={ctx.height}
onChange={onChange}
onReset={onReset}

View File

@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { InvControl } from 'common/components/InvControl/InvControl';
import { InvSlider } from 'common/components/InvSlider/InvSlider';
import { isStagingSelector } from 'features/canvas/store/canvasSelectors';
import { CANVAS_GRID_SIZE_COARSE, CANVAS_GRID_SIZE_FINE } from 'features/canvas/store/canvasSlice';
import { useImageSizeContext } from 'features/parameters/components/ImageSize/ImageSizeContext';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@ -46,7 +47,8 @@ const ParamBoundingBoxWidth = () => {
<InvSlider
min={64}
max={1536}
step={64}
step={CANVAS_GRID_SIZE_COARSE}
fineStep={CANVAS_GRID_SIZE_FINE}
value={ctx.width}
onChange={onChange}
onReset={onReset}

View File

@ -13,6 +13,9 @@ const BLUE = { H: 200, S: 76 };
const GREEN = { H: 110, S: 69 };
const RED = { H: 16, S: 92 };
export const getArbitraryBaseColor = (lightness: number) =>
`hsl(${BASE.H} ${BASE.S}% ${lightness}%)`;
export const InvokeAIColors: InvokeAIThemeColors = {
base: generateColorPalette(BASE.H, BASE.S),
baseAlpha: generateColorPalette(BASE.H, BASE.S, true),