adds interactivity to services.swpc.noaa.gov/images/animations. uses prefect for a backend to scrape images generate movies with ffmpeg. flask frontend at /iframe + proxy server via bare domain and API access via /api.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

271 lines
10 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animator</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
margin: 0;
padding: 20px;
}
iframe {
width: 99%;
/* Full width to use available space */
height: 500px;
border: 1px solid #ccc;
margin-bottom: 20px;
}
#current-url {
font-size: 16px;
color: #333;
width: 100%;
/* Makes sure it takes the full container width */
word-wrap: break-word;
/* Ensures the text wraps in the div */
margin-bottom: 10px;
/* Space before the navigation buttons */
}
.button-group {
display: flex;
/* Aligns buttons in a row */
justify-content: center;
/* Centers buttons horizontally */
gap: 10px;
/* Adds space between buttons */
}
button {
padding: 5px 10px;
margin: 10px;
border-radius: 5px;
/* Rounded corners */
border: 1px solid #ccc;
/* Grey border */
display: inline-block;
/* Ensures buttons are inline and can control additional layout properties */
font-size: 16px;
}
#backButton,
#forwardButton {
background-color: #f0f0f0;
/* Light grey background */
color: #333;
/* Dark text */
padding: 0px 10px;
/* Reduced vertical padding for narrow height */
cursor: pointer;
/* Cursor indicates button */
height: 24px;
/* Fixed height for a narrower button */
line-height: 16px;
/* Adjust line height to vertically center the text */
margin: 2px;
/* Small margin to separate buttons slightly */
}
#backButton:hover,
#forwardButton:hover {
background-color: #e8e8e8;
/* Slightly darker background on hover */
}
video {
display: none;
/* Initially hide the video player */
width: 99%;
/* Adjust based on your layout needs, or use max-width for responsiveness */
height: auto;
/* Maintain aspect ratio */
margin-top: 20px;
/* Ensure it's centered properly */
max-width: 640px;
/* Max width of the video */
border: 1px solid #ccc;
/* Optional, adds a border for better visibility */
}
</style>
</head>
<body>
<h1>Animate a Folder of Images</h1>
<p>Navigate to a folder of 60+ images.</p>
<iframe id="iframe" src="{{ initial_url }}"></iframe>
<div class="button-group"> <!-- Button group for inline display -->
<button id="backButton" onclick="goBack()">&#8592;</button>
<button id="forwardButton" onclick="goForward()">&#8594;</button>
</div>
<button id="submit-button" onclick="submitUrl()" style="display:none;">Create Latest Movie</button>
<div id="loading-spinner" style="display: none;">
<div class="spinner"></div>
</div>
<style>
.spinner {
border: 8px solid #f3f3f3;
/* Light grey */
border-top: 8px solid #3498db;
/* Blue */
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 45s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<video id="video-player" controls loop style="display: none;">
<source id="video-source" type="video/mp4">
Your browser does not support the video tag.
</video>
<script>
function goBack() {
document.getElementById('iframe').contentWindow.history.back();
}
function goForward() {
document.getElementById('iframe').contentWindow.history.forward();
}
// function updateUrl(url) {
// document.getElementById('url').textContent = url;
// }
function updateUrl(url) {
document.getElementById('url').textContent = url;
}
function handleVideoSuccess() {
console.log("Video loaded successfully.");
document.getElementById('video-player').style.display = 'block'; // Show the video player only if the video loads successfully
}
function handleVideoError() {
console.log("Unable to load video.");
document.getElementById('video-player').style.display = 'none'; // Hide the video player
document.getElementById('submit-button').textContent = 'Generate Movie';
}
function updateVideo(url) {
// Convert the full URL to a format suitable for your video path
let formattedPath = url.replace(/https?:\/\//, '') // Remove the protocol part
.replace(/\./g, '_') // Replace dots with underscores
.replace(/\//g, '-'); // Replace slashes with hyphens
// Check if the formattedPath ends with a slash, if not append '-'
if (!formattedPath.endsWith('-')) {
formattedPath += '-';
}
// Append '.mp4' to the formatted path + cache bust
let videoPath = `${formattedPath}latest.mp4` + "?t=" + Date.now();
let videoPlayer = document.getElementById('video-player');
let videoSource = document.getElementById('video-source');
videoPlayer.muted = true;
// Set up event listeners before setting the source
videoSource.onerror = handleVideoError;
// videoSource.onloadedmetadata = handleVideoSuccess;
console.log("Fetched latest")
videoSource.src = `/videos/${videoPath}`;
videoPlayer.load();
// videoPlayer.style.display = 'block';
videoPlayer.play().then(() => {
// The video is playing, show the player
console.log("Video loaded and playing.");
videoPlayer.style.display = 'block';
}).catch(error => {
// Error playing the video
console.log("Failed to play video: ", error);
videoPlayer.style.display = 'none';
});
document.getElementById('submit-button').textContent = 'Generate Latest Movie';
}
window.addEventListener('message', function (event) {
if (event.origin === '{{ host }}') {
var data = event.data;
if (data && data.type === 'urlUpdate') {
const submitButton = document.getElementById('submit-button');
const videoPlayer = document.getElementById('video-player');
updateUrl(data.url);
if (data.eligible) {
submitButton.style.display = 'block'; // Show the button
updateVideo(data.url);
} else {
submitButton.style.display = 'none'; // Hide the button
videoPlayer.style.display = 'none'; // Hide the video
}
const newSubpath = new URL(data.url).pathname; // Extract the path from the URL
// Update the browser's URL to reflect the iframe's navigation
const newPath = `/iframe${newSubpath}`; // Construct the new path
document.getElementById('share-button').setAttribute('data-url', window.location.origin + newPath);
// history.pushState({ path: newPath }, '', newPath);
}
}
});
function submitUrl() {
const url = document.getElementById('url').textContent;
const payload = { url: url };
document.getElementById('loading-spinner').style.display = 'block'; // Show the loading spinner
document.getElementById('submit-button').style.display = 'none'; // Hide the button
console.log("Requesting new video.")
fetch('{{ api_url }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
}).then(response => response.json())
.then(data => {
console.log(data);
// Hide the loading spinner
document.getElementById('loading-spinner').style.display = 'none';
document.getElementById('submit-button').style.display = 'block';
updateUrl(url);
// Re-attempt to load the video
updateVideo(url);
})
.catch(error => {
console.error('Error:', error);
// Hide the loading spinner
document.getElementById('loading-spinner').style.display = 'none';
document.getElementById('submit-button').style.display = 'block';
});
}
function copyUrlToClipboard() {
const url = document.getElementById('share-button').getAttribute('data-url');
navigator.clipboard.writeText(url).then(() => {
alert('URL copied to clipboard!');
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
</script>
<button id="share-button" onclick="copyUrlToClipboard()">Share Link</button>
<div align="center" id="current-url">Source: <span id="url">Loading...</span></div>
</body>
</html>