Components POC

This commit is contained in:
hazam 2020-01-05 00:02:07 +05:00
parent 3172109050
commit a73d74c4df
5 changed files with 404 additions and 2 deletions

269
src/components/Canvas.tsx Normal file
View file

@ -0,0 +1,269 @@
import React, {
createContext,
useReducer,
useCallback,
Dispatch,
useContext,
useState,
useRef,
useEffect
} from "react";
import rough from "roughjs/bin/wrappers/rough";
import { RoughCanvas } from "roughjs/bin/canvas";
import { Drawable } from "roughjs/bin/core";
type CanvasProps = {
width?: number;
height?: number;
children?: React.ReactNode;
backgroundColor?: string;
};
type CanvasState = {
scrollX: number;
scrollY: number;
scale: number;
};
type CanvasDraw = {
context: CanvasRenderingContext2D;
rc: RoughCanvas;
};
type MouseEventListener = (ev: React.MouseEvent) => void;
type CanvasEventListeners = {
mouseMoveListeners: MouseEventListener[];
mouseDownListeners: MouseEventListener[];
};
const CanvasEventListenersContext = createContext<CanvasEventListeners>({
mouseDownListeners: [],
mouseMoveListeners: []
});
const CanvasDrawContext = createContext<CanvasDraw | null>(null);
const CanvasStateContext = createContext<CanvasState>({
scale: 1,
scrollX: 0,
scrollY: 0
});
const CanvasDispatchContext = createContext<Dispatch<Action>>(() => {});
type Action =
| { type: "shift-scroll"; dx: number; dy: number }
| { type: "set-scale"; scale: number };
function reducer(state: CanvasState, action: Action): CanvasState {
switch (action.type) {
case "set-scale":
return {
...state,
scale: action.scale
};
case "shift-scroll":
return {
...state,
scrollX: state.scrollX + action.dx,
scrollY: state.scrollY + action.dy
};
default:
return state;
}
}
export function Canvas({
width,
height,
children,
backgroundColor
}: CanvasProps) {
const eventListenersRef = useRef<CanvasEventListeners>({
mouseDownListeners: [],
mouseMoveListeners: []
});
const [state, dispatch] = useReducer(reducer, {
scrollX: 0,
scrollY: 0,
scale: 1
});
const [canvasDraw, setCanvasDraw] = useState<CanvasDraw | null>(null);
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault();
dispatch({ type: "shift-scroll", dx: -e.deltaX, dy: -e.deltaY });
}, []);
const handleCanvasRef = useCallback((canvas: HTMLCanvasElement | null) => {
if (canvas) {
setCanvasDraw({
context: canvas.getContext("2d")!,
rc: rough.canvas(canvas)
});
} else {
setCanvasDraw(null);
}
}, []);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return;
e.preventDefault();
eventListenersRef.current.mouseDownListeners.forEach(fn => fn(e));
}, []);
const handleMouseMove = useCallback((e: React.MouseEvent) => {
e.preventDefault();
eventListenersRef.current.mouseMoveListeners.forEach(fn => fn(e));
}, []);
return (
<CanvasEventListenersContext.Provider value={eventListenersRef.current}>
<CanvasDrawContext.Provider value={canvasDraw}>
<CanvasDispatchContext.Provider value={dispatch}>
<CanvasStateContext.Provider value={state}>
<canvas
id="canvas"
ref={handleCanvasRef}
onWheel={handleWheel}
width={width}
height={height}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
>
{canvasDraw && (
<CanvasBackground
context={canvasDraw.context}
color={backgroundColor}
/>
)}
</canvas>
{children}
</CanvasStateContext.Provider>
</CanvasDispatchContext.Provider>
</CanvasDrawContext.Provider>
</CanvasEventListenersContext.Provider>
);
}
function CanvasBackground({
context,
color
}: {
context: CanvasRenderingContext2D;
color?: string;
}) {
const fillStyle = context.fillStyle;
if (typeof color === "string") {
context.fillStyle = color;
context.fillRect(-0.5, -0.5, context.canvas.width, context.canvas.height);
} else {
context.clearRect(-0.5, -0.5, context.canvas.width, context.canvas.height);
}
context.fillStyle = fillStyle;
return null;
}
function useCanvasState(): CanvasState {
return useContext(CanvasStateContext);
}
function useCanvasDispatch(): Dispatch<Action> {
return useContext(CanvasDispatchContext);
}
function useCanvasEventListener(): CanvasEventListeners {
return useContext(CanvasEventListenersContext);
}
function useCanvasDraw(): CanvasDraw | null {
return useContext(CanvasDrawContext);
}
type CanvasElementProps = {
x: number;
y: number;
hitTest(targetX: number, targetY: number): boolean;
getShape?(rc: RoughCanvas): Drawable;
draw(
rc: RoughCanvas,
context: CanvasRenderingContext2D,
shape: Drawable | undefined
): void;
onTap?(ev: React.MouseEvent): void;
onHover?(ev: React.MouseEvent): void;
};
export function CanvasElement({
x,
y,
getShape,
draw,
hitTest,
onTap,
onHover
}: CanvasElementProps) {
const canvasDraw = useCanvasDraw();
const { scrollX, scrollY, scale } = useCanvasState();
const shapeRef = useRef<Drawable | undefined>(undefined);
const { mouseDownListeners, mouseMoveListeners } = useCanvasEventListener();
useEffect(() => {
const mouseDownHandler: MouseEventListener = e => {
if (onTap && e.target instanceof HTMLElement) {
const targetX = e.clientX - e.target.offsetLeft - scrollX;
const targetY = e.clientY - e.target.offsetTop - scrollY;
if (hitTest(targetX, targetY)) onTap(e);
}
};
mouseDownListeners.push(mouseDownHandler);
const mouseMoveHandler: MouseEventListener = e => {
if (onHover && e.target instanceof HTMLElement) {
const targetX = e.clientX - e.target.offsetLeft - scrollX;
const targetY = e.clientY - e.target.offsetTop - scrollY;
if (hitTest(targetX, targetY)) onHover(e);
}
};
mouseMoveListeners.push(mouseMoveHandler);
return () => {
const mouseDownIdx = mouseDownListeners.indexOf(mouseDownHandler);
mouseDownListeners.splice(mouseDownIdx, 1);
const mouseMoveIdx = mouseMoveListeners.indexOf(mouseMoveHandler);
mouseMoveListeners.splice(mouseMoveIdx, 1);
};
}, [
hitTest,
mouseDownListeners,
mouseMoveListeners,
onHover,
onTap,
scrollX,
scrollY
]);
useEffect(() => {
if (canvasDraw && getShape) shapeRef.current = getShape(canvasDraw.rc);
}, [getShape, canvasDraw]);
if (!canvasDraw) {
return null;
}
if (!shapeRef.current && getShape) {
shapeRef.current = getShape(canvasDraw.rc);
}
const { rc, context } = canvasDraw;
context.translate(x + scrollX, y + scrollY);
draw(rc, context, shapeRef.current);
context.translate(-x - scrollX, -y - scrollY);
return null;
}

