Files
image-studio/view/CanvasViewport.tsx

101 lines
4.0 KiB
TypeScript

import { useMemo, useRef } from "react";
import type { ImageDocument } from "@core/document";
import type { AppState, MaskEditState } from "@editor/state";
import type { AppStore } from "@editor/store";
import type { InteractionMode } from "@editor/tools";
import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index";
import { brushUnavailableHint, canPreviewBrush, type BrushTargetEditorState } from "@operations/paint/brush";
import { canvasCursorClass } from "./canvas/cursor";
import { useCanvasInput } from "./canvas/useCanvasInput";
import { useCanvasRenderer } from "./canvas/useCanvasRenderer";
import { useCanvasResize } from "./canvas/useCanvasResize";
import { useAppState } from "./useAppState";
const ignoreGlobalKeybind: GlobalKeybindConsumer = () => false;
const ignoreGlobalPointer: GlobalPointerConsumer = () => false;
const ignoreGlobalWheel: GlobalWheelConsumer = () => false;
export type CanvasViewportProps = {
store: AppStore;
globalKeybindConsumer?: GlobalKeybindConsumer;
globalPointerConsumer?: GlobalPointerConsumer;
globalWheelConsumer?: GlobalWheelConsumer;
};
export function CanvasViewport({
store,
globalKeybindConsumer = ignoreGlobalKeybind,
globalPointerConsumer = ignoreGlobalPointer,
globalWheelConsumer = ignoreGlobalWheel,
}: CanvasViewportProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const cursorState = useAppState(store, selectCanvasCursorState, canvasCursorStatesEqual);
const inputOptions = useMemo(
() => ({ globalKeybindConsumer, globalPointerConsumer, globalWheelConsumer }),
[globalKeybindConsumer, globalPointerConsumer, globalWheelConsumer],
);
useCanvasRenderer(canvasRef, store);
useCanvasResize(canvasRef, store.dispatch);
useCanvasInput(canvasRef, store, inputOptions);
const brushHint = brushUnavailableHint(cursorState.document, cursorState.editor);
const hasBrushPreview = Boolean(cursorState.hasBrushPreview && !brushHint && canPreviewBrush(cursorState.document, cursorState.editor));
const cursorClass = canvasCursorClass(cursorState.editor.tools.interactionMode, cursorState.isPanning, hasBrushPreview, !brushHint);
return (
<canvas
ref={canvasRef}
className={`h-full w-full touch-none ${cursorClass}`}
role="img"
aria-label="Image editing canvas. Use the tools and layers controls to edit the document."
/>
);
}
type CanvasCursorState = {
document: ImageDocument;
editor: BrushTargetEditorState;
hasBrushPreview: boolean;
isPanning: boolean;
};
function selectCanvasCursorState(state: AppState): CanvasCursorState {
return {
document: state.document,
editor: {
selection: state.editor.selection,
tools: {
activeTool: state.editor.tools.activeTool,
interactionMode: state.editor.tools.interactionMode,
},
maskEdit: state.editor.maskEdit,
},
hasBrushPreview: Boolean(state.editor.brushPreview),
isPanning: state.editor.pointerSession?.type === "pan",
};
}
function canvasCursorStatesEqual(a: CanvasCursorState, b: CanvasCursorState): boolean {
return (
a.document === b.document &&
a.hasBrushPreview === b.hasBrushPreview &&
a.isPanning === b.isPanning &&
a.editor.selection === b.editor.selection &&
a.editor.tools.activeTool === b.editor.tools.activeTool &&
interactionModesEqual(a.editor.tools.interactionMode, b.editor.tools.interactionMode) &&
maskEditStatesEqual(a.editor.maskEdit, b.editor.maskEdit)
);
}
function interactionModesEqual(a: InteractionMode, b: InteractionMode): boolean {
if (a.type !== b.type) return false;
if (a.type === "temporary-pan") return b.type === "temporary-pan" && a.previousTool === b.previousTool;
return b.type === "tool" && a.tool === b.tool;
}
function maskEditStatesEqual(a: MaskEditState | undefined, b: MaskEditState | undefined): boolean {
if (a === b) return true;
if (!a || !b) return false;
return a.targetLayerId === b.targetLayerId && a.maskLayerId === b.maskLayerId && a.viewMode === b.viewMode;
}