Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Video Processing Workflow</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --bg-dark: #1a1a1f; | |
| --bg-card: #252530; | |
| --bg-input: #1e1e28; | |
| --text-primary: #e0e0e0; | |
| --text-secondary: #888; | |
| --border-color: #3a3a4a; | |
| --accent: #6366f1; | |
| --accent-hover: #818cf8; | |
| --node-header-height: 28px; | |
| } | |
| body { | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| background: var(--bg-dark); | |
| color: var(--text-primary); | |
| overflow: hidden; | |
| height: 100vh; | |
| } | |
| /* Header */ | |
| .header { | |
| background: linear-gradient(135deg, #252530 0%, #1a1a1f 100%); | |
| padding: 12px 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-bottom: 1px solid var(--border-color); | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.3); | |
| z-index: 100; | |
| position: relative; | |
| } | |
| .header-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .logo { | |
| font-size: 20px; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #6366f1, #a855f7); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .logo a { | |
| text-decoration: none; | |
| color: inherit; | |
| } | |
| .anycoder-link { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| text-decoration: none; | |
| padding: 4px 10px; | |
| background: rgba(99, 102, 241, 0.1); | |
| border-radius: 12px; | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .anycoder-link:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| border-color: rgba(99, 102, 241, 0.5); | |
| } | |
| .header-title { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 12px; | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| font-weight: 500; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .btn-primary { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| background: var(--accent-hover); | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| border: 1px solid var(--border-color); | |
| } | |
| .btn-secondary:hover { | |
| background: var(--border-color); | |
| } | |
| /* Toolbar */ | |
| .toolbar { | |
| background: var(--bg-card); | |
| padding: 8px 16px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| .toolbar-btn { | |
| width: 32px; | |
| height: 32px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| border-radius: 6px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.2s ease; | |
| } | |
| .toolbar-btn:hover { | |
| background: var(--border-color); | |
| color: var(--text-primary); | |
| } | |
| .toolbar-btn.active { | |
| background: var(--accent); | |
| color: white; | |
| } | |
| .toolbar-divider { | |
| width: 1px; | |
| height: 24px; | |
| background: var(--border-color); | |
| margin: 0 8px; | |
| } | |
| /* Main Workspace */ | |
| .workspace { | |
| display: flex; | |
| height: calc(100vh - 100px); | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| width: 260px; | |
| background: var(--bg-card); | |
| border-right: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s ease; | |
| } | |
| .sidebar.collapsed { | |
| transform: translateX(-260px); | |
| } | |
| .sidebar-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border-color); | |
| font-weight: 600; | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .sidebar-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 12px; | |
| } | |
| .node-template { | |
| background: var(--bg-input); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| padding: 12px; | |
| margin-bottom: 8px; | |
| cursor: grab; | |
| transition: all 0.2s ease; | |
| } | |
| .node-template:hover { | |
| border-color: var(--accent); | |
| transform: translateX(4px); | |
| } | |
| .node-template-name { | |
| font-weight: 500; | |
| font-size: 13px; | |
| margin-bottom: 4px; | |
| } | |
| .node-template-type { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| /* Canvas Area */ | |
| .canvas-container { | |
| flex: 1; | |
| position: relative; | |
| overflow: hidden; | |
| background: | |
| radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.03) 0%, transparent 50%), | |
| linear-gradient(rgba(40, 40, 55, 0.3) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(40, 40, 55, 0.3) 1px, transparent 1px); | |
| background-size: 100% 100%, 30px 30px, 30px 30px; | |
| } | |
| .canvas { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| transform-origin: 0 0; | |
| } | |
| /* Groups */ | |
| .group { | |
| position: absolute; | |
| border: 2px dashed; | |
| border-radius: 8px; | |
| pointer-events: none; | |
| } | |
| .group-title { | |
| position: absolute; | |
| top: -20px; | |
| left: 8px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| padding: 2px 8px; | |
| border-radius: 4px; | |
| white-space: nowrap; | |
| } | |
| /* Nodes */ | |
| .node { | |
| position: absolute; | |
| background: var(--bg-card); | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | |
| border: 1px solid var(--border-color); | |
| min-width: 200px; | |
| cursor: move; | |
| transition: box-shadow 0.2s ease, border-color 0.2s ease; | |
| user-select: none; | |
| } | |
| .node:hover { | |
| box-shadow: 0 6px 30px rgba(0, 0, 0, 0.5); | |
| } | |
| .node.selected { | |
| border-color: var(--accent); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3), 0 6px 30px rgba(0, 0, 0, 0.5); | |
| } | |
| .node-header { | |
| height: var(--node-header-height); | |
| padding: 0 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-radius: 8px 8px 0 0; | |
| font-size: 12px; | |
| font-weight: 600; | |
| } | |
| .node-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .node-actions { | |
| display: flex; | |
| gap: 4px; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .node:hover .node-actions { | |
| opacity: 1; | |
| } | |
| .node-action-btn { | |
| width: 20px; | |
| height: 20px; | |
| border: none; | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: background 0.2s ease; | |
| } | |
| .node-action-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| .node-action-btn.close:hover { | |
| background: #ef4444; | |
| } | |
| .node-content { | |
| padding: 8px 12px 12px; | |
| } | |
| /* Slots */ | |
| .node-slots { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 8px; | |
| } | |
| .slots-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .slot { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| min-width: 100px; | |
| } | |
| .slot:hover { | |
| background: rgba(255, 255, 255, 0.05); | |
| } | |
| .slot.input { | |
| justify-content: flex-start; | |
| } | |
| .slot.output { | |
| justify-content: flex-end; | |
| flex-direction: row-reverse; | |
| } | |
| .slot-socket { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| border: 2px solid; | |
| flex-shrink: 0; | |
| transition: transform 0.2s ease, box-shadow 0.2s ease; | |
| } | |
| .slot-socket:hover { | |
| transform: scale(1.3); | |
| box-shadow: 0 0 10px currentColor; | |
| } | |
| .slot.input .slot-socket { | |
| background: var(--bg-input); | |
| } | |
| .slot.output .slot-socket { | |
| background: var(--bg-input); | |
| } | |
| .slot-label { | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| white-space: nowrap; | |
| } | |
| .slot-type { | |
| font-size: 9px; | |
| padding: 1px 4px; | |
| border-radius: 3px; | |
| opacity: 0.7; | |
| } | |
| /* Widgets */ | |
| .node-widgets { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 8px; | |
| } | |
| .widget { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .widget-label { | |
| font-size: 10px; | |
| color: var(--text-secondary); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .widget input[type="text"], | |
| .widget input[type="number"] { | |
| background: var(--bg-input); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| padding: 6px 10px; | |
| color: var(--text-primary); | |
| font-size: 12px; | |
| width: 100%; | |
| transition: border-color 0.2s ease; | |
| } | |
| .widget input:focus { | |
| outline: none; | |
| border-color: var(--accent); | |
| } | |
| .widget input[type="range"] { | |
| width: 100%; | |
| height: 4px; | |
| border-radius: 2px; | |
| background: var(--bg-input); | |
| -webkit-appearance: none; | |
| appearance: none; | |
| cursor: pointer; | |
| } | |
| .widget input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 14px; | |
| height: 14px; | |
| border-radius: 50%; | |
| background: var(--accent); | |
| cursor: pointer; | |
| transition: transform 0.2s ease; | |
| } | |
| .widget input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| } | |
| .widget select { | |
| background: var(--bg-input); | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| padding: 6px 10px; | |
| color: var(--text-primary); | |
| font-size: 12px; | |
| width: 100%; | |
| cursor: pointer; | |
| } | |
| .widget button { | |
| background: var(--accent); | |
| border: none; | |
| border-radius: 4px; | |
| padding: 8px 16px; | |
| color: white; | |
| font-size: 12px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .widget button:hover { | |
| background: var(--accent-hover); | |
| } | |
| .widget button:active { | |
| transform: scale(0.98); | |
| } | |
| .widget-checkbox { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .widget-checkbox input { | |
| width: 16px; | |
| height: 16px; | |
| accent-color: var(--accent); | |
| cursor: pointer; | |
| } | |
| .widget-checkbox label { | |
| font-size: 12px; | |
| cursor: pointer; | |
| } | |
| /* Connections SVG */ | |
| .connections-svg { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| .connection-path { | |
| fill: none; | |
| stroke-width: 2; | |
| stroke-linecap: round; | |
| transition: stroke-width 0.2s ease; | |
| } | |
| .connection-path:hover { | |
| stroke-width: 3; | |
| } | |
| /* Mini Map */ | |
| .minimap { | |
| position: absolute; | |
| bottom: 20px; | |
| right: 20px; | |
| width: 200px; | |
| height: 150px; | |
| background: rgba(37, 37, 48, 0.9); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| z-index: 50; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | |
| } | |
| .minimap-canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Properties Panel */ | |
| .properties-panel { | |
| width: 300px; | |
| background: var(--bg-card); | |
| border-left: 1px solid var(--border-color); | |
| display: flex; | |
| flex-direction: column; | |
| transition: transform 0.3s ease; | |
| } | |
| .properties-panel.collapsed { | |
| transform: translateX(300px); | |
| } | |
| .properties-header { | |
| padding: 16px; | |
| border-bottom: 1px solid var(--border-color); | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .properties-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 16px; | |
| } | |
| .property-item { | |
| margin-bottom: 16px; | |
| } | |
| .property-label { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| margin-bottom: 6px; | |
| } | |
| .property-value { | |
| font-size: 14px; | |
| padding: 8px 12px; | |
| background: var(--bg-input); | |
| border-radius: 4px; | |
| border: 1px solid var(--border-color); | |
| } | |
| /* Status Bar */ | |
| .status-bar { | |
| background: var(--bg-card); | |
| padding: 6px 16px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| border-top: 1px solid var(--border-color); | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| } | |
| .status-items { | |
| display: flex; | |
| gap: 16px; | |
| } | |
| .status-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #22c55e; | |
| } | |
| /* Preview Panel */ | |
| .preview-panel { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 320px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| z-index: 50; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | |
| display: none; | |
| } | |
| .preview-panel.visible { | |
| display: block; | |
| } | |
| .preview-header { | |
| padding: 12px 16px; | |
| border-bottom: 1px solid var(--border-color); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-weight: 600; | |
| } | |
| .preview-close { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| font-size: 14px; | |
| } | |
| .preview-content { | |
| padding: 16px; | |
| } | |
| .preview-canvas { | |
| width: 100%; | |
| aspect-ratio: 16/9; | |
| background: var(--bg-input); | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--text-secondary); | |
| } | |
| .preview-controls { | |
| display: flex; | |
| gap: 8px; | |
| margin-top: 12px; | |
| } | |
| /* Animations */ | |
| @keyframes nodeAppear { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.9); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| } | |
| .node { | |
| animation: nodeAppear 0.3s ease; | |
| } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border-color); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-secondary); | |
| } | |
| /* Context Menu */ | |
| .context-menu { | |
| position: fixed; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | |
| padding: 8px 0; | |
| z-index: 1000; | |
| min-width: 180px; | |
| display: none; | |
| } | |
| .context-menu.visible { | |
| display: block; | |
| } | |
| .context-menu-item { | |
| padding: 8px 16px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 13px; | |
| transition: background 0.2s ease; | |
| } | |
| .context-menu-item:hover { | |
| background: var(--border-color); | |
| } | |
| .context-menu-item i { | |
| width: 16px; | |
| color: var(--text-secondary); | |
| } | |
| .context-menu-divider { | |
| height: 1px; | |
| background: var(--border-color); | |
| margin: 4px 0; | |
| } | |
| /* Tooltip */ | |
| .tooltip { | |
| position: fixed; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border-color); | |
| padding: 6px 10px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| z-index: 1000; | |
| pointer-events: none; | |
| display: none; | |
| } | |
| .tooltip.visible { | |
| display: block; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 1024px) { | |
| .sidebar { | |
| position: absolute; | |
| z-index: 50; | |
| height: calc(100vh - 100px); | |
| } | |
| .properties-panel { | |
| position: absolute; | |
| right: 0; | |
| height: calc(100vh - 100px); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .header { | |
| padding: 8px 16px; | |
| } | |
| .logo { | |
| font-size: 16px; | |
| } | |
| .anycoder-link { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="logo"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">🎬 VideoFlow</a> | |
| </div> | |
| <span class="header-title">Video Processing Workflow</span> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link">Built with anycoder</a> | |
| </div> | |
| <div class="header-actions"> | |
| <button class="btn btn-secondary" onclick="exportWorkflow()"> | |
| <i class="fas fa-download"></i> Export | |
| </button> | |
| <button class="btn btn-secondary" onclick="importWorkflow()"> | |
| <i class="fas fa-upload"></i> Import | |
| </button> | |
| <button class="btn btn-primary" onclick="runWorkflow()"> | |
| <i class="fas fa-play"></i> Run | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Toolbar --> | |
| <div class="toolbar"> | |
| <button class="toolbar-btn" title="Zoom In" onclick="zoomIn()"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| <button class="toolbar-btn" title="Zoom Out" onclick="zoomOut()"> | |
| <i class="fas fa-minus"></i> | |
| </button> | |
| <button class="toolbar-btn" title="Fit View" onclick="fitView()"> | |
| <i class="fas fa-expand"></i> | |
| </button> | |
| <button class="toolbar-btn" title="Grid" id="gridToggle" onclick="toggleGrid()"> | |
| <i class="fas fa-th"></i> | |
| </button> | |
| <div class="toolbar-divider"></div> | |
| <button class="toolbar-btn" title="Add Node" onclick="showNodePalette()"> | |
| <i class="fas fa-plus-circle"></i> | |
| </button> | |
| <button class="toolbar-btn" title="Clear All" onclick="clearAll()"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| <div class="toolbar-divider"></div> | |
| <button class="toolbar-btn" title="Play/Pause" id="playPauseBtn" onclick="togglePlayback()"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <button class="toolbar-btn" title="Preview" onclick="togglePreview()"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| </div> | |
| <!-- Main Workspace --> | |
| <div class="workspace"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <i class="fas fa-cube"></i> Node Library | |
| </div> | |
| <div class="sidebar-content"> | |
| <div class="node-template" draggable="true" data-type="VideoInput"> | |
| <div class="node-template-name"><i class="fas fa-video"></i> Video Input</div> | |
| <div class="node-template-type">Input source for video files</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="PrimitiveNode"> | |
| <div class="node-template-name"><i class="fas fa-hashtag"></i> Primitive</div> | |
| <div class="node-template-type">Basic value input node</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="VideoFrameSampler"> | |
| <div class="node-template-name"><i class="fas fa-images"></i> Frame Sampler</div> | |
| <div class="node-template-type">Extract frames from video</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="CanvasAutoCrop"> | |
| <div class="node-template-name"><i class="fas fa-crop-alt"></i> Auto Crop</div> | |
| <div class="node-template-type">Automatically crop images</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="VideoMatchMixer"> | |
| <div class="node-template-name"><i class="fas fa-layer-group"></i> Match Mixer</div> | |
| <div class="node-template-type">Blend frames with formula</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="CanvasEffect"> | |
| <div class="node-template-name"><i class="fas fa-magic"></i> Canvas Effect</div> | |
| <div class="node-template-type">Apply image effects</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="SaveVideo"> | |
| <div class="node-template-name"><i class="fas fa-save"></i> Save Video</div> | |
| <div class="node-template-type">Export processed video</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="PreviewVideo"> | |
| <div class="node-template-name"><i class="fas fa-play-circle"></i> Preview</div> | |
| <div class="node-template-type">Preview output frames</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="JavaScriptExecute"> | |
| <div class="node-template-name"><i class="fab fa-js"></i> JS Controller</div> | |
| <div class="node-template-type">JavaScript execution</div> | |
| </div> | |
| <div class="node-template" draggable="true" data-type="CanvasDisplay"> | |
| <div class="node-template-name"><i class="fas fa-desktop"></i> Canvas Display</div> | |
| <div class="node-template-type">Display processed frames</div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Canvas --> | |
| <div class="canvas-container" id="canvasContainer"> | |
| <div class="canvas" id="canvas"> | |
| <svg class="connections-svg" id="connectionsSvg"></svg> | |
| <div class="groups" id="groups"></div> | |
| <div class="nodes" id="nodes"></div> | |
| </div> | |
| <!-- Mini Map --> | |
| <div class="minimap" id="minimap"> | |
| <canvas class="minimap-canvas" id="minimapCanvas"></canvas> | |
| </div> | |
| <!-- Preview Panel --> | |
| <div class="preview-panel" id="previewPanel"> | |
| <div class="preview-header"> | |
| <span>Preview Output</span> | |
| <button class="preview-close" onclick="togglePreview()"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="preview-content"> | |
| <div class="preview-canvas" id="previewCanvas"> | |
| <i class="fas fa-film" style="font-size: 48px; margin-bottom: 8px;"></i> | |
| <div>Preview will appear here</div> | |
| </div> | |
| <div class="preview-controls"> | |
| <button class="btn btn-primary" style="flex: 1;" onclick="playPreview()"> | |
| <i class="fas fa-play"></i> Play | |
| </button> | |
| <button class="btn btn-secondary" onclick="pausePreview()"> | |
| <i class="fas fa-pause"></i> | |
| </button> | |
| <button class="btn btn-secondary" onclick="stopPreview()"> | |
| <i class="fas fa-stop"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Properties Panel --> | |
| <aside class="properties-panel" id="propertiesPanel"> | |
| <div class="properties-header"> | |
| <span>Properties</span> | |
| <button class="toolbar-btn" onclick="togglePropertiesPanel()"> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| </div> | |
| <div class="properties-content" id="propertiesContent"> | |
| <div style="color: var(--text-secondary); text-align: center; padding: 20px;"> | |
| <i class="fas fa-info-circle" style="font-size: 24px; margin-bottom: 8px;"></i> | |
| <div>Select a node to view properties</div> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Status Bar --> | |
| <footer class="status-bar"> | |
| <div class="status-items"> | |
| <div class="status-item"> | |
| <div class="status-dot" id="statusDot"></div> | |
| <span id="statusText">Ready</span> | |
| </div> | |
| <div class="status-item"> | |
| <i class="fas fa-mouse-pointer"></i> | |
| <span id="cursorPos">0, 0</span> | |
| </div> | |
| </div> | |
| <div class="status-items"> | |
| <div class="status-item"> | |
| <i class="fas fa-layer-group"></i> | |
| <span id="nodeCount">0 nodes</span> | |
| </div> | |
| <div class="status-item"> | |
| <i class="fas fa-link"></i> | |
| <span id="linkCount">0 links</span> | |
| </div> | |
| <div class="status-item"> | |
| <i class="fas fa-search"></i> | |
| <span id="zoomLevel">100%</span> | |
| </div> | |
| </div> | |
| </footer> | |
| <!-- Context Menu --> | |
| <div class="context-menu" id="contextMenu"> | |
| <div class="context-menu-item" onclick="duplicateSelected()"> | |
| <i class="fas fa-copy"></i> Duplicate | |
| </div> | |
| <div class="context-menu-item" onclick="copySelected()"> | |
| <i class="fas fa-cut"></i> Copy | |
| </div> | |
| <div class="context-menu-item" onclick="pasteSelected()"> | |
| <i class="fas fa-paste"></i> Paste | |
| </div> | |
| <div class="context-menu-divider"></div> | |
| <div class="context-menu-item" onclick="bringToFront()"> | |
| <i class="fas fa-arrow-up"></i> Bring to Front | |
| </div> | |
| <div class="context-menu-item" onclick="sendToBack()"> | |
| <i class="fas fa-arrow-down"></i> Send to Back | |
| </div> | |
| <div class="context-menu-divider"></div> | |
| <div class="context-menu-item" onclick="deleteSelected()"> | |
| <i class="fas fa-trash"></i> Delete | |
| </div> | |
| </div> | |
| <!-- Tooltip --> | |
| <div class="tooltip" id="tooltip"></div> | |
| <script> | |
| // Workflow Data | |
| let workflowData = null; | |
| let nodes = []; | |
| let links = []; | |
| let groups = []; | |
| let selectedNodes = []; | |
| let currentZoom = 1; | |
| let panOffset = { x: 0, y: 0 }; | |
| let isDragging = false; | |
| let isPanning = false; | |
| let dragStart = { x: 0, y: 0 }; | |
| let draggedNode = null; | |
| let connectionStart = null; | |
| let clipboard = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadWorkflowFromJSON(); | |
| setupEventListeners(); | |
| startRenderLoop(); | |
| }); | |
| // Load workflow from JSON | |
| function loadWorkflowFromJSON() { | |
| const jsonData = `<?json_output>`; | |
| try { | |
| workflowData = JSON.parse(jsonData.replace(/```json|```/g, '').trim()); | |
| if (workflowData.nodes) { | |
| nodes = workflowData.nodes.map(node => ({ | |
| ...node, | |
| x: node.pos[0], | |
| y: node.pos[1] | |
| })); | |
| } | |
| if (workflowData.links) { | |
| links = workflowData.links.map(link => ({ | |
| source: link[2], | |
| target: link[0], | |
| sourceOutput: link[3], | |
| targetInput: link[1] | |
| })); | |
| } | |
| if (workflowData.groups) { | |
| groups = workflowData.groups; | |
| } | |
| updateStats(); | |
| renderAll(); | |
| } catch (e) { | |
| console.error('Failed to load workflow:', e); | |
| // Use default workflow data | |
| initializeDefaultWorkflow(); | |
| } | |
| } | |
| function initializeDefaultWorkflow() { | |
| // Default nodes | |
| nodes = [ | |
| { | |
| id: 10, | |
| type: 'JavaScriptExecute', | |
| title: 'JS Controller', | |
| x: 100, | |
| y: 100, | |
| width: 400, | |
| height: 150, | |
| color: '#442266', | |
| bgcolor: '#553377', | |
| inputs: [], | |
| outputs: [{ name: 'trigger', type: 'EVENT', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'video_button', label: 'Video Input Button', type: 'button', value: 'Upload Video' }, | |
| { name: 'fps_input', label: 'FPS Input', type: 'number', value: 30, min: 1, max: 120 }, | |
| { name: 'formula_input', label: 'JS Math Formula', type: 'text', value: 'Math.sin(x) * Math.cos(y)' }, | |
| { name: 'rematch_button', label: 'Rematch Button', type: 'button', value: 'Rematch' } | |
| ] | |
| }, | |
| { | |
| id: 1, | |
| type: 'VideoInput', | |
| title: 'VideoInput', | |
| x: 100, | |
| y: 300, | |
| width: 315, | |
| height: 106, | |
| color: '#323224', | |
| bgcolor: '#434330', | |
| inputs: [], | |
| outputs: [{ name: 'VIDEO', type: 'VIDEO', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'video', label: 'Select Video', type: 'file' } | |
| ] | |
| }, | |
| { | |
| id: 2, | |
| type: 'PrimitiveNode', | |
| title: 'FPS_Input', | |
| x: 100, | |
| y: 450, | |
| width: 200, | |
| height: 50, | |
| color: '#223322', | |
| bgcolor: '#334433', | |
| inputs: [], | |
| outputs: [{ name: 'value', type: 'FLOAT', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'value', label: 'FPS', type: 'number', value: 30, min: 1, max: 120 } | |
| ] | |
| }, | |
| { | |
| id: 3, | |
| type: 'VideoFrameSampler', | |
| title: 'VideoFrameSampler', | |
| x: 500, | |
| y: 300, | |
| width: 315, | |
| height: 150, | |
| color: '#222244', | |
| bgcolor: '#333355', | |
| inputs: [ | |
| { name: 'video', type: 'VIDEO' }, | |
| { name: 'fps', type: 'FLOAT' } | |
| ], | |
| outputs: [ | |
| { name: 'frames', type: 'IMAGE', slotIndex: 0 }, | |
| { name: 'frame_count', type: 'INT', slotIndex: 1 } | |
| ], | |
| widgets: [ | |
| { name: 'start_frame', label: 'Start Frame', type: 'number', value: 0 }, | |
| { name: 'max_frames', label: 'Max Frames', type: 'number', value: 100 } | |
| ] | |
| }, | |
| { | |
| id: 4, | |
| type: 'PrimitiveNode', | |
| title: 'Math_Formula_Input', | |
| x: 500, | |
| y: 500, | |
| width: 280, | |
| height: 50, | |
| color: '#442222', | |
| bgcolor: '#553333', | |
| inputs: [], | |
| outputs: [{ name: 'value', type: 'STRING', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'value', label: 'Math Formula (JS)', type: 'text', value: 'x * y' } | |
| ] | |
| }, | |
| { | |
| id: 5, | |
| type: 'CanvasAutoCrop', | |
| title: 'CanvasAutoCrop', | |
| x: 900, | |
| y: 300, | |
| width: 315, | |
| height: 120, | |
| color: '#224422', | |
| bgcolor: '#335533', | |
| inputs: [{ name: 'images', type: 'IMAGE' }], | |
| outputs: [ | |
| { name: 'cropped_images', type: 'IMAGE', slotIndex: 0 }, | |
| { name: 'crop_coords', type: 'INT', slotIndex: 1 } | |
| ], | |
| widgets: [ | |
| { name: 'canvas_width', label: 'Canvas Width', type: 'number', value: 512, step: 8 }, | |
| { name: 'canvas_height', label: 'Canvas Height', type: 'number', value: 512, step: 8 }, | |
| { name: 'crop_mode', label: 'Crop Mode', type: 'select', value: 'center', options: ['center', 'top', 'bottom', 'left', 'right'] } | |
| ] | |
| }, | |
| { | |
| id: 6, | |
| type: 'VideoMatchMixer', | |
| title: 'VideoMatchMixer', | |
| x: 1300, | |
| y: 300, | |
| width: 350, | |
| height: 200, | |
| color: '#332266', | |
| bgcolor: '#443377', | |
| inputs: [ | |
| { name: 'frames', type: 'IMAGE' }, | |
| { name: 'formula', type: 'STRING' } | |
| ], | |
| outputs: [ | |
| { name: 'mixed_frames', type: 'IMAGE', slotIndex: 0 }, | |
| { name: 'match_data', type: 'DATA', slotIndex: 1 } | |
| ], | |
| widgets: [ | |
| { name: 'match_strength', label: 'Match Strength', type: 'range', value: 0.5, min: 0, max: 1, step: 0.01 }, | |
| { name: 'mix_mode', label: 'Mix Mode', type: 'select', value: 'multiply', options: ['multiply', 'screen', 'overlay', 'soft_light', 'hard_light'] }, | |
| { name: 'rematch_on_click', label: 'Enable Rematch on Click', type: 'checkbox', value: true } | |
| ] | |
| }, | |
| { | |
| id: 7, | |
| type: 'CanvasEffect', | |
| title: 'CanvasEffect', | |
| x: 1700, | |
| y: 300, | |
| width: 315, | |
| height: 180, | |
| color: '#224466', | |
| bgcolor: '#335577', | |
| inputs: [{ name: 'images', type: 'IMAGE' }], | |
| outputs: [{ name: 'processed', type: 'IMAGE', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'brightness', label: 'Brightness', type: 'range', value: 0, min: -1, max: 1, step: 0.1 }, | |
| { name: 'contrast', label: 'Contrast', type: 'range', value: 1, min: 0, max: 3, step: 0.1 }, | |
| { name: 'saturation', label: 'Saturation', type: 'range', value: 1, min: 0, max: 3, step: 0.1 }, | |
| { name: 'hue_shift', label: 'Hue Shift', type: 'range', value: 0, min: 0, max: 360, step: 1 } | |
| ] | |
| }, | |
| { | |
| id: 8, | |
| type: 'SaveVideo', | |
| title: 'SaveVideo', | |
| x: 2100, | |
| y: 300, | |
| width: 315, | |
| height: 140, | |
| color: '#224422', | |
| bgcolor: '#335533', | |
| inputs: [ | |
| { name: 'images', type: 'IMAGE' }, | |
| { name: 'fps', type: 'FLOAT' } | |
| ], | |
| outputs: [{ name: 'output_path', type: 'STRING', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'output_path', label: 'Output Path', type: 'text', value: './output/video_output.mp4' }, | |
| { name: 'codec', label: 'Codec', type: 'select', value: 'libx264', options: ['libx264', 'libx265', 'vp9'] }, | |
| { name: 'quality', label: 'CRF Quality', type: 'number', value: 23, min: 0, max: 51 } | |
| ] | |
| }, | |
| { | |
| id: 9, | |
| type: 'PreviewVideo', | |
| title: 'PreviewVideo', | |
| x: 2100, | |
| y: 500, | |
| width: 315, | |
| height: 80, | |
| color: '#333333', | |
| bgcolor: '#444444', | |
| inputs: [{ name: 'images', type: 'IMAGE' }], | |
| outputs: [], | |
| widgets: [] | |
| }, | |
| { | |
| id: 11, | |
| type: 'CanvasDisplay', | |
| title: 'CanvasDisplay', | |
| x: 1700, | |
| y: 550, | |
| width: 350, | |
| height: 200, | |
| color: '#224444', | |
| bgcolor: '#335555', | |
| inputs: [{ name: 'images', type: 'IMAGE' }], | |
| outputs: [{ name: 'canvas_data', type: 'DATA', slotIndex: 0 }], | |
| widgets: [ | |
| { name: 'auto_play', label: 'Auto Play', type: 'checkbox', value: true }, | |
| { name: 'loop', label: 'Loop', type: 'checkbox', value: true } | |
| ] | |
| } | |
| ]; | |
| links = [ | |
| { source: 1, target: 3, sourceOutput: 'VIDEO', targetInput: 'video' }, | |
| { source: 2, target: 3, sourceOutput: 'value', targetInput: 'fps' }, | |
| { source: 2, target: 8, sourceOutput: 'value', targetInput: 'fps' }, | |
| { source: 3, target: 5, sourceOutput: 'frames', targetInput: 'images' }, | |
| { source: 4, target: 6, sourceOutput: 'value', targetInput: 'formula' }, | |
| { source: 5, target: 6, sourceOutput: 'cropped_images', targetInput: 'frames' }, | |
| { source: 6, target: 7, sourceOutput: 'mixed_frames', targetInput: 'images' }, | |
| { source: 7 |