Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Interactive Mesh Gradient Creator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script> | |
| <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"> | |
| <style> | |
| :root { | |
| --dark-gray: #4B4B4B; | |
| --light-gray: #DDDDDD; | |
| --medium-blue: #3B7AF0; | |
| --light-blue: #69A3FF; | |
| --pale-blue: #A8D8F0; | |
| --mint-green: #8CD3A7; | |
| --lighter-mint: #B6E2C8; | |
| --very-pale-mint: #DAF2E1; | |
| --pale-peach: #F0D4A8; | |
| --light-beige: #F0E1C8; | |
| --off-white: #F5F5F7; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| overflow: hidden; | |
| background-color: #000; | |
| } | |
| #gradient-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| z-index: 0; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .control-panel { | |
| position: fixed; | |
| top: 0; | |
| right: 0; | |
| width: 320px; | |
| height: 100vh; | |
| background-color: rgba(0, 0, 0, 0.85); | |
| backdrop-filter: blur(10px); | |
| z-index: 10; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| overflow-y: auto; | |
| transition: transform 0.3s ease; | |
| color: white; | |
| box-shadow: -5px 0 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .mobile .control-panel { | |
| transform: translateX(100%); | |
| } | |
| .mobile .control-panel.open { | |
| transform: translateX(0); | |
| } | |
| .panel-toggle { | |
| position: fixed; | |
| right: 20px; | |
| top: 20px; | |
| z-index: 20; | |
| background-color: var(--medium-blue); | |
| color: white; | |
| border: none; | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | |
| } | |
| .color-preview { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 4px; | |
| display: inline-block; | |
| margin-right: 8px; | |
| vertical-align: middle; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .slider-container { | |
| margin-bottom: 15px; | |
| } | |
| .slider-label { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-bottom: 5px; | |
| font-size: 14px; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| height: 6px; | |
| -webkit-appearance: none; | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| background: var(--medium-blue); | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| .btn { | |
| background-color: var(--medium-blue); | |
| color: white; | |
| border: none; | |
| padding: 10px 15px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| font-weight: 500; | |
| font-size: 14px; | |
| transition: background-color 0.2s; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .btn:hover { | |
| background-color: var(--light-blue); | |
| } | |
| .btn svg { | |
| margin-right: 8px; | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 15px; | |
| } | |
| .btn-group .btn { | |
| flex: 1; | |
| } | |
| .btn.active { | |
| background-color: var(--light-blue); | |
| } | |
| .section-title { | |
| font-size: 16px; | |
| font-weight: 700; | |
| margin: 20px 0 10px; | |
| padding-bottom: 5px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .color-option { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| cursor: pointer; | |
| padding: 6px; | |
| border-radius: 4px; | |
| transition: background-color 0.2s; | |
| } | |
| .color-option:hover { | |
| background-color: rgba(255, 255, 255, 0.1); | |
| } | |
| .color-option.active { | |
| background-color: rgba(59, 122, 240, 0.3); | |
| } | |
| .color-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 8px; | |
| } | |
| .recording-indicator { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| background-color: red; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.3; } | |
| 100% { opacity: 1; } | |
| } | |
| .status-message { | |
| font-size: 12px; | |
| color: var(--light-gray); | |
| margin-top: 5px; | |
| min-height: 18px; | |
| } | |
| @media (max-width: 768px) { | |
| .control-panel { | |
| width: 280px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="gradient-container"> | |
| <canvas id="gradient-canvas"></canvas> | |
| </div> | |
| <button class="panel-toggle mobile:hidden" id="toggle-panel"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <line x1="3" y1="12" x2="21" y2="12"></line> | |
| <line x1="3" y1="6" x2="21" y2="6"></line> | |
| <line x1="3" y1="18" x2="21" y2="18"></line> | |
| </svg> | |
| </button> | |
| <div class="control-panel" id="control-panel"> | |
| <h2 class="text-xl font-bold mb-6">Mesh Gradient Creator</h2> | |
| <div class="section-title">Animation Settings</div> | |
| <div class="slider-container"> | |
| <div class="slider-label"> | |
| <span>Speed</span> | |
| <span id="speed-value">1.0x</span> | |
| </div> | |
| <input type="range" id="speed-slider" min="0.5" max="2" step="0.1" value="1"> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label"> | |
| <span>Distortion X</span> | |
| <span id="distortion-x-value">50</span> | |
| </div> | |
| <input type="range" id="distortion-x-slider" min="0" max="100" value="50"> | |
| </div> | |
| <div class="slider-container"> | |
| <div class="slider-label"> | |
| <span>Distortion Y</span> | |
| <span id="distortion-y-value">50</span> | |
| </div> | |
| <input type="range" id="distortion-y-slider" min="0" max="100" value="50"> | |
| </div> | |
| <div class="flex items-center mb-4"> | |
| <input type="checkbox" id="loop-toggle" class="mr-2"> | |
| <label for="loop-toggle">Enable 15-second loop</label> | |
| </div> | |
| <div class="section-title">Aspect Ratio</div> | |
| <div class="btn-group"> | |
| <button class="btn active" data-ratio="16:9">16:9</button> | |
| <button class="btn" data-ratio="1:1">1:1</button> | |
| <button class="btn" data-ratio="9:16">9:16</button> | |
| </div> | |
| <div class="section-title">Color Palette</div> | |
| <div class="color-grid"> | |
| <div class="color-option active" data-color="#4B4B4B"> | |
| <span class="color-preview" style="background-color: #4B4B4B;"></span> | |
| <span>Dark Gray</span> | |
| </div> | |
| <div class="color-option" data-color="#DDDDDD"> | |
| <span class="color-preview" style="background-color: #DDDDDD;"></span> | |
| <span>Light Gray</span> | |
| </div> | |
| <div class="color-option" data-color="#3B7AF0"> | |
| <span class="color-preview" style="background-color: #3B7AF0;"></span> | |
| <span>Medium Blue</span> | |
| </div> | |
| <div class="color-option" data-color="#69A3FF"> | |
| <span class="color-preview" style="background-color: #69A3FF;"></span> | |
| <span>Light Blue</span> | |
| </div> | |
| <div class="color-option" data-color="#A8D8F0"> | |
| <span class="color-preview" style="background-color: #A8D8F0;"></span> | |
| <span>Pale Blue</span> | |
| </div> | |
| <div class="color-option" data-color="#8CD3A7"> | |
| <span class="color-preview" style="background-color: #8CD3A7;"></span> | |
| <span>Mint Green</span> | |
| </div> | |
| <div class="color-option" data-color="#B6E2C8"> | |
| <span class="color-preview" style="background-color: #B6E2C8;"></span> | |
| <span>Lighter Mint</span> | |
| </div> | |
| <div class="color-option" data-color="#DAF2E1"> | |
| <span class="color-preview" style="background-color: #DAF2E1;"></span> | |
| <span>Very Pale Mint</span> | |
| </div> | |
| <div class="color-option" data-color="#F0D4A8"> | |
| <span class="color-preview" style="background-color: #F0D4A8;"></span> | |
| <span>Pale Peach</span> | |
| </div> | |
| <div class="color-option" data-color="#F0E1C8"> | |
| <span class="color-preview" style="background-color: #F0E1C8;"></span> | |
| <span>Light Beige</span> | |
| </div> | |
| <div class="color-option" data-color="#F5F5F7"> | |
| <span class="color-preview" style="background-color: #F5F5F7;"></span> | |
| <span>Off-White</span> | |
| </div> | |
| </div> | |
| <div class="section-title">Export</div> | |
| <button class="btn w-full mb-2" id="record-btn"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg> | |
| <span>Start Recording</span> | |
| </button> | |
| <div class="status-message" id="status-message"></div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Check if mobile | |
| const isMobile = window.matchMedia('(max-width: 768px)').matches; | |
| if (isMobile) { | |
| document.body.classList.add('mobile'); | |
| document.getElementById('control-panel').classList.add('mobile'); | |
| } | |
| // Canvas setup | |
| const canvas = document.getElementById('gradient-canvas'); | |
| const container = document.getElementById('gradient-container'); | |
| const ctx = canvas.getContext('2d'); | |
| // Set initial canvas size | |
| function resizeCanvas() { | |
| const aspectRatio = document.querySelector('.btn-group .btn.active').dataset.ratio; | |
| let width, height; | |
| if (aspectRatio === '16:9') { | |
| width = window.innerWidth; | |
| height = (width * 9) / 16; | |
| } else if (aspectRatio === '1:1') { | |
| width = Math.min(window.innerWidth, window.innerHeight); | |
| height = width; | |
| } else { // 9:16 | |
| height = window.innerHeight; | |
| width = (height * 9) / 16; | |
| } | |
| canvas.width = width; | |
| canvas.height = height; | |
| // Center the canvas | |
| container.style.width = `${width}px`; | |
| container.style.height = `${height}px`; | |
| container.style.left = `${(window.innerWidth - width) / 2}px`; | |
| container.style.top = `${(window.innerHeight - height) / 2}px`; | |
| } | |
| resizeCanvas(); | |
| window.addEventListener('resize', resizeCanvas); | |
| // Gradient animation | |
| let animationId; | |
| let time = 0; | |
| let speed = 1; | |
| let distortionX = 50; | |
| let distortionY = 50; | |
| let currentColor = '#4B4B4B'; | |
| let isLooping = false; | |
| let loopStartTime = 0; | |
| const loopDuration = 15; // seconds | |
| function hexToRgb(hex) { | |
| const r = parseInt(hex.slice(1, 3), 16); | |
| const g = parseInt(hex.slice(3, 5), 16); | |
| const b = parseInt(hex.slice(5, 7), 16); | |
| return { r, g, b }; | |
| } | |
| function animate() { | |
| time += 0.01 * speed; | |
| if (isLooping) { | |
| const loopTime = (Date.now() - loopStartTime) / 1000; | |
| if (loopTime >= loopDuration) { | |
| loopStartTime = Date.now(); | |
| } | |
| } | |
| // Clear canvas | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Create gradient | |
| const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height); | |
| const rgb = hexToRgb(currentColor); | |
| // Add color stops with distortion | |
| for (let i = 0; i <= 1; i += 0.1) { | |
| const pos = i + Math.sin(time + i * 10) * (distortionX / 500); | |
| const r = Math.min(255, rgb.r + Math.sin(time + i * 5) * 30); | |
| const g = Math.min(255, rgb.g + Math.cos(time + i * 7) * 30); | |
| const b = Math.min(255, rgb.b + Math.sin(time + i * 3) * 30); | |
| gradient.addColorStop(pos, `rgb(${r}, ${g}, ${b})`); | |
| } | |
| // Fill with gradient | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| animationId = requestAnimationFrame(animate); | |
| } | |
| // Start animation | |
| animate(); | |
| // Control panel interactions | |
| document.getElementById('toggle-panel').addEventListener('click', function() { | |
| document.getElementById('control-panel').classList.toggle('open'); | |
| }); | |
| // Speed slider | |
| const speedSlider = document.getElementById('speed-slider'); | |
| const speedValue = document.getElementById('speed-value'); | |
| speedSlider.addEventListener('input', function() { | |
| speed = parseFloat(this.value); | |
| speedValue.textContent = `${speed.toFixed(1)}x`; | |
| }); | |
| // Distortion sliders | |
| const distortionXSlider = document.getElementById('distortion-x-slider'); | |
| const distortionXValue = document.getElementById('distortion-x-value'); | |
| distortionXSlider.addEventListener('input', function() { | |
| distortionX = parseInt(this.value); | |
| distortionXValue.textContent = distortionX; | |
| }); | |
| const distortionYSlider = document.getElementById('distortion-y-slider'); | |
| const distortionYValue = document.getElementById('distortion-y-value'); | |
| distortionYSlider.addEventListener('input', function() { | |
| distortionY = parseInt(this.value); | |
| distortionYValue.textContent = distortionY; | |
| }); | |
| // Loop toggle | |
| const loopToggle = document.getElementById('loop-toggle'); | |
| loopToggle.addEventListener('change', function() { | |
| isLooping = this.checked; | |
| if (isLooping) { | |
| loopStartTime = Date.now(); | |
| } | |
| }); | |
| // Aspect ratio buttons | |
| const aspectRatioButtons = document.querySelectorAll('.btn-group .btn'); | |
| aspectRatioButtons.forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| aspectRatioButtons.forEach(b => b.classList.remove('active')); | |
| this.classList.add('active'); | |
| resizeCanvas(); | |
| }); | |
| }); | |
| // Color options | |
| const colorOptions = document.querySelectorAll('.color-option'); | |
| colorOptions.forEach(option => { | |
| option.addEventListener('click', function() { | |
| colorOptions.forEach(o => o.classList.remove('active')); | |
| this.classList.add('active'); | |
| currentColor = this.dataset.color; | |
| }); | |
| }); | |
| // Recording functionality | |
| const recordBtn = document.getElementById('record-btn'); | |
| const statusMessage = document.getElementById('status-message'); | |
| let mediaRecorder; | |
| let recordedChunks = []; | |
| let isRecording = false; | |
| recordBtn.addEventListener('click', function() { | |
| if (!isRecording) { | |
| startRecording(); | |
| } else { | |
| stopRecording(); | |
| } | |
| }); | |
| function startRecording() { | |
| if (!canvas.captureStream) { | |
| statusMessage.textContent = "Recording not supported in this browser"; | |
| return; | |
| } | |
| recordedChunks = []; | |
| const stream = canvas.captureStream(30); | |
| mediaRecorder = new MediaRecorder(stream, { | |
| mimeType: 'video/webm;codecs=vp9', | |
| videoBitsPerSecond: 8000000 | |
| }); | |
| mediaRecorder.ondataavailable = function(e) { | |
| if (e.data.size > 0) { | |
| recordedChunks.push(e.data); | |
| } | |
| }; | |
| mediaRecorder.onstop = function() { | |
| const blob = new Blob(recordedChunks, { type: 'video/webm' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| a.href = url; | |
| a.download = 'gradient-animation.webm'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 100); | |
| statusMessage.textContent = "Recording saved as gradient-animation.webm"; | |
| }; | |
| mediaRecorder.start(100); // Collect data every 100ms | |
| isRecording = true; | |
| recordBtn.innerHTML = ` | |
| <span class="recording-indicator"></span> | |
| <span>Stop Recording</span> | |
| `; | |
| statusMessage.textContent = "Recording started..."; | |
| if (isLooping) { | |
| loopStartTime = Date.now(); | |
| const checkLoop = setInterval(() => { | |
| const elapsed = (Date.now() - loopStartTime) / 1000; | |
| if (elapsed >= loopDuration) { | |
| stopRecording(); | |
| clearInterval(checkLoop); | |
| } | |
| }, 100); | |
| } | |
| } | |
| function stopRecording() { | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| } | |
| isRecording = false; | |
| recordBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg> | |
| <span>Start Recording</span> | |
| `; | |
| } | |
| // Clean up on page unload | |
| window.addEventListener('beforeunload', function() { | |
| cancelAnimationFrame(animationId); | |
| if (mediaRecorder && mediaRecorder.state !== 'inactive') { | |
| mediaRecorder.stop(); | |
| } | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=benner3000/test" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |