+
+
+
+
+
+ );
+}
+
+export { ReactCanvasSketch };
diff --git a/src/atoms/forms/canvas-sketch/tools/base-canvas-draw-tool.ts b/src/atoms/forms/canvas-sketch/tools/base-canvas-draw-tool.ts
new file mode 100644
index 0000000..e6a2f9d
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/base-canvas-draw-tool.ts
@@ -0,0 +1,75 @@
+import { CanvasToolSettings, ToolConfig } from "./base-canvas-tool";
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { PointerPosition } from "../interfaces/pointer-position";
+import { CoreUtils } from "../../../../utilities/core-utils";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+export interface CanvasDrawTool {
+ dispose: () => void;
+ drawStrokes: (strokes: CanvasDrawToolSettings[]) => void;
+ initialize: () => void;
+ toolType: CanvasToolType;
+}
+
+export interface CanvasDrawToolSettings extends CanvasToolSettings {
+ stroke: string; // color
+ strokeWidth: number;
+}
+
+export interface DrawToolUiSettings {
+ color: string;
+ width: number;
+}
+
+export interface DrawToolConfig extends ToolConfig {
+ onFinishStroke: (strokeSettings: CanvasDrawToolSettings) => void;
+ uiSettings: DrawToolUiSettings;
+}
+
+// #endregion Interfaces
+
+class BaseCanvasDrawTool {
+ protected _canvas: HTMLCanvasElement;
+ protected _color: string;
+ protected _config: DrawToolConfig;
+ protected _context: CanvasRenderingContext2D;
+ protected _currentPosition: PointerPosition;
+ protected _isPointerActive: boolean;
+ protected _previousPosition: PointerPosition;
+ protected _uiSettings: DrawToolUiSettings;
+ protected _width: number;
+
+ constructor(config: DrawToolConfig) {
+ this._uiSettings = config.uiSettings;
+
+ this._canvas = config.canvas;
+ this._color = "FFFFFF"; // default
+ this._config = config;
+ this._context = config.context;
+ this._currentPosition = { x: 0, y: 0 }; // default
+ this._previousPosition = { x: 0, y: 0 }; // default
+ this._width = 1; // default
+
+ this._isPointerActive = false;
+
+ CoreUtils.bindAll(this);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Protected Methods
+ // ---------------------------------------------------------------------------------------------
+
+ protected _onFinishStroke(strokeSettings: CanvasDrawToolSettings): void {
+ if (this._config.onFinishStroke) {
+ this._config.onFinishStroke(strokeSettings);
+ }
+ }
+
+ // #endregion Public Methods
+
+}
+
+export { BaseCanvasDrawTool };
diff --git a/src/atoms/forms/canvas-sketch/tools/base-canvas-tool.ts b/src/atoms/forms/canvas-sketch/tools/base-canvas-tool.ts
new file mode 100644
index 0000000..c3b8353
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/base-canvas-tool.ts
@@ -0,0 +1,41 @@
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { CanvasObjectType } from "../enums/canvas-object-type";
+import { CoreUtils } from "../../../../utilities/core-utils";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+export interface CanvasTool {
+ dispose: () => void;
+ initialize: () => void;
+ toolType: CanvasToolType;
+}
+
+export interface CanvasToolSettings {
+ type: CanvasObjectType;
+}
+
+export interface ToolConfig {
+ canvas: HTMLCanvasElement;
+ context: CanvasRenderingContext2D;
+ redraw: () => void;
+}
+
+// #endregion Interfaces
+
+class BaseCanvasTool {
+ protected _canvas: HTMLCanvasElement;
+ protected _config: ToolConfig;
+ protected _context: CanvasRenderingContext2D;
+
+ constructor(toolConfig: ToolConfig) {
+ this._canvas = toolConfig.canvas;
+ this._config = toolConfig;
+ this._context = toolConfig.context;
+
+ CoreUtils.bindAll(this);
+ }
+}
+
+export { BaseCanvasTool };
diff --git a/src/atoms/forms/canvas-sketch/tools/image-canvas-tool.ts b/src/atoms/forms/canvas-sketch/tools/image-canvas-tool.ts
new file mode 100644
index 0000000..959eb37
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/image-canvas-tool.ts
@@ -0,0 +1,170 @@
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { CoreUtils } from "../../../../utilities/core-utils";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+export interface ImageSettings {
+ destRecEndX?: number;
+ destRecEndY?: number;
+ destRecStartX?: number;
+ destRecStartY?: number;
+ src: string; // URL
+}
+
+export interface ImageConfig {
+ canvas: HTMLCanvasElement;
+ context: CanvasRenderingContext2D;
+}
+
+// #endregion Interfaces
+
+class ImageCanvasTool {
+ public toolType: CanvasToolType;
+
+ private _config: ImageConfig;
+ private _shouldCenterInDestRectangle: boolean;
+ private _shouldFitInCanvas: boolean;
+ private _shouldScaleToFitDestRectangle: boolean;
+
+
+ constructor(config: ImageConfig) {
+ this._config = config;
+ this._shouldCenterInDestRectangle = false;
+ this._shouldFitInCanvas = false;
+ this._shouldScaleToFitDestRectangle = false;
+
+ this.toolType = CanvasToolType.image;
+
+ CoreUtils.bindAll(this);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Public Methods
+ // ---------------------------------------------------------------------------------------------
+
+ public dispose(): void {
+ }
+
+ public drawImages(images: ImageSettings[]): void {
+ (images as ImageSettings[]).forEach((image: ImageSettings, imageI: number) => {
+ this.drawImage(image);
+ });
+ }
+
+ public drawImage(image: ImageSettings): void {
+ this._drawImage(
+ image.destRecEndX,
+ image.destRecEndY,
+ image.destRecStartX,
+ image.destRecStartY,
+ image.src);
+ }
+
+ public initialize(): void { }
+
+ public setShouldCenterInDestRectangle(shouldCenterInDestRectangle: boolean): void {
+ this._shouldCenterInDestRectangle = shouldCenterInDestRectangle;
+ }
+
+ public setShouldFitInCanvas(shouldFitInCanvas: boolean): void {
+ this._shouldFitInCanvas = shouldFitInCanvas;
+ }
+
+ public setShouldScaleToFitDestRectangle(shouldScaleToFitDestRectangle: boolean): void {
+ this._shouldScaleToFitDestRectangle = shouldScaleToFitDestRectangle;
+ }
+
+ // #endregion Public Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Private Methods
+ // ---------------------------------------------------------------------------------------------
+
+ /**
+ * Draws the image url to the canvas
+ *
+ * @param url
+ * @param destRecStartX
+ * @param destRecStartY
+ * @param destRecEndX
+ * @param destRecEndY
+ */
+ private _drawImage(
+ destRecEndX?: number,
+ destRecEndY?: number,
+ destRecStartX?: number,
+ destRecStartY?: number,
+ url?: string,
+ ): void {
+
+ const image = new Image();
+ image.onload = () => {
+
+ if (destRecStartX == null) {
+ destRecStartX = 0;
+ }
+ if (destRecStartY == null) {
+ destRecStartY = 0;
+ }
+ if (destRecEndX == null) {
+ destRecEndX = image.width;
+ }
+ if (destRecEndY == null) {
+ destRecEndY = image.height;
+ }
+
+ // get destination rectangle dimensions and aspect ratio
+ const destRectWidth = destRecEndX - destRecStartX;
+ const destRectHeight = destRecEndY - destRecStartY;
+
+ let newDestRecStartX: number = destRecStartX;
+ let newDestRecStartY: number = destRecStartY;
+
+ let newImageWidth = image.width;
+ let newImageHeight = image.height;
+
+ if (this._shouldFitInCanvas &&
+ (image.width > this._config.canvas.width || image.height > this._config.canvas.height)) {
+ // scale down the image dimensions to fit inside the canvas
+ const canvasAspectRatio = this._config.canvas.width / this._config.canvas.height;
+ newImageWidth = image.width / canvasAspectRatio;
+ newImageHeight = image.height / canvasAspectRatio;
+ }
+
+ if (this._shouldScaleToFitDestRectangle &&
+ (image.width > destRectWidth || image.height > destRectHeight)) {
+ // scale down the image dimension to fit the destination rectangle canvas space
+ const destRectAspectRatio = destRectWidth / destRectHeight;
+ newImageWidth = image.width / destRectAspectRatio;
+ newImageHeight = image.height / destRectAspectRatio;
+ }
+
+ if (this._shouldCenterInDestRectangle) {
+ // define the new rect space in order to center the image in the canvas
+ newDestRecStartX = destRecStartX + ((destRectWidth - newImageWidth) / 2);
+ newDestRecStartY = destRecStartY + ((destRectHeight - newImageHeight) / 2);
+ }
+
+ this._config.context.drawImage(image,
+ 0, // start of image clipping X
+ 0, // start of image clipping Y
+ image.width, // finish of image clipping X
+ image.height, // finish of image clipping Y
+ newDestRecStartX, // start of destination rectangle X
+ newDestRecStartY, // start of destination rectangle Y
+ newImageWidth, // new rectangle width
+ newImageHeight); // new rectangle height
+ };
+
+ if (url != null) {
+ image.src = url;
+ }
+ }
+
+ // #endregion Private Methods
+
+}
+
+export { ImageCanvasTool };
diff --git a/src/atoms/forms/canvas-sketch/tools/line-canvas-draw-tool.ts b/src/atoms/forms/canvas-sketch/tools/line-canvas-draw-tool.ts
new file mode 100644
index 0000000..4987f6d
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/line-canvas-draw-tool.ts
@@ -0,0 +1,259 @@
+import { CanvasDrawToolSettings, CanvasDrawTool, DrawToolConfig, BaseCanvasDrawTool } from "./base-canvas-draw-tool";
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { CoreUtils } from "../../../../utilities/core-utils";
+import { PointerPosition } from "../interfaces/pointer-position";
+import { CanvasObjectType } from "../enums/canvas-object-type";
+import { PositionUtils } from "../utils/position-utils";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+export interface LineStrokeSettings extends CanvasDrawToolSettings {
+ endX: number;
+ endY: number;
+ startX: number;
+ startY: number;
+}
+
+// #endregion Interfaces
+
+class LineCanvasDrawTool extends BaseCanvasDrawTool implements CanvasDrawTool {
+ public toolType: CanvasToolType;
+
+ constructor(drawToolConfig: DrawToolConfig) {
+ super(drawToolConfig);
+
+ this.toolType = CanvasToolType.line;
+
+ CoreUtils.bindAll(this);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Public Methods
+ // ---------------------------------------------------------------------------------------------
+
+ public dispose(): void {
+ this._removeEventListeners();
+ }
+
+ public drawStrokes(strokes: CanvasDrawToolSettings[]): void {
+ (strokes as LineStrokeSettings[]).forEach((stroke: LineStrokeSettings) => {
+ const startX: number = stroke.startX;
+ const endX: number = stroke.endX
+ const startY: number = stroke.startY;
+ const endY: number = stroke.endY;
+
+ const color = stroke.stroke;
+ const width = stroke.strokeWidth;
+
+ this._drawStroke(startX, startY, endX, endY, color, width);
+ });
+ }
+
+ public initialize(): void {
+ this._addEventListeners();
+ }
+
+ // #endregion Private Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Private Methods
+ // ---------------------------------------------------------------------------------------------
+
+ /**
+ * Binds the necessary mouse and touch events
+ */
+ private _addEventListeners(): void {
+ this._canvas.addEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._canvas.addEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.addEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._canvas.addEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._canvas.addEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.addEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Draws the current state of the interaction
+ */
+ private _drawInteraction(): void {
+ this._drawStroke(
+ this._previousPosition.x,
+ this._previousPosition.y,
+ this._currentPosition.x,
+ this._currentPosition.y,
+ this._uiSettings.color,
+ this._uiSettings.width);
+ }
+
+ /**
+ * Draws the interaction based on the provided state
+ *
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ * @param color
+ * @param width
+ */
+ private _drawStroke(
+ startX: number,
+ startY: number,
+ endX: number,
+ endY: number,
+ color: string,
+ width: number): void {
+ this._context.beginPath();
+
+ // Draw a line between two points
+ this._context.strokeStyle = color;
+ this._context.moveTo(startX, startY);
+ this._context.lineCap = "round";
+ this._context.lineWidth = width;
+ this._context.lineTo(endX, endY);
+
+ if (endX !== startX || endY !== startY) {
+ // only draw the line if the mouse actually moved
+ this._context.stroke();
+ }
+
+ this._context.closePath();
+ }
+
+ /**
+ * Finalizes the entire stroke interaction
+ */
+ private _finishStroke(): void {
+ if (!this._isPointerActive) {
+ // currently not active... bail
+ return;
+ }
+
+ this._isPointerActive = false;
+
+ const strokeSettings = this._getStrokeSettings();
+
+ this._onFinishStroke(strokeSettings);
+ }
+
+ /**
+ * Returns the stroke settings for the entire interaction including the stroke, color, and width
+ */
+ private _getStrokeSettings(): LineStrokeSettings {
+ // Put together tool stroke here
+ return {
+ endX: this._currentPosition.x,
+ endY: this._currentPosition.y,
+ startX: this._previousPosition.x,
+ startY: this._previousPosition.y,
+ stroke: this._uiSettings.color,
+ strokeWidth: this._uiSettings.width,
+ type: CanvasObjectType.line,
+ };
+ }
+
+ /**
+ * Handles the move interaction while drawing
+ *
+ * @param newCurrentPosition
+ */
+ private _move(newCurrentPosition: PointerPosition): void {
+ if (newCurrentPosition == null) {
+ // null checking - being defensive
+ return;
+ }
+
+ // If the pointer is active... draw!
+ if (this._isPointerActive) {
+ // Undo the last interaction by redrawing the canvas strokes
+ this._config.redraw();
+
+ this._currentPosition = newCurrentPosition;
+ this._drawInteraction();
+ }
+ }
+
+ /**
+ * Removed the bound mouse and touch events
+ */
+ private _removeEventListeners(): void {
+ this._canvas.removeEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._canvas.removeEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.removeEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._canvas.removeEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._canvas.removeEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.removeEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Captures the starting position and begins the entire stroke interaction
+ *
+ * @param startingPosition
+ */
+ private _startStroke(startingPosition: PointerPosition): void {
+ // Start the path of the stroke
+ this._previousPosition = startingPosition;
+ this._isPointerActive = true;
+ this._currentPosition = startingPosition;
+
+ // Draw!
+ this._drawInteraction();
+ }
+
+ // #endregion Private Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Event Handlers
+ // ---------------------------------------------------------------------------------------------
+
+ private _onMouseDownCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._startStroke(mousePosition);
+ }
+ }
+
+ private _onMouseMoveCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._move(mousePosition);
+ }
+ }
+
+ private _onMouseUpWindow(): void {
+ this._finishStroke();
+ }
+
+ private _onTouchEndWindow(e: TouchEvent): void {
+ this._finishStroke();
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchMoveCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._move(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchStartCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._startStroke(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ // #endregion Event Handlers
+}
+
+export { LineCanvasDrawTool };
diff --git a/src/atoms/forms/canvas-sketch/tools/pan-canvas-tool.ts b/src/atoms/forms/canvas-sketch/tools/pan-canvas-tool.ts
new file mode 100644
index 0000000..f7004e9
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/pan-canvas-tool.ts
@@ -0,0 +1,178 @@
+import { PointerPosition } from "../interfaces/pointer-position";
+import { CoreUtils } from "../../../../utilities/core-utils";
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { PositionUtils } from "../utils/position-utils";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+export interface PanConfig {
+ canvas: HTMLCanvasElement;
+ context: CanvasRenderingContext2D;
+ panTo: (moveX: number, moveY: number) => void;
+}
+
+// #endregion Interfaces
+
+class PanCanvasTool {
+ public toolType: CanvasToolType;
+
+ private _config: PanConfig;
+ private _isPointerActive: boolean;
+ private _lastPosition: PointerPosition;
+
+ constructor(panConfig: PanConfig) {
+ this._config = panConfig;
+ this._isPointerActive = false;
+ this._lastPosition = { x: 0, y: 0 };
+ this.toolType = CanvasToolType.pan;
+
+ CoreUtils.bindAll(this);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Public Methods
+ // ---------------------------------------------------------------------------------------------
+
+ public dispose(): void {
+ this._removeEventListeners();
+ this._removeCursor();
+ }
+
+ public initialize(): void {
+ this._addEventListeners();
+ this._addCursor();
+ }
+
+ // #endregion Public Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Private Methods
+ // ---------------------------------------------------------------------------------------------
+
+ private _addCursor(): void {
+ this._config.canvas.style.cursor = "move";
+ }
+
+ /**
+ * Binds the necessary mouse and touch events
+ */
+ private _addEventListeners(): void {
+ this._config.canvas.addEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._config.canvas.addEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.addEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._config.canvas.addEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._config.canvas.addEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.addEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Completes the panning interaction
+ */
+ private _finishPan(): void {
+ // Finish the path of the stroke
+ this._lastPosition = { x: 0, y: 0 };
+ this._isPointerActive = false;
+ }
+
+ /**
+ * Tracks the point of interaction and moves to it
+ *
+ * @param newCurrentPosition
+ */
+ private _pan(newCurrentPosition: PointerPosition): void {
+ // If the pointer is active... draw!
+ if (this._isPointerActive) {
+
+ const newPanX = newCurrentPosition.x - this._lastPosition.x;
+ const newPanY = newCurrentPosition.y - this._lastPosition.y;
+
+ this._config.panTo(newPanX, newPanY);
+ }
+ }
+
+ private _removeCursor(): void {
+ this._config.canvas.style.cursor = "default";
+ }
+
+ /**
+ * Removed the bound mouse and touch events
+ */
+ private _removeEventListeners(): void {
+ this._config.canvas.removeEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._config.canvas.removeEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.removeEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._config.canvas.removeEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._config.canvas.removeEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.removeEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Captures the starting position and begins the entire interaction
+ *
+ * @param startingPosition
+ */
+ private _startPan(startingPosition: PointerPosition): void {
+ // Start the path of the stroke
+ this._lastPosition = startingPosition;
+ this._isPointerActive = true;
+ }
+
+ // #endregion Private Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Event Handlers
+ // ---------------------------------------------------------------------------------------------
+
+ private _onMouseDownCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._startPan(mousePosition);
+ }
+ }
+
+ private _onMouseMoveCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._pan(mousePosition);
+ }
+ }
+
+ private _onMouseUpWindow(): void {
+ this._finishPan();
+ }
+
+ private _onTouchEndWindow(e: TouchEvent): void {
+ this._finishPan();
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchMoveCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._pan(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchStartCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._startPan(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ // #endregion Event Handlers
+}
+
+export { PanCanvasTool };
diff --git a/src/atoms/forms/canvas-sketch/tools/pencil-canvas-draw-tool.ts b/src/atoms/forms/canvas-sketch/tools/pencil-canvas-draw-tool.ts
new file mode 100644
index 0000000..f06ef85
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/tools/pencil-canvas-draw-tool.ts
@@ -0,0 +1,313 @@
+import { CanvasDrawToolSettings, BaseCanvasDrawTool, CanvasDrawTool, DrawToolConfig } from "./base-canvas-draw-tool";
+import { CanvasToolType } from "../enums/canvas-tool-type";
+import { PointerPosition } from "../interfaces/pointer-position";
+import { CoreUtils } from "../../../../utilities/core-utils";
+import { PositionUtils } from "../utils/position-utils";
+import { CanvasObjectType } from "../enums/canvas-object-type";
+
+// -------------------------------------------------------------------------------------------------
+// #region Interfaces
+// -------------------------------------------------------------------------------------------------
+
+enum PathType {
+ Finishing = "F",
+ Moving = "M",
+ Starting = "S",
+}
+
+interface PencilStrokeSettings extends CanvasDrawToolSettings {
+ path: [PathType, number, number][];
+}
+
+// #endregion Interfaces
+
+class PencilCanvasDrawTool extends BaseCanvasDrawTool implements CanvasDrawTool {
+ public toolType: CanvasToolType;
+
+
+ protected _path: PointerPosition[];
+
+ constructor(drawToolConfig: DrawToolConfig) {
+ super(drawToolConfig);
+
+ this._path = [];
+
+ this.toolType = CanvasToolType.pencil;
+
+ CoreUtils.bindAll(this);
+ }
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Public Methods
+ // ---------------------------------------------------------------------------------------------
+
+ public dispose(): void {
+ this._removeEventListeners();
+ this._removeCursor();
+ }
+
+ public drawStrokes(strokes: CanvasDrawToolSettings[]): void {
+ (strokes as PencilStrokeSettings[]).forEach((stroke: PencilStrokeSettings, strokeI: number) => {
+ let lastX: number = 0;
+ let lastY: number = 0;
+ stroke.path.forEach((path: [PathType, number, number], pathI: number) => {
+ const type = path[0];
+ const color = stroke.stroke;
+ const width = stroke.strokeWidth;
+ if (type === PathType.Starting) {
+ // started stroke
+ this._drawStroke(path[1], path[2], path[1], path[2], color, width);
+ lastX = path[1];
+ lastY = path[2];
+ }
+ if (type === PathType.Moving) {
+ // moving
+ this._drawStroke(lastX, lastY, path[1], path[2], color, width);
+ lastX = path[1];
+ lastY = path[2];
+ }
+ if (type === PathType.Finishing) {
+ // ended stroke
+ this._drawStroke(lastX, lastY, path[1], path[2], color, width);
+ lastX = path[1];
+ lastY = path[2];
+ }
+ });
+ });
+ }
+
+ public initialize(): void {
+ this._addEventListeners();
+ this._addCursor();
+ }
+
+ // #endregion Private Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Private Methods
+ // ---------------------------------------------------------------------------------------------
+
+ private _addCursor(): void {
+ this._canvas.style.cursor = "crosshair";
+ }
+
+ /**
+ * Binds the necessary mouse and touch events
+ */
+ private _addEventListeners(): void {
+ this._canvas.addEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._canvas.addEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.addEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._canvas.addEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._canvas.addEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.addEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Draws the current state of the interaction
+ */
+ private _drawInteraction(): void {
+ this._drawStroke(
+ this._previousPosition.x,
+ this._previousPosition.y,
+ this._currentPosition.x,
+ this._currentPosition.y,
+ this._uiSettings.color,
+ this._uiSettings.width);
+ }
+
+ /**
+ * Draws the interaction based on the provided state
+ *
+ * @param startX
+ * @param startY
+ * @param endX
+ * @param endY
+ * @param color
+ * @param width
+ */
+ private _drawStroke(
+ startX: number,
+ startY: number,
+ endX: number,
+ endY: number,
+ color: string,
+ width: number): void {
+ this._context.beginPath();
+
+ // Draw a line between two points
+ this._context.strokeStyle = color;
+ this._context.moveTo(startX, startY);
+ this._context.lineCap = "round";
+ this._context.lineWidth = width;
+ this._context.lineTo(endX, endY);
+ this._context.stroke();
+
+ this._context.closePath();
+ }
+
+ /**
+ * Finalizes the entire stroke interaction
+ */
+ private _finishStroke(): void {
+ if (!this._isPointerActive) {
+ // currently not active... bail
+ return;
+ }
+
+ this._isPointerActive = false;
+
+ const strokeSettings = this._getStrokeSettings();
+
+ // Clear the current path
+ this._path = [];
+
+ this._onFinishStroke(strokeSettings);
+ }
+
+ /**
+ * Returns the path of the entire stroke that can then be persisted for later use
+ */
+ private _getPath(): [PathType, number, number][] {
+ const reformattedPath: [PathType, number, number][] = [];
+ this._path.forEach((value: PointerPosition, index: number) => {
+ if (index === 0) {
+ // starting point
+ reformattedPath.push([PathType.Starting, value.x, value.y]);
+ }
+ else if (index + 1 === this._path.length) {
+ // ending point
+ reformattedPath.push([PathType.Finishing, value.x, value.y]);
+ }
+ else {
+ // moving point
+ reformattedPath.push([PathType.Moving, value.x, value.y]);
+ }
+ });
+
+ return reformattedPath;
+ }
+
+ /**
+ * Returns the stroke settings for the entire interaction including the stroke, color, and width
+ */
+ private _getStrokeSettings(): PencilStrokeSettings {
+ // Put together tool stroke here
+ return {
+ path: this._getPath(),
+ stroke: this._uiSettings.color,
+ strokeWidth: this._uiSettings.width,
+ type: CanvasObjectType.path,
+ };
+ }
+
+ /**
+ * Handles the move interaction while drawing
+ *
+ * @param newCurrentPosition
+ */
+ private _move(newCurrentPosition: PointerPosition): void {
+ // If the pointer is active... draw!
+ if (this._isPointerActive) {
+ // Update the mouse coordinates when moved
+ this._previousPosition = this._currentPosition;
+ this._path.push(newCurrentPosition);
+ this._currentPosition = newCurrentPosition;
+ this._drawInteraction();
+ }
+ }
+
+ private _removeCursor(): void {
+ this._canvas.style.cursor = "default";
+ }
+
+ /**
+ * Removed the bound mouse and touch events
+ */
+ private _removeEventListeners(): void {
+ this._canvas.removeEventListener("mousedown", this._onMouseDownCanvas, false);
+ this._canvas.removeEventListener("mousemove", this._onMouseMoveCanvas, false);
+ window.removeEventListener("mouseup", this._onMouseUpWindow, false);
+
+ this._canvas.removeEventListener("touchstart", this._onTouchStartCanvas, false);
+ this._canvas.removeEventListener("touchmove", this._onTouchMoveCanvas, false);
+ window.removeEventListener("touchend", this._onTouchEndWindow, false);
+ }
+
+ /**
+ * Captures the starting position and begins the entire stroke interaction
+ *
+ * @param startingPosition
+ */
+ private _startStroke(startingPosition: PointerPosition): void {
+ if (startingPosition == null) {
+ // null checking - being defensive
+ return;
+ }
+
+ // Start the path of the stroke
+ this._path.push(startingPosition);
+ this._previousPosition = startingPosition;
+ this._isPointerActive = true;
+ this._currentPosition = startingPosition;
+
+ // Draw!
+ this._drawInteraction();
+ }
+
+ // #endregion Private Methods
+
+ // ---------------------------------------------------------------------------------------------
+ // #region Event Handlers
+ // ---------------------------------------------------------------------------------------------
+
+ private _onMouseDownCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._startStroke(mousePosition);
+ }
+ }
+
+ private _onMouseMoveCanvas(e: MouseEvent): void {
+ const mousePosition = PositionUtils.getMousePosition(e);
+ if (mousePosition != null) {
+ this._move(mousePosition);
+ }
+ }
+
+ private _onMouseUpWindow(): void {
+ this._finishStroke();
+ }
+
+ private _onTouchEndWindow(e: TouchEvent): void {
+ this._finishStroke();
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchMoveCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._move(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ private _onTouchStartCanvas(e: TouchEvent): void {
+ const touchPosition = PositionUtils.getTouchPosition(e, this._config.canvas);
+ if (touchPosition != null) {
+ this._startStroke(touchPosition);
+ }
+
+ // Don't allow touch events to be called
+ e.preventDefault();
+ }
+
+ // #endregion Event Handlers
+}
+
+export { PencilCanvasDrawTool };
diff --git a/src/atoms/forms/canvas-sketch/utils/position-utils.ts b/src/atoms/forms/canvas-sketch/utils/position-utils.ts
new file mode 100644
index 0000000..700a70c
--- /dev/null
+++ b/src/atoms/forms/canvas-sketch/utils/position-utils.ts
@@ -0,0 +1,56 @@
+import { PointerPosition } from "../interfaces/pointer-position";
+
+/**
+ * Get the current mouse position relative to the top-left of the canvas
+ *
+ * @param e The mouse event
+ */
+const getMousePosition = (e: MouseEvent): PointerPosition | null => {
+ if (!e) {
+ // is this necessary?
+ // e = event as MouseEvent;
+ }
+
+ if (e.offsetX) {
+ return {
+ x: e.offsetX,
+ y: e.offsetY,
+ };
+ }
+ else if ((e as any).layerX) { // fallback if mousing outside canvas
+ return {
+ x: (e as any).layerX,
+ y: (e as any).layerY,
+ };
+ }
+
+ return null;
+};
+
+/**
+ * Gets the current touch position relative to the top-left of the canvas
+ *
+ * @param e The touch event
+ */
+const getTouchPosition = (e: TouchEvent, canvas: HTMLCanvasElement): PointerPosition | null => {
+ if (!e) {
+ // is this necessary?
+ // e = event as TouchEvent;
+ }
+
+ if (e.touches) {
+
+ const viewportOffset = canvas.getBoundingClientRect();
+ return {
+ x: (e.touches[0].clientX - viewportOffset.left),
+ y: (e.touches[0].clientY - viewportOffset.top),
+ };
+ }
+
+ return null;
+};
+
+export const PositionUtils = {
+ getMousePosition,
+ getTouchPosition,
+};
diff --git a/src/index.ts b/src/index.ts
index f50f99a..53112ae 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,7 @@ export { Image } from "./atoms/images/image";
export { ProgressBar } from "./atoms/progress-bar/progress-bar";
// Forms
+export { ReactCanvasSketch } from "./atoms/forms/canvas-sketch/react-canvas-sketch";
export { CheckboxButton } from "./atoms/forms/checkbox-button";
export { CheckboxInput } from "./atoms/forms/checkbox-input";
export { InputCharacterCount } from "./atoms/forms/input-character-count";
diff --git a/src/utilities/core-utils.ts b/src/utilities/core-utils.ts
new file mode 100644
index 0000000..e65fa0a
--- /dev/null
+++ b/src/utilities/core-utils.ts
@@ -0,0 +1,33 @@
+// -----------------------------------------------------------------------------------------
+// #region Functions
+// -----------------------------------------------------------------------------------------
+
+/**
+ * Automatically binds all of an object's functions to itself
+ * @param obj Object for which to bind
+ */
+const _bindAll = (obj: any) => {
+ for (const key of Object.getOwnPropertyNames(obj.constructor.prototype)) {
+ const val = obj[key];
+
+ if (key !== "constructor" && typeof val === "function") {
+ obj[key] = val.bind(obj);
+ }
+ }
+
+ return obj;
+};
+
+// #endregion Functions
+
+// -----------------------------------------------------------------------------------------
+// #region Exports
+// -----------------------------------------------------------------------------------------
+
+const CoreUtils = {
+ bindAll: _bindAll,
+};
+
+export { CoreUtils };
+
+// #endregion Exports
diff --git a/test.txt b/test.txt
new file mode 100644
index 0000000..e69de29