-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
675c8bb
commit 96acbef
Showing
4 changed files
with
297 additions
and
465 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ | ||
let coepCredentialless = false; | ||
if (typeof window === 'undefined') { | ||
self.addEventListener("install", () => self.skipWaiting()); | ||
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim())); | ||
|
||
self.addEventListener("message", (ev) => { | ||
if (!ev.data) { | ||
return; | ||
} else if (ev.data.type === "deregister") { | ||
self.registration | ||
.unregister() | ||
.then(() => { | ||
return self.clients.matchAll(); | ||
}) | ||
.then(clients => { | ||
clients.forEach((client) => client.navigate(client.url)); | ||
}); | ||
} else if (ev.data.type === "coepCredentialless") { | ||
coepCredentialless = ev.data.value; | ||
} | ||
}); | ||
|
||
self.addEventListener("fetch", function (event) { | ||
const r = event.request; | ||
if (r.cache === "only-if-cached" && r.mode !== "same-origin") { | ||
return; | ||
} | ||
|
||
const request = (coepCredentialless && r.mode === "no-cors") | ||
? new Request(r, { | ||
credentials: "omit", | ||
}) | ||
: r; | ||
event.respondWith( | ||
fetch(request) | ||
.then((response) => { | ||
if (response.status === 0) { | ||
return response; | ||
} | ||
|
||
const newHeaders = new Headers(response.headers); | ||
newHeaders.set("Cross-Origin-Embedder-Policy", | ||
coepCredentialless ? "credentialless" : "require-corp" | ||
); | ||
if (!coepCredentialless) { | ||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin"); | ||
} | ||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin"); | ||
|
||
return new Response(response.body, { | ||
status: response.status, | ||
statusText: response.statusText, | ||
headers: newHeaders, | ||
}); | ||
}) | ||
.catch((e) => console.error(e)) | ||
); | ||
}); | ||
|
||
} else { | ||
(() => { | ||
const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf"); | ||
window.sessionStorage.removeItem("coiReloadedBySelf"); | ||
const coepDegrading = (reloadedBySelf == "coepdegrade"); | ||
|
||
// You can customize the behavior of this script through a global `coi` variable. | ||
const coi = { | ||
shouldRegister: () => !reloadedBySelf, | ||
shouldDeregister: () => false, | ||
coepCredentialless: () => true, | ||
coepDegrade: () => true, | ||
doReload: () => window.location.reload(), | ||
quiet: false, | ||
...window.coi | ||
}; | ||
|
||
const n = navigator; | ||
const controlling = n.serviceWorker && n.serviceWorker.controller; | ||
|
||
// Record the failure if the page is served by serviceWorker. | ||
if (controlling && !window.crossOriginIsolated) { | ||
window.sessionStorage.setItem("coiCoepHasFailed", "true"); | ||
} | ||
const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed"); | ||
|
||
if (controlling) { | ||
// Reload only on the first failure. | ||
const reloadToDegrade = coi.coepDegrade() && !( | ||
coepDegrading || window.crossOriginIsolated | ||
); | ||
n.serviceWorker.controller.postMessage({ | ||
type: "coepCredentialless", | ||
value: (reloadToDegrade || coepHasFailed && coi.coepDegrade()) | ||
? false | ||
: coi.coepCredentialless(), | ||
}); | ||
if (reloadToDegrade) { | ||
!coi.quiet && console.log("Reloading page to degrade COEP."); | ||
window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade"); | ||
coi.doReload("coepdegrade"); | ||
} | ||
|
||
if (coi.shouldDeregister()) { | ||
n.serviceWorker.controller.postMessage({ type: "deregister" }); | ||
} | ||
} | ||
|
||
// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are | ||
// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here. | ||
if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return; | ||
|
||
if (!window.isSecureContext) { | ||
!coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required."); | ||
return; | ||
} | ||
|
||
// In some environments (e.g. Firefox private mode) this won't be available | ||
if (!n.serviceWorker) { | ||
!coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."); | ||
return; | ||
} | ||
|
||
n.serviceWorker.register(window.document.currentScript.src).then( | ||
(registration) => { | ||
!coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope); | ||
|
||
registration.addEventListener("updatefound", () => { | ||
!coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker."); | ||
window.sessionStorage.setItem("coiReloadedBySelf", "updatefound"); | ||
coi.doReload(); | ||
}); | ||
|
||
// If the registration is active, but it's not controlling the page | ||
if (registration.active && !n.serviceWorker.controller) { | ||
!coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker."); | ||
window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling"); | ||
coi.doReload(); | ||
} | ||
}, | ||
(err) => { | ||
!coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err); | ||
} | ||
); | ||
})(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,117 +1,28 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<link rel="stylesheet" type="text/css" href="style.css"> | ||
|
||
<link rel="preconnect" href="https://fonts.googleapis.com"> | ||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
<link href="https://fonts.googleapis.com/css2?family=Permanent+Marker&display=swap" rel="stylesheet"> | ||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | ||
|
||
<meta property="og:title" content="Colliding Scopes: kaleidoscope animations" /> | ||
<meta property="og:description" content="Turn photos into kaleidoscope animations" /> | ||
<meta property="og:type" content="website" /> | ||
<meta property="og:url" content="https://collidingscopes.github.io/" /> | ||
<meta property="og:image" content="https://collidingscopes.github.io/images/siteOGImage.png"> | ||
<meta property="og:image:type" content="image/png" > | ||
<meta property="og:image:width" content="800" > | ||
<meta property="og:image:height" content="800" > | ||
|
||
<link rel="icon" href="images/siteFavicon2.png"> | ||
<link rel="apple-touch-icon" href="images/siteFavicon2.png"> | ||
|
||
</head> | ||
<body> | ||
|
||
<div id="coverScreen" class="hidden"> | ||
</div> | ||
|
||
<div id="introDiv"> | ||
<span id="siteNameText">Colliding Scopes</span> | ||
<span id="subtitleText">Turn photos into kaleidoscope animations</span> | ||
</div> | ||
|
||
<div id="toolDiv"> | ||
|
||
<canvas id="animation"></canvas> | ||
|
||
<table id="inputTable"> | ||
<tr> | ||
<td> | ||
<label for="imageInput" class="custom-file-upload"> | ||
<i class="fa fa-cloud-upload"></i> Select Image | ||
</label> | ||
<input type="file" id="imageInput" accept="image/*"> | ||
</td> | ||
<td> | ||
<pre><span class="tableText">Animation Speed</span><br><input class="input-number-noSpinners" type="range" id="speedInput" value="4" min="1" max="10"></pre> | ||
</td> | ||
<td><button id="recordVideoButton" class="recordButton">Record Video (r)</button></td> | ||
<td><span class="tableText">Seconds: </span><input type="number" id="videoDurationInput" class="input-number-noSpinners" value="10" min="1" max="120"></td> | ||
<!-- | ||
<td><button id="pauseAnimationButton">Pause/Play (p)</button></td> | ||
<td><button id="save-image-button">Screenshot (s)</button></td> | ||
--> | ||
</tr> | ||
|
||
</table> | ||
|
||
|
||
<div id="videoRecordingMessageDiv" class="hidden"> | ||
</div> | ||
<button id="downloadButton" class="hidden">Download video</button> | ||
|
||
<div id="imageContainer" class="hidden"> | ||
<img id="originalImg" src="images/HK400px.jpg"> | ||
</div> | ||
<div id="newImageContainer" class="hidden"> | ||
<img id="flippedImg" src="images/HK400pxFlipped.jpg"> | ||
</div> | ||
|
||
</div> | ||
|
||
|
||
<div id="notesDiv"> | ||
<div id="textBox"> | ||
|
||
<h2 id="aboutText">About</h2> | ||
|
||
<p>This web tool is completely free, open source, without any paywalls or premium options. You are welcome to use it for personal or commercial purposes.</p> | ||
<p>If you found this tool useful, feel free to buy me a coffee. This would be much appreciated during late-night coding sessions!</p> | ||
|
||
<a href="https://www.buymeacoffee.com/stereoDrift" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png" alt="Buy Me A Coffee"></a> | ||
|
||
<p>Enormous thanks and credits to Luke Hannam, whose <a href="https://www.pepperoni.blog/canvas-kaleidoscope/" target="_blank" rel="noopener">blog post</a> explained the code and mechanics for creating kaleidoscope animations in javascript.</p> | ||
<p>I made only a few tweaks to Luke's original algorithm, with my main work being to add the front-end user interface allowing users to upload their own photos, control variables like animation speed, and easily export the canvas animation to video.</p> | ||
<p>There are a few hotkeys which can speed up using the tool:</p> | ||
<ul> | ||
<li>Press <b>"r"</b> to start recording a video of the animation. You can specify the length of the video in seconds. An mp4 video file will be exported to your downloads folder afterwards</li> | ||
<li>Press <b>"p"</b> to pause / play the animation. This lets you stop at an interesting point of the animation</li> | ||
<li>Press <b>"s"</b> to save a screenshot of the current state of the animation (png image)</li> | ||
</ul> | ||
<p>This project is coded using Javascript, HTML, and CSS (see github repo linked below). Video creation and encoding is done using mp4 muxer.</p> | ||
<p>I do not have access to any of the images that you upload here, as all processing is done "client-side" (i.e., <b>no images are saved or stored by me — they stay on your computer only</b>).</p> | ||
<p>Feel free to reach out to discuss, ask questions, or to share your creations! The animations can be easily uploaded to instagram or otherwise -- you can tag me <a href="https://www.instagram.com/stereo.drift/" target="_blank" rel="noopener">@stereo.drift</a> :)</p> | ||
</div> | ||
</div> | ||
|
||
<div id="linksDiv"> | ||
<table id="infoMenuTable"> | ||
<tr> | ||
<td><button id="gitHubButton"class="socialMediaButton"><a href="https://github.com/collidingScopes/collidingScopes.github.io" target="_blank" rel="noopener"><i class="fa-brands fa-github"></i></a></button></td> | ||
<td><button id="coffeeButton" class="socialMediaButton"><a href="https://www.buymeacoffee.com/stereoDrift" target="_blank" rel="noopener"><i class="fa-solid fa-heart"></i></a></button></td> | ||
<td><button id="instagramButton" class="socialMediaButton"><a href="https://www.instagram.com/stereo.drift/" target="_blank" rel="noopener"><i class="fa-brands fa-instagram"></i></a></button></td> | ||
<td><button id="emailButton" class="socialMediaButton"><a href="mailto:[email protected]" target="_blank" rel="noopener"><i class="fa-solid fa-envelope"></i></a></button></td> | ||
</tr> | ||
</table> | ||
</div> | ||
|
||
|
||
</body> | ||
|
||
<script src="kaleidoscope.js"></script> | ||
<script src="mp4-muxer-main/build/mp4-muxer.js"></script> | ||
|
||
</html> | ||
<!doctype html> | ||
<head> | ||
<title>CANVAS TO VIDEO FAILS ON SAFARI</title> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
</head> | ||
<body> | ||
<header> | ||
<h1>CANVAS TO VIDEO FAILS ON SAFARI</h1> | ||
<p>Record canvas to video</p> | ||
<p>This example renders white noise in a <code>canvas</code> for three seconds and records the output in a video clip, using <code>canvas.captureStream</code> and <code>MediaRecorder</code></p> | ||
</header> | ||
<main class="row"> | ||
<figure> | ||
<canvas id="canvas" width="800" height="800"></canvas><br> | ||
<caption>This is a <code>canvas</code> element</caption> | ||
</figure> | ||
<!-- | ||
<figure> | ||
<video id="video" controls loop></video><br> | ||
<caption>This is a <code>video</code> element</caption> | ||
</figure> | ||
--> | ||
<button id="recordButton">Record 10 second video</button> | ||
<button id="downloadButton">Download video</button> | ||
</main> | ||
</body> | ||
<script src="saveCanvas.js"></script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// This example gets a video stream from a canvas on which we will draw | ||
// black and white noise, and captures it to a video | ||
// | ||
// The relevant functions in use are: | ||
// | ||
// requestAnimationFrame -> to create a render loop (better than setTimeout) | ||
// canvas.captureStream -> to get a stream from a canvas | ||
// context.getImageData -> to get access to the canvas pixels | ||
// URL.createObjectURL -> to create a URL from a stream so we can use it as src | ||
|
||
var finishedBlob; | ||
var downloadButton = document.getElementById("downloadButton"); | ||
downloadButton.addEventListener("click",downloadBlob); | ||
|
||
var recordButton = document.getElementById("recordButton"); | ||
recordButton.addEventListener("click",startRecording); | ||
var videoDuration = 10000; //milliseconds | ||
|
||
var canvas = document.getElementById('canvas'); | ||
var width = canvas.width; | ||
var height = canvas.height; | ||
var capturing = false; | ||
|
||
// We need the 2D context to individually manipulate pixel data | ||
var ctx = canvas.getContext('2d'); | ||
|
||
// Start with a black background | ||
ctx.fillStyle = '#000'; | ||
ctx.fillRect(0, 0, width, height); | ||
|
||
// Since we're continuously accessing and overwriting the pixels | ||
// object, we'll request it once and reuse it across calls to draw() | ||
// for best performance (we don't need to create ImageData objects | ||
// on every frame) | ||
var pixels = ctx.getImageData(0, 0, width, height); | ||
var data = pixels.data; | ||
var numPixels = data.length; | ||
|
||
var fps = 24; | ||
var stream = canvas.captureStream(fps); | ||
var recorder = new MediaRecorder(stream, { 'type': 'video/mp4' }); | ||
recorder.addEventListener('dataavailable', finishCapturing); | ||
|
||
//main method | ||
draw(); | ||
|
||
function startRecording(){ | ||
recorder.start(); //moved here | ||
capturing = true; | ||
|
||
setTimeout(function() { | ||
recorder.stop(); | ||
}, videoDuration); | ||
} | ||
|
||
function finishCapturing(e) { | ||
//capturing = false; | ||
var videoData = [ e.data ]; | ||
finishedBlob = new Blob(videoData, { 'type': 'video/mp4' }); | ||
console.log(finishedBlob); | ||
downloadBlob(finishedBlob); | ||
//var videoURL = URL.createObjectURL(finishedBlob); | ||
//video.src = videoURL; | ||
//video.play(); | ||
//downloadBlob(finishedBlob); | ||
} | ||
|
||
function draw() { | ||
// We don't want to render again if we're not capturing | ||
//requestAnimationFrame(draw); | ||
setInterval(drawWhiteNoise,1000/fps); | ||
//drawWhiteNoise(); | ||
} | ||
|
||
|
||
function drawWhiteNoise() { | ||
|
||
var offset = 0; | ||
for(var i = 0; i < numPixels; i++) { | ||
var grey = Math.round(Math.random() * 255); | ||
data[offset++] = grey; | ||
data[offset++] = grey; | ||
data[offset++] = grey; | ||
offset++; | ||
} | ||
|
||
// And tell the context to draw the updated pixels in the canvas | ||
ctx.putImageData(pixels, 0, 0); | ||
} | ||
|
||
|
||
function downloadBlob() { | ||
let url = window.URL.createObjectURL(finishedBlob); | ||
let a = document.createElement("a"); | ||
a.style.display = "none"; | ||
a.href = url; | ||
const date = new Date(); | ||
const filename = `video_${date.toLocaleDateString()}_${date.toLocaleTimeString()}.mp4`; | ||
a.download = filename; | ||
document.body.appendChild(a); | ||
a.click(); | ||
window.URL.revokeObjectURL(url); | ||
} |
Oops, something went wrong.