@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.
Files changed (41) hide show
  1. package/README.md +33 -0
  2. package/dist/CanvasConfig-HFgH7irb.d.mts +310 -0
  3. package/dist/CanvasConfig-HFgH7irb.d.ts +310 -0
  4. package/dist/application/index.d.mts +78 -0
  5. package/dist/application/index.d.ts +78 -0
  6. package/dist/application/index.js +1146 -0
  7. package/dist/application/index.mjs +11 -0
  8. package/dist/chunk-DNKRSL6D.mjs +358 -0
  9. package/dist/chunk-HJ24NSQA.mjs +777 -0
  10. package/dist/chunk-MC6PNV2F.mjs +34 -0
  11. package/dist/chunk-TZGTTYTZ.mjs +0 -0
  12. package/dist/chunk-UOGTEMIJ.mjs +712 -0
  13. package/dist/chunk-Z6IB4BQV.mjs +31 -0
  14. package/dist/domain/config/index.d.mts +87 -0
  15. package/dist/domain/config/index.d.ts +87 -0
  16. package/dist/domain/config/index.js +92 -0
  17. package/dist/domain/config/index.mjs +15 -0
  18. package/dist/domain/index.d.mts +45 -0
  19. package/dist/domain/index.d.ts +45 -0
  20. package/dist/domain/index.js +137 -0
  21. package/dist/domain/index.mjs +55 -0
  22. package/dist/index.d.mts +7 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +1927 -0
  25. package/dist/index.mjs +40 -0
  26. package/dist/infrastructure/index.d.mts +198 -0
  27. package/dist/infrastructure/index.d.ts +198 -0
  28. package/dist/infrastructure/index.js +765 -0
  29. package/dist/infrastructure/index.mjs +13 -0
  30. package/dist/presentation/index.d.mts +145 -0
  31. package/dist/presentation/index.d.ts +145 -0
  32. package/dist/presentation/index.js +1911 -0
  33. package/dist/presentation/index.mjs +16 -0
  34. package/dist/report.types-DWDnprBx.d.mts +76 -0
  35. package/dist/report.types-DWDnprBx.d.ts +76 -0
  36. package/package.json +8 -2
  37. package/src/domain/types/freehand.types.ts +42 -0
  38. package/src/domain/types/index.ts +1 -0
  39. package/src/index.ts +10 -0
  40. package/src/presentation/hooks/useFreehandCanvas.ts +244 -0
  41. 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.6",
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
+ }
@@ -6,3 +6,4 @@
6
6
  export * from './canvas.types';
7
7
  export * from './screenshot.types';
8
8
  export * from './report.types';
9
+ export * from './freehand.types';
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
+ }
@@ -6,3 +6,4 @@
6
6
  export * from './hooks/useScreenshotStudio';
7
7
  export * from './hooks/useCanvasExport';
8
8
  export * from './hooks/useReportExport';
9
+ export * from './hooks/useFreehandCanvas';