From 1fe0b35757d7ffa4cd3db1f63a2ef7f1778fb498 Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Sat, 7 Mar 2020 20:22:31 -0700 Subject: [PATCH] Add convulse toy --- toys/convulse/LICENSE.md | 22 ++++ toys/convulse/README.md | 7 ++ toys/convulse/convulse.css | 57 ++++++++++ toys/convulse/convulse.js | 207 ++++++++++++++++++++++++++++++++++++ toys/convulse/convulse.png | Bin 0 -> 2038 bytes toys/convulse/index.html | 56 ++++++++++ toys/convulse/manifest.json | 15 +++ toys/convulse/sw.js | 26 +++++ toys/index.md | 1 + 9 files changed, 391 insertions(+) create mode 100644 toys/convulse/LICENSE.md create mode 100644 toys/convulse/README.md create mode 100644 toys/convulse/convulse.css create mode 100644 toys/convulse/convulse.js create mode 100644 toys/convulse/convulse.png create mode 100644 toys/convulse/index.html create mode 100644 toys/convulse/manifest.json create mode 100644 toys/convulse/sw.js diff --git a/toys/convulse/LICENSE.md b/toys/convulse/LICENSE.md new file mode 100644 index 0000000..ed0a240 --- /dev/null +++ b/toys/convulse/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright © 2020 Neale Pickett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The software is provided "as is", without warranty of any kind, +express or implied, including but not limited to the warranties of +merchantability, fitness for a particular purpose and +noninfringement. In no event shall the authors or copyright holders +be liable for any claim, damages or other liability, whether in an +action of contract, tort or otherwise, arising from, out of or in +connection with the software or the use or other dealings in the +software. diff --git a/toys/convulse/README.md b/toys/convulse/README.md new file mode 100644 index 0000000..763a0c6 --- /dev/null +++ b/toys/convulse/README.md @@ -0,0 +1,7 @@ +This is a HTML5 doodad that lets you screencast with your face on top. + +It saves videos locally as `.webm` files. + +You can move your face around, zoom in to a region of the screen, +I guess this is enough for my needs. +Maybe it'll be enough for yours. diff --git a/toys/convulse/convulse.css b/toys/convulse/convulse.css new file mode 100644 index 0000000..3ba7407 --- /dev/null +++ b/toys/convulse/convulse.css @@ -0,0 +1,57 @@ +body { + background-color: #000; + color: #fff; + margin: 0; + font-family: sans-serif; +} +canvas { + width: 100vw; +} + +.hidden { + display: none; +} + +#download { + display: none; +} +#toasts { + position: fixed; + z-index: 2; + top: 0; + background-color: rgba(40, 40, 200, 0.3); + width: 100%; + text-align: center; +} +#controls { + position: fixed; + z-index: 2; + width: 100%; + background-color: rgba(0, 0, 0, 0.8); + opacity: 0.8; + bottom: 0; +} +#rec, #save { + font: 12pt sans-serif; + padding: 1em 2em; + margin: 0.2em 1em; + background: #ccc; + border: none; + border-radius: 3px; +} +dt { + font-weight: bold; +} +#indicator { + position: fixed; + top: 0.3em; + right: 0.3em; + font: 18pt sans-serif; + color: red; + animation: blinker 0.5s cubic-bezier(0, 0.87, 0.58, 1) infinite alternate; +} + +@keyframes blinker { + from { opacity: 0; } + to { opacity: 1; } +} \ No newline at end of file diff --git a/toys/convulse/convulse.js b/toys/convulse/convulse.js new file mode 100644 index 0000000..f7fc52a --- /dev/null +++ b/toys/convulse/convulse.js @@ -0,0 +1,207 @@ +// jshint asi:true + +Math.TAU = Math.PI * 2 + +function toast(text, timeout=8000) { + let toasts = document.querySelector("#toasts") + if (! text) { + while (toasts.firstChild) { + toasts.firstChild.remove() + } + } else { + let p = document.querySelector("#toasts").appendChild(document.createElement("p")) + p.textContent = text + if (timeout) { + setTimeout(() => p.remove(), timeout) + } + } +} + +class Convulse { + constructor() { + this.chunks = [] + this.canvas = document.querySelector("canvas") + this.ctx = this.canvas.getContext("2d") + this.dllink = document.querySelector("#download") + this.webcamVideo = document.querySelector("#webcam") + this.desktopVideo = document.querySelector("#desktop") + + this.canvas.width = 1920 + this.canvas.height = 1080 + document.querySelector("#indicator").classList.add("hidden") + + document.addEventListener("mouseenter", e => this.showControls(true)) + document.addEventListener("mouseleave", e => this.showControls(false)) + + document.querySelector("canvas").addEventListener("click", e => this.rec(e)) + document.querySelector("#rec").addEventListener("click", e => this.rec(e)) + //document.querySelector("#save").addEventListener("click", e => this.save(e)) + + document.querySelector("#webcam-size").addEventListener("input", e => this.setWebcamSize(e)) + document.querySelector("#webcam-size").value = localStorage.webcamSize || 0.3 + document.querySelector("#webcam-size").dispatchEvent(new Event("input")) + document.querySelector("#webcam-pos").addEventListener("click", e => this.setWebcamPos(e)) + this.webcamPos = localStorage.webcamPos || 2 + + document.querySelector("#desktop-size").addEventListener("input", e => this.setDesktopSize(e)) + document.querySelector("#desktop-size").value = localStorage.desktopSize || 2.0 + document.querySelector("#desktop-size").dispatchEvent(new Event("input")) + document.querySelector("#desktop-pos").addEventListener("click", e => this.setDesktopPos(e)) + this.desktopPos = localStorage.desktopPos || 0 + + this.recorder = {state: "unstarted"} + + this.mediaStream = new MediaStream() + + navigator.mediaDevices.getUserMedia({video: true, audio: true}) + .then(media => { + document.querySelector("#hello").classList.add("hidden") + this.webcamVideo.muted = true + this.webcamVideo.srcObject = media + this.webcamVideo.play() + for (let at of media.getAudioTracks()) { + this.mediaStream.addTrack(at) + console.log("Adding audio track", at) + } + }) + .catch(err => { + toast("Couldn't open camera!") + }) + + navigator.mediaDevices.getDisplayMedia({video: {cursor: "always"}}) + .then(media => { + document.querySelector("#hello").classList.add("hidden") + this.desktopVideo.srcObject = media + this.desktopVideo.play() + }) + .catch(err => { + toast("Couldn't open screen grabber!") + }) + + let canvasStream = this.canvas.captureStream(30) + for (let vt of canvasStream.getVideoTracks()) { + this.mediaStream.addTrack(vt) + console.log("Adding video track", vt) + } + + this.frame() + + toast("Click anywhere to start and stop recording") + } + + setWebcamSize(event) { + this.webcamSize = event.target.value + localStorage.webcamSize = this.webcamSize + } + + setWebcamPos(event) { + this.webcamPos = (this.webcamPos + 1) % 9 + localStorage.webcamPos = this.webcamPos + } + + setDesktopSize(event) { + this.desktopSize = event.target.value + localStorage.desktopSize = this.desktopSize + } + + setDesktopPos(event) { + this.desktopPos = (this.desktopPos + 1) % 9 + localStorage.desktopPos = this.desktopPos + } + + rec(event) { + let button = document.querySelector("#rec") + if (this.recorder.state == "recording") { + // Stop + this.recorder.stop() + this.canvas.classList.remove("recording") + document.querySelector("#indicator").classList.add("hidden") + button.textContent = "⏺️" + toast("Stopped") + document.title = "Convulse: stopped" + this.save() + } else { + // Start + this.chunks = [] + this.recorder = new MediaRecorder(this.mediaStream, {mimeType: "video/webm"}) + this.recorder.addEventListener("dataavailable", event => { + if (event.data && event.data.size > 0) { + this.chunks.push(event.data) + } + }) + + this.recorder.start(10) + this.canvas.classList.add("recording") + document.querySelector("#indicator").classList.remove("hidden") + button.textContent = "⏹️" + toast("Recording: click anywhere to stop") + document.title = "Convulse: recording" + } + } + + showControls(show) { + let controls = document.querySelector("#controls") + if (show) { + controls.classList.remove("hidden") + } else { + controls.classList.add("hidden") + } + } + + save(event) { + let recording = window.URL.createObjectURL(new Blob(this.chunks, {type: this.recorder.mimeType})) + let now = new Date().toISOString() + let saveButton = document.querySelector("#save") + + saveButton.addEventListener('progress', event => console.log(event)) + saveButton.href = recording + saveButton.download = "convulse-" + now + ".webm" + saveButton.click() + } + + frame(timestamp) { + if (this.desktopVideo.videoWidth > 0) { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height) + + let desktopAR = this.desktopVideo.videoWidth / this.desktopVideo.videoHeight + let desktopHeight = this.canvas.height * this.desktopSize + let desktopWidth = desktopHeight * desktopAR + let desktopY = (this.canvas.height - desktopHeight) * (Math.floor(this.desktopPos / 3) / 2) + let desktopX = (this.canvas.width - desktopWidth) * (Math.floor(this.desktopPos % 3) / 2) + this.ctx.drawImage( + this.desktopVideo, + desktopX, desktopY, + desktopWidth, desktopHeight + ) + } + + if (this.webcamVideo.videoWidth > 0) { + let webcamAR = this.webcamVideo.videoWidth / this.webcamVideo.videoHeight + let webcamHeight = this.canvas.height * this.webcamSize + let webcamWidth = webcamHeight * webcamAR + let webcamY = (this.canvas.height - webcamHeight) * (Math.floor(this.webcamPos / 3) / 2) + let webcamX = (this.canvas.width - webcamWidth) * (Math.floor(this.webcamPos % 3) / 2) + this.ctx.drawImage( + this.webcamVideo, + webcamX, webcamY, + webcamWidth, webcamHeight + ) + } + + requestAnimationFrame(ts => this.frame(ts)) + } + +} + +function init() { + if (navigator.serviceWorker) { + navigator.serviceWorker.register("sw.js") + } + window.app = new Convulse() +} + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init) +} else { + init() +} diff --git a/toys/convulse/convulse.png b/toys/convulse/convulse.png new file mode 100644 index 0000000000000000000000000000000000000000..0057a274c1a72b6ee7eb117812a83eeed844876f GIT binary patch literal 2038 zcmb7EeK?fq9)5?RMxkDkO*U!B(XiQ*eAb4+prhH0gGh|bNNj{y+RV--i-tkIE}Wz^ zHebn6%k&{@8m(GvV?Nf5ZyME(l=Qj1`@HYW{<+t6&UHQWd!G0AyYKsVKhOKTlSd8o z)z{mg2SJd&ABDUh%zfx@r8bDKGfstp374|p*ApkZPs)ZMEq&@YT7L71c18f2s>3@u@ls6mt)uO?!6jR>{|B`{({s30nfu~51MNd$r; z2vdqMWayePq5+_EzycVxB|woPu_CkzzzLXa9Hs{H0H!vKngI&HiCmLG^(6@70rm(y zV>&=VqYPkT)JD`e!UXoPYCPxyl0a%0%Zz9a1ZZj+Vco%w7}!9qVs6zQ)SCqgXbaF< zrrCm~3b;TSYpOmCH5W7muE1WvBib75LW&&4%mWb!(YzWI15QAUB;WfBDMDf~14-i; zQGFWN+hG8t1PQ*Pu?<*&8?Qbz%<#csG@~A%i~6XU;y8Fk4!96)srv(ipig7&h@K$i zYxd{&%-TM_9^M~V`*e+!>-hZ_OFt|6;&;QAd(-L<{c{D2W@F$Kb+Lr>Hc4`FetS;Q z=GlY1=Pu2!Ivonl-@V+Jp!ZAfR{FjYXOj~LR#GZzP0MObf;|)O5vx*$J{+SaWiBUY zEAo}0U1yBR-OmOFr<4V|Vmn5NSISsN|1IApyoUS!EoEkq&_H#wzPqM^o#;7c_h0SQ z_x^K^!xlGJUGjav_-Qh-dgoIiuue##grz``0l4`FeYmo124)b17y6MsXc;xX*D^zl zJdDqLM%2|gXT6+X!m6cvc+>Fsb>4!)Td&!-UXNuh-1^`%z35d^>&EXORCl>O-o43X z;o0qd$DQ3uJKR>Wiiz*I1-7cn z183%(U6rkk?C-qyDgF>Ck31a;NZ;j{DGQsK|BC1TANl4(=}u`c8epC7rXPYVQfx$u z2l_d1o~1${rxJ^mE&NKe%xMEbcUIR$(=z2ne!1JbI{2G3$#LI9wBGfPE}Oo==Ue@1 z7(o5js<8*AMGNajQ1Pj& z!@uBC>#eJ+EeCX9!%f`Tc$glLykTind(Xw%g^qhhzQzDdVAOuff3y$ z^K2f=%5$0OIpagjmFsfG?s447R8=SWQN-cH9hF`SY#-IMy)WJFrF(g05huc8?m&A) zQ)PIg&1eH$Fmz#Rq+Y+EGfxah?w*J&X&=cd#q<pYk_~Ynj~bWm*&JO&=g=y6Suv8|Gw0cM-IuoI@CH0AFR!?SDadL_N zN!RJ3$i*!c&j$y}J$J&q4QU}kzP30XI@w=%gSXd7Al7-My97TY*2UaXWyVYW$&SA$ z?ZKCW-*exd-L|@O!lrnqUMbS3r0h|IX%i=E9!^K!E@nAA*5^V$MzQe`J3O@Pt1NI8 zBg^^qb~u-i)m&DS1O$OZpUW~+jq&}1v?eYSVCi+EkEN{n8=y3r4nqGyZK8!&@_DUa z;iQGzI2dxYrt}yX5oxCa8$QDs7Mfb%3~QH3i{IhhGZR>NcMm6nS5^3)AS2>L6#;%O z#}lOEhgtZW8Am{Pa*TygEjC}a@k_IM7n))H?rm0g-y53bnJ73V zlhZu>iu<}k(c2GJk@S3(tKW-P;Wd5fio_D@6NW=+is1bgh%h^MQ+L6odxMAWczvA> bBg^YQ&m4TlFe^0z|8tO^cObddGm8Bu32`*8 literal 0 HcmV?d00001 diff --git a/toys/convulse/index.html b/toys/convulse/index.html new file mode 100644 index 0000000..185e15d --- /dev/null +++ b/toys/convulse/index.html @@ -0,0 +1,56 @@ + + + + Convulse + + + + + + +
+

