hocmai-feedback-widget 0.2.1 → 0.2.3

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 (59) hide show
  1. package/README.md +78 -113
  2. package/dist/capture/device-detection.d.ts +13 -0
  3. package/dist/capture/fonts.d.ts +4 -0
  4. package/dist/capture/index.d.ts +14 -0
  5. package/dist/capture/mathjax-fonts.d.ts +5 -0
  6. package/dist/capture/types.d.ts +25 -0
  7. package/dist/ckeditor.mjs +113 -0
  8. package/dist/ckeditor.mjs.map +1 -0
  9. package/dist/components/CkEditor.d.ts +14 -0
  10. package/dist/components/FeedbackWidget.d.ts +13 -1
  11. package/dist/context/FeedbackContext.d.ts +14 -2
  12. package/dist/hocmai-feedback-widget.mjs +7550 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/package.json +39 -29
  16. package/src/App.tsx +147 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +361 -0
  20. package/src/capture/mathjax-fonts.ts +143 -0
  21. package/src/capture/types.ts +34 -0
  22. package/src/ckeditor.d.ts +14 -0
  23. package/src/components/CkEditor.tsx +130 -0
  24. package/src/components/CropOverlay.tsx +29 -47
  25. package/src/components/FeedbackWidget.tsx +105 -93
  26. package/src/context/FeedbackContext.tsx +49 -9
  27. package/src/index.ts +9 -3
  28. package/src/main.tsx +7 -9
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3946
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/test-mobile-screenshot.html +0 -211
  33. package/dist/test-simple-capture.html +0 -107
  34. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  35. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  36. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  37. package/dist/utils/screenshot/device-detection.d.ts +0 -16
  38. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  39. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  40. package/dist/utils/screenshot/performance.d.ts +0 -3
  41. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  42. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  43. package/dist/utils/screenshot/types.d.ts +0 -35
  44. package/dist/utils/screenshot.d.ts +0 -9
  45. package/dist/vite.svg +0 -1
  46. package/src/App.css +0 -42
  47. package/src/assets/react.svg +0 -1
  48. package/src/index.css +0 -68
  49. package/src/utils/screenshot/background-renderer.ts +0 -54
  50. package/src/utils/screenshot/canvas-renderer.ts +0 -343
  51. package/src/utils/screenshot/cleanup.ts +0 -32
  52. package/src/utils/screenshot/device-detection.ts +0 -90
  53. package/src/utils/screenshot/element-collector.ts +0 -134
  54. package/src/utils/screenshot/image-renderer.ts +0 -148
  55. package/src/utils/screenshot/performance.ts +0 -57
  56. package/src/utils/screenshot/svg-renderer.ts +0 -149
  57. package/src/utils/screenshot/text-renderer.ts +0 -212
  58. package/src/utils/screenshot/types.ts +0 -40
  59. package/src/utils/screenshot.ts +0 -83
