@umituz/web-canvas 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/CanvasConfig-HFgH7irb.d.mts +310 -0
- package/dist/CanvasConfig-HFgH7irb.d.ts +310 -0
- package/dist/application/index.d.mts +78 -0
- package/dist/application/index.d.ts +78 -0
- package/dist/application/index.js +1146 -0
- package/dist/application/index.mjs +11 -0
- package/dist/chunk-DNKRSL6D.mjs +358 -0
- package/dist/chunk-HJ24NSQA.mjs +777 -0
- package/dist/chunk-MC6PNV2F.mjs +34 -0
- package/dist/chunk-TZGTTYTZ.mjs +0 -0
- package/dist/chunk-UOGTEMIJ.mjs +712 -0
- package/dist/chunk-Z6IB4BQV.mjs +31 -0
- package/dist/domain/config/index.d.mts +87 -0
- package/dist/domain/config/index.d.ts +87 -0
- package/dist/domain/config/index.js +92 -0
- package/dist/domain/config/index.mjs +15 -0
- package/dist/domain/index.d.mts +45 -0
- package/dist/domain/index.d.ts +45 -0
- package/dist/domain/index.js +137 -0
- package/dist/domain/index.mjs +55 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1927 -0
- package/dist/index.mjs +40 -0
- package/dist/infrastructure/index.d.mts +198 -0
- package/dist/infrastructure/index.d.ts +198 -0
- package/dist/infrastructure/index.js +765 -0
- package/dist/infrastructure/index.mjs +13 -0
- package/dist/presentation/index.d.mts +145 -0
- package/dist/presentation/index.d.ts +145 -0
- package/dist/presentation/index.js +1911 -0
- package/dist/presentation/index.mjs +16 -0
- package/dist/report.types-DWDnprBx.d.mts +76 -0
- package/dist/report.types-DWDnprBx.d.ts +76 -0
- package/package.json +8 -2
- package/src/domain/types/freehand.types.ts +42 -0
- package/src/domain/types/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/presentation/hooks/useFreehandCanvas.ts +244 -0
- package/src/presentation/index.ts +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCanvasExport,
|
|
3
|
+
useFreehandCanvas,
|
|
4
|
+
useReportExport,
|
|
5
|
+
useScreenshotStudio
|
|
6
|
+
} from "../chunk-HJ24NSQA.mjs";
|
|
7
|
+
import "../chunk-DNKRSL6D.mjs";
|
|
8
|
+
import "../chunk-UOGTEMIJ.mjs";
|
|
9
|
+
import "../chunk-Z6IB4BQV.mjs";
|
|
10
|
+
import "../chunk-MC6PNV2F.mjs";
|
|
11
|
+
export {
|
|
12
|
+
useCanvasExport,
|
|
13
|
+
useFreehandCanvas,
|
|
14
|
+
useReportExport,
|
|
15
|
+
useScreenshotStudio
|
|
16
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Types
|
|
3
|
+
* PDF export and report generation types
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Report item type
|
|
7
|
+
*/
|
|
8
|
+
type ReportItemType = 'app' | 'campaign' | 'post';
|
|
9
|
+
/**
|
|
10
|
+
* Report item
|
|
11
|
+
*/
|
|
12
|
+
interface ReportItem {
|
|
13
|
+
name: string;
|
|
14
|
+
type: ReportItemType;
|
|
15
|
+
metrics: Record<string, number | string>;
|
|
16
|
+
details: Record<string, string | number | boolean | null | undefined>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Report summary
|
|
20
|
+
*/
|
|
21
|
+
interface ReportSummary {
|
|
22
|
+
totalReach: number;
|
|
23
|
+
totalEngagement: number;
|
|
24
|
+
averageScore: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Report data
|
|
28
|
+
*/
|
|
29
|
+
interface ReportData {
|
|
30
|
+
title: string;
|
|
31
|
+
dateRange: string;
|
|
32
|
+
items: ReportItem[];
|
|
33
|
+
summary: ReportSummary;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Export format
|
|
37
|
+
*/
|
|
38
|
+
type ExportFormat = 'pdf' | 'excel' | 'csv';
|
|
39
|
+
/**
|
|
40
|
+
* PDF export options
|
|
41
|
+
*/
|
|
42
|
+
interface PDFExportOptions {
|
|
43
|
+
filename?: string;
|
|
44
|
+
scale?: number;
|
|
45
|
+
useCORS?: boolean;
|
|
46
|
+
backgroundColor?: string;
|
|
47
|
+
format?: 'a4' | 'letter' | 'legal';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Excel export options
|
|
51
|
+
*/
|
|
52
|
+
interface ExcelExportOptions {
|
|
53
|
+
filename?: string;
|
|
54
|
+
sheetName?: string;
|
|
55
|
+
includeSummary?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* CSV export options
|
|
59
|
+
*/
|
|
60
|
+
interface CSVExportOptions {
|
|
61
|
+
filename?: string;
|
|
62
|
+
delimiter?: ',' | ';';
|
|
63
|
+
includeHeaders?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Report export result
|
|
67
|
+
*/
|
|
68
|
+
interface ReportExportResult {
|
|
69
|
+
format: ExportFormat;
|
|
70
|
+
filename: string;
|
|
71
|
+
size: number;
|
|
72
|
+
url: string;
|
|
73
|
+
timestamp: Date;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type { CSVExportOptions as C, ExcelExportOptions as E, PDFExportOptions as P, ReportData as R, ExportFormat as a, ReportExportResult as b, ReportItem as c, ReportItemType as d, ReportSummary as e };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Types
|
|
3
|
+
* PDF export and report generation types
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Report item type
|
|
7
|
+
*/
|
|
8
|
+
type ReportItemType = 'app' | 'campaign' | 'post';
|
|
9
|
+
/**
|
|
10
|
+
* Report item
|
|
11
|
+
*/
|
|
12
|
+
interface ReportItem {
|
|
13
|
+
name: string;
|
|
14
|
+
type: ReportItemType;
|
|
15
|
+
metrics: Record<string, number | string>;
|
|
16
|
+
details: Record<string, string | number | boolean | null | undefined>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Report summary
|
|
20
|
+
*/
|
|
21
|
+
interface ReportSummary {
|
|
22
|
+
totalReach: number;
|
|
23
|
+
totalEngagement: number;
|
|
24
|
+
averageScore: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Report data
|
|
28
|
+
*/
|
|
29
|
+
interface ReportData {
|
|
30
|
+
title: string;
|
|
31
|
+
dateRange: string;
|
|
32
|
+
items: ReportItem[];
|
|
33
|
+
summary: ReportSummary;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Export format
|
|
37
|
+
*/
|
|
38
|
+
type ExportFormat = 'pdf' | 'excel' | 'csv';
|
|
39
|
+
/**
|
|
40
|
+
* PDF export options
|
|
41
|
+
*/
|
|
42
|
+
interface PDFExportOptions {
|
|
43
|
+
filename?: string;
|
|
44
|
+
scale?: number;
|
|
45
|
+
useCORS?: boolean;
|
|
46
|
+
backgroundColor?: string;
|
|
47
|
+
format?: 'a4' | 'letter' | 'legal';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Excel export options
|
|
51
|
+
*/
|
|
52
|
+
interface ExcelExportOptions {
|
|
53
|
+
filename?: string;
|
|
54
|
+
sheetName?: string;
|
|
55
|
+
includeSummary?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* CSV export options
|
|
59
|
+
*/
|
|
60
|
+
interface CSVExportOptions {
|
|
61
|
+
filename?: string;
|
|
62
|
+
delimiter?: ',' | ';';
|
|
63
|
+
includeHeaders?: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Report export result
|
|
67
|
+
*/
|
|
68
|
+
interface ReportExportResult {
|
|
69
|
+
format: ExportFormat;
|
|
70
|
+
filename: string;
|
|
71
|
+
size: number;
|
|
72
|
+
url: string;
|
|
73
|
+
timestamp: Date;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type { CSVExportOptions as C, ExcelExportOptions as E, PDFExportOptions as P, ReportData as R, ExportFormat as a, ReportExportResult as b, ReportItem as c, ReportItemType as d, ReportSummary as e };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-canvas",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Comprehensive canvas and screenshot generation library with device frames, patterns, and PDF export",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -65,7 +65,13 @@
|
|
|
65
65
|
"android-screenshot",
|
|
66
66
|
"typescript",
|
|
67
67
|
"react",
|
|
68
|
-
"screenshot-studio"
|
|
68
|
+
"screenshot-studio",
|
|
69
|
+
"freehand",
|
|
70
|
+
"drawing",
|
|
71
|
+
"handwriting",
|
|
72
|
+
"signature",
|
|
73
|
+
"stylus",
|
|
74
|
+
"apple-pencil"
|
|
69
75
|
],
|
|
70
76
|
"author": "umituz",
|
|
71
77
|
"license": "MIT",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Freehand Types
|
|
3
|
+
* Types for pointer-based freehand drawing on a canvas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single sampled point along a stroke, in CSS pixels relative to the canvas
|
|
8
|
+
*/
|
|
9
|
+
export interface FreehandPoint {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
/** Normalized stylus pressure (0–1). ~0.5 for mouse/touch when unsupported. */
|
|
13
|
+
pressure: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A completed (or in-progress) freehand stroke
|
|
18
|
+
*/
|
|
19
|
+
export interface FreehandStroke {
|
|
20
|
+
/** Base brush size in CSS pixels. */
|
|
21
|
+
size: number;
|
|
22
|
+
/** CSS color of the stroke. */
|
|
23
|
+
color: string;
|
|
24
|
+
/** True when the stroke should vary width with per-point pressure. */
|
|
25
|
+
dynamic: boolean;
|
|
26
|
+
/** Ordered points from pointer-down to pointer-up. */
|
|
27
|
+
points: FreehandPoint[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for the useFreehandCanvas hook
|
|
32
|
+
*/
|
|
33
|
+
export interface UseFreehandCanvasOptions {
|
|
34
|
+
/** Base brush size in CSS pixels. Default: 3 */
|
|
35
|
+
brushSize?: number;
|
|
36
|
+
/** Stroke color. Default: '#1a1a1a' */
|
|
37
|
+
color?: string;
|
|
38
|
+
/** Vary stroke width with stylus pressure (mouse/touch stay constant). Default: true */
|
|
39
|
+
pressureEnabled?: boolean;
|
|
40
|
+
/** Called after each completed stroke (pointer-up). */
|
|
41
|
+
onStrokeEnd?: (stroke: FreehandStroke) => void;
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,11 @@ export type {
|
|
|
44
44
|
ExcelExportOptions,
|
|
45
45
|
CSVExportOptions,
|
|
46
46
|
ReportExportResult,
|
|
47
|
+
|
|
48
|
+
// Freehand types
|
|
49
|
+
FreehandPoint,
|
|
50
|
+
FreehandStroke,
|
|
51
|
+
UseFreehandCanvasOptions,
|
|
47
52
|
} from './domain/types';
|
|
48
53
|
|
|
49
54
|
export type {
|
|
@@ -100,3 +105,8 @@ export {
|
|
|
100
105
|
useReportExport,
|
|
101
106
|
type UseReportExportReturn,
|
|
102
107
|
} from './presentation/hooks/useReportExport';
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
useFreehandCanvas,
|
|
111
|
+
type UseFreehandCanvasReturn,
|
|
112
|
+
} from './presentation/hooks/useFreehandCanvas';
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFreehandCanvas Hook
|
|
3
|
+
* React hook for pointer-based freehand drawing on a canvas
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
import type {
|
|
8
|
+
FreehandPoint,
|
|
9
|
+
FreehandStroke,
|
|
10
|
+
UseFreehandCanvasOptions,
|
|
11
|
+
} from '../../domain/types/freehand.types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BRUSH_SIZE = 3;
|
|
14
|
+
const DEFAULT_COLOR = '#1a1a1a';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Width for a point: constant for mouse/touch, pressure-scaled for pens
|
|
18
|
+
*/
|
|
19
|
+
function widthFor(stroke: FreehandStroke, point: FreehandPoint): number {
|
|
20
|
+
if (!stroke.dynamic) return stroke.size;
|
|
21
|
+
// pressure is 0 (some mice) – 1 (hard press); clamp the visual range
|
|
22
|
+
return stroke.size * (0.5 + Math.min(1, Math.max(0, point.pressure)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return value of the useFreehandCanvas hook
|
|
27
|
+
*/
|
|
28
|
+
export interface UseFreehandCanvasReturn {
|
|
29
|
+
/** Ref to attach to the <canvas> element that captures drawing. */
|
|
30
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
31
|
+
/** Remove the most recent stroke. */
|
|
32
|
+
undo: () => void;
|
|
33
|
+
/** Remove all strokes. */
|
|
34
|
+
clear: () => void;
|
|
35
|
+
/** Number of committed strokes. */
|
|
36
|
+
strokeCount: number;
|
|
37
|
+
/** True when no strokes are committed. */
|
|
38
|
+
isEmpty: boolean;
|
|
39
|
+
/** Snapshot of committed strokes (safe to serialize). */
|
|
40
|
+
getStrokes: () => FreehandStroke[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* useFreehandCanvas hook
|
|
45
|
+
*
|
|
46
|
+
* Attaches pointer-event drawing to a canvas element. The canvas is sized to
|
|
47
|
+
* its CSS box at device-pixel resolution (and re-rendered on resize), strokes
|
|
48
|
+
* are replayed from captured points so undo/clear are lossless, and stylus
|
|
49
|
+
* pressure modulates line width when the pointer reports it.
|
|
50
|
+
*
|
|
51
|
+
* The canvas gets `touch-action: none` applied so drawing works on touch
|
|
52
|
+
* screens without scrolling the page. Consumers typically layer the canvas
|
|
53
|
+
* over their own guide content (e.g. a faint glyph to trace).
|
|
54
|
+
*/
|
|
55
|
+
export function useFreehandCanvas(
|
|
56
|
+
options: UseFreehandCanvasOptions = {}
|
|
57
|
+
): UseFreehandCanvasReturn {
|
|
58
|
+
const { brushSize = DEFAULT_BRUSH_SIZE, color = DEFAULT_COLOR, pressureEnabled = true } = options;
|
|
59
|
+
|
|
60
|
+
// Latest option values for event handlers, without re-binding listeners.
|
|
61
|
+
const optionsRef = useRef({ brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd });
|
|
62
|
+
optionsRef.current = { brushSize, color, pressureEnabled, onStrokeEnd: options.onStrokeEnd };
|
|
63
|
+
|
|
64
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
65
|
+
const strokesRef = useRef<FreehandStroke[]>([]);
|
|
66
|
+
const activeStrokeRef = useRef<FreehandStroke | null>(null);
|
|
67
|
+
const dprRef = useRef(1);
|
|
68
|
+
|
|
69
|
+
const [strokeCount, setStrokeCount] = useState(0);
|
|
70
|
+
|
|
71
|
+
/** Replay every committed stroke (and the active one) at current DPR. */
|
|
72
|
+
const redraw = useCallback(() => {
|
|
73
|
+
const canvas = canvasRef.current;
|
|
74
|
+
const ctx = canvas?.getContext('2d');
|
|
75
|
+
if (!canvas || !ctx) return;
|
|
76
|
+
const dpr = dprRef.current;
|
|
77
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
78
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
79
|
+
ctx.scale(dpr, dpr);
|
|
80
|
+
const all = activeStrokeRef.current
|
|
81
|
+
? [...strokesRef.current, activeStrokeRef.current]
|
|
82
|
+
: strokesRef.current;
|
|
83
|
+
ctx.lineCap = 'round';
|
|
84
|
+
ctx.lineJoin = 'round';
|
|
85
|
+
for (const stroke of all) {
|
|
86
|
+
ctx.strokeStyle = stroke.color;
|
|
87
|
+
ctx.fillStyle = stroke.color;
|
|
88
|
+
if (stroke.points.length === 1) {
|
|
89
|
+
const point = stroke.points[0];
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
|
|
92
|
+
ctx.fill();
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (let i = 1; i < stroke.points.length; i += 1) {
|
|
96
|
+
const from = stroke.points[i - 1];
|
|
97
|
+
const to = stroke.points[i];
|
|
98
|
+
ctx.beginPath();
|
|
99
|
+
ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
|
|
100
|
+
ctx.moveTo(from.x, from.y);
|
|
101
|
+
ctx.lineTo(to.x, to.y);
|
|
102
|
+
ctx.stroke();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
/** Match the backing store to the CSS box; returns true when it changed. */
|
|
108
|
+
const syncDimensions = useCallback((): boolean => {
|
|
109
|
+
const canvas = canvasRef.current;
|
|
110
|
+
if (!canvas) return false;
|
|
111
|
+
const dpr = window.devicePixelRatio || 1;
|
|
112
|
+
const width = Math.max(1, Math.round(canvas.clientWidth * dpr));
|
|
113
|
+
const height = Math.max(1, Math.round(canvas.clientHeight * dpr));
|
|
114
|
+
if (canvas.width === width && canvas.height === height && dprRef.current === dpr) return false;
|
|
115
|
+
canvas.width = width;
|
|
116
|
+
canvas.height = height;
|
|
117
|
+
dprRef.current = dpr;
|
|
118
|
+
return true;
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const canvas = canvasRef.current;
|
|
123
|
+
if (!canvas) return;
|
|
124
|
+
|
|
125
|
+
const el: HTMLCanvasElement = canvas;
|
|
126
|
+
|
|
127
|
+
// Touch screens must draw, not scroll, while the pointer is down.
|
|
128
|
+
el.style.touchAction = 'none';
|
|
129
|
+
|
|
130
|
+
syncDimensions();
|
|
131
|
+
redraw();
|
|
132
|
+
|
|
133
|
+
const observer = new ResizeObserver(() => {
|
|
134
|
+
if (syncDimensions()) redraw();
|
|
135
|
+
});
|
|
136
|
+
observer.observe(canvas);
|
|
137
|
+
|
|
138
|
+
function pointFrom(event: PointerEvent): FreehandPoint {
|
|
139
|
+
const rect = el.getBoundingClientRect();
|
|
140
|
+
return {
|
|
141
|
+
x: event.clientX - rect.left,
|
|
142
|
+
y: event.clientY - rect.top,
|
|
143
|
+
pressure: event.pressure > 0 ? event.pressure : 0.5,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function drawSegment(stroke: FreehandStroke, from: FreehandPoint, to: FreehandPoint): void {
|
|
148
|
+
const ctx = el.getContext('2d');
|
|
149
|
+
if (!ctx) return;
|
|
150
|
+
ctx.lineCap = 'round';
|
|
151
|
+
ctx.lineJoin = 'round';
|
|
152
|
+
ctx.strokeStyle = stroke.color;
|
|
153
|
+
ctx.lineWidth = (widthFor(stroke, from) + widthFor(stroke, to)) / 2;
|
|
154
|
+
ctx.beginPath();
|
|
155
|
+
ctx.moveTo(from.x, from.y);
|
|
156
|
+
ctx.lineTo(to.x, to.y);
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function drawDot(stroke: FreehandStroke, point: FreehandPoint): void {
|
|
161
|
+
const ctx = el.getContext('2d');
|
|
162
|
+
if (!ctx) return;
|
|
163
|
+
ctx.fillStyle = stroke.color;
|
|
164
|
+
ctx.beginPath();
|
|
165
|
+
ctx.arc(point.x, point.y, widthFor(stroke, point) / 2, 0, Math.PI * 2);
|
|
166
|
+
ctx.fill();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function onPointerDown(event: PointerEvent): void {
|
|
170
|
+
if (event.button !== 0 && event.pointerType === 'mouse') return;
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
el.setPointerCapture(event.pointerId);
|
|
173
|
+
const { brushSize: size, color: ink, pressureEnabled: dynamic } = optionsRef.current;
|
|
174
|
+
const stroke: FreehandStroke = {
|
|
175
|
+
size,
|
|
176
|
+
color: ink,
|
|
177
|
+
dynamic: dynamic && event.pointerType === 'pen',
|
|
178
|
+
points: [pointFrom(event)],
|
|
179
|
+
};
|
|
180
|
+
activeStrokeRef.current = stroke;
|
|
181
|
+
drawDot(stroke, stroke.points[0]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function onPointerMove(event: PointerEvent): void {
|
|
185
|
+
const stroke = activeStrokeRef.current;
|
|
186
|
+
if (!stroke) return;
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
const point = pointFrom(event);
|
|
189
|
+
const previous = stroke.points[stroke.points.length - 1];
|
|
190
|
+
stroke.points.push(point);
|
|
191
|
+
drawSegment(stroke, previous, point);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function commit(event: PointerEvent): void {
|
|
195
|
+
const stroke = activeStrokeRef.current;
|
|
196
|
+
if (!stroke) return;
|
|
197
|
+
activeStrokeRef.current = null;
|
|
198
|
+
if (el.hasPointerCapture(event.pointerId)) {
|
|
199
|
+
el.releasePointerCapture(event.pointerId);
|
|
200
|
+
}
|
|
201
|
+
strokesRef.current = [...strokesRef.current, stroke];
|
|
202
|
+
setStrokeCount(strokesRef.current.length);
|
|
203
|
+
optionsRef.current.onStrokeEnd?.(stroke);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
el.addEventListener('pointerdown', onPointerDown);
|
|
207
|
+
el.addEventListener('pointermove', onPointerMove);
|
|
208
|
+
el.addEventListener('pointerup', commit);
|
|
209
|
+
el.addEventListener('pointercancel', commit);
|
|
210
|
+
|
|
211
|
+
return () => {
|
|
212
|
+
observer.disconnect();
|
|
213
|
+
el.removeEventListener('pointerdown', onPointerDown);
|
|
214
|
+
el.removeEventListener('pointermove', onPointerMove);
|
|
215
|
+
el.removeEventListener('pointerup', commit);
|
|
216
|
+
el.removeEventListener('pointercancel', commit);
|
|
217
|
+
};
|
|
218
|
+
}, [redraw, syncDimensions]);
|
|
219
|
+
|
|
220
|
+
const undo = useCallback(() => {
|
|
221
|
+
if (strokesRef.current.length === 0) return;
|
|
222
|
+
strokesRef.current = strokesRef.current.slice(0, -1);
|
|
223
|
+
setStrokeCount(strokesRef.current.length);
|
|
224
|
+
redraw();
|
|
225
|
+
}, [redraw]);
|
|
226
|
+
|
|
227
|
+
const clear = useCallback(() => {
|
|
228
|
+
if (strokesRef.current.length === 0) return;
|
|
229
|
+
strokesRef.current = [];
|
|
230
|
+
setStrokeCount(0);
|
|
231
|
+
redraw();
|
|
232
|
+
}, [redraw]);
|
|
233
|
+
|
|
234
|
+
const getStrokes = useCallback(() => strokesRef.current.map((stroke) => ({ ...stroke, points: stroke.points.map((point) => ({ ...point })) })), []);
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
canvasRef,
|
|
238
|
+
undo,
|
|
239
|
+
clear,
|
|
240
|
+
strokeCount,
|
|
241
|
+
isEmpty: strokeCount === 0,
|
|
242
|
+
getStrokes,
|
|
243
|
+
};
|
|
244
|
+
}
|