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.
365 lines
10 KiB
365 lines
10 KiB
# VFX2 |
|
|
|
. |
|
├── index.html |
|
├── index.js |
|
├── LICENSE |
|
├── package.json |
|
├── package-lock.json |
|
├── README.md |
|
├── script.js |
|
├── styles.css |
|
└── summary.txt |
|
|
|
1 directory, 9 files |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<title>VFX2</title> |
|
<link rel="stylesheet" href="styles.css"> |
|
</head> |
|
<body> |
|
<div id="visualizer"> |
|
|
|
<canvas id="videoCanvas"></canvas> |
|
|
|
<div class="glitch-layer"> |
|
<!-- Add occasional glitch effects here --> |
|
</div> |
|
</div> |
|
|
|
<script src="script.js"></script> |
|
</body> |
|
</html> |
|
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() |
|
})
|
|
|