# VFX2 . ├── index.html ├── index.js ├── LICENSE ├── package.json ├── package-lock.json ├── README.md ├── script.js ├── styles.css └── summary.txt 1 directory, 9 files VFX2
const express = require('express'); const app = express(); const path = require('path'); const fs = require('fs'); const port = 8000 const assetsDir = path.join(__dirname, 'assets'); app.use(express.static(path.join(__dirname))); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')); }); app.get('/api/videos', (req, res) => { fs.readdir(assetsDir, (err, files) => { if (err) { console.error('Error reading directory:', err); res.status(500).send('Internal Server Error'); return; } const webmFiles = files.filter(file => file.endsWith('.webm')); const videoFiles = webmFiles.map(file => ({ src: `assets/${file}` })); res.json(videoFiles); }); }); app.listen(port, () => { console.log('Server started on port', port); }); // Define a debug mode variable let debugMode = true const canvas = document.getElementById("videoCanvas") const ctx = canvas.getContext("2d") let videosToLoad = 2 const maxVideosToLoad = 4 const videos = [] let videosLoaded = 0 let aspectRatio = 1 let cachedVideos = [] let showText = true const parameterStore = { blendModeParams: { mode: "screen", opacity: 1, }, filterParams: { grayscale: 0, blur: 0, }, transformParams: { scale: 1, // Additional parameters specific to the tile mode or other transformation tilePositionX: 0, // X-coordinate for positioning the tiled video tilePositionY: 0, // Y-coordinate for positioning the tiled video tileScaleX: 1, // Scale factor along the X-axis for the tiled video tileScaleY: 1, // Scale factor along the Y-axis for the tiled video }, presets: { default: { blendModeParams: { mode: "screen", opacity: 1 }, }, sepia: { blendModeParams: { mode: "overlay", opacity: 0.5 }, }, tile: { tilePositionX: 0, tilePositionY: 0, tileScaleX: 1, tileScaleY: 1, }, // Add more presets as needed }, selectedPreset: "default", }; function applyPreset(presetName) { const preset = parameterStore.presets[presetName]; if (preset) { for (const [category, categoryParams] of Object.entries(preset)) { // Check if the category exists in the parameter store if (parameterStore.hasOwnProperty(category)) { // Iterate over each parameter in the category for (const [paramKey, paramValue] of Object.entries(categoryParams)) { // Update the corresponding parameter in the parameter store parameterStore[category][paramKey] = paramValue; } } else { console.error(`Category "${category}" not found in parameter store.`); } } // Update selected preset parameterStore.selectedPreset = presetName; } else { console.error(`Preset "${presetName}" not found.`); } } function debugLog(...args) { if (debugMode) { console.log(...args) } } function loadVideos(num) { debugLog(`Loading ${num} videos`) if (num < 0) { num = Math.max(num,-Math.abs(maxVideosToLoad)) } else { num = Math.min(num,Math.abs(maxVideosToLoad)) } while (num > 0) { let video = createVideoElement() video.addEventListener("loadedmetadata", handleVideoLoaded) video.addEventListener("ended", handleVideoEnded) video.addEventListener("error", handleVideoEnded) video.src = selectRandomVideo(video) videos.push(video) num-- } while (num < 0) { let video = videos.pop() video.removeEventListener("loadedmetadata", handleVideoLoaded) video.removeEventListener("ended", handleVideoEnded) video.removeEventListener("error", handleVideoEnded) num++ } } function createVideoElement() { const video = document.createElement("video") video.autoplay = true video.muted = true return video } function handleVideoLoaded() { videosLoaded++ if (videosLoaded === videos.length) { updateCanvasSize() drawVideos() } } function handleVideoEnded(event) { const video = event.target //debugLog(video,event) selectRandomVideo(video) } function drawVideos() { ctx.clearRect(0, 0, canvas.width, canvas.height) videos.forEach((video,i) => { // Calculate scaling factor to fit the canvas const scale = Math.max(canvas.width/video.videoWidth, canvas.height/video.videoHeight) // Calculate scaled dimensions const scaledWidth = video.videoWidth * scale const scaledHeight = video.videoHeight * scale // Calculate horizontal and vertical centering offset const offsetX = (canvas.width-scaledWidth)/2 const offsetY = (canvas.height-scaledHeight)/2 // Use default blend mode for first video (i=0), repeat the rest of blendModes if (i === 0) { ctx.globalCompositeOperation = "source-over" } else { // Using values from parameterStore ctx.globalCompositeOperation = parameterStore.blendModeParams.mode ctx.globalAlpha = parameterStore.blendModeParams.opacity } video.globalCompositeOperation = ctx.globalCompositeOperation || "default" // Draw the current video frame onto the canvas ctx.drawImage(video, offsetX, offsetY, scaledWidth, scaledHeight) // Reset composite operation ctx.globalCompositeOperation = "source-over" }) // Get pixel data let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // If showText is true, draw debug text if (showText) { drawDebugText() } // Request next frame requestAnimationFrame(drawVideos) } function drawDebugText() { // Set font style and color ctx.font = "16px Arial" ctx.fillStyle = "white" let padding = 20 // Calculate the position for the active preset text at the top center of the canvas const presetText = `${parameterStore.selectedPreset}`; const textWidth = ctx.measureText(presetText).width; const centerX = canvas.width / 2; const positionX = centerX - textWidth / 2; const positionY = padding; // Set text alignment to center horizontally ctx.textAlign = "center"; ctx.textBaseline = "top"; // Draw the active preset text ctx.fillText(`${presetText}`, positionX, positionY); ctx.fillText(`${canvas.width}x${canvas.height} (${aspectRatio.toFixed(2)})`, positionX, positionY+20); let corners = [[0, 0], [1, 1], [0, 1], [1, 0]]; // Top-left, bottom-right, bottom-left, top-right videos.forEach((video, i) => { // Get the corner coordinates for the current video const corner = corners[i % corners.length] // Calculate the position of the text in the corner let positionX = corner[0] ? canvas.width : padding let positionY = corner[1] ? canvas.height : padding // Adjust position to ensure text is within the canvas bounds //positionX = Math.max(0, Math.min(positionX, canvas.width - ctx.measureText(getFilename(video.src)).width)) positionY = Math.max(0, Math.min(positionY, canvas.height - padding*3)) // Set text alignment based on corner ctx.textAlign = corner[0] ? "right" : "left" ctx.textBaseline = corner[1] ? "bottom" : "top" // Draw debug text for the video ctx.fillText(getFilename(video.src), positionX, positionY) ctx.fillText(`Dimensions: ${video.videoWidth}x${video.videoHeight} ()`, positionX, positionY + 20) ctx.fillText(formatTime(video.currentTime) + "/" + formatTime(video.duration), positionX, positionY + 40) ctx.fillText(`${video.globalCompositeOperation}` ,positionX,positionY+60) // Add more debug information as needed }) } // Function to extract filename from full path function getFilename(src) { const parts = src.split('/') return decodeURIComponent(parts[parts.length - 1].replace(/\%20/g, ' ')) } function formatTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = Math.floor(seconds % 60); return `${(hours ? hours.toString().padStart(2, '0') + ':' : '')}` + `${minutes.toString().padStart(2, '0')}:`+ `${remainingSeconds.toString().padStart(2, '0')}`; } function updateCanvasSize() { let maxAspectRatio = 0; // Calculate the maximum aspect ratio among all videos for (let i = 0; i < videos.length; i++) { const videoAspectRatio = videos[i].videoWidth / videos[i].videoHeight; maxAspectRatio = Math.max(maxAspectRatio, videoAspectRatio); } canvas.width = window.innerWidth; canvas.height = window.innerWidth / maxAspectRatio; } window.addEventListener("resize", updateCanvasSize) function getSourceFiles() { debugLog("Getting source files") return fetch("/api/videos") .then(response => response.json()) .then(videoFiles => { debugLog("Success") cachedVideos = videoFiles; }) .catch(error => console.error("Error fetching videos:", error)); } function selectRandomVideo(videoElement) { if (cachedVideos.length) { const currentSrc = videoElement.src; const filteredVideos = cachedVideos.filter(video => video.src !== currentSrc); if (filteredVideos.length > 0) { const randomIndex = Math.floor(Math.random() * filteredVideos.length); const randomVideo = filteredVideos[randomIndex]; videoElement.src = randomVideo.src; } else { debugLog("No other videos available."); getSourceFiles().then(()=>{ selectRandomVideo(videoElement) }) } } else { debugLog("Cache empty, doing new request") getSourceFiles().then(()=>{ selectRandomVideo(videoElement) }) } } document.addEventListener('keydown', function(event) { console.log('keyDown',event) if (event.key === 'Enter') { showText = !showText } if (event.key === 'ArrowDown') { loadVideos(-1) } if (event.key === 'ArrowUp') { loadVideos(1) } }) getSourceFiles().then(()=>{ loadVideos(videosToLoad) updateCanvasSize() })