Skip to content

Commit dda27fa

Browse files
author
Kevin Busch
committed
Initial add of all the sketchpad files
1 parent e0a28b2 commit dda27fa

13 files changed

Lines changed: 1640 additions & 0 deletions
Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
import { ImageSettings, ImageCanvasTool } from "./tools/image-canvas-tool";
2+
import { CanvasToolSettings, CanvasTool, ToolConfig } from "./tools/base-canvas-tool";
3+
import { LineCanvasDrawTool } from "./tools/line-canvas-draw-tool";
4+
import { PencilCanvasDrawTool } from "./tools/pencil-canvas-draw-tool";
5+
import { PanCanvasTool } from "./tools/pan-canvas-tool";
6+
import { DrawToolUiSettings, DrawToolConfig, CanvasDrawToolSettings, CanvasDrawTool } from "./tools/base-canvas-draw-tool";
7+
import { CanvasToolType } from "./enums/canvas-tool-type";
8+
import { CoreUtils } from "andculturecode-javascript-core";
9+
import { CanvasObjectType } from "./enums/canvas-object-type";
10+
11+
// -------------------------------------------------------------------------------------------------
12+
// #region Interfaces
13+
// -------------------------------------------------------------------------------------------------
14+
15+
export interface CanvasSketchConfig {
16+
backgroundImage?: ImageSettings;
17+
backgroundImageCanvas?: HTMLCanvasElement;
18+
currentObjectIndex: number;
19+
panX: number;
20+
panY: number;
21+
scaleFactor: number;
22+
sketchCanvas: HTMLCanvasElement;
23+
objectStack: CanvasToolSettings[];
24+
}
25+
26+
// #endregion Interfaces
27+
28+
/**
29+
* Binds to the provided HTML canvases element and gives various tool capabilities
30+
*/
31+
class CanvasSketch {
32+
33+
public backgroundImageTool!: ImageCanvasTool;
34+
public imageTool!: ImageCanvasTool;
35+
public lineDrawTool!: LineCanvasDrawTool;
36+
public panTool!: PanCanvasTool;
37+
public pencilDrawTool!: PencilCanvasDrawTool;
38+
public selectedTool!: CanvasTool;
39+
40+
private _config: CanvasSketchConfig;
41+
private _backgroundImageContext!: CanvasRenderingContext2D;
42+
private _drawToolUiSettings: DrawToolUiSettings;
43+
private _drawToolConfig!: DrawToolConfig;
44+
private _isBrowserSupported!: boolean;
45+
private _sketchContext!: CanvasRenderingContext2D;
46+
private _toolConfig!: ToolConfig;
47+
48+
constructor(canvasSketchConfig: CanvasSketchConfig) {
49+
this._drawToolUiSettings = {
50+
color: "FFFFFF", // default
51+
width: 1, // default
52+
};
53+
54+
this._config = { ...canvasSketchConfig };
55+
56+
// initialize local canvas contexts references
57+
this._initializeCanvasContexts();
58+
59+
if (!this._isBrowserSupported) {
60+
// browser doesn't support the canvas... bail
61+
return;
62+
}
63+
64+
// order here matters... handlers passed into tools will be bound to those tools
65+
CoreUtils.bindAll(this);
66+
67+
// initialize all tools used by the sketchpad
68+
this._initializeTools();
69+
70+
// initialize current drawing
71+
this._initializeCurrentDrawing();
72+
}
73+
74+
// ---------------------------------------------------------------------------------------------
75+
// #region Public Methods
76+
// ---------------------------------------------------------------------------------------------
77+
78+
public dispose(): void {
79+
if (!this._isBrowserSupported) {
80+
return;
81+
}
82+
83+
this.selectedTool.dispose();
84+
}
85+
86+
public getTool(toolType: CanvasToolType): CanvasTool | null {
87+
if (!this._isBrowserSupported) {
88+
return null;
89+
}
90+
91+
switch (toolType) {
92+
case this.pencilDrawTool.toolType:
93+
return this.pencilDrawTool;
94+
case this.lineDrawTool.toolType:
95+
return this.lineDrawTool;
96+
case this.imageTool.toolType:
97+
return this.imageTool;
98+
case this.panTool.toolType:
99+
return this.panTool;
100+
default:
101+
return null;
102+
}
103+
}
104+
105+
public redrawBackgroundImageUsing(backgroundImageUrl: string): void {
106+
if (this._config.backgroundImage == null) {
107+
return;
108+
}
109+
this._config.backgroundImage.src = backgroundImageUrl;
110+
this._redrawBackgroundImage();
111+
}
112+
113+
public redrawSketchAt(objects: CanvasToolSettings[], newCurrentObjectIndex: number): void {
114+
if (!this._isBrowserSupported) {
115+
return;
116+
}
117+
118+
if (objects == null) {
119+
console.error(`Please provide a new stroke stack`);
120+
return;
121+
}
122+
if (!Array.isArray(objects)) {
123+
console.error(`Please provide a valid array`);
124+
return;
125+
}
126+
if (newCurrentObjectIndex + 1 > objects.length) {
127+
console.error(`Drawing history only contains ${objects.length} objects. Cannot set object index to ${newCurrentObjectIndex}`);
128+
return;
129+
}
130+
if (newCurrentObjectIndex < -1) {
131+
console.error(`Cannot set new object index below -1. -1 should be used when nothing is to be drawn`);
132+
return;
133+
}
134+
135+
this._config.objectStack = objects;
136+
this._config.currentObjectIndex = newCurrentObjectIndex;
137+
138+
this._redrawSketch();
139+
}
140+
141+
public setSelectedTool(toolType: CanvasToolType): void {
142+
if (!this._isBrowserSupported) {
143+
return;
144+
}
145+
146+
if (this.selectedTool) {
147+
this.selectedTool.dispose();
148+
}
149+
150+
const selectedTool = this.getTool(toolType);
151+
if (selectedTool == null) {
152+
return;
153+
}
154+
this.selectedTool = selectedTool;
155+
if (this.selectedTool) {
156+
this.selectedTool.initialize();
157+
}
158+
}
159+
160+
public setToolColor(color: string): void {
161+
if (!this._isBrowserSupported) {
162+
return;
163+
}
164+
165+
this._drawToolUiSettings.color = color;
166+
}
167+
168+
public setOnAddedToolStroke(onAddedStroke: (strokeSettings: CanvasDrawToolSettings) => void): void {
169+
if (!this._isBrowserSupported) {
170+
return;
171+
}
172+
173+
this._drawToolConfig.onFinishStroke = (strokeSettings: CanvasDrawToolSettings) => {
174+
175+
// make sure to track stroke in stack
176+
this._onAddStroke(strokeSettings);
177+
178+
if (typeof (onAddedStroke) === "function") {
179+
// call
180+
onAddedStroke(strokeSettings);
181+
}
182+
};
183+
}
184+
185+
public setToolWidth(width: number): void {
186+
if (!this._isBrowserSupported) {
187+
return;
188+
}
189+
190+
this._drawToolUiSettings.width = width;
191+
}
192+
193+
// #endregion Public Methods
194+
195+
196+
// ---------------------------------------------------------------------------------------------
197+
// #region Private Methods
198+
// ---------------------------------------------------------------------------------------------
199+
200+
private _clearCanvas(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void {
201+
context.clearRect(0, 0, canvas.width, canvas.height);
202+
}
203+
204+
private _clearSketchCanvas(): void {
205+
if (!this._isBrowserSupported) {
206+
return;
207+
}
208+
209+
this._clearCanvas(this._config.sketchCanvas, this._sketchContext);
210+
}
211+
212+
private _clearBackgroundImageCanvas(): void {
213+
if (!this._isBrowserSupported) {
214+
return;
215+
}
216+
217+
if (this._config.backgroundImageCanvas == null) {
218+
return;
219+
}
220+
221+
this._clearCanvas(this._config.backgroundImageCanvas, this._backgroundImageContext);
222+
}
223+
224+
private _drawObjects(objectStack: CanvasToolSettings[]): void {
225+
if (!this._isBrowserSupported) {
226+
return;
227+
}
228+
229+
// Draw the pencil strokes
230+
const pencilStrokes = objectStack.filter((value: CanvasToolSettings) => value.type === CanvasObjectType.path) as CanvasDrawToolSettings[];
231+
this.pencilDrawTool.drawStrokes(pencilStrokes);
232+
233+
// Draw the line strokes
234+
const lineStrokes = objectStack.filter((value: CanvasToolSettings) => value.type === CanvasObjectType.line) as CanvasDrawToolSettings[];
235+
this.lineDrawTool.drawStrokes(lineStrokes);
236+
237+
// Draw the images
238+
const images = objectStack.filter((value: CanvasToolSettings) => value.type === CanvasObjectType.image) as any as ImageSettings[];
239+
this.imageTool.drawImages(images);
240+
}
241+
242+
private _onAddStroke(strokeSettings: CanvasDrawToolSettings): void {
243+
// track the changes in it's own stack.. making sure to consider the current object index (history in view currently)
244+
this._config.objectStack = this._getStackToDraw();
245+
this._config.objectStack.push(strokeSettings);
246+
247+
this._config.currentObjectIndex = this._config.currentObjectIndex + 1;
248+
}
249+
250+
private _getStackToDraw(): CanvasToolSettings[] {
251+
const stackToDraw = [...this._config.objectStack].slice(0, this._config.currentObjectIndex + 1);
252+
return stackToDraw;
253+
}
254+
255+
private _isInstanceOfDrawTool(tool: CanvasTool): tool is CanvasDrawTool {
256+
return "color" in tool
257+
&& "width" in tool
258+
&& "drawStrokes" in tool;
259+
}
260+
261+
private _initializeCanvasContexts(): void {
262+
if (this._config.sketchCanvas.getContext != null) {
263+
// browser supports the canvas tag, get the 2d drawing context for this canvas
264+
const sketchContext = this._config.sketchCanvas.getContext("2d");
265+
if (sketchContext == null) {
266+
return;
267+
}
268+
this._sketchContext = sketchContext;
269+
this._isBrowserSupported = true;
270+
}
271+
else {
272+
// browser does not support the canvas tag, bail
273+
this._isBrowserSupported = false;
274+
return;
275+
}
276+
277+
if (this._config.backgroundImageCanvas != null && this._config.backgroundImageCanvas.getContext != null) {
278+
// browser supports the canvas tag, get the 2d drawing context for this canvas
279+
const backgroundImageContext = this._config.backgroundImageCanvas.getContext("2d");
280+
if (backgroundImageContext == null) {
281+
return;
282+
}
283+
this._backgroundImageContext = backgroundImageContext;
284+
this._isBrowserSupported = true;
285+
}
286+
else {
287+
// browser does not support the canvas tag, bail
288+
this._isBrowserSupported = false;
289+
return;
290+
}
291+
}
292+
293+
private _initializeCurrentDrawing(): void {
294+
if (this._config.objectStack?.length > 0) {
295+
// default the current stroke based on the initialized stroke stack's last object
296+
const lastObjectIndex = this._config.objectStack.length - 1;
297+
298+
if (this._config.currentObjectIndex != null && lastObjectIndex !== this._config.currentObjectIndex) {
299+
// caller wants the index to be something different... overwrite with caller's demand
300+
this._config.currentObjectIndex = this._config.currentObjectIndex;
301+
}
302+
else {
303+
this._config.currentObjectIndex = lastObjectIndex;
304+
}
305+
306+
const stackToDraw = this._getStackToDraw();
307+
308+
this._drawObjects(stackToDraw);
309+
}
310+
311+
}
312+
313+
private _initializeTools(): void {
314+
// setup default config settings for all tools
315+
this._toolConfig = {
316+
canvas: this._config.sketchCanvas,
317+
context: this._sketchContext,
318+
redraw: this._redrawSketch,
319+
};
320+
this._drawToolConfig = Object.assign({
321+
onFinishStroke: (strokeSettings: CanvasDrawToolSettings) => { },
322+
uiSettings: this._drawToolUiSettings,
323+
}, this._toolConfig);
324+
325+
this.imageTool = new ImageCanvasTool(Object.assign({}, this._toolConfig, { redraw: this._redrawSketch }));
326+
this.lineDrawTool = new LineCanvasDrawTool(this._drawToolConfig);
327+
this.pencilDrawTool = new PencilCanvasDrawTool(this._drawToolConfig);
328+
this.panTool = new PanCanvasTool(Object.assign({}, this._toolConfig, { panTo: this._panTo }));
329+
330+
if (this._config.backgroundImage?.src != null && this._config.backgroundImageCanvas != null) {
331+
const toolConfig: ToolConfig = {
332+
canvas: this._config.backgroundImageCanvas,
333+
context: this._backgroundImageContext,
334+
redraw: this._redrawBackgroundImage,
335+
};
336+
this.backgroundImageTool = new ImageCanvasTool(toolConfig);
337+
338+
// default the dimension of the destination rectangle for the background image to fit center
339+
const defaultBackgroundImageSettings: ImageSettings = {
340+
destRecEndX: this._config.backgroundImageCanvas.width,
341+
destRecEndY: this._config.backgroundImageCanvas.height,
342+
destRecStartX: 0,
343+
destRecStartY: 0,
344+
src: '',
345+
};
346+
Object.assign(defaultBackgroundImageSettings, this._config.backgroundImage);
347+
348+
// draw the background image
349+
this.backgroundImageTool.drawImage(defaultBackgroundImageSettings);
350+
}
351+
}
352+
353+
private _panTo(panXDelta: number, panYDelta: number): void {
354+
const newPanX = this._config.panX + panXDelta;
355+
const newPanY = this._config.panY + panYDelta;
356+
this._config.panX = newPanX;
357+
this._config.panY = newPanY;
358+
359+
this._config.sketchCanvas.style.top = newPanY + "px";
360+
this._config.sketchCanvas.style.left = newPanX + "px";
361+
362+
if (this._config.backgroundImageCanvas != null) {
363+
this._config.backgroundImageCanvas.style.top = newPanY + "px";
364+
this._config.backgroundImageCanvas.style.left = newPanX + "px";
365+
}
366+
}
367+
368+
private _redrawSketch(): void {
369+
if (!this._isBrowserSupported) {
370+
return;
371+
}
372+
373+
this._clearSketchCanvas();
374+
375+
const stackToDraw = this._getStackToDraw();
376+
377+
this._drawObjects(stackToDraw);
378+
}
379+
380+
private _redrawBackgroundImage(): void {
381+
if (!this._isBrowserSupported) {
382+
return;
383+
}
384+
385+
this._clearBackgroundImageCanvas();
386+
387+
if (this.backgroundImageTool != null && this._config.backgroundImage != null) {
388+
this.backgroundImageTool.drawImage(this._config.backgroundImage);
389+
}
390+
}
391+
392+
// #endregion Public Methods
393+
}
394+
395+
export { CanvasSketch };

0 commit comments

Comments
 (0)