Files
erosDiffusion-ComfyUI-Erosd…/gemini.html
T

786 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Video Timeline Editor</title>
<style>
:root {
--bg-body: #121212;
--bg-node: #1e1e1e;
--bg-overlay: #252525;
--text-main: #e0e0e0;
--text-dim: #a0a0a0;
--timeline-track: #333;
--timeline-filled: #444;
--accent-blue: #3b82f6;
--accent-orange: #f97316;
--border-color: #404040;
--input-bg: #2d2d2d;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
}
/* --- Node Container (The "Small" View) --- */
.node-container {
background: var(--bg-node);
border: 1px solid var(--border-color);
border-radius: 8px;
width: 400px;
padding: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.node-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.node-title {
font-size: 1.1rem;
font-weight: 600;
}
.control-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 0.9rem;
color: var(--text-dim);
}
input[type="number"] {
background: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 4px 8px;
border-radius: 4px;
width: 60px;
}
/* --- Timeline Base Styles --- */
.timeline-scroll-container {
width: 100%;
overflow-x: auto;
background: var(--timeline-track);
border: 1px solid var(--border-color);
border-radius: 4px;
position: relative;
margin-bottom: 12px;
height: 60px;
/* Custom Scrollbar */
scrollbar-width: thin;
scrollbar-color: #555 #333;
}
.timeline-scroll-container::-webkit-scrollbar {
height: 6px;
}
.timeline-scroll-container::-webkit-scrollbar-track {
background: #333;
}
.timeline-scroll-container::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
.timeline-track {
height: 100%;
position: relative;
min-width: 100%;
/* Ensures it fills container at minimum */
cursor: crosshair;
}
/* Grid lines for the track */
.timeline-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
background-image: linear-gradient(90deg, #444 1px, transparent 1px);
background-size: 50px 100%;
/* Arbitrary grid spacing */
opacity: 0.3;
}
.marker {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
/* Hit area width handled by pseudo element usually, but here simple */
background-color: var(--accent-blue);
cursor: ew-resize;
z-index: 10;
transition: background-color 0.2s;
}
/* Hit area expansion */
.marker::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -4px;
right: -4px;
cursor: ew-resize;
}
.marker.orange {
background-color: var(--accent-orange);
border-bottom: 4px solid #fff;
/* Visual indicator */
}
.marker:hover {
box-shadow: 0 0 4px rgba(255, 255, 255, 0.5);
}
.marker.selected {
background-color: #fff;
z-index: 20;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
}
.marker-label {
position: absolute;
bottom: 2px;
left: 4px;
font-size: 10px;
color: #fff;
pointer-events: none;
background: rgba(0, 0, 0, 0.5);
padding: 1px 3px;
border-radius: 2px;
}
.btn-primary {
background-color: #404040;
color: white;
border: 1px solid #555;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
width: 100%;
font-size: 0.9rem;
transition: background 0.2s;
}
.btn-primary:hover {
background-color: #505050;
}
/* --- Overlay Editor --- */
.overlay-backdrop {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 100;
justify-content: center;
align-items: center;
backdrop-filter: blur(2px);
}
.editor-modal {
background: var(--bg-overlay);
width: 90%;
max-width: 900px;
height: 80vh;
border-radius: 8px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
padding: 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
margin: 0;
font-size: 1.2rem;
}
.modal-close {
background: none;
border: none;
color: var(--text-dim);
font-size: 1.5rem;
cursor: pointer;
}
.modal-close:hover {
color: var(--text-main);
}
.modal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.instructions {
font-size: 0.85rem;
color: var(--text-dim);
background: #333;
padding: 8px;
border-radius: 4px;
border-left: 3px solid var(--accent-blue);
}
/* --- Editor Specific Components --- */
.editor-timeline-area {
height: 120px;
/* Taller timeline in editor */
}
.properties-panel {
background: #1a1a1a;
border: 1px solid var(--border-color);
padding: 16px;
border-radius: 6px;
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
opacity: 0.5;
pointer-events: none;
transition: opacity 0.2s;
}
.properties-panel.active {
opacity: 1;
pointer-events: all;
}
.image-row {
display: flex;
align-items: center;
gap: 20px;
}
.image-uploader {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.uploader-label {
font-size: 0.8rem;
color: var(--text-dim);
}
.upload-box {
width: 100px;
height: 100px;
border: 2px dashed #555;
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
overflow: hidden;
background: #222;
transition: border-color 0.2s;
}
.upload-box:hover {
border-color: #777;
}
.upload-box span {
font-size: 2rem;
color: #555;
}
.uploaded-img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
}
.transition-toggle {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
color: #555;
}
.transition-toggle.active {
color: var(--accent-blue);
}
.transition-icon {
font-size: 1.5rem;
border: 1px solid transparent;
padding: 4px;
border-radius: 4px;
}
.transition-toggle.active .transition-icon {
border-color: var(--accent-blue);
}
textarea {
background: var(--input-bg);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 12px;
border-radius: 4px;
resize: none;
height: 80px;
font-family: inherit;
}
textarea:focus {
outline: 1px solid var(--accent-blue);
}
</style>
</head>
<body>
<!-- --- Node View --- -->
<div class="node-container">
<div class="node-header">
<span class="node-title">Video Timeline</span>
</div>
<div class="control-row">
<label>Duration (s):</label>
<input type="number" id="durationInput" value="30" min="5">
</div>
<!-- Mini Timeline Preview -->
<div class="timeline-scroll-container" style="height: 40px; overflow:hidden;">
<div id="miniTrack" class="timeline-track">
<!-- Markers go here -->
</div>
</div>
<button class="btn-primary" id="openEditorBtn">Open Editor</button>
</div>
<!-- --- Overlay Editor --- -->
<div class="overlay-backdrop" id="editorOverlay">
<div class="editor-modal">
<div class="modal-header">
<h2 class="modal-title">Timeline Editor</h2>
<button class="modal-close" id="closeEditorBtn">&times;</button>
</div>
<div class="modal-body">
<div class="instructions">
<strong>Controls:</strong> Click to add marker. <strong>Alt+Click</strong> for orange frame. Drag to
move. <strong>Ctrl+Click</strong> to remove.
</div>
<!-- Main Editor Timeline -->
<div class="timeline-scroll-container editor-timeline-area" id="mainScrollContainer">
<div id="mainTrack" class="timeline-track">
<div class="timeline-grid"></div>
<!-- Markers go here -->
</div>
</div>
<!-- Selected Marker Properties -->
<div class="properties-panel" id="propertiesPanel">
<div class="image-row">
<!-- Start Image (or Frame Image) -->
<div class="image-uploader">
<span class="uploader-label" id="labelImg1">Start Image</span>
<div class="upload-box" id="boxImg1">
<span>+</span>
</div>
<input type="file" id="fileImg1" hidden accept="image/*">
</div>
<!-- Transition Control -->
<div class="transition-toggle" id="cutTransition">
<div class="transition-icon">✂️</div>
<span style="font-size:0.7rem; margin-top:4px;">CUT</span>
</div>
<!-- End Image (Hidden for orange markers) -->
<div class="image-uploader" id="wrapperImg2">
<span class="uploader-label">End Image</span>
<div class="upload-box" id="boxImg2">
<span>+</span>
</div>
<input type="file" id="fileImg2" hidden accept="image/*">
</div>
</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<label style="font-size:0.9rem; color:var(--text-dim);">Prompt / Notes</label>
<textarea id="promptInput" placeholder="Enter prompt or details for this segment..."></textarea>
</div>
</div>
</div>
</div>
</div>
<script>
// --- State Management ---
const state = {
duration: 30, // seconds
markers: [],
selectedId: null,
pixelsPerSec: 20 // Zoom level in editor
};
// --- DOM Elements ---
const durationInput = document.getElementById('durationInput');
const openEditorBtn = document.getElementById('openEditorBtn');
const closeEditorBtn = document.getElementById('closeEditorBtn');
const editorOverlay = document.getElementById('editorOverlay');
const mainScrollContainer = document.getElementById('mainScrollContainer');
const mainTrack = document.getElementById('mainTrack');
const miniTrack = document.getElementById('miniTrack');
const propertiesPanel = document.getElementById('propertiesPanel');
const labelImg1 = document.getElementById('labelImg1');
const wrapperImg2 = document.getElementById('wrapperImg2');
const cutTransition = document.getElementById('cutTransition');
const promptInput = document.getElementById('promptInput');
const uploaders = [
{ box: document.getElementById('boxImg1'), input: document.getElementById('fileImg1'), key: 'img1' },
{ box: document.getElementById('boxImg2'), input: document.getElementById('fileImg2'), key: 'img2' }
];
// --- Initialization ---
function init() {
durationInput.addEventListener('change', (e) => updateDuration(e.target.value));
openEditorBtn.addEventListener('click', () => {
editorOverlay.style.display = 'flex';
render();
});
closeEditorBtn.addEventListener('click', () => {
editorOverlay.style.display = 'none';
render(); // Re-render mini view
});
// Track Interactions
mainTrack.addEventListener('mousedown', handleTrackClick);
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag);
// Property Inputs
promptInput.addEventListener('input', (e) => updateMarkerData('prompt', e.target.value));
cutTransition.addEventListener('click', () => toggleTransition());
// Image Uploaders
uploaders.forEach((u, index) => setupUploader(u, index));
// Initial Render
render();
}
// --- Core Logic ---
function updateDuration(val) {
let v = parseFloat(val);
if (v < 1) v = 1;
state.duration = v;
durationInput.value = v;
// Remove markers out of bounds
state.markers = state.markers.filter(m => m.time <= state.duration);
render();
}
function createMarker(time, type) {
return {
id: 'm_' + Date.now() + Math.random().toString(36).substr(2, 5),
time: time,
type: type, // 'standard' or 'orange'
prompt: '',
img1: null,
img2: null,
cut: false
};
}
// --- Interaction Handlers ---
function handleTrackClick(e) {
if (e.target.closest('.marker')) return; // handled by marker mousedown
const rect = mainTrack.getBoundingClientRect();
const x = e.clientX - rect.left;
const time = (x / rect.width) * state.duration;
if (time < 0 || time > state.duration) return;
const type = e.altKey ? 'orange' : 'standard';
const newMarker = createMarker(time, type);
state.markers.push(newMarker);
selectMarker(newMarker.id);
render();
}
let dragTargetId = null;
function startDrag(e, id) {
e.stopPropagation(); // prevent track creation
if (e.ctrlKey) {
// Delete
state.markers = state.markers.filter(m => m.id !== id);
if (state.selectedId === id) selectMarker(null);
render();
return;
}
selectMarker(id);
dragTargetId = id;
mainTrack.style.cursor = 'ew-resize';
}
function handleDrag(e) {
if (!dragTargetId) return;
const rect = mainTrack.getBoundingClientRect();
let x = e.clientX - rect.left;
// Clamp visual bounds
if (x < 0) x = 0;
if (x > rect.width) x = rect.width;
const newTime = (x / rect.width) * state.duration;
const m = state.markers.find(m => m.id === dragTargetId);
if (m) {
m.time = newTime;
render(); // This is efficient enough for this scale
}
}
function endDrag() {
dragTargetId = null;
mainTrack.style.cursor = 'crosshair';
}
function selectMarker(id) {
state.selectedId = id;
renderProperties();
render(); // to update visual selection class
}
function updateMarkerData(key, value) {
if (!state.selectedId) return;
const m = state.markers.find(m => m.id === state.selectedId);
if (m) m[key] = value;
}
function toggleTransition() {
if (!state.selectedId) return;
const m = state.markers.find(m => m.id === state.selectedId);
if (m) {
m.cut = !m.cut;
renderProperties();
}
}
// --- Rendering ---
function render() {
// 1. Calculate Widths
// Editor: fixed pixels per second
const editorWidth = state.duration * state.pixelsPerSec;
mainTrack.style.width = `${editorWidth}px`;
// Mini: 100% width
miniTrack.style.width = '100%';
// 2. Clear
mainTrack.innerHTML = '<div class="timeline-grid"></div>'; // keep grid
miniTrack.innerHTML = '';
// 3. Render Markers
state.markers.forEach(m => {
// Editor Marker
const elMain = createMarkerElement(m, true);
mainTrack.appendChild(elMain);
// Mini Marker
const elMini = createMarkerElement(m, false);
miniTrack.appendChild(elMini);
});
}
function createMarkerElement(m, isEditor) {
const el = document.createElement('div');
el.className = `marker ${m.type}`;
if (isEditor && m.id === state.selectedId) el.classList.add('selected');
// Position percentage
const pct = (m.time / state.duration) * 100;
el.style.left = `${pct}%`;
if (isEditor) {
// Add timestamp label
const label = document.createElement('div');
label.className = 'marker-label';
label.innerText = m.time.toFixed(1) + 's';
el.appendChild(label);
// Event
el.addEventListener('mousedown', (e) => startDrag(e, m.id));
}
return el;
}
function renderProperties() {
if (!state.selectedId) {
propertiesPanel.classList.remove('active');
return;
}
const m = state.markers.find(m => m.id === state.selectedId);
if (!m) return; // Should not happen
propertiesPanel.classList.add('active');
promptInput.value = m.prompt;
// Type specific UI
if (m.type === 'orange') {
labelImg1.innerText = "Frame Image";
wrapperImg2.style.display = 'none';
} else {
labelImg1.innerText = "Start Image";
wrapperImg2.style.display = 'flex';
}
// Transition State
if (m.cut) cutTransition.classList.add('active');
else cutTransition.classList.remove('active');
// Images
updateUploaderBox(0, m.img1);
updateUploaderBox(1, m.img2);
}
// --- Image Handling ---
function setupUploader(u, index) {
// Click box to trigger file or delete
u.box.addEventListener('click', (e) => {
const m = state.markers.find(m => m.id === state.selectedId);
if (!m) return;
if (m[u.key]) {
// Remove existing
m[u.key] = null;
renderProperties();
} else {
u.input.click();
}
});
// File Change
u.input.addEventListener('change', (e) => {
if (e.target.files && e.target.files[0]) {
handleFileUpload(e.target.files[0], u.key);
e.target.value = ''; // reset
}
});
// Drag Drop
u.box.addEventListener('dragover', (e) => {
e.preventDefault();
u.box.style.borderColor = '#fff';
});
u.box.addEventListener('dragleave', (e) => {
e.preventDefault();
u.box.style.borderColor = '';
});
u.box.addEventListener('drop', (e) => {
e.preventDefault();
u.box.style.borderColor = '';
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
handleFileUpload(e.dataTransfer.files[0], u.key);
}
});
}
function handleFileUpload(file, key) {
if (!state.selectedId) return;
// Mock Upload to ComfyUI (Local URL)
const url = URL.createObjectURL(file);
const m = state.markers.find(m => m.id === state.selectedId);
if (m) {
m[key] = url;
renderProperties();
}
}
function updateUploaderBox(index, url) {
const u = uploaders[index];
u.box.innerHTML = '';
if (url) {
const img = document.createElement('img');
img.src = url;
img.className = 'uploaded-img';
u.box.appendChild(img);
} else {
const span = document.createElement('span');
span.innerText = '+';
u.box.appendChild(span);
}
}
// Start
init();
</script>
</body>
</html>