178 lines
5.2 KiB
TypeScript
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);
|