@pallasoft/ps-file-components 1.4.0 → 2.0.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/dist/components/ps-document-scanner-loader.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.d.ts +1 -1
- package/dist/components/ps-document-scanner-sheet.js +64 -86
- package/dist/components/ps-document-scanner.d.ts +1 -1
- package/dist/components/ps-document-scanner.js +2 -3
- package/dist/components/ps-file-picker-actions.d.ts +10 -0
- package/dist/components/ps-file-picker-actions.js +21 -0
- package/dist/components/ps-file-picker.d.ts +7 -1
- package/dist/components/ps-file-picker.js +30 -6
- package/dist/components/ps-scan-button.d.ts +13 -0
- package/dist/components/ps-scan-button.js +9 -0
- package/dist/components/ps-scan-camera-view.d.ts +11 -8
- package/dist/components/ps-scan-camera-view.js +14 -16
- package/dist/components/ps-scan-crop-editor.d.ts +4 -4
- package/dist/components/ps-scan-crop-editor.js +24 -8
- package/dist/components/ps-scan-denied-steps.d.ts +6 -0
- package/dist/components/ps-scan-denied-steps.js +33 -0
- package/dist/components/ps-scan-icon-button.d.ts +4 -3
- package/dist/components/ps-scan-icon-button.js +3 -1
- package/dist/components/ps-scan-illustration.d.ts +1 -0
- package/dist/components/ps-scan-illustration.js +6 -0
- package/dist/components/ps-scan-loupe.d.ts +10 -0
- package/dist/components/ps-scan-loupe.js +31 -0
- package/dist/components/ps-scan-permission-view.d.ts +10 -0
- package/dist/components/ps-scan-permission-view.js +48 -0
- package/dist/components/ps-scan-photo-button.d.ts +7 -0
- package/dist/components/ps-scan-photo-button.js +20 -0
- package/dist/components/ps-scan-preview.d.ts +12 -0
- package/dist/components/ps-scan-preview.js +10 -0
- package/dist/components/ps-scan-shutter.d.ts +6 -0
- package/dist/components/ps-scan-shutter.js +6 -0
- package/dist/components/ps-scan-viewfinder-overlay.d.ts +8 -0
- package/dist/components/ps-scan-viewfinder-overlay.js +28 -0
- package/dist/components/ps-upload-row.js +1 -1
- package/dist/constants/ps-document-scanner-labels.d.ts +39 -38
- package/dist/constants/ps-document-scanner-labels.js +39 -38
- package/dist/constants/ps-file-picker-labels.d.ts +5 -3
- package/dist/constants/ps-file-picker-labels.js +5 -3
- package/dist/constants/ps-scan-camera-status.d.ts +4 -0
- package/dist/constants/ps-scan-camera-status.js +4 -0
- package/dist/constants/ps-scan-chrome-classes.d.ts +10 -0
- package/dist/constants/ps-scan-chrome-classes.js +13 -0
- package/dist/constants/ps-scan-config.d.ts +34 -24
- package/dist/constants/ps-scan-config.js +34 -24
- package/dist/constants/ps-scan-platform.d.ts +6 -0
- package/dist/constants/ps-scan-platform.js +8 -0
- package/dist/constants/ps-scan-step.d.ts +1 -1
- package/dist/constants/ps-scan-step.js +1 -1
- package/dist/esm/components/ps-document-scanner-sheet.js +67 -89
- package/dist/esm/components/ps-document-scanner.js +2 -3
- package/dist/esm/components/ps-file-picker-actions.js +17 -0
- package/dist/esm/components/ps-file-picker.js +31 -7
- package/dist/esm/components/ps-scan-button.js +5 -0
- package/dist/esm/components/ps-scan-camera-view.js +17 -19
- package/dist/esm/components/ps-scan-crop-editor.js +26 -10
- package/dist/esm/components/ps-scan-denied-steps.js +29 -0
- package/dist/esm/components/ps-scan-icon-button.js +3 -1
- package/dist/esm/components/ps-scan-illustration.js +2 -0
- package/dist/esm/components/ps-scan-loupe.js +27 -0
- package/dist/esm/components/ps-scan-permission-view.js +44 -0
- package/dist/esm/components/ps-scan-photo-button.js +16 -0
- package/dist/esm/components/ps-scan-preview.js +6 -0
- package/dist/esm/components/ps-scan-shutter.js +2 -0
- package/dist/esm/components/ps-scan-viewfinder-overlay.js +24 -0
- package/dist/esm/components/ps-upload-row.js +1 -1
- package/dist/esm/constants/ps-document-scanner-labels.js +39 -38
- package/dist/esm/constants/ps-file-picker-labels.js +5 -3
- package/dist/esm/constants/ps-scan-camera-status.js +4 -0
- package/dist/esm/constants/ps-scan-chrome-classes.js +10 -0
- package/dist/esm/constants/ps-scan-config.js +34 -24
- package/dist/esm/constants/ps-scan-platform.js +5 -0
- package/dist/esm/constants/ps-scan-step.js +1 -1
- package/dist/esm/hooks/use-ps-scan-camera.js +52 -15
- package/dist/esm/hooks/use-ps-scan-live-detection.js +41 -0
- package/dist/esm/hooks/use-ps-scan-photo.js +42 -0
- package/dist/esm/index.js +1 -2
- package/dist/esm/models/ps-scan-region.js +1 -0
- package/dist/esm/models/ps-scan-result.js +1 -0
- package/dist/esm/models/ps-scan-stability.js +1 -0
- package/dist/esm/services/ps-scan-camera-service.js +52 -7
- package/dist/esm/services/ps-scan-canny-service.js +93 -0
- package/dist/esm/services/ps-scan-canvas-service.js +13 -1
- package/dist/esm/services/ps-scan-edge-service.js +73 -137
- package/dist/esm/services/ps-scan-file-name-service.js +2 -11
- package/dist/esm/services/ps-scan-geometry-service.js +56 -0
- package/dist/esm/services/ps-scan-hull-service.js +0 -39
- package/dist/esm/services/ps-scan-image-filter-service.js +0 -21
- package/dist/esm/services/ps-scan-morphology-service.js +44 -0
- package/dist/esm/services/ps-scan-pipeline-service.js +9 -19
- package/dist/esm/services/ps-scan-platform-service.js +15 -0
- package/dist/esm/services/ps-scan-polygon-service.js +75 -0
- package/dist/esm/services/ps-scan-region-service.js +52 -0
- package/dist/esm/services/ps-scan-stability-service.js +28 -0
- package/dist/esm/services/ps-scan-warp-service.js +8 -2
- package/dist/esm/services/ps-scan-yield-service.js +5 -0
- package/dist/hooks/use-ps-scan-camera.d.ts +3 -1
- package/dist/hooks/use-ps-scan-camera.js +52 -15
- package/dist/hooks/use-ps-scan-live-detection.d.ts +3 -0
- package/dist/hooks/use-ps-scan-live-detection.js +45 -0
- package/dist/hooks/use-ps-scan-photo.d.ts +9 -0
- package/dist/hooks/use-ps-scan-photo.js +46 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/models/ps-document-scanner-props.d.ts +0 -3
- package/dist/models/ps-document-scanner-sheet-props.d.ts +0 -3
- package/dist/models/ps-file-picker-scan.d.ts +4 -0
- package/dist/models/ps-scan-gradient.d.ts +4 -0
- package/dist/models/ps-scan-live-detection.d.ts +4 -0
- package/dist/models/ps-scan-located.d.ts +5 -0
- package/dist/models/ps-scan-photo.d.ts +9 -0
- package/dist/models/ps-scan-region.d.ts +5 -0
- package/dist/models/ps-scan-region.js +2 -0
- package/dist/models/ps-scan-result.d.ts +6 -0
- package/dist/models/ps-scan-result.js +2 -0
- package/dist/models/ps-scan-stability.d.ts +5 -0
- package/dist/models/ps-scan-stability.js +2 -0
- package/dist/services/ps-scan-camera-service.d.ts +4 -0
- package/dist/services/ps-scan-camera-service.js +52 -7
- package/dist/services/ps-scan-canny-service.d.ts +7 -0
- package/dist/services/ps-scan-canny-service.js +97 -0
- package/dist/services/ps-scan-canvas-service.d.ts +1 -0
- package/dist/services/ps-scan-canvas-service.js +13 -1
- package/dist/services/ps-scan-edge-service.d.ts +5 -5
- package/dist/services/ps-scan-edge-service.js +73 -137
- package/dist/services/ps-scan-file-name-service.d.ts +1 -3
- package/dist/services/ps-scan-file-name-service.js +2 -11
- package/dist/services/ps-scan-geometry-service.d.ts +4 -0
- package/dist/services/ps-scan-geometry-service.js +56 -0
- package/dist/services/ps-scan-hull-service.d.ts +0 -2
- package/dist/services/ps-scan-hull-service.js +0 -39
- package/dist/services/ps-scan-image-filter-service.d.ts +0 -1
- package/dist/services/ps-scan-image-filter-service.js +0 -21
- package/dist/services/ps-scan-morphology-service.d.ts +7 -0
- package/dist/services/ps-scan-morphology-service.js +48 -0
- package/dist/services/ps-scan-pipeline-service.d.ts +2 -5
- package/dist/services/ps-scan-pipeline-service.js +9 -19
- package/dist/services/ps-scan-platform-service.d.ts +4 -0
- package/dist/services/ps-scan-platform-service.js +19 -0
- package/dist/services/ps-scan-polygon-service.d.ts +8 -0
- package/dist/services/ps-scan-polygon-service.js +79 -0
- package/dist/services/ps-scan-region-service.d.ts +4 -0
- package/dist/services/ps-scan-region-service.js +56 -0
- package/dist/services/ps-scan-stability-service.d.ts +7 -0
- package/dist/services/ps-scan-stability-service.js +32 -0
- package/dist/services/ps-scan-warp-service.d.ts +1 -1
- package/dist/services/ps-scan-warp-service.js +8 -2
- package/dist/services/ps-scan-yield-service.d.ts +3 -0
- package/dist/services/ps-scan-yield-service.js +9 -0
- package/package.json +3 -3
- package/dist/components/ps-scan-page-list.d.ts +0 -12
- package/dist/components/ps-scan-page-list.js +0 -23
- package/dist/components/ps-scan-review.d.ts +0 -23
- package/dist/components/ps-scan-review.js +0 -20
- package/dist/constants/ps-scan-filter.d.ts +0 -7
- package/dist/constants/ps-scan-filter.js +0 -9
- package/dist/constants/ps-scan-output-format.d.ts +0 -5
- package/dist/constants/ps-scan-output-format.js +0 -7
- package/dist/esm/components/ps-scan-page-list.js +0 -19
- package/dist/esm/components/ps-scan-review.js +0 -16
- package/dist/esm/constants/ps-scan-filter.js +0 -6
- package/dist/esm/constants/ps-scan-output-format.js +0 -4
- package/dist/esm/hooks/use-ps-scan-pages.js +0 -62
- package/dist/esm/hooks/use-ps-scan-previews.js +0 -57
- package/dist/esm/services/ps-scan-enhance-service.js +0 -97
- package/dist/esm/services/ps-scan-order-service.js +0 -13
- package/dist/esm/services/ps-scan-output-service.js +0 -24
- package/dist/esm/services/ps-scan-pdf-service.js +0 -56
- package/dist/hooks/use-ps-scan-pages.d.ts +0 -12
- package/dist/hooks/use-ps-scan-pages.js +0 -66
- package/dist/hooks/use-ps-scan-previews.d.ts +0 -2
- package/dist/hooks/use-ps-scan-previews.js +0 -61
- package/dist/models/ps-scan-added-page.d.ts +0 -5
- package/dist/models/ps-scan-levels.d.ts +0 -4
- package/dist/models/ps-scan-output-options.d.ts +0 -7
- package/dist/models/ps-scan-page.d.ts +0 -12
- package/dist/models/ps-scan-pdf-page.d.ts +0 -5
- package/dist/services/ps-scan-enhance-service.d.ts +0 -9
- package/dist/services/ps-scan-enhance-service.js +0 -101
- package/dist/services/ps-scan-order-service.d.ts +0 -4
- package/dist/services/ps-scan-order-service.js +0 -17
- package/dist/services/ps-scan-output-service.d.ts +0 -5
- package/dist/services/ps-scan-output-service.js +0 -28
- package/dist/services/ps-scan-pdf-service.d.ts +0 -5
- package/dist/services/ps-scan-pdf-service.js +0 -60
- /package/dist/esm/models/{ps-scan-added-page.js → ps-file-picker-scan.js} +0 -0
- /package/dist/esm/models/{ps-scan-levels.js → ps-scan-gradient.js} +0 -0
- /package/dist/esm/models/{ps-scan-output-options.js → ps-scan-live-detection.js} +0 -0
- /package/dist/esm/models/{ps-scan-page.js → ps-scan-located.js} +0 -0
- /package/dist/esm/models/{ps-scan-pdf-page.js → ps-scan-photo.js} +0 -0
- /package/dist/models/{ps-scan-added-page.js → ps-file-picker-scan.js} +0 -0
- /package/dist/models/{ps-scan-levels.js → ps-scan-gradient.js} +0 -0
- /package/dist/models/{ps-scan-output-options.js → ps-scan-live-detection.js} +0 -0
- /package/dist/models/{ps-scan-page.js → ps-scan-located.js} +0 -0
- /package/dist/models/{ps-scan-pdf-page.js → ps-scan-photo.js} +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
3
|
+
import { PsScanCanvasService } from '../services/ps-scan-canvas-service';
|
|
4
|
+
import { PsScanEdgeService } from '../services/ps-scan-edge-service';
|
|
5
|
+
import { PsScanStabilityService } from '../services/ps-scan-stability-service';
|
|
6
|
+
export const usePsScanLiveDetection = (videoRef, isActive) => {
|
|
7
|
+
const [detection, setDetection] = useState({ quad: null });
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!isActive) {
|
|
10
|
+
setDetection({ quad: null });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let isCancelled = false;
|
|
14
|
+
let state = PsScanStabilityService.initial();
|
|
15
|
+
let timer;
|
|
16
|
+
const tick = async () => {
|
|
17
|
+
const video = videoRef.current;
|
|
18
|
+
if (video !== null && video.videoWidth > 0 && document.visibilityState === 'visible') {
|
|
19
|
+
try {
|
|
20
|
+
const raster = PsScanCanvasService.sample(video, PsScanConfig.liveDetectMaxDimension);
|
|
21
|
+
const quad = await PsScanEdgeService.detect(raster, PsScanConfig.liveDetectMaxDimension);
|
|
22
|
+
if (isCancelled)
|
|
23
|
+
return;
|
|
24
|
+
state = PsScanStabilityService.next(state, quad);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
state = PsScanStabilityService.initial();
|
|
28
|
+
}
|
|
29
|
+
setDetection((current) => (current.quad === state.quad ? current : { quad: state.quad }));
|
|
30
|
+
}
|
|
31
|
+
if (!isCancelled)
|
|
32
|
+
timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
|
|
33
|
+
};
|
|
34
|
+
timer = setTimeout(tick, PsScanConfig.liveDetectIntervalMs);
|
|
35
|
+
return () => {
|
|
36
|
+
isCancelled = true;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
};
|
|
39
|
+
}, [isActive]);
|
|
40
|
+
return detection;
|
|
41
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { PsScanPipelineService } from '../services/ps-scan-pipeline-service';
|
|
3
|
+
export const usePsScanPhoto = () => {
|
|
4
|
+
const [photo, setPhoto] = useState(null);
|
|
5
|
+
const url = useRef('');
|
|
6
|
+
const release = useCallback(() => {
|
|
7
|
+
if (url.current !== '')
|
|
8
|
+
URL.revokeObjectURL(url.current);
|
|
9
|
+
url.current = '';
|
|
10
|
+
}, []);
|
|
11
|
+
useEffect(() => release, [release]);
|
|
12
|
+
const load = useCallback(async (source) => {
|
|
13
|
+
const prepared = await PsScanPipelineService.prepare(source);
|
|
14
|
+
const next = {
|
|
15
|
+
corners: prepared.corners,
|
|
16
|
+
height: prepared.height,
|
|
17
|
+
isDetected: prepared.isDetected,
|
|
18
|
+
source,
|
|
19
|
+
sourceUrl: URL.createObjectURL(source),
|
|
20
|
+
width: prepared.width,
|
|
21
|
+
};
|
|
22
|
+
release();
|
|
23
|
+
url.current = next.sourceUrl;
|
|
24
|
+
setPhoto(next);
|
|
25
|
+
return next;
|
|
26
|
+
}, [release]);
|
|
27
|
+
const setCorners = useCallback((corners) => setPhoto((current) => (current === null ? current : { ...current, corners })), []);
|
|
28
|
+
const detectAgain = useCallback(async () => {
|
|
29
|
+
if (photo === null)
|
|
30
|
+
return false;
|
|
31
|
+
const prepared = await PsScanPipelineService.prepare(photo.source);
|
|
32
|
+
setPhoto((current) => current === null
|
|
33
|
+
? current
|
|
34
|
+
: { ...current, corners: prepared.corners, isDetected: prepared.isDetected });
|
|
35
|
+
return prepared.isDetected;
|
|
36
|
+
}, [photo]);
|
|
37
|
+
const clear = useCallback(() => {
|
|
38
|
+
release();
|
|
39
|
+
setPhoto(null);
|
|
40
|
+
}, [release]);
|
|
41
|
+
return { clear, detectAgain, load, photo, setCorners };
|
|
42
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -71,6 +71,5 @@ export * from './hooks/use-ps-in-view';
|
|
|
71
71
|
export * from './hooks/use-ps-upload-queue';
|
|
72
72
|
export * from './components/ps-document-scanner';
|
|
73
73
|
export * from './constants/ps-document-scanner-labels';
|
|
74
|
-
export * from './constants/ps-scan-filter';
|
|
75
|
-
export * from './constants/ps-scan-output-format';
|
|
76
74
|
export * from './models/ps-document-scanner-props';
|
|
75
|
+
export * from './models/ps-file-picker-scan';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,13 +1,45 @@
|
|
|
1
1
|
import { PsScanCameraStatus } from '../constants/ps-scan-camera-status';
|
|
2
2
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
3
3
|
export class PsScanCameraService {
|
|
4
|
+
static async attach(video, session) {
|
|
5
|
+
if (video.srcObject !== session.stream)
|
|
6
|
+
video.srcObject = session.stream;
|
|
7
|
+
await video.play().catch(() => undefined);
|
|
8
|
+
}
|
|
9
|
+
static async check() {
|
|
10
|
+
if (globalThis.isSecureContext === false)
|
|
11
|
+
return PsScanCameraStatus.Insecure;
|
|
12
|
+
if (!PsScanCameraService.isSupported())
|
|
13
|
+
return PsScanCameraStatus.Unavailable;
|
|
14
|
+
if (!(await PsScanCameraService.hasCamera()))
|
|
15
|
+
return PsScanCameraStatus.Unavailable;
|
|
16
|
+
const permission = await PsScanCameraService.permission();
|
|
17
|
+
if (permission === null)
|
|
18
|
+
return PsScanCameraStatus.Prompt;
|
|
19
|
+
if (permission.state === 'granted')
|
|
20
|
+
return PsScanCameraStatus.Starting;
|
|
21
|
+
return permission.state === 'denied' ? PsScanCameraStatus.Denied : PsScanCameraStatus.Prompt;
|
|
22
|
+
}
|
|
4
23
|
static classify(error) {
|
|
5
24
|
const name = typeof error === 'object' && error !== null && 'name' in error ? String(error.name) : '';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
if (name === 'NotAllowedError' || name === 'PermissionDeniedError')
|
|
26
|
+
return PsScanCameraStatus.Denied;
|
|
27
|
+
if (name === 'SecurityError')
|
|
28
|
+
return PsScanCameraStatus.Insecure;
|
|
29
|
+
if (name === 'NotReadableError' || name === 'TrackStartError' || name === 'AbortError')
|
|
30
|
+
return PsScanCameraStatus.Busy;
|
|
31
|
+
return PsScanCameraStatus.Unavailable;
|
|
32
|
+
}
|
|
33
|
+
static async hasCamera() {
|
|
34
|
+
if (typeof navigator.mediaDevices.enumerateDevices !== 'function')
|
|
35
|
+
return true;
|
|
36
|
+
try {
|
|
37
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
38
|
+
return devices.length === 0 || devices.some((device) => device.kind === 'videoinput');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
11
43
|
}
|
|
12
44
|
static isSupported() {
|
|
13
45
|
return (typeof navigator !== 'undefined' &&
|
|
@@ -19,13 +51,26 @@ export class PsScanCameraService {
|
|
|
19
51
|
audio: false,
|
|
20
52
|
video: {
|
|
21
53
|
facingMode: { ideal: 'environment' },
|
|
22
|
-
height: { ideal:
|
|
23
|
-
width: { ideal:
|
|
54
|
+
height: { ideal: 2160 },
|
|
55
|
+
width: { ideal: 3840 },
|
|
24
56
|
},
|
|
25
57
|
});
|
|
26
58
|
const track = stream.getVideoTracks()[0];
|
|
27
59
|
return { canTorch: PsScanCameraService.supportsTorch(track), stream, track };
|
|
28
60
|
}
|
|
61
|
+
static async permission() {
|
|
62
|
+
if (typeof navigator.permissions === 'undefined' ||
|
|
63
|
+
typeof navigator.permissions.query !== 'function')
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
return await navigator.permissions.query({
|
|
67
|
+
name: PsScanConfig.cameraPermission,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
29
74
|
static async setTorch(track, isOn) {
|
|
30
75
|
try {
|
|
31
76
|
await track.applyConstraints({
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
export class PsScanCannyService {
|
|
3
|
+
static edges(gray, width, height) {
|
|
4
|
+
const gradient = PsScanCannyService.gradient(gray, width, height);
|
|
5
|
+
const ridges = PsScanCannyService.suppress(gradient, width, height);
|
|
6
|
+
const high = PsScanCannyService.highThreshold(ridges);
|
|
7
|
+
return PsScanCannyService.hysteresis(ridges, width, height, high * PsScanConfig.cannyLowRatio, high);
|
|
8
|
+
}
|
|
9
|
+
static gradient(gray, width, height) {
|
|
10
|
+
const magnitude = new Float32Array(width * height);
|
|
11
|
+
const direction = new Uint8Array(width * height);
|
|
12
|
+
for (let y = 1; y < height - 1; y += 1)
|
|
13
|
+
for (let x = 1; x < width - 1; x += 1) {
|
|
14
|
+
const i = y * width + x;
|
|
15
|
+
const gx = -gray[i - width - 1] +
|
|
16
|
+
gray[i - width + 1] -
|
|
17
|
+
2 * gray[i - 1] +
|
|
18
|
+
2 * gray[i + 1] -
|
|
19
|
+
gray[i + width - 1] +
|
|
20
|
+
gray[i + width + 1];
|
|
21
|
+
const gy = -gray[i - width - 1] -
|
|
22
|
+
2 * gray[i - width] -
|
|
23
|
+
gray[i - width + 1] +
|
|
24
|
+
gray[i + width - 1] +
|
|
25
|
+
2 * gray[i + width] +
|
|
26
|
+
gray[i + width + 1];
|
|
27
|
+
const ax = Math.abs(gx);
|
|
28
|
+
const ay = Math.abs(gy);
|
|
29
|
+
magnitude[i] = Math.hypot(gx, gy) / 4;
|
|
30
|
+
if (ay <= ax * 0.41421356)
|
|
31
|
+
direction[i] = 0;
|
|
32
|
+
else if (ay >= ax * 2.41421356)
|
|
33
|
+
direction[i] = 2;
|
|
34
|
+
else
|
|
35
|
+
direction[i] = gx * gy > 0 ? 1 : 3;
|
|
36
|
+
}
|
|
37
|
+
return { direction, magnitude };
|
|
38
|
+
}
|
|
39
|
+
static highThreshold(ridges) {
|
|
40
|
+
const values = [];
|
|
41
|
+
for (let index = 0; index < ridges.length; index += 1)
|
|
42
|
+
if (ridges[index] > 0)
|
|
43
|
+
values.push(ridges[index]);
|
|
44
|
+
if (values.length === 0)
|
|
45
|
+
return PsScanConfig.cannyMinHigh;
|
|
46
|
+
values.sort((a, b) => a - b);
|
|
47
|
+
const at = Math.min(values.length - 1, Math.floor(values.length * PsScanConfig.cannyHighPercentile));
|
|
48
|
+
return Math.max(PsScanConfig.cannyMinHigh, values[at]);
|
|
49
|
+
}
|
|
50
|
+
static hysteresis(ridges, width, height, low, high) {
|
|
51
|
+
const out = new Uint8Array(width * height);
|
|
52
|
+
const stack = [];
|
|
53
|
+
for (let index = 0; index < ridges.length; index += 1)
|
|
54
|
+
if (ridges[index] >= high) {
|
|
55
|
+
out[index] = 1;
|
|
56
|
+
stack.push(index);
|
|
57
|
+
}
|
|
58
|
+
while (stack.length > 0) {
|
|
59
|
+
const index = stack.pop();
|
|
60
|
+
if (index === undefined)
|
|
61
|
+
break;
|
|
62
|
+
const x = index % width;
|
|
63
|
+
const y = (index - x) / width;
|
|
64
|
+
for (let dy = -1; dy <= 1; dy += 1)
|
|
65
|
+
for (let dx = -1; dx <= 1; dx += 1) {
|
|
66
|
+
const nx = x + dx;
|
|
67
|
+
const ny = y + dy;
|
|
68
|
+
if (nx < 0 || ny < 0 || nx >= width || ny >= height)
|
|
69
|
+
continue;
|
|
70
|
+
const next = ny * width + nx;
|
|
71
|
+
if (out[next] === 0 && ridges[next] >= low) {
|
|
72
|
+
out[next] = 1;
|
|
73
|
+
stack.push(next);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
static suppress(gradient, width, height) {
|
|
80
|
+
const { direction, magnitude } = gradient;
|
|
81
|
+
const out = new Float32Array(width * height);
|
|
82
|
+
const offsets = [1, width + 1, width, width - 1];
|
|
83
|
+
for (let y = 1; y < height - 1; y += 1)
|
|
84
|
+
for (let x = 1; x < width - 1; x += 1) {
|
|
85
|
+
const i = y * width + x;
|
|
86
|
+
const offset = offsets[direction[i]];
|
|
87
|
+
const value = magnitude[i];
|
|
88
|
+
if (value >= magnitude[i - offset] && value > magnitude[i + offset])
|
|
89
|
+
out[i] = value;
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
1
2
|
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
2
3
|
export class PsScanCanvasService {
|
|
3
4
|
static async decode(blob, maxDimension) {
|
|
4
5
|
const bitmap = await createImageBitmap(blob, { imageOrientation: 'from-image' });
|
|
5
|
-
const
|
|
6
|
+
const capped = PsScanGeometryService.fitWithin({ height: bitmap.height, width: bitmap.width }, maxDimension);
|
|
7
|
+
const shrink = Math.min(1, Math.sqrt(PsScanConfig.decodeMaxPixels / (capped.width * capped.height)));
|
|
8
|
+
const size = {
|
|
9
|
+
height: Math.max(1, Math.round(capped.height * shrink)),
|
|
10
|
+
width: Math.max(1, Math.round(capped.width * shrink)),
|
|
11
|
+
};
|
|
6
12
|
const context = PsScanCanvasService.context(size.width, size.height);
|
|
7
13
|
context.drawImage(bitmap, 0, 0, size.width, size.height);
|
|
8
14
|
const naturalHeight = bitmap.height;
|
|
@@ -27,6 +33,12 @@ export class PsScanCanvasService {
|
|
|
27
33
|
context.drawImage(video, 0, 0);
|
|
28
34
|
return await PsScanCanvasService.encode(context.getImageData(0, 0, video.videoWidth, video.videoHeight), quality);
|
|
29
35
|
}
|
|
36
|
+
static sample(video, maxDimension) {
|
|
37
|
+
const size = PsScanGeometryService.fitWithin({ height: video.videoHeight, width: video.videoWidth }, maxDimension);
|
|
38
|
+
const context = PsScanCanvasService.context(size.width, size.height);
|
|
39
|
+
context.drawImage(video, 0, 0, size.width, size.height);
|
|
40
|
+
return context.getImageData(0, 0, size.width, size.height);
|
|
41
|
+
}
|
|
30
42
|
static context(width, height) {
|
|
31
43
|
if (typeof OffscreenCanvas !== 'undefined') {
|
|
32
44
|
const context = new OffscreenCanvas(width, height).getContext('2d', {
|
|
@@ -1,156 +1,92 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
+
import { PsScanCannyService } from './ps-scan-canny-service';
|
|
2
3
|
import { PsScanGeometryService } from './ps-scan-geometry-service';
|
|
3
4
|
import { PsScanHullService } from './ps-scan-hull-service';
|
|
4
5
|
import { PsScanImageFilterService } from './ps-scan-image-filter-service';
|
|
6
|
+
import { PsScanMorphologyService } from './ps-scan-morphology-service';
|
|
7
|
+
import { PsScanPolygonService } from './ps-scan-polygon-service';
|
|
5
8
|
import { PsScanRasterService } from './ps-scan-raster-service';
|
|
9
|
+
import { PsScanRegionService } from './ps-scan-region-service';
|
|
10
|
+
import { PsScanYieldService } from './ps-scan-yield-service';
|
|
6
11
|
export class PsScanEdgeService {
|
|
7
|
-
static detect(source) {
|
|
8
|
-
const scale = Math.min(1,
|
|
9
|
-
const width = Math.max(
|
|
10
|
-
const height = Math.max(
|
|
12
|
+
static async detect(source, maxDimension) {
|
|
13
|
+
const scale = Math.min(1, maxDimension / Math.max(source.width, source.height));
|
|
14
|
+
const width = Math.max(16, Math.round(source.width * scale));
|
|
15
|
+
const height = Math.max(16, Math.round(source.height * scale));
|
|
11
16
|
const raster = PsScanRasterService.resize(source, width, height);
|
|
12
17
|
const gray = PsScanImageFilterService.blur(PsScanImageFilterService.grayscale(raster), width, height);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
for (let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
await PsScanYieldService.next();
|
|
19
|
+
const edges = PsScanCannyService.edges(gray, width, height);
|
|
20
|
+
for (let index = 0; index < width; index += 1) {
|
|
21
|
+
edges[index] = 1;
|
|
22
|
+
edges[(height - 1) * width + index] = 1;
|
|
23
|
+
}
|
|
24
|
+
for (let index = 0; index < height; index += 1) {
|
|
25
|
+
edges[index * width] = 1;
|
|
26
|
+
edges[index * width + width - 1] = 1;
|
|
27
|
+
}
|
|
28
|
+
let best = null;
|
|
29
|
+
let bestArea = 0;
|
|
30
|
+
for (const radius of PsScanConfig.closeRadii) {
|
|
31
|
+
await PsScanYieldService.next();
|
|
32
|
+
const closed = PsScanMorphologyService.close(edges, width, height, radius);
|
|
33
|
+
const regions = PsScanRegionService.enclosed(closed, width, height, width * height * PsScanConfig.detectMinAreaRatio * 0.5);
|
|
34
|
+
for (const region of regions) {
|
|
35
|
+
const quad = PsScanEdgeService.candidate(region.boundary, width, height);
|
|
36
|
+
if (quad === null)
|
|
37
|
+
continue;
|
|
38
|
+
const area = PsScanGeometryService.area(quad);
|
|
39
|
+
if (area > bestArea) {
|
|
40
|
+
bestArea = area;
|
|
41
|
+
best = quad;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (best === null)
|
|
20
46
|
return null;
|
|
21
|
-
|
|
47
|
+
return PsScanEdgeService.normalise(best, width, height);
|
|
48
|
+
}
|
|
49
|
+
static async locate(source, maxDimension) {
|
|
50
|
+
const quad = await PsScanEdgeService.detect(source, maxDimension);
|
|
51
|
+
return quad === null
|
|
52
|
+
? { isDetected: false, quad: PsScanGeometryService.defaultQuad() }
|
|
53
|
+
: { isDetected: true, quad };
|
|
54
|
+
}
|
|
55
|
+
static candidate(boundary, width, height) {
|
|
56
|
+
const points = boundary.map((point) => ({ x: point.x + 0.5, y: point.y + 0.5 }));
|
|
57
|
+
const hull = PsScanHullService.convexHull(points);
|
|
22
58
|
if (hull.length < 4)
|
|
23
59
|
return null;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
60
|
+
const corners = PsScanPolygonService.toQuad(hull);
|
|
61
|
+
if (corners === null)
|
|
62
|
+
return null;
|
|
63
|
+
const ordered = PsScanGeometryService.order(corners);
|
|
64
|
+
const margin = PsScanConfig.detectBorderMargin;
|
|
65
|
+
const onBorder = ordered.filter((point) => point.x <= margin ||
|
|
66
|
+
point.y <= margin ||
|
|
67
|
+
point.x >= width - margin ||
|
|
68
|
+
point.y >= height - margin);
|
|
69
|
+
if (onBorder.length >= 2)
|
|
29
70
|
return null;
|
|
30
|
-
|
|
71
|
+
const quad = PsScanGeometryService.expand(ordered, PsScanConfig.detectExpand);
|
|
72
|
+
const imageArea = width * height;
|
|
73
|
+
const area = PsScanGeometryService.area(quad);
|
|
74
|
+
const fill = PsScanGeometryService.area(ordered) / PsScanGeometryService.area(hull);
|
|
75
|
+
const ratio = area / imageArea;
|
|
76
|
+
if (ratio < PsScanConfig.detectMinAreaRatio || ratio > PsScanConfig.detectMaxAreaRatio)
|
|
31
77
|
return null;
|
|
32
|
-
if (
|
|
78
|
+
if (fill < PsScanConfig.detectMinQuadFill)
|
|
33
79
|
return null;
|
|
34
80
|
if (!PsScanGeometryService.isConvex(quad))
|
|
35
81
|
return null;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const out = new Uint8Array(mask.length);
|
|
43
|
-
for (let y = 0; y < height; y += 1)
|
|
44
|
-
for (let x = 0; x < width; x += 1) {
|
|
45
|
-
if (mask[y * width + x] === 0)
|
|
46
|
-
continue;
|
|
47
|
-
for (let dy = -1; dy <= 1; dy += 1)
|
|
48
|
-
for (let dx = -1; dx <= 1; dx += 1) {
|
|
49
|
-
const nx = x + dx;
|
|
50
|
-
const ny = y + dy;
|
|
51
|
-
if (nx >= 0 && ny >= 0 && nx < width && ny < height)
|
|
52
|
-
out[ny * width + nx] = 1;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
static foreground(mask, width, height) {
|
|
58
|
-
const background = new Uint8Array(mask.length);
|
|
59
|
-
const stack = [];
|
|
60
|
-
const seed = (index) => {
|
|
61
|
-
if (mask[index] === 0 && background[index] === 0) {
|
|
62
|
-
background[index] = 1;
|
|
63
|
-
stack.push(index);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
for (let x = 0; x < width; x += 1) {
|
|
67
|
-
seed(x);
|
|
68
|
-
seed((height - 1) * width + x);
|
|
69
|
-
}
|
|
70
|
-
for (let y = 0; y < height; y += 1) {
|
|
71
|
-
seed(y * width);
|
|
72
|
-
seed(y * width + width - 1);
|
|
73
|
-
}
|
|
74
|
-
while (stack.length > 0) {
|
|
75
|
-
const index = stack.pop();
|
|
76
|
-
if (index === undefined)
|
|
77
|
-
break;
|
|
78
|
-
const x = index % width;
|
|
79
|
-
const y = (index - x) / width;
|
|
80
|
-
if (x > 0)
|
|
81
|
-
seed(index - 1);
|
|
82
|
-
if (x < width - 1)
|
|
83
|
-
seed(index + 1);
|
|
84
|
-
if (y > 0)
|
|
85
|
-
seed(index - width);
|
|
86
|
-
if (y < height - 1)
|
|
87
|
-
seed(index + width);
|
|
88
|
-
}
|
|
89
|
-
return background.map((value) => 1 - value);
|
|
90
|
-
}
|
|
91
|
-
static largestComponent(mask, width, height) {
|
|
92
|
-
const seen = new Uint8Array(mask.length);
|
|
93
|
-
let best = [];
|
|
94
|
-
let bestSize = 0;
|
|
95
|
-
for (let start = 0; start < mask.length; start += 1) {
|
|
96
|
-
if (mask[start] === 0 || seen[start] === 1)
|
|
97
|
-
continue;
|
|
98
|
-
const members = [];
|
|
99
|
-
const stack = [start];
|
|
100
|
-
let size = 0;
|
|
101
|
-
seen[start] = 1;
|
|
102
|
-
while (stack.length > 0) {
|
|
103
|
-
const index = stack.pop();
|
|
104
|
-
if (index === undefined)
|
|
105
|
-
break;
|
|
106
|
-
const x = index % width;
|
|
107
|
-
const y = (index - x) / width;
|
|
108
|
-
let isBoundary = false;
|
|
109
|
-
size += 1;
|
|
110
|
-
for (let dy = -1; dy <= 1; dy += 1)
|
|
111
|
-
for (let dx = -1; dx <= 1; dx += 1) {
|
|
112
|
-
if (dx === 0 && dy === 0)
|
|
113
|
-
continue;
|
|
114
|
-
const nx = x + dx;
|
|
115
|
-
const ny = y + dy;
|
|
116
|
-
if (nx < 0 || ny < 0 || nx >= width || ny >= height) {
|
|
117
|
-
isBoundary = true;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const next = ny * width + nx;
|
|
121
|
-
if (mask[next] === 0)
|
|
122
|
-
isBoundary = true;
|
|
123
|
-
else if (seen[next] === 0) {
|
|
124
|
-
seen[next] = 1;
|
|
125
|
-
stack.push(next);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (isBoundary)
|
|
129
|
-
members.push({ x, y });
|
|
130
|
-
}
|
|
131
|
-
if (size > bestSize) {
|
|
132
|
-
bestSize = size;
|
|
133
|
-
best = members;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return best;
|
|
82
|
+
const shortSide = Math.min(width, height) * PsScanConfig.detectMinSideRatio;
|
|
83
|
+
const isLongEnough = quad.every((point, index) => PsScanGeometryService.distance(point, quad[(index + 1) % quad.length]) >= shortSide);
|
|
84
|
+
if (!isLongEnough)
|
|
85
|
+
return null;
|
|
86
|
+
const isSane = PsScanGeometryService.angles(quad).every((angle) => angle >= PsScanConfig.detectMinAngle && angle <= PsScanConfig.detectMaxAngle);
|
|
87
|
+
return isSane ? quad : null;
|
|
137
88
|
}
|
|
138
|
-
static
|
|
139
|
-
|
|
140
|
-
edges.forEach((value) => {
|
|
141
|
-
bins[Math.min(511, value)] += 1;
|
|
142
|
-
});
|
|
143
|
-
const target = edges.length * PsScanConfig.detectPercentile;
|
|
144
|
-
let cumulative = 0;
|
|
145
|
-
let high = 0;
|
|
146
|
-
for (let bin = 0; bin < bins.length; bin += 1) {
|
|
147
|
-
cumulative += bins[bin];
|
|
148
|
-
if (cumulative >= target) {
|
|
149
|
-
high = bin;
|
|
150
|
-
break;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
const limit = Math.max(PsScanConfig.detectMinEdgeStrength, high * PsScanConfig.detectRelativeStrength);
|
|
154
|
-
return Uint8Array.from(edges, (value) => (value >= limit ? 1 : 0));
|
|
89
|
+
static normalise(quad, width, height) {
|
|
90
|
+
return quad.map((point) => PsScanGeometryService.clamp({ x: point.x / width, y: point.y / height }));
|
|
155
91
|
}
|
|
156
92
|
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
|
-
import { PsScanOutputFormat } from '../constants/ps-scan-output-format';
|
|
3
2
|
export class PsScanFileNameService {
|
|
4
|
-
static build(base
|
|
5
|
-
|
|
6
|
-
if (format === PsScanOutputFormat.Pdf)
|
|
7
|
-
return `${stem}.pdf`;
|
|
8
|
-
if (count <= 1)
|
|
9
|
-
return `${stem}.jpg`;
|
|
10
|
-
return `${stem}-${String(index + 1).padStart(String(count).length, '0')}.jpg`;
|
|
11
|
-
}
|
|
12
|
-
static mimeType(format) {
|
|
13
|
-
return format === PsScanOutputFormat.Pdf ? 'application/pdf' : 'image/jpeg';
|
|
3
|
+
static build(base) {
|
|
4
|
+
return `${PsScanFileNameService.sanitize(base)}.jpg`;
|
|
14
5
|
}
|
|
15
6
|
static sanitize(base) {
|
|
16
7
|
const cleaned = Array.from(base)
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { PsScanConfig } from '../constants/ps-scan-config';
|
|
2
2
|
export class PsScanGeometryService {
|
|
3
|
+
static angles(quad) {
|
|
4
|
+
return quad.map((point, index) => {
|
|
5
|
+
const before = quad[(index + quad.length - 1) % quad.length];
|
|
6
|
+
const after = quad[(index + 1) % quad.length];
|
|
7
|
+
const ax = before.x - point.x;
|
|
8
|
+
const ay = before.y - point.y;
|
|
9
|
+
const bx = after.x - point.x;
|
|
10
|
+
const by = after.y - point.y;
|
|
11
|
+
const cosine = (ax * bx + ay * by) / (Math.hypot(ax, ay) * Math.hypot(bx, by));
|
|
12
|
+
return (Math.acos(Math.min(1, Math.max(-1, cosine))) * 180) / Math.PI;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
3
15
|
static area(points) {
|
|
4
16
|
let sum = 0;
|
|
5
17
|
points.forEach((point, index) => {
|
|
@@ -11,6 +23,12 @@ export class PsScanGeometryService {
|
|
|
11
23
|
static clamp(point) {
|
|
12
24
|
return { x: Math.min(1, Math.max(0, point.x)), y: Math.min(1, Math.max(0, point.y)) };
|
|
13
25
|
}
|
|
26
|
+
static cover(container, content) {
|
|
27
|
+
if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
|
|
28
|
+
return { height: 0, width: 0 };
|
|
29
|
+
const scale = Math.max(container.width / content.width, container.height / content.height);
|
|
30
|
+
return { height: content.height * scale, width: content.width * scale };
|
|
31
|
+
}
|
|
14
32
|
static defaultQuad() {
|
|
15
33
|
const inset = 0.08;
|
|
16
34
|
return [
|
|
@@ -23,6 +41,30 @@ export class PsScanGeometryService {
|
|
|
23
41
|
static distance(from, to) {
|
|
24
42
|
return Math.hypot(to.x - from.x, to.y - from.y);
|
|
25
43
|
}
|
|
44
|
+
static expand(quad, distance) {
|
|
45
|
+
const lines = quad.map((point, index) => {
|
|
46
|
+
const next = quad[(index + 1) % quad.length];
|
|
47
|
+
const length = PsScanGeometryService.distance(point, next);
|
|
48
|
+
const nx = (next.y - point.y) / length;
|
|
49
|
+
const ny = -(next.x - point.x) / length;
|
|
50
|
+
return {
|
|
51
|
+
dx: next.x - point.x,
|
|
52
|
+
dy: next.y - point.y,
|
|
53
|
+
x: point.x + nx * distance,
|
|
54
|
+
y: point.y + ny * distance,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
return quad.map((point, index) => {
|
|
58
|
+
const previous = lines[(index + lines.length - 1) % lines.length];
|
|
59
|
+
const current = lines[index];
|
|
60
|
+
const denominator = previous.dx * current.dy - previous.dy * current.dx;
|
|
61
|
+
if (Math.abs(denominator) < 1e-9)
|
|
62
|
+
return point;
|
|
63
|
+
const t = ((current.x - previous.x) * current.dy - (current.y - previous.y) * current.dx) /
|
|
64
|
+
denominator;
|
|
65
|
+
return { x: previous.x + previous.dx * t, y: previous.y + previous.dy * t };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
26
68
|
static fit(container, content) {
|
|
27
69
|
if (container.width <= 0 || container.height <= 0 || content.width <= 0 || content.height <= 0)
|
|
28
70
|
return { height: 0, width: 0 };
|
|
@@ -39,6 +81,20 @@ export class PsScanGeometryService {
|
|
|
39
81
|
width: Math.max(1, Math.round(size.width * scale)),
|
|
40
82
|
};
|
|
41
83
|
}
|
|
84
|
+
static inset(quad, distance) {
|
|
85
|
+
const centre = {
|
|
86
|
+
x: quad.reduce((sum, point) => sum + point.x, 0) / quad.length,
|
|
87
|
+
y: quad.reduce((sum, point) => sum + point.y, 0) / quad.length,
|
|
88
|
+
};
|
|
89
|
+
return quad.map((point) => {
|
|
90
|
+
const length = PsScanGeometryService.distance(point, centre);
|
|
91
|
+
const ratio = length <= distance ? 0 : (length - distance) / length;
|
|
92
|
+
return {
|
|
93
|
+
x: centre.x + (point.x - centre.x) * ratio,
|
|
94
|
+
y: centre.y + (point.y - centre.y) * ratio,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
42
98
|
static isConvex(points) {
|
|
43
99
|
let sign = 0;
|
|
44
100
|
for (let index = 0; index < points.length; index += 1) {
|