@@ -1,3 +0,0 @@
1
- import { CleanupTracker } from './types';
2
- export declare function createCleanupTracker(): CleanupTracker;
3
- export declare function cleanup(tracker: CleanupTracker): void;
@@ -1,16 +0,0 @@
1
- import { DeviceCapabilities } from './types';
2
- /**
3
- * Detect if browser is Safari (including iOS Safari)
4
- */
5
- export declare function isSafari(): boolean;
6
- /**
7
- * Detect if browser is iOS Safari specifically
8
- * Must exclude Chrome/Chromium on iOS (Chrome DevTools emulation, Chrome iOS app)
9
- */
10
- export declare function isIOSSafari(): boolean;
11
- /**
12
- * Check if browser supports SVG foreignObject reliably
13
- * Safari iOS has known issues with foreignObject causing canvas tainting
14
- */
15
- export declare function supportsForeignObject(): boolean;
16
- export declare function getDeviceCapabilities(): DeviceCapabilities;
@@ -1,10 +0,0 @@
1
- import { Rect, CaptureBounds } from './types';
2
- export declare function calculateCaptureBounds(cropRect: Rect | undefined, buffer: number, viewportWidth?: number, viewportHeight?: number): CaptureBounds;
3
- export declare function isElementInViewport(element: HTMLElement, bounds: CaptureBounds): boolean;
4
- export declare function collectVisibleElements(bounds: CaptureBounds): Element[];
5
- export declare function estimateDOMComplexity(element: HTMLElement): {
6
- nodeCount: number;
7
- imageCount: number;
8
- svgCount: number;
9
- isComplex: boolean;
10
- };
@@ -1,21 +0,0 @@
1
- export interface ImageRenderResult {
2
- success: boolean;
3
- method?: 'canvas' | 'placeholder' | 'skip';
4
- error?: string;
5
- }
6
- /**
7
- * Main image rendering function with 3-tier strategy
8
- */
9
- export declare function renderImage(img: HTMLImageElement, ctx: CanvasRenderingContext2D): Promise<ImageRenderResult>;
10
- /**
11
- * Render all images in the document
12
- */
13
- export declare function renderImages(images: HTMLImageElement[], ctx: CanvasRenderingContext2D, onProgress?: (current: number, total: number) => void): Promise<void>;
14
- /**
15
- * Check if image is CORS-safe
16
- */
17
- export declare function isImageCORSSafe(img: HTMLImageElement): boolean;
18
- /**
19
- * Get all renderable images from elements
20
- */
21
- export declare function getImageElements(elements: Element[]): HTMLImageElement[];
@@ -1,3 +0,0 @@
1
- export declare function yieldToMainThread(): Promise<void>;
2
- export declare function processInChunks<T>(items: T[], chunkSize: number, processor: (chunk: T[]) => Promise<void>, onProgress?: (progress: number) => void): Promise<void>;
3
- export declare function loadImageWithTimeout(img: HTMLImageElement, url: string, timeout: number): Promise<void>;
@@ -1,24 +0,0 @@
1
- export interface SVGRenderResult {
2
- success: boolean;
3
- error?: string;
4
- }
5
- /**
6
- * Render SVG element to canvas
7
- */
8
- export declare function renderSVG(svg: SVGElement, ctx: CanvasRenderingContext2D): Promise<SVGRenderResult>;
9
- /**
10
- * Render all SVG elements
11
- */
12
- export declare function renderSVGs(svgs: SVGElement[], ctx: CanvasRenderingContext2D, onProgress?: (current: number, total: number) => void): Promise<void>;
13
- /**
14
- * Check if element is MathJax rendered
15
- */
16
- export declare function isMathJaxElement(element: Element): boolean;
17
- /**
18
- * Get all SVG elements from the document
19
- */
20
- export declare function getSVGElements(elements: Element[]): SVGElement[];
21
- /**
22
- * Get all MathJax elements from the document
23
- */
24
- export declare function getMathJaxElements(elements: Element[]): Element[];
@@ -1,23 +0,0 @@
1
- /**
2
- * Text Content Renderer - Layer 4
3
- * Renders HTML text elements to canvas using SVG foreignObject technique
4
- * This preserves exact browser rendering including fonts, wrapping, and nested styles
5
- */
6
- export interface TextRenderResult {
7
- success: boolean;
8
- error?: string;
9
- }
10
- /**
11
- * Render HTML element as image using SVG foreignObject
12
- * This technique captures the exact browser rendering of text with all styles
13
- */
14
- export declare function renderTextElement(element: HTMLElement, ctx: CanvasRenderingContext2D): Promise<TextRenderResult>;
15
- /**
16
- * Get text elements suitable for rendering
17
- * Selects leaf elements that contain actual text content
18
- */
19
- export declare function getTextElements(elements: Element[]): HTMLElement[];
20
- /**
21
- * Render all text elements to canvas
22
- */
23
- export declare function renderTextContent(elements: Element[], ctx: CanvasRenderingContext2D, onProgress?: (current: number, total: number) => void): Promise<void>;
@@ -1,35 +0,0 @@
1
- export interface Rect {
2
- x: number;
3
- y: number;
4
- width: number;
5
- height: number;
6
- }
7
- export interface DeviceCapabilities {
8
- maxCanvasSize: number;
9
- recommendedScale: number;
10
- recommendedQuality: number;
11
- chunkSize: number;
12
- yieldInterval: number;
13
- maxTimeout: number;
14
- }
15
- export interface CaptureOptions {
16
- quality: number;
17
- scale: number;
18
- includeMathJax: boolean;
19
- timeout: number;
20
- }
21
- export interface CaptureProgress {
22
- phase: 'analyzing' | 'rendering' | 'exporting';
23
- progress: number;
24
- message: string;
25
- }
26
- export interface CleanupTracker {
27
- objectUrls: string[];
28
- tempCanvases: HTMLCanvasElement[];
29
- }
30
- export interface CaptureBounds {
31
- left: number;
32
- top: number;
33
- right: number;
34
- bottom: number;
35
- }
@@ -1,9 +0,0 @@
1
- import { Rect, CaptureProgress } from './screenshot/types';
2
- export type { Rect };
3
- /**
4
- * Capture screenshot with 3-tier fallback strategy:
5
- * 1. Canvas-based capture (primary)
6
- * 2. modern-screenshot library (fallback)
7
- * 3. Return null (final fallback)
8
- */
9
- export declare const captureScreenshot: (cropRect?: Rect, onProgress?: (progress: CaptureProgress) => void) => Promise<string | null>;
package/dist/vite.svg DELETED
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/src/App.css DELETED
@@ -1,42 +0,0 @@
1
- #root {
2
- max-width: 1280px;
3
- margin: 0 auto;
4
- padding: 2rem;
5
- text-align: center;
6
- }
7
-
8
- .logo {
9
- height: 6em;
10
- padding: 1.5em;
11
- will-change: filter;
12
- transition: filter 300ms;
13
- }
14
- .logo:hover {
15
- filter: drop-shadow(0 0 2em #646cffaa);
16
- }
17
- .logo.react:hover {
18
- filter: drop-shadow(0 0 2em #61dafbaa);
19
- }
20
-
21
- @keyframes logo-spin {
22
- from {
23
- transform: rotate(0deg);
24
- }
25
- to {
26
- transform: rotate(360deg);
27
- }
28
- }
29
-
30
- @media (prefers-reduced-motion: no-preference) {
31
- a:nth-of-type(2) .logo {
32
- animation: logo-spin infinite 20s linear;
33
- }
34
- }
35
-
36
- .card {
37
- padding: 2em;
38
- }
39
-
40
- .read-the-docs {
41
- color: #888;
42
- }
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
package/src/index.css DELETED
@@ -1,68 +0,0 @@
1
- :root {
2
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
3
- line-height: 1.5;
4
- font-weight: 400;
5
-
6
-
7
- color: rgba(255, 255, 255, 0.87);
8
- background-color: #242424;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- a {
17
- font-weight: 500;
18
- color: #646cff;
19
- text-decoration: inherit;
20
- }
21
- a:hover {
22
- color: #535bf2;
23
- }
24
-
25
- body {
26
- margin: 0;
27
- display: flex;
28
- place-items: center;
29
- min-width: 320px;
30
- min-height: 100vh;
31
- }
32
-
33
- h1 {
34
- font-size: 3.2em;
35
- line-height: 1.1;
36
- }
37
-
38
- button {
39
- border-radius: 8px;
40
- border: 1px solid transparent;
41
- padding: 0.6em 1.2em;
42
- font-size: 1em;
43
- font-weight: 500;
44
- font-family: inherit;
45
- background-color: #1a1a1a;
46
- cursor: pointer;
47
- transition: border-color 0.25s;
48
- }
49
- button:hover {
50
- border-color: #646cff;
51
- }
52
- button:focus,
53
- button:focus-visible {
54
- outline: 4px auto -webkit-focus-ring-color;
55
- }
56
-
57
- @media (prefers-color-scheme: light) {
58
- :root {
59
- color: #213547;
60
- background-color: #ffffff;
61
- }
62
- a:hover {
63
- color: #747bff;
64
- }
65
- button {
66
- background-color: #f9f9f9;
67
- }
68
- }
@@ -1,54 +0,0 @@
1
- export function getComputedBackgroundColor(element: HTMLElement): string | null {
2
- const styles = window.getComputedStyle(element);
3
- const bgColor = styles.backgroundColor;
4
-
5
- // Check if transparent
6
- if (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') {
7
- return null;
8
- }
9
-
10
- return bgColor;
11
- }
12
-
13
- export async function renderBackground(
14
- element: HTMLElement,
15
- ctx: CanvasRenderingContext2D
16
- ): Promise<void> {
17
- const rect = element.getBoundingClientRect();
18
- const styles = window.getComputedStyle(element);
19
-
20
- // Use viewport coordinates directly from getBoundingClientRect
21
- const x = rect.left;
22
- const y = rect.top;
23
- const width = rect.width;
24
- const height = rect.height;
25
-
26
- // Render background color
27
- const bgColor = styles.backgroundColor;
28
- if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') {
29
- ctx.fillStyle = bgColor;
30
- ctx.fillRect(x, y, width, height);
31
- }
32
-
33
- // Render border
34
- const borderWidth = parseFloat(styles.borderWidth) || 0;
35
- if (borderWidth > 0) {
36
- const borderColor = styles.borderColor;
37
- if (borderColor && borderColor !== 'transparent') {
38
- ctx.strokeStyle = borderColor;
39
- ctx.lineWidth = borderWidth;
40
- ctx.strokeRect(x, y, width, height);
41
- }
42
- }
43
- }
44
-
45
- export async function renderBackgrounds(
46
- elements: Element[],
47
- ctx: CanvasRenderingContext2D
48
- ): Promise<void> {
49
- for (const element of elements) {
50
- if (element instanceof HTMLElement) {
51
- await renderBackground(element, ctx);
52
- }
53
- }
54
- }
@@ -1,343 +0,0 @@
1
- import type { Rect, CaptureOptions, CaptureProgress, CaptureBounds } from './types';
2
- import { getDeviceCapabilities } from './device-detection';
3
- import { createCleanupTracker, cleanup } from './cleanup';
4
- import {
5
- calculateCaptureBounds,
6
- collectVisibleElements,
7
- estimateDOMComplexity
8
- } from './element-collector';
9
- import { yieldToMainThread } from './performance';
10
- import { renderBackgrounds } from './background-renderer';
11
- import { getImageElements, renderImages } from './image-renderer';
12
- import { getSVGElements, renderSVGs, getMathJaxElements } from './svg-renderer';
13
- import { renderTextContent } from './text-renderer';
14
-
15
- /**
16
- * Main canvas-based capture with layered rendering
17
- */
18
- export async function captureWithCanvas(
19
- cropRect: Rect | undefined,
20
- options: Partial<CaptureOptions>,
21
- onProgress?: (progress: CaptureProgress) => void
22
- ): Promise<string | null> {
23
- const cleanupTracker = createCleanupTracker();
24
-
25
- try {
26
- // 1. Get device capabilities and adjust options
27
- const capabilities = getDeviceCapabilities();
28
- const quality = options.quality ?? capabilities.recommendedQuality;
29
- const scale = options.scale ?? capabilities.recommendedScale;
30
- const includeMathJax = options.includeMathJax ?? true;
31
-
32
- onProgress?.({
33
- phase: 'analyzing',
34
- progress: 10,
35
- message: 'Analyzing DOM structure...'
36
- });
37
-
38
- // 2. Analyze DOM complexity
39
- const element = document.documentElement;
40
- const complexity = estimateDOMComplexity(element);
41
- console.log('[Canvas] DOM complexity:', complexity);
42
-
43
- // Adjust quality based on complexity
44
- let finalQuality = quality;
45
- if (complexity.isComplex) {
46
- finalQuality = Math.max(0.6, quality - 0.1);
47
- console.log('[Canvas] Complex DOM detected, reducing quality to', finalQuality);
48
- }
49
-
50
- // 3. Calculate capture dimensions
51
- // Use Visual Viewport API on mobile for accurate dimensions
52
- // Visual viewport accounts for browser UI (URL bar, keyboard) and zoom level
53
- const getViewportWidth = () => {
54
- if (window.visualViewport) {
55
- return window.visualViewport.width;
56
- }
57
- return document.documentElement.clientWidth || window.innerWidth;
58
- };
59
-
60
- const getViewportHeight = () => {
61
- if (window.visualViewport) {
62
- return window.visualViewport.height;
63
- }
64
- return document.documentElement.clientHeight || window.innerHeight;
65
- };
66
-
67
- const targetX = cropRect ? cropRect.x : 0;
68
- const targetY = cropRect ? cropRect.y : 0;
69
- const targetWidth = cropRect ? cropRect.width : getViewportWidth();
70
- const targetHeight = cropRect ? cropRect.height : getViewportHeight();
71
-
72
- // 4. Check canvas size limits
73
- let finalScale = scale;
74
- if (
75
- targetWidth * finalScale > capabilities.maxCanvasSize ||
76
- targetHeight * finalScale > capabilities.maxCanvasSize
77
- ) {
78
- console.warn('[Canvas] Size exceeds device limits, reducing scale');
79
- finalScale = Math.min(
80
- capabilities.maxCanvasSize / targetWidth,
81
- capabilities.maxCanvasSize / targetHeight,
82
- finalScale
83
- );
84
- }
85
-
86
- // 5. Create canvas
87
- const canvas = document.createElement('canvas');
88
- canvas.width = targetWidth * finalScale;
89
- canvas.height = targetHeight * finalScale;
90
- cleanupTracker.tempCanvases.push(canvas);
91
-
92
- const ctx = canvas.getContext('2d', {
93
- alpha: false,
94
- willReadFrequently: false
95
- });
96
-
97
- if (!ctx) {
98
- throw new Error('Failed to get canvas context');
99
- }
100
-
101
- // Apply scale
102
- ctx.scale(finalScale, finalScale);
103
-
104
- // Fill with white background
105
- ctx.fillStyle = '#ffffff';
106
- ctx.fillRect(0, 0, targetWidth, targetHeight);
107
-
108
- onProgress?.({
109
- phase: 'analyzing',
110
- progress: 20,
111
- message: 'Collecting visible elements...'
112
- });
113
-
114
- // 6. Calculate capture bounds and collect elements
115
- // Pass viewport dimensions to ensure consistent bounds calculation
116
- const bounds: CaptureBounds = calculateCaptureBounds(
117
- cropRect,
118
- capabilities.maxCanvasSize > 8192 ? 500 : 300, // Buffer based on device
119
- targetWidth,
120
- targetHeight
121
- );
122
-
123
- const visibleElements = collectVisibleElements(bounds);
124
- console.log('[Canvas] Visible elements:', visibleElements.length);
125
-
126
- // 7. Separate elements by type
127
- const images = getImageElements(visibleElements);
128
- const svgs = getSVGElements(visibleElements);
129
- const mathJaxElements = includeMathJax ? getMathJaxElements(visibleElements) : [];
130
-
131
- console.log('[Canvas] Images:', images.length, 'SVGs:', svgs.length, 'MathJax:', mathJaxElements.length);
132
-
133
- onProgress?.({
134
- phase: 'rendering',
135
- progress: 30,
136
- message: 'Rendering backgrounds...'
137
- });
138
-
139
- // 8. Apply translation for crop offset
140
- // When cropRect is provided, translate canvas to show only the cropped area
141
- ctx.translate(-targetX, -targetY);
142
-
143
- // Helper function to test if canvas is tainted
144
- const isCanvasTainted = (): boolean => {
145
- try {
146
- ctx.getImageData(0, 0, 1, 1);
147
- return false;
148
- } catch {
149
- return true;
150
- }
151
- };
152
-
153
- // 9. Layer 1: Render backgrounds
154
- await renderBackgrounds(visibleElements, ctx);
155
- await yieldToMainThread();
156
-
157
- // Test canvas after backgrounds
158
- if (isCanvasTainted()) {
159
- console.warn('[Canvas] Canvas tainted after background rendering');
160
- return null;
161
- }
162
-
163
- onProgress?.({
164
- phase: 'rendering',
165
- progress: 50,
166
- message: `Rendering ${images.length} images...`
167
- });
168
-
169
- // 10. Layer 2: Render images
170
- await renderImages(images, ctx, (current, total) => {
171
- const progress = 50 + Math.round((current / total) * 30);
172
- onProgress?.({
173
- phase: 'rendering',
174
- progress,
175
- message: `Rendering images (${current}/${total})...`
176
- });
177
- });
178
- await yieldToMainThread();
179
-
180
- // Test canvas after images - if tainted, continue without SVG/text layers
181
- if (isCanvasTainted()) {
182
- console.warn('[Canvas] Canvas tainted after image rendering, skipping SVG and text layers');
183
-
184
- onProgress?.({
185
- phase: 'exporting',
186
- progress: 95,
187
- message: 'Exporting canvas (images tainted, skipping remaining layers)...'
188
- });
189
-
190
- try {
191
- const dataUrl = canvas.toDataURL('image/png', finalQuality);
192
- if (dataUrl.length < 5000) {
193
- console.warn('[Canvas] Canvas appears blank');
194
- return null;
195
- }
196
- console.log('[Canvas] Capture successful (backgrounds + images only, skipped SVG/text due to taint)');
197
- return dataUrl;
198
- } catch (error) {
199
- console.error('[Canvas] Export failed after image layer:', error);
200
- return null;
201
- }
202
- }
203
-
204
- onProgress?.({
205
- phase: 'rendering',
206
- progress: 80,
207
- message: `Rendering ${svgs.length} SVG elements...`
208
- });
209
-
210
- // 11. Layer 3: Render SVG elements (including MathJax)
211
- await renderSVGs(svgs, ctx, (current, total) => {
212
- const progress = 80 + Math.round((current / total) * 10);
213
- onProgress?.({
214
- phase: 'rendering',
215
- progress,
216
- message: `Rendering SVG (${current}/${total})...`
217
- });
218
- });
219
- await yieldToMainThread();
220
-
221
- // Test canvas after SVGs - if tainted, skip text layer
222
- if (isCanvasTainted()) {
223
- console.warn('[Canvas] Canvas tainted after SVG rendering, skipping text layer');
224
-
225
- onProgress?.({
226
- phase: 'exporting',
227
- progress: 95,
228
- message: 'Exporting canvas (SVG tainted, skipping text)...'
229
- });
230
-
231
- try {
232
- const dataUrl = canvas.toDataURL('image/png', finalQuality);
233
- if (dataUrl.length < 5000) {
234
- console.warn('[Canvas] Canvas appears blank');
235
- return null;
236
- }
237
- console.log('[Canvas] Capture successful (backgrounds + images + SVG, skipped text due to taint)');
238
- return dataUrl;
239
- } catch (error) {
240
- console.error('[Canvas] Export failed after SVG layer:', error);
241
- return null;
242
- }
243
- }
244
-
245
- // 12. Layer 4: Render text content
246
- onProgress?.({
247
- phase: 'rendering',
248
- progress: 90,
249
- message: 'Rendering text content...'
250
- });
251
-
252
- await renderTextContent(visibleElements, ctx, (current, total) => {
253
- const progress = 90 + Math.round((current / total) * 5);
254
- onProgress?.({
255
- phase: 'rendering',
256
- progress,
257
- message: `Rendering text (${current}/${total})...`
258
- });
259
- });
260
- await yieldToMainThread();
261
-
262
- onProgress?.({
263
- phase: 'exporting',
264
- progress: 95,
265
- message: 'Exporting canvas to image...'
266
- });
267
-
268
- // 13. Final export with taint detection
269
- try {
270
- // Test if canvas is tainted
271
- ctx.getImageData(0, 0, 1, 1);
272
-
273
- // Canvas is clean - export
274
- const dataUrl = canvas.toDataURL('image/png', finalQuality);
275
-
276
- // Validate that canvas is not blank
277
- const minExpectedSize = 5000;
278
- if (dataUrl.length < minExpectedSize) {
279
- console.warn('[Canvas] Canvas export appears to be blank or too small');
280
- return null;
281
- }
282
-
283
- onProgress?.({
284
- phase: 'exporting',
285
- progress: 100,
286
- message: 'Capture complete'
287
- });
288
-
289
- console.log('[Canvas] Capture successful (all layers)');
290
- return dataUrl;
291
-
292
- } catch (taintError) {
293
- // Canvas tainted by text layer - retry without it
294
- console.warn('[Canvas] Canvas tainted after text rendering, retrying without text layer');
295
-
296
- // Clear and recreate canvas
297
- ctx.clearRect(0, 0, canvas.width, canvas.height);
298
- ctx.fillStyle = '#ffffff';
299
- ctx.fillRect(0, 0, targetWidth, targetHeight);
300
- ctx.translate(-targetX, -targetY);
301
-
302
- // Re-render without text
303
- await renderBackgrounds(visibleElements, ctx);
304
- await yieldToMainThread();
305
- await renderImages(images, ctx, () => { });
306
- await yieldToMainThread();
307
- await renderSVGs(svgs, ctx, () => { });
308
- await yieldToMainThread();
309
-
310
- // Try export again
311
- try {
312
- const dataUrl = canvas.toDataURL('image/png', finalQuality);
313
-
314
- if (dataUrl.length < 5000) {
315
- console.warn('[Canvas] Canvas still appears blank after retry');
316
- return null;
317
- }
318
-
319
- console.log('[Canvas] Capture successful (without text layer)');
320
- return dataUrl;
321
- } catch (retryError) {
322
- console.error('[Canvas] Export failed even without text layer:', retryError);
323
- return null;
324
- }
325
- }
326
-
327
- } catch (error) {
328
- console.error('[Canvas] Capture failed:', error);
329
- return null;
330
- } finally {
331
- // 14. Cleanup resources
332
- cleanup(cleanupTracker);
333
- }
334
- }
335
-
336
- /**
337
- * Basic canvas capture without progress reporting
338
- */
339
- export async function basicCanvasCapture(
340
- cropRect?: Rect
341
- ): Promise<string | null> {
342
- return captureWithCanvas(cropRect, {}, undefined);
343
- }