test / index.html
benner3000's picture
Add 3 files
b1ae6b5 verified
<!DOCTYPE html>
<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>