Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Flux | Creative Transition Playground</title> | |
| <!-- Fonts & Icons --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=Space+Grotesk:wght@700&display=swap" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-dark: #0f0f13; | |
| --panel-bg: #1a1a24; | |
| --accent: #6c5ce7; | |
| --accent-hover: #a29bfe; | |
| --text-main: #ffffff; | |
| --text-muted: #a0a0a0; | |
| --border: #2d2d3b; | |
| --glass: rgba(26, 26, 36, 0.8); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-dark); | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* Header */ | |
| header { | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--panel-bg); | |
| z-index: 10; | |
| } | |
| .brand { | |
| font-family: 'Space Grotesk', sans-serif; | |
| font-size: 1.5rem; | |
| background: linear-gradient(45deg, var(--accent), #00d2d3); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -1px; | |
| } | |
| .anycoder-link { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .anycoder-link:hover { | |
| color: var(--accent-hover); | |
| } | |
| /* Main Layout */ | |
| main { | |
| flex: 1; | |
| display: grid; | |
| grid-template-columns: 350px 1fr; | |
| height: calc(100vh - 70px); | |
| } | |
| /* Sidebar Controls */ | |
| .controls { | |
| background: var(--panel-bg); | |
| border-right: 1px solid var(--border); | |
| padding: 2rem; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.8rem; | |
| } | |
| .control-group label { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| color: var(--text-muted); | |
| } | |
| /* Inputs & Selects */ | |
| input[type="file"] { | |
| display: none; | |
| } | |
| .file-btn { | |
| background: var(--bg-dark); | |
| border: 1px dashed var(--border); | |
| padding: 1rem; | |
| border-radius: 8px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| color: var(--text-muted); | |
| font-size: 0.9rem; | |
| } | |
| .file-btn:hover { | |
| border-color: var(--accent); | |
| color: var(--text-main); | |
| } | |
| select { | |
| width: 100%; | |
| padding: 0.8rem; | |
| background: var(--bg-dark); | |
| border: 1px solid var(--border); | |
| color: white; | |
| border-radius: 6px; | |
| outline: none; | |
| } | |
| /* Sliders */ | |
| .slider-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .slider-header { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.8rem; | |
| } | |
| input[type="range"] { | |
| -webkit-appearance: none; | |
| width: 100%; | |
| height: 4px; | |
| background: var(--border); | |
| border-radius: 2px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--accent); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| transition: transform 0.2s; | |
| } | |
| input[type="range"]::-webkit-slider-thumb:hover { | |
| transform: scale(1.2); | |
| } | |
| /* Play Button */ | |
| .play-btn { | |
| background: var(--accent); | |
| color: white; | |
| border: none; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.3s, transform 0.1s; | |
| font-size: 1rem; | |
| margin-top: auto; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .play-btn:hover { | |
| background: var(--accent-hover); | |
| } | |
| .play-btn:active { | |
| transform: scale(0.98); | |
| } | |
| /* Stage / Canvas */ | |
| .stage { | |
| position: relative; | |
| background: #000; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| overflow: hidden; | |
| /* Grid pattern background */ | |
| background-image: | |
| linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); | |
| background-size: 40px 40px; | |
| } | |
| .canvas-wrapper { | |
| position: relative; | |
| width: 600px; | |
| height: 400px; | |
| box-shadow: 0 20px 50px rgba(0,0,0,0.5); | |
| overflow: hidden; | |
| border-radius: 4px; | |
| transform-style: preserve-3d; /* For 3D effects */ | |
| perspective: 1000px; | |
| } | |
| .canvas-wrapper img { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| object-fit: cover; | |
| user-select: none; | |
| } | |
| /* Layer A (Base) */ | |
| #layer-a { | |
| z-index: 1; | |
| } | |
| /* Layer B (Transition) */ | |
| #layer-b { | |
| z-index: 2; | |
| opacity: 0; | |
| pointer-events: none; | |
| } | |
| /* --- TRANSITIONS --- */ | |
| /* 1. Fade */ | |
| .transition-fade #layer-b { | |
| opacity: 0; | |
| transition: opacity var(--duration) var(--ease); | |
| } | |
| .transition-fade.active #layer-b { | |
| opacity: var(--opacity-end); | |
| } | |
| /* 2. Slide Left */ | |
| .transition-slide-left #layer-b { | |
| transform: translateX(100%); | |
| opacity: 1; | |
| transition: transform var(--duration) var(--ease); | |
| } | |
| .transition-slide-left.active #layer-b { | |
| transform: translateX(0); | |
| } | |
| /* 3. Zoom In */ | |
| .transition-zoom #layer-b { | |
| transform: scale(var(--scale-start)); | |
| opacity: 1; | |
| transition: transform var(--duration) var(--ease); | |
| } | |
| .transition-zoom.active #layer-b { | |
| transform: scale(var(--scale-end)); | |
| } | |
| /* 4. Rotate 3D Cube */ | |
| .transition-cube #canvas-wrapper { | |
| transform-style: preserve-3d; | |
| } | |
| .transition-cube #layer-a { | |
| transform: rotateY(0deg); | |
| backface-visibility: hidden; | |
| } | |
| .transition-cube #layer-b { | |
| transform: rotateY(90deg); | |
| opacity: 1; | |
| backface-visibility: hidden; | |
| } | |
| .transition-cube.active #layer-b { | |
| animation: cubeRotate var(--duration) forwards var(--ease); | |
| } | |
| @keyframes cubeRotate { | |
| 0% { transform: rotateY(90deg); } | |
| 100% { transform: rotateY(0deg); } | |
| } | |
| /* Fix for cube reverse logic requires more complex DOM, simplifying for CSS only */ | |
| .transition-cube.active #layer-a { | |
| animation: cubeRotateBack var(--duration) forwards var(--ease); | |
| } | |
| @keyframes cubeRotateBack { | |
| 0% { transform: rotateY(0deg); } | |
| 100% { transform: rotateY(-90deg); } | |
| } | |
| /* 5. Vertical Blinds (Clip Path) */ | |
| .transition-blinds #layer-b { | |
| clip-path: polygon(0 0, 0 0, 0 100%, 0 100%); | |
| opacity: 1; | |
| transition: clip-path var(--duration) var(--ease); | |
| } | |
| .transition-blinds.active #layer-b { | |
| clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); | |
| } | |
| /* 6. Glitch (Cyberpunk) */ | |
| .transition-glitch #layer-b { | |
| opacity: 1; | |
| clip-path: polygon(0 0, 100% 0, 100% 0, 0 0); | |
| transition: none; | |
| } | |
| .transition-glitch.active #layer-b { | |
| animation: glitch-anim var(--duration) steps(10) forwards; | |
| } | |
| @keyframes glitch-anim { | |
| 0% { clip-path: polygon(0 0, 100% 0, 100% 0, 0 0); opacity: 1;} | |
| 20% { clip-path: polygon(0 20%, 100% 20%, 100% 20%, 0 20%); transform: translate(-5px, 0); } | |
| 40% { clip-path: polygon(0 60%, 100% 60%, 100% 60%, 0 60%); transform: translate(5px, 0); } | |
| 60% { clip-path: polygon(0 80%, 100% 80%, 100% 80%, 0 80%); transform: translate(-5px, 0); } | |
| 80% { clip-path: polygon(0 40%, 100% 40%, 100% 40%, 0 40%); transform: translate(5px, 0); } | |
| 100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); transform: translate(0, 0); opacity: 1;} | |
| } | |
| /* Responsive */ | |
| @media (max-width: 900px) { | |
| main { | |
| grid-template-columns: 1fr; | |
| grid-template-rows: 1fr 300px; | |
| overflow-y: auto; | |
| height: auto; | |
| } | |
| .stage { | |
| min-height: 50vh; | |
| } | |
| .canvas-wrapper { | |
| width: 90%; | |
| height: 300px; | |
| } | |
| .controls { | |
| border-right: none; | |
| border-top: 1px solid var(--border); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"><i class="fa-solid fa-layer-group"></i> FLUX</div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <aside class="controls"> | |
| <!-- Image Inputs --> | |
| <div class="control-group"> | |
| <label>Source Images</label> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> | |
| <label class="file-btn"> | |
| <input type="file" id="input-a" accept="image/*"> | |
| <i class="fa-regular fa-image"></i> Image A | |
| </label> | |
| <label class="file-btn"> | |
| <input type="file" id="input-b" accept="image/*"> | |
| <i class="fa-solid fa-image"></i> Image B | |
| </label> | |
| </div> | |
| </div> | |
| <!-- Transition Selector --> | |
| <div class="control-group"> | |
| <label>Transition Style</label> | |
| <select id="effect-select"> | |
| <option value="fade">Smooth Fade</option> | |
| <option value="slide-left">Slide Left</option> | |
| <option value="zoom">Zoom In/Out</option> | |
| <option value="cube">3D Cube Rotate</option> | |
| <option value="blinds">Vertical Blinds</option> | |
| <option value="glitch">Cyber Glitch</option> | |
| </select> | |
| </div> | |
| <!-- Parameter Editors --> | |
| <div class="control-group"> | |
| <label>Parameters</label> | |
| <div class="slider-container"> | |
| <div class="slider-header"> | |
| <span>Duration</span> | |
| <span id="val-duration">1000ms</span> | |
| </div> | |
| <input type="range" id="duration" min="100" max="3000" step="100" value="1000"> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-header"> | |
| <span>Scale Multiplier</span> | |
| <span id="val-scale">1.5</span> | |
| </div> | |
| <input type="range" id="scale" min="0.5" max="3.0" step="0.1" value="1.5"> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-header"> | |
| <span>End Opacity</span> | |
| <span id="val-opacity">1.0</span> | |
| </div> | |
| <input type="range" id="opacity" min="0" max="1" step="0.1" value="1"> | |
| </div> | |
| </div> | |
| <button class="play-btn" id="play-btn"> | |
| <i class="fa-solid fa-play"></i> Play Transition | |
| </button> | |
| </aside> | |
| <section class="stage"> | |
| <div class="canvas-wrapper" id="canvas-wrapper"> | |
| <!-- Default placeholders --> | |
| <img id="layer-a" src="https://picsum.photos/id/10/800/600" alt="Image A"> | |
| <img id="layer-b" src="https://picsum.photos/id/20/800/600" alt="Image B"> | |
| </div> | |
| </section> | |
| </main> | |
| <script> | |
| // Elements | |
| const playBtn = document.getElementById('play-btn'); | |
| const canvasWrapper = document.getElementById('canvas-wrapper'); | |
| const layerA = document.getElementById('layer-a'); | |
| const layerB = document.getElementById('layer-b'); | |
| const effectSelect = document.getElementById('effect-select'); | |
| const inputA = document.getElementById('input-a'); | |
| const inputB = document.getElementById('input-b'); | |
| // Sliders | |
| const durationSlider = document.getElementById('duration'); | |
| const scaleSlider = document.getElementById('scale'); | |
| const opacitySlider = document.getElementById('opacity'); | |
| // Display Values | |
| const valDuration = document.getElementById('val-duration'); | |
| const valScale = document.getElementById('val-scale'); | |
| const valOpacity = document.getElementById('val-opacity'); | |
| // State | |
| let isTransitioning = false; | |
| let currentEffect = 'fade'; | |
| // 1. Handle File Uploads | |
| const handleImageUpload = (input, imgElement) => { | |
| input.addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| imgElement.src = e.target.result; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| }; | |
| handleImageUpload(inputA, layerA); | |
| handleImageUpload(inputB, layerB); | |
| // 2. Update CSS Variables based on sliders | |
| function updateParams() { | |
| const duration = durationSlider.value; | |
| const scale = scaleSlider.value; | |
| const opacity = opacitySlider.value; | |
| // Update text | |
| valDuration.textContent = duration + 'ms'; | |
| valScale.textContent = scale; | |
| valOpacity.textContent = opacity; | |
| // Update CSS variables on the canvas wrapper | |
| canvasWrapper.style.setProperty('--duration', `${duration}ms`); | |
| canvasWrapper.style.setProperty('--scale-start', '0.5'); | |
| canvasWrapper.style.setProperty('--scale-end', scale); | |
| canvasWrapper.style.setProperty('--opacity-end', opacity); | |
| } | |
| durationSlider.addEventListener('input', updateParams); | |
| scaleSlider.addEventListener('input', updateParams); | |
| opacitySlider.addEventListener('input', updateParams); | |
| // 3. Handle Effect Change | |
| effectSelect.addEventListener('change', (e) => { | |
| // Reset classes | |
| canvasWrapper.classList.remove(`transition-${currentEffect}`); | |
| currentEffect = e.target.value; | |
| canvasWrapper.classList.add(`transition-${currentEffect}`); | |
| // Reset state | |
| canvasWrapper.classList.remove('active'); | |
| isTransitioning = false; | |
| playBtn.innerHTML = '<i class="fa-solid fa-play"></i> Play Transition'; | |
| // Adjust sliders for default feel based on effect | |
| if(currentEffect === 'zoom') { | |
| scaleSlider.value = 2.0; | |
| } else { | |
| scaleSlider.value = 1.5; | |
| } | |
| updateParams(); | |
| }); | |
| // Initialize first effect | |
| canvasWrapper.classList.add(`transition-${currentEffect}`); | |
| updateParams(); | |
| // 4. Play Animation Logic | |
| playBtn.addEventListener('click', () => { | |
| if (isTransitioning) { | |
| // If we are mid-way, reset and play again | |
| canvasWrapper.classList.remove('active'); | |
| setTimeout(runAnimation, 50); | |
| } else { | |
| runAnimation(); | |
| } | |
| }); | |
| function runAnimation() { | |
| isTransitioning = true; | |
| playBtn.innerHTML = '<i class="fa-solid fa-rotate-right"></i> Replay'; | |
| // Force reflow to restart CSS transition | |
| void canvasWrapper.offsetWidth; | |
| canvasWrapper.classList.add('active'); | |
| // Optional: Reset automatically after duration + buffer | |
| // For a playground, we usually let the user toggle back, | |
| // but for "Play" feel, let's let it stay on image B. | |
| } | |
| </script> | |
| </body> | |
| </html> |