Convulse: it's sorta like Twitch!

+

+ I need the following permissions: +

+
+
Use your microphone
+
So I can record your velvety-smooth voice
+ +
Use your camera
+
So I can record your velvety-smooth face
+ +
Share your screen
+
So I can record your velvety-smooth computer
+
+
+ + +
+ + + +
+ +
+ UR FACE + + +
+ +
+ Capture Area + + +
+
+ +
+ + diff --git a/toys/convulse/manifest.json b/toys/convulse/manifest.json new file mode 100644 index 0000000..0bffc0c --- /dev/null +++ b/toys/convulse/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "Convulse Desktop Recorder", + "short_name": "Convulse", + "start_url": ".", + "display": "standalone", + "background_color": "#000", + "theme_color": "#ff68d7", + "description": "Records your desktop, with a webcam overlay.", + "icons": [ + { + "src": "convulse.png", + "sizes": "196x196" + } + ] +} diff --git a/toys/convulse/sw.js b/toys/convulse/sw.js new file mode 100644 index 0000000..9ec07c5 --- /dev/null +++ b/toys/convulse/sw.js @@ -0,0 +1,26 @@ +var cacheName = "convulse-v1"; +var content = [ + "index.html", + "convulse.css", + "convulse.js", + "convulse.png" +]; + + +self.addEventListener("install", e => { + e.waitUntil(caches.Open(cacheName).then(cache => cache.addAll(content))); +}); + +// Have mercy, this is a horror show +self.addEventListener("fetch", e => { + e.respondWith( + caches.match(e.request).then(r => { + return r || fetch(e.request).then(response => { + return caches.open(cacheName).then(cache => { + cache.put(e.request, response.clone()); + return response; + }); + }); + }) + ); +}); diff --git a/toys/index.md b/toys/index.md index 3e2ad8a..869d5f1 100644 --- a/toys/index.md +++ b/toys/index.md @@ -5,6 +5,7 @@ title: Toys Here is some various junk I've done. Maybe you'll find it amusing. Maybe you'll just wonder why I spend so much time on this garbage. +* [Convulse Screen Recorder](convulse/), a pure HTML5 screen recorder, with webcam overlay * [Starship Noise Generator](starship/) * [Grep Dict](grepdict/) runs `grep` on `/usr/share/dict/words`; perfect for cheating on crossword puzzles * If you need to write someone a letter but really don't want to, try my