feat(ui): decouple konva logic from nanostores

This commit is contained in:
psychedelicious 2024-06-12 16:08:03 +10:00
parent 302efcf6e8
commit 22925f92bd
3 changed files with 146 additions and 111 deletions

View File

@ -20,12 +20,12 @@ import {
$brushSpacingPx, $brushSpacingPx,
$isDrawing, $isDrawing,
$isMouseDown, $isMouseDown,
$isSpaceDown,
$lastAddedPoint, $lastAddedPoint,
$lastCursorPos, $lastCursorPos,
$lastMouseDownPos, $lastMouseDownPos,
$selectedLayer, $selectedLayer,
$shouldInvertBrushSizeScrollDirection, $shouldInvertBrushSizeScrollDirection,
$spaceKey,
$stageAttrs, $stageAttrs,
$tool, $tool,
$toolBuffer, $toolBuffer,
@ -188,20 +188,27 @@ const useStageRenderer = (stage: Konva.Stage, container: HTMLDivElement | null,
const cleanup = setStageEventHandlers({ const cleanup = setStageEventHandlers({
stage, stage,
$tool, getTool: $tool.get,
$toolBuffer, setTool: $tool.set,
$isDrawing, getToolBuffer: $toolBuffer.get,
$isMouseDown, setToolBuffer: $toolBuffer.set,
$lastMouseDownPos, getIsDrawing: $isDrawing.get,
$lastCursorPos, setIsDrawing: $isDrawing.set,
$lastAddedPoint, getIsMouseDown: $isMouseDown.get,
$stageAttrs, setIsMouseDown: $isMouseDown.set,
$brushSize, getBrushColor: $brushColor.get,
$brushColor, getBrushSize: $brushSize.get,
$brushSpacingPx, getBrushSpacingPx: $brushSpacingPx.get,
$selectedLayer, getSelectedLayer: $selectedLayer.get,
$shouldInvertBrushSizeScrollDirection, getLastAddedPoint: $lastAddedPoint.get,
$isSpaceDown, setLastAddedPoint: $lastAddedPoint.set,
getLastCursorPos: $lastCursorPos.get,
setLastCursorPos: $lastCursorPos.set,
getLastMouseDownPos: $lastMouseDownPos.get,
setLastMouseDownPos: $lastMouseDownPos.set,
getShouldInvert: $shouldInvertBrushSizeScrollDirection.get,
getSpaceKey: $spaceKey.get,
setStageAttrs: $stageAttrs.set,
onBrushSizeChanged, onBrushSizeChanged,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,

View File

@ -1,6 +1,6 @@
import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom'; import { calculateNewBrushSize } from 'features/canvas/hooks/useCanvasZoom';
import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants';
import { getIsMouseDown, getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util'; import { getScaledFlooredCursorPosition, snapPosToStage } from 'features/controlLayers/konva/util';
import type { import type {
AddBrushLineArg, AddBrushLineArg,
AddEraserLineArg, AddEraserLineArg,
@ -14,27 +14,33 @@ import { DEFAULT_RGBA_COLOR } from 'features/controlLayers/store/types';
import type Konva from 'konva'; import type Konva from 'konva';
import type { Vector2d } from 'konva/lib/types'; import type { Vector2d } from 'konva/lib/types';
import { clamp } from 'lodash-es'; import { clamp } from 'lodash-es';
import type { WritableAtom } from 'nanostores';
import type { RgbaColor } from 'react-colorful'; import type { RgbaColor } from 'react-colorful';
import { PREVIEW_TOOL_GROUP_ID } from './naming'; import { PREVIEW_TOOL_GROUP_ID } from './naming';
type SetStageEventHandlersArg = { type Arg = {
stage: Konva.Stage; stage: Konva.Stage;
$tool: WritableAtom<Tool>; getTool: () => Tool;
$toolBuffer: WritableAtom<Tool | null>; setTool: (tool: Tool) => void;
$isDrawing: WritableAtom<boolean>; getToolBuffer: () => Tool | null;
$isMouseDown: WritableAtom<boolean>; setToolBuffer: (tool: Tool | null) => void;
$lastMouseDownPos: WritableAtom<Vector2d | null>; getIsDrawing: () => boolean;
$lastCursorPos: WritableAtom<Vector2d | null>; setIsDrawing: (isDrawing: boolean) => void;
$lastAddedPoint: WritableAtom<Vector2d | null>; getIsMouseDown: () => boolean;
$stageAttrs: WritableAtom<StageAttrs>; setIsMouseDown: (isMouseDown: boolean) => void;
$brushColor: WritableAtom<RgbaColor>; getLastMouseDownPos: () => Vector2d | null;
$brushSize: WritableAtom<number>; setLastMouseDownPos: (pos: Vector2d | null) => void;
$brushSpacingPx: WritableAtom<number>; getLastCursorPos: () => Vector2d | null;
$selectedLayer: WritableAtom<Layer | null>; setLastCursorPos: (pos: Vector2d | null) => void;
$shouldInvertBrushSizeScrollDirection: WritableAtom<boolean>; getLastAddedPoint: () => Vector2d | null;
$isSpaceDown: WritableAtom<boolean>; setLastAddedPoint: (pos: Vector2d | null) => void;
setStageAttrs: (attrs: StageAttrs) => void;
getBrushColor: () => RgbaColor;
getBrushSize: () => number;
getBrushSpacingPx: () => number;
getSelectedLayer: () => Layer | null;
getShouldInvert: () => boolean;
getSpaceKey: () => boolean;
onBrushLineAdded: (arg: AddBrushLineArg) => void; onBrushLineAdded: (arg: AddBrushLineArg) => void;
onEraserLineAdded: (arg: AddEraserLineArg) => void; onEraserLineAdded: (arg: AddEraserLineArg) => void;
onPointAddedToLine: (arg: AddPointToLineArg) => void; onPointAddedToLine: (arg: AddPointToLineArg) => void;
@ -46,14 +52,14 @@ type SetStageEventHandlersArg = {
* Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the * Updates the last cursor position atom with the current cursor position, returning the new position or `null` if the
* cursor is not over the stage. * cursor is not over the stage.
* @param stage The konva stage * @param stage The konva stage
* @param $lastCursorPos The last cursor pos as a nanostores atom * @param setLastCursorPos The callback to store the cursor pos
*/ */
const updateLastCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom<Vector2d | null>) => { const updateLastCursorPos = (stage: Konva.Stage, setLastCursorPos: Arg['setLastCursorPos']) => {
const pos = getScaledFlooredCursorPosition(stage); const pos = getScaledFlooredCursorPosition(stage);
if (!pos) { if (!pos) {
return null; return null;
} }
$lastCursorPos.set(pos); setLastCursorPos(pos);
return pos; return pos;
}; };
@ -68,51 +74,59 @@ const updateLastCursorPos = (stage: Konva.Stage, $lastCursorPos: WritableAtom<Ve
const maybeAddNextPoint = ( const maybeAddNextPoint = (
layerId: string, layerId: string,
currentPos: Vector2d, currentPos: Vector2d,
$lastAddedPoint: WritableAtom<Vector2d | null>, getLastAddedPoint: Arg['getLastAddedPoint'],
$brushSpacingPx: WritableAtom<number>, setLastAddedPoint: Arg['setLastAddedPoint'],
onPointAddedToLine: (arg: AddPointToLineArg) => void getBrushSpacingPx: Arg['getBrushSpacingPx'],
onPointAddedToLine: Arg['onPointAddedToLine']
) => { ) => {
// Continue the last line // Continue the last line
const lastAddedPoint = $lastAddedPoint.get(); const lastAddedPoint = getLastAddedPoint();
if (lastAddedPoint) { if (lastAddedPoint) {
// Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number // Dispatching redux events impacts perf substantially - using brush spacing keeps dispatches to a reasonable number
if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < $brushSpacingPx.get()) { if (Math.hypot(lastAddedPoint.x - currentPos.x, lastAddedPoint.y - currentPos.y) < getBrushSpacingPx()) {
return; return;
} }
} }
$lastAddedPoint.set(currentPos); setLastAddedPoint(currentPos);
onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] }); onPointAddedToLine({ layerId, point: [currentPos.x, currentPos.y] });
}; };
export const setStageEventHandlers = ({ export const setStageEventHandlers = ({
stage, stage,
$tool, getTool,
$toolBuffer, setTool,
$isDrawing, getToolBuffer,
$isMouseDown, setToolBuffer,
$lastMouseDownPos, getIsDrawing,
$lastCursorPos, setIsDrawing,
$lastAddedPoint, getIsMouseDown,
$stageAttrs, setIsMouseDown,
$brushColor, getLastMouseDownPos,
$brushSize, setLastMouseDownPos,
$brushSpacingPx, getLastCursorPos,
$selectedLayer, setLastCursorPos,
$shouldInvertBrushSizeScrollDirection, getLastAddedPoint,
$isSpaceDown, setLastAddedPoint,
setStageAttrs,
getBrushColor,
getBrushSize,
getBrushSpacingPx,
getSelectedLayer,
getShouldInvert,
getSpaceKey,
onBrushLineAdded, onBrushLineAdded,
onEraserLineAdded, onEraserLineAdded,
onPointAddedToLine, onPointAddedToLine,
onRectShapeAdded, onRectShapeAdded,
onBrushSizeChanged, onBrushSizeChanged,
}: SetStageEventHandlersArg): (() => void) => { }: Arg): (() => void) => {
//#region mouseenter //#region mouseenter
stage.on('mouseenter', (e) => { stage.on('mouseenter', (e) => {
const stage = e.target.getStage(); const stage = e.target.getStage();
if (!stage) { if (!stage) {
return; return;
} }
const tool = $tool.get(); const tool = getTool();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
}); });
@ -122,10 +136,10 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
$isMouseDown.set(true); setIsMouseDown(true);
const tool = $tool.get(); const tool = getTool();
const pos = updateLastCursorPos(stage, $lastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedLayer = $selectedLayer.get(); const selectedLayer = getSelectedLayer();
if (!pos || !selectedLayer) { if (!pos || !selectedLayer) {
return; return;
} }
@ -133,7 +147,7 @@ export const setStageEventHandlers = ({
return; return;
} }
if ($isSpaceDown.get()) { if (getSpaceKey()) {
// No drawing when space is down - we are panning the stage // No drawing when space is down - we are panning the stage
return; return;
} }
@ -142,10 +156,10 @@ export const setStageEventHandlers = ({
onBrushLineAdded({ onBrushLineAdded({
layerId: selectedLayer.id, layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y], points: [pos.x, pos.y, pos.x, pos.y],
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
}); });
$isDrawing.set(true); setIsDrawing(true);
$lastMouseDownPos.set(pos); setLastMouseDownPos(pos);
} }
if (tool === 'eraser') { if (tool === 'eraser') {
@ -153,13 +167,13 @@ export const setStageEventHandlers = ({
layerId: selectedLayer.id, layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y], points: [pos.x, pos.y, pos.x, pos.y],
}); });
$isDrawing.set(true); setIsDrawing(true);
$lastMouseDownPos.set(pos); setLastMouseDownPos(pos);
} }
if (tool === 'rect') { if (tool === 'rect') {
$isDrawing.set(true); setIsDrawing(true);
$lastMouseDownPos.set(snapPosToStage(pos, stage)); setLastMouseDownPos(snapPosToStage(pos, stage));
} }
}); });
@ -169,9 +183,9 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
$isMouseDown.set(false); setIsMouseDown(false);
const pos = $lastCursorPos.get(); const pos = getLastCursorPos();
const selectedLayer = $selectedLayer.get(); const selectedLayer = getSelectedLayer();
if (!pos || !selectedLayer) { if (!pos || !selectedLayer) {
return; return;
@ -180,15 +194,15 @@ export const setStageEventHandlers = ({
return; return;
} }
if ($isSpaceDown.get()) { if (getSpaceKey()) {
// No drawing when space is down - we are panning the stage // No drawing when space is down - we are panning the stage
return; return;
} }
const tool = $tool.get(); const tool = getTool();
if (tool === 'rect') { if (tool === 'rect') {
const lastMouseDownPos = $lastMouseDownPos.get(); const lastMouseDownPos = getLastMouseDownPos();
if (lastMouseDownPos) { if (lastMouseDownPos) {
const snappedPos = snapPosToStage(pos, stage); const snappedPos = snapPosToStage(pos, stage);
onRectShapeAdded({ onRectShapeAdded({
@ -199,13 +213,13 @@ export const setStageEventHandlers = ({
width: Math.abs(snappedPos.x - lastMouseDownPos.x), width: Math.abs(snappedPos.x - lastMouseDownPos.x),
height: Math.abs(snappedPos.y - lastMouseDownPos.y), height: Math.abs(snappedPos.y - lastMouseDownPos.y),
}, },
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
}); });
} }
} }
$isDrawing.set(false); setIsDrawing(false);
$lastMouseDownPos.set(null); setLastMouseDownPos(null);
}); });
//#region mousemove //#region mousemove
@ -214,9 +228,9 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
const tool = $tool.get(); const tool = getTool();
const pos = updateLastCursorPos(stage, $lastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
const selectedLayer = $selectedLayer.get(); const selectedLayer = getSelectedLayer();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser'); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(tool === 'brush' || tool === 'eraser');
@ -227,38 +241,52 @@ export const setStageEventHandlers = ({
return; return;
} }
if ($isSpaceDown.get()) { if (getSpaceKey()) {
// No drawing when space is down - we are panning the stage // No drawing when space is down - we are panning the stage
return; return;
} }
if (!getIsMouseDown(e)) { if (!getIsMouseDown()) {
return; return;
} }
if (tool === 'brush') { if (tool === 'brush') {
if ($isDrawing.get()) { if (getIsDrawing()) {
// Continue the last line // Continue the last line
maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); maybeAddNextPoint(
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
} else { } else {
// Start a new line // Start a new line
onBrushLineAdded({ onBrushLineAdded({
layerId: selectedLayer.id, layerId: selectedLayer.id,
points: [pos.x, pos.y, pos.x, pos.y], points: [pos.x, pos.y, pos.x, pos.y],
color: selectedLayer.type === 'raster_layer' ? $brushColor.get() : DEFAULT_RGBA_COLOR, color: selectedLayer.type === 'raster_layer' ? getBrushColor() : DEFAULT_RGBA_COLOR,
}); });
$isDrawing.set(true); setIsDrawing(true);
} }
} }
if (tool === 'eraser') { if (tool === 'eraser') {
if ($isDrawing.get()) { if (getIsDrawing()) {
// Continue the last line // Continue the last line
maybeAddNextPoint(selectedLayer.id, pos, $lastAddedPoint, $brushSpacingPx, onPointAddedToLine); maybeAddNextPoint(
selectedLayer.id,
pos,
getLastAddedPoint,
setLastAddedPoint,
getBrushSpacingPx,
onPointAddedToLine
);
} else { } else {
// Start a new line // Start a new line
onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] }); onEraserLineAdded({ layerId: selectedLayer.id, points: [pos.x, pos.y, pos.x, pos.y] });
$isDrawing.set(true); setIsDrawing(true);
} }
} }
}); });
@ -269,12 +297,12 @@ export const setStageEventHandlers = ({
if (!stage) { if (!stage) {
return; return;
} }
const pos = updateLastCursorPos(stage, $lastCursorPos); const pos = updateLastCursorPos(stage, setLastCursorPos);
$isDrawing.set(false); setIsDrawing(false);
$lastCursorPos.set(null); setLastCursorPos(null);
$lastMouseDownPos.set(null); setLastMouseDownPos(null);
const selectedLayer = $selectedLayer.get(); const selectedLayer = getSelectedLayer();
const tool = $tool.get(); const tool = getTool();
stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false); stage.findOne<Konva.Layer>(`#${PREVIEW_TOOL_GROUP_ID}`)?.visible(false);
@ -284,11 +312,11 @@ export const setStageEventHandlers = ({
if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') { if (selectedLayer.type !== 'regional_guidance_layer' && selectedLayer.type !== 'raster_layer') {
return; return;
} }
if ($isSpaceDown.get()) { if (getSpaceKey()) {
// No drawing when space is down - we are panning the stage // No drawing when space is down - we are panning the stage
return; return;
} }
if (getIsMouseDown(e)) { if (getIsMouseDown()) {
if (tool === 'brush') { if (tool === 'brush') {
onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] }); onPointAddedToLine({ layerId: selectedLayer.id, point: [pos.x, pos.y] });
} }
@ -304,11 +332,11 @@ export const setStageEventHandlers = ({
if (e.evt.ctrlKey || e.evt.metaKey) { if (e.evt.ctrlKey || e.evt.metaKey) {
let delta = e.evt.deltaY; let delta = e.evt.deltaY;
if ($shouldInvertBrushSizeScrollDirection.get()) { if (getShouldInvert()) {
delta = -delta; delta = -delta;
} }
// Holding ctrl or meta while scrolling changes the brush size // Holding ctrl or meta while scrolling changes the brush size
onBrushSizeChanged(calculateNewBrushSize($brushSize.get(), delta)); onBrushSizeChanged(calculateNewBrushSize(getBrushSize(), delta));
} else { } else {
// We need the absolute cursor position - not the scaled position // We need the absolute cursor position - not the scaled position
const cursorPos = stage.getPointerPosition(); const cursorPos = stage.getPointerPosition();
@ -332,12 +360,12 @@ export const setStageEventHandlers = ({
stage.scaleX(newScale); stage.scaleX(newScale);
stage.scaleY(newScale); stage.scaleY(newScale);
stage.position(newPos); stage.position(newPos);
$stageAttrs.set({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale }); setStageAttrs({ ...newPos, width: stage.width(), height: stage.height(), scale: newScale });
} }
}); });
stage.on('dragmove', () => { stage.on('dragmove', () => {
$stageAttrs.set({ setStageAttrs({
x: stage.x(), x: stage.x(),
y: stage.y(), y: stage.y(),
width: stage.width(), width: stage.width(),
@ -350,7 +378,7 @@ export const setStageEventHandlers = ({
// Stage position should always be an integer, else we get fractional pixels which are blurry // Stage position should always be an integer, else we get fractional pixels which are blurry
stage.x(Math.floor(stage.x())); stage.x(Math.floor(stage.x()));
stage.y(Math.floor(stage.y())); stage.y(Math.floor(stage.y()));
$stageAttrs.set({ setStageAttrs({
x: stage.x(), x: stage.x(),
y: stage.y(), y: stage.y(),
width: stage.width(), width: stage.width(),
@ -365,11 +393,11 @@ export const setStageEventHandlers = ({
} }
// Cancel shape drawing on escape // Cancel shape drawing on escape
if (e.key === 'Escape') { if (e.key === 'Escape') {
$isDrawing.set(false); setIsDrawing(false);
$lastMouseDownPos.set(null); setLastMouseDownPos(null);
} else if (e.key === ' ') { } else if (e.key === ' ') {
$toolBuffer.set($tool.get()); setToolBuffer(getTool());
$tool.set('view'); setTool('view');
} }
}; };
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
@ -380,9 +408,9 @@ export const setStageEventHandlers = ({
return; return;
} }
if (e.key === ' ') { if (e.key === ' ') {
const toolBuffer = $toolBuffer.get(); const toolBuffer = getToolBuffer();
$tool.set(toolBuffer ?? 'move'); setTool(toolBuffer ?? 'move');
$toolBuffer.set(null); setToolBuffer(null);
} }
}; };
window.addEventListener('keyup', onKeyUp); window.addEventListener('keyup', onKeyUp);

View File

@ -997,7 +997,7 @@ export const $toolBuffer = atom<Tool | null>(null);
export const $lastCursorPos = atom<Vector2d | null>(null); export const $lastCursorPos = atom<Vector2d | null>(null);
export const $isPreviewVisible = atom(true); export const $isPreviewVisible = atom(true);
export const $lastAddedPoint = atom<Vector2d | null>(null); export const $lastAddedPoint = atom<Vector2d | null>(null);
export const $isSpaceDown = atom(false); export const $spaceKey = atom(false);
export const $stageAttrs = atom<StageAttrs>({ export const $stageAttrs = atom<StageAttrs>({
x: 0, x: 0,
y: 0, y: 0,