Skip to content

Commit fff7511

Browse files
Merge pull request #45 from KevinBusch/feature/new-react-sketch-pad-component
Feature/new react sketch pad component
2 parents 1c9c686 + c02142e commit fff7511

16 files changed

Lines changed: 1990 additions & 0 deletions

src/atoms/forms/canvas-sketch/canvas-sketch.ts

Lines changed: 507 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export enum CanvasObjectType {
2+
image = "image",
3+
line = "line",
4+
path = "path",
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum CanvasToolType {
2+
image = "Image-Draw-Tool",
3+
line = "Line-Canvas-Draw-Tool",
4+
pan = "Pan-Canvas-Object",
5+
pencil = "Pencil-Canvas-Draw-Tool",
6+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface PointerPosition {
2+
x: number;
3+
y: number;
4+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react";
2+
import { text, number, select, boolean } from "@storybook/addon-knobs";
3+
import { ReactCanvasSketch, ReactCanvasSketchValue } from "./react-canvas-sketch";
4+
import { CanvasDrawToolSettings } from "./tools/base-canvas-draw-tool";
5+
import { CanvasToolType } from "./enums/canvas-tool-type";
6+
7+
export default {
8+
component: ReactCanvasSketch,
9+
title: "Atoms | Forms / Canvas Sketch",
10+
};
11+
12+
const value: ReactCanvasSketchValue = {
13+
currentObjectIndex: -1,
14+
objects: []
15+
}
16+
17+
const onAddedStroke: (strokeSettings: CanvasDrawToolSettings) => void =
18+
(strokeSettings: CanvasDrawToolSettings) => { console.log(`STORYBOOK MESSAGE - onAddedStroke: ${JSON.stringify(strokeSettings)}`) };
19+
20+
const canvasToolTypes = [
21+
CanvasToolType.line,
22+
CanvasToolType.pan,
23+
CanvasToolType.pencil,
24+
];
25+
26+
export const reactCanvasSketch = () => (
27+
<ReactCanvasSketch
28+
backgroundImageUrl={text("Background Image URL", "https://rlv.zcache.com/yellow_emoji_birthday_party_happy_face_symbol_classic_round_sticker-r821c9ad7d35943228f0f0e973050e063_0ugmm_8byvr_704.jpg")}
29+
canvasHeight={number("Canvas Height", 720)}
30+
canvasWidth={number("Canvas Width", 1000)}
31+
className={text("Class Name", "")}
32+
containerHeight={number("Container Height", 700)}
33+
containerWidth={number("Container Width", 700)}
34+
onAddedStroke={onAddedStroke}
35+
redrawIncrement={number("Redraw Trigger Increment", 1)}
36+
canvasToolType={select("Tool Type", canvasToolTypes, CanvasToolType.pencil)}
37+
showCanvasBorder={boolean("Show Border", true)}
38+
toolWidth={number("Tool Width", 1)}
39+
toolColor={text("Tool Color", "#000000")}
40+
value={value}
41+
/>
42+
);
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import {
2+
CanvasSketch,
3+
CanvasSketchConfig
4+
} from "./canvas-sketch";
5+
import * as React from "react";
6+
import { CanvasToolType } from "./enums/canvas-tool-type";
7+
import { CanvasDrawToolSettings } from "./tools/base-canvas-draw-tool";
8+
import { useEffect } from "react";
9+
10+
// -------------------------------------------------------------------------------------------------
11+
// #region Interfaces
12+
// -------------------------------------------------------------------------------------------------
13+
14+
/**
15+
* Represents the objects containing all data necessary to redraw the sketch canvas at a point in
16+
* the history of the stack of drawing objects
17+
*/
18+
export interface ReactCanvasSketchValue {
19+
/**
20+
* The current point/index in the history stack of drawing objects
21+
*/
22+
currentObjectIndex: number;
23+
/**
24+
* The drawn objects stack
25+
*/
26+
objects: CanvasDrawToolSettings[];
27+
}
28+
29+
/**
30+
* Represents all properties supported by the <ReactCanvasSketch> component
31+
*/
32+
export interface ReactCanvasSketchProps {
33+
/**
34+
* The URL of the background image to be drawn to the sketchpad
35+
*/
36+
backgroundImageUrl: string;
37+
/**
38+
* The height of the canvas element
39+
*/
40+
canvasHeight: number;
41+
/**
42+
* The width of the canvas element
43+
*/
44+
canvasWidth: number;
45+
/**
46+
* The className that will be appended to the sketchpad's outer most container
47+
*/
48+
className: string;
49+
/**
50+
* The height of the container wrapping the sketchpad
51+
* NOTE: This can be smaller than the canvasHeight value since the sketchpad supports panning
52+
*/
53+
containerHeight: number;
54+
/**
55+
* The width of the container wrapping the sketchpad
56+
* NOTE: This can be smaller than the canvasWidth value since the sketchpad supports panning
57+
*/
58+
containerWidth: number;
59+
/**
60+
* Handler to allow caller to track added stroke settings whenever a tool draws a new object
61+
* to the drawn objects stack
62+
*/
63+
onAddedStroke: (strokeSettings: CanvasDrawToolSettings) => void;
64+
/**
65+
* Allows the caller to redraw the sketchpad by changing the numeric value provided
66+
*/
67+
redrawIncrement: number;
68+
/**
69+
* The tool to be selected by the canvas sketch library
70+
*/
71+
canvasToolType: CanvasToolType;
72+
/**
73+
* When true, displays a dashed border around the canvas to identify drawable area. When false,
74+
* hides the dashed border around the canvas.
75+
*/
76+
showCanvasBorder: boolean;
77+
/**
78+
* The width of the selected tool for use by the selected tool
79+
*/
80+
toolWidth: number;
81+
/**
82+
* The color of the selected tool for use by the selected tool
83+
*/
84+
toolColor: string;
85+
/**
86+
* The object literal containing all information necessary to redraw the objects containing all
87+
* data necessary to redraw the sketch canvas at a point in the history of the stack of drawing
88+
* objects
89+
*/
90+
value: ReactCanvasSketchValue;
91+
}
92+
93+
// #endregion Interfaces
94+
95+
96+
// -------------------------------------------------------------------------------------------------
97+
// #region Component
98+
// -------------------------------------------------------------------------------------------------
99+
100+
const ReactCanvasSketch: React.FunctionComponent<ReactCanvasSketchProps> = (
101+
props: React.PropsWithChildren<ReactCanvasSketchProps>
102+
) => {
103+
104+
// track the ref of the canvas elements
105+
const htmlCanvasBackgroundImage = React.createRef<HTMLCanvasElement>();
106+
const htmlCanvasSketch = React.createRef<HTMLCanvasElement>();
107+
108+
// initialize state
109+
const canvasSketchDefaultInstance: CanvasSketch = null as any;
110+
const [isInitialized, setIsInitialized] = React.useState(false);
111+
const [canvasSketch, setCanvasSketch] = React.useState(canvasSketchDefaultInstance);
112+
113+
// ---------------------------------------------------------------------------------------------
114+
// #region Effect Hooks
115+
// ---------------------------------------------------------------------------------------------
116+
117+
// initialization of canvas sketch - NOTE: Must be before other effects using canvasSketch!
118+
useEffect(() => {
119+
if (isInitialized) {
120+
// already initialized, bail
121+
return;
122+
}
123+
const {
124+
backgroundImageUrl,
125+
value,
126+
} = { ...props };
127+
128+
// set up the default options for the background image
129+
const canvasSketchConfig: CanvasSketchConfig = {
130+
backgroundImage: { src: backgroundImageUrl },
131+
backgroundImageCanvas: htmlCanvasBackgroundImage.current as HTMLCanvasElement,
132+
currentObjectIndex: value.currentObjectIndex,
133+
objectStack: value.objects,
134+
panX: 0,
135+
panY: 0,
136+
scaleFactor: 1,
137+
sketchCanvas: htmlCanvasSketch.current as HTMLCanvasElement,
138+
};
139+
140+
// initialize the canvas sketch object
141+
const newCanvasSketch = new CanvasSketch(canvasSketchConfig);
142+
143+
// set state
144+
setCanvasSketch(newCanvasSketch);
145+
setIsInitialized(true);
146+
}, [
147+
canvasSketch,
148+
htmlCanvasBackgroundImage,
149+
htmlCanvasSketch,
150+
isInitialized,
151+
props
152+
]);
153+
154+
// cleanup of canvas sketch when unmounting component
155+
useEffect(() => {
156+
return () => {
157+
// cleanup on unmount
158+
if (canvasSketch != null) {
159+
canvasSketch.dispose();
160+
}
161+
}
162+
}, [canvasSketch]);
163+
164+
// redraws current state when dimensions of container or canvas change
165+
useEffect(() => {
166+
if (canvasSketch == null) {
167+
return;
168+
}
169+
canvasSketch.redrawCurrentState();
170+
}, [
171+
canvasSketch,
172+
props.canvasHeight,
173+
props.canvasWidth,
174+
props.containerHeight,
175+
props.containerWidth,
176+
]);
177+
178+
// redraw the sketch canvas when current object index or objects changes
179+
useEffect(() => {
180+
if (canvasSketch == null) {
181+
return;
182+
}
183+
canvasSketch.redrawSketchAt(props.value.objects, props.value.currentObjectIndex);
184+
}, [
185+
canvasSketch,
186+
props.value.currentObjectIndex,
187+
props.value.objects,
188+
]);
189+
190+
// redraw the sketch canvas when redraw increment changes
191+
useEffect(() => {
192+
if (canvasSketch == null) {
193+
return;
194+
}
195+
canvasSketch.redrawSketch();
196+
}, [
197+
canvasSketch,
198+
props.redrawIncrement,
199+
]);
200+
201+
// redraw the background image when it changes
202+
useEffect(() => {
203+
if (canvasSketch == null) {
204+
return;
205+
}
206+
canvasSketch.redrawBackgroundImageUsing(props.backgroundImageUrl);
207+
}, [
208+
props.backgroundImageUrl,
209+
canvasSketch
210+
]);
211+
212+
// set on added stroke when changes
213+
useEffect(() => {
214+
if (canvasSketch == null) {
215+
return;
216+
}
217+
canvasSketch.setOnAddedToolStroke(props.onAddedStroke);
218+
}, [
219+
props.onAddedStroke,
220+
canvasSketch
221+
]);
222+
223+
// set tool color when it changes
224+
useEffect(() => {
225+
if (canvasSketch == null) {
226+
return;
227+
}
228+
canvasSketch.setToolColor(props.toolColor);
229+
}, [
230+
props.toolColor,
231+
canvasSketch
232+
]);
233+
234+
// set tool width when it changes
235+
useEffect(() => {
236+
if (canvasSketch == null) {
237+
return;
238+
}
239+
canvasSketch.setToolWidth(props.toolWidth);
240+
}, [
241+
props.toolWidth,
242+
canvasSketch
243+
]);
244+
245+
// set selected tool when it changes
246+
useEffect(() => {
247+
if (canvasSketch == null) {
248+
return;
249+
}
250+
canvasSketch.setSelectedTool(props.canvasToolType);
251+
}, [
252+
props.canvasToolType,
253+
canvasSketch
254+
]);
255+
256+
// #endregion Effect Hooks
257+
258+
259+
// configure styles for elemtns
260+
const canvasContainerStyles: React.CSSProperties = {
261+
height: props.canvasHeight,
262+
position: "relative",
263+
width: props.canvasWidth,
264+
};
265+
const sketchStyles: React.CSSProperties = {
266+
height: props.containerHeight,
267+
width: props.containerWidth,
268+
};
269+
const canvasStyles: React.CSSProperties = {
270+
position: "absolute",
271+
};
272+
if (props.showCanvasBorder) {
273+
canvasStyles.border = "1px dashed black";
274+
}
275+
276+
return (
277+
<div
278+
className={`c-react-canvas-sketch ${props.className}`}
279+
style={sketchStyles}>
280+
<div className="c-react-canvas-sketch__canvas-container" style={canvasContainerStyles}>
281+
<canvas
282+
className="c-react-canvas-sketch__background-image"
283+
height={props.canvasHeight}
284+
ref={htmlCanvasBackgroundImage}
285+
style={{ ...canvasStyles, zIndex: 0 }}
286+
width={props.canvasWidth} />
287+
<canvas
288+
className="c-react-canvas-sketch__field"
289+
height={props.canvasHeight}
290+
ref={htmlCanvasSketch}
291+
style={{ ...canvasStyles, zIndex: 1 }}
292+
width={props.canvasWidth}>
293+
Sorry, Canvas HTML5 element is not supported by your browser :(
294+
</canvas>
295+
</div>
296+
</div>
297+
);
298+
}
299+
300+
export { ReactCanvasSketch };

0 commit comments

Comments
 (0)