Skip to content

Commit 7bd13b0

Browse files
committed
Create camera-capture element and an element for logging
- These new elements uses material design and are build into build/elements using rollupjs.
1 parent b0d680c commit 7bd13b0

15 files changed

+919
-377
lines changed

babel.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const plugins = [
2+
'@babel/plugin-proposal-class-properties',
3+
];
4+
5+
module.exports = { plugins };
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { html, css, LitElement } from '../../node_modules/lit-element';
2+
3+
import '../../node_modules/@material/mwc-icon';
4+
import '../../node_modules/@material/mwc-ripple';
5+
6+
import './settings-pane.js';
7+
8+
class CameraCapture extends LitElement {
9+
static styles = css`
10+
:host {
11+
display: block;
12+
width: 100vw;
13+
height: 100vh;
14+
overflow: hidden;
15+
}
16+
17+
.hidden {
18+
display: none !important;
19+
}
20+
21+
#mainContent {
22+
margin: 0 auto;
23+
}
24+
25+
video {
26+
display: inline;
27+
}
28+
29+
video:hover {
30+
cursor: pointer;
31+
}
32+
33+
#resetButton {
34+
position: relative;
35+
padding: 15px;
36+
color: white;
37+
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
38+
font-size: 18px;
39+
background: none;
40+
border: none;
41+
z-index: 15;
42+
outline: none;
43+
}
44+
45+
.canvas-wrapper {
46+
position: relative;
47+
width: 100%;
48+
}
49+
50+
.settings-wrapper {
51+
position: absolute;
52+
top: 0;
53+
width: 100%;
54+
height: 100%;
55+
display: flex;
56+
flex-direction: column;
57+
justify-content: center;
58+
align-items: flex-end;
59+
}
60+
61+
#cameraBar {
62+
height: 100px;
63+
background-color: transparent;
64+
display: flex;
65+
flex-direction: row;
66+
flex-wrap: nowrap;
67+
justify-content: space-around;
68+
align-items: center;
69+
}
70+
71+
#gallery {
72+
width: 48px;
73+
height: 48px;
74+
}
75+
76+
#takePhotoButton {
77+
height: 72px;
78+
width: 72px;
79+
}
80+
81+
#takePhotoButton mwc-icon {
82+
--mdc-icon-size: 48px;
83+
}
84+
85+
.camera-bar-icon {
86+
height: 52px;
87+
width: 52px;
88+
display: flex;
89+
justify-content: center;
90+
outline: none;
91+
border: 2px solid white;
92+
border-radius: 52px;
93+
color: white;
94+
background-color: transparent;
95+
}
96+
`;
97+
98+
facingMode = "user";
99+
100+
_onResetClicked(e) {
101+
const settings = this.shadowRoot.querySelector('settings-pane');
102+
settings.reset();
103+
104+
const resetButton = this.shadowRoot.querySelector('#resetButton');
105+
resetButton.classList.add('hidden');
106+
}
107+
108+
async _onConstraintsChange(e) {
109+
try {
110+
await this.videoTrack.applyConstraints(e.detail.constraints);
111+
112+
const settings = this.shadowRoot.querySelector('settings-pane');
113+
settings.applyFromTrack(this.videoTrack);
114+
} catch(err) {
115+
console.error(err);
116+
}
117+
118+
const resetButton = this.shadowRoot.querySelector('#resetButton');
119+
resetButton.classList.remove('hidden');
120+
}
121+
122+
_onSettingsBackgroundClicked(e) {
123+
const settings = this.shadowRoot.querySelector('settings-pane');
124+
settings.hide();
125+
126+
const resetButton = this.shadowRoot.querySelector('#resetButton');
127+
resetButton.classList.add('hidden');
128+
}
129+
130+
async _onFacingModeClicked() {
131+
this.selectedCamera = (this.selectedCamera + 1) % this.cameras.length;
132+
const camera = this.cameras[this.selectedCamera];
133+
this.constraints.deviceId = { exact: camera.deviceId };
134+
this.facingMode = this.getFacingMode(camera);
135+
this.requestUpdate();
136+
137+
this.stopCamera();
138+
139+
const videoElement = this.shadowRoot.querySelector('video');
140+
await this.startCamera(videoElement, this.constraints);
141+
142+
// Timeout needed in Chrome, see https://crbug.com/711524.
143+
const settings = this.shadowRoot.querySelector('settings-pane');
144+
setTimeout(async () => {
145+
await customElements.whenDefined('settings-pane');
146+
settings.applyFromTrack(this.videoTrack);
147+
}, 500);
148+
}
149+
150+
async takePhoto() {
151+
try {
152+
const blob = await this.imageCapturer.takePhoto();
153+
const img = await createImageBitmap(blob);
154+
155+
const canvas = this.shadowRoot.querySelector('#gallery');
156+
canvas.width = getComputedStyle(canvas).width.split('px')[0];
157+
canvas.height = getComputedStyle(canvas).height.split('px')[0];
158+
let ratio = Math.max(canvas.width / img.width, canvas.height / img.height);
159+
let x = (canvas.width - img.width * ratio) / 2;
160+
let y = (canvas.height - img.height * ratio) / 2;
161+
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
162+
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height,
163+
x, y, img.width * ratio, img.height * ratio);
164+
} catch(err) {
165+
console.error("takePhoto() failed: ", err)
166+
}
167+
}
168+
169+
startCamera(target, constraints) {
170+
return new Promise(async (resolve, reject) => {
171+
try {
172+
const stream = await navigator.mediaDevices.getUserMedia({
173+
video: constraints,
174+
audio: false
175+
});
176+
177+
this.video = target;
178+
this.stream = stream;
179+
180+
target.srcObject = stream;
181+
target.addEventListener('canplay', resolve, { once: true });
182+
target.play();
183+
} catch(err) {
184+
reject(err);
185+
}
186+
});
187+
}
188+
189+
stopCamera() {
190+
if (this.video) {
191+
this.video.pause();
192+
this.video.srcObject = null;
193+
}
194+
if (this.stream) {
195+
this.stream.getVideoTracks()[0].stop();
196+
}
197+
}
198+
199+
getFacingMode(device) {
200+
if (device.facingMode == "environment"
201+
|| device.label.indexOf("facing back") >= 0) {
202+
return "environment";
203+
}
204+
// We assume by default that cameras are user facing
205+
// which is mostly the case for desktop.
206+
return "user";
207+
}
208+
209+
async firstUpdated() {
210+
this.constraints = {};
211+
212+
const devices = await navigator.mediaDevices.enumerateDevices();
213+
214+
this.cameras = [];
215+
this.selectedCamera = 0;
216+
217+
devices.forEach(device => {
218+
if (device.kind == 'videoinput') {
219+
if (this.getFacingMode(device) == "user") {
220+
this.cameras.push(device);
221+
} else {
222+
this.cameras.unshift(device);
223+
}
224+
}
225+
});
226+
227+
// Android bug, doesn't work with DevTools emulation.
228+
if (navigator.userAgent.includes("Android") &&
229+
screen.orientation.type.includes("portrait")) {
230+
this.constraints.width = Math.ceil(visualViewport.height);
231+
this.constraints.height = Math.ceil(visualViewport.width);
232+
} else {
233+
this.constraints.width = Math.ceil(visualViewport.width);
234+
this.constraints.height = Math.ceil(visualViewport.height);
235+
}
236+
237+
// Disable facingModeButton if there is no environment or user mode.
238+
let facingModeButton = this.shadowRoot.getElementById('facingModeButton');
239+
if (this.cameras.length < 2) {
240+
facingModeButton.style.color = 'gray';
241+
facingModeButton.style.border = '2px solid gray';
242+
} else {
243+
facingModeButton.disabled = false;
244+
}
245+
246+
this.facingMode = this.getFacingMode(this.cameras[0]);
247+
this.requestUpdate();
248+
this.constraints.deviceId = { exact: this.cameras[0].deviceId};
249+
250+
const videoElement = this.shadowRoot.querySelector('video');
251+
252+
await this.startCamera(videoElement, this.constraints);
253+
254+
this.videoTrack = videoElement.srcObject.getVideoTracks()[0];
255+
this.imageCapturer = new ImageCapture(this.videoTrack);
256+
257+
let cameraBar = this.shadowRoot.querySelector('#cameraBar');
258+
cameraBar.style.width = `${videoElement.videoWidth}px`;
259+
260+
let mainContent = this.shadowRoot.getElementById('mainContent');
261+
mainContent.style.width = `${videoElement.videoWidth}px`;
262+
mainContent.classList.remove('hidden');
263+
264+
this.shadowRoot.querySelector('.canvas-wrapper').style.height =
265+
`${videoElement.videoHeight}px`;
266+
267+
let resetButton = this.shadowRoot.querySelector('#resetButton');
268+
resetButton.classList.remove('hidden');
269+
resetButton.style.left = `${videoElement.videoWidth - resetButton.offsetWidth}px`;
270+
resetButton.style.bottom = `${videoElement.videoHeight}px`;
271+
resetButton.classList.add('hidden');
272+
273+
this.shadowRoot.getElementById('takePhotoButton').disabled = false;
274+
275+
// Timeout needed in Chrome, see https://crbug.com/711524.
276+
const settings = this.shadowRoot.querySelector('settings-pane');
277+
setTimeout(async () => {
278+
await customElements.whenDefined('settings-pane');
279+
settings.applyFromTrack(this.videoTrack);
280+
}, 500);
281+
}
282+
283+
render() {
284+
return html`
285+
<div id="mainContent" class="centered hidden">
286+
<div class="canvas-wrapper">
287+
<video id="videoInput"></video>
288+
289+
<button id="resetButton" class='hidden' @click=${this._onResetClicked}>
290+
Reset
291+
</button>
292+
293+
<div class="settings-wrapper">
294+
<settings-pane
295+
@click=${this._onSettingsBackgroundClicked}
296+
@constraintschange=${this._onConstraintsChange}>
297+
</settings-pane>
298+
299+
<div id="cameraBar">
300+
<canvas id="gallery" class="camera-bar-icon"></canvas>
301+
<div>
302+
<button id="takePhotoButton" class="camera-bar-icon" disabled
303+
@click=${this.takePhoto}>
304+
<mwc-icon>photo_camera</mwc-icon>
305+
</button>
306+
<mwc-ripple unbounded></mwc-ripple>
307+
</div>
308+
<button id="facingModeButton" class="camera-bar-icon" disabled
309+
@click=${this._onFacingModeClicked}>
310+
<mwc-icon>${this.facingMode === "user" ? "camera_front" : "camera_rear"}</mwc-icon>
311+
</button>
312+
</div>
313+
</div>
314+
</div>
315+
</div>
316+
`;
317+
}
318+
}
319+
320+
customElements.define('camera-capture', CameraCapture);

elements/camera-capture/index.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
7+
<title>Pro Camera</title>
8+
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
9+
<link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">
10+
</head>
11+
12+
<script type="module" src="./camera-capture.js"></script>
13+
14+
<style>
15+
body {
16+
font-family: Roboto, sans-serif;
17+
margin: 0;
18+
}
19+
20+
camera-view {
21+
width: 100vw;
22+
height: 100vh;
23+
}
24+
25+
.centered {
26+
display: flex;
27+
justify-content: center;
28+
flex-wrap: wrap;
29+
}
30+
31+
</style>
32+
33+
<body>
34+
<camera-capture></camera-capture>
35+
</body>
36+
</html>

0 commit comments

Comments
 (0)