Add convulse toy
This commit is contained in:
parent
0df9e5cd44
commit
1fe0b35757
|
@ -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.
|
|
@ -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.
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,56 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Convulse</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
<link rel="icon" href="convulse.png">
|
||||||
|
<link rel="stylesheet" href="convulse.css">
|
||||||
|
<script src="convulse.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="hello">
|
||||||
|
<h1>Convulse: it's sorta like Twitch!</h1>
|
||||||
|
<p>
|
||||||
|
I need the following permissions:
|
||||||
|
</p>
|
||||||
|
<dl>
|
||||||
|
<dt>Use your microphone</dt>
|
||||||
|
<dd>So I can record your velvety-smooth voice</dd>
|
||||||
|
|
||||||
|
<dt>Use your camera</dt>
|
||||||
|
<dd>So I can record your velvety-smooth face</dd>
|
||||||
|
|
||||||
|
<dt>Share your screen</dt>
|
||||||
|
<dd>So I can record your velvety-smooth computer</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<canvas></canvas>
|
||||||
|
<div id="indicator">⬤</div>
|
||||||
|
|
||||||
|
<div id="videos" class="hidden">
|
||||||
|
<video id="webcam"></video>
|
||||||
|
<video id="desktop"></video>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="controls">
|
||||||
|
<div class="hidden">
|
||||||
|
<button id="rec">⏺️</button>
|
||||||
|
<a id="save">💾</a>
|
||||||
|
</div>
|
||||||
|
<div id="webcam-controls">
|
||||||
|
UR FACE
|
||||||
|
<input id="webcam-size" type="range" min="0" max="1" step="0.01" value="0.2">
|
||||||
|
<button id="webcam-pos">Move</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="desktop-controls">
|
||||||
|
Capture Area
|
||||||
|
<input id="desktop-size" type="range" min="1.0" max="3.0" step="0.01" value="0.2">
|
||||||
|
<button id="desktop-pos">Move</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toasts"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
|
@ -5,6 +5,7 @@ title: Toys
|
||||||
Here is some various junk I've done. Maybe you'll find it amusing.
|
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.
|
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/)
|
* [Starship Noise Generator](starship/)
|
||||||
* [Grep Dict](grepdict/) runs `grep` on `/usr/share/dict/words`; perfect for cheating on crossword puzzles
|
* [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
|
* If you need to write someone a letter but really don't want to, try my
|
||||||
|
|
Loading…
Reference in New Issue