@wemap/camera 12.10.6 → 12.10.8-alpha.2
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/index.js +1 -399
- package/dist/index.js.map +1 -1
- package/dist/src/Camera.d.ts +66 -0
- package/dist/src/CameraUtils.d.ts +35 -0
- package/dist/src/QrCodeScanner.d.ts +24 -0
- package/dist/src/SharedCameras.d.ts +21 -0
- package/dist/vite.config.d.ts +19 -0
- package/package.json +8 -5
- package/src/Camera.ts +0 -282
- package/src/CameraUtils.ts +0 -175
- package/src/QrCodeScanner.ts +0 -90
- package/src/SharedCameras.ts +0 -39
- package/tsconfig.json +0 -3
- package/vite.config.ts +0 -4
- /package/{index.ts → dist/index.d.ts} +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { default as EventEmitter } from 'events';
|
|
2
|
+
import { default as Camera } from './Camera.js';
|
|
3
|
+
declare interface QrCodeScanner {
|
|
4
|
+
on(event: 'scan', listener: (result: string) => void): this;
|
|
5
|
+
off(event: 'scan', listener: (result: string) => void): this;
|
|
6
|
+
}
|
|
7
|
+
declare class QrCodeScanner extends EventEmitter {
|
|
8
|
+
_height?: number;
|
|
9
|
+
_width?: number;
|
|
10
|
+
_camera: Camera;
|
|
11
|
+
_videoElement: HTMLVideoElement;
|
|
12
|
+
_canvas: HTMLCanvasElement;
|
|
13
|
+
_ctx: CanvasRenderingContext2D;
|
|
14
|
+
_intervalTime: number | null;
|
|
15
|
+
_intervalTick?: number;
|
|
16
|
+
_isStarted: boolean;
|
|
17
|
+
constructor(camera: Camera);
|
|
18
|
+
start(intervalTime?: number): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
_tryStart(): void;
|
|
21
|
+
_stop(): void;
|
|
22
|
+
_onFrame(): void;
|
|
23
|
+
}
|
|
24
|
+
export default QrCodeScanner;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { default as EventEmitter } from 'events';
|
|
2
|
+
import { default as Camera } from './Camera.js';
|
|
3
|
+
declare interface SharedCameras {
|
|
4
|
+
on(event: 'added', listener: (obj: CameraContainer) => void): this;
|
|
5
|
+
on(event: 'removed', listener: (obj: {
|
|
6
|
+
camera: Camera;
|
|
7
|
+
}) => void): this;
|
|
8
|
+
}
|
|
9
|
+
type CameraContainer = {
|
|
10
|
+
container: HTMLElement;
|
|
11
|
+
camera: Camera;
|
|
12
|
+
};
|
|
13
|
+
declare class SharedCameras extends EventEmitter {
|
|
14
|
+
_list: CameraContainer[];
|
|
15
|
+
_add(camera: Camera, container: HTMLElement): void;
|
|
16
|
+
_remove(camera: Camera): void;
|
|
17
|
+
get list(): CameraContainer[];
|
|
18
|
+
getCameraByContainer(container: HTMLElement): Camera | null;
|
|
19
|
+
}
|
|
20
|
+
declare const _default: SharedCameras;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
build: {
|
|
3
|
+
minify: string;
|
|
4
|
+
lib: {
|
|
5
|
+
entry: string;
|
|
6
|
+
name: string;
|
|
7
|
+
formats: string[];
|
|
8
|
+
fileName: (format: string) => "index.mjs" | "index.js";
|
|
9
|
+
};
|
|
10
|
+
sourcemap: boolean;
|
|
11
|
+
};
|
|
12
|
+
plugins: import('vite').Plugin[];
|
|
13
|
+
optimizeDeps: {
|
|
14
|
+
esbuildOptions: {
|
|
15
|
+
jsx: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export default _default;
|
package/package.json
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
],
|
|
6
6
|
"description": "Wemap Camera utils package",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
|
-
"types": "index.ts",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/wemap/wemap-modules-js.git",
|
|
12
12
|
"directory": "packages/camera"
|
|
13
13
|
},
|
|
14
14
|
"name": "@wemap/camera",
|
|
15
|
-
"version": "12.10.
|
|
15
|
+
"version": "12.10.8-alpha.2",
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/wemap/wemap-modules-js/issues"
|
|
18
18
|
},
|
|
@@ -29,17 +29,20 @@
|
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@types/events": "^3.0.0",
|
|
32
|
-
"@wemap/logger": "^12.10.
|
|
32
|
+
"@wemap/logger": "^12.10.8-alpha.2",
|
|
33
33
|
"events": "^3.3.0",
|
|
34
34
|
"qr-scanner": "^1.4.2"
|
|
35
35
|
},
|
|
36
36
|
"type": "module",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist"
|
|
39
|
+
],
|
|
37
40
|
"exports": {
|
|
38
41
|
".": {
|
|
39
|
-
"types": "./index.ts",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
40
43
|
"import": "./dist/index.mjs",
|
|
41
44
|
"require": "./dist/index.js"
|
|
42
45
|
}
|
|
43
46
|
},
|
|
44
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "76c13880fb0d409c142cfc464dd48abc298ae58e"
|
|
45
48
|
}
|
package/src/Camera.ts
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import EventEmitter from 'events';
|
|
2
|
-
|
|
3
|
-
import Logger from '@wemap/logger';
|
|
4
|
-
import SharedCameras from './SharedCameras.js';
|
|
5
|
-
import { calcDisplayedFov } from './CameraUtils.js';
|
|
6
|
-
|
|
7
|
-
// Helped from https://github.com/jeromeetienne/AR.js/blob/master/three.js/src/threex/threex-artoolkitsource.js
|
|
8
|
-
|
|
9
|
-
// Camera preview will fill component bounds without transformations.
|
|
10
|
-
// If the component aspect ratio is different from camera aspect ratio,
|
|
11
|
-
// camera preview is extended then cropped.
|
|
12
|
-
|
|
13
|
-
type CameraOptions = {
|
|
14
|
-
width?: number,
|
|
15
|
-
height?: number,
|
|
16
|
-
resizeOnWindowChange?: boolean
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type CameraState = 'stopped' | 'starting' | 'started' | 'stopping';
|
|
20
|
-
|
|
21
|
-
declare interface Camera {
|
|
22
|
-
on(event: 'starting', listener: () => void): this;
|
|
23
|
-
on(event: 'started', listener: (obj: ({ videoElement: HTMLVideoElement, stream: MediaStream })) => void): this;
|
|
24
|
-
on(event: 'stopping', listener: () => void): this;
|
|
25
|
-
on(event: 'stopped', listener: () => void): this;
|
|
26
|
-
on(event: 'fov.changed', listener: (obj: ({ vertical: number, horizontal: number })) => void): this;
|
|
27
|
-
off(event: 'starting', listener: () => void): this;
|
|
28
|
-
off(event: 'started', listener: (obj: ({ videoElement: HTMLVideoElement, stream: MediaStream })) => void): this;
|
|
29
|
-
off(event: 'stopping', listener: () => void): this;
|
|
30
|
-
off(event: 'stopped', listener: () => void): this;
|
|
31
|
-
off(event: 'fov.changed', listener: (obj: ({ vertical: number, horizontal: number })) => void): this;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Camera extends EventEmitter {
|
|
36
|
-
|
|
37
|
-
static DEFAULT_OPTIONS = {
|
|
38
|
-
width: 1024,
|
|
39
|
-
height: 768,
|
|
40
|
-
resizeOnWindowChange: false
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
static GENERIC_HARDWARE_VERTICAL_FOV = 60;
|
|
44
|
-
|
|
45
|
-
_userMediaConstraints: MediaStreamConstraints;
|
|
46
|
-
videoStream: MediaStream | null = null;
|
|
47
|
-
|
|
48
|
-
videoContainer: HTMLElement;
|
|
49
|
-
videoElement: HTMLVideoElement;
|
|
50
|
-
|
|
51
|
-
fov: {horizontal: number, vertical: number} | null = null;
|
|
52
|
-
|
|
53
|
-
_state: CameraState = 'stopped';
|
|
54
|
-
|
|
55
|
-
_hardwareVerticalFov = Camera.GENERIC_HARDWARE_VERTICAL_FOV;
|
|
56
|
-
_resizeOnWindowChange;
|
|
57
|
-
|
|
58
|
-
constructor(container: HTMLElement, options: CameraOptions = {}) {
|
|
59
|
-
super();
|
|
60
|
-
|
|
61
|
-
options = Object.assign({}, Camera.DEFAULT_OPTIONS, options);
|
|
62
|
-
|
|
63
|
-
SharedCameras._add(this, container);
|
|
64
|
-
|
|
65
|
-
this.videoContainer = container;
|
|
66
|
-
|
|
67
|
-
this._resizeOnWindowChange = options.resizeOnWindowChange;
|
|
68
|
-
this._userMediaConstraints = {
|
|
69
|
-
audio: false,
|
|
70
|
-
video: {
|
|
71
|
-
facingMode: 'environment',
|
|
72
|
-
width: { ideal: options.width },
|
|
73
|
-
height: { ideal: options.height },
|
|
74
|
-
resizeMode: 'none'
|
|
75
|
-
} as any // For resizeMode
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
this.videoContainer.style.overflow = 'hidden';
|
|
79
|
-
|
|
80
|
-
this.videoElement = document.createElement('video');
|
|
81
|
-
this.videoElement.setAttribute('id', 'wemap-camera');
|
|
82
|
-
this.videoElement.setAttribute('preload', 'none');
|
|
83
|
-
this.videoElement.setAttribute('muted', '');
|
|
84
|
-
this.videoElement.setAttribute('playsinline', '');
|
|
85
|
-
this.videoContainer.appendChild(this.videoElement);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async start(videoMediaConstraints?: MediaTrackConstraints) {
|
|
89
|
-
|
|
90
|
-
if (this._state === 'started' || this._state === 'starting') {
|
|
91
|
-
throw new Error('Camera is already started');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (this._state === 'stopping') {
|
|
95
|
-
await new Promise(resolve => this.once('stopped', resolve));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
this._state = 'starting';
|
|
99
|
-
this.emit('starting');
|
|
100
|
-
|
|
101
|
-
await Camera.checkAvailability();
|
|
102
|
-
|
|
103
|
-
if (typeof videoMediaConstraints === 'object') {
|
|
104
|
-
Object.assign(this._userMediaConstraints.video as MediaTrackConstraints,
|
|
105
|
-
this._userMediaConstraints.video as MediaTrackConstraints, videoMediaConstraints);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const stream = await navigator.mediaDevices.getUserMedia(this._userMediaConstraints);
|
|
109
|
-
|
|
110
|
-
// Listeners have to be set before srcObject assigment or they can be never called
|
|
111
|
-
this.videoElement.oncanplaythrough = () => this.videoElement.play();
|
|
112
|
-
const metadataPr = new Promise(resolve => (this.videoElement.onloadedmetadata = resolve));
|
|
113
|
-
|
|
114
|
-
this.videoElement.srcObject = this.videoStream = stream;
|
|
115
|
-
await metadataPr;
|
|
116
|
-
|
|
117
|
-
this._resizeElement();
|
|
118
|
-
this._computeFov();
|
|
119
|
-
|
|
120
|
-
if (this._resizeOnWindowChange) {
|
|
121
|
-
window.addEventListener('resize', this.notifyContainerSizeChanged);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
this._state = 'started';
|
|
125
|
-
const output = {
|
|
126
|
-
videoElement: this.videoElement,
|
|
127
|
-
stream
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
this.emit('started', output);
|
|
131
|
-
return output;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async stop() {
|
|
135
|
-
|
|
136
|
-
if (this._state === 'stopped') {
|
|
137
|
-
throw new Error('Camera is already stopped');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (this._state === 'starting') {
|
|
141
|
-
await new Promise(resolve => this.once('started', resolve));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this._state = 'stopping';
|
|
145
|
-
this.emit('stopping');
|
|
146
|
-
|
|
147
|
-
if (this.videoStream) {
|
|
148
|
-
this.videoStream.getVideoTracks().forEach(track => track.stop());
|
|
149
|
-
}
|
|
150
|
-
this.videoStream = null;
|
|
151
|
-
this.videoElement.srcObject = null;
|
|
152
|
-
|
|
153
|
-
if (this._resizeOnWindowChange) {
|
|
154
|
-
window.removeEventListener('resize', this.notifyContainerSizeChanged);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
this._state = 'stopped';
|
|
158
|
-
this.emit('stopped');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
release() {
|
|
162
|
-
this.videoContainer.removeChild(this.videoElement);
|
|
163
|
-
SharedCameras._remove(this);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
static async checkAvailability(testUserMedia = false) {
|
|
167
|
-
|
|
168
|
-
if (!navigator.mediaDevices) {
|
|
169
|
-
throw new Error('navigator.mediaDevices not present in your browser');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (!navigator.mediaDevices.enumerateDevices) {
|
|
173
|
-
throw new Error('navigator.mediaDevices.enumerateDevices not present in your browser');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (!navigator.mediaDevices.getUserMedia) {
|
|
177
|
-
throw new Error('navigator.mediaDevices.getUserMedia not present in your browser');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
181
|
-
if (!devices.find(device => device.kind === 'videoinput')) {
|
|
182
|
-
throw new Error('No camera found');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (testUserMedia) {
|
|
186
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
187
|
-
audio: false, video: { facingMode: 'environment' }
|
|
188
|
-
});
|
|
189
|
-
stream.getVideoTracks().forEach(track => track.stop());
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
get state() {
|
|
194
|
-
return this._state;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
get hardwareVerticalFov() {
|
|
198
|
-
return this._hardwareVerticalFov;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
set hardwareVerticalFov(hardwareVerticalFov) {
|
|
202
|
-
this._hardwareVerticalFov = hardwareVerticalFov;
|
|
203
|
-
this._computeFov();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
notifyContainerSizeChanged = () => {
|
|
207
|
-
this._resizeElement();
|
|
208
|
-
this._computeFov();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
_resizeElement = () => {
|
|
212
|
-
|
|
213
|
-
if (!this.videoElement) {
|
|
214
|
-
throw new Error('No video element found');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const sourceWidth = this.videoElement.videoWidth;
|
|
218
|
-
const sourceHeight = this.videoElement.videoHeight;
|
|
219
|
-
|
|
220
|
-
const containerWidth = this.videoContainer.offsetWidth;
|
|
221
|
-
const containerHeight = this.videoContainer.offsetHeight;
|
|
222
|
-
|
|
223
|
-
const sourceAspect = sourceWidth / sourceHeight;
|
|
224
|
-
const screenAspect = containerWidth / containerHeight;
|
|
225
|
-
|
|
226
|
-
if (screenAspect < sourceAspect) {
|
|
227
|
-
|
|
228
|
-
const newWidth = sourceAspect * containerHeight;
|
|
229
|
-
this.videoElement.style.width = newWidth + 'px';
|
|
230
|
-
this.videoElement.style.marginLeft = -(newWidth - containerWidth) / 2 + 'px';
|
|
231
|
-
|
|
232
|
-
this.videoElement.style.height = containerHeight + 'px';
|
|
233
|
-
this.videoElement.style.marginTop = '0px';
|
|
234
|
-
|
|
235
|
-
} else {
|
|
236
|
-
|
|
237
|
-
const newHeight = 1 / (sourceAspect / containerWidth);
|
|
238
|
-
this.videoElement.style.height = newHeight + 'px';
|
|
239
|
-
this.videoElement.style.marginTop = -(newHeight - containerHeight) / 2 + 'px';
|
|
240
|
-
|
|
241
|
-
this.videoElement.style.width = containerWidth + 'px';
|
|
242
|
-
this.videoElement.style.marginLeft = '0px';
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
_computeFov = () => {
|
|
248
|
-
|
|
249
|
-
if (['stopping', 'stopped'].includes(this.state)) {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
this.fov = calcDisplayedFov(
|
|
254
|
-
this.videoContainer,
|
|
255
|
-
this.videoElement,
|
|
256
|
-
this._hardwareVerticalFov
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
this.emit('fov.changed', this.fov);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
get currentImage() {
|
|
263
|
-
|
|
264
|
-
if (this._state !== 'started') {
|
|
265
|
-
Logger.warn('Trying to access image but video is not started');
|
|
266
|
-
return Promise.resolve(null);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const { videoWidth: width, videoHeight: height } = this.videoElement;
|
|
270
|
-
const canvas = document.createElement('canvas');
|
|
271
|
-
const context = canvas.getContext('2d');
|
|
272
|
-
// context.filter = 'grayscale(100%)';
|
|
273
|
-
|
|
274
|
-
canvas.width = width;
|
|
275
|
-
canvas.height = height;
|
|
276
|
-
|
|
277
|
-
context?.drawImage(this.videoElement, 0, 0, width, height);
|
|
278
|
-
return Promise.resolve(canvas);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export default Camera;
|
package/src/CameraUtils.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
export type IntrinsicParams = [number, number, number, number, number, number, number, number, number];
|
|
2
|
-
export type DistortionsParams = [number, number, number, number, number];
|
|
3
|
-
|
|
4
|
-
export type Calibration = {
|
|
5
|
-
intrinsic: IntrinsicParams,
|
|
6
|
-
distortions?: DistortionsParams
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function convertFov(angle: number, aspectRatio: number) {
|
|
10
|
-
return 2 * Math.atan(Math.tan((angle / 180 * Math.PI) / 2) * aspectRatio) * 180 / Math.PI;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function calcDisplayedFov(
|
|
14
|
-
videoContainer: HTMLElement,
|
|
15
|
-
videoElement: HTMLVideoElement,
|
|
16
|
-
hardwareVerticalFov: number
|
|
17
|
-
) {
|
|
18
|
-
|
|
19
|
-
const sourceWidth = videoElement.videoWidth;
|
|
20
|
-
const sourceHeight = videoElement.videoHeight;
|
|
21
|
-
|
|
22
|
-
if (!sourceWidth || !sourceHeight) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const containerWidth = videoContainer.offsetWidth;
|
|
27
|
-
const containerHeight = videoContainer.offsetHeight;
|
|
28
|
-
|
|
29
|
-
const sourceAspect = sourceWidth / sourceHeight;
|
|
30
|
-
const screenAspect = containerWidth / containerHeight;
|
|
31
|
-
|
|
32
|
-
let fovV = hardwareVerticalFov;
|
|
33
|
-
let fovH = convertFov(fovV, sourceAspect);
|
|
34
|
-
|
|
35
|
-
if (screenAspect < sourceAspect) {
|
|
36
|
-
fovH = convertFov(fovV, screenAspect);
|
|
37
|
-
} else {
|
|
38
|
-
fovV = convertFov(fovH, 1 / screenAspect);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
vertical: fovV,
|
|
43
|
-
horizontal: fovH
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
|
|
49
|
-
*/
|
|
50
|
-
export function canvasToBase64(canvas: HTMLCanvasElement, type = 'image/png', quality: any = null) {
|
|
51
|
-
return canvas
|
|
52
|
-
.toDataURL(type, quality)
|
|
53
|
-
.substring(('data:' + type + ';base64,').length);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function base64ToCanvas(base64: string, type = 'image/png') {
|
|
57
|
-
if (type !== 'image/png') {
|
|
58
|
-
throw new Error('Only image/png is supported');
|
|
59
|
-
}
|
|
60
|
-
const canvas = document.createElement('canvas');
|
|
61
|
-
const image = new Image();
|
|
62
|
-
image.src = 'data:image/png;base64,' + base64;
|
|
63
|
-
canvas.width = image.width;
|
|
64
|
-
canvas.height = image.height;
|
|
65
|
-
canvas.getContext('2d')?.drawImage(image, 0, 0);
|
|
66
|
-
return canvas;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Creates a pinhole camera from dimensions and a field of view
|
|
71
|
-
* Distortions are not considered
|
|
72
|
-
*
|
|
73
|
-
* @param {number} width the camera width
|
|
74
|
-
* @param {number} height the camera height
|
|
75
|
-
* @param {number} fovOfWidth the field of view along the width axis in radians
|
|
76
|
-
* @returns {object} the calibration matrix
|
|
77
|
-
*/
|
|
78
|
-
export function createCameraCalibrationFromWidthHeightFov(
|
|
79
|
-
width: number,
|
|
80
|
-
height: number,
|
|
81
|
-
fovOfWidth: number
|
|
82
|
-
): Calibration {
|
|
83
|
-
|
|
84
|
-
const fovOfHeight = 2 * Math.atan(height * Math.tan(fovOfWidth / 2) / width);
|
|
85
|
-
|
|
86
|
-
const fx = width / (2 * Math.tan(0.5 * fovOfWidth));
|
|
87
|
-
const fy = height / (2 * Math.tan(0.5 * fovOfHeight));
|
|
88
|
-
const cx = width / 2;
|
|
89
|
-
const cy = height / 2;
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
intrinsic: [fx, 0, cx, 0, fy, cy, 0, 0, 1],
|
|
93
|
-
distortions: [0, 0, 0, 0, 0]
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @param {HTMLCanvasElement} imageCanvas
|
|
99
|
-
*/
|
|
100
|
-
export function convertToGrayscale(imageCanvas: HTMLCanvasElement) {
|
|
101
|
-
const ctx = imageCanvas.getContext('2d');
|
|
102
|
-
if (!ctx) return;
|
|
103
|
-
|
|
104
|
-
const imgData = ctx.getImageData(0, 0, imageCanvas.width, imageCanvas.height);
|
|
105
|
-
const pixels = imgData.data;
|
|
106
|
-
for (let i = 0; i < pixels.length; i += 4) {
|
|
107
|
-
|
|
108
|
-
const lightness = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
|
|
109
|
-
pixels[i] = lightness;
|
|
110
|
-
pixels[i + 1] = lightness;
|
|
111
|
-
pixels[i + 2] = lightness;
|
|
112
|
-
}
|
|
113
|
-
ctx.putImageData(imgData, 0, 0);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Reduce image size and keep aspect ratio
|
|
118
|
-
*/
|
|
119
|
-
export function reduceImageSize(
|
|
120
|
-
imageCanvas: HTMLCanvasElement,
|
|
121
|
-
maxWidth: number,
|
|
122
|
-
maxHeight?: number
|
|
123
|
-
) {
|
|
124
|
-
|
|
125
|
-
let newWidth = imageCanvas.width;
|
|
126
|
-
let newHeight = imageCanvas.height;
|
|
127
|
-
|
|
128
|
-
if (typeof maxHeight !== 'undefined') {
|
|
129
|
-
if (newWidth > maxWidth) {
|
|
130
|
-
newHeight *= maxWidth / newWidth;
|
|
131
|
-
newWidth = maxWidth;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (newHeight > maxHeight) {
|
|
135
|
-
newWidth *= maxHeight / newHeight;
|
|
136
|
-
newHeight = maxHeight;
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
const aspectRatio = imageCanvas.width / imageCanvas.height;
|
|
140
|
-
if (imageCanvas.width > imageCanvas.height) {
|
|
141
|
-
if (imageCanvas.width > maxWidth) {
|
|
142
|
-
newWidth = maxWidth;
|
|
143
|
-
newHeight = maxWidth / aspectRatio;
|
|
144
|
-
}
|
|
145
|
-
} else if (imageCanvas.height > maxWidth) {
|
|
146
|
-
newHeight = maxWidth;
|
|
147
|
-
newWidth = maxWidth * aspectRatio;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (newWidth === imageCanvas.width
|
|
153
|
-
&& newHeight === imageCanvas.height) {
|
|
154
|
-
return imageCanvas;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const newCanvas = document.createElement('canvas');
|
|
158
|
-
const newContext = newCanvas.getContext('2d');
|
|
159
|
-
|
|
160
|
-
newCanvas.width = newWidth;
|
|
161
|
-
newCanvas.height = newHeight;
|
|
162
|
-
newContext?.drawImage(imageCanvas, 0, 0, newCanvas.width, newCanvas.height);
|
|
163
|
-
|
|
164
|
-
return newCanvas;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function htmlImageToCanvas(image: HTMLImageElement) {
|
|
168
|
-
const canvas = document.createElement('canvas');
|
|
169
|
-
canvas.width = image.width;
|
|
170
|
-
canvas.height = image.height;
|
|
171
|
-
const context = canvas.getContext('2d');
|
|
172
|
-
context?.scale(canvas.width / image.width, canvas.height / image.height);
|
|
173
|
-
context?.drawImage(image, 0, 0);
|
|
174
|
-
return canvas;
|
|
175
|
-
}
|
package/src/QrCodeScanner.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import EventEmitter from 'events';
|
|
2
|
-
import QrScanner from 'qr-scanner';
|
|
3
|
-
|
|
4
|
-
import Camera from './Camera.js';
|
|
5
|
-
|
|
6
|
-
declare interface QrCodeScanner {
|
|
7
|
-
on(event: 'scan', listener: (result: string) => void): this;
|
|
8
|
-
off(event: 'scan', listener: (result: string) => void): this;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
class QrCodeScanner extends EventEmitter {
|
|
12
|
-
_height?: number;
|
|
13
|
-
_width?: number;
|
|
14
|
-
_camera: Camera;
|
|
15
|
-
_videoElement: HTMLVideoElement;
|
|
16
|
-
_canvas: HTMLCanvasElement;
|
|
17
|
-
_ctx: CanvasRenderingContext2D;
|
|
18
|
-
_intervalTime: number | null = null;
|
|
19
|
-
_intervalTick?: number;
|
|
20
|
-
_isStarted = false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
constructor(camera: Camera) {
|
|
24
|
-
super();
|
|
25
|
-
|
|
26
|
-
this._camera = camera;
|
|
27
|
-
this._videoElement = camera.videoElement;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this._canvas = document.createElement('canvas');
|
|
31
|
-
this._ctx = this._canvas.getContext('2d') as CanvasRenderingContext2D;
|
|
32
|
-
|
|
33
|
-
this._camera.on('started', () => this._tryStart());
|
|
34
|
-
this._camera.on('stopped', () => this._stop());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
start(intervalTime = 500) {
|
|
38
|
-
this._intervalTime = intervalTime;
|
|
39
|
-
|
|
40
|
-
if (this._isStarted) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
this._isStarted = true;
|
|
44
|
-
|
|
45
|
-
this._tryStart();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
stop() {
|
|
49
|
-
this._isStarted = false;
|
|
50
|
-
this._stop();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
_tryStart() {
|
|
54
|
-
|
|
55
|
-
if (!this._isStarted || this._camera.state !== 'started') {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
this._width = this._videoElement.videoWidth;
|
|
60
|
-
this._height = this._videoElement.videoHeight;
|
|
61
|
-
this._canvas.width = this._width;
|
|
62
|
-
this._canvas.height = this._height;
|
|
63
|
-
|
|
64
|
-
this._intervalTick = window.setInterval(() => this._onFrame(), this._intervalTime as number);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
_stop() {
|
|
68
|
-
clearInterval(this._intervalTick);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
_onFrame() {
|
|
72
|
-
|
|
73
|
-
if (!this._width || !this._height) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
this._ctx.drawImage(this._videoElement, 0, 0, this._width, this._height);
|
|
78
|
-
const scanResultPromise = QrScanner.scanImage(this._canvas, {
|
|
79
|
-
returnDetailedScanResult: true,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
scanResultPromise.then((result) => {
|
|
83
|
-
this.emit('scan', result.data);
|
|
84
|
-
}).catch((e) => {
|
|
85
|
-
// do nothing
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export default QrCodeScanner;
|
package/src/SharedCameras.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import EventEmitter from 'events';
|
|
2
|
-
|
|
3
|
-
import Camera from './Camera.js';
|
|
4
|
-
|
|
5
|
-
declare interface SharedCameras {
|
|
6
|
-
on(event: 'added', listener: (obj: CameraContainer) => void): this;
|
|
7
|
-
on(event: 'removed', listener: (obj: { camera: Camera }) => void): this;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type CameraContainer = { container: HTMLElement, camera: Camera };
|
|
11
|
-
|
|
12
|
-
class SharedCameras extends EventEmitter {
|
|
13
|
-
|
|
14
|
-
_list: CameraContainer[] = [];
|
|
15
|
-
|
|
16
|
-
_add(camera: Camera, container: HTMLElement) {
|
|
17
|
-
const obj = {
|
|
18
|
-
camera,
|
|
19
|
-
container
|
|
20
|
-
};
|
|
21
|
-
this._list.push(obj);
|
|
22
|
-
this.emit('added', obj);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
_remove(camera: Camera) {
|
|
26
|
-
this._list = this._list.filter(({ camera: _camera }) => _camera !== camera);
|
|
27
|
-
this.emit('removed', { camera });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
get list() {
|
|
31
|
-
return this._list;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
getCameraByContainer(container: HTMLElement) {
|
|
35
|
-
return this._list.find(obj => obj.container === container)?.camera || null;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default new SharedCameras();
|
package/tsconfig.json
DELETED