A simple PWA camera built using Vue, Tailwind, and WebRTC. Try adding the Debug view to your home screen and read the companion blog to learn more.
A Pen by Lee Martin on CodePen.
A simple PWA camera built using Vue, Tailwind, and WebRTC. Try adding the Debug view to your home screen and read the companion blog to learn more.
A Pen by Lee Martin on CodePen.
| <!-- | |
| CodePen Camera | |
| ========== | |
| A Progressive Web App Camera built using Vue, Tailwind, and WebRTC. Try adding the Debug view to your home screen and read the companion blog to learn more: | |
| [Blog](https://medium.com/@leemartin/how-to-build-a-simple-ios-home-screen-pwa-camera-using-vue-tailwind-and-webrtc-on-codepen-2d61a9754d47?source=friends_link&sk=2ed90bf1e4f52db8491636cebb4b582b) | |
| --> | |
| <template> | |
| <main ontouchstart="" class="md:text-xl lg:text-2xl"> | |
| <!-- Intro --> | |
| <!-- ---------- --> | |
| <!-- Inform the user of the camera's purpose and prepare them for camera permissions. --> | |
| <section id="intro" v-if="!stream" class="absolute flex flex-col inset-0 px-4 py-8 z-20"> | |
| <article class="flex flex-1 flex-col items-center justify-center"> | |
| <img src="https://assets.codepen.io/141041/Button-Fill-Black-Large.png" alt="CodePen" class="h-32 md:h-40 lg:h-64 mb-4 w-32 md:w-40 lg:w-64"> | |
| <h1 class="font-bold mb-4 text-2xl md:text-3xl lg:text-5xl text-center">CodePen Camera</h1> | |
| <p class="leading-relaxed md:max-w-screen-sm lg:max-w-screen-md text-center">This is a Progressive Web App Camera built on CodePen using <a href="https://vuejs.org/" target="_blank" class="underline">Vue</a>, <a href="https://tailwindcss.com/" target="_blank" class="underline">Tailwind</a>, and <a href="http://webrtc.org/" target="_blank" class="underline">WebRTC</a>. Try adding the Debug view to your home screen and read the companion <a href="https://medium.com/@leemartin/how-to-build-a-simple-ios-home-screen-pwa-camera-using-vue-tailwind-and-webrtc-on-codepen-2d61a9754d47?source=friends_link&sk=2ed90bf1e4f52db8491636cebb4b582b" target="_blank" class="underline">blog</a> to learn more.</p> | |
| </article> | |
| <footer class="text-center"> | |
| <button @click="startCamera" class="bg-black font-bold px-4 py-2 rounded-md text-white">Allow Access</button> | |
| </footer> | |
| </section> | |
| <!-- Camera --> | |
| <!-- ---------- --> | |
| <!-- Allow the user to capture photos and take other camera actions. --> | |
| <section id="camera" v-if="stream" class="absolute flex flex-col inset-0 items-center justify-end px-4 py-8 z-20"> | |
| <footer> | |
| <button class="capture" @click="capturePhoto"> | |
| <img src="https://assets.codepen.io/141041/Button-Fill-White-Large.png" alt="CodePen" class="h-24 w-24" :disabled="!ready"> | |
| </button> | |
| </footer> | |
| </section> | |
| <!-- Download --> | |
| <!-- ---------- --> | |
| <!-- Allow the user to preview and download the captured photo or return to camera. --> | |
| <section id="download" v-if="photo" class="absolute bg-white flex flex-col inset-0 items-center justify-between px-4 py-8 z-30"> | |
| <header> | |
| <button @click="photo = null"> | |
| <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" class="h-10 md:h-12 lg:h-16 w-10 lg:w-12 md:w-16"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> | |
| </button> | |
| </header> | |
| <article> | |
| <img :src="photo.toDataURL('image/jpeg')" alt="Photo" class="h-64 w-64"> | |
| </article> | |
| <footer> | |
| <button @click="downloadPhoto"> | |
| <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" class="h-10 md:h-12 lg:h-16 w-10 lg:w-12 md:w-16"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg> | |
| </button> | |
| </footer> | |
| </section> | |
| <!-- Video --> | |
| <!-- ---------- --> | |
| <video ref="video" class="absolute h-full inset-0 object-cover w-full z-10" autoplay muted playsinline></video> | |
| </main> | |
| </template> | |
| <script> | |
| export default { | |
| data() { | |
| return { | |
| stream: null, | |
| ready: false, | |
| photo: null | |
| } | |
| }, | |
| methods: { | |
| async startCamera() { | |
| this.stream = await navigator.mediaDevices.getUserMedia({ | |
| audio: false, | |
| video: { | |
| facingMode: 'environment' | |
| } | |
| }) | |
| this.$refs.video.srcObject = this.stream | |
| this.$refs.video.onloadedmetadata = (e) => { | |
| this.ready = true | |
| } | |
| this.$refs.video.onended = (e) => { | |
| this.ready = false | |
| this.stream = null | |
| } | |
| }, | |
| capturePhoto() { | |
| let video = this.$refs.video | |
| let videoCanvas = document.createElement('canvas') | |
| videoCanvas.height = video.videoHeight | |
| videoCanvas.width = video.videoWidth | |
| let videoContext = videoCanvas.getContext('2d') | |
| videoContext.drawImage(video, 0, 0) | |
| this.photo = loadImage.scale(videoCanvas, { | |
| maxHeight: 1080, | |
| maxWidth: 1080, | |
| cover: true, | |
| crop: true, | |
| canvas: true | |
| }) | |
| }, | |
| downloadPhoto() { | |
| this.photo.toBlob(blob => { | |
| let data = window.URL.createObjectURL(blob) | |
| let link = document.createElement('a') | |
| link.href = data | |
| link.download = "photo.jpg" | |
| link.click() | |
| }, 'image/jpeg') | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| html, body, main, section{ | |
| height: 100%; | |
| width: 100%; | |
| } | |
| html{ | |
| position: fixed; | |
| } | |
| body{ | |
| font-family: 'Lato', sans-serif; | |
| -webkit-tap-highlight-color: transparent; | |
| -webkit-touch-callout: none; | |
| -webkit-user-select: none; | |
| } | |
| button.capture:disabled{ | |
| opacity: 0.25; | |
| } | |
| button.capture:active{ | |
| opacity: 0.9; | |
| } | |
| </style> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/5.14.0/load-image.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-load-image/5.14.0/load-image-scale.min.js"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/1.7.6/tailwind.min.css" rel="stylesheet" /> | |
| <link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap" rel="stylesheet" /> |