@gueshbzh-perso/scanner 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +23 -4
  3. package/dist/camera.d.ts.map +1 -1
  4. package/dist/camera.js +5 -2
  5. package/dist/camera.js.map +1 -1
  6. package/dist/core.d.ts +1 -3
  7. package/dist/core.d.ts.map +1 -1
  8. package/dist/core.js +2 -16
  9. package/dist/core.js.map +1 -1
  10. package/dist/engine.d.ts +16 -2
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +34 -18
  13. package/dist/engine.js.map +1 -1
  14. package/dist/grabber.d.ts +28 -0
  15. package/dist/grabber.d.ts.map +1 -1
  16. package/dist/grabber.js +81 -3
  17. package/dist/grabber.js.map +1 -1
  18. package/dist/index.d.ts +0 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/overlay.d.ts.map +1 -1
  23. package/dist/overlay.js +12 -11
  24. package/dist/overlay.js.map +1 -1
  25. package/dist/reader.d.ts +8 -1
  26. package/dist/reader.d.ts.map +1 -1
  27. package/dist/reader.js +43 -7
  28. package/dist/reader.js.map +1 -1
  29. package/dist/types.d.ts +1 -3
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/wasm/THIRD_PARTY_LICENSES.md +3879 -0
  32. package/dist/wasm/scanner_wasm_bg.wasm +0 -0
  33. package/package.json +6 -3
  34. package/src/camera.ts +107 -0
  35. package/src/core.ts +189 -0
  36. package/src/engine.ts +348 -0
  37. package/src/feedback.ts +50 -0
  38. package/src/geometry.ts +55 -0
  39. package/src/grabber.ts +385 -0
  40. package/src/index.ts +20 -0
  41. package/src/overlay.ts +225 -0
  42. package/src/protocol.ts +38 -0
  43. package/src/reader.ts +474 -0
  44. package/src/scan-image.ts +88 -0
  45. package/src/types.ts +155 -0
  46. package/src/worker.ts +57 -0
  47. package/dist/wasm/scanner_wasm_simd_bg.wasm +0 -0
