import { TransformableImage } from "@/components/transformable-image"; import type { KonvaEventObject } from "konva/lib/Node"; import { Layer, Rect, Stage } from "react-konva"; import { useAppDispatch, useAppSelector } from "@/hooks"; import { appSlice, deselectItem, StageItemType, updateImage, updateText, } from "@/store/app.slice"; import { useCallback, useEffect, useRef, useState } from "react"; import { Toolbar } from "@/components/toolbar"; import TransformableText from "@/components/transformable-text"; import LayerBorder from "@/components/layer-border"; import { CANVAS_PADDING_X, CANVAS_PADDING_Y } from "@/consts/canvas-params"; import Legacy from "@/components/layout/sidebar/legacy"; import { type Shape } from "konva/lib/Shape"; import Konva from "konva"; type Snap = "start" | "center" | "end"; type SnappingEdges = { vertical: Array<{ guide: number; offset: number; snap: Snap; }>; horizontal: Array<{ guide: number; offset: number; snap: Snap; }>; }; const GUIDELINE_OFFSET = 5; const Canvas = () => { const dispatch = useAppDispatch(); const currentStep = useAppSelector((state) => state.app.currentStep); const stage = useAppSelector( (state) => state.app.history[currentStep]?.stage, ); const stageRef = useRef(null); const layerRef = useRef(null); const selectedItemId = useAppSelector( (state) => state.app.history[currentStep]?.selectedItemId, ); const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id)); const items = useAppSelector( (state) => state.app.history[currentStep]?.items, ); const backgroundRect = useAppSelector( (state) => state.app.history[currentStep]?.backgroundColor, ); const backgroundId = "background"; const deselectHandler = ( e: KonvaEventObject | KonvaEventObject, ) => { const target = e.target; const clickedOnEmpty = target === target.getStage() || (typeof target === "object" && target.attrs.id === backgroundId); if (!clickedOnEmpty) { return; } dispatch(deselectItem()); }; const [isEditing, setIsEditing] = useState(false); const [isTransforming, setIsTransforming] = useState(false); const [selected, setSelected] = useState(false); useEffect(() => { if (!selected && isEditing) { setIsEditing(false); } else if (!selected && isTransforming) { setIsTransforming(false); } }, [selected, isEditing, isTransforming]); function toggleEdit() { setIsEditing(!isEditing); setSelected(!isEditing); } /* Drag Drop logic */ const getObjectSnappingEdges = useCallback((node: Shape): SnappingEdges => { const box = node.getClientRect(); const absPos = node.absolutePosition(); return { vertical: [ { guide: Math.round(box.x), offset: Math.round(absPos.x - box.x), snap: "start", }, { guide: Math.round(box.x + box.width / 2), offset: Math.round(absPos.x - box.x - box.width / 2), snap: "center", }, { guide: Math.round(box.x + box.width), offset: Math.round(absPos.x - box.x - box.width), snap: "end", }, ], horizontal: [ { guide: Math.round(box.y), offset: Math.round(absPos.y - box.y), snap: "start", }, { guide: Math.round(box.y + box.height / 2), offset: Math.round(absPos.y - box.y - box.height / 2), snap: "center", }, { guide: Math.round(box.y + box.height), offset: Math.round(absPos.y - box.y - box.height), snap: "end", }, ], }; }, []); const getLineGuideStops = (skipShape: Konva.Shape) => { const stage = skipShape.getStage(); if (!stage) return { vertical: [], horizontal: [] }; // we can snap to playground-area borders and the center of the playground-area const vertical = [0, stage.width() / 2, stage.width()]; const horizontal = [0, stage.height() / 2, stage.height()]; // and we snap over edges and center of each object on the canvas stage.find(".object").forEach((guideItem) => { if (guideItem === skipShape) { return; } const box = guideItem.getClientRect(); // and we can snap to all edges of shapes vertical.push(box.x, box.x + box.width, box.x + box.width / 2); horizontal.push(box.y, box.y + box.height, box.y + box.height / 2); }); return { vertical, horizontal, }; }; const getGuides = useCallback( ( lineGuideStops: ReturnType, itemBounds: ReturnType, ) => { const resultV: Array<{ lineGuide: number; diff: number; snap: Snap; offset: number; }> = []; const resultH: Array<{ lineGuide: number; diff: number; snap: Snap; offset: number; }> = []; lineGuideStops.vertical.forEach((lineGuide) => { itemBounds.vertical.forEach((itemBound) => { const diff = Math.abs(lineGuide - itemBound.guide); if (diff < GUIDELINE_OFFSET) { resultV.push({ lineGuide: lineGuide, diff: diff, snap: itemBound.snap, offset: itemBound.offset, }); } }); }); lineGuideStops.horizontal.forEach((lineGuide) => { itemBounds.horizontal.forEach((itemBound) => { const diff = Math.abs(lineGuide - itemBound.guide); if (diff < GUIDELINE_OFFSET) { resultH.push({ lineGuide: lineGuide, diff: diff, snap: itemBound.snap, offset: itemBound.offset, }); } }); }); const guides: Array<{ lineGuide: number; offset: number; orientation: "V" | "H"; snap: "start" | "center" | "end"; }> = []; const minV = resultV.sort((a, b) => a.diff - b.diff)[0]; const minH = resultH.sort((a, b) => a.diff - b.diff)[0]; if (minV) { guides.push({ lineGuide: minV.lineGuide, offset: minV.offset, orientation: "V", snap: minV.snap, }); } if (minH) { guides.push({ lineGuide: minH.lineGuide, offset: minH.offset, orientation: "H", snap: minH.snap, }); } return guides; }, [], ); const drawGuides = useCallback( (guides: ReturnType, layer: Konva.Layer) => { guides.forEach((lg) => { if (lg.orientation === "H") { const line = new Konva.Line({ points: [-6000, 0, 6000, 0], stroke: "rgb(0, 161, 255)", strokeWidth: 1, name: "guid-line", dash: [4, 6], }); layer.add(line); line.absolutePosition({ x: 0, y: lg.lineGuide, }); } else if (lg.orientation === "V") { const line = new Konva.Line({ points: [0, -6000, 0, 6000], stroke: "rgb(0, 161, 255)", strokeWidth: 1, name: "guid-line", dash: [4, 6], }); layer.add(line); line.absolutePosition({ x: lg.lineGuide, y: 0, }); } }); }, [], ); const onDragMove = useCallback( (e: Konva.KonvaEventObject) => { const layer = e.target.getLayer(); if (!layer) { return; } // clear all previous lines on the screen layer.find(".guid-line").forEach((l) => l.destroy()); // find possible snapping lines const lineGuideStops = getLineGuideStops(e.target as Konva.Shape); // find snapping points of current object const itemBounds = getObjectSnappingEdges(e.target as Konva.Shape); // now find where can we snap current object const guides = getGuides(lineGuideStops, itemBounds); // do nothing if no snapping if (!guides.length) { return; } drawGuides(guides, layer); const absPos = e.target.absolutePosition(); // now force object position guides.forEach((lg) => { switch (lg.snap) { case "start": { switch (lg.orientation) { case "V": { absPos.x = lg.lineGuide + lg.offset; break; } case "H": { absPos.y = lg.lineGuide + lg.offset; break; } } break; } case "center": { switch (lg.orientation) { case "V": { absPos.x = lg.lineGuide + lg.offset; break; } case "H": { absPos.y = lg.lineGuide + lg.offset; break; } } break; } case "end": { switch (lg.orientation) { case "V": { absPos.x = lg.lineGuide + lg.offset; break; } case "H": { absPos.y = lg.lineGuide + lg.offset; break; } } break; } } }); e.target.absolutePosition(absPos); }, [drawGuides, getGuides, getObjectSnappingEdges], ); const onDragEnd = (e: Konva.KonvaEventObject) => { const layer = e.target.getLayer(); // clear all previous lines on the screen layer?.find(".guid-line").forEach((l) => l.destroy()); }; if (!stage) return null; return (
{ if (e.target !== e.currentTarget) { return; } dispatch(deselectItem()); }} > {!!backgroundRect && ( )} {items?.toReversed().map((item) => { if (item.type === StageItemType.Image) { return ( selectItem(item.id)} isSelected={item.id === selectedItemId} onChange={(newAttrs) => { dispatch(updateImage({ id: item.id, ...newAttrs })); }} imageProps={{ ...item.params, onDragMove, onDragEnd, name: "object", }} key={item.id} isBlocked={item.isBlocked} /> ); } if (item.type === StageItemType.Text) { return ( selectItem(item.id)} isSelected={item.id === selectedItemId} onChange={(newAttrs) => { dispatch(updateText({ id: item.id, ...newAttrs })); }} textProps={{ ...item.params, onDragMove, onDragEnd, name: "object", }} key={item.id} id={item.id} /> ); } })}
); }; export default Canvas;