receipts/js/camera.ts

178 lines
5.2 KiB
TypeScript

import "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
import "@shoelace-style/shoelace/dist/components/tooltip/tooltip.js";
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js";
import { notifyError } from "./alert.js";
const STYLESHEET = `
.camera-input {
text-align: center;
}
.camera-input video,
.camera-input canvas {
width: 100%;
height: auto;
object-fit: contain;
}
.camera-buttons sl-icon-button {
font-size: 3em;
margin: 0 0.5em;
}
.hidden {
display: none;
}
`;
export default class CameraInput extends HTMLElement {
elmVideo: HTMLVideoElement;
btnShutter: SlIconButton;
btnClear: SlIconButton;
constructor() {
super();
}
connectedCallback() {
this.innerHTML = "";
const shadow = this.attachShadow({ mode: "open" });
shadow.append(
Object.assign(document.createElement("style"), {
textContent: STYLESHEET,
}),
);
const wrapper = document.createElement("div");
wrapper.className = "camera-input";
shadow.appendChild(wrapper);
this.elmVideo = Object.assign(document.createElement("video"), {
className: "hidden",
});
this.elmVideo.addEventListener("canplay", () => {
this.btnShutter.disabled = false;
});
wrapper.appendChild(this.elmVideo);
const buttons = document.createElement("div");
buttons.classList.add("camera-buttons");
wrapper.appendChild(buttons);
this.btnShutter = Object.assign(
document.createElement("sl-icon-button"),
{
name: "camera",
label: "Take Photo",
disabled: true,
},
);
this.btnShutter.addEventListener("click", () => {
this.takePhoto();
});
const ttShutter = Object.assign(document.createElement("sl-tooltip"), {
content: "Take Photo",
});
ttShutter.appendChild(this.btnShutter);
buttons.appendChild(ttShutter);
this.btnClear = Object.assign(
document.createElement("sl-icon-button"),
{
name: "trash",
label: "Start Over",
disabled: true,
className: "hidden",
},
);
this.btnClear.addEventListener("click", () => {
this.clearCamera();
this.startCamera();
});
const ttClear = Object.assign(document.createElement("sl-tooltip"), {
content: "Start Over",
});
ttClear.appendChild(this.btnClear);
buttons.appendChild(ttClear);
}
public async getBlob(): Promise<Blob | null> {
const canvas = this.shadowRoot!.querySelector("canvas");
return await new Promise((resolve) => {
if (canvas) {
canvas.toBlob((blob) => {
resolve(blob);
}, "image/jpeg");
} else {
resolve(null);
}
});
}
async clearCamera() {
this.elmVideo.pause();
this.elmVideo.srcObject = null;
this.elmVideo.classList.add("hidden");
this.elmVideo.parentNode
?.querySelectorAll("canvas")
.forEach((e) => e.remove());
this.btnShutter.disabled = true;
this.btnShutter.classList.add("hidden");
this.btnClear.disabled = true;
this.btnClear.classList.add("hidden");
this.sendReady(false);
}
sendReady(hasPhoto: boolean) {
this.dispatchEvent(new CustomEvent("ready", { detail: { hasPhoto } }));
}
async startCamera() {
let stream: MediaStream;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: {
ideal: "environment",
},
width: { ideal: 1280 },
height: { ideal: 720 },
},
audio: false,
});
} catch (ex) {
console.error(ex);
notifyError(`${ex}`);
return;
}
this.btnShutter.classList.remove("hidden");
this.elmVideo.classList.remove("hidden");
this.elmVideo.srcObject = stream;
this.elmVideo.play();
}
takePhoto() {
this.btnShutter.disabled = true;
this.btnShutter.classList.add("hidden");
this.elmVideo.pause();
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) {
notifyError("Failed to get canvas 2D rendering context");
return;
}
const width = this.elmVideo.videoWidth;
const height = this.elmVideo.videoHeight;
canvas.width = width;
canvas.height = height;
context.drawImage(this.elmVideo, 0, 0, width, height);
this.elmVideo.srcObject = null;
this.elmVideo.classList.add("hidden");
this.elmVideo.after(canvas);
this.btnClear.disabled = false;
this.btnClear.classList.remove("hidden");
this.sendReady(true);
}
}
customElements.define("camera-input", CameraInput);