@@ -0,0 +1,50 @@
1
+ // Audio / haptic feedback on detection.
2
+ import type { FeedbackOptions } from './types.js';
3
+
4
+ export class Feedback {
5
+ private audio: AudioContext | null = null;
6
+
7
+ constructor(private options: FeedbackOptions | false) {}
8
+
9
+ /** Must be called from a user gesture on iOS for audio to be allowed. */
10
+ unlock(): void {
11
+ if (!this.options || this.options.beep === false || this.audio) return;
12
+ const Ctx =
13
+ window.AudioContext ??
14
+ (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
15
+ if (!Ctx) return;
16
+ try {
17
+ this.audio = new Ctx();
18
+ void this.audio.resume();
19
+ } catch {
20
+ this.audio = null;
21
+ }
22
+ }
23
+
24
+ play(): void {
25
+ if (!this.options) return;
26
+ if (this.options.vibrate !== false) navigator.vibrate?.(60);
27
+ if (this.options.beep !== false) this.beep();
28
+ }
29
+
30
+ private beep(): void {
31
+ const ctx = this.audio;
32
+ if (!ctx || ctx.state !== 'running') return;
33
+ const t = ctx.currentTime;
34
+ const osc = ctx.createOscillator();
35
+ const gain = ctx.createGain();
36
+ osc.type = 'sine';
37
+ osc.frequency.value = 2400;
38
+ gain.gain.setValueAtTime(0.0001, t);
39
+ gain.gain.exponentialRampToValueAtTime(0.25, t + 0.005);
40
+ gain.gain.exponentialRampToValueAtTime(0.0001, t + 0.09);
41
+ osc.connect(gain).connect(ctx.destination);
42
+ osc.start(t);
43
+ osc.stop(t + 0.1);
44
+ }
45
+
46
+ destroy(): void {
47
+ void this.audio?.close();
48
+ this.audio = null;
49
+ }
50
+ }
@@ -0,0 +1,55 @@
1
+ // Coordinate mapping between the host element (what the user sees), the video
2
+ // frame, and the downscaled image handed to the decoder.
3
+ import type { Rect } from './grabber.js';
4
+ import type { Point, Region } from './types.js';
5
+
6
+ /** How a video of `vw×vh` is laid out in an element of `ew×eh` with `object-fit: cover`. */
7
+ export interface CoverLayout {
8
+ scale: number;
9
+ offsetX: number;
10
+ offsetY: number;
11
+ }
12
+
13
+ export function coverLayout(vw: number, vh: number, ew: number, eh: number): CoverLayout {
14
+ const scale = Math.max(ew / vw, eh / vh);
15
+ return { scale, offsetX: (ew - vw * scale) / 2, offsetY: (eh - vh * scale) / 2 };
16
+ }
17
+
18
+ export function videoToElement(p: Point, l: CoverLayout): Point {
19
+ return { x: p.x * l.scale + l.offsetX, y: p.y * l.scale + l.offsetY };
20
+ }
21
+
22
+ /** Region of interest (fractions of the element) → rectangle in video pixels, clamped to the frame. */
23
+ export function regionToVideoRect(r: Region, vw: number, vh: number, ew: number, eh: number): Rect {
24
+ const l = coverLayout(vw, vh, ew, eh);
25
+ const x0 = clamp((r.x * ew - l.offsetX) / l.scale, 0, vw);
26
+ const y0 = clamp((r.y * eh - l.offsetY) / l.scale, 0, vh);
27
+ const x1 = clamp(((r.x + r.width) * ew - l.offsetX) / l.scale, 0, vw);
28
+ const y1 = clamp(((r.y + r.height) * eh - l.offsetY) / l.scale, 0, vh);
29
+ return { x: x0, y: y0, width: Math.max(1, x1 - x0), height: Math.max(1, y1 - y0) };
30
+ }
31
+
32
+ /** Size of the decoder image for a crop: downscaled to `maxDim`, width rounded to a multiple of 4. */
33
+ export function outputSize(rect: Rect, maxDim: number): { width: number; height: number } {
34
+ const scale = Math.min(1, maxDim / Math.max(rect.width, rect.height));
35
+ const width = Math.max(4, Math.floor((rect.width * scale) / 4) * 4);
36
+ const height = Math.max(1, Math.round(rect.height * scale));
37
+ return { width, height };
38
+ }
39
+
40
+ /** Map points from decoder-image space back to video pixels. */
41
+ export function decoderToVideo(points: Point[], rect: Rect, width: number, height: number): Point[] {
42
+ const sx = rect.width / width;
43
+ const sy = rect.height / height;
44
+ return points.map((p) => ({ x: rect.x + (p.x + 0.5) * sx, y: rect.y + (p.y + 0.5) * sy }));
45
+ }
46
+
47
+ export function clampRegion(r: Region): Region {
48
+ const x = clamp(r.x, 0, 1);
49
+ const y = clamp(r.y, 0, 1);
50
+ return { x, y, width: clamp(r.width, 0.01, 1 - x), height: clamp(r.height, 0.01, 1 - y) };
51
+ }
52
+
53
+ function clamp(v: number, lo: number, hi: number): number {
54
+ return Math.min(hi, Math.max(lo, v));
55
+ }
package/src/grabber.ts ADDED
@@ -0,0 +1,385 @@
1
+ // Frame pipeline: turns a video frame into the small grayscale image the decoder needs.
2
+ //
3
+ // The WebGL2 path does all the per-pixel work on the GPU in a single pass:
4
+ // upload video texture → crop to the region of interest → RGB→luma
5
+ // → box-filtered downscale → pack 4 luma pixels per RGBA texel
6
+ // so the CPU readback is exactly `width × height` bytes (4× less than
7
+ // getImageData) and, thanks to a pixel-pack buffer + fence, it does not
8
+ // stall the main thread waiting for the GPU.
9
+
10
+ export interface Rect {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface FrameGrabber {
18
+ readonly kind: 'webgl' | 'canvas';
19
+ /**
20
+ * Render `src` (cropped to `rect`, in source pixels) into `out`, a luma
21
+ * buffer of `width × height` bytes. `width` must be a multiple of 4.
22
+ */
23
+ grab(source: TexImageSource, rect: Rect, width: number, height: number, out: Uint8Array): Promise<void>;
24
+ destroy(): void;
25
+ }
26
+
27
+ type AnyCanvas = HTMLCanvasElement | OffscreenCanvas;
28
+
29
+ function makeCanvas(): AnyCanvas {
30
+ return typeof OffscreenCanvas !== 'undefined'
31
+ ? new OffscreenCanvas(4, 4)
32
+ : document.createElement('canvas');
33
+ }
34
+
35
+ const VERT = `#version 300 es
36
+ in vec2 a_pos;
37
+ void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }`;
38
+
39
+ const FRAG = `#version 300 es
40
+ precision highp float;
41
+ uniform sampler2D u_src;
42
+ uniform vec4 u_rect; // crop rectangle in texture coordinates (origin top-left)
43
+ uniform vec2 u_out; // size of the luma image
44
+ uniform vec2 u_tap; // sub-sample offset for the 2x2 box filter
45
+ out vec4 o_color;
46
+ const vec3 LUMA = vec3(0.299, 0.587, 0.114);
47
+
48
+ float luma(vec2 px) {
49
+ vec2 uv = u_rect.xy + (px + 0.5) / u_out * u_rect.zw;
50
+ vec3 c = texture(u_src, uv + vec2(-u_tap.x, -u_tap.y)).rgb
51
+ + texture(u_src, uv + vec2( u_tap.x, -u_tap.y)).rgb
52
+ + texture(u_src, uv + vec2(-u_tap.x, u_tap.y)).rgb
53
+ + texture(u_src, uv + vec2( u_tap.x, u_tap.y)).rgb;
54
+ return dot(c * 0.25, LUMA);
55
+ }
56
+
57
+ void main() {
58
+ // Framebuffer row 0 is read back first, so row r samples image row r from the top.
59
+ float y = floor(gl_FragCoord.y);
60
+ float x = floor(gl_FragCoord.x) * 4.0;
61
+ o_color = vec4(luma(vec2(x, y)), luma(vec2(x + 1.0, y)), luma(vec2(x + 2.0, y)), luma(vec2(x + 3.0, y)));
62
+ }`;
63
+
64
+ export class WebGLGrabber implements FrameGrabber {
65
+ readonly kind = 'webgl';
66
+ private gl: WebGL2RenderingContext;
67
+ private program: WebGLProgram;
68
+ private srcTex: WebGLTexture;
69
+ private dstTex: WebGLTexture;
70
+ private fbo: WebGLFramebuffer;
71
+ private pbo: WebGLBuffer;
72
+ private uniforms: Record<'rect' | 'out' | 'tap', WebGLUniformLocation | null>;
73
+ private size = { w: 0, h: 0 };
74
+ private lost = false;
75
+
76
+ /** Returns `null` when WebGL2 is not available. */
77
+ static create(): WebGLGrabber | null {
78
+ try {
79
+ const canvas = makeCanvas();
80
+ const gl = canvas.getContext('webgl2', {
81
+ alpha: true,
82
+ antialias: false,
83
+ depth: false,
84
+ stencil: false,
85
+ premultipliedAlpha: false,
86
+ preserveDrawingBuffer: false,
87
+ powerPreference: 'low-power',
88
+ }) as WebGL2RenderingContext | null;
89
+ return gl ? new WebGLGrabber(canvas, gl) : null;
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+
95
+ private constructor(canvas: AnyCanvas, gl: WebGL2RenderingContext) {
96
+ this.gl = gl;
97
+ (canvas as HTMLCanvasElement).addEventListener?.('webglcontextlost', (e) => {
98
+ e.preventDefault();
99
+ this.lost = true;
100
+ });
101
+
102
+ this.program = link(gl, VERT, FRAG);
103
+ gl.useProgram(this.program);
104
+
105
+ // Full-screen triangle.
106
+ const vbo = gl.createBuffer();
107
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
108
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
109
+ const loc = gl.getAttribLocation(this.program, 'a_pos');
110
+ gl.enableVertexAttribArray(loc);
111
+ gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
112
+
113
+ this.uniforms = {
114
+ rect: gl.getUniformLocation(this.program, 'u_rect'),
115
+ out: gl.getUniformLocation(this.program, 'u_out'),
116
+ tap: gl.getUniformLocation(this.program, 'u_tap'),
117
+ };
118
+ gl.uniform1i(gl.getUniformLocation(this.program, 'u_src'), 0);
119
+
120
+ this.srcTex = texture(gl);
121
+ this.dstTex = texture(gl);
122
+ this.fbo = must(gl.createFramebuffer(), 'framebuffer');
123
+ this.pbo = must(gl.createBuffer(), 'pixel buffer');
124
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
125
+ gl.pixelStorei(gl.PACK_ALIGNMENT, 1);
126
+ }
127
+
128
+ async grab(
129
+ source: TexImageSource,
130
+ rect: Rect,
131
+ width: number,
132
+ height: number,
133
+ out: Uint8Array,
134
+ ): Promise<void> {
135
+ const gl = this.gl;
136
+ if (this.lost || gl.isContextLost()) throw new Error('WebGL context lost');
137
+ const [sw, sh] = sourceSize(source);
138
+ const texW = width / 4;
139
+
140
+ gl.activeTexture(gl.TEXTURE0);
141
+ gl.bindTexture(gl.TEXTURE_2D, this.srcTex);
142
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
143
+
144
+ if (this.size.w !== texW || this.size.h !== height) {
145
+ gl.bindTexture(gl.TEXTURE_2D, this.dstTex);
146
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, texW, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
147
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
148
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, this.dstTex, 0);
149
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.pbo);
150
+ gl.bufferData(gl.PIXEL_PACK_BUFFER, width * height, gl.STREAM_READ);
151
+ this.size = { w: texW, h: height };
152
+ gl.bindTexture(gl.TEXTURE_2D, this.srcTex);
153
+ }
154
+
155
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.fbo);
156
+ gl.viewport(0, 0, texW, height);
157
+ gl.uniform4f(this.uniforms.rect, rect.x / sw, rect.y / sh, rect.width / sw, rect.height / sh);
158
+ gl.uniform2f(this.uniforms.out, width, height);
159
+ // Box filter footprint: a quarter of an output pixel, only when downscaling.
160
+ const fx = rect.width / width;
161
+ const fy = rect.height / height;
162
+ gl.uniform2f(this.uniforms.tap, fx > 1 ? (0.25 * fx) / sw : 0, fy > 1 ? (0.25 * fy) / sh : 0);
163
+ gl.drawArrays(gl.TRIANGLES, 0, 3);
164
+
165
+ // Asynchronous readback into the pixel-pack buffer.
166
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.pbo);
167
+ gl.readPixels(0, 0, texW, height, gl.RGBA, gl.UNSIGNED_BYTE, 0);
168
+ const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
169
+ gl.flush();
170
+ if (sync) {
171
+ await waitSync(gl, sync);
172
+ gl.deleteSync(sync);
173
+ }
174
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this.pbo);
175
+ gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, out, 0, width * height);
176
+ gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
177
+ }
178
+
179
+ destroy() {
180
+ const gl = this.gl;
181
+ gl.deleteTexture(this.srcTex);
182
+ gl.deleteTexture(this.dstTex);
183
+ gl.deleteFramebuffer(this.fbo);
184
+ gl.deleteBuffer(this.pbo);
185
+ gl.deleteProgram(this.program);
186
+ gl.getExtension('WEBGL_lose_context')?.loseContext();
187
+ }
188
+ }
189
+
190
+ /** Fallback pipeline using Canvas 2D (`drawImage` does the crop + scale). */
191
+ export class CanvasGrabber implements FrameGrabber {
192
+ readonly kind = 'canvas';
193
+ private canvas = makeCanvas();
194
+ private ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
195
+
196
+ constructor() {
197
+ const ctx = this.canvas.getContext('2d', { willReadFrequently: true, alpha: false });
198
+ this.ctx = must(ctx as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null, '2D context');
199
+ }
200
+
201
+ async grab(
202
+ source: TexImageSource,
203
+ rect: Rect,
204
+ width: number,
205
+ height: number,
206
+ out: Uint8Array,
207
+ ): Promise<void> {
208
+ if (this.canvas.width !== width || this.canvas.height !== height) {
209
+ this.canvas.width = width;
210
+ this.canvas.height = height;
211
+ }
212
+ const ctx = this.ctx;
213
+ ctx.imageSmoothingEnabled = true;
214
+ ctx.imageSmoothingQuality = 'medium';
215
+ ctx.drawImage(source as CanvasImageSource, rect.x, rect.y, rect.width, rect.height, 0, 0, width, height);
216
+ const rgba = ctx.getImageData(0, 0, width, height).data;
217
+ rgbaToLuma(rgba, out);
218
+ }
219
+
220
+ destroy() {
221
+ this.canvas.width = this.canvas.height = 0;
222
+ }
223
+ }
224
+
225
+ export function createGrabber(backend: 'auto' | 'webgl' | 'canvas' = 'auto'): FrameGrabber {
226
+ if (backend !== 'canvas') {
227
+ const gl = WebGLGrabber.create();
228
+ if (gl) return backend === 'auto' ? new AdaptiveGrabber(gl, () => new CanvasGrabber()) : gl;
229
+ if (backend === 'webgl') throw new Error('WebGL2 is not available');
230
+ }
231
+ return new CanvasGrabber();
232
+ }
233
+
234
+ /** Frames timed per backend before deciding. */
235
+ const PROBE_FRAMES = 15;
236
+ /** WebGL is kept without probing when its median grab is below this (ms). */
237
+ const SLOW_GRAB_MS = 20;
238
+ /** Canvas 2D must be this much faster to be kept: it blocks the main thread. */
239
+ const CANVAS_WIN_RATIO = 0.6;
240
+
241
+ /**
242
+ * `'auto'` backend: WebGL2, unless Canvas 2D is measured to be much faster on
243
+ * this device.
244
+ *
245
+ * WebGL does the crop / luma / downscale on the GPU and reads back 4× less
246
+ * data, asynchronously, so it is the right default. But on some GPUs the
247
+ * readback itself is slow (the fence takes tens of ms to signal), and then
248
+ * the grab limits the frame rate now that decoding runs in parallel. So when
249
+ * the median WebGL grab exceeds `SLOW_GRAB_MS`, Canvas 2D is timed on the
250
+ * next `PROBE_FRAMES` frames and kept only if clearly faster (`drawImage` +
251
+ * `getImageData` run on the main thread, while WebGL waits asynchronously).
252
+ * The decision is made once, from measurements: no device list.
253
+ */
254
+ export class AdaptiveGrabber implements FrameGrabber {
255
+ private active: FrameGrabber;
256
+ private other: FrameGrabber | null = null;
257
+ private samples: number[] = [];
258
+ private webglMedian = 0;
259
+ private state: 'webgl' | 'probing' | 'decided' = 'webgl';
260
+
261
+ constructor(
262
+ private webgl: FrameGrabber,
263
+ private makeCanvas: () => FrameGrabber,
264
+ private now: () => number = () => performance.now(),
265
+ ) {
266
+ this.active = webgl;
267
+ }
268
+
269
+ get kind(): 'webgl' | 'canvas' {
270
+ return this.active.kind;
271
+ }
272
+
273
+ async grab(source: TexImageSource, rect: Rect, width: number, height: number, out: Uint8Array) {
274
+ const grabber = this.active;
275
+ const t0 = this.now();
276
+ await grabber.grab(source, rect, width, height, out);
277
+ if (this.state !== 'decided' && grabber === this.active) this.record(this.now() - t0);
278
+ }
279
+
280
+ private record(ms: number): void {
281
+ this.samples.push(ms);
282
+ if (this.samples.length < PROBE_FRAMES) return;
283
+ const median = medianOf(this.samples);
284
+ this.samples = [];
285
+ if (this.state === 'webgl') {
286
+ if (median < SLOW_GRAB_MS) {
287
+ this.state = 'decided';
288
+ return;
289
+ }
290
+ this.webglMedian = median;
291
+ this.other = this.webgl;
292
+ this.active = this.makeCanvas();
293
+ this.state = 'probing';
294
+ return;
295
+ }
296
+ // Probing Canvas 2D: keep it, or go back to WebGL.
297
+ this.state = 'decided';
298
+ if (median < this.webglMedian * CANVAS_WIN_RATIO) {
299
+ this.other?.destroy();
300
+ } else {
301
+ this.active.destroy();
302
+ this.active = this.webgl;
303
+ }
304
+ this.other = null;
305
+ }
306
+
307
+ destroy(): void {
308
+ this.active.destroy();
309
+ this.other?.destroy();
310
+ }
311
+ }
312
+
313
+ function medianOf(values: number[]): number {
314
+ const sorted = [...values].sort((a, b) => a - b);
315
+ return sorted[sorted.length >> 1] ?? 0;
316
+ }
317
+
318
+ /** BT.601 luma with integer arithmetic (same weights as the GPU path and the engine). */
319
+ export function rgbaToLuma(rgba: Uint8ClampedArray | Uint8Array, out: Uint8Array): void {
320
+ const n = Math.min(out.length, rgba.length >> 2);
321
+ for (let i = 0, j = 0; i < n; i++, j += 4) {
322
+ // In bounds by construction (`n`): `?? 0` only satisfies the type checker.
323
+ out[i] = (19595 * (rgba[j] ?? 0) + 38470 * (rgba[j + 1] ?? 0) + 7471 * (rgba[j + 2] ?? 0)) >> 16;
324
+ }
325
+ }
326
+
327
+ export function sourceSize(source: TexImageSource): [number, number] {
328
+ if (typeof HTMLVideoElement !== 'undefined' && source instanceof HTMLVideoElement) {
329
+ return [source.videoWidth, source.videoHeight];
330
+ }
331
+ if (typeof HTMLImageElement !== 'undefined' && source instanceof HTMLImageElement) {
332
+ return [source.naturalWidth, source.naturalHeight];
333
+ }
334
+ if (typeof VideoFrame !== 'undefined' && source instanceof VideoFrame) {
335
+ return [source.displayWidth, source.displayHeight];
336
+ }
337
+ const s = source as { width: number; height: number };
338
+ return [s.width, s.height];
339
+ }
340
+
341
+ // ── WebGL helpers ──────────────────────────────────────────────────────────
342
+
343
+ function must<T>(v: T | null, what: string): T {
344
+ if (!v) throw new Error(`WebGL: failed to create ${what}`);
345
+ return v;
346
+ }
347
+
348
+ function texture(gl: WebGL2RenderingContext): WebGLTexture {
349
+ const t = must(gl.createTexture(), 'texture');
350
+ gl.bindTexture(gl.TEXTURE_2D, t);
351
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
352
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
353
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
354
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
355
+ return t;
356
+ }
357
+
358
+ function link(gl: WebGL2RenderingContext, vs: string, fs: string): WebGLProgram {
359
+ const compile = (type: number, src: string) => {
360
+ const s = must(gl.createShader(type), 'shader');
361
+ gl.shaderSource(s, src);
362
+ gl.compileShader(s);
363
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(`Shader: ${gl.getShaderInfoLog(s)}`);
364
+ return s;
365
+ };
366
+ const p = must(gl.createProgram(), 'program');
367
+ gl.attachShader(p, compile(gl.VERTEX_SHADER, vs));
368
+ gl.attachShader(p, compile(gl.FRAGMENT_SHADER, fs));
369
+ gl.linkProgram(p);
370
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(`Program: ${gl.getProgramInfoLog(p)}`);
371
+ return p;
372
+ }
373
+
374
+ function waitSync(gl: WebGL2RenderingContext, sync: WebGLSync): Promise<void> {
375
+ return new Promise((resolve) => {
376
+ const started = performance.now();
377
+ const poll = () => {
378
+ const status = gl.clientWaitSync(sync, 0, 0);
379
+ // Give up waiting after 50 ms: getBufferSubData will then block, but never hang.
380
+ if (status !== gl.TIMEOUT_EXPIRED || performance.now() - started > 50) resolve();
381
+ else setTimeout(poll, 1);
382
+ };
383
+ poll();
384
+ });
385
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { BarcodeReader } from './reader.js';
2
+ export { scanImage, type ImageInput, type ScanImageOptions } from './scan-image.js';
3
+ export { createEngine, type Engine, type FrameReply } from './engine.js';
4
+ export { listCameras } from './camera.js';
5
+ export type {
6
+ AssetOptions,
7
+ BarcodeFormat,
8
+ CameraOptions,
9
+ CameraResolution,
10
+ Detection,
11
+ EngineOptions,
12
+ FeedbackOptions,
13
+ OverlayOptions,
14
+ Point,
15
+ ProcessingOptions,
16
+ ReaderEvents,
17
+ ReaderOptions,
18
+ ReaderStats,
19
+ Region,
20
+ } from './types.js';