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 { 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);