View file

@ -0,0 +1,67 @@
import React from "react";
import { distanceBetweenPointAndSegment } from "../utils/distanceBetweenPointAndSegment";
import { CanvasElement } from "./Canvas";
type RectangleProps = {
x: number;
y: number;
width: number;
height: number;
strokeColor?: string;
fillColor?: string;
onHover?(): void;
onTap?(): void;
};
export function Rectangle({
x,
y,
width,
height,
strokeColor,
fillColor,
onHover,
onTap
}: RectangleProps) {
return (
<CanvasElement
x={x}
y={y}
hitTest={(targetX, targetY) => {
const x1 = Math.min(x, x + width);
const x2 = Math.max(x, x + width);
const y1 = Math.min(y, y + height);
const y2 = Math.max(y, y + height);
return rectHitTest(targetX, targetY, x1, y1, x2, y2);
}}
getShape={rc =>
rc.generator.rectangle(0, 0, width, height, {
stroke: strokeColor,
fill: fillColor
})
}
draw={(rc, context, shape) => {
rc.draw(shape!);
}}
onHover={onHover}
onTap={onTap}
/>
);
}
function rectHitTest(
x: number,
y: number,
x1: number,
y1: number,
x2: number,
y2: number
) {
const lineThreshold = 10;
return (
distanceBetweenPointAndSegment(x, y, x1, y1, x2, y1) < lineThreshold || // A
distanceBetweenPointAndSegment(x, y, x2, y1, x2, y2) < lineThreshold || // B
distanceBetweenPointAndSegment(x, y, x2, y2, x1, y2) < lineThreshold || // C
distanceBetweenPointAndSegment(x, y, x1, y2, x1, y1) < lineThreshold // D
);
}

View file

@ -0,0 +1,30 @@
import React from "react";
import { Canvas } from "./Canvas";
import { Rectangle } from "./Rectangle";
export function TestApp() {
return (
<Canvas
width={window.innerWidth}
height={window.innerHeight - 4}
backgroundColor="#eeeeee"
>
<Rectangle
x={10}
y={10}
width={50}
height={20}
onTap={() => alert("Clicked on rect 1")}
strokeColor="blue"
/>
<Rectangle
x={70}
y={20}
width={30}
height={40}
onHover={() => alert("Hovered over rect 2")}
fillColor="red"
/>
</Canvas>
);
}

View file

@ -6,6 +6,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
import "./styles.scss";
import { TestApp } from "./components/TestApp";
type ExcalidrawElement = ReturnType<typeof newElement>;
type ExcalidrawTextElement = ExcalidrawElement & {
@ -1399,7 +1400,7 @@ class App extends React.Component<{}, AppState> {
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ReactDOM.render(<TestApp />, rootElement);
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const rc = rough.canvas(canvas);
const context = canvas.getContext("2d")!;
@ -1408,4 +1409,4 @@ const context = canvas.getContext("2d")!;
// https://stackoverflow.com/questions/13879322/drawing-a-1px-thick-line-in-canvas-creates-a-2px-thick-line/13879402#comment90766599_13879402
context.translate(0.5, 0.5);
ReactDOM.render(<App />, rootElement);
ReactDOM.render(<TestApp />, rootElement);

View file

@ -0,0 +1,35 @@
// https://stackoverflow.com/a/6853926/232122
export function distanceBetweenPointAndSegment(
x: number,
y: number,
x1: number,
y1: number,
x2: number,
y2: number
) {
const A = x - x1;
const B = y - y1;
const C = x2 - x1;
const D = y2 - y1;
const dot = A * C + B * D;
const lenSquare = C * C + D * D;
let param = -1;
if (lenSquare !== 0) {
// in case of 0 length line
param = dot / lenSquare;
}
let xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
const dx = x - xx;
const dy = y - yy;
return Math.hypot(dx, dy);
}