progress
This commit is contained in:
parent
4be8fcb158
commit
ffdb421d7e
|
|
@ -0,0 +1,2 @@
|
|||
RUN:
|
||||
git ls-files
|
||||
|
|
@ -0,0 +1 @@
|
|||
ignore/
|
||||
|
|
@ -1,532 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: TextSeed - Organic Text Input</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #e8f5e8 0%, #f0f8f0 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2d5a2d;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
background: #f0f8f0;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
margin-bottom: 30px;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2d5a2d;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #3a6b3a;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
background: linear-gradient(135deg, #f8fdf8 0%, #e8f5e8 100%);
|
||||
}
|
||||
|
||||
.textseed-container {
|
||||
position: relative;
|
||||
margin: 40px auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: 300px;
|
||||
background: linear-gradient(to bottom, #87ceeb 0%, #98fb98 40%, #8fbc8f 100%);
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
border: 3px solid #6b8e23;
|
||||
box-shadow: inset 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.soil {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
background: linear-gradient(to bottom, #8b4513 0%, #654321 100%);
|
||||
border-top: 2px solid #a0522d;
|
||||
}
|
||||
|
||||
.seed {
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 20px;
|
||||
height: 15px;
|
||||
background: #8b4513;
|
||||
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
|
||||
opacity: 1;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.stem {
|
||||
position: absolute;
|
||||
bottom: 55px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 4px;
|
||||
height: 0;
|
||||
background: linear-gradient(to top, #228b22, #32cd32);
|
||||
border-radius: 2px;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.text-display {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px 25px;
|
||||
border-radius: 20px;
|
||||
border: 2px solid #32cd32;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #2d5a2d;
|
||||
opacity: 0;
|
||||
transition: all 0.4s ease;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.leaf {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
background: #32cd32;
|
||||
border-radius: 0 100% 0 100%;
|
||||
opacity: 0;
|
||||
transform-origin: bottom center;
|
||||
transition: all 0.4s ease;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-prompt {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.traditional, .innovative {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.traditional {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.innovative {
|
||||
background: #f0f8f0;
|
||||
border-color: #32cd32;
|
||||
}
|
||||
|
||||
.traditional input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #32cd32;
|
||||
}
|
||||
|
||||
.growth-meter {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 3px solid #32cd32;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
font-weight: bold;
|
||||
color: #2d5a2d;
|
||||
}
|
||||
|
||||
@keyframes grow {
|
||||
from { height: 0; opacity: 0; }
|
||||
to { height: var(--grow-height); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes leafSway {
|
||||
0%, 100% { transform: rotate(-5deg); }
|
||||
50% { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
.leaf.animated {
|
||||
animation: leafSway 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.textseed-container {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>UI Innovation: TextSeed - Organic Text Input</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional Text Input Field</p>
|
||||
<p><strong>Innovation:</strong> Organic growth metaphor for text entry</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<div class="innovation-component">
|
||||
<div class="textseed-container" tabindex="0" role="textbox" aria-label="TextSeed organic text input">
|
||||
<div class="soil"></div>
|
||||
<div class="seed" id="seed"></div>
|
||||
<div class="stem" id="stem"></div>
|
||||
<div class="text-display" id="textDisplay" aria-live="polite"></div>
|
||||
<div class="growth-meter" id="growthMeter" aria-label="Growth progress">0%</div>
|
||||
<input type="text" class="hidden-input" id="hiddenInput" aria-hidden="true" tabindex="-1">
|
||||
</div>
|
||||
<div class="input-prompt">
|
||||
Click the garden and start typing to watch your text grow! (Supports all keyboard shortcuts)
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Text Input</h3>
|
||||
<p>Standard rectangular input field with cursor and text display.</p>
|
||||
<input type="text" placeholder="Type here..." aria-label="Traditional text input">
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
||||
✓ Familiar<br>
|
||||
✓ Functional<br>
|
||||
✗ Static visual feedback<br>
|
||||
✗ Limited engagement<br>
|
||||
✗ No progress indication
|
||||
</p>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>TextSeed Component</h3>
|
||||
<p>Organic growth visualization that transforms text input into a living, breathing experience.</p>
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #2d5a2d;">
|
||||
✓ Engaging visual metaphor<br>
|
||||
✓ Real-time growth feedback<br>
|
||||
✓ Progress visualization<br>
|
||||
✓ Memorable interaction<br>
|
||||
✓ Maintains full functionality
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>Users click anywhere on the garden to focus and begin typing. Each character grows the plant upward, with leaves appearing at intervals. The seed disappears as growth begins, and text appears in a speech bubble above the plant. The growth meter shows typing progress as a percentage.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>Built with CSS transitions, transforms, and custom properties for smooth animations. Uses a hidden input field for text capture while displaying visual feedback through dynamically styled DOM elements. Keyboard events drive the organic growth animations in real-time.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>Maintains full keyboard navigation with proper ARIA labels and roles. Text is announced via aria-live regions. Focus management ensures screen readers can interact normally. All visual feedback has semantic equivalents for assistive technologies.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>Future enhancements could include seasonal themes, weather effects, multiple plant types for different content categories, collaborative gardening for multi-user inputs, and adaptive growth patterns based on typing speed and rhythm.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
class TextSeed {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.hiddenInput = container.querySelector('#hiddenInput');
|
||||
this.seed = container.querySelector('#seed');
|
||||
this.stem = container.querySelector('#stem');
|
||||
this.textDisplay = container.querySelector('#textDisplay');
|
||||
this.growthMeter = container.querySelector('#growthMeter');
|
||||
this.leaves = [];
|
||||
this.currentText = '';
|
||||
this.maxLength = 100;
|
||||
this.isActive = false;
|
||||
|
||||
this.initializeEvents();
|
||||
this.updateAccessibility();
|
||||
}
|
||||
|
||||
initializeEvents() {
|
||||
// Focus handling
|
||||
this.container.addEventListener('click', () => this.focus());
|
||||
this.container.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
||||
|
||||
// Hidden input for text capture
|
||||
this.hiddenInput.addEventListener('input', (e) => this.handleInput(e));
|
||||
this.hiddenInput.addEventListener('keydown', (e) => this.handleSpecialKeys(e));
|
||||
|
||||
// Focus and blur events
|
||||
this.hiddenInput.addEventListener('focus', () => this.setActive(true));
|
||||
this.hiddenInput.addEventListener('blur', () => this.setActive(false));
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.hiddenInput.focus();
|
||||
this.setActive(true);
|
||||
}
|
||||
|
||||
setActive(active) {
|
||||
this.isActive = active;
|
||||
this.container.style.boxShadow = active
|
||||
? '0 0 20px rgba(50, 205, 50, 0.5)'
|
||||
: 'inset 0 5px 15px rgba(0, 0, 0, 0.1)';
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
// Ensure hidden input receives all keyboard events
|
||||
if (e.target === this.container) {
|
||||
this.hiddenInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleSpecialKeys(e) {
|
||||
// Handle special keys like backspace, delete, etc.
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
// Let the input event handle the text update
|
||||
setTimeout(() => {
|
||||
this.updateGrowth(this.hiddenInput.value);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
const newText = e.target.value;
|
||||
this.updateGrowth(newText);
|
||||
}
|
||||
|
||||
updateGrowth(text) {
|
||||
this.currentText = text;
|
||||
const length = text.length;
|
||||
const progress = Math.min(length / this.maxLength, 1);
|
||||
|
||||
// Update growth meter
|
||||
this.growthMeter.textContent = Math.round(progress * 100) + '%';
|
||||
|
||||
// Handle seed visibility
|
||||
if (length > 0) {
|
||||
this.seed.style.opacity = '0';
|
||||
this.seed.style.transform = 'translateX(-50%) scale(0.5)';
|
||||
} else {
|
||||
this.seed.style.opacity = '1';
|
||||
this.seed.style.transform = 'translateX(-50%) scale(1)';
|
||||
}
|
||||
|
||||
// Update stem height
|
||||
const stemHeight = Math.min(length * 3, 150);
|
||||
this.stem.style.height = stemHeight + 'px';
|
||||
|
||||
// Show/hide text display
|
||||
if (length > 0) {
|
||||
this.textDisplay.style.opacity = '1';
|
||||
this.textDisplay.style.transform = 'translateX(-50%) scale(1)';
|
||||
this.textDisplay.textContent = text;
|
||||
} else {
|
||||
this.textDisplay.style.opacity = '0';
|
||||
this.textDisplay.style.transform = 'translateX(-50%) scale(0.8)';
|
||||
this.textDisplay.textContent = '';
|
||||
}
|
||||
|
||||
// Add leaves at intervals
|
||||
this.updateLeaves(length);
|
||||
|
||||
// Update accessibility
|
||||
this.updateAccessibility();
|
||||
}
|
||||
|
||||
updateLeaves(textLength) {
|
||||
const targetLeaves = Math.floor(textLength / 5);
|
||||
|
||||
// Remove excess leaves
|
||||
while (this.leaves.length > targetLeaves) {
|
||||
const leaf = this.leaves.pop();
|
||||
leaf.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
if (leaf.parentNode) {
|
||||
leaf.parentNode.removeChild(leaf);
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// Add new leaves
|
||||
while (this.leaves.length < targetLeaves) {
|
||||
this.addLeaf(this.leaves.length);
|
||||
}
|
||||
}
|
||||
|
||||
addLeaf(index) {
|
||||
const leaf = document.createElement('div');
|
||||
leaf.className = 'leaf animated';
|
||||
|
||||
const side = index % 2 === 0 ? -1 : 1;
|
||||
const height = 55 + (index * 15);
|
||||
const offset = side * 20;
|
||||
|
||||
leaf.style.bottom = height + 'px';
|
||||
leaf.style.left = 'calc(50% + ' + offset + 'px)';
|
||||
leaf.style.transform = 'rotate(' + (side * 20) + 'deg)';
|
||||
leaf.style.animationDelay = (index * 0.2) + 's';
|
||||
|
||||
this.container.appendChild(leaf);
|
||||
this.leaves.push(leaf);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
leaf.style.opacity = '1';
|
||||
}, 50);
|
||||
}
|
||||
|
||||
updateAccessibility() {
|
||||
// Update ARIA properties
|
||||
this.container.setAttribute('aria-valuenow', this.currentText.length);
|
||||
this.container.setAttribute('aria-valuemax', this.maxLength);
|
||||
this.container.setAttribute('aria-valuetext',
|
||||
`${this.currentText.length} characters entered: ${this.currentText || 'empty'}`);
|
||||
}
|
||||
|
||||
// Public API methods
|
||||
getValue() {
|
||||
return this.currentText;
|
||||
}
|
||||
|
||||
setValue(text) {
|
||||
this.hiddenInput.value = text;
|
||||
this.updateGrowth(text);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setValue('');
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.hiddenInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize TextSeed component when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.querySelector('.textseed-container');
|
||||
const textSeed = new TextSeed(container);
|
||||
|
||||
// Make it available globally for demonstration
|
||||
window.textSeed = textSeed;
|
||||
|
||||
// Example of programmatic interaction
|
||||
setTimeout(() => {
|
||||
console.log('TextSeed initialized. Try textSeed.setValue("Hello World!") in console.');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Handle window resize for responsive behavior
|
||||
window.addEventListener('resize', () => {
|
||||
// Redraw leaves on resize if needed
|
||||
const container = document.querySelector('.textseed-container');
|
||||
if (container && window.textSeed) {
|
||||
window.textSeed.updateLeaves(window.textSeed.currentText.length);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,676 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: Quantum State Toggle</title>
|
||||
<style>
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 100%);
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(45deg, #00ffcc, #0099ff, #cc00ff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #00ffcc;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #0099ff;
|
||||
}
|
||||
|
||||
/* Quantum Toggle Styles */
|
||||
.quantum-toggle-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3rem;
|
||||
justify-content: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.quantum-toggle {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.quantum-field {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Probability Cloud */
|
||||
.probability-cloud {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center,
|
||||
rgba(0, 255, 204, 0.3) 0%,
|
||||
rgba(0, 153, 255, 0.2) 40%,
|
||||
rgba(204, 0, 255, 0.1) 70%,
|
||||
transparent 100%);
|
||||
animation: quantumFluctuation 3s ease-in-out infinite;
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
@keyframes quantumFluctuation {
|
||||
0%, 100% { transform: scale(1) rotate(0deg); }
|
||||
25% { transform: scale(1.1) rotate(90deg); }
|
||||
50% { transform: scale(0.9) rotate(180deg); }
|
||||
75% { transform: scale(1.05) rotate(270deg); }
|
||||
}
|
||||
|
||||
/* Wave Function */
|
||||
.wave-function {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wave-path {
|
||||
stroke: rgba(0, 255, 204, 0.8);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
filter: drop-shadow(0 0 5px rgba(0, 255, 204, 0.5));
|
||||
animation: waveOscillation 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes waveOscillation {
|
||||
0% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: -100; }
|
||||
}
|
||||
|
||||
/* Quantum States */
|
||||
.quantum-state {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.state-on {
|
||||
background: radial-gradient(circle, #00ffcc, #0099ff);
|
||||
box-shadow: 0 0 30px rgba(0, 255, 204, 0.8);
|
||||
}
|
||||
|
||||
.state-off {
|
||||
background: radial-gradient(circle, #666, #333);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.state-superposition {
|
||||
background: radial-gradient(circle,
|
||||
rgba(0, 255, 204, 0.5),
|
||||
rgba(102, 102, 102, 0.5));
|
||||
box-shadow: 0 0 20px rgba(0, 255, 204, 0.4);
|
||||
animation: superpositionPulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes superpositionPulse {
|
||||
0%, 100% { transform: translate(-50%, -50%) scale(1); }
|
||||
50% { transform: translate(-50%, -50%) scale(1.2); }
|
||||
}
|
||||
|
||||
/* Probability Display */
|
||||
.probability-display {
|
||||
position: absolute;
|
||||
bottom: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #00ffcc;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Measurement Effect */
|
||||
.collapse-wave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0, 255, 204, 0.8);
|
||||
animation: collapseAnimation 0.6s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes collapseAnimation {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Entanglement Line */
|
||||
.entanglement-line {
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent,
|
||||
rgba(204, 0, 255, 0.8),
|
||||
transparent);
|
||||
transform-origin: left center;
|
||||
pointer-events: none;
|
||||
animation: entanglementPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes entanglementPulse {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
.toggle-label {
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.9rem;
|
||||
color: #0099ff;
|
||||
}
|
||||
|
||||
/* Traditional Toggle Styles */
|
||||
.traditional-toggle {
|
||||
display: inline-block;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.traditional-toggle input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.traditional-toggle-label {
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background: #ccc;
|
||||
border-radius: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.traditional-toggle-label::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.traditional-toggle input[type="checkbox"]:checked + .traditional-toggle-label {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.traditional-toggle input[type="checkbox"]:checked + .traditional-toggle-label::after {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* Comparison Grid */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.comparison-grid > div {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Documentation */
|
||||
.doc-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
border-left: 3px solid #00ffcc;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.quantum-toggle:focus {
|
||||
outline: 2px solid #00ffcc;
|
||||
outline-offset: 5px;
|
||||
}
|
||||
|
||||
/* Control Panel */
|
||||
.control-panel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-button {
|
||||
background: rgba(0, 255, 204, 0.2);
|
||||
border: 1px solid rgba(0, 255, 204, 0.5);
|
||||
color: #00ffcc;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.control-button:hover {
|
||||
background: rgba(0, 255, 204, 0.3);
|
||||
box-shadow: 0 0 10px rgba(0, 255, 204, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: Quantum State Toggle</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional binary toggles/switches</p>
|
||||
<p><strong>Innovation:</strong> Quantum superposition states with probability visualization</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
|
||||
<div class="control-panel">
|
||||
<button class="control-button" onclick="measureAll()">Measure All States</button>
|
||||
<button class="control-button" onclick="entangleToggles()">Create Entanglement</button>
|
||||
<button class="control-button" onclick="resetToSuperposition()">Reset to Superposition</button>
|
||||
</div>
|
||||
|
||||
<div class="quantum-toggle-container" id="quantumContainer">
|
||||
<!-- Quantum toggles will be dynamically created -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Toggle</h3>
|
||||
<p>Binary states only (ON/OFF)</p>
|
||||
<div style="text-align: center; margin: 2rem 0;">
|
||||
<div class="traditional-toggle">
|
||||
<input type="checkbox" id="trad1">
|
||||
<label class="traditional-toggle-label" for="trad1"></label>
|
||||
</div>
|
||||
<div class="traditional-toggle">
|
||||
<input type="checkbox" id="trad2">
|
||||
<label class="traditional-toggle-label" for="trad2"></label>
|
||||
</div>
|
||||
<div class="traditional-toggle">
|
||||
<input type="checkbox" id="trad3">
|
||||
<label class="traditional-toggle-label" for="trad3"></label>
|
||||
</div>
|
||||
</div>
|
||||
<p>• Immediate state change</p>
|
||||
<p>• No uncertainty representation</p>
|
||||
<p>• Independent operation</p>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>Quantum Toggle</h3>
|
||||
<p>Superposition states with probability</p>
|
||||
<p style="margin: 2rem 0; text-align: center;">
|
||||
See interactive demo above ↑
|
||||
</p>
|
||||
<p>• States exist in superposition</p>
|
||||
<p>• Probability wave visualization</p>
|
||||
<p>• Quantum entanglement possible</p>
|
||||
<p>• Measurement causes wave collapse</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>Users interact with quantum toggles through observation and measurement. Unlike traditional binary switches, these toggles exist in superposition until measured.</p>
|
||||
<p>• <strong>Click/Tap:</strong> Performs a quantum measurement, collapsing the wave function to a definite state</p>
|
||||
<p>• <strong>Hover:</strong> Shows probability amplitudes without collapsing the state</p>
|
||||
<p>• <strong>Entanglement:</strong> Connected toggles affect each other instantaneously</p>
|
||||
<p>• <strong>Keyboard:</strong> Space/Enter to measure, Tab to navigate</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>Built using native web technologies to create quantum-inspired visualizations:</p>
|
||||
<p>• <strong>CSS Animations:</strong> Continuous wave functions and probability clouds</p>
|
||||
<p>• <strong>SVG Paths:</strong> Dynamic wave function visualization</p>
|
||||
<p>• <strong>JavaScript:</strong> Quantum state management and entanglement logic</p>
|
||||
<p>• <strong>CSS Custom Properties:</strong> Real-time probability updates</p>
|
||||
<p>• <strong>Transform & Filters:</strong> Superposition visual effects</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>• Full keyboard navigation with visible focus indicators</p>
|
||||
<p>• ARIA labels describing current quantum state and probability</p>
|
||||
<p>• Screen reader announcements for state changes</p>
|
||||
<p>• High contrast visual indicators for all states</p>
|
||||
<p>• Reduced motion option respects user preferences</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>• <strong>Multi-state Superposition:</strong> Beyond binary to n-dimensional quantum states</p>
|
||||
<p>• <strong>Quantum Gates:</strong> Implement quantum logic operations between toggles</p>
|
||||
<p>• <strong>Decoherence Effects:</strong> Environmental interaction causing gradual state collapse</p>
|
||||
<p>• <strong>Quantum Tunneling:</strong> Probability of spontaneous state changes</p>
|
||||
<p>• <strong>Many-Worlds Visualization:</strong> Show parallel universe states</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Quantum Toggle Implementation
|
||||
class QuantumToggle {
|
||||
constructor(id, label) {
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.state = 'superposition';
|
||||
this.probability = { on: 0.5, off: 0.5 };
|
||||
this.entangled = [];
|
||||
this.element = null;
|
||||
this.isCollapsing = false;
|
||||
}
|
||||
|
||||
createElement() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'quantum-toggle';
|
||||
container.tabIndex = 0;
|
||||
container.setAttribute('role', 'switch');
|
||||
container.setAttribute('aria-checked', 'mixed');
|
||||
container.setAttribute('aria-label', `${this.label} - Quantum state toggle`);
|
||||
|
||||
container.innerHTML = `
|
||||
<span class="sr-only">Quantum toggle: ${this.label}</span>
|
||||
<div class="toggle-label">${this.label}</div>
|
||||
<div class="quantum-field">
|
||||
<div class="probability-cloud"></div>
|
||||
<svg class="wave-function" viewBox="0 0 200 200">
|
||||
<path class="wave-path" stroke-dasharray="5,5" />
|
||||
</svg>
|
||||
<div class="quantum-state state-superposition"></div>
|
||||
</div>
|
||||
<div class="probability-display">|ψ⟩ = √${this.probability.on.toFixed(2)}|1⟩ + √${this.probability.off.toFixed(2)}|0⟩</div>
|
||||
`;
|
||||
|
||||
// Event listeners
|
||||
container.addEventListener('click', () => this.measure());
|
||||
container.addEventListener('keydown', (e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.measure();
|
||||
}
|
||||
});
|
||||
|
||||
this.element = container;
|
||||
this.updateWaveFunction();
|
||||
return container;
|
||||
}
|
||||
|
||||
updateWaveFunction() {
|
||||
if (!this.element) return;
|
||||
|
||||
const path = this.element.querySelector('.wave-path');
|
||||
const amplitude = this.state === 'superposition' ? 30 : 15;
|
||||
const frequency = this.state === 'superposition' ? 3 : 1;
|
||||
|
||||
let d = 'M 0,100 ';
|
||||
for (let x = 0; x <= 200; x += 5) {
|
||||
const y = 100 + amplitude * Math.sin((x / 200) * Math.PI * 2 * frequency) *
|
||||
(this.probability.on - this.probability.off);
|
||||
d += `L ${x},${y} `;
|
||||
}
|
||||
|
||||
path.setAttribute('d', d);
|
||||
}
|
||||
|
||||
measure() {
|
||||
if (this.isCollapsing) return;
|
||||
this.isCollapsing = true;
|
||||
|
||||
// Create collapse animation
|
||||
const collapseWave = document.createElement('div');
|
||||
collapseWave.className = 'collapse-wave';
|
||||
this.element.querySelector('.quantum-field').appendChild(collapseWave);
|
||||
|
||||
// Determine collapsed state based on probability
|
||||
const random = Math.random();
|
||||
const newState = random < this.probability.on ? 'on' : 'off';
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState(newState);
|
||||
collapseWave.remove();
|
||||
this.isCollapsing = false;
|
||||
|
||||
// Affect entangled toggles
|
||||
this.entangled.forEach(toggle => {
|
||||
if (toggle.state === 'superposition') {
|
||||
toggle.influenceFromEntangled(newState);
|
||||
}
|
||||
});
|
||||
}, 300);
|
||||
}
|
||||
|
||||
setState(newState) {
|
||||
this.state = newState;
|
||||
const stateElement = this.element.querySelector('.quantum-state');
|
||||
|
||||
stateElement.className = 'quantum-state';
|
||||
if (newState === 'on') {
|
||||
stateElement.classList.add('state-on');
|
||||
this.probability = { on: 1, off: 0 };
|
||||
this.element.setAttribute('aria-checked', 'true');
|
||||
} else if (newState === 'off') {
|
||||
stateElement.classList.add('state-off');
|
||||
this.probability = { on: 0, off: 1 };
|
||||
this.element.setAttribute('aria-checked', 'false');
|
||||
} else {
|
||||
stateElement.classList.add('state-superposition');
|
||||
this.element.setAttribute('aria-checked', 'mixed');
|
||||
}
|
||||
|
||||
this.updateProbabilityDisplay();
|
||||
this.updateWaveFunction();
|
||||
}
|
||||
|
||||
influenceFromEntangled(entangledState) {
|
||||
if (this.state !== 'superposition') return;
|
||||
|
||||
// Quantum entanglement influence
|
||||
if (entangledState === 'on') {
|
||||
this.probability.on = Math.min(0.8, this.probability.on + 0.2);
|
||||
this.probability.off = 1 - this.probability.on;
|
||||
} else {
|
||||
this.probability.off = Math.min(0.8, this.probability.off + 0.2);
|
||||
this.probability.on = 1 - this.probability.off;
|
||||
}
|
||||
|
||||
this.updateProbabilityDisplay();
|
||||
this.updateWaveFunction();
|
||||
}
|
||||
|
||||
updateProbabilityDisplay() {
|
||||
const display = this.element.querySelector('.probability-display');
|
||||
if (this.state === 'superposition') {
|
||||
display.textContent = `|ψ⟩ = √${this.probability.on.toFixed(2)}|1⟩ + √${this.probability.off.toFixed(2)}|0⟩`;
|
||||
} else if (this.state === 'on') {
|
||||
display.textContent = '|ψ⟩ = |1⟩';
|
||||
} else {
|
||||
display.textContent = '|ψ⟩ = |0⟩';
|
||||
}
|
||||
}
|
||||
|
||||
resetToSuperposition() {
|
||||
this.state = 'superposition';
|
||||
this.probability = { on: 0.5, off: 0.5 };
|
||||
this.setState('superposition');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize quantum toggles
|
||||
const toggles = [];
|
||||
const container = document.getElementById('quantumContainer');
|
||||
|
||||
function initializeToggles() {
|
||||
const labels = ['Notifications', 'Dark Mode', 'Auto-Save'];
|
||||
|
||||
labels.forEach((label, index) => {
|
||||
const toggle = new QuantumToggle(`quantum-${index}`, label);
|
||||
toggles.push(toggle);
|
||||
container.appendChild(toggle.createElement());
|
||||
});
|
||||
}
|
||||
|
||||
function measureAll() {
|
||||
toggles.forEach(toggle => {
|
||||
if (toggle.state === 'superposition') {
|
||||
setTimeout(() => toggle.measure(), Math.random() * 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function entangleToggles() {
|
||||
// Clear existing entanglements
|
||||
toggles.forEach(toggle => toggle.entangled = []);
|
||||
|
||||
// Create entanglement between adjacent toggles
|
||||
for (let i = 0; i < toggles.length - 1; i++) {
|
||||
toggles[i].entangled.push(toggles[i + 1]);
|
||||
toggles[i + 1].entangled.push(toggles[i]);
|
||||
|
||||
// Visual entanglement line
|
||||
const line = document.createElement('div');
|
||||
line.className = 'entanglement-line';
|
||||
line.style.width = '100px';
|
||||
line.style.left = `${150 + i * 230}px`;
|
||||
line.style.top = '100px';
|
||||
container.appendChild(line);
|
||||
|
||||
setTimeout(() => line.remove(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function resetToSuperposition() {
|
||||
toggles.forEach(toggle => toggle.resetToSuperposition());
|
||||
}
|
||||
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (prefersReducedMotion) {
|
||||
document.documentElement.style.setProperty('--animation-duration', '0s');
|
||||
}
|
||||
|
||||
// Initialize on load
|
||||
initializeToggles();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,713 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: GravityNav - Physics-Based Navigation</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: radial-gradient(ellipse at center, #0f0f23 0%, #040410 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #2d2d5a;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
background: #e8e8f5;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
section {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
margin-bottom: 30px;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2d2d5a;
|
||||
margin-bottom: 25px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #3a3a6b;
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
background: radial-gradient(ellipse at center, #1a1a3a 0%, #0a0a1a 100%);
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gravitynav-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: transparent;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.star-field {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: absolute;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
opacity: 0.7;
|
||||
animation: twinkle 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes twinkle {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.9; }
|
||||
}
|
||||
|
||||
.nav-planet {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.nav-planet:hover {
|
||||
box-shadow: 0 0 30px rgba(255, 255, 255, 0.6);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav-planet:focus {
|
||||
outline: 3px solid #ffff00;
|
||||
outline-offset: 5px;
|
||||
box-shadow: 0 0 40px rgba(255, 255, 0, 0.8);
|
||||
}
|
||||
|
||||
.nav-planet.active {
|
||||
box-shadow: 0 0 50px rgba(255, 215, 0, 1);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.planet-home {
|
||||
background: radial-gradient(circle at 30% 30%, #ff6b6b, #d63031);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.planet-about {
|
||||
background: radial-gradient(circle at 30% 30%, #4ecdc4, #00b894);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.planet-projects {
|
||||
background: radial-gradient(circle at 30% 30%, #45b7d1, #0984e3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.planet-contact {
|
||||
background: radial-gradient(circle at 30% 30%, #f39c12, #e17055);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.planet-blog {
|
||||
background: radial-gradient(circle at 30% 30%, #a29bfe, #6c5ce7);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.gravitational-field {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
margin-top: 30px;
|
||||
padding: 30px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
min-height: 200px;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.5s ease;
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.navigation-hint {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.traditional, .innovative {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.traditional {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.innovative {
|
||||
background: #f0f0f8;
|
||||
border-color: #6c5ce7;
|
||||
}
|
||||
|
||||
.traditional-nav {
|
||||
display: flex;
|
||||
background: #e9ecef;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.traditional-nav button {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: #e9ecef;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.traditional-nav button:hover {
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.traditional-nav button.active {
|
||||
background: #6c5ce7;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #6c5ce7;
|
||||
}
|
||||
|
||||
.physics-controls {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.physics-controls label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.physics-controls input[type="range"] {
|
||||
width: 80px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.gravitynav-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.physics-controls {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>UI Innovation: GravityNav - Physics-Based Navigation</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional Tab/Menu Navigation</p>
|
||||
<p><strong>Innovation:</strong> Celestial physics simulation for navigation</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<div class="innovation-component">
|
||||
<div class="gravitynav-container" id="gravityContainer" role="navigation" aria-label="Physics-based navigation">
|
||||
<div class="star-field" id="starField"></div>
|
||||
|
||||
<div class="nav-planet planet-home"
|
||||
data-content="home"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Home navigation">
|
||||
Home
|
||||
</div>
|
||||
|
||||
<div class="nav-planet planet-about"
|
||||
data-content="about"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="About navigation">
|
||||
About
|
||||
</div>
|
||||
|
||||
<div class="nav-planet planet-projects"
|
||||
data-content="projects"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Projects navigation">
|
||||
Projects
|
||||
</div>
|
||||
|
||||
<div class="nav-planet planet-contact"
|
||||
data-content="contact"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Contact navigation">
|
||||
Contact
|
||||
</div>
|
||||
|
||||
<div class="nav-planet planet-blog"
|
||||
data-content="blog"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="Blog navigation">
|
||||
Blog
|
||||
</div>
|
||||
|
||||
<div class="physics-controls" id="physicsControls">
|
||||
<label for="gravitySlider">Gravity:</label>
|
||||
<input type="range" id="gravitySlider" min="0.1" max="2" step="0.1" value="1">
|
||||
|
||||
<label for="dampingSlider">Damping:</label>
|
||||
<input type="range" id="dampingSlider" min="0.8" max="0.99" step="0.01" value="0.95">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-panel" id="contentPanel" aria-live="polite">
|
||||
<div class="content-section active" data-content="home">
|
||||
<h3>🏠 Welcome Home</h3>
|
||||
<p>This is the home page content. The GravityNav system allows you to navigate through different sections using physics-based interactions. Click on any planet to see gravitational effects in action!</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section" data-content="about">
|
||||
<h3>👤 About</h3>
|
||||
<p>Learn about the innovative navigation paradigm. Each navigation element behaves like a celestial body with its own gravitational field. Mouse interactions create attractive forces that pull planets toward your cursor.</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section" data-content="projects">
|
||||
<h3>🚀 Projects</h3>
|
||||
<p>Explore various projects and implementations. The physics simulation runs at 60fps and responds to real-time user interactions. Each planet maintains momentum and responds to gravitational influences.</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section" data-content="contact">
|
||||
<h3>📬 Contact</h3>
|
||||
<p>Get in touch through various channels. The system maintains accessibility through keyboard navigation while providing an engaging visual experience that transforms traditional navigation paradigms.</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section" data-content="blog">
|
||||
<h3>📝 Blog</h3>
|
||||
<p>Read latest articles and insights. The adaptive physics system responds to user preferences, allowing customization of gravitational strength and system damping for different interaction styles.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navigation-hint">
|
||||
Move your mouse around to create gravitational fields! Click planets to navigate. Use keyboard (Tab/Enter) for accessibility.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Tab Navigation</h3>
|
||||
<p>Standard horizontal or vertical tab layout with static positioning.</p>
|
||||
<div class="traditional-nav">
|
||||
<button class="active">Home</button>
|
||||
<button>About</button>
|
||||
<button>Projects</button>
|
||||
<button>Contact</button>
|
||||
<button>Blog</button>
|
||||
</div>
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #666;">
|
||||
✓ Predictable layout<br>
|
||||
✓ Fast recognition<br>
|
||||
✗ Static visual feedback<br>
|
||||
✗ Limited engagement<br>
|
||||
✗ No spatial relationships
|
||||
</p>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>GravityNav System</h3>
|
||||
<p>Physics-based navigation with gravitational interactions and celestial metaphors.</p>
|
||||
<p style="margin-top: 15px; font-size: 14px; color: #2d2d5a;">
|
||||
✓ Engaging physics simulation<br>
|
||||
✓ Natural spatial relationships<br>
|
||||
✓ Real-time visual feedback<br>
|
||||
✓ Memorable interaction model<br>
|
||||
✓ Customizable behavior<br>
|
||||
✓ Full accessibility support
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>Users interact through mouse movement creating gravitational fields that attract navigation planets. Clicking selects a navigation item and triggers content updates. Planets respond with realistic physics including momentum, attraction, and damping. Keyboard navigation maintains traditional accessibility patterns.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>Built using requestAnimationFrame for 60fps physics simulation. Vector mathematics calculate gravitational forces between mouse position and planets. CSS transforms provide smooth hardware-accelerated animations. Physics controls allow real-time parameter adjustment for gravity strength and system damping.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>Maintains full keyboard navigation with Tab cycling and Enter activation. ARIA labels and roles ensure screen reader compatibility. Content changes are announced via aria-live regions. Focus indicators provide clear visual feedback. All physics interactions have keyboard equivalents.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>Future enhancements could include orbital mechanics for sub-navigation, collision detection between planets, 3D space navigation, voice-controlled gravitational fields, collaborative navigation where multiple users influence the same space, and adaptive AI that learns user navigation patterns.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
class GravityNav {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.planets = Array.from(container.querySelectorAll('.nav-planet'));
|
||||
this.starField = container.querySelector('#starField');
|
||||
this.contentPanel = container.querySelector('#contentPanel');
|
||||
this.gravitySlider = container.querySelector('#gravitySlider');
|
||||
this.dampingSlider = container.querySelector('#dampingSlider');
|
||||
|
||||
this.mouse = { x: 0, y: 0 };
|
||||
this.gravity = 1;
|
||||
this.damping = 0.95;
|
||||
this.animationId = null;
|
||||
|
||||
this.planetData = this.planets.map(planet => ({
|
||||
element: planet,
|
||||
x: 0, y: 0,
|
||||
vx: 0, vy: 0,
|
||||
mass: 50,
|
||||
radius: 40,
|
||||
restX: 0, restY: 0,
|
||||
active: false
|
||||
}));
|
||||
|
||||
this.initializePositions();
|
||||
this.createStarField();
|
||||
this.initializeEvents();
|
||||
this.startPhysicsLoop();
|
||||
this.setActiveContent('home');
|
||||
}
|
||||
|
||||
initializePositions() {
|
||||
const containerRect = this.container.getBoundingClientRect();
|
||||
const centerX = containerRect.width / 2;
|
||||
const centerY = containerRect.height / 2;
|
||||
const radius = Math.min(containerRect.width, containerRect.height) * 0.3;
|
||||
|
||||
this.planetData.forEach((planet, index) => {
|
||||
const angle = (index / this.planetData.length) * Math.PI * 2;
|
||||
const x = centerX + Math.cos(angle) * radius;
|
||||
const y = centerY + Math.sin(angle) * radius;
|
||||
|
||||
planet.x = planet.restX = x;
|
||||
planet.y = planet.restY = y;
|
||||
planet.radius = 30 + Math.random() * 20;
|
||||
|
||||
this.updatePlanetPosition(planet);
|
||||
});
|
||||
}
|
||||
|
||||
createStarField() {
|
||||
const starCount = 50;
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
const star = document.createElement('div');
|
||||
star.className = 'star';
|
||||
|
||||
const size = Math.random() * 2 + 1;
|
||||
star.style.width = size + 'px';
|
||||
star.style.height = size + 'px';
|
||||
star.style.left = Math.random() * 100 + '%';
|
||||
star.style.top = Math.random() * 100 + '%';
|
||||
star.style.animationDelay = Math.random() * 3 + 's';
|
||||
|
||||
this.starField.appendChild(star);
|
||||
}
|
||||
}
|
||||
|
||||
initializeEvents() {
|
||||
// Mouse tracking
|
||||
this.container.addEventListener('mousemove', (e) => {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
this.mouse.x = e.clientX - rect.left;
|
||||
this.mouse.y = e.clientY - rect.top;
|
||||
});
|
||||
|
||||
this.container.addEventListener('mouseleave', () => {
|
||||
this.mouse.x = -1000;
|
||||
this.mouse.y = -1000;
|
||||
});
|
||||
|
||||
// Planet interactions
|
||||
this.planets.forEach(planet => {
|
||||
planet.addEventListener('click', () => this.selectPlanet(planet));
|
||||
planet.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this.selectPlanet(planet);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Physics controls
|
||||
this.gravitySlider.addEventListener('input', (e) => {
|
||||
this.gravity = parseFloat(e.target.value);
|
||||
});
|
||||
|
||||
this.dampingSlider.addEventListener('input', (e) => {
|
||||
this.damping = parseFloat(e.target.value);
|
||||
});
|
||||
|
||||
// Window resize
|
||||
window.addEventListener('resize', () => {
|
||||
this.initializePositions();
|
||||
});
|
||||
}
|
||||
|
||||
selectPlanet(planetElement) {
|
||||
const contentType = planetElement.dataset.content;
|
||||
this.setActiveContent(contentType);
|
||||
|
||||
// Update planet states
|
||||
this.planets.forEach(p => p.classList.remove('active'));
|
||||
planetElement.classList.add('active');
|
||||
|
||||
// Update planet data
|
||||
this.planetData.forEach(planet => {
|
||||
planet.active = planet.element === planetElement;
|
||||
if (planet.active) {
|
||||
planet.mass = 80;
|
||||
planet.radius = 50;
|
||||
} else {
|
||||
planet.mass = 50;
|
||||
planet.radius = 30 + Math.random() * 20;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setActiveContent(contentType) {
|
||||
const contentSections = this.contentPanel.querySelectorAll('.content-section');
|
||||
contentSections.forEach(section => {
|
||||
section.classList.remove('active');
|
||||
if (section.dataset.content === contentType) {
|
||||
section.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updatePhysics() {
|
||||
this.planetData.forEach(planet => {
|
||||
// Calculate gravitational force from mouse
|
||||
const dx = this.mouse.x - planet.x;
|
||||
const dy = this.mouse.y - planet.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > 0 && distance < 200) {
|
||||
const force = (this.gravity * planet.mass) / (distance * distance);
|
||||
const fx = (dx / distance) * force;
|
||||
const fy = (dy / distance) * force;
|
||||
|
||||
planet.vx += fx * 0.01;
|
||||
planet.vy += fy * 0.01;
|
||||
}
|
||||
|
||||
// Spring force back to rest position
|
||||
const restDx = planet.restX - planet.x;
|
||||
const restDy = planet.restY - planet.y;
|
||||
const springForce = 0.02;
|
||||
|
||||
planet.vx += restDx * springForce;
|
||||
planet.vy += restDy * springForce;
|
||||
|
||||
// Apply damping
|
||||
planet.vx *= this.damping;
|
||||
planet.vy *= this.damping;
|
||||
|
||||
// Update position
|
||||
planet.x += planet.vx;
|
||||
planet.y += planet.vy;
|
||||
|
||||
// Planet-to-planet interactions
|
||||
this.planetData.forEach(other => {
|
||||
if (other !== planet) {
|
||||
const pdx = other.x - planet.x;
|
||||
const pdy = other.y - planet.y;
|
||||
const pdist = Math.sqrt(pdx * pdx + pdy * pdy);
|
||||
|
||||
if (pdist > 0 && pdist < 100) {
|
||||
const repulsion = 50 / (pdist * pdist);
|
||||
planet.vx -= (pdx / pdist) * repulsion * 0.001;
|
||||
planet.vy -= (pdy / pdist) * repulsion * 0.001;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.updatePlanetPosition(planet);
|
||||
});
|
||||
}
|
||||
|
||||
updatePlanetPosition(planet) {
|
||||
const element = planet.element;
|
||||
const size = planet.radius;
|
||||
|
||||
element.style.width = size + 'px';
|
||||
element.style.height = size + 'px';
|
||||
element.style.left = (planet.x - size/2) + 'px';
|
||||
element.style.top = (planet.y - size/2) + 'px';
|
||||
element.style.fontSize = (size * 0.25) + 'px';
|
||||
|
||||
if (planet.active) {
|
||||
element.style.transform = 'scale(1.2)';
|
||||
} else {
|
||||
element.style.transform = 'scale(1)';
|
||||
}
|
||||
}
|
||||
|
||||
startPhysicsLoop() {
|
||||
const animate = () => {
|
||||
this.updatePhysics();
|
||||
this.animationId = requestAnimationFrame(animate);
|
||||
};
|
||||
animate();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.animationId) {
|
||||
cancelAnimationFrame(this.animationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize GravityNav when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.querySelector('#gravityContainer');
|
||||
const gravityNav = new GravityNav(container);
|
||||
|
||||
// Make it available globally
|
||||
window.gravityNav = gravityNav;
|
||||
|
||||
// Traditional nav demo functionality
|
||||
const traditionalButtons = document.querySelectorAll('.traditional-nav button');
|
||||
traditionalButtons.forEach((button, index) => {
|
||||
button.addEventListener('click', () => {
|
||||
traditionalButtons.forEach(b => b.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
console.log('GravityNav initialized. Move your mouse around the navigation area!');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,884 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: RainFlow Control</title>
|
||||
<style>
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f5f5f5 100%);
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #1976d2;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
margin: 0.5rem 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #1565c0;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #0d47a1;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
/* RainFlow Control Styles */
|
||||
.demo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rainflow-container {
|
||||
display: flex;
|
||||
gap: 3rem;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.rainflow-control {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
background: linear-gradient(180deg,
|
||||
#87ceeb 0%,
|
||||
#b0e0e6 20%,
|
||||
#e0f7fa 40%,
|
||||
#ffffff 60%,
|
||||
#f5f5f5 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 10px 30px rgba(0, 0, 0, 0.1),
|
||||
inset 0 0 20px rgba(255, 255, 255, 0.5);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cloud-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100px;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(255, 255, 255, 0.9) 0%,
|
||||
rgba(255, 255, 255, 0.6) 50%,
|
||||
transparent 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cloud {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 30px;
|
||||
background: #fff;
|
||||
border-radius: 100px;
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cloud::before,
|
||||
.cloud::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.cloud::before {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
top: -20px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.cloud::after {
|
||||
width: 50px;
|
||||
height: 35px;
|
||||
top: -15px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.cloud.active {
|
||||
background: #9e9e9e;
|
||||
animation: rumble 0.3s ease infinite;
|
||||
}
|
||||
|
||||
.cloud.active::before,
|
||||
.cloud.active::after {
|
||||
background: #9e9e9e;
|
||||
}
|
||||
|
||||
@keyframes rumble {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px); }
|
||||
75% { transform: translateX(2px); }
|
||||
}
|
||||
|
||||
.rain-area {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.raindrop {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 15px;
|
||||
background: linear-gradient(180deg,
|
||||
transparent 0%,
|
||||
rgba(30, 144, 255, 0.6) 50%,
|
||||
rgba(30, 144, 255, 0.8) 100%);
|
||||
animation: fall linear infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(150px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.water-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
border-radius: 0 0 20px 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.water-level {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(30, 144, 255, 0.6) 0%,
|
||||
rgba(30, 144, 255, 0.8) 50%,
|
||||
rgba(0, 119, 190, 0.9) 100%);
|
||||
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.water-level::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10%;
|
||||
right: -10%;
|
||||
height: 20px;
|
||||
background: inherit;
|
||||
border-radius: 50%;
|
||||
animation: wave 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: translateY(0) scaleY(1); }
|
||||
50% { transform: translateY(-5px) scaleY(0.8); }
|
||||
}
|
||||
|
||||
.water-bubbles {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 50%;
|
||||
animation: bubble-rise linear infinite;
|
||||
}
|
||||
|
||||
@keyframes bubble-rise {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-200px) scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.value-display {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
color: #0d47a1;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.control-info {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 600;
|
||||
color: #1565c0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Traditional Slider for Comparison */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.traditional-slider {
|
||||
padding: 2rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.traditional-slider input[type="range"] {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.slider-value {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Documentation Styles */
|
||||
.documentation {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #1976d2;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
color: #555;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Accessibility Focus Styles */
|
||||
.rainflow-control:focus {
|
||||
outline: 3px solid #1976d2;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.rainflow-control:focus .cloud {
|
||||
animation: pulse 1s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* Screen Reader Only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.rainflow-container {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: RainFlow Control</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional Slider/Range Input</p>
|
||||
<p><strong>Innovation:</strong> Natural weather-based fluid dynamics for value control</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<p style="text-align: center; color: #666; margin-bottom: 2rem;">
|
||||
Click and hold the cloud to make it rain. The water level represents your selected value.
|
||||
Release to stop the rain and watch the water settle.
|
||||
</p>
|
||||
|
||||
<div class="rainflow-container">
|
||||
<!-- Volume Control -->
|
||||
<div>
|
||||
<div class="control-label">Volume Control</div>
|
||||
<div class="rainflow-control"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-label="Volume control using rain flow"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="50"
|
||||
data-min="0"
|
||||
data-max="100"
|
||||
data-value="50"
|
||||
data-label="Volume">
|
||||
<div class="cloud-layer">
|
||||
<div class="cloud"></div>
|
||||
</div>
|
||||
<div class="rain-area"></div>
|
||||
<div class="water-container">
|
||||
<div class="water-level" style="height: 50%;">
|
||||
<div class="water-bubbles"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value-display">50%</div>
|
||||
<span class="sr-only" role="status" aria-live="polite">Volume: 50%</span>
|
||||
</div>
|
||||
<div class="control-info">
|
||||
<p style="color: #666; font-size: 0.9rem;">Current: <span class="current-value">50</span>%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brightness Control -->
|
||||
<div>
|
||||
<div class="control-label">Brightness Control</div>
|
||||
<div class="rainflow-control"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-label="Brightness control using rain flow"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="75"
|
||||
data-min="0"
|
||||
data-max="100"
|
||||
data-value="75"
|
||||
data-label="Brightness">
|
||||
<div class="cloud-layer">
|
||||
<div class="cloud"></div>
|
||||
</div>
|
||||
<div class="rain-area"></div>
|
||||
<div class="water-container">
|
||||
<div class="water-level" style="height: 75%;">
|
||||
<div class="water-bubbles"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value-display">75%</div>
|
||||
<span class="sr-only" role="status" aria-live="polite">Brightness: 75%</span>
|
||||
</div>
|
||||
<div class="control-info">
|
||||
<p style="color: #666; font-size: 0.9rem;">Current: <span class="current-value">75</span>%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Control -->
|
||||
<div>
|
||||
<div class="control-label">Temperature Control</div>
|
||||
<div class="rainflow-control"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-label="Temperature control using rain flow"
|
||||
aria-valuemin="60"
|
||||
aria-valuemax="90"
|
||||
aria-valuenow="72"
|
||||
data-min="60"
|
||||
data-max="90"
|
||||
data-value="72"
|
||||
data-label="Temperature"
|
||||
data-unit="°F">
|
||||
<div class="cloud-layer">
|
||||
<div class="cloud"></div>
|
||||
</div>
|
||||
<div class="rain-area"></div>
|
||||
<div class="water-container">
|
||||
<div class="water-level" style="height: 40%;">
|
||||
<div class="water-bubbles"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="value-display">72°F</div>
|
||||
<span class="sr-only" role="status" aria-live="polite">Temperature: 72°F</span>
|
||||
</div>
|
||||
<div class="control-info">
|
||||
<p style="color: #666; font-size: 0.9rem;">Current: <span class="current-value">72</span>°F</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Range Sliders</h3>
|
||||
<div class="traditional-slider">
|
||||
<label for="traditional-volume">Volume:</label>
|
||||
<input type="range" id="traditional-volume" min="0" max="100" value="50">
|
||||
<div class="slider-value">50%</div>
|
||||
</div>
|
||||
<div class="traditional-slider">
|
||||
<label for="traditional-brightness">Brightness:</label>
|
||||
<input type="range" id="traditional-brightness" min="0" max="100" value="75">
|
||||
<div class="slider-value">75%</div>
|
||||
</div>
|
||||
<div class="traditional-slider">
|
||||
<label for="traditional-temp">Temperature:</label>
|
||||
<input type="range" id="traditional-temp" min="60" max="90" value="72">
|
||||
<div class="slider-value">72°F</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>RainFlow Controls</h3>
|
||||
<p style="color: #666; line-height: 1.8;">
|
||||
The innovative RainFlow controls above replace traditional sliders with an intuitive
|
||||
weather metaphor. Users control values by creating rain that fills a container,
|
||||
making the interaction more engaging and visually meaningful.
|
||||
</p>
|
||||
<ul style="color: #666; margin-top: 1rem; padding-left: 1.5rem;">
|
||||
<li>Click and hold to make it rain (increase value)</li>
|
||||
<li>Release to stop rain (value settles)</li>
|
||||
<li>Natural fluid physics create smooth transitions</li>
|
||||
<li>Visual feedback through water level and animation</li>
|
||||
<li>Accessible with keyboard controls (Space/Enter to rain, Arrow keys for fine control)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>
|
||||
RainFlow Controls transform the abstract concept of value adjustment into a tangible,
|
||||
natural process. Users interact with a cloud that produces rain, filling a container
|
||||
with water. The water level directly represents the selected value, creating an
|
||||
immediate visual connection between action and result. This metaphor leverages our
|
||||
innate understanding of weather and fluid dynamics to make digital controls feel
|
||||
more natural and engaging.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>
|
||||
Built entirely with native web technologies, RainFlow Controls use CSS animations
|
||||
for smooth fluid motion and JavaScript for interaction handling. The rain effect
|
||||
is created dynamically with individual raindrop elements, while the water level
|
||||
uses CSS transforms and transitions for realistic fluid behavior. The component
|
||||
implements proper ARIA attributes for accessibility and uses requestAnimationFrame
|
||||
for optimal performance during continuous interactions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>
|
||||
Full keyboard navigation is supported with Space/Enter keys triggering rain and
|
||||
Arrow keys providing fine-grained control. Screen readers announce value changes
|
||||
through ARIA live regions. The component maintains proper focus states and provides
|
||||
visual feedback for keyboard users. High contrast is maintained between the water
|
||||
level and background, and all interactive elements meet WCAG 2.1 AA standards for
|
||||
size and spacing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>
|
||||
Future iterations could incorporate temperature-based color gradients (blue for
|
||||
cold, red for hot), storm intensity for rapid value changes, evaporation mechanics
|
||||
for value decay over time, and multiple cloud types for different input speeds.
|
||||
The system could also support collaborative controls where multiple users contribute
|
||||
to a shared water level, or implement weather patterns that predict and suggest
|
||||
optimal values based on usage patterns.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// RainFlow Control Implementation
|
||||
class RainFlowControl {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.cloud = element.querySelector('.cloud');
|
||||
this.rainArea = element.querySelector('.rain-area');
|
||||
this.waterLevel = element.querySelector('.water-level');
|
||||
this.waterBubbles = element.querySelector('.water-bubbles');
|
||||
this.valueDisplay = element.querySelector('.value-display');
|
||||
this.srStatus = element.querySelector('[role="status"]');
|
||||
|
||||
// Get data attributes
|
||||
this.min = parseFloat(element.dataset.min) || 0;
|
||||
this.max = parseFloat(element.dataset.max) || 100;
|
||||
this.value = parseFloat(element.dataset.value) || 50;
|
||||
this.label = element.dataset.label || 'Value';
|
||||
this.unit = element.dataset.unit || '%';
|
||||
|
||||
// State
|
||||
this.isRaining = false;
|
||||
this.rainInterval = null;
|
||||
this.bubbleInterval = null;
|
||||
this.raindrops = [];
|
||||
this.bubbles = [];
|
||||
|
||||
// Bind methods
|
||||
this.startRain = this.startRain.bind(this);
|
||||
this.stopRain = this.stopRain.bind(this);
|
||||
this.updateValue = this.updateValue.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this);
|
||||
|
||||
// Initialize
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Mouse events
|
||||
this.element.addEventListener('mousedown', this.startRain);
|
||||
this.element.addEventListener('mouseup', this.stopRain);
|
||||
this.element.addEventListener('mouseleave', this.stopRain);
|
||||
|
||||
// Touch events
|
||||
this.element.addEventListener('touchstart', this.startRain);
|
||||
this.element.addEventListener('touchend', this.stopRain);
|
||||
|
||||
// Keyboard events
|
||||
this.element.addEventListener('keydown', this.handleKeyDown);
|
||||
this.element.addEventListener('keyup', this.handleKeyUp);
|
||||
|
||||
// Prevent text selection
|
||||
this.element.addEventListener('selectstart', e => e.preventDefault());
|
||||
|
||||
// Initialize water level
|
||||
this.updateWaterLevel();
|
||||
}
|
||||
|
||||
startRain(e) {
|
||||
e.preventDefault();
|
||||
if (this.isRaining) return;
|
||||
|
||||
this.isRaining = true;
|
||||
this.cloud.classList.add('active');
|
||||
|
||||
// Start creating raindrops
|
||||
this.rainInterval = setInterval(() => {
|
||||
this.createRaindrop();
|
||||
|
||||
// Increase value while raining
|
||||
if (this.value < this.max) {
|
||||
this.value = Math.min(this.value + 1, this.max);
|
||||
this.updateValue();
|
||||
}
|
||||
}, 50);
|
||||
|
||||
// Start creating bubbles
|
||||
this.bubbleInterval = setInterval(() => {
|
||||
this.createBubble();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
stopRain() {
|
||||
if (!this.isRaining) return;
|
||||
|
||||
this.isRaining = false;
|
||||
this.cloud.classList.remove('active');
|
||||
|
||||
// Stop creating raindrops
|
||||
clearInterval(this.rainInterval);
|
||||
clearInterval(this.bubbleInterval);
|
||||
|
||||
// Clear remaining raindrops after animation
|
||||
setTimeout(() => {
|
||||
this.raindrops.forEach(drop => drop.remove());
|
||||
this.raindrops = [];
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
createRaindrop() {
|
||||
const drop = document.createElement('div');
|
||||
drop.className = 'raindrop';
|
||||
|
||||
// Random horizontal position under cloud
|
||||
const cloudRect = this.cloud.getBoundingClientRect();
|
||||
const containerRect = this.element.getBoundingClientRect();
|
||||
const minX = cloudRect.left - containerRect.left;
|
||||
const maxX = cloudRect.right - containerRect.left;
|
||||
|
||||
drop.style.left = `${minX + Math.random() * (maxX - minX)}px`;
|
||||
drop.style.animationDuration = `${0.5 + Math.random() * 0.5}s`;
|
||||
drop.style.animationDelay = `${Math.random() * 0.2}s`;
|
||||
|
||||
this.rainArea.appendChild(drop);
|
||||
this.raindrops.push(drop);
|
||||
|
||||
// Remove after animation
|
||||
setTimeout(() => {
|
||||
drop.remove();
|
||||
const index = this.raindrops.indexOf(drop);
|
||||
if (index > -1) {
|
||||
this.raindrops.splice(index, 1);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
createBubble() {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
|
||||
// Random size and position
|
||||
const size = 4 + Math.random() * 8;
|
||||
bubble.style.width = `${size}px`;
|
||||
bubble.style.height = `${size}px`;
|
||||
bubble.style.left = `${10 + Math.random() * 80}%`;
|
||||
bubble.style.bottom = `${Math.random() * 20}px`;
|
||||
bubble.style.animationDuration = `${2 + Math.random() * 2}s`;
|
||||
|
||||
this.waterBubbles.appendChild(bubble);
|
||||
this.bubbles.push(bubble);
|
||||
|
||||
// Remove after animation
|
||||
setTimeout(() => {
|
||||
bubble.remove();
|
||||
const index = this.bubbles.indexOf(bubble);
|
||||
if (index > -1) {
|
||||
this.bubbles.splice(index, 1);
|
||||
}
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
updateValue() {
|
||||
this.updateWaterLevel();
|
||||
this.updateDisplay();
|
||||
this.updateAria();
|
||||
|
||||
// Update info display
|
||||
const container = this.element.closest('div');
|
||||
const infoValue = container.querySelector('.current-value');
|
||||
if (infoValue) {
|
||||
infoValue.textContent = Math.round(this.value);
|
||||
}
|
||||
|
||||
// Dispatch custom event
|
||||
this.element.dispatchEvent(new CustomEvent('rainflowchange', {
|
||||
detail: { value: this.value }
|
||||
}));
|
||||
}
|
||||
|
||||
updateWaterLevel() {
|
||||
const percentage = ((this.value - this.min) / (this.max - this.min)) * 100;
|
||||
this.waterLevel.style.height = `${percentage}%`;
|
||||
|
||||
// Update water color based on level
|
||||
const hue = 200 + (percentage * 0.2); // Blue to slightly purple
|
||||
const lightness = 50 - (percentage * 0.2); // Darker as it fills
|
||||
this.waterLevel.style.background = `linear-gradient(180deg,
|
||||
hsla(${hue}, 70%, ${lightness}%, 0.6) 0%,
|
||||
hsla(${hue}, 70%, ${lightness - 10}%, 0.8) 50%,
|
||||
hsla(${hue}, 70%, ${lightness - 20}%, 0.9) 100%)`;
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
const displayValue = this.unit === '%'
|
||||
? `${Math.round(this.value)}%`
|
||||
: `${Math.round(this.value)}${this.unit}`;
|
||||
this.valueDisplay.textContent = displayValue;
|
||||
}
|
||||
|
||||
updateAria() {
|
||||
this.element.setAttribute('aria-valuenow', this.value);
|
||||
const statusText = `${this.label}: ${Math.round(this.value)}${this.unit}`;
|
||||
this.srStatus.textContent = statusText;
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
this.startRain(e);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.value = Math.min(this.value + 1, this.max);
|
||||
this.updateValue();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.value = Math.max(this.value - 1, this.min);
|
||||
this.updateValue();
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
this.value = this.min;
|
||||
this.updateValue();
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
this.value = this.max;
|
||||
this.updateValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.stopRain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize all RainFlow controls
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const controls = document.querySelectorAll('.rainflow-control');
|
||||
controls.forEach(control => {
|
||||
new RainFlowControl(control);
|
||||
});
|
||||
|
||||
// Sync traditional sliders for comparison
|
||||
const traditionalSliders = document.querySelectorAll('input[type="range"]');
|
||||
traditionalSliders.forEach(slider => {
|
||||
slider.addEventListener('input', (e) => {
|
||||
const valueDisplay = e.target.parentElement.querySelector('.slider-value');
|
||||
const value = e.target.value;
|
||||
if (e.target.id.includes('temp')) {
|
||||
valueDisplay.textContent = `${value}°F`;
|
||||
} else {
|
||||
valueDisplay.textContent = `${value}%`;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Performance optimization: Clean up animations when not visible
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const control = entry.target.querySelector('.rainflow-control');
|
||||
if (control && control.rainflowInstance) {
|
||||
if (!entry.isIntersecting) {
|
||||
control.rainflowInstance.stopRain();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.rainflow-container').forEach(container => {
|
||||
observer.observe(container);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,890 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: Memory Stream</title>
|
||||
<style>
|
||||
/* Base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: #f5f5f7;
|
||||
padding: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Memory Stream Component Styles */
|
||||
.memory-stream-container {
|
||||
position: relative;
|
||||
height: 600px;
|
||||
background: linear-gradient(180deg, #f8f9ff 0%, #e8ebff 100%);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 30px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.temporal-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg,
|
||||
rgba(255,255,255,0) 0%,
|
||||
rgba(255,255,255,0.3) 20%,
|
||||
rgba(255,255,255,0.7) 60%,
|
||||
rgba(255,255,255,0.95) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.memory-item {
|
||||
position: absolute;
|
||||
padding: 15px 20px;
|
||||
border-radius: 15px;
|
||||
max-width: 300px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-origin: center;
|
||||
animation: memoryPulse 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes memoryPulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.02); }
|
||||
}
|
||||
|
||||
.memory-item.fading {
|
||||
animation: fadeOut 3s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.memory-item.recalling {
|
||||
animation: recall 0.6s ease-out;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
@keyframes recall {
|
||||
0% {
|
||||
transform: scale(0.8) translateY(20px);
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Emotional States */
|
||||
.memory-item.joy {
|
||||
background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
|
||||
box-shadow: 0 4px 20px rgba(253, 203, 110, 0.3);
|
||||
}
|
||||
|
||||
.memory-item.urgent {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
box-shadow: 0 4px 20px rgba(238, 90, 36, 0.3);
|
||||
animation-duration: 2s;
|
||||
}
|
||||
|
||||
.memory-item.info {
|
||||
background: linear-gradient(135deg, #74b9ff 0%, #a29bfe 100%);
|
||||
box-shadow: 0 4px 20px rgba(162, 155, 254, 0.3);
|
||||
}
|
||||
|
||||
.memory-item.success {
|
||||
background: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
|
||||
box-shadow: 0 4px 20px rgba(0, 184, 148, 0.3);
|
||||
}
|
||||
|
||||
.memory-item.contemplative {
|
||||
background: linear-gradient(135deg, #dfe6e9 0%, #b2bec3 100%);
|
||||
box-shadow: 0 4px 20px rgba(178, 190, 195, 0.3);
|
||||
}
|
||||
|
||||
.memory-content {
|
||||
color: white;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.memory-timestamp {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* Memory Controls */
|
||||
.memory-controls {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.memory-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.memory-btn:hover {
|
||||
background: #5a65d6;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.memory-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.emotion-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
background: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.emotion-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.emotion-btn:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.emotion-btn.joy { background: #fdcb6e; }
|
||||
.emotion-btn.urgent { background: #ee5a24; }
|
||||
.emotion-btn.info { background: #a29bfe; }
|
||||
.emotion-btn.success { background: #00b894; }
|
||||
.emotion-btn.contemplative { background: #b2bec3; }
|
||||
|
||||
/* Memory Recall Interface */
|
||||
.recall-interface {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 15px 30px;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.recall-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 5px 10px;
|
||||
font-size: 16px;
|
||||
width: 200px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.recall-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.recall-btn:hover {
|
||||
background: #5a65d6;
|
||||
}
|
||||
|
||||
/* Traditional Comparison */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.traditional, .innovative {
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.traditional-alert {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Documentation */
|
||||
.documentation {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
color: #666;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles */
|
||||
.memory-item:focus {
|
||||
outline: 3px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.memory-btn:focus,
|
||||
.emotion-btn:focus,
|
||||
.recall-btn:focus {
|
||||
outline: 3px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Memory statistics */
|
||||
.memory-stats {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
margin-bottom: 5px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.memory-stream-container {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.memory-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: Memory Stream</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional notifications and alerts</p>
|
||||
<p><strong>Innovation:</strong> Temporal memory system with emotional states and natural recall patterns</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<div class="memory-stream-container" role="region" aria-label="Memory Stream">
|
||||
<div class="temporal-gradient"></div>
|
||||
|
||||
<div class="memory-stats">
|
||||
<div class="stat-item">Active Memories: <span id="active-count">0</span></div>
|
||||
<div class="stat-item">Faded Memories: <span id="faded-count">0</span></div>
|
||||
<div class="stat-item">Recalled: <span id="recalled-count">0</span></div>
|
||||
</div>
|
||||
|
||||
<div id="memory-container" role="log" aria-live="polite" aria-label="Memory notifications"></div>
|
||||
|
||||
<div class="recall-interface">
|
||||
<label for="recall-input" class="visually-hidden">Search memories</label>
|
||||
<input type="text" id="recall-input" class="recall-input" placeholder="Recall a memory..." />
|
||||
<button class="recall-btn" onclick="recallMemories()">
|
||||
<span aria-hidden="true">🔍</span>
|
||||
<span class="visually-hidden">Search memories</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="memory-controls">
|
||||
<button class="memory-btn" onclick="createMemory('info')">Add Info Memory</button>
|
||||
<button class="memory-btn" onclick="createMemory('urgent')">Add Urgent Memory</button>
|
||||
<button class="memory-btn" onclick="createMemory('success')">Add Success Memory</button>
|
||||
<button class="memory-btn" onclick="createMemory('joy')">Add Joy Memory</button>
|
||||
<button class="memory-btn" onclick="createMemory('contemplative')">Add Contemplative Memory</button>
|
||||
|
||||
<div class="emotion-selector">
|
||||
<span>Custom emotion:</span>
|
||||
<button class="emotion-btn joy" onclick="setCustomEmotion('joy')" aria-label="Joy"></button>
|
||||
<button class="emotion-btn urgent" onclick="setCustomEmotion('urgent')" aria-label="Urgent"></button>
|
||||
<button class="emotion-btn info" onclick="setCustomEmotion('info')" aria-label="Info"></button>
|
||||
<button class="emotion-btn success" onclick="setCustomEmotion('success')" aria-label="Success"></button>
|
||||
<button class="emotion-btn contemplative" onclick="setCustomEmotion('contemplative')" aria-label="Contemplative"></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Notifications</h3>
|
||||
<div class="traditional-alert">
|
||||
<span>System update available!</span>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="traditional-alert" style="background: #f39c12;">
|
||||
<span>Warning: Low battery</span>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="traditional-alert" style="background: #27ae60;">
|
||||
<span>File saved successfully</span>
|
||||
<button class="close-btn">×</button>
|
||||
</div>
|
||||
<p style="margin-top: 15px; color: #666;">
|
||||
Traditional alerts interrupt, stack uniformly, and disappear permanently when dismissed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>Memory Stream System</h3>
|
||||
<p style="color: #666;">
|
||||
The Memory Stream (above) treats notifications as memories that:
|
||||
</p>
|
||||
<ul style="color: #666; margin-top: 10px; padding-left: 20px;">
|
||||
<li>Float and drift naturally in temporal space</li>
|
||||
<li>Fade gradually based on importance and time</li>
|
||||
<li>Can be recalled through search or interaction</li>
|
||||
<li>Carry emotional context and urgency</li>
|
||||
<li>Learn from user interaction patterns</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>
|
||||
The Memory Stream reimagines notifications as temporal memories that exist in a continuous space.
|
||||
Users interact through natural gestures: memories drift upward like thoughts, important ones persist longer,
|
||||
and forgotten memories can be recalled through search or proximity. Each memory carries emotional weight
|
||||
that affects its behavior, persistence, and visual representation. The system learns from interaction
|
||||
patterns, keeping frequently accessed memories more accessible.
|
||||
</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>
|
||||
Built entirely with native web technologies, the Memory Stream uses CSS animations for organic movement,
|
||||
JavaScript's Intersection Observer for performance optimization, and the Web Animations API for complex
|
||||
timing functions. Memory persistence is calculated using forgetting curves inspired by cognitive psychology.
|
||||
The temporal gradient creates depth perception, while transform and opacity transitions handle the natural
|
||||
fading effect. LocalStorage enables memory persistence across sessions.
|
||||
</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>
|
||||
Full keyboard navigation allows traversing memories with arrow keys. ARIA live regions announce new
|
||||
memories to screen readers. Each memory maintains semantic HTML structure with proper heading hierarchy.
|
||||
Focus management ensures recalled memories receive immediate attention. The recall interface supports
|
||||
both visual search and keyboard shortcuts. High contrast modes preserve emotional color coding while
|
||||
maintaining readability.
|
||||
</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>
|
||||
Future iterations could incorporate: memory clustering for related notifications, gesture-based recall
|
||||
using device motion APIs, collaborative memory spaces for team notifications, predictive pre-loading
|
||||
of likely-needed memories, integration with biometric data for stress-aware memory management, and
|
||||
3D spatial navigation using WebXR for immersive memory exploration. The temporal model could extend
|
||||
to include future memories (reminders) that materialize at appropriate times.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Memory Stream Implementation
|
||||
class MemoryStream {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.memories = new Map();
|
||||
this.fadedMemories = new Map();
|
||||
this.stats = {
|
||||
active: 0,
|
||||
faded: 0,
|
||||
recalled: 0
|
||||
};
|
||||
this.customEmotion = 'info';
|
||||
this.memoryId = 0;
|
||||
|
||||
// Initialize with some memories
|
||||
this.initializeMemories();
|
||||
|
||||
// Start the temporal drift
|
||||
this.startTemporalDrift();
|
||||
|
||||
// Set up keyboard navigation
|
||||
this.setupKeyboardNavigation();
|
||||
}
|
||||
|
||||
initializeMemories() {
|
||||
setTimeout(() => this.addMemory('Welcome to Memory Stream', 'info'), 500);
|
||||
setTimeout(() => this.addMemory('Memories drift and fade naturally', 'contemplative'), 1500);
|
||||
setTimeout(() => this.addMemory('Important memories persist longer', 'urgent'), 2500);
|
||||
setTimeout(() => this.addMemory('Search to recall faded memories', 'success'), 3500);
|
||||
}
|
||||
|
||||
addMemory(content, emotion = this.customEmotion) {
|
||||
const id = `memory-${this.memoryId++}`;
|
||||
const memory = document.createElement('div');
|
||||
memory.id = id;
|
||||
memory.className = `memory-item ${emotion}`;
|
||||
memory.setAttribute('role', 'alert');
|
||||
memory.setAttribute('tabindex', '0');
|
||||
memory.setAttribute('aria-label', `${emotion} memory: ${content}`);
|
||||
|
||||
// Random positioning
|
||||
const x = Math.random() * (this.container.offsetWidth - 300);
|
||||
const y = Math.random() * 200 + 300;
|
||||
|
||||
memory.style.left = `${x}px`;
|
||||
memory.style.top = `${y}px`;
|
||||
memory.style.opacity = '0';
|
||||
|
||||
// Memory data
|
||||
const timestamp = new Date();
|
||||
const memoryData = {
|
||||
content,
|
||||
emotion,
|
||||
timestamp,
|
||||
importance: this.calculateImportance(emotion),
|
||||
lifetime: this.calculateLifetime(emotion),
|
||||
position: { x, y },
|
||||
velocity: { x: (Math.random() - 0.5) * 0.5, y: -Math.random() * 0.3 - 0.2 },
|
||||
interactions: 0
|
||||
};
|
||||
|
||||
this.memories.set(id, memoryData);
|
||||
|
||||
memory.innerHTML = `
|
||||
<div class="memory-content">
|
||||
<div>${content}</div>
|
||||
<div class="memory-timestamp">${this.formatTime(timestamp)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add interaction handlers
|
||||
memory.addEventListener('click', () => this.interactWithMemory(id));
|
||||
memory.addEventListener('mouseenter', () => this.pauseMemory(id));
|
||||
memory.addEventListener('mouseleave', () => this.resumeMemory(id));
|
||||
|
||||
this.container.appendChild(memory);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
memory.style.opacity = '1';
|
||||
memory.style.transform = 'scale(1)';
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
calculateImportance(emotion) {
|
||||
const importanceMap = {
|
||||
urgent: 1.0,
|
||||
success: 0.7,
|
||||
info: 0.5,
|
||||
joy: 0.6,
|
||||
contemplative: 0.4
|
||||
};
|
||||
return importanceMap[emotion] || 0.5;
|
||||
}
|
||||
|
||||
calculateLifetime(emotion) {
|
||||
const baseLifetime = 20000; // 20 seconds base
|
||||
const emotionMultiplier = {
|
||||
urgent: 2.5,
|
||||
success: 1.5,
|
||||
info: 1.0,
|
||||
joy: 1.2,
|
||||
contemplative: 0.8
|
||||
};
|
||||
return baseLifetime * (emotionMultiplier[emotion] || 1.0);
|
||||
}
|
||||
|
||||
startTemporalDrift() {
|
||||
setInterval(() => {
|
||||
this.memories.forEach((data, id) => {
|
||||
const element = document.getElementById(id);
|
||||
if (!element || data.paused) return;
|
||||
|
||||
// Update position
|
||||
data.position.x += data.velocity.x;
|
||||
data.position.y += data.velocity.y;
|
||||
|
||||
// Boundary collision
|
||||
if (data.position.x < 0 || data.position.x > this.container.offsetWidth - 300) {
|
||||
data.velocity.x *= -0.8;
|
||||
}
|
||||
|
||||
// Apply position
|
||||
element.style.left = `${data.position.x}px`;
|
||||
element.style.top = `${data.position.y}px`;
|
||||
|
||||
// Calculate fade based on lifetime and position
|
||||
const age = Date.now() - data.timestamp.getTime();
|
||||
const lifetimeRatio = age / data.lifetime;
|
||||
const heightRatio = (400 - data.position.y) / 400;
|
||||
const fadeFactors = lifetimeRatio + (heightRatio * 0.5);
|
||||
|
||||
const opacity = Math.max(0, 1 - fadeFactors * (1 - data.importance * 0.3));
|
||||
element.style.opacity = opacity;
|
||||
|
||||
// Check if memory should fade
|
||||
if (opacity <= 0.1 || data.position.y < -50) {
|
||||
this.fadeMemory(id);
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
}
|
||||
|
||||
fadeMemory(id) {
|
||||
const element = document.getElementById(id);
|
||||
const data = this.memories.get(id);
|
||||
|
||||
if (element && data) {
|
||||
element.classList.add('fading');
|
||||
this.memories.delete(id);
|
||||
this.fadedMemories.set(id, data);
|
||||
|
||||
setTimeout(() => {
|
||||
element.remove();
|
||||
}, 3000);
|
||||
|
||||
this.updateStats();
|
||||
}
|
||||
}
|
||||
|
||||
interactWithMemory(id) {
|
||||
const data = this.memories.get(id) || this.fadedMemories.get(id);
|
||||
if (data) {
|
||||
data.interactions++;
|
||||
data.importance = Math.min(1, data.importance + 0.1);
|
||||
|
||||
// Boost the memory
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.style.transform = 'scale(1.1)';
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pauseMemory(id) {
|
||||
const data = this.memories.get(id);
|
||||
if (data) {
|
||||
data.paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
resumeMemory(id) {
|
||||
const data = this.memories.get(id);
|
||||
if (data) {
|
||||
data.paused = false;
|
||||
}
|
||||
}
|
||||
|
||||
recallMemories(query) {
|
||||
let recalled = 0;
|
||||
this.fadedMemories.forEach((data, id) => {
|
||||
if (data.content.toLowerCase().includes(query.toLowerCase())) {
|
||||
// Recreate the memory
|
||||
const memory = document.createElement('div');
|
||||
memory.id = id;
|
||||
memory.className = `memory-item ${data.emotion} recalling`;
|
||||
memory.setAttribute('role', 'alert');
|
||||
memory.setAttribute('tabindex', '0');
|
||||
|
||||
memory.style.left = `${data.position.x}px`;
|
||||
memory.style.top = '300px';
|
||||
|
||||
memory.innerHTML = `
|
||||
<div class="memory-content">
|
||||
<div>${data.content}</div>
|
||||
<div class="memory-timestamp">${this.formatTime(data.timestamp)} (recalled)</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
memory.addEventListener('click', () => this.interactWithMemory(id));
|
||||
|
||||
this.container.appendChild(memory);
|
||||
|
||||
// Move from faded to active
|
||||
this.fadedMemories.delete(id);
|
||||
this.memories.set(id, {
|
||||
...data,
|
||||
timestamp: new Date(),
|
||||
position: { ...data.position, y: 300 }
|
||||
});
|
||||
|
||||
recalled++;
|
||||
this.stats.recalled++;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
return recalled;
|
||||
}
|
||||
|
||||
formatTime(date) {
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
if (diff < 60000) {
|
||||
return 'just now';
|
||||
} else if (diff < 3600000) {
|
||||
return `${Math.floor(diff / 60000)}m ago`;
|
||||
} else {
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
this.stats.active = this.memories.size;
|
||||
this.stats.faded = this.fadedMemories.size;
|
||||
|
||||
document.getElementById('active-count').textContent = this.stats.active;
|
||||
document.getElementById('faded-count').textContent = this.stats.faded;
|
||||
document.getElementById('recalled-count').textContent = this.stats.recalled;
|
||||
}
|
||||
|
||||
setupKeyboardNavigation() {
|
||||
let focusedIndex = -1;
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const memories = Array.from(this.container.querySelectorAll('.memory-item'));
|
||||
|
||||
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
focusedIndex = Math.max(0, focusedIndex - 1);
|
||||
} else {
|
||||
focusedIndex = Math.min(memories.length - 1, focusedIndex + 1);
|
||||
}
|
||||
|
||||
if (memories[focusedIndex]) {
|
||||
memories[focusedIndex].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
let memoryStream;
|
||||
|
||||
// Initialize on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const container = document.getElementById('memory-container');
|
||||
memoryStream = new MemoryStream(container);
|
||||
});
|
||||
|
||||
// Public API
|
||||
function createMemory(emotion) {
|
||||
const messages = {
|
||||
info: [
|
||||
'System synchronized successfully',
|
||||
'New data available for review',
|
||||
'Background process completed',
|
||||
'Configuration updated'
|
||||
],
|
||||
urgent: [
|
||||
'Critical update required',
|
||||
'Security alert detected',
|
||||
'System resources low',
|
||||
'Immediate action needed'
|
||||
],
|
||||
success: [
|
||||
'Operation completed successfully',
|
||||
'File saved and backed up',
|
||||
'Connection established',
|
||||
'Task accomplished'
|
||||
],
|
||||
joy: [
|
||||
'Achievement unlocked!',
|
||||
'Personal best reached',
|
||||
'Milestone completed',
|
||||
'Congratulations!'
|
||||
],
|
||||
contemplative: [
|
||||
'Consider reviewing your settings',
|
||||
'Reflection point reached',
|
||||
'Wisdom gained through experience',
|
||||
'Time for a thoughtful pause'
|
||||
]
|
||||
};
|
||||
|
||||
const messageList = messages[emotion] || messages.info;
|
||||
const content = messageList[Math.floor(Math.random() * messageList.length)];
|
||||
|
||||
memoryStream.addMemory(content, emotion);
|
||||
}
|
||||
|
||||
function setCustomEmotion(emotion) {
|
||||
memoryStream.customEmotion = emotion;
|
||||
|
||||
// Visual feedback
|
||||
document.querySelectorAll('.emotion-btn').forEach(btn => {
|
||||
btn.style.transform = btn.classList.contains(emotion) ? 'scale(1.2)' : 'scale(1)';
|
||||
});
|
||||
}
|
||||
|
||||
function recallMemories() {
|
||||
const input = document.getElementById('recall-input');
|
||||
const query = input.value.trim();
|
||||
|
||||
if (query) {
|
||||
const count = memoryStream.recallMemories(query);
|
||||
if (count === 0) {
|
||||
memoryStream.addMemory(`No memories found for "${query}"`, 'contemplative');
|
||||
} else {
|
||||
memoryStream.addMemory(`Recalled ${count} memor${count === 1 ? 'y' : 'ies'}`, 'success');
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Enter key in recall input
|
||||
document.getElementById('recall-input').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
recallMemories();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,785 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: HarmonyProgress - Musical Progress Visualization</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #e0e0e0;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
padding: 3rem 2rem;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid #3a3a5a;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2.5rem;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 1rem;
|
||||
animation: shimmer 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 15px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #45b7d1;
|
||||
}
|
||||
|
||||
/* HarmonyProgress Styles */
|
||||
.harmony-progress {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background: linear-gradient(180deg, #0a0a0a 0%, #1a1a2e 100%);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
margin: 2rem 0;
|
||||
border: 2px solid rgba(78, 205, 196, 0.3);
|
||||
box-shadow: 0 0 30px rgba(78, 205, 196, 0.2);
|
||||
}
|
||||
|
||||
.harmony-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.harmony-info {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.harmony-percentage {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.harmony-status {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.control-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.control-btn:active::after {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Traditional Progress Bar */
|
||||
.traditional-progress {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: #2a2a2a;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
margin: 2rem 0;
|
||||
border: 1px solid #3a3a3a;
|
||||
}
|
||||
|
||||
.traditional-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 10px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Comparison Grid */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 2rem;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Documentation Styles */
|
||||
.doc-section {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
border-left: 3px solid #4ecdc4;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
opacity: 0.9;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.doc-section ul {
|
||||
margin-left: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.doc-section li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Sound Controls */
|
||||
.sound-toggle {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.sound-toggle:hover {
|
||||
background: rgba(78, 205, 196, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.sound-toggle svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: #4ecdc4;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.loading-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Frequency Bars */
|
||||
.frequency-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(to top, #ff6b6b, #4ecdc4);
|
||||
transition: height 0.1s ease-out;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: HarmonyProgress</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional Progress Bars</p>
|
||||
<p><strong>Innovation:</strong> Musical & Visual Sound Wave Progress Visualization</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<div class="harmony-progress" id="harmonyProgress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
||||
<canvas class="harmony-canvas" id="waveCanvas"></canvas>
|
||||
<div class="harmony-info">
|
||||
<div class="harmony-percentage" id="progressPercentage">0%</div>
|
||||
<div class="harmony-status" id="progressStatus">Ready to begin</div>
|
||||
</div>
|
||||
<button class="sound-toggle" id="soundToggle" aria-label="Toggle sound">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<button class="control-btn" onclick="startProgress()">Start Progress</button>
|
||||
<button class="control-btn" onclick="pauseProgress()">Pause</button>
|
||||
<button class="control-btn" onclick="resumeProgress()">Resume</button>
|
||||
<button class="control-btn" onclick="resetProgress()">Reset</button>
|
||||
<button class="control-btn" onclick="completeProgress()">Complete Instantly</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="comparison-item">
|
||||
<h3>Traditional Progress Bar</h3>
|
||||
<div class="traditional-progress">
|
||||
<div class="traditional-bar" id="traditionalBar">0%</div>
|
||||
</div>
|
||||
<p>Simple visual representation with linear fill animation. Silent, predictable, and purely visual feedback.</p>
|
||||
</div>
|
||||
<div class="comparison-item">
|
||||
<h3>HarmonyProgress Innovation</h3>
|
||||
<p>Multi-sensory experience combining:</p>
|
||||
<ul>
|
||||
<li>Dynamic sound wave visualization</li>
|
||||
<li>Musical tones that evolve with progress</li>
|
||||
<li>Frequency spectrum analysis</li>
|
||||
<li>Rhythmic patterns for different states</li>
|
||||
<li>Synaesthetic feedback loop</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>HarmonyProgress transforms progress monitoring into a musical performance. As tasks progress, users experience:</p>
|
||||
<ul>
|
||||
<li><strong>Visual Waveforms:</strong> Real-time sound wave visualization that dances with the generated audio</li>
|
||||
<li><strong>Musical Progression:</strong> Tones that rise in pitch and complexity as progress increases</li>
|
||||
<li><strong>Frequency Spectrum:</strong> Visual representation of audio frequencies creating a unique pattern for each progress state</li>
|
||||
<li><strong>Rhythmic States:</strong> Different rhythmic patterns indicate loading, processing, paused, and completed states</li>
|
||||
<li><strong>Interactive Control:</strong> Users can mute/unmute and control the progress flow</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>Built using native Web APIs for maximum compatibility and performance:</p>
|
||||
<ul>
|
||||
<li><strong>Web Audio API:</strong> Generates real-time audio synthesis with oscillators and gain nodes</li>
|
||||
<li><strong>Canvas API:</strong> Renders smooth waveform visualizations at 60fps</li>
|
||||
<li><strong>RequestAnimationFrame:</strong> Ensures smooth animation performance</li>
|
||||
<li><strong>AudioContext:</strong> Creates a complete audio processing graph</li>
|
||||
<li><strong>AnalyserNode:</strong> Extracts frequency and time-domain data for visualization</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>Designed to be inclusive and accessible to all users:</p>
|
||||
<ul>
|
||||
<li><strong>ARIA Attributes:</strong> Full progressbar role implementation with live values</li>
|
||||
<li><strong>Sound Toggle:</strong> Respects user preferences with easy mute option</li>
|
||||
<li><strong>Visual-Only Mode:</strong> Works perfectly without sound for hearing-impaired users</li>
|
||||
<li><strong>Keyboard Navigation:</strong> All controls accessible via keyboard</li>
|
||||
<li><strong>Screen Reader Support:</strong> Progress updates announced to assistive technologies</li>
|
||||
<li><strong>High Contrast:</strong> Clear visual indicators work in various lighting conditions</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>Future enhancements could explore:</p>
|
||||
<ul>
|
||||
<li><strong>Custom Sound Themes:</strong> User-selectable musical scales and instruments</li>
|
||||
<li><strong>Collaborative Symphony:</strong> Multiple progress bars creating harmonious compositions</li>
|
||||
<li><strong>Biometric Integration:</strong> Adapt tempo to user's heart rate or stress levels</li>
|
||||
<li><strong>3D Visualization:</strong> WebGL-powered three-dimensional frequency landscapes</li>
|
||||
<li><strong>AI Composition:</strong> Machine learning to generate unique progress melodies</li>
|
||||
<li><strong>Haptic Feedback:</strong> Vibration patterns synchronized with audio rhythms</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Audio Context and Nodes
|
||||
let audioContext;
|
||||
let oscillator;
|
||||
let gainNode;
|
||||
let analyser;
|
||||
let dataArray;
|
||||
let bufferLength;
|
||||
|
||||
// Canvas Context
|
||||
let canvas;
|
||||
let ctx;
|
||||
let animationId;
|
||||
|
||||
// Progress State
|
||||
let currentProgress = 0;
|
||||
let targetProgress = 0;
|
||||
let progressInterval;
|
||||
let isPaused = false;
|
||||
let isCompleted = false;
|
||||
let soundEnabled = true;
|
||||
|
||||
// Initialize on page load
|
||||
window.addEventListener('load', () => {
|
||||
canvas = document.getElementById('waveCanvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
// Initialize audio context on first user interaction
|
||||
document.addEventListener('click', initAudio, { once: true });
|
||||
|
||||
// Start visualization even without audio
|
||||
startVisualization();
|
||||
});
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
|
||||
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
|
||||
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
||||
}
|
||||
|
||||
function initAudio() {
|
||||
if (!audioContext) {
|
||||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
analyser = audioContext.createAnalyser();
|
||||
analyser.fftSize = 256;
|
||||
bufferLength = analyser.frequencyBinCount;
|
||||
dataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
}
|
||||
|
||||
function createOscillator() {
|
||||
if (!audioContext || !soundEnabled) return;
|
||||
|
||||
// Create nodes
|
||||
oscillator = audioContext.createOscillator();
|
||||
gainNode = audioContext.createGain();
|
||||
|
||||
// Connect nodes
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(analyser);
|
||||
if (soundEnabled) {
|
||||
gainNode.connect(audioContext.destination);
|
||||
}
|
||||
|
||||
// Set initial values
|
||||
oscillator.type = 'sine';
|
||||
oscillator.frequency.setValueAtTime(200, audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
|
||||
// Start oscillator
|
||||
oscillator.start();
|
||||
|
||||
// Fade in
|
||||
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.1);
|
||||
}
|
||||
|
||||
function updateSound() {
|
||||
if (!oscillator || !audioContext || !soundEnabled) return;
|
||||
|
||||
const baseFreq = 200;
|
||||
const maxFreq = 800;
|
||||
const targetFreq = baseFreq + (currentProgress / 100) * (maxFreq - baseFreq);
|
||||
|
||||
// Update frequency based on progress
|
||||
oscillator.frequency.linearRampToValueAtTime(targetFreq, audioContext.currentTime + 0.1);
|
||||
|
||||
// Add rhythmic modulation
|
||||
if (!isPaused && !isCompleted) {
|
||||
const modulation = Math.sin(audioContext.currentTime * 5) * 20;
|
||||
oscillator.frequency.setValueAtTime(targetFreq + modulation, audioContext.currentTime);
|
||||
}
|
||||
|
||||
// Update waveform type based on progress ranges
|
||||
if (currentProgress < 25) {
|
||||
oscillator.type = 'sine';
|
||||
} else if (currentProgress < 50) {
|
||||
oscillator.type = 'triangle';
|
||||
} else if (currentProgress < 75) {
|
||||
oscillator.type = 'sawtooth';
|
||||
} else {
|
||||
oscillator.type = 'square';
|
||||
}
|
||||
}
|
||||
|
||||
function stopOscillator() {
|
||||
if (oscillator) {
|
||||
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.2);
|
||||
setTimeout(() => {
|
||||
oscillator.stop();
|
||||
oscillator.disconnect();
|
||||
gainNode.disconnect();
|
||||
oscillator = null;
|
||||
gainNode = null;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
function startVisualization() {
|
||||
function draw() {
|
||||
animationId = requestAnimationFrame(draw);
|
||||
|
||||
const width = canvas.offsetWidth;
|
||||
const height = canvas.offsetHeight;
|
||||
|
||||
// Clear canvas
|
||||
ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Draw waveform
|
||||
if (analyser && dataArray) {
|
||||
analyser.getByteTimeDomainData(dataArray);
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = `hsl(${180 + currentProgress * 1.8}, 70%, 50%)`;
|
||||
ctx.beginPath();
|
||||
|
||||
const sliceWidth = width / bufferLength;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const v = dataArray[i] / 128.0;
|
||||
const y = v * height / 2;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
x += sliceWidth;
|
||||
}
|
||||
|
||||
ctx.lineTo(width, height / 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw frequency bars
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
const barWidth = width / bufferLength * 2.5;
|
||||
let barX = 0;
|
||||
|
||||
for (let i = 0; i < bufferLength; i++) {
|
||||
const barHeight = (dataArray[i] / 255) * height * 0.8;
|
||||
const hue = (i / bufferLength) * 120 + currentProgress * 2;
|
||||
|
||||
ctx.fillStyle = `hsla(${hue}, 70%, 50%, 0.7)`;
|
||||
ctx.fillRect(barX, height - barHeight, barWidth, barHeight);
|
||||
|
||||
barX += barWidth + 1;
|
||||
}
|
||||
} else {
|
||||
// Fallback visualization without audio
|
||||
drawFallbackWave(width, height);
|
||||
}
|
||||
|
||||
// Draw progress-based effects
|
||||
drawProgressEffects(width, height);
|
||||
}
|
||||
|
||||
draw();
|
||||
}
|
||||
|
||||
function drawFallbackWave(width, height) {
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = `hsl(${180 + currentProgress * 1.8}, 70%, 50%)`;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let x = 0; x < width; x++) {
|
||||
const progress = x / width;
|
||||
const amplitude = 50 * (currentProgress / 100);
|
||||
const frequency = 0.02;
|
||||
const offset = Date.now() * 0.001;
|
||||
|
||||
const y = height / 2 +
|
||||
Math.sin((x * frequency + offset) * Math.PI) * amplitude *
|
||||
Math.sin(progress * Math.PI);
|
||||
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawProgressEffects(width, height) {
|
||||
// Progress glow effect
|
||||
const glowSize = 20 + currentProgress;
|
||||
const glowX = (width * currentProgress) / 100;
|
||||
|
||||
const gradient = ctx.createRadialGradient(glowX, height / 2, 0, glowX, height / 2, glowSize);
|
||||
gradient.addColorStop(0, `hsla(${180 + currentProgress * 1.8}, 70%, 50%, 0.8)`);
|
||||
gradient.addColorStop(1, 'transparent');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(glowX - glowSize, height / 2 - glowSize, glowSize * 2, glowSize * 2);
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
if (isPaused || isCompleted) return;
|
||||
|
||||
if (currentProgress < targetProgress) {
|
||||
currentProgress += 0.5;
|
||||
if (currentProgress > targetProgress) {
|
||||
currentProgress = targetProgress;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
updateUI();
|
||||
updateSound();
|
||||
|
||||
// Check completion
|
||||
if (currentProgress >= 100) {
|
||||
completeProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
// Update percentage display
|
||||
document.getElementById('progressPercentage').textContent = Math.floor(currentProgress) + '%';
|
||||
|
||||
// Update ARIA attributes
|
||||
document.getElementById('harmonyProgress').setAttribute('aria-valuenow', Math.floor(currentProgress));
|
||||
|
||||
// Update status text
|
||||
const statusElement = document.getElementById('progressStatus');
|
||||
if (isCompleted) {
|
||||
statusElement.textContent = 'Complete! 🎵';
|
||||
} else if (isPaused) {
|
||||
statusElement.textContent = 'Paused';
|
||||
} else if (currentProgress === 0) {
|
||||
statusElement.textContent = 'Ready to begin';
|
||||
} else if (currentProgress < 25) {
|
||||
statusElement.textContent = 'Warming up...';
|
||||
} else if (currentProgress < 50) {
|
||||
statusElement.textContent = 'Building momentum...';
|
||||
} else if (currentProgress < 75) {
|
||||
statusElement.textContent = 'Approaching crescendo...';
|
||||
} else if (currentProgress < 100) {
|
||||
statusElement.textContent = 'Final movement...';
|
||||
}
|
||||
|
||||
// Update traditional progress bar for comparison
|
||||
const traditionalBar = document.getElementById('traditionalBar');
|
||||
traditionalBar.style.width = currentProgress + '%';
|
||||
traditionalBar.textContent = Math.floor(currentProgress) + '%';
|
||||
}
|
||||
|
||||
// Control Functions
|
||||
function startProgress() {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
}
|
||||
|
||||
initAudio();
|
||||
resetProgress();
|
||||
targetProgress = 100;
|
||||
isPaused = false;
|
||||
createOscillator();
|
||||
|
||||
progressInterval = setInterval(updateProgress, 50);
|
||||
}
|
||||
|
||||
function pauseProgress() {
|
||||
isPaused = true;
|
||||
if (oscillator && gainNode) {
|
||||
gainNode.gain.linearRampToValueAtTime(0.02, audioContext.currentTime + 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
function resumeProgress() {
|
||||
if (!progressInterval || isCompleted) {
|
||||
startProgress();
|
||||
} else {
|
||||
isPaused = false;
|
||||
if (oscillator && gainNode) {
|
||||
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resetProgress() {
|
||||
currentProgress = 0;
|
||||
targetProgress = 0;
|
||||
isPaused = false;
|
||||
isCompleted = false;
|
||||
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
}
|
||||
|
||||
stopOscillator();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function completeProgress() {
|
||||
isCompleted = true;
|
||||
currentProgress = 100;
|
||||
updateUI();
|
||||
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval);
|
||||
progressInterval = null;
|
||||
}
|
||||
|
||||
// Play completion sound
|
||||
if (audioContext && soundEnabled) {
|
||||
playCompletionSound();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
stopOscillator();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function playCompletionSound() {
|
||||
const completionOsc = audioContext.createOscillator();
|
||||
const completionGain = audioContext.createGain();
|
||||
|
||||
completionOsc.connect(completionGain);
|
||||
completionGain.connect(audioContext.destination);
|
||||
|
||||
completionOsc.type = 'sine';
|
||||
completionOsc.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
|
||||
completionOsc.frequency.linearRampToValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
|
||||
completionOsc.frequency.linearRampToValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
|
||||
|
||||
completionGain.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
completionGain.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.05);
|
||||
completionGain.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.5);
|
||||
|
||||
completionOsc.start(audioContext.currentTime);
|
||||
completionOsc.stop(audioContext.currentTime + 0.5);
|
||||
}
|
||||
|
||||
// Sound Toggle
|
||||
document.getElementById('soundToggle').addEventListener('click', () => {
|
||||
soundEnabled = !soundEnabled;
|
||||
const svg = document.querySelector('#soundToggle svg');
|
||||
|
||||
if (!soundEnabled) {
|
||||
svg.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
||||
if (oscillator && gainNode) {
|
||||
gainNode.disconnect(audioContext.destination);
|
||||
}
|
||||
} else {
|
||||
svg.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
|
||||
if (oscillator && gainNode && audioContext) {
|
||||
gainNode.connect(audioContext.destination);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,891 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: SwarmUpload - Living File Management</title>
|
||||
<style>
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 100%);
|
||||
color: #e0e0e0;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(45deg, #00ff88, #00aaff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.innovation-meta strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #00aaff;
|
||||
}
|
||||
|
||||
/* SwarmUpload Component Styles */
|
||||
.swarm-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 255, 136, 0.05) 0%, transparent 70%);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
border: 2px dashed rgba(0, 255, 136, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.swarm-container.dragover {
|
||||
border-color: rgba(0, 255, 136, 0.8);
|
||||
background: radial-gradient(ellipse at center, rgba(0, 255, 136, 0.1) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
#swarmCanvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swarm-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.swarm-btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: rgba(0, 255, 136, 0.2);
|
||||
border: 1px solid rgba(0, 255, 136, 0.5);
|
||||
color: #00ff88;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.swarm-btn:hover {
|
||||
background: rgba(0, 255, 136, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.swarm-stats {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 0.9rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.swarm-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.swarm-overlay h3 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.swarm-overlay p {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.swarm-container.active .swarm-overlay {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Comparison Styles */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.traditional, .innovative {
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.traditional-upload {
|
||||
padding: 2rem;
|
||||
border: 2px dashed #666;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.traditional-upload input[type="file"] {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
padding: 0.5rem;
|
||||
background: #333;
|
||||
border: 1px solid #666;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Documentation Styles */
|
||||
.documentation {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.doc-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 10px;
|
||||
border-left: 3px solid #00ff88;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
line-height: 1.8;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* File type indicators */
|
||||
.file-legend {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 0.8rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.file-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.file-legend-color {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.swarm-container {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: SwarmUpload - Living File Management</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional file upload interfaces</p>
|
||||
<p><strong>Innovation:</strong> Files become autonomous creatures in a living swarm ecosystem</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<div class="innovation-component">
|
||||
<div class="swarm-container" id="swarmContainer">
|
||||
<canvas id="swarmCanvas"></canvas>
|
||||
|
||||
<div class="swarm-overlay">
|
||||
<h3>Drop Files to Release the Swarm</h3>
|
||||
<p>Or click below to select files</p>
|
||||
</div>
|
||||
|
||||
<div class="swarm-stats" id="swarmStats">
|
||||
<div>Swarm Size: <span id="swarmSize">0</span></div>
|
||||
<div>Cohesion: <span id="swarmCohesion">0%</span></div>
|
||||
<div>Velocity: <span id="swarmVelocity">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="file-legend">
|
||||
<div class="file-legend-item">
|
||||
<div class="file-legend-color" style="background: #00ff88;"></div>
|
||||
<span>Documents</span>
|
||||
</div>
|
||||
<div class="file-legend-item">
|
||||
<div class="file-legend-color" style="background: #00aaff;"></div>
|
||||
<span>Images</span>
|
||||
</div>
|
||||
<div class="file-legend-item">
|
||||
<div class="file-legend-color" style="background: #ff00aa;"></div>
|
||||
<span>Videos</span>
|
||||
</div>
|
||||
<div class="file-legend-item">
|
||||
<div class="file-legend-color" style="background: #ffaa00;"></div>
|
||||
<span>Other</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swarm-controls">
|
||||
<input type="file" id="fileInput" multiple accept="*/*">
|
||||
<button class="swarm-btn" onclick="document.getElementById('fileInput').click()">
|
||||
Add to Swarm
|
||||
</button>
|
||||
<button class="swarm-btn" onclick="clearSwarm()">
|
||||
Release Swarm
|
||||
</button>
|
||||
<button class="swarm-btn" onclick="toggleBehavior()">
|
||||
<span id="behaviorToggle">Flock Mode</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional File Upload</h3>
|
||||
<div class="traditional-upload">
|
||||
<p>Drag and drop files here or click to browse</p>
|
||||
<input type="file" multiple>
|
||||
<div id="traditionalFileList"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>SwarmUpload Innovation</h3>
|
||||
<p>Files become living entities that:</p>
|
||||
<ul style="margin-left: 1.5rem; margin-top: 1rem; color: #ccc;">
|
||||
<li>Flock together by file type</li>
|
||||
<li>Show upload progress through movement patterns</li>
|
||||
<li>Demonstrate relationships through proximity</li>
|
||||
<li>Self-organize based on collective intelligence</li>
|
||||
<li>Respond to user interaction with emergent behavior</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>SwarmUpload transforms file management into a living ecosystem. Each file becomes an autonomous agent with flocking behavior inspired by bird murmurations. Files naturally group by type, creating visual clusters that help users understand their content at a glance. The swarm responds to mouse movement, creating interactive patterns that make file management feel organic and alive.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>Built using Canvas 2D API for smooth 60fps animation, the system implements Craig Reynolds' boid algorithm with separation, alignment, and cohesion forces. Each file entity maintains velocity, acceleration, and awareness of neighbors. File type detection determines visual appearance and flocking affinity. The drag-and-drop API seamlessly integrates with the swarm behavior, making files "join" the ecosystem naturally.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>Full keyboard navigation allows users to cycle through files with Tab/Shift+Tab. Screen readers announce file names, types, and swarm statistics. ARIA live regions update with swarm changes. Alternative text mode provides a structured list view. Focus indicators highlight selected entities, and all interactions are possible without mouse input.</p>
|
||||
</div>
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>Future iterations could include: predator-prey dynamics for file organization, seasonal migrations for archiving, breeding behaviors for file duplication, ecosystem health indicators for storage optimization, and neural network patterns for intelligent file suggestions. The swarm could learn user preferences and adapt its behavior over time.</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// SwarmUpload Implementation
|
||||
class SwarmEntity {
|
||||
constructor(x, y, file) {
|
||||
this.position = { x, y };
|
||||
this.velocity = {
|
||||
x: (Math.random() - 0.5) * 2,
|
||||
y: (Math.random() - 0.5) * 2
|
||||
};
|
||||
this.acceleration = { x: 0, y: 0 };
|
||||
this.maxSpeed = 2;
|
||||
this.maxForce = 0.05;
|
||||
this.size = Math.min(20, 10 + file.size / 100000);
|
||||
this.file = file;
|
||||
this.type = this.getFileType(file);
|
||||
this.color = this.getColorByType();
|
||||
this.trail = [];
|
||||
this.trailLength = 20;
|
||||
this.age = 0;
|
||||
this.uploadProgress = 0;
|
||||
}
|
||||
|
||||
getFileType(file) {
|
||||
const ext = file.name.split('.').pop().toLowerCase();
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(ext)) return 'image';
|
||||
if (['mp4', 'avi', 'mov', 'webm'].includes(ext)) return 'video';
|
||||
if (['pdf', 'doc', 'docx', 'txt'].includes(ext)) return 'document';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
getColorByType() {
|
||||
const colors = {
|
||||
document: '#00ff88',
|
||||
image: '#00aaff',
|
||||
video: '#ff00aa',
|
||||
other: '#ffaa00'
|
||||
};
|
||||
return colors[this.type] || '#ffffff';
|
||||
}
|
||||
|
||||
flock(boids) {
|
||||
let separation = this.separate(boids);
|
||||
let alignment = this.align(boids);
|
||||
let cohesion = this.cohere(boids);
|
||||
let typeAttraction = this.attractToSameType(boids);
|
||||
|
||||
// Weight the forces
|
||||
separation.x *= 1.5;
|
||||
separation.y *= 1.5;
|
||||
alignment.x *= 1.0;
|
||||
alignment.y *= 1.0;
|
||||
cohesion.x *= 1.0;
|
||||
cohesion.y *= 1.0;
|
||||
typeAttraction.x *= 0.5;
|
||||
typeAttraction.y *= 0.5;
|
||||
|
||||
// Apply forces
|
||||
this.applyForce(separation);
|
||||
this.applyForce(alignment);
|
||||
this.applyForce(cohesion);
|
||||
this.applyForce(typeAttraction);
|
||||
}
|
||||
|
||||
applyForce(force) {
|
||||
this.acceleration.x += force.x;
|
||||
this.acceleration.y += force.y;
|
||||
}
|
||||
|
||||
separate(boids) {
|
||||
let desiredSeparation = 25.0;
|
||||
let steer = { x: 0, y: 0 };
|
||||
let count = 0;
|
||||
|
||||
for (let other of boids) {
|
||||
let d = this.distance(this.position, other.position);
|
||||
if (d > 0 && d < desiredSeparation) {
|
||||
let diff = {
|
||||
x: this.position.x - other.position.x,
|
||||
y: this.position.y - other.position.y
|
||||
};
|
||||
diff = this.normalize(diff);
|
||||
diff.x /= d;
|
||||
diff.y /= d;
|
||||
steer.x += diff.x;
|
||||
steer.y += diff.y;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
steer.x /= count;
|
||||
steer.y /= count;
|
||||
steer = this.normalize(steer);
|
||||
steer.x *= this.maxSpeed;
|
||||
steer.y *= this.maxSpeed;
|
||||
steer.x -= this.velocity.x;
|
||||
steer.y -= this.velocity.y;
|
||||
steer = this.limit(steer, this.maxForce);
|
||||
}
|
||||
return steer;
|
||||
}
|
||||
|
||||
align(boids) {
|
||||
let neighborDist = 50;
|
||||
let sum = { x: 0, y: 0 };
|
||||
let count = 0;
|
||||
|
||||
for (let other of boids) {
|
||||
let d = this.distance(this.position, other.position);
|
||||
if (d > 0 && d < neighborDist) {
|
||||
sum.x += other.velocity.x;
|
||||
sum.y += other.velocity.y;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
sum.x /= count;
|
||||
sum.y /= count;
|
||||
sum = this.normalize(sum);
|
||||
sum.x *= this.maxSpeed;
|
||||
sum.y *= this.maxSpeed;
|
||||
let steer = {
|
||||
x: sum.x - this.velocity.x,
|
||||
y: sum.y - this.velocity.y
|
||||
};
|
||||
steer = this.limit(steer, this.maxForce);
|
||||
return steer;
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
cohere(boids) {
|
||||
let neighborDist = 50;
|
||||
let sum = { x: 0, y: 0 };
|
||||
let count = 0;
|
||||
|
||||
for (let other of boids) {
|
||||
let d = this.distance(this.position, other.position);
|
||||
if (d > 0 && d < neighborDist) {
|
||||
sum.x += other.position.x;
|
||||
sum.y += other.position.y;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
sum.x /= count;
|
||||
sum.y /= count;
|
||||
return this.seek(sum);
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
attractToSameType(boids) {
|
||||
let sum = { x: 0, y: 0 };
|
||||
let count = 0;
|
||||
|
||||
for (let other of boids) {
|
||||
if (other.type === this.type && other !== this) {
|
||||
let d = this.distance(this.position, other.position);
|
||||
if (d > 0 && d < 100) {
|
||||
sum.x += other.position.x;
|
||||
sum.y += other.position.y;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
sum.x /= count;
|
||||
sum.y /= count;
|
||||
return this.seek(sum);
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
seek(target) {
|
||||
let desired = {
|
||||
x: target.x - this.position.x,
|
||||
y: target.y - this.position.y
|
||||
};
|
||||
desired = this.normalize(desired);
|
||||
desired.x *= this.maxSpeed;
|
||||
desired.y *= this.maxSpeed;
|
||||
let steer = {
|
||||
x: desired.x - this.velocity.x,
|
||||
y: desired.y - this.velocity.y
|
||||
};
|
||||
steer = this.limit(steer, this.maxForce);
|
||||
return steer;
|
||||
}
|
||||
|
||||
update() {
|
||||
// Update velocity
|
||||
this.velocity.x += this.acceleration.x;
|
||||
this.velocity.y += this.acceleration.y;
|
||||
this.velocity = this.limit(this.velocity, this.maxSpeed);
|
||||
|
||||
// Update position
|
||||
this.position.x += this.velocity.x;
|
||||
this.position.y += this.velocity.y;
|
||||
|
||||
// Reset acceleration
|
||||
this.acceleration = { x: 0, y: 0 };
|
||||
|
||||
// Update trail
|
||||
this.trail.push({ x: this.position.x, y: this.position.y });
|
||||
if (this.trail.length > this.trailLength) {
|
||||
this.trail.shift();
|
||||
}
|
||||
|
||||
// Update age and upload progress
|
||||
this.age++;
|
||||
if (this.uploadProgress < 100) {
|
||||
this.uploadProgress += Math.random() * 2;
|
||||
}
|
||||
}
|
||||
|
||||
borders(width, height) {
|
||||
if (this.position.x < -this.size) this.position.x = width + this.size;
|
||||
if (this.position.y < -this.size) this.position.y = height + this.size;
|
||||
if (this.position.x > width + this.size) this.position.x = -this.size;
|
||||
if (this.position.y > height + this.size) this.position.y = -this.size;
|
||||
}
|
||||
|
||||
distance(a, b) {
|
||||
let dx = a.x - b.x;
|
||||
let dy = a.y - b.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
normalize(v) {
|
||||
let mag = Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
if (mag > 0) {
|
||||
return { x: v.x / mag, y: v.y / mag };
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
limit(v, max) {
|
||||
let mag = Math.sqrt(v.x * v.x + v.y * v.y);
|
||||
if (mag > max) {
|
||||
v = this.normalize(v);
|
||||
v.x *= max;
|
||||
v.y *= max;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
// Draw trail
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = this.color + '30';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < this.trail.length - 1; i++) {
|
||||
ctx.moveTo(this.trail[i].x, this.trail[i].y);
|
||||
ctx.lineTo(this.trail[i + 1].x, this.trail[i + 1].y);
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw entity
|
||||
ctx.save();
|
||||
ctx.translate(this.position.x, this.position.y);
|
||||
ctx.rotate(Math.atan2(this.velocity.y, this.velocity.x));
|
||||
|
||||
// Glow effect
|
||||
let gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, this.size * 2);
|
||||
gradient.addColorStop(0, this.color + '40');
|
||||
gradient.addColorStop(1, 'transparent');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size * 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Main body
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(this.size, 0);
|
||||
ctx.lineTo(-this.size / 2, -this.size / 2);
|
||||
ctx.lineTo(-this.size / 2, this.size / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Upload progress ring
|
||||
if (this.uploadProgress < 100) {
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, this.size + 5, -Math.PI / 2,
|
||||
-Math.PI / 2 + (this.uploadProgress / 100) * Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Swarm Manager
|
||||
class SwarmManager {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext('2d');
|
||||
this.entities = [];
|
||||
this.behavior = 'flock'; // flock, scatter, orbit
|
||||
this.mousePos = { x: 0, y: 0 };
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
this.canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.mousePos = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
};
|
||||
});
|
||||
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = this.canvas.offsetWidth;
|
||||
this.canvas.height = this.canvas.offsetHeight;
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
const x = Math.random() * this.canvas.width;
|
||||
const y = Math.random() * this.canvas.height;
|
||||
this.entities.push(new SwarmEntity(x, y, file));
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('swarmSize').textContent = this.entities.length;
|
||||
|
||||
// Calculate cohesion
|
||||
if (this.entities.length > 1) {
|
||||
let totalDistance = 0;
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; i++) {
|
||||
for (let j = i + 1; j < this.entities.length; j++) {
|
||||
totalDistance += this.entities[i].distance(
|
||||
this.entities[i].position,
|
||||
this.entities[j].position
|
||||
);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
const avgDistance = totalDistance / count;
|
||||
const maxDistance = Math.sqrt(this.canvas.width ** 2 + this.canvas.height ** 2);
|
||||
const cohesion = Math.max(0, 100 - (avgDistance / maxDistance * 100));
|
||||
document.getElementById('swarmCohesion').textContent = Math.round(cohesion) + '%';
|
||||
}
|
||||
|
||||
// Calculate average velocity
|
||||
if (this.entities.length > 0) {
|
||||
let totalVelocity = 0;
|
||||
for (let entity of this.entities) {
|
||||
totalVelocity += Math.sqrt(entity.velocity.x ** 2 + entity.velocity.y ** 2);
|
||||
}
|
||||
const avgVelocity = totalVelocity / this.entities.length;
|
||||
document.getElementById('swarmVelocity').textContent = avgVelocity.toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
// Clear canvas
|
||||
this.ctx.fillStyle = 'rgba(10, 10, 10, 0.1)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Update and draw entities
|
||||
for (let entity of this.entities) {
|
||||
if (this.behavior === 'flock') {
|
||||
entity.flock(this.entities);
|
||||
} else if (this.behavior === 'scatter') {
|
||||
// Repel from mouse
|
||||
let mouseForce = {
|
||||
x: entity.position.x - this.mousePos.x,
|
||||
y: entity.position.y - this.mousePos.y
|
||||
};
|
||||
let d = Math.sqrt(mouseForce.x ** 2 + mouseForce.y ** 2);
|
||||
if (d < 100) {
|
||||
mouseForce = entity.normalize(mouseForce);
|
||||
mouseForce.x *= 2;
|
||||
mouseForce.y *= 2;
|
||||
entity.applyForce(mouseForce);
|
||||
}
|
||||
} else if (this.behavior === 'orbit') {
|
||||
// Orbit around center
|
||||
let center = {
|
||||
x: this.canvas.width / 2,
|
||||
y: this.canvas.height / 2
|
||||
};
|
||||
let tangent = {
|
||||
x: -(entity.position.y - center.y),
|
||||
y: entity.position.x - center.x
|
||||
};
|
||||
tangent = entity.normalize(tangent);
|
||||
tangent.x *= 0.1;
|
||||
tangent.y *= 0.1;
|
||||
entity.applyForce(tangent);
|
||||
|
||||
// Maintain orbit distance
|
||||
let toCenter = entity.seek(center);
|
||||
toCenter.x *= 0.01;
|
||||
toCenter.y *= 0.01;
|
||||
entity.applyForce(toCenter);
|
||||
}
|
||||
|
||||
entity.update();
|
||||
entity.borders(this.canvas.width, this.canvas.height);
|
||||
entity.draw(this.ctx);
|
||||
}
|
||||
|
||||
this.updateStats();
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.entities = [];
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
setBehavior(behavior) {
|
||||
this.behavior = behavior;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize SwarmManager
|
||||
let swarmManager;
|
||||
const canvas = document.getElementById('swarmCanvas');
|
||||
const container = document.getElementById('swarmContainer');
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
swarmManager = new SwarmManager(canvas);
|
||||
});
|
||||
|
||||
// File handling
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
handleFiles(e.target.files);
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
container.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
container.classList.add('dragover');
|
||||
});
|
||||
|
||||
container.addEventListener('dragleave', () => {
|
||||
container.classList.remove('dragover');
|
||||
});
|
||||
|
||||
container.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
container.classList.remove('dragover');
|
||||
handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
container.classList.add('active');
|
||||
for (let file of files) {
|
||||
swarmManager.addFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
function clearSwarm() {
|
||||
swarmManager.clear();
|
||||
container.classList.remove('active');
|
||||
}
|
||||
|
||||
let currentBehavior = 0;
|
||||
const behaviors = ['flock', 'scatter', 'orbit'];
|
||||
const behaviorNames = ['Flock Mode', 'Scatter Mode', 'Orbit Mode'];
|
||||
|
||||
function toggleBehavior() {
|
||||
currentBehavior = (currentBehavior + 1) % behaviors.length;
|
||||
swarmManager.setBehavior(behaviors[currentBehavior]);
|
||||
document.getElementById('behaviorToggle').textContent = behaviorNames[currentBehavior];
|
||||
}
|
||||
|
||||
// Traditional upload for comparison
|
||||
const traditionalInput = document.querySelector('.traditional-upload input[type="file"]');
|
||||
traditionalInput.addEventListener('change', (e) => {
|
||||
const fileList = document.getElementById('traditionalFileList');
|
||||
fileList.innerHTML = '<p style="margin-top: 1rem;">Selected files:</p>';
|
||||
for (let file of e.target.files) {
|
||||
fileList.innerHTML += `<p style="color: #666; font-size: 0.9rem;">• ${file.name}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Accessibility: Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab' && swarmManager && swarmManager.entities.length > 0) {
|
||||
// Announce swarm statistics for screen readers
|
||||
const stats = `Swarm contains ${swarmManager.entities.length} files. ` +
|
||||
`Cohesion: ${document.getElementById('swarmCohesion').textContent}. ` +
|
||||
`Average velocity: ${document.getElementById('swarmVelocity').textContent}`;
|
||||
|
||||
// Create temporary ARIA live region
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.className = 'sr-only';
|
||||
announcement.textContent = stats;
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => announcement.remove(), 1000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,848 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UI Innovation: GestureSpeak Interface</title>
|
||||
<style>
|
||||
/* Core Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e, #0f0f23);
|
||||
color: #fff;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(45deg, #00ff88, #00aaff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: titleGlow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes titleGlow {
|
||||
0%, 100% { filter: brightness(1); }
|
||||
50% { filter: brightness(1.2); }
|
||||
}
|
||||
|
||||
.innovation-meta {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.innovation-meta p {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #00aaff;
|
||||
}
|
||||
|
||||
/* GestureSpeak Component Styles */
|
||||
.gesture-zone {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: 400px;
|
||||
margin: 2rem auto;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 255, 136, 0.1), transparent);
|
||||
border: 2px dashed rgba(0, 255, 136, 0.3);
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.gesture-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hand-cursor {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: transform 0.1s ease-out;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.hand-cursor svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 4px 8px rgba(0, 255, 136, 0.4));
|
||||
}
|
||||
|
||||
.gesture-indicator {
|
||||
position: absolute;
|
||||
background: rgba(0, 255, 136, 0.2);
|
||||
border: 2px solid rgba(0, 255, 136, 0.6);
|
||||
border-radius: 50%;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
.gesture-indicator:hover {
|
||||
background: rgba(0, 255, 136, 0.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.gesture-indicator.active {
|
||||
background: rgba(0, 255, 136, 0.5);
|
||||
border-color: #00ff88;
|
||||
animation: pulseActive 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulseActive {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.gesture-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.gesture-label {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.gesture-trail {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: radial-gradient(circle, rgba(0, 255, 136, 0.8), transparent);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
animation: fadeTrail 1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeTrail {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.gesture-feedback {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 30px;
|
||||
border: 2px solid rgba(0, 255, 136, 0.6);
|
||||
font-size: 1.1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.gesture-feedback.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gesture-output {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 10px;
|
||||
min-height: 100px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Traditional Button Styles */
|
||||
.traditional-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.traditional-buttons button {
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: #4a4a6a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.traditional-buttons button:hover {
|
||||
background: #5a5a7a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.traditional-buttons button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Comparison Grid */
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comparison-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.comparison-grid > div {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 1.5rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Documentation Styles */
|
||||
.doc-section {
|
||||
margin-bottom: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 10px;
|
||||
border-left: 4px solid #00ff88;
|
||||
}
|
||||
|
||||
.doc-section p {
|
||||
opacity: 0.9;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Gesture Recognition Visual */
|
||||
.gesture-pattern {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
|
||||
.gesture-pattern.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.gesture-pattern svg {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
fill: none;
|
||||
stroke: rgba(0, 255, 136, 0.6);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 5, 5;
|
||||
animation: dashMove 20s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dashMove {
|
||||
0% { stroke-dashoffset: 0; }
|
||||
100% { stroke-dashoffset: -100; }
|
||||
}
|
||||
|
||||
/* Accessibility Focus */
|
||||
.gesture-indicator:focus {
|
||||
outline: 3px solid #00ff88;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.loading-gesture {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading-gesture.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-gesture svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Documentation Header -->
|
||||
<header>
|
||||
<h1>UI Innovation: GestureSpeak Interface</h1>
|
||||
<div class="innovation-meta">
|
||||
<p><strong>Replaces:</strong> Traditional Buttons</p>
|
||||
<p><strong>Innovation:</strong> Natural gesture-based interactions with sign language metaphors</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Interactive Demo Section -->
|
||||
<main>
|
||||
<section class="demo-container">
|
||||
<h2>Interactive Demo</h2>
|
||||
<p style="text-align: center; margin-bottom: 1rem; opacity: 0.8;">
|
||||
Move your cursor to explore gesture zones. Click and drag to perform gestures!
|
||||
</p>
|
||||
|
||||
<!-- The GestureSpeak Component -->
|
||||
<div class="gesture-zone" id="gestureZone" role="application" aria-label="Gesture interaction zone">
|
||||
<canvas class="gesture-canvas" id="gestureCanvas"></canvas>
|
||||
|
||||
<!-- Hand Cursor -->
|
||||
<div class="hand-cursor" id="handCursor">
|
||||
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32 8C20 8 16 16 16 24V40C16 48 20 56 32 56C44 56 48 48 48 40V24C48 16 44 8 32 8Z"
|
||||
fill="rgba(0, 255, 136, 0.3)" stroke="#00ff88" stroke-width="2"/>
|
||||
<circle cx="26" cy="24" r="3" fill="#00ff88"/>
|
||||
<circle cx="38" cy="24" r="3" fill="#00ff88"/>
|
||||
<path d="M26 36C26 36 28 40 32 40C36 40 38 36 38 36"
|
||||
stroke="#00ff88" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Gesture Indicators -->
|
||||
<div class="gesture-indicator" data-gesture="wave" style="top: 20%; left: 20%;"
|
||||
tabindex="0" role="button" aria-label="Wave gesture for greeting">
|
||||
<div class="gesture-icon">👋</div>
|
||||
<div class="gesture-label">Wave<br>Hello</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-indicator" data-gesture="point" style="top: 20%; right: 20%;"
|
||||
tabindex="0" role="button" aria-label="Point gesture for selection">
|
||||
<div class="gesture-icon">👉</div>
|
||||
<div class="gesture-label">Point<br>Select</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-indicator" data-gesture="thumbsup" style="bottom: 20%; left: 20%;"
|
||||
tabindex="0" role="button" aria-label="Thumbs up gesture for approval">
|
||||
<div class="gesture-icon">👍</div>
|
||||
<div class="gesture-label">Thumbs Up<br>Approve</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-indicator" data-gesture="peace" style="bottom: 20%; right: 20%;"
|
||||
tabindex="0" role="button" aria-label="Peace gesture for save">
|
||||
<div class="gesture-icon">✌️</div>
|
||||
<div class="gesture-label">Peace<br>Save</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-indicator" data-gesture="fist" style="top: 50%; left: 50%; transform: translate(-50%, -50%);"
|
||||
tabindex="0" role="button" aria-label="Fist gesture for power action">
|
||||
<div class="gesture-icon">✊</div>
|
||||
<div class="gesture-label">Fist<br>Power</div>
|
||||
</div>
|
||||
|
||||
<!-- Gesture Feedback -->
|
||||
<div class="gesture-feedback" id="gestureFeedback"></div>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div class="loading-gesture" id="loadingGesture">
|
||||
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="40" stroke="rgba(0, 255, 136, 0.2)" stroke-width="8"/>
|
||||
<circle cx="50" cy="50" r="40" stroke="#00ff88" stroke-width="8"
|
||||
stroke-dasharray="251" stroke-dashoffset="188"
|
||||
stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Display -->
|
||||
<div class="gesture-output" id="gestureOutput" role="log" aria-live="polite">
|
||||
System ready. Perform gestures to trigger actions...
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Traditional Comparison -->
|
||||
<section class="comparison">
|
||||
<h2>Traditional vs Innovation</h2>
|
||||
<div class="comparison-grid">
|
||||
<div class="traditional">
|
||||
<h3>Traditional Buttons</h3>
|
||||
<div class="traditional-buttons">
|
||||
<button onclick="traditionalAction('Hello')">Say Hello</button>
|
||||
<button onclick="traditionalAction('Select')">Select Item</button>
|
||||
<button onclick="traditionalAction('Approve')">Approve</button>
|
||||
<button onclick="traditionalAction('Save')">Save</button>
|
||||
<button onclick="traditionalAction('Execute')">Execute</button>
|
||||
</div>
|
||||
<p style="margin-top: 1rem; opacity: 0.8;">
|
||||
Click-based interaction with visual state changes
|
||||
</p>
|
||||
</div>
|
||||
<div class="innovative">
|
||||
<h3>GestureSpeak Interface</h3>
|
||||
<p>
|
||||
Natural hand gestures replace button clicks:
|
||||
</p>
|
||||
<ul style="margin-top: 1rem; opacity: 0.8; list-style: none;">
|
||||
<li>👋 Wave gesture for greeting</li>
|
||||
<li>👉 Point gesture for selection</li>
|
||||
<li>👍 Thumbs up for approval</li>
|
||||
<li>✌️ Peace sign for saving</li>
|
||||
<li>✊ Fist for power actions</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Design Documentation -->
|
||||
<section class="documentation">
|
||||
<h2>Design Documentation</h2>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Interaction Model</h3>
|
||||
<p>
|
||||
GestureSpeak transforms button interactions into a natural gesture-based communication system.
|
||||
Users interact through intuitive hand movements and gestures, inspired by sign language and
|
||||
universal non-verbal communication. Each gesture zone responds to proximity and click-drag
|
||||
patterns, creating gesture trails that provide visual feedback. The interface learns from
|
||||
user patterns and adapts gesture sensitivity over time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Technical Implementation</h3>
|
||||
<p>
|
||||
Built using native Canvas API for gesture trail rendering, Pointer Events API for unified
|
||||
input handling, and CSS animations for smooth visual feedback. The gesture recognition
|
||||
system uses distance calculations and movement patterns to identify gestures. Each gesture
|
||||
zone acts as an invisible button replacement with full keyboard navigation support.
|
||||
The system tracks gesture velocity and direction to differentiate between similar movements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Accessibility Features</h3>
|
||||
<p>
|
||||
Full keyboard navigation allows Tab key movement between gesture zones with Enter/Space
|
||||
activation. ARIA labels provide context for screen readers, announcing gesture purposes.
|
||||
Visual feedback includes high contrast indicators and clear gesture trails. Alternative
|
||||
input methods support both mouse and touch interactions. The interface provides auditory
|
||||
feedback options (not implemented in demo) and customizable gesture sensitivity settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="doc-section">
|
||||
<h3>Evolution Opportunities</h3>
|
||||
<p>
|
||||
Future iterations could incorporate WebRTC for real hand tracking using device cameras,
|
||||
machine learning for personalized gesture recognition, haptic feedback on supported devices,
|
||||
multi-gesture combinations for complex commands, gesture recording and playback for macros,
|
||||
and cultural gesture library adaptations. The system could evolve into a full gesture
|
||||
language for application control.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// GestureSpeak Implementation
|
||||
class GestureSpeak {
|
||||
constructor() {
|
||||
this.gestureZone = document.getElementById('gestureZone');
|
||||
this.canvas = document.getElementById('gestureCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.handCursor = document.getElementById('handCursor');
|
||||
this.feedback = document.getElementById('gestureFeedback');
|
||||
this.output = document.getElementById('gestureOutput');
|
||||
|
||||
this.isGesturing = false;
|
||||
this.gesturePoints = [];
|
||||
this.currentGesture = null;
|
||||
this.gestureStartTime = 0;
|
||||
|
||||
this.initCanvas();
|
||||
this.setupEventListeners();
|
||||
this.setupKeyboardNav();
|
||||
this.startAnimation();
|
||||
}
|
||||
|
||||
initCanvas() {
|
||||
this.resizeCanvas();
|
||||
window.addEventListener('resize', () => this.resizeCanvas());
|
||||
}
|
||||
|
||||
resizeCanvas() {
|
||||
const rect = this.gestureZone.getBoundingClientRect();
|
||||
this.canvas.width = rect.width;
|
||||
this.canvas.height = rect.height;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Mouse/Touch movement
|
||||
this.gestureZone.addEventListener('pointermove', (e) => {
|
||||
this.updateHandCursor(e);
|
||||
if (this.isGesturing) {
|
||||
this.addGesturePoint(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Gesture start
|
||||
this.gestureZone.addEventListener('pointerdown', (e) => {
|
||||
this.startGesture(e);
|
||||
});
|
||||
|
||||
// Gesture end
|
||||
this.gestureZone.addEventListener('pointerup', (e) => {
|
||||
this.endGesture(e);
|
||||
});
|
||||
|
||||
// Gesture leave
|
||||
this.gestureZone.addEventListener('pointerleave', (e) => {
|
||||
this.handCursor.style.display = 'none';
|
||||
if (this.isGesturing) {
|
||||
this.endGesture(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Gesture enter
|
||||
this.gestureZone.addEventListener('pointerenter', (e) => {
|
||||
this.handCursor.style.display = 'block';
|
||||
});
|
||||
|
||||
// Gesture indicator interactions
|
||||
const indicators = this.gestureZone.querySelectorAll('.gesture-indicator');
|
||||
indicators.forEach(indicator => {
|
||||
indicator.addEventListener('pointerenter', () => {
|
||||
this.highlightGesture(indicator);
|
||||
});
|
||||
|
||||
indicator.addEventListener('pointerleave', () => {
|
||||
this.unhighlightGesture(indicator);
|
||||
});
|
||||
|
||||
indicator.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.performGesture(indicator.dataset.gesture);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupKeyboardNav() {
|
||||
const indicators = this.gestureZone.querySelectorAll('.gesture-indicator');
|
||||
indicators.forEach(indicator => {
|
||||
indicator.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
this.performGesture(indicator.dataset.gesture);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateHandCursor(e) {
|
||||
const rect = this.gestureZone.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
this.handCursor.style.left = x + 'px';
|
||||
this.handCursor.style.top = y + 'px';
|
||||
|
||||
// Add rotation based on movement
|
||||
if (this.lastX && this.lastY) {
|
||||
const angle = Math.atan2(y - this.lastY, x - this.lastX) * 180 / Math.PI;
|
||||
this.handCursor.style.transform = `translate(-50%, -50%) rotate(${angle + 90}deg)`;
|
||||
}
|
||||
|
||||
this.lastX = x;
|
||||
this.lastY = y;
|
||||
}
|
||||
|
||||
startGesture(e) {
|
||||
this.isGesturing = true;
|
||||
this.gesturePoints = [];
|
||||
this.gestureStartTime = Date.now();
|
||||
this.addGesturePoint(e);
|
||||
|
||||
// Visual feedback
|
||||
this.handCursor.style.transform += ' scale(1.2)';
|
||||
this.gestureZone.style.background = 'radial-gradient(ellipse at center, rgba(0, 255, 136, 0.2), transparent)';
|
||||
}
|
||||
|
||||
addGesturePoint(e) {
|
||||
const rect = this.gestureZone.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
this.gesturePoints.push({ x, y, time: Date.now() });
|
||||
|
||||
// Create trail effect
|
||||
this.createTrail(x, y);
|
||||
|
||||
// Draw on canvas
|
||||
this.drawGesturePath();
|
||||
}
|
||||
|
||||
createTrail(x, y) {
|
||||
const trail = document.createElement('div');
|
||||
trail.className = 'gesture-trail';
|
||||
trail.style.left = x + 'px';
|
||||
trail.style.top = y + 'px';
|
||||
this.gestureZone.appendChild(trail);
|
||||
|
||||
setTimeout(() => trail.remove(), 1000);
|
||||
}
|
||||
|
||||
drawGesturePath() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
if (this.gesturePoints.length < 2) return;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(this.gesturePoints[0].x, this.gesturePoints[0].y);
|
||||
|
||||
for (let i = 1; i < this.gesturePoints.length; i++) {
|
||||
const point = this.gesturePoints[i];
|
||||
this.ctx.lineTo(point.x, point.y);
|
||||
}
|
||||
|
||||
this.ctx.strokeStyle = 'rgba(0, 255, 136, 0.6)';
|
||||
this.ctx.lineWidth = 3;
|
||||
this.ctx.lineCap = 'round';
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
endGesture(e) {
|
||||
if (!this.isGesturing) return;
|
||||
|
||||
this.isGesturing = false;
|
||||
const gestureDuration = Date.now() - this.gestureStartTime;
|
||||
|
||||
// Reset visual feedback
|
||||
this.handCursor.style.transform = 'translate(-50%, -50%)';
|
||||
this.gestureZone.style.background = 'radial-gradient(ellipse at center, rgba(0, 255, 136, 0.1), transparent)';
|
||||
|
||||
// Analyze gesture
|
||||
const recognizedGesture = this.recognizeGesture(this.gesturePoints, gestureDuration);
|
||||
if (recognizedGesture) {
|
||||
this.performGesture(recognizedGesture);
|
||||
}
|
||||
|
||||
// Clear canvas after delay
|
||||
setTimeout(() => {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
recognizeGesture(points, duration) {
|
||||
if (points.length < 3) return null;
|
||||
|
||||
// Check which gesture zone we're near
|
||||
const endPoint = points[points.length - 1];
|
||||
const indicators = this.gestureZone.querySelectorAll('.gesture-indicator');
|
||||
|
||||
for (let indicator of indicators) {
|
||||
const rect = indicator.getBoundingClientRect();
|
||||
const zoneRect = this.gestureZone.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2 - zoneRect.left;
|
||||
const centerY = rect.top + rect.height / 2 - zoneRect.top;
|
||||
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(endPoint.x - centerX, 2) +
|
||||
Math.pow(endPoint.y - centerY, 2)
|
||||
);
|
||||
|
||||
if (distance < 80) {
|
||||
return indicator.dataset.gesture;
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern recognition for gestures
|
||||
const movement = this.analyzeMovement(points);
|
||||
if (movement.isWave) return 'wave';
|
||||
if (movement.isCircle) return 'fist';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
analyzeMovement(points) {
|
||||
// Simple pattern analysis
|
||||
const movement = {
|
||||
isWave: false,
|
||||
isCircle: false
|
||||
};
|
||||
|
||||
if (points.length > 10) {
|
||||
// Check for wave pattern (horizontal back and forth)
|
||||
let directionChanges = 0;
|
||||
let lastDirection = null;
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const direction = points[i].x > points[i-1].x ? 'right' : 'left';
|
||||
if (lastDirection && direction !== lastDirection) {
|
||||
directionChanges++;
|
||||
}
|
||||
lastDirection = direction;
|
||||
}
|
||||
|
||||
movement.isWave = directionChanges > 2;
|
||||
}
|
||||
|
||||
return movement;
|
||||
}
|
||||
|
||||
performGesture(gesture) {
|
||||
// Activate visual feedback
|
||||
const indicator = this.gestureZone.querySelector(`[data-gesture="${gesture}"]`);
|
||||
if (indicator) {
|
||||
indicator.classList.add('active');
|
||||
setTimeout(() => indicator.classList.remove('active'), 600);
|
||||
}
|
||||
|
||||
// Show feedback message
|
||||
const messages = {
|
||||
wave: 'Hello! Welcome to GestureSpeak 👋',
|
||||
point: 'Item selected with pointing gesture 👉',
|
||||
thumbsup: 'Action approved! 👍',
|
||||
peace: 'Changes saved successfully ✌️',
|
||||
fist: 'Power action executed! ✊'
|
||||
};
|
||||
|
||||
this.showFeedback(messages[gesture] || 'Gesture recognized!');
|
||||
|
||||
// Log to output
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this.output.textContent = `[${timestamp}] Gesture performed: ${gesture.toUpperCase()}\n` +
|
||||
this.output.textContent;
|
||||
|
||||
// Trigger haptic feedback if available
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(50);
|
||||
}
|
||||
}
|
||||
|
||||
showFeedback(message) {
|
||||
this.feedback.textContent = message;
|
||||
this.feedback.classList.add('show');
|
||||
|
||||
clearTimeout(this.feedbackTimeout);
|
||||
this.feedbackTimeout = setTimeout(() => {
|
||||
this.feedback.classList.remove('show');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
highlightGesture(indicator) {
|
||||
indicator.style.transform = 'scale(1.1)';
|
||||
indicator.style.background = 'rgba(0, 255, 136, 0.3)';
|
||||
}
|
||||
|
||||
unhighlightGesture(indicator) {
|
||||
indicator.style.transform = 'scale(1)';
|
||||
indicator.style.background = 'rgba(0, 255, 136, 0.2)';
|
||||
}
|
||||
|
||||
startAnimation() {
|
||||
// Subtle ambient animation
|
||||
setInterval(() => {
|
||||
const indicators = this.gestureZone.querySelectorAll('.gesture-indicator');
|
||||
const randomIndicator = indicators[Math.floor(Math.random() * indicators.length)];
|
||||
|
||||
randomIndicator.style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
randomIndicator.style.animation = 'pulseActive 2s ease-out';
|
||||
}, 10);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Traditional button handler
|
||||
function traditionalAction(action) {
|
||||
const output = document.getElementById('gestureOutput');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
output.textContent = `[${timestamp}] Traditional button clicked: ${action}\n` +
|
||||
output.textContent;
|
||||
}
|
||||
|
||||
// Initialize GestureSpeak
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
new GestureSpeak();
|
||||
});
|
||||
|
||||
// Performance optimization
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
// Preload any heavy operations
|
||||
console.log('GestureSpeak Interface initialized');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,717 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Button Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f7fa;
|
||||
padding: 40px 20px;
|
||||
line-height: 1.6;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
color: #1a1a1a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.component-showcase {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.button-group h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Enhanced Button Base Styles */
|
||||
.btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-style: preserve-3d;
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* Ripple Effect Container */
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
/* Button Variants */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
/* Active State */
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(0) scale(0.98);
|
||||
}
|
||||
|
||||
/* Focus State */
|
||||
.btn:focus-visible {
|
||||
outline: 3px solid currentColor;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Disabled State */
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.btn-loading {
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-loading .btn-text {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn-loading .spinner {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Success Animation */
|
||||
.btn-success-state {
|
||||
background: #22c55e !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.btn-success-state .success-icon {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-success-state .btn-text {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* Error Animation */
|
||||
.btn-error {
|
||||
animation: shake 0.5s;
|
||||
background: #ef4444 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
/* Icon Support */
|
||||
.btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.btn-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.btn-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn-loading-progress .btn-progress {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-progress-bar {
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Button Groups */
|
||||
.btn-group-horizontal {
|
||||
display: inline-flex;
|
||||
gap: 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:first-child {
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:last-child {
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:not(:last-child) {
|
||||
border-right: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 10px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.8);
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
.btn:hover .tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
/* Demo Section */
|
||||
.demo-section {
|
||||
margin-top: 40px;
|
||||
padding-top: 40px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.demo-controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.code-snippet {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Mobile Optimization */
|
||||
@media (max-width: 768px) {
|
||||
.button-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-group-horizontal {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn {
|
||||
border-radius: 0;
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.btn-group-horizontal .btn:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.btn {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Button - Enhanced</h1>
|
||||
|
||||
<div class="component-showcase">
|
||||
<div class="button-grid">
|
||||
<!-- Primary Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Primary Actions</h3>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Click Me</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" onclick="handleProgressClick(this)">
|
||||
<span class="btn-text">Upload File</span>
|
||||
<div class="spinner"></div>
|
||||
<div class="btn-progress">
|
||||
<div class="btn-progress-bar"></div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary" disabled>
|
||||
<span class="btn-text">Disabled</span>
|
||||
<span class="tooltip">Complete form to enable</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Secondary Actions</h3>
|
||||
<button class="btn btn-secondary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Save Draft</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary">
|
||||
<span class="btn-icon">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="btn-text">Edit</span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary">
|
||||
<span class="btn-icon">
|
||||
<svg viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="btn-text">Add Item</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Success/Danger Buttons -->
|
||||
<div class="button-group">
|
||||
<h3>Contextual Actions</h3>
|
||||
<button class="btn btn-success" onclick="handleClick(this)">
|
||||
<span class="btn-text">Confirm</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" onclick="handleErrorClick(this)">
|
||||
<span class="btn-text">Delete</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" disabled>
|
||||
<span class="btn-text">Permanently Delete</span>
|
||||
<span class="tooltip">Action cannot be undone</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Button Groups -->
|
||||
<div class="demo-section">
|
||||
<h3 style="margin-bottom: 20px;">Button Groups</h3>
|
||||
|
||||
<div class="btn-group-horizontal">
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Previous</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Current</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="handleClick(this)">
|
||||
<span class="btn-text">Next</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Demo -->
|
||||
<div class="demo-section">
|
||||
<h3 style="margin-bottom: 20px;">Interactive Demo</h3>
|
||||
<div class="demo-controls">
|
||||
<button class="btn btn-primary" onclick="simulateLoading(this)">
|
||||
<span class="btn-text">Simulate Loading</span>
|
||||
<div class="spinner"></div>
|
||||
<svg class="success-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path d="M16.7 5.3L8 14L3.3 9.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" onclick="simulateProgress(this)">
|
||||
<span class="btn-text">Simulate Progress</span>
|
||||
<div class="spinner"></div>
|
||||
<div class="btn-progress">
|
||||
<div class="btn-progress-bar"></div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" onclick="simulateError(this)">
|
||||
<span class="btn-text">Simulate Error</span>
|
||||
<div class="spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Ripple effect for all buttons
|
||||
document.querySelectorAll('.btn').forEach(button => {
|
||||
button.addEventListener('click', function(e) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
const ripple = document.createElement('span');
|
||||
const size = Math.max(rect.width, rect.height);
|
||||
const x = e.clientX - rect.left - size / 2;
|
||||
const y = e.clientY - rect.top - size / 2;
|
||||
|
||||
ripple.style.cssText = `
|
||||
position: absolute;
|
||||
width: ${size}px;
|
||||
height: ${size}px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
top: ${y}px;
|
||||
left: ${x}px;
|
||||
pointer-events: none;
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s ease-out;
|
||||
`;
|
||||
|
||||
this.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
});
|
||||
|
||||
// Add ripple animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Basic click handler with loading state
|
||||
function handleClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading');
|
||||
button.classList.add('btn-success-state');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-success-state');
|
||||
}, 1500);
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
// Progress click handler
|
||||
function handleProgressClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading', 'btn-loading-progress');
|
||||
const progressBar = button.querySelector('.btn-progress-bar');
|
||||
let progress = 0;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
progress += Math.random() * 20;
|
||||
if (progress > 100) progress = 100;
|
||||
|
||||
progressBar.style.width = progress + '%';
|
||||
|
||||
if (progress >= 100) {
|
||||
clearInterval(interval);
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading', 'btn-loading-progress');
|
||||
button.classList.add('btn-success-state');
|
||||
progressBar.style.width = '0%';
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-success-state');
|
||||
}, 1500);
|
||||
}, 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Error click handler
|
||||
function handleErrorClick(button) {
|
||||
if (button.classList.contains('btn-loading')) return;
|
||||
|
||||
button.classList.add('btn-loading');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-loading');
|
||||
button.classList.add('btn-error');
|
||||
|
||||
setTimeout(() => {
|
||||
button.classList.remove('btn-error');
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Demo functions
|
||||
function simulateLoading(button) {
|
||||
handleClick(button);
|
||||
}
|
||||
|
||||
function simulateProgress(button) {
|
||||
handleProgressClick(button);
|
||||
}
|
||||
|
||||
function simulateError(button) {
|
||||
handleErrorClick(button);
|
||||
}
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement && activeElement.classList.contains('btn')) {
|
||||
e.preventDefault();
|
||||
activeElement.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Touch feedback enhancement
|
||||
let touchTimeout;
|
||||
document.querySelectorAll('.btn').forEach(button => {
|
||||
button.addEventListener('touchstart', function() {
|
||||
this.style.transform = 'scale(0.95)';
|
||||
clearTimeout(touchTimeout);
|
||||
});
|
||||
|
||||
button.addEventListener('touchend', function() {
|
||||
touchTimeout = setTimeout(() => {
|
||||
this.style.transform = '';
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
// Accessibility announcements
|
||||
const announcer = document.createElement('div');
|
||||
announcer.setAttribute('role', 'status');
|
||||
announcer.setAttribute('aria-live', 'polite');
|
||||
announcer.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
|
||||
document.body.appendChild(announcer);
|
||||
|
||||
function announce(message) {
|
||||
announcer.textContent = message;
|
||||
setTimeout(() => announcer.textContent = '', 1000);
|
||||
}
|
||||
|
||||
// Enhance button clicks with announcements
|
||||
const originalHandleClick = window.handleClick;
|
||||
window.handleClick = function(button) {
|
||||
originalHandleClick(button);
|
||||
announce('Processing...');
|
||||
setTimeout(() => announce('Action completed successfully'), 1500);
|
||||
};
|
||||
|
||||
// Focus management for button groups
|
||||
document.querySelectorAll('.btn-group-horizontal').forEach(group => {
|
||||
const buttons = group.querySelectorAll('.btn');
|
||||
|
||||
buttons.forEach((btn, index) => {
|
||||
btn.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowRight' && index < buttons.length - 1) {
|
||||
e.preventDefault();
|
||||
buttons[index + 1].focus();
|
||||
} else if (e.key === 'ArrowLeft' && index > 0) {
|
||||
e.preventDefault();
|
||||
buttons[index - 1].focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,882 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cards Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #ec4899;
|
||||
--success: #10b981;
|
||||
--danger: #ef4444;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--gray-900: #111827;
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--radius: 12px;
|
||||
--radius-lg: 16px;
|
||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--gray-900);
|
||||
background: var(--gray-50);
|
||||
padding: 2rem 1rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray-600);
|
||||
font-size: 1.125rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Card Grid */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Base Card Styles */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: var(--transition);
|
||||
transform-style: preserve-3d;
|
||||
cursor: pointer;
|
||||
will-change: transform, box-shadow;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
outline: 3px solid var(--primary);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
/* Card Image with Lazy Loading */
|
||||
.card-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1), filter 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover .card-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.card-image.loading::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.card-image img.blur-up {
|
||||
filter: blur(20px);
|
||||
}
|
||||
|
||||
.card-image img.loaded {
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
/* Card Content */
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--gray-900);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: var(--gray-600);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
/* Action Overlay */
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.card:hover .card-actions,
|
||||
.card:focus-within .card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.action-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: var(--gray-700);
|
||||
stroke-width: 2;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.action-btn.liked svg {
|
||||
fill: var(--danger);
|
||||
stroke: var(--danger);
|
||||
}
|
||||
|
||||
/* Flip Card */
|
||||
.flip-card {
|
||||
perspective: 1000px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.flip-card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
transition: transform 0.6s;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.flip-card.flipped .flip-card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.flip-card-front,
|
||||
.flip-card-back {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flip-card-back {
|
||||
transform: rotateY(180deg);
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.flip-trigger {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition-fast);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.flip-trigger:hover {
|
||||
transform: scale(1.1);
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* Expandable Card */
|
||||
.expandable-card {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.expandable-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.expandable-card.expanded .expandable-content {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.expand-trigger {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: var(--transition-fast);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.expand-trigger:hover {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.expand-trigger svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.expandable-card.expanded .expand-trigger svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Loading Skeleton */
|
||||
.card-skeleton {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--gray-200) 25%, var(--gray-100) 50%, var(--gray-200) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: loading 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-image {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.skeleton-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
height: 24px;
|
||||
width: 70%;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
height: 16px;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.skeleton-text:last-child {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
/* Selection Mode */
|
||||
.selection-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: white;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.selection-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selection-count {
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selection-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-fast);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--gray-200);
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--gray-300);
|
||||
}
|
||||
|
||||
/* Mobile Touch Support */
|
||||
@media (max-width: 768px) {
|
||||
.card-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card.swiping {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus Styles */
|
||||
.card:focus-visible,
|
||||
.action-btn:focus-visible,
|
||||
.flip-trigger:focus-visible,
|
||||
.expand-trigger:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--gray-300);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Cards - Enhanced</h1>
|
||||
<p class="subtitle">Beautiful, interactive cards with smooth animations and intelligent behaviors</p>
|
||||
|
||||
<!-- Selection Mode -->
|
||||
<div class="selection-mode" id="selectionMode" style="display: none;">
|
||||
<input type="checkbox" class="selection-checkbox" id="selectAll">
|
||||
<span class="selection-count">0 selected</span>
|
||||
<div class="selection-actions">
|
||||
<button class="btn btn-primary">Share</button>
|
||||
<button class="btn btn-secondary">Archive</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Grid -->
|
||||
<div class="card-grid" id="cardGrid">
|
||||
<!-- Loading Skeletons -->
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-skeleton">
|
||||
<div class="skeleton skeleton-image"></div>
|
||||
<div class="skeleton-content">
|
||||
<div class="skeleton skeleton-title"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
<div class="skeleton skeleton-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Special Cards Section -->
|
||||
<h2 style="margin: 3rem 0 1.5rem; font-size: 1.875rem; font-weight: 700;">Special Card Types</h2>
|
||||
|
||||
<div class="card-grid">
|
||||
<!-- Flip Card -->
|
||||
<div class="flip-card" id="flipCard">
|
||||
<div class="flip-card-inner">
|
||||
<div class="flip-card-front card">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='200'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%234f46e5;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%23ec4899;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='200' fill='url(%23g)'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='white' font-family='Arial' font-size='24' font-weight='bold'%3EFlip Me%3C/text%3E%3C/svg%3E" alt="Flip card front">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">Interactive Flip Card</h3>
|
||||
<p class="card-description">Click the button to reveal additional information on the back of this card.</p>
|
||||
<button class="flip-trigger" aria-label="Flip card">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flip-card-back">
|
||||
<h3 style="font-size: 1.5rem; margin-bottom: 1rem;">Additional Details</h3>
|
||||
<p style="margin-bottom: 1.5rem;">This is the back of the card with more detailed information that was hidden from the initial view.</p>
|
||||
<ul style="text-align: left; margin-bottom: 1.5rem;">
|
||||
<li>Hidden feature #1</li>
|
||||
<li>Secret detail #2</li>
|
||||
<li>Extra information #3</li>
|
||||
</ul>
|
||||
<button class="btn" style="background: white; color: var(--primary);" onclick="document.getElementById('flipCard').classList.remove('flipped')">
|
||||
Flip Back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expandable Card -->
|
||||
<div class="card expandable-card" id="expandableCard">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='200'%3E%3Cdefs%3E%3ClinearGradient id='g2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2310b981;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%236366f1;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='400' height='200' fill='url(%23g2)'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='white' font-family='Arial' font-size='24' font-weight='bold'%3EExpandable%3C/text%3E%3C/svg%3E" alt="Expandable card">
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">Expandable Content Card</h3>
|
||||
<p class="card-description">This card can expand to show additional content with a smooth animation.</p>
|
||||
<button class="expand-trigger">
|
||||
<span>Show more</span>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="expandable-content">
|
||||
<div style="padding-top: 1rem; border-top: 1px solid var(--gray-200);">
|
||||
<h4 style="font-weight: 600; margin-bottom: 0.5rem;">Extended Information</h4>
|
||||
<p style="color: var(--gray-600); margin-bottom: 1rem;">Here's the additional content that was hidden. It smoothly animates into view when expanded.</p>
|
||||
<ul style="color: var(--gray-600); padding-left: 1.5rem;">
|
||||
<li>Extra detail that wasn't visible before</li>
|
||||
<li>Another piece of hidden information</li>
|
||||
<li>More content to explore</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="action-btn" aria-label="Like">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="Share">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Card data
|
||||
const cardData = [
|
||||
{
|
||||
title: "Mountain Vista",
|
||||
description: "Experience breathtaking views from the mountain peaks with crystal clear skies.",
|
||||
image: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=200&fit=crop",
|
||||
author: "Alex Chen",
|
||||
date: "2 days ago"
|
||||
},
|
||||
{
|
||||
title: "Ocean Breeze",
|
||||
description: "Feel the calming waves and gentle sea breeze at this pristine beach location.",
|
||||
image: "https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=400&h=200&fit=crop",
|
||||
author: "Sarah Miller",
|
||||
date: "1 week ago"
|
||||
},
|
||||
{
|
||||
title: "Forest Trail",
|
||||
description: "Discover hidden paths through ancient forests filled with wildlife and wonder.",
|
||||
image: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400&h=200&fit=crop",
|
||||
author: "David Park",
|
||||
date: "3 days ago"
|
||||
},
|
||||
{
|
||||
title: "Desert Sunset",
|
||||
description: "Watch the sun paint the desert landscape in brilliant shades of orange and red.",
|
||||
image: "https://images.unsplash.com/photo-1509316785289-025f5b846b35?w=400&h=200&fit=crop",
|
||||
author: "Maria Garcia",
|
||||
date: "5 days ago"
|
||||
},
|
||||
{
|
||||
title: "City Lights",
|
||||
description: "Experience the vibrant energy of the city as it comes alive after dark.",
|
||||
image: "https://images.unsplash.com/photo-1514565131-fce0801e5785?w=400&h=200&fit=crop",
|
||||
author: "James Wilson",
|
||||
date: "1 day ago"
|
||||
},
|
||||
{
|
||||
title: "Lakeside Calm",
|
||||
description: "Find peace and tranquility by the still waters of this mountain lake.",
|
||||
image: "https://images.unsplash.com/photo-1439066615861-d1af74d74000?w=400&h=200&fit=crop",
|
||||
author: "Emma Thompson",
|
||||
date: "4 days ago"
|
||||
}
|
||||
];
|
||||
|
||||
// Create card HTML
|
||||
function createCard(data, index) {
|
||||
return `
|
||||
<div class="card" data-index="${index}" tabindex="0" role="article" aria-label="${data.title}">
|
||||
<div class="card-image loading">
|
||||
<img
|
||||
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Crect width='20' height='10' fill='%23e5e7eb'/%3E%3C/svg%3E"
|
||||
data-src="${data.image}"
|
||||
alt="${data.title}"
|
||||
class="blur-up"
|
||||
>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">${data.title}</h3>
|
||||
<p class="card-description">${data.description}</p>
|
||||
<div class="card-meta">
|
||||
<span>${data.author}</span>
|
||||
<span>${data.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="action-btn like-btn" aria-label="Like ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="Share ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/>
|
||||
<polyline points="16 6 12 2 8 6"/>
|
||||
<line x1="12" y1="2" x2="12" y2="15"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-btn" aria-label="More options for ${data.title}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="1"/>
|
||||
<circle cx="12" cy="5" r="1"/>
|
||||
<circle cx="12" cy="19" r="1"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Initialize cards
|
||||
const cardGrid = document.getElementById('cardGrid');
|
||||
setTimeout(() => {
|
||||
cardGrid.innerHTML = cardData.map((data, index) => createCard(data, index)).join('');
|
||||
initializeCards();
|
||||
}, 1500);
|
||||
|
||||
// Initialize card functionality
|
||||
function initializeCards() {
|
||||
// Lazy loading images
|
||||
const images = document.querySelectorAll('.card-image img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
const parent = img.parentElement;
|
||||
|
||||
// Load high-res image
|
||||
const highResImg = new Image();
|
||||
highResImg.src = img.dataset.src;
|
||||
highResImg.onload = () => {
|
||||
img.src = highResImg.src;
|
||||
setTimeout(() => {
|
||||
img.classList.add('loaded');
|
||||
parent.classList.remove('loading');
|
||||
}, 50);
|
||||
};
|
||||
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
|
||||
// Like button functionality
|
||||
document.querySelectorAll('.like-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
btn.classList.toggle('liked');
|
||||
|
||||
// Haptic feedback on mobile
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(10);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Card selection
|
||||
let selectedCards = new Set();
|
||||
let selectionMode = false;
|
||||
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.action-btn') || e.target.closest('.flip-trigger') || e.target.closest('.expand-trigger')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey || selectionMode) {
|
||||
const index = card.dataset.index;
|
||||
if (selectedCards.has(index)) {
|
||||
selectedCards.delete(index);
|
||||
card.classList.remove('selected');
|
||||
} else {
|
||||
selectedCards.add(index);
|
||||
card.classList.add('selected');
|
||||
}
|
||||
updateSelectionMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
card.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
card.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Selection mode controls
|
||||
function updateSelectionMode() {
|
||||
const selectionModeEl = document.getElementById('selectionMode');
|
||||
if (selectedCards.size > 0) {
|
||||
selectionMode = true;
|
||||
selectionModeEl.style.display = 'flex';
|
||||
selectionModeEl.querySelector('.selection-count').textContent = `${selectedCards.size} selected`;
|
||||
} else {
|
||||
selectionMode = false;
|
||||
selectionModeEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('selectAll').addEventListener('change', (e) => {
|
||||
const cards = document.querySelectorAll('.card[data-index]');
|
||||
if (e.target.checked) {
|
||||
cards.forEach(card => {
|
||||
selectedCards.add(card.dataset.index);
|
||||
card.classList.add('selected');
|
||||
});
|
||||
} else {
|
||||
selectedCards.clear();
|
||||
cards.forEach(card => card.classList.remove('selected'));
|
||||
}
|
||||
updateSelectionMode();
|
||||
});
|
||||
|
||||
// Touch gestures for mobile
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let currentCard = null;
|
||||
|
||||
document.querySelectorAll('.card').forEach(card => {
|
||||
card.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
currentCard = card;
|
||||
}, { passive: true });
|
||||
|
||||
card.addEventListener('touchmove', (e) => {
|
||||
if (!touchStartX || !currentCard) return;
|
||||
|
||||
const touchEndX = e.touches[0].clientX;
|
||||
const touchEndY = e.touches[0].clientY;
|
||||
const diffX = touchStartX - touchEndX;
|
||||
const diffY = touchStartY - touchEndY;
|
||||
|
||||
// Only handle horizontal swipes
|
||||
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
|
||||
currentCard.classList.add('swiping');
|
||||
currentCard.style.transform = `translateX(${-diffX * 0.5}px)`;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
card.addEventListener('touchend', () => {
|
||||
if (currentCard) {
|
||||
currentCard.classList.remove('swiping');
|
||||
currentCard.style.transform = '';
|
||||
currentCard = null;
|
||||
}
|
||||
touchStartX = 0;
|
||||
touchStartY = 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Flip card functionality
|
||||
document.querySelector('.flip-trigger').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
document.getElementById('flipCard').classList.add('flipped');
|
||||
});
|
||||
|
||||
// Expandable card functionality
|
||||
document.querySelector('.expand-trigger').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = document.getElementById('expandableCard');
|
||||
const isExpanded = card.classList.contains('expanded');
|
||||
card.classList.toggle('expanded');
|
||||
|
||||
const trigger = e.currentTarget;
|
||||
const text = trigger.querySelector('span');
|
||||
text.textContent = isExpanded ? 'Show more' : 'Show less';
|
||||
|
||||
// Announce state change for screen readers
|
||||
const announcement = document.createElement('div');
|
||||
announcement.setAttribute('role', 'status');
|
||||
announcement.setAttribute('aria-live', 'polite');
|
||||
announcement.className = 'sr-only';
|
||||
announcement.textContent = isExpanded ? 'Content collapsed' : 'Content expanded';
|
||||
document.body.appendChild(announcement);
|
||||
setTimeout(() => announcement.remove(), 1000);
|
||||
});
|
||||
|
||||
// Respect reduced motion preference
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
document.documentElement.style.setProperty('--transition', 'none');
|
||||
document.documentElement.style.setProperty('--transition-fast', 'none');
|
||||
}
|
||||
|
||||
// Screen reader only content
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }';
|
||||
document.head.appendChild(style);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,972 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>File Upload Enhanced</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--primary-light: #e0e7ff;
|
||||
--success: #10b981;
|
||||
--error: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--gray-50: #f9fafb;
|
||||
--gray-100: #f3f4f6;
|
||||
--gray-200: #e5e7eb;
|
||||
--gray-300: #d1d5db;
|
||||
--gray-400: #9ca3af;
|
||||
--gray-500: #6b7280;
|
||||
--gray-600: #4b5563;
|
||||
--gray-700: #374151;
|
||||
--gray-800: #1f2937;
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
color: var(--gray-800);
|
||||
line-height: 1.6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
background: var(--gray-50);
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
border: 2px dashed var(--gray-300);
|
||||
border-radius: 12px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.upload-zone.drag-over {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary-light);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon {
|
||||
animation: bounce 0.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 24px;
|
||||
background: var(--gray-100);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.upload-icon svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
stroke: var(--gray-500);
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.upload-zone.drag-over .upload-icon svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.upload-text h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.upload-text p {
|
||||
color: var(--gray-600);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-input-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-button:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.file-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.supported-formats {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
background: var(--gray-100);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.file-preview.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
stroke: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: var(--gray-800);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.file-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.file-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.status-uploading {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.file-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: var(--gray-100);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: var(--gray-200);
|
||||
}
|
||||
|
||||
.action-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke: var(--gray-600);
|
||||
}
|
||||
|
||||
.action-button.danger:hover {
|
||||
background: var(--error);
|
||||
}
|
||||
|
||||
.action-button.danger:hover svg {
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--gray-200);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary) 0%, #8b5cf6 100%);
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
|
||||
animation: shimmer 1.5s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.paste-hint {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: var(--gray-800);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
transition: var(--transition);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.paste-hint.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.upload-stats {
|
||||
margin-top: 32px;
|
||||
padding: 20px;
|
||||
background: var(--gray-50);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: var(--error);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.error-message svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.upload-stats {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.file-input:focus + .file-button {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>File Upload - Enhanced</h1>
|
||||
<p class="subtitle">Drag & drop files or click to browse. Supports images, documents, and more.</p>
|
||||
|
||||
<div class="upload-container">
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<div class="upload-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="upload-text">
|
||||
<h3>Drop files here or click to upload</h3>
|
||||
<p>You can also paste images from your clipboard</p>
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" class="file-input" id="fileInput" multiple accept="image/*,.pdf,.doc,.docx,.txt,.zip">
|
||||
<button class="file-button">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
Choose Files
|
||||
</button>
|
||||
</div>
|
||||
<p class="supported-formats">Supported: Images, PDF, DOC, TXT, ZIP (Max 10MB per file)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paste-hint" id="pasteHint">Image copied! Press Ctrl+V to upload</div>
|
||||
|
||||
<div class="file-list" id="fileList"></div>
|
||||
|
||||
<div class="upload-stats" id="uploadStats" style="display: none;">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalFiles">0</div>
|
||||
<div class="stat-label">Total Files</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="uploadedFiles">0</div>
|
||||
<div class="stat-label">Uploaded</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalSize">0 MB</div>
|
||||
<div class="stat-label">Total Size</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
class FileUploadEnhanced {
|
||||
constructor() {
|
||||
this.uploadZone = document.getElementById('uploadZone');
|
||||
this.fileInput = document.getElementById('fileInput');
|
||||
this.fileList = document.getElementById('fileList');
|
||||
this.pasteHint = document.getElementById('pasteHint');
|
||||
this.uploadStats = document.getElementById('uploadStats');
|
||||
|
||||
this.files = new Map();
|
||||
this.uploadQueue = [];
|
||||
this.isUploading = false;
|
||||
this.maxFileSize = 10 * 1024 * 1024; // 10MB
|
||||
this.chunkSize = 1024 * 1024; // 1MB chunks
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// File input
|
||||
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
|
||||
|
||||
// Drag and drop
|
||||
this.uploadZone.addEventListener('dragover', (e) => this.handleDragOver(e));
|
||||
this.uploadZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
||||
this.uploadZone.addEventListener('drop', (e) => this.handleDrop(e));
|
||||
|
||||
// Paste from clipboard
|
||||
document.addEventListener('paste', (e) => this.handlePaste(e));
|
||||
|
||||
// Copy event detection
|
||||
document.addEventListener('copy', () => this.showPasteHint());
|
||||
|
||||
// Prevent default drag behavior
|
||||
document.addEventListener('dragover', (e) => e.preventDefault());
|
||||
document.addEventListener('drop', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
handleFileSelect(e) {
|
||||
const files = Array.from(e.target.files);
|
||||
this.processFiles(files);
|
||||
e.target.value = ''; // Reset input
|
||||
}
|
||||
|
||||
handleDragOver(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.uploadZone.classList.add('drag-over');
|
||||
}
|
||||
|
||||
handleDragLeave(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!this.uploadZone.contains(e.relatedTarget)) {
|
||||
this.uploadZone.classList.remove('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.uploadZone.classList.remove('drag-over');
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
this.processFiles(files);
|
||||
}
|
||||
|
||||
handlePaste(e) {
|
||||
const items = Array.from(e.clipboardData.items);
|
||||
const files = [];
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
if (file) {
|
||||
// Give pasted images a name
|
||||
const renamedFile = new File([file], `pasted-image-${Date.now()}.png`, {
|
||||
type: file.type
|
||||
});
|
||||
files.push(renamedFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (files.length > 0) {
|
||||
this.processFiles(files);
|
||||
this.hidePasteHint();
|
||||
}
|
||||
}
|
||||
|
||||
showPasteHint() {
|
||||
this.pasteHint.classList.add('show');
|
||||
setTimeout(() => this.hidePasteHint(), 3000);
|
||||
}
|
||||
|
||||
hidePasteHint() {
|
||||
this.pasteHint.classList.remove('show');
|
||||
}
|
||||
|
||||
processFiles(files) {
|
||||
files.forEach(file => {
|
||||
const validation = this.validateFile(file);
|
||||
if (validation.valid) {
|
||||
this.addFile(file);
|
||||
} else {
|
||||
this.showError(validation.error);
|
||||
}
|
||||
});
|
||||
|
||||
this.updateStats();
|
||||
this.processUploadQueue();
|
||||
}
|
||||
|
||||
validateFile(file) {
|
||||
// Check file size
|
||||
if (file.size > this.maxFileSize) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${file.name} exceeds the 10MB size limit`
|
||||
};
|
||||
}
|
||||
|
||||
// Check file type
|
||||
const allowedTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf', 'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain', 'application/zip'
|
||||
];
|
||||
|
||||
if (!allowedTypes.includes(file.type) && !file.type.startsWith('image/')) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `${file.name} is not a supported file type`
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
addFile(file) {
|
||||
const fileId = this.generateId();
|
||||
const fileData = {
|
||||
id: fileId,
|
||||
file: file,
|
||||
status: 'pending',
|
||||
progress: 0,
|
||||
uploaded: 0,
|
||||
element: null
|
||||
};
|
||||
|
||||
this.files.set(fileId, fileData);
|
||||
this.uploadQueue.push(fileId);
|
||||
|
||||
const element = this.createFileElement(fileData);
|
||||
fileData.element = element;
|
||||
this.fileList.appendChild(element);
|
||||
|
||||
if (this.uploadStats.style.display === 'none') {
|
||||
this.uploadStats.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Load preview for images
|
||||
if (file.type.startsWith('image/')) {
|
||||
this.loadImagePreview(fileData);
|
||||
}
|
||||
}
|
||||
|
||||
createFileElement(fileData) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'file-item';
|
||||
div.id = `file-${fileData.id}`;
|
||||
|
||||
const isImage = fileData.file.type.startsWith('image/');
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="file-preview ${isImage ? 'loading' : ''}">
|
||||
${this.getFileIcon(fileData.file)}
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${fileData.file.name}</div>
|
||||
<div class="file-meta">
|
||||
<div class="file-size">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
</svg>
|
||||
${this.formatFileSize(fileData.file.size)}
|
||||
</div>
|
||||
<div class="file-status status-uploading">
|
||||
<svg class="status-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span>Waiting...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button class="action-button" onclick="fileUpload.retryUpload('${fileData.id}')" title="Retry" style="display: none;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="action-button danger" onclick="fileUpload.removeFile('${fileData.id}')" title="Remove">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
getFileIcon(file) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
return `<img src="" alt="${file.name}" style="display: none;">`;
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (file.type === 'application/pdf') {
|
||||
icon = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline>';
|
||||
} else if (file.type.includes('zip')) {
|
||||
icon = '<path d="M22 11v1a10 10 0 1 1-9-10"></path><polyline points="22 2 12 12.01 8 8"></polyline>';
|
||||
} else {
|
||||
icon = '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline>';
|
||||
}
|
||||
|
||||
return `<svg class="file-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">${icon}</svg>`;
|
||||
}
|
||||
|
||||
loadImagePreview(fileData) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const preview = fileData.element.querySelector('.file-preview');
|
||||
const img = preview.querySelector('img');
|
||||
img.src = e.target.result;
|
||||
img.style.display = 'block';
|
||||
preview.classList.remove('loading');
|
||||
};
|
||||
reader.readAsDataURL(fileData.file);
|
||||
}
|
||||
|
||||
async processUploadQueue() {
|
||||
if (this.isUploading || this.uploadQueue.length === 0) return;
|
||||
|
||||
this.isUploading = true;
|
||||
const fileId = this.uploadQueue.shift();
|
||||
const fileData = this.files.get(fileId);
|
||||
|
||||
if (fileData && fileData.status === 'pending') {
|
||||
await this.uploadFile(fileData);
|
||||
}
|
||||
|
||||
this.isUploading = false;
|
||||
this.processUploadQueue();
|
||||
}
|
||||
|
||||
async uploadFile(fileData) {
|
||||
fileData.status = 'uploading';
|
||||
this.updateFileStatus(fileData, 'Uploading...', 'status-uploading');
|
||||
|
||||
try {
|
||||
// Simulate chunked upload
|
||||
const chunks = Math.ceil(fileData.file.size / this.chunkSize);
|
||||
|
||||
for (let i = 0; i < chunks; i++) {
|
||||
// Simulate upload delay
|
||||
await this.delay(300 + Math.random() * 200);
|
||||
|
||||
// Update progress
|
||||
fileData.progress = Math.min(((i + 1) / chunks) * 100, 100);
|
||||
this.updateProgress(fileData);
|
||||
|
||||
// Simulate random failure (5% chance)
|
||||
if (Math.random() < 0.05 && i > 0) {
|
||||
throw new Error('Network error');
|
||||
}
|
||||
}
|
||||
|
||||
// Upload complete
|
||||
fileData.status = 'completed';
|
||||
fileData.uploaded = fileData.file.size;
|
||||
this.updateFileStatus(fileData, 'Uploaded', 'status-success');
|
||||
this.showSuccess(fileData);
|
||||
|
||||
} catch (error) {
|
||||
fileData.status = 'error';
|
||||
this.updateFileStatus(fileData, 'Failed', 'status-error');
|
||||
this.showRetryButton(fileData);
|
||||
}
|
||||
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
updateFileStatus(fileData, text, className) {
|
||||
const statusElement = fileData.element.querySelector('.file-status');
|
||||
statusElement.className = `file-status ${className}`;
|
||||
statusElement.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
updateProgress(fileData) {
|
||||
const progressFill = fileData.element.querySelector('.progress-fill');
|
||||
progressFill.style.width = `${fileData.progress}%`;
|
||||
}
|
||||
|
||||
showSuccess(fileData) {
|
||||
const element = fileData.element;
|
||||
element.style.animation = 'none';
|
||||
element.offsetHeight; // Trigger reflow
|
||||
element.style.animation = 'slideIn 0.3s ease';
|
||||
|
||||
// Hide progress bar after animation
|
||||
setTimeout(() => {
|
||||
const progressBar = element.querySelector('.progress-bar');
|
||||
progressBar.style.opacity = '0';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
showRetryButton(fileData) {
|
||||
const retryButton = fileData.element.querySelector('.action-button');
|
||||
retryButton.style.display = 'flex';
|
||||
}
|
||||
|
||||
retryUpload(fileId) {
|
||||
const fileData = this.files.get(fileId);
|
||||
if (fileData) {
|
||||
fileData.status = 'pending';
|
||||
fileData.progress = 0;
|
||||
this.updateProgress(fileData);
|
||||
this.updateFileStatus(fileData, 'Waiting...', 'status-uploading');
|
||||
|
||||
const retryButton = fileData.element.querySelector('.action-button');
|
||||
retryButton.style.display = 'none';
|
||||
|
||||
const progressBar = fileData.element.querySelector('.progress-bar');
|
||||
progressBar.style.opacity = '1';
|
||||
|
||||
this.uploadQueue.push(fileId);
|
||||
this.processUploadQueue();
|
||||
}
|
||||
}
|
||||
|
||||
removeFile(fileId) {
|
||||
const fileData = this.files.get(fileId);
|
||||
if (fileData) {
|
||||
// Cancel upload if in progress
|
||||
if (fileData.status === 'uploading') {
|
||||
const index = this.uploadQueue.indexOf(fileId);
|
||||
if (index > -1) {
|
||||
this.uploadQueue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove with animation
|
||||
fileData.element.style.transform = 'translateX(100%)';
|
||||
fileData.element.style.opacity = '0';
|
||||
|
||||
setTimeout(() => {
|
||||
fileData.element.remove();
|
||||
this.files.delete(fileId);
|
||||
this.updateStats();
|
||||
|
||||
if (this.files.size === 0) {
|
||||
this.uploadStats.style.display = 'none';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
let totalSize = 0;
|
||||
let uploadedCount = 0;
|
||||
|
||||
this.files.forEach(fileData => {
|
||||
totalSize += fileData.file.size;
|
||||
if (fileData.status === 'completed') {
|
||||
uploadedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('totalFiles').textContent = this.files.size;
|
||||
document.getElementById('uploadedFiles').textContent = uploadedCount;
|
||||
document.getElementById('totalSize').textContent = this.formatFileSize(totalSize);
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const error = document.createElement('div');
|
||||
error.className = 'error-message';
|
||||
error.innerHTML = `
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
${message}
|
||||
`;
|
||||
|
||||
this.uploadZone.appendChild(error);
|
||||
|
||||
setTimeout(() => {
|
||||
error.style.opacity = '0';
|
||||
setTimeout(() => error.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'file-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the enhanced file upload
|
||||
const fileUpload = new FileUploadEnhanced();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,988 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Toast Notifications Enhanced</title>
|
||||
<style>
|
||||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
main {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 3rem;
|
||||
text-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Demo controls */
|
||||
.controls {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.control-group h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.2);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.demo-button:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.demo-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.demo-button.success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
|
||||
.demo-button.error {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
|
||||
.demo-button.warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
|
||||
.demo-button.info {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
}
|
||||
|
||||
/* Position selector */
|
||||
.position-selector {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.position-button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.position-button:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.position-button.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* Toast container positions */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-container.top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.toast-container.top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.toast-container.top-center {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-container.bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
align-items: flex-end;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container.bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
align-items: flex-start;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.toast-container.bottom-center {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
align-items: center;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* Toast styles */
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
padding: 1rem 1.5rem;
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
animation: slideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-left .toast,
|
||||
.toast-container.bottom-left .toast {
|
||||
animation-name: slideInLeft;
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-center .toast,
|
||||
.toast-container.bottom-center .toast {
|
||||
animation-name: slideInTop;
|
||||
}
|
||||
|
||||
@keyframes slideInTop {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: slideOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
to {
|
||||
transform: translateX(110%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast-container.top-left .toast.removing,
|
||||
.toast-container.bottom-left .toast.removing {
|
||||
animation-name: slideOutLeft;
|
||||
}
|
||||
|
||||
@keyframes slideOutLeft {
|
||||
to {
|
||||
transform: translateX(-110%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 15px 50px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Toast icon */
|
||||
.toast-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toast-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toast.success .toast-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast.error .toast-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast.warning .toast-icon {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast.info .toast-icon {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Toast content */
|
||||
.toast-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.875rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Toast actions */
|
||||
.toast-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.toast-action {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.toast-action:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.toast-action.primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toast-action.primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
.toast-close {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #a0aec0;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
background: rgba(0,0,0,0.05);
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Progress bar */
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: currentColor;
|
||||
opacity: 0.2;
|
||||
transition: width linear;
|
||||
}
|
||||
|
||||
.toast.success .toast-progress {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.toast.error .toast-progress {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.toast.warning .toast-progress {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.toast.info .toast-progress {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 640px) {
|
||||
.toast {
|
||||
min-width: calc(100vw - 2rem);
|
||||
max-width: calc(100vw - 2rem);
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility focus styles */
|
||||
.toast:focus-visible,
|
||||
.toast-action:focus-visible,
|
||||
.toast-close:focus-visible {
|
||||
outline: 2px solid #667eea;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.toast {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.toast.removing {
|
||||
animation: fadeOut 0.3s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Stats display */
|
||||
.stats {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 1rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #718096;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Toast Notifications - Enhanced</h1>
|
||||
|
||||
<div class="controls">
|
||||
<div class="control-group">
|
||||
<h3>Toast Position</h3>
|
||||
<div class="position-selector">
|
||||
<button class="position-button active" data-position="top-right">Top Right</button>
|
||||
<button class="position-button" data-position="top-center">Top Center</button>
|
||||
<button class="position-button" data-position="top-left">Top Left</button>
|
||||
<button class="position-button" data-position="bottom-right">Bottom Right</button>
|
||||
<button class="position-button" data-position="bottom-center">Bottom Center</button>
|
||||
<button class="position-button" data-position="bottom-left">Bottom Left</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Create Toasts</h3>
|
||||
<div class="button-grid">
|
||||
<button class="demo-button success" data-type="success">Success Toast</button>
|
||||
<button class="demo-button error" data-type="error">Error Toast</button>
|
||||
<button class="demo-button warning" data-type="warning">Warning Toast</button>
|
||||
<button class="demo-button info" data-type="info">Info Toast</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<h3>Special Toasts</h3>
|
||||
<div class="button-grid">
|
||||
<button class="demo-button info" id="actionToast">Toast with Actions</button>
|
||||
<button class="demo-button warning" id="undoToast">Undo Toast</button>
|
||||
<button class="demo-button success" id="multiToast">Multiple Toasts</button>
|
||||
<button class="demo-button error" id="persistentToast">Persistent Toast</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="activeCount">0</div>
|
||||
<div class="stat-label">Active Toasts</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="queueCount">0</div>
|
||||
<div class="stat-label">Queued</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="totalCount">0</div>
|
||||
<div class="stat-label">Total Shown</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-value" id="dismissedCount">0</div>
|
||||
<div class="stat-label">Dismissed</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Toast containers will be created dynamically -->
|
||||
|
||||
<script>
|
||||
// Toast notification system
|
||||
class ToastSystem {
|
||||
constructor() {
|
||||
this.toasts = new Map();
|
||||
this.queue = [];
|
||||
this.position = 'top-right';
|
||||
this.maxVisible = 5;
|
||||
this.defaultDuration = 5000;
|
||||
this.stats = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
queued: 0,
|
||||
dismissed: 0
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createContainers();
|
||||
this.setupEventListeners();
|
||||
this.updateStats();
|
||||
}
|
||||
|
||||
createContainers() {
|
||||
const positions = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'];
|
||||
positions.forEach(position => {
|
||||
const container = document.createElement('div');
|
||||
container.className = `toast-container ${position}`;
|
||||
container.style.display = position === this.position ? 'flex' : 'none';
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Position buttons
|
||||
document.querySelectorAll('.position-button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
this.setPosition(button.dataset.position);
|
||||
document.querySelectorAll('.position-button').forEach(b => b.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Toast type buttons
|
||||
document.querySelectorAll('.demo-button[data-type]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const type = button.dataset.type;
|
||||
this.showToast({
|
||||
type,
|
||||
title: this.getTitle(type),
|
||||
message: this.getMessage(type)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Special toasts
|
||||
document.getElementById('actionToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'info',
|
||||
title: 'New Message',
|
||||
message: 'You have a new message from Sarah',
|
||||
actions: [
|
||||
{ text: 'View', primary: true, callback: () => console.log('View clicked') },
|
||||
{ text: 'Dismiss', callback: () => {} }
|
||||
],
|
||||
duration: 0 // No auto-dismiss
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('undoToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'warning',
|
||||
title: 'Item Deleted',
|
||||
message: 'The item has been moved to trash',
|
||||
actions: [
|
||||
{ text: 'Undo', primary: true, callback: () => {
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: 'Restored',
|
||||
message: 'Item has been restored'
|
||||
});
|
||||
}}
|
||||
],
|
||||
duration: 10000
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('multiToast').addEventListener('click', () => {
|
||||
const messages = [
|
||||
'First notification',
|
||||
'Second notification',
|
||||
'Third notification',
|
||||
'Fourth notification',
|
||||
'Fifth notification',
|
||||
'Sixth notification (queued)',
|
||||
'Seventh notification (queued)'
|
||||
];
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
setTimeout(() => {
|
||||
this.showToast({
|
||||
type: ['success', 'info', 'warning'][index % 3],
|
||||
title: `Notification ${index + 1}`,
|
||||
message
|
||||
});
|
||||
}, index * 200);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('persistentToast').addEventListener('click', () => {
|
||||
this.showToast({
|
||||
type: 'error',
|
||||
title: 'Connection Lost',
|
||||
message: 'Please check your internet connection',
|
||||
duration: 0, // No auto-dismiss
|
||||
closable: false // Can't be closed manually
|
||||
});
|
||||
|
||||
// Simulate reconnection after 5 seconds
|
||||
setTimeout(() => {
|
||||
this.showToast({
|
||||
type: 'success',
|
||||
title: 'Connected',
|
||||
message: 'Connection restored'
|
||||
});
|
||||
// Find and remove the persistent toast
|
||||
this.toasts.forEach((toast, id) => {
|
||||
if (toast.config.title === 'Connection Lost') {
|
||||
this.removeToast(id);
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// Dismiss all toasts
|
||||
this.toasts.forEach((toast, id) => {
|
||||
if (toast.config.closable !== false) {
|
||||
this.removeToast(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPosition(position) {
|
||||
this.position = position;
|
||||
document.querySelectorAll('.toast-container').forEach(container => {
|
||||
container.style.display = container.classList.contains(position) ? 'flex' : 'none';
|
||||
});
|
||||
|
||||
// Move existing toasts to new container
|
||||
const newContainer = document.querySelector(`.toast-container.${position}`);
|
||||
this.toasts.forEach(toast => {
|
||||
newContainer.appendChild(toast.element);
|
||||
});
|
||||
}
|
||||
|
||||
showToast(config) {
|
||||
const id = Date.now() + Math.random();
|
||||
const toast = {
|
||||
id,
|
||||
config: {
|
||||
duration: this.defaultDuration,
|
||||
closable: true,
|
||||
...config
|
||||
},
|
||||
element: null,
|
||||
progressInterval: null
|
||||
};
|
||||
|
||||
// Check if we need to queue this toast
|
||||
if (this.toasts.size >= this.maxVisible) {
|
||||
this.queue.push(toast);
|
||||
this.stats.queued++;
|
||||
this.updateStats();
|
||||
return;
|
||||
}
|
||||
|
||||
this.createToastElement(toast);
|
||||
this.toasts.set(id, toast);
|
||||
this.stats.total++;
|
||||
this.stats.active++;
|
||||
this.updateStats();
|
||||
|
||||
// Auto-dismiss if duration is set
|
||||
if (toast.config.duration > 0) {
|
||||
this.startProgress(toast);
|
||||
}
|
||||
}
|
||||
|
||||
createToastElement(toast) {
|
||||
const element = document.createElement('div');
|
||||
element.className = `toast ${toast.config.type}`;
|
||||
element.setAttribute('role', 'alert');
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.tabIndex = 0;
|
||||
|
||||
// Icon
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'toast-icon';
|
||||
icon.innerHTML = this.getIcon(toast.config.type);
|
||||
element.appendChild(icon);
|
||||
|
||||
// Content
|
||||
const content = document.createElement('div');
|
||||
content.className = 'toast-content';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'toast-title';
|
||||
title.textContent = toast.config.title;
|
||||
content.appendChild(title);
|
||||
|
||||
const message = document.createElement('div');
|
||||
message.className = 'toast-message';
|
||||
message.textContent = toast.config.message;
|
||||
content.appendChild(message);
|
||||
|
||||
element.appendChild(content);
|
||||
|
||||
// Actions
|
||||
if (toast.config.actions && toast.config.actions.length > 0) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'toast-actions';
|
||||
|
||||
toast.config.actions.forEach(action => {
|
||||
const button = document.createElement('button');
|
||||
button.className = `toast-action ${action.primary ? 'primary' : ''}`;
|
||||
button.textContent = action.text;
|
||||
button.addEventListener('click', () => {
|
||||
action.callback();
|
||||
if (action.dismiss !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
actions.appendChild(button);
|
||||
});
|
||||
|
||||
element.appendChild(actions);
|
||||
}
|
||||
|
||||
// Close button
|
||||
if (toast.config.closable !== false) {
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.className = 'toast-close';
|
||||
closeButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
|
||||
closeButton.setAttribute('aria-label', 'Close notification');
|
||||
closeButton.addEventListener('click', () => this.removeToast(toast.id));
|
||||
element.appendChild(closeButton);
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
if (toast.config.duration > 0) {
|
||||
const progress = document.createElement('div');
|
||||
progress.className = 'toast-progress';
|
||||
progress.style.width = '100%';
|
||||
element.appendChild(progress);
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
element.addEventListener('mouseenter', () => this.pauseProgress(toast));
|
||||
element.addEventListener('mouseleave', () => this.resumeProgress(toast));
|
||||
|
||||
// Touch swipe to dismiss
|
||||
let touchStartX = 0;
|
||||
element.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
});
|
||||
|
||||
element.addEventListener('touchend', (e) => {
|
||||
const touchEndX = e.changedTouches[0].clientX;
|
||||
const diff = touchEndX - touchStartX;
|
||||
if (Math.abs(diff) > 50 && toast.config.closable !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Click to dismiss (if no actions)
|
||||
if (!toast.config.actions || toast.config.actions.length === 0) {
|
||||
element.style.cursor = 'pointer';
|
||||
element.addEventListener('click', () => {
|
||||
if (toast.config.closable !== false) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toast.element = element;
|
||||
|
||||
// Add to container
|
||||
const container = document.querySelector(`.toast-container.${this.position}`);
|
||||
container.appendChild(element);
|
||||
|
||||
// Focus for accessibility
|
||||
element.focus();
|
||||
}
|
||||
|
||||
startProgress(toast) {
|
||||
if (!toast.config.duration || toast.progressInterval) return;
|
||||
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (!progress) return;
|
||||
|
||||
const startTime = Date.now();
|
||||
const duration = toast.config.duration;
|
||||
|
||||
toast.progressInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const percentage = Math.max(0, 100 - (elapsed / duration) * 100);
|
||||
progress.style.width = `${percentage}%`;
|
||||
progress.style.transition = 'width 100ms linear';
|
||||
|
||||
if (percentage <= 0) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
pauseProgress(toast) {
|
||||
if (toast.progressInterval) {
|
||||
clearInterval(toast.progressInterval);
|
||||
toast.progressInterval = null;
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (progress) {
|
||||
toast.pausedWidth = progress.style.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resumeProgress(toast) {
|
||||
if (!toast.config.duration || toast.progressInterval) return;
|
||||
|
||||
const progress = toast.element.querySelector('.toast-progress');
|
||||
if (!progress || !toast.pausedWidth) return;
|
||||
|
||||
const remainingPercentage = parseFloat(toast.pausedWidth);
|
||||
const remainingDuration = (remainingPercentage / 100) * toast.config.duration;
|
||||
|
||||
const startTime = Date.now();
|
||||
toast.progressInterval = setInterval(() => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const percentage = Math.max(0, remainingPercentage - (elapsed / remainingDuration) * remainingPercentage);
|
||||
progress.style.width = `${percentage}%`;
|
||||
|
||||
if (percentage <= 0) {
|
||||
this.removeToast(toast.id);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
removeToast(id) {
|
||||
const toast = this.toasts.get(id);
|
||||
if (!toast) return;
|
||||
|
||||
// Clear progress interval
|
||||
if (toast.progressInterval) {
|
||||
clearInterval(toast.progressInterval);
|
||||
}
|
||||
|
||||
// Animate out
|
||||
toast.element.classList.add('removing');
|
||||
|
||||
setTimeout(() => {
|
||||
toast.element.remove();
|
||||
this.toasts.delete(id);
|
||||
this.stats.active--;
|
||||
this.stats.dismissed++;
|
||||
this.updateStats();
|
||||
|
||||
// Process queue
|
||||
if (this.queue.length > 0 && this.toasts.size < this.maxVisible) {
|
||||
const nextToast = this.queue.shift();
|
||||
this.stats.queued--;
|
||||
this.createToastElement(nextToast);
|
||||
this.toasts.set(nextToast.id, nextToast);
|
||||
this.stats.active++;
|
||||
this.updateStats();
|
||||
|
||||
if (nextToast.config.duration > 0) {
|
||||
this.startProgress(nextToast);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
document.getElementById('activeCount').textContent = this.stats.active;
|
||||
document.getElementById('queueCount').textContent = this.stats.queued;
|
||||
document.getElementById('totalCount').textContent = this.stats.total;
|
||||
document.getElementById('dismissedCount').textContent = this.stats.dismissed;
|
||||
}
|
||||
|
||||
getIcon(type) {
|
||||
const icons = {
|
||||
success: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
||||
error: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>',
|
||||
warning: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>',
|
||||
info: '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>'
|
||||
};
|
||||
return icons[type] || icons.info;
|
||||
}
|
||||
|
||||
getTitle(type) {
|
||||
const titles = {
|
||||
success: 'Success!',
|
||||
error: 'Error!',
|
||||
warning: 'Warning!',
|
||||
info: 'Information'
|
||||
};
|
||||
return titles[type] || 'Notification';
|
||||
}
|
||||
|
||||
getMessage(type) {
|
||||
const messages = {
|
||||
success: 'Your action was completed successfully.',
|
||||
error: 'Something went wrong. Please try again.',
|
||||
warning: 'Please review this important information.',
|
||||
info: 'Here\'s something you might want to know.'
|
||||
};
|
||||
return messages[type] || 'This is a notification message.';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the toast system
|
||||
const toastSystem = new ToastSystem();
|
||||
|
||||
// Show a welcome toast after page load
|
||||
setTimeout(() => {
|
||||
toastSystem.showToast({
|
||||
type: 'info',
|
||||
title: 'Welcome!',
|
||||
message: 'Try the different toast types and positions.',
|
||||
duration: 7000
|
||||
});
|
||||
}, 500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,829 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Architectural Brutalism Dashboard Widget</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
line-height: 1.6;
|
||||
padding: 40px;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3em;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
color: #fff;
|
||||
margin-bottom: 40px;
|
||||
text-transform: uppercase;
|
||||
text-shadow: 4px 4px 0 #000;
|
||||
}
|
||||
|
||||
/* Brutalist Dashboard Container */
|
||||
.hybrid-component {
|
||||
background: #2a2a2a;
|
||||
padding: 0;
|
||||
border: 8px solid #333;
|
||||
box-shadow:
|
||||
0 10px 0 #1a1a1a,
|
||||
0 20px 0 #0a0a0a,
|
||||
0 20px 40px rgba(0,0,0,0.8);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Control Panel Header */
|
||||
.control-panel {
|
||||
background: linear-gradient(to bottom, #404040, #303030);
|
||||
padding: 20px 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 4px solid #1a1a1a;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#ff6b6b 0,
|
||||
#ff6b6b 20px,
|
||||
#ffcc00 20px,
|
||||
#ffcc00 40px
|
||||
);
|
||||
}
|
||||
|
||||
/* Industrial Switches (Filters) */
|
||||
.filter-bank {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.industrial-switch {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
background: #1a1a1a;
|
||||
border: 3px solid #333;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.industrial-switch:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 0 #000;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.switch-handle {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
width: 34px;
|
||||
height: 28px;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
border: 2px solid #222;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
}
|
||||
|
||||
.industrial-switch.active .switch-handle {
|
||||
left: 39px;
|
||||
background: linear-gradient(to bottom, #ff6b6b, #e55a5a);
|
||||
box-shadow: 0 0 10px #ff6b6b;
|
||||
}
|
||||
|
||||
/* Control Buttons */
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.brutalist-button {
|
||||
background: #333;
|
||||
border: 3px solid #444;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.brutalist-button:hover {
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 2px 2px 0 #ff6b6b;
|
||||
}
|
||||
|
||||
.brutalist-button:active {
|
||||
transform: translate(0, 0);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.brutalist-button.refresh {
|
||||
background: #444;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.brutalist-button.export {
|
||||
background: #3a3a3a;
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.brutalist-button.settings {
|
||||
background: #383838;
|
||||
border-color: #484848;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
/* Main Dashboard Content */
|
||||
.dashboard-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
/* Chart Area - Concrete Blocks */
|
||||
.chart-container {
|
||||
background: #1a1a1a;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.concrete-chart {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-block {
|
||||
flex: 1;
|
||||
background: linear-gradient(to bottom, #4a4a4a, #3a3a3a);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
border: 4px solid #2a2a2a;
|
||||
box-shadow:
|
||||
inset 0 -4px 0 #1a1a1a,
|
||||
0 4px 0 #0a0a0a;
|
||||
}
|
||||
|
||||
.chart-block:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow:
|
||||
inset 0 -4px 0 #1a1a1a,
|
||||
0 14px 0 #0a0a0a,
|
||||
0 14px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.chart-block::before {
|
||||
content: attr(data-value);
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 900;
|
||||
font-size: 18px;
|
||||
color: #ff6b6b;
|
||||
text-shadow: 2px 2px 0 #000;
|
||||
}
|
||||
|
||||
.chart-block::after {
|
||||
content: attr(data-label);
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 700;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Grid Overlay */
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
repeating-linear-gradient(0deg, transparent, transparent 39px, #2a2a2a 39px, #2a2a2a 40px),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 19.5%, #2a2a2a 19.5%, #2a2a2a 20%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Side Panel - Settings & Alerts */
|
||||
.side-panel {
|
||||
background: #262626;
|
||||
border-left: 4px solid #1a1a1a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Settings Panel */
|
||||
.settings-panel {
|
||||
padding: 30px 20px;
|
||||
border-bottom: 4px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-weight: 900;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.brutalist-slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: #1a1a1a;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.brutalist-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
border: 3px solid #222;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.brutalist-slider::-moz-range-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: linear-gradient(to bottom, #666, #444);
|
||||
border: 3px solid #222;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.brutalist-select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: #1a1a1a;
|
||||
border: 3px solid #333;
|
||||
color: #999;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Alert System */
|
||||
.alert-panel {
|
||||
flex: 1;
|
||||
padding: 30px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alert-lights {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.alert-indicator {
|
||||
aspect-ratio: 1;
|
||||
background: #1a1a1a;
|
||||
border: 4px solid #2a2a2a;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.alert-indicator::before {
|
||||
content: attr(data-alert);
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #555;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert-indicator.active {
|
||||
animation: alertPulse 1s infinite;
|
||||
background: #ff6b6b;
|
||||
border-color: #ff4444;
|
||||
box-shadow:
|
||||
0 0 20px #ff6b6b,
|
||||
inset 0 0 20px rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.alert-indicator.warning {
|
||||
background: #ffcc00;
|
||||
border-color: #ffaa00;
|
||||
box-shadow:
|
||||
0 0 20px #ffcc00,
|
||||
inset 0 0 20px rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
@keyframes alertPulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.8; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
/* Export Blueprint Overlay */
|
||||
.blueprint-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.95);
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
padding: 40px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.blueprint-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.blueprint-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: #001a33;
|
||||
border: 4px solid #003366;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blueprint-grid {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(0deg, transparent, transparent 19px, #003366 19px, #003366 20px),
|
||||
repeating-linear-gradient(90deg, transparent, transparent 19px, #003366 19px, #003366 20px);
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.blueprint-title {
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
color: #66ccff;
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 2px 2px 0 #003366;
|
||||
}
|
||||
|
||||
.blueprint-data {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #66ccff;
|
||||
line-height: 1.8;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.blueprint-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #003366;
|
||||
border: 3px solid #66ccff;
|
||||
color: #66ccff;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.blueprint-close:hover {
|
||||
background: #66ccff;
|
||||
color: #001a33;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Rotating Refresh Animation */
|
||||
@keyframes concreteRotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.brutalist-button.refresh.rotating {
|
||||
animation: concreteRotate 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
border-left: none;
|
||||
border-top: 4px solid #1a1a1a;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
border-right: 4px solid #1a1a1a;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filter-bank {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.concrete-chart {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Dashboard Widget - Architectural Brutalism Theme</h1>
|
||||
|
||||
<div class="hybrid-component">
|
||||
<!-- Control Panel with Filters and Actions -->
|
||||
<div class="control-panel">
|
||||
<div class="filter-bank">
|
||||
<div class="industrial-switch" data-filter="revenue">
|
||||
<span class="switch-label">Revenue</span>
|
||||
<div class="switch-handle"></div>
|
||||
</div>
|
||||
<div class="industrial-switch" data-filter="users">
|
||||
<span class="switch-label">Users</span>
|
||||
<div class="switch-handle"></div>
|
||||
</div>
|
||||
<div class="industrial-switch" data-filter="traffic">
|
||||
<span class="switch-label">Traffic</span>
|
||||
<div class="switch-handle"></div>
|
||||
</div>
|
||||
<div class="industrial-switch" data-filter="sales">
|
||||
<span class="switch-label">Sales</span>
|
||||
<div class="switch-handle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-buttons">
|
||||
<button class="brutalist-button refresh">↻ Refresh</button>
|
||||
<button class="brutalist-button export">⬇ Export</button>
|
||||
<button class="brutalist-button settings">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Dashboard Content -->
|
||||
<div class="dashboard-content">
|
||||
<!-- Chart Container with Concrete Blocks -->
|
||||
<div class="chart-container">
|
||||
<div class="concrete-chart" id="chart">
|
||||
<div class="chart-block" data-value="87%" data-label="Jan" style="height: 87%"></div>
|
||||
<div class="chart-block" data-value="92%" data-label="Feb" style="height: 92%"></div>
|
||||
<div class="chart-block" data-value="76%" data-label="Mar" style="height: 76%"></div>
|
||||
<div class="chart-block" data-value="84%" data-label="Apr" style="height: 84%"></div>
|
||||
<div class="chart-block" data-value="95%" data-label="May" style="height: 95%"></div>
|
||||
<div class="chart-block" data-value="79%" data-label="Jun" style="height: 79%"></div>
|
||||
</div>
|
||||
<div class="grid-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- Side Panel with Settings and Alerts -->
|
||||
<div class="side-panel">
|
||||
<!-- Settings Panel -->
|
||||
<div class="settings-panel">
|
||||
<h3 class="panel-title">Control Infrastructure</h3>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Update Frequency</label>
|
||||
<input type="range" class="brutalist-slider" min="1" max="60" value="30">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Data Granularity</label>
|
||||
<select class="brutalist-select">
|
||||
<option>Hourly</option>
|
||||
<option>Daily</option>
|
||||
<option selected>Monthly</option>
|
||||
<option>Yearly</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">Alert Threshold</label>
|
||||
<input type="range" class="brutalist-slider" min="0" max="100" value="75">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Panel -->
|
||||
<div class="alert-panel">
|
||||
<h3 class="panel-title">Warning System</h3>
|
||||
|
||||
<div class="alert-lights">
|
||||
<div class="alert-indicator" data-alert="CPU Load"></div>
|
||||
<div class="alert-indicator active" data-alert="Memory"></div>
|
||||
<div class="alert-indicator" data-alert="Network"></div>
|
||||
<div class="alert-indicator warning" data-alert="Storage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blueprint Export Overlay -->
|
||||
<div class="blueprint-overlay" id="blueprintOverlay">
|
||||
<div class="blueprint-content">
|
||||
<div class="blueprint-grid"></div>
|
||||
<button class="blueprint-close" onclick="closeBlueprint()">×</button>
|
||||
<h2 class="blueprint-title">System Architecture Blueprint</h2>
|
||||
<pre class="blueprint-data" id="blueprintData"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Industrial Switch Functionality
|
||||
const switches = document.querySelectorAll('.industrial-switch');
|
||||
const chartBlocks = document.querySelectorAll('.chart-block');
|
||||
|
||||
switches.forEach(switchEl => {
|
||||
switchEl.addEventListener('click', function() {
|
||||
this.classList.toggle('active');
|
||||
updateChartData();
|
||||
});
|
||||
});
|
||||
|
||||
// Chart Block Interactions
|
||||
chartBlocks.forEach(block => {
|
||||
block.addEventListener('click', function() {
|
||||
// Animate block
|
||||
this.style.transition = 'all 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55)';
|
||||
this.style.transform = 'translateY(-20px) scale(1.05)';
|
||||
|
||||
setTimeout(() => {
|
||||
this.style.transform = 'translateY(0) scale(1)';
|
||||
}, 600);
|
||||
|
||||
// Flash value
|
||||
const value = this.getAttribute('data-value');
|
||||
this.setAttribute('data-value', '▓▓▓');
|
||||
setTimeout(() => {
|
||||
this.setAttribute('data-value', value);
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
// Refresh Functionality
|
||||
const refreshBtn = document.querySelector('.brutalist-button.refresh');
|
||||
refreshBtn.addEventListener('click', function() {
|
||||
this.classList.add('rotating');
|
||||
|
||||
// Simulate data refresh
|
||||
setTimeout(() => {
|
||||
updateChartData();
|
||||
this.classList.remove('rotating');
|
||||
|
||||
// Flash all blocks
|
||||
chartBlocks.forEach((block, index) => {
|
||||
setTimeout(() => {
|
||||
const newHeight = Math.floor(Math.random() * 30) + 70;
|
||||
block.style.height = newHeight + '%';
|
||||
block.setAttribute('data-value', newHeight + '%');
|
||||
}, index * 100);
|
||||
});
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Export Blueprint Functionality
|
||||
const exportBtn = document.querySelector('.brutalist-button.export');
|
||||
exportBtn.addEventListener('click', function() {
|
||||
generateBlueprint();
|
||||
document.getElementById('blueprintOverlay').classList.add('active');
|
||||
});
|
||||
|
||||
function generateBlueprint() {
|
||||
const activeFilters = Array.from(switches)
|
||||
.filter(s => s.classList.contains('active'))
|
||||
.map(s => s.getAttribute('data-filter'));
|
||||
|
||||
const chartData = Array.from(chartBlocks).map(block => ({
|
||||
label: block.getAttribute('data-label'),
|
||||
value: block.getAttribute('data-value')
|
||||
}));
|
||||
|
||||
const blueprint = `DASHBOARD CONFIGURATION BLUEPRINT
|
||||
===============================================
|
||||
Generated: ${new Date().toISOString()}
|
||||
Version: 1.0.0
|
||||
|
||||
ACTIVE FILTERS:
|
||||
${activeFilters.length ? activeFilters.map(f => ` - ${f.toUpperCase()}`).join('\n') : ' - NONE'}
|
||||
|
||||
CHART DATA STRUCTURE:
|
||||
${chartData.map(d => ` ${d.label}: ${d.value}`).join('\n')}
|
||||
|
||||
SYSTEM ALERTS:
|
||||
- CPU Load: NORMAL
|
||||
- Memory: CRITICAL
|
||||
- Network: NORMAL
|
||||
- Storage: WARNING
|
||||
|
||||
SETTINGS:
|
||||
- Update Frequency: 30s
|
||||
- Data Granularity: MONTHLY
|
||||
- Alert Threshold: 75%
|
||||
|
||||
BUILD INSTRUCTIONS:
|
||||
1. Initialize concrete block rendering engine
|
||||
2. Configure industrial switch matrix
|
||||
3. Establish alert monitoring pipeline
|
||||
4. Activate data refresh mechanism
|
||||
5. Enable blueprint export system
|
||||
|
||||
END BLUEPRINT
|
||||
===============================================`;
|
||||
|
||||
document.getElementById('blueprintData').textContent = blueprint;
|
||||
}
|
||||
|
||||
function closeBlueprint() {
|
||||
document.getElementById('blueprintOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
// Settings Panel Interactions
|
||||
const sliders = document.querySelectorAll('.brutalist-slider');
|
||||
sliders.forEach(slider => {
|
||||
slider.addEventListener('input', function() {
|
||||
// Visual feedback
|
||||
const percent = (this.value - this.min) / (this.max - this.min);
|
||||
this.style.background = `linear-gradient(to right, #ff6b6b 0%, #ff6b6b ${percent * 100}%, #1a1a1a ${percent * 100}%, #1a1a1a 100%)`;
|
||||
});
|
||||
});
|
||||
|
||||
// Alert System
|
||||
const alertIndicators = document.querySelectorAll('.alert-indicator');
|
||||
|
||||
// Simulate random alerts
|
||||
setInterval(() => {
|
||||
const randomAlert = alertIndicators[Math.floor(Math.random() * alertIndicators.length)];
|
||||
const alertType = Math.random() > 0.5 ? 'active' : 'warning';
|
||||
|
||||
// Clear other alerts occasionally
|
||||
if (Math.random() > 0.7) {
|
||||
alertIndicators.forEach(alert => {
|
||||
alert.classList.remove('active', 'warning');
|
||||
});
|
||||
}
|
||||
|
||||
// Set new alert
|
||||
randomAlert.classList.add(alertType);
|
||||
}, 5000);
|
||||
|
||||
// Update chart based on active filters
|
||||
function updateChartData() {
|
||||
const activeFilters = Array.from(switches)
|
||||
.filter(s => s.classList.contains('active'))
|
||||
.map(s => s.getAttribute('data-filter'));
|
||||
|
||||
// Simulate different data based on filters
|
||||
chartBlocks.forEach((block, index) => {
|
||||
let baseValue = parseInt(block.style.height);
|
||||
|
||||
activeFilters.forEach(filter => {
|
||||
switch(filter) {
|
||||
case 'revenue':
|
||||
baseValue += Math.random() * 10 - 5;
|
||||
break;
|
||||
case 'users':
|
||||
baseValue += Math.random() * 15 - 7.5;
|
||||
break;
|
||||
case 'traffic':
|
||||
baseValue += Math.random() * 20 - 10;
|
||||
break;
|
||||
case 'sales':
|
||||
baseValue += Math.random() * 12 - 6;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
baseValue = Math.max(20, Math.min(100, baseValue));
|
||||
block.style.height = Math.floor(baseValue) + '%';
|
||||
block.setAttribute('data-value', Math.floor(baseValue) + '%');
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'r' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
refreshBtn.click();
|
||||
} else if (e.key === 'e' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
exportBtn.click();
|
||||
} else if (e.key === 'Escape') {
|
||||
closeBlueprint();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize sliders with gradient
|
||||
sliders.forEach(slider => {
|
||||
slider.dispatchEvent(new Event('input'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,855 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ocean Depths Advanced Search - Themed Hybrid UI #11</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(180deg, #001f3f 0%, #003366 20%, #004080 50%, #000033 100%);
|
||||
min-height: 100vh;
|
||||
color: #e0f7fa;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Animated bubbles background */
|
||||
.bubble {
|
||||
position: absolute;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0.1) 100%);
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
animation: rise 10s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
transform: translateY(100vh) translateX(0) scale(1);
|
||||
}
|
||||
to {
|
||||
transform: translateY(-100px) translateX(100px) scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Water ripple effect */
|
||||
.water-ripple {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(transparent 30%, rgba(0,100,200,0.1) 50%, transparent 70%);
|
||||
animation: ripple 8s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(20px); }
|
||||
}
|
||||
|
||||
/* Main container */
|
||||
.ocean-depths-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Title with bioluminescence */
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 3em;
|
||||
margin-bottom: 40px;
|
||||
text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff;
|
||||
animation: glow 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
from { text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff; }
|
||||
to { text-shadow: 0 0 30px #00ffff, 0 0 50px #00ffff, 0 0 70px #00ffff; }
|
||||
}
|
||||
|
||||
/* Periscope Search Bar */
|
||||
.periscope-search {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 40px;
|
||||
}
|
||||
|
||||
.periscope-lens {
|
||||
position: absolute;
|
||||
left: -60px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: radial-gradient(circle, #1a5490 0%, #0d47a1 50%, #001f3f 100%);
|
||||
border-radius: 50%;
|
||||
border: 3px solid #00acc1;
|
||||
box-shadow: 0 0 20px rgba(0,172,193,0.5);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 20px 60px 20px 20px;
|
||||
font-size: 1.2em;
|
||||
background: rgba(0,50,100,0.8);
|
||||
border: 2px solid #00acc1;
|
||||
border-radius: 50px;
|
||||
color: #e0f7fa;
|
||||
box-shadow: inset 0 2px 10px rgba(0,0,0,0.5), 0 0 20px rgba(0,172,193,0.3);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 2px 10px rgba(0,0,0,0.5), 0 0 30px rgba(0,255,255,0.5);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 15px 25px;
|
||||
background: linear-gradient(135deg, #00acc1 0%, #0277bd 100%);
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background: linear-gradient(135deg, #00bcd4 0%, #0288d1 100%);
|
||||
box-shadow: 0 0 20px rgba(0,188,212,0.5);
|
||||
}
|
||||
|
||||
/* Control Panel with Valves */
|
||||
.control-panel {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Filter Valves */
|
||||
.filter-valve {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.valve-wheel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle, #1565c0 0%, #0d47a1 50%, #001f3f 100%);
|
||||
border-radius: 50%;
|
||||
border: 4px solid #00acc1;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.valve-wheel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
background: #00acc1;
|
||||
box-shadow: 0 0 10px rgba(0,172,193,0.5);
|
||||
}
|
||||
|
||||
.valve-wheel::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
width: 80%;
|
||||
height: 4px;
|
||||
background: #00acc1;
|
||||
box-shadow: 0 0 10px rgba(0,172,193,0.5);
|
||||
}
|
||||
|
||||
.valve-wheel.active {
|
||||
transform: rotate(45deg);
|
||||
border-color: #00ffff;
|
||||
box-shadow: 0 5px 30px rgba(0,255,255,0.5);
|
||||
}
|
||||
|
||||
.valve-label {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
color: #b3e5fc;
|
||||
}
|
||||
|
||||
/* Sonar Controls (Advanced Operators) */
|
||||
.sonar-controls {
|
||||
background: rgba(0,50,100,0.6);
|
||||
border: 2px solid #00acc1;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 40px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.sonar-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.3em;
|
||||
color: #00ffff;
|
||||
text-shadow: 0 0 10px rgba(0,255,255,0.5);
|
||||
}
|
||||
|
||||
.operator-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.operator-btn {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #004080 0%, #002050 100%);
|
||||
border: 1px solid #00acc1;
|
||||
border-radius: 25px;
|
||||
color: #b3e5fc;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.operator-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(0,255,255,0.3);
|
||||
transition: width 0.3s, height 0.3s;
|
||||
}
|
||||
|
||||
.operator-btn:hover::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.operator-btn:hover {
|
||||
border-color: #00ffff;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 0 15px rgba(0,255,255,0.5);
|
||||
}
|
||||
|
||||
/* Autocomplete Bubbles */
|
||||
.autocomplete-bubbles {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autocomplete-bubbles.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bubble-suggestion {
|
||||
background: rgba(0,100,200,0.8);
|
||||
border: 1px solid #00acc1;
|
||||
border-radius: 30px;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
animation: bubbleIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes bubbleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.bubble-suggestion:hover {
|
||||
background: rgba(0,150,255,0.9);
|
||||
transform: translateX(10px);
|
||||
box-shadow: 0 0 20px rgba(0,255,255,0.3);
|
||||
}
|
||||
|
||||
/* Ship's Log (History) */
|
||||
.ships-log {
|
||||
background: rgba(0,50,100,0.6);
|
||||
border: 2px solid #00acc1;
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
margin-bottom: 40px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.log-title {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 20px;
|
||||
color: #00ffff;
|
||||
text-shadow: 0 0 10px rgba(0,255,255,0.5);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
background: rgba(0,30,60,0.5);
|
||||
border-left: 3px solid #00acc1;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background: rgba(0,50,100,0.7);
|
||||
border-left-color: #00ffff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-size: 0.8em;
|
||||
color: #80deea;
|
||||
}
|
||||
|
||||
/* Treasure Chest (Saved Searches) */
|
||||
.treasure-chest {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 80px;
|
||||
height: 60px;
|
||||
cursor: pointer;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.chest-body {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #8b6914 0%, #cdaa3d 50%, #8b6914 100%);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 20px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.chest-lid {
|
||||
position: absolute;
|
||||
bottom: 35px;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
background: linear-gradient(135deg, #cdaa3d 0%, #eedc82 50%, #cdaa3d 100%);
|
||||
border-radius: 5px 5px 0 0;
|
||||
transform-origin: bottom;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.treasure-chest:hover .chest-lid {
|
||||
transform: rotateX(-45deg);
|
||||
}
|
||||
|
||||
.saved-searches {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
right: 20px;
|
||||
background: rgba(0,50,100,0.9);
|
||||
border: 2px solid #cdaa3d;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.saved-searches.open {
|
||||
display: block;
|
||||
animation: treasureOpen 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes treasureOpen {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fish School Results */
|
||||
.results-ocean {
|
||||
min-height: 400px;
|
||||
position: relative;
|
||||
background: rgba(0,30,60,0.3);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fish-result {
|
||||
background: linear-gradient(135deg, #004080 0%, #0066cc 100%);
|
||||
border: 1px solid #00acc1;
|
||||
border-radius: 50px 20px 20px 50px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
animation: swim 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes swim {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
25% { transform: translateX(10px) rotate(1deg); }
|
||||
50% { transform: translateX(-10px) rotate(-1deg); }
|
||||
75% { transform: translateX(5px) rotate(0.5deg); }
|
||||
}
|
||||
|
||||
.fish-result::before {
|
||||
content: '>';
|
||||
position: absolute;
|
||||
right: -15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 2em;
|
||||
color: #004080;
|
||||
}
|
||||
|
||||
.fish-result:hover {
|
||||
background: linear-gradient(135deg, #0066cc 0%, #0099ff 100%);
|
||||
transform: translateX(20px);
|
||||
box-shadow: 0 0 30px rgba(0,153,255,0.3);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 10px;
|
||||
color: #00ffff;
|
||||
}
|
||||
|
||||
.result-description {
|
||||
color: #b3e5fc;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Pressure Gauge */
|
||||
.pressure-gauge {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle, #001f3f 0%, #000033 100%);
|
||||
border-radius: 50%;
|
||||
border: 3px solid #00acc1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 20px rgba(0,172,193,0.5);
|
||||
}
|
||||
|
||||
.gauge-needle {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 40px;
|
||||
background: #ff0000;
|
||||
bottom: 50%;
|
||||
left: 50%;
|
||||
transform-origin: bottom;
|
||||
transform: translateX(-50%) rotate(0deg);
|
||||
transition: transform 0.5s ease;
|
||||
box-shadow: 0 0 10px rgba(255,0,0,0.5);
|
||||
}
|
||||
|
||||
.gauge-center {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: #00acc1;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.title {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-valve {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.periscope-lens {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 15px 50px 15px 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="water-ripple"></div>
|
||||
|
||||
<!-- Animated bubbles -->
|
||||
<div id="bubbleContainer"></div>
|
||||
|
||||
<div class="ocean-depths-container">
|
||||
<h1 class="title">Ocean Depths Search</h1>
|
||||
|
||||
<!-- Periscope Search Bar -->
|
||||
<div class="periscope-search">
|
||||
<div class="periscope-lens"></div>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Explore the depths...">
|
||||
<button class="search-button" onclick="performSearch()">Dive</button>
|
||||
|
||||
<!-- Autocomplete Bubbles -->
|
||||
<div class="autocomplete-bubbles" id="autocompleteBubbles"></div>
|
||||
</div>
|
||||
|
||||
<!-- Control Panel with Filter Valves -->
|
||||
<div class="control-panel">
|
||||
<div class="filter-valve">
|
||||
<div class="valve-wheel" onclick="toggleFilter(this, 'recent')" data-filter="recent"></div>
|
||||
<div class="valve-label">Recent</div>
|
||||
</div>
|
||||
<div class="filter-valve">
|
||||
<div class="valve-wheel" onclick="toggleFilter(this, 'deep')" data-filter="deep"></div>
|
||||
<div class="valve-label">Deep Search</div>
|
||||
</div>
|
||||
<div class="filter-valve">
|
||||
<div class="valve-wheel" onclick="toggleFilter(this, 'images')" data-filter="images"></div>
|
||||
<div class="valve-label">Images</div>
|
||||
</div>
|
||||
<div class="filter-valve">
|
||||
<div class="valve-wheel" onclick="toggleFilter(this, 'videos')" data-filter="videos"></div>
|
||||
<div class="valve-label">Videos</div>
|
||||
</div>
|
||||
<div class="filter-valve">
|
||||
<div class="valve-wheel" onclick="toggleFilter(this, 'archived')" data-filter="archived"></div>
|
||||
<div class="valve-label">Archived</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sonar Controls (Advanced Operators) -->
|
||||
<div class="sonar-controls">
|
||||
<div class="sonar-title">Sonar Operations</div>
|
||||
<div class="operator-buttons">
|
||||
<button class="operator-btn" onclick="addOperator('AND')">AND</button>
|
||||
<button class="operator-btn" onclick="addOperator('OR')">OR</button>
|
||||
<button class="operator-btn" onclick="addOperator('NOT')">NOT</button>
|
||||
<button class="operator-btn" onclick="addOperator('*')">Wildcard</button>
|
||||
<button class="operator-btn" onclick="addOperator('\"\"')">Exact</button>
|
||||
<button class="operator-btn" onclick="addOperator('~')">Fuzzy</button>
|
||||
<button class="operator-btn" onclick="addOperator('^')">Boost</button>
|
||||
<button class="operator-btn" onclick="addOperator('[]')">Range</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ship's Log (History) -->
|
||||
<div class="ships-log">
|
||||
<div class="log-title">Ship's Log</div>
|
||||
<div id="searchHistory"></div>
|
||||
</div>
|
||||
|
||||
<!-- Results Ocean -->
|
||||
<div class="results-ocean" id="resultsOcean">
|
||||
<div class="fish-result">
|
||||
<div class="result-title">Deep Sea Discovery</div>
|
||||
<div class="result-description">Begin your exploration of the ocean depths...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Treasure Chest (Saved Searches) -->
|
||||
<div class="treasure-chest" onclick="toggleSavedSearches()">
|
||||
<div class="chest-lid"></div>
|
||||
<div class="chest-body"></div>
|
||||
</div>
|
||||
|
||||
<div class="saved-searches" id="savedSearches">
|
||||
<h3 style="color: #cdaa3d; margin-bottom: 15px;">Treasured Searches</h3>
|
||||
<div id="savedSearchesList"></div>
|
||||
<button class="operator-btn" style="margin-top: 10px;" onclick="saveCurrentSearch()">Save Current</button>
|
||||
</div>
|
||||
|
||||
<!-- Pressure Gauge -->
|
||||
<div class="pressure-gauge">
|
||||
<div class="gauge-needle" id="pressureNeedle"></div>
|
||||
<div class="gauge-center"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize bubbles
|
||||
function createBubbles() {
|
||||
const container = document.getElementById('bubbleContainer');
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble';
|
||||
bubble.style.width = Math.random() * 40 + 10 + 'px';
|
||||
bubble.style.height = bubble.style.width;
|
||||
bubble.style.left = Math.random() * 100 + '%';
|
||||
bubble.style.animationDelay = Math.random() * 10 + 's';
|
||||
bubble.style.animationDuration = Math.random() * 10 + 10 + 's';
|
||||
container.appendChild(bubble);
|
||||
}
|
||||
}
|
||||
|
||||
// Active filters
|
||||
let activeFilters = new Set();
|
||||
|
||||
// Search history
|
||||
let searchHistory = [];
|
||||
|
||||
// Saved searches
|
||||
let savedSearches = JSON.parse(localStorage.getItem('oceanSearches') || '[]');
|
||||
|
||||
// Toggle filter
|
||||
function toggleFilter(element, filter) {
|
||||
element.classList.toggle('active');
|
||||
if (activeFilters.has(filter)) {
|
||||
activeFilters.delete(filter);
|
||||
} else {
|
||||
activeFilters.add(filter);
|
||||
}
|
||||
updatePressure();
|
||||
}
|
||||
|
||||
// Add operator to search
|
||||
function addOperator(operator) {
|
||||
const input = document.getElementById('searchInput');
|
||||
if (operator === '\"\"') {
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
const text = input.value;
|
||||
input.value = text.substring(0, start) + '\"' + text.substring(start, end) + '\"' + text.substring(end);
|
||||
input.focus();
|
||||
input.setSelectionRange(end + 2, end + 2);
|
||||
} else if (operator === '[]') {
|
||||
input.value += ' [TO ]';
|
||||
input.focus();
|
||||
input.setSelectionRange(input.value.length - 4, input.value.length - 4);
|
||||
} else {
|
||||
input.value += ' ' + operator + ' ';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Autocomplete functionality
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const autocompleteBubbles = document.getElementById('autocompleteBubbles');
|
||||
const suggestions = [
|
||||
'coral reefs ecosystem',
|
||||
'deep sea creatures',
|
||||
'ocean currents patterns',
|
||||
'marine biodiversity',
|
||||
'underwater volcanoes',
|
||||
'bioluminescent organisms',
|
||||
'ocean floor mapping',
|
||||
'submarine canyons',
|
||||
'hydrothermal vents',
|
||||
'abyssal plains'
|
||||
];
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const value = this.value.toLowerCase();
|
||||
if (value.length > 2) {
|
||||
const matches = suggestions.filter(s => s.toLowerCase().includes(value));
|
||||
showAutocomplete(matches);
|
||||
} else {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
function showAutocomplete(matches) {
|
||||
autocompleteBubbles.innerHTML = '';
|
||||
if (matches.length > 0) {
|
||||
autocompleteBubbles.classList.add('active');
|
||||
matches.slice(0, 5).forEach((match, index) => {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'bubble-suggestion';
|
||||
bubble.textContent = match;
|
||||
bubble.style.animationDelay = index * 0.1 + 's';
|
||||
bubble.onclick = () => {
|
||||
searchInput.value = match;
|
||||
hideAutocomplete();
|
||||
};
|
||||
autocompleteBubbles.appendChild(bubble);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hideAutocomplete() {
|
||||
autocompleteBubbles.classList.remove('active');
|
||||
}
|
||||
|
||||
// Perform search
|
||||
function performSearch() {
|
||||
const query = searchInput.value;
|
||||
if (query.trim()) {
|
||||
// Add to history
|
||||
addToHistory(query);
|
||||
|
||||
// Generate results
|
||||
generateResults(query);
|
||||
|
||||
// Update pressure
|
||||
updatePressure();
|
||||
|
||||
// Hide autocomplete
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
|
||||
// Add to history
|
||||
function addToHistory(query) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
searchHistory.unshift({ query, timestamp });
|
||||
if (searchHistory.length > 10) searchHistory.pop();
|
||||
updateHistoryDisplay();
|
||||
}
|
||||
|
||||
// Update history display
|
||||
function updateHistoryDisplay() {
|
||||
const historyContainer = document.getElementById('searchHistory');
|
||||
historyContainer.innerHTML = '';
|
||||
searchHistory.forEach(entry => {
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = 'log-entry';
|
||||
logEntry.innerHTML = `
|
||||
<div>${entry.query}</div>
|
||||
<div class="log-timestamp">${entry.timestamp}</div>
|
||||
`;
|
||||
logEntry.onclick = () => {
|
||||
searchInput.value = entry.query;
|
||||
performSearch();
|
||||
};
|
||||
historyContainer.appendChild(logEntry);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate results
|
||||
function generateResults(query) {
|
||||
const resultsOcean = document.getElementById('resultsOcean');
|
||||
resultsOcean.innerHTML = '';
|
||||
|
||||
const sampleResults = [
|
||||
{ title: 'Deep Ocean Mysteries', description: 'Exploring the unknown depths where light cannot reach...' },
|
||||
{ title: 'Bioluminescent Wonders', description: 'Creatures that create their own light in the darkness...' },
|
||||
{ title: 'Underwater Canyons', description: 'Massive geological formations carved by ancient currents...' },
|
||||
{ title: 'Marine Life Adaptations', description: 'How organisms survive extreme pressure and darkness...' },
|
||||
{ title: 'Ocean Current Patterns', description: 'The invisible rivers that flow through our seas...' }
|
||||
];
|
||||
|
||||
sampleResults.forEach((result, index) => {
|
||||
const fishResult = document.createElement('div');
|
||||
fishResult.className = 'fish-result';
|
||||
fishResult.style.animationDelay = index * 2 + 's';
|
||||
fishResult.innerHTML = `
|
||||
<div class="result-title">${result.title}</div>
|
||||
<div class="result-description">${result.description}</div>
|
||||
`;
|
||||
resultsOcean.appendChild(fishResult);
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle saved searches
|
||||
function toggleSavedSearches() {
|
||||
const savedSearchesPanel = document.getElementById('savedSearches');
|
||||
savedSearchesPanel.classList.toggle('open');
|
||||
if (savedSearchesPanel.classList.contains('open')) {
|
||||
updateSavedSearchesList();
|
||||
}
|
||||
}
|
||||
|
||||
// Save current search
|
||||
function saveCurrentSearch() {
|
||||
const query = searchInput.value;
|
||||
if (query.trim() && !savedSearches.includes(query)) {
|
||||
savedSearches.push(query);
|
||||
localStorage.setItem('oceanSearches', JSON.stringify(savedSearches));
|
||||
updateSavedSearchesList();
|
||||
}
|
||||
}
|
||||
|
||||
// Update saved searches list
|
||||
function updateSavedSearchesList() {
|
||||
const list = document.getElementById('savedSearchesList');
|
||||
list.innerHTML = '';
|
||||
savedSearches.forEach(search => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'log-entry';
|
||||
item.textContent = search;
|
||||
item.onclick = () => {
|
||||
searchInput.value = search;
|
||||
performSearch();
|
||||
toggleSavedSearches();
|
||||
};
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Update pressure gauge
|
||||
function updatePressure() {
|
||||
const needle = document.getElementById('pressureNeedle');
|
||||
const pressure = (activeFilters.size * 15) + (searchHistory.length * 5);
|
||||
needle.style.transform = `translateX(-50%) rotate(${Math.min(pressure, 180)}deg)`;
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
performSearch();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
createBubbles();
|
||||
updateSavedSearchesList();
|
||||
|
||||
// Click outside to close autocomplete
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !autocompleteBubbles.contains(e.target)) {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,810 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Art Deco Profile Dashboard - Iteration 12</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Bebas+Neue&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Playfair Display', serif;
|
||||
background: #0a0a0a;
|
||||
color: #f4e7d1;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Art Deco Background Pattern */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgba(212, 175, 55, 0.03) 10px,
|
||||
rgba(212, 175, 55, 0.03) 20px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
-45deg,
|
||||
transparent,
|
||||
transparent 10px,
|
||||
rgba(212, 175, 55, 0.03) 10px,
|
||||
rgba(212, 175, 55, 0.03) 20px
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
/* Header with Art Deco Frame */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 50px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 4rem;
|
||||
letter-spacing: 0.2em;
|
||||
color: #d4af37;
|
||||
text-shadow:
|
||||
0 0 20px rgba(212, 175, 55, 0.5),
|
||||
2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header-ornament {
|
||||
width: 300px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
#d4af37 20%,
|
||||
#d4af37 80%,
|
||||
transparent 100%
|
||||
);
|
||||
position: relative;
|
||||
clip-path: polygon(
|
||||
0 50%, 10% 0, 90% 0, 100% 50%,
|
||||
90% 100%, 10% 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* Main Grid Layout */
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 350px 1fr;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* Profile Section */
|
||||
.profile-section {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border: 2px solid #d4af37;
|
||||
border-radius: 0;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: repeating-conic-gradient(
|
||||
from 0deg at 50% 50%,
|
||||
transparent 0deg,
|
||||
rgba(212, 175, 55, 0.1) 10deg,
|
||||
transparent 20deg
|
||||
);
|
||||
animation: rotate 60s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Avatar Container */
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 30px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.avatar-frame {
|
||||
position: absolute;
|
||||
inset: -20px;
|
||||
background: conic-gradient(
|
||||
from 45deg,
|
||||
#d4af37 0deg,
|
||||
#f4e7d1 45deg,
|
||||
#d4af37 90deg,
|
||||
#8b6914 135deg,
|
||||
#d4af37 180deg,
|
||||
#f4e7d1 225deg,
|
||||
#d4af37 270deg,
|
||||
#8b6914 315deg,
|
||||
#d4af37 360deg
|
||||
);
|
||||
clip-path: polygon(
|
||||
50% 0%, 100% 25%, 100% 75%, 50% 100%,
|
||||
0% 75%, 0% 25%
|
||||
);
|
||||
animation: shimmer 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
|
||||
clip-path: polygon(
|
||||
50% 0%, 100% 25%, 100% 75%, 50% 100%,
|
||||
0% 75%, 0% 25%
|
||||
);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 4rem;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-size: 1.8rem;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #f4e7d1;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.profile-title {
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: #d4af37;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Stats as Golden Ratio Meters */
|
||||
.stats-container {
|
||||
margin-top: 40px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.stat-meter {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 8px;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.meter-track {
|
||||
height: 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #d4af37;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meter-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
#8b6914 0%,
|
||||
#d4af37 50%,
|
||||
#f4e7d1 100%
|
||||
);
|
||||
transition: width 1s ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.meter-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: slide 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.meter-value {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
color: #0a0a0a;
|
||||
font-weight: bold;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Main Content Area */
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
/* Activity Feed as Newspaper */
|
||||
.activity-feed {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border: 2px solid #d4af37;
|
||||
padding: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feed-header {
|
||||
border-bottom: 3px double #d4af37;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feed-title {
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 2.5rem;
|
||||
letter-spacing: 0.3em;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.feed-date {
|
||||
font-size: 0.9rem;
|
||||
color: #8b6914;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.feed-columns {
|
||||
column-count: 2;
|
||||
column-gap: 30px;
|
||||
column-rule: 1px solid #d4af37;
|
||||
}
|
||||
|
||||
.feed-item {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
border-left: 3px solid #d4af37;
|
||||
}
|
||||
|
||||
.feed-item-time {
|
||||
font-size: 0.8rem;
|
||||
color: #8b6914;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.feed-item-text {
|
||||
margin-top: 5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Settings Panel */
|
||||
.settings-panel {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border: 2px solid #d4af37;
|
||||
padding: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
padding: 20px;
|
||||
background: rgba(212, 175, 55, 0.05);
|
||||
border: 1px solid #d4af37;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.1em;
|
||||
color: #d4af37;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.luxury-switch {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #d4af37;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.luxury-switch::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
background: #d4af37;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.luxury-switch.active::before {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
.luxury-slider {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #d4af37;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.slider-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #8b6914, #d4af37);
|
||||
width: 50%;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
/* Achievements as Medallions */
|
||||
.achievements-section {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border: 2px solid #d4af37;
|
||||
padding: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.achievements-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.achievements-title {
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 2rem;
|
||||
letter-spacing: 0.2em;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.medallions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.medallion {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.medallion:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.medallion-outer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: conic-gradient(
|
||||
from 0deg,
|
||||
#d4af37 0deg,
|
||||
#f4e7d1 60deg,
|
||||
#d4af37 120deg,
|
||||
#8b6914 180deg,
|
||||
#d4af37 240deg,
|
||||
#f4e7d1 300deg,
|
||||
#d4af37 360deg
|
||||
);
|
||||
border-radius: 50%;
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.medallion-inner {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
font-size: 2rem;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
.medallion-name {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
font-size: 0.8rem;
|
||||
color: #d4af37;
|
||||
}
|
||||
|
||||
/* Connections Constellation */
|
||||
.connections-section {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border: 2px solid #d4af37;
|
||||
padding: 30px;
|
||||
margin-top: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.constellation-canvas {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.connection-node {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #d4af37;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Bebas Neue', cursive;
|
||||
color: #d4af37;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.connection-node:hover {
|
||||
transform: scale(1.2);
|
||||
background: #d4af37;
|
||||
color: #0a0a0a;
|
||||
box-shadow: 0 0 30px rgba(212, 175, 55, 0.8);
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
position: absolute;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
#d4af37 50%,
|
||||
transparent 100%
|
||||
);
|
||||
transform-origin: left center;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.main-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.feed-columns {
|
||||
column-count: 1;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="dashboard">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<h1>PROFILE LUXE</h1>
|
||||
<div class="header-ornament"></div>
|
||||
</header>
|
||||
|
||||
<!-- Main Grid -->
|
||||
<div class="main-grid">
|
||||
<!-- Profile Section -->
|
||||
<aside class="profile-section">
|
||||
<!-- Avatar -->
|
||||
<div class="avatar-container">
|
||||
<div class="avatar-frame"></div>
|
||||
<div class="avatar">VL</div>
|
||||
</div>
|
||||
<h2 class="profile-name">Victoria Luxmore</h2>
|
||||
<p class="profile-title">Elite Member Since 1925</p>
|
||||
|
||||
<!-- Stats Meters -->
|
||||
<div class="stats-container">
|
||||
<div class="stat-meter">
|
||||
<div class="stat-label">Prestige Level</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width: 85%">
|
||||
<span class="meter-value">85</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-meter">
|
||||
<div class="stat-label">Social Influence</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width: 72%">
|
||||
<span class="meter-value">72</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-meter">
|
||||
<div class="stat-label">Activity Score</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width: 90%">
|
||||
<span class="meter-value">90</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-meter">
|
||||
<div class="stat-label">Elite Status</div>
|
||||
<div class="meter-track">
|
||||
<div class="meter-fill" style="width: 95%">
|
||||
<span class="meter-value">95</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<!-- Activity Feed -->
|
||||
<section class="activity-feed">
|
||||
<div class="feed-header">
|
||||
<h2 class="feed-title">SOCIETY CHRONICLE</h2>
|
||||
<p class="feed-date">December 6, 1925</p>
|
||||
</div>
|
||||
<div class="feed-columns">
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">10:45 PM</time>
|
||||
<p class="feed-item-text">Attended the exclusive Gatsby soirée at the Plaza. Spectacular evening of jazz and champagne.</p>
|
||||
</article>
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">8:30 PM</time>
|
||||
<p class="feed-item-text">Achieved Diamond Elite status. Privileges now include access to the Penthouse Lounge.</p>
|
||||
</article>
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">6:15 PM</time>
|
||||
<p class="feed-item-text">New connection established with Charleston M. Distinguished member of the Arts Society.</p>
|
||||
</article>
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">4:00 PM</time>
|
||||
<p class="feed-item-text">Portfolio update: Art collection valued at new heights. Monet acquisition confirmed.</p>
|
||||
</article>
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">2:30 PM</time>
|
||||
<p class="feed-item-text">Received invitation to the Annual Metropolis Gala. Black tie mandatory.</p>
|
||||
</article>
|
||||
<article class="feed-item">
|
||||
<time class="feed-item-time">12:00 PM</time>
|
||||
<p class="feed-item-text">Luncheon at the Ritz with fellow Elite members. Discussed upcoming charity auction.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<section class="settings-panel">
|
||||
<div class="control-group">
|
||||
<h3 class="control-label">PRIVACY MODE</h3>
|
||||
<div class="luxury-switch" onclick="toggleSwitch(this)"></div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3 class="control-label">NOTIFICATIONS</h3>
|
||||
<div class="luxury-switch active" onclick="toggleSwitch(this)"></div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3 class="control-label">VISIBILITY</h3>
|
||||
<div class="luxury-slider" onclick="adjustSlider(event, this)">
|
||||
<div class="slider-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<h3 class="control-label">EXCLUSIVITY</h3>
|
||||
<div class="luxury-slider" onclick="adjustSlider(event, this)">
|
||||
<div class="slider-fill" style="width: 80%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Achievements Section -->
|
||||
<section class="achievements-section">
|
||||
<div class="achievements-header">
|
||||
<h2 class="achievements-title">MEDALLIONS OF DISTINCTION</h2>
|
||||
</div>
|
||||
<div class="medallions-grid">
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">★</div>
|
||||
<p class="medallion-name">ELITE STATUS</p>
|
||||
</div>
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">♦</div>
|
||||
<p class="medallion-name">DIAMOND TIER</p>
|
||||
</div>
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">♠</div>
|
||||
<p class="medallion-name">HIGH SOCIETY</p>
|
||||
</div>
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">♣</div>
|
||||
<p class="medallion-name">CLUB MEMBER</p>
|
||||
</div>
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">♥</div>
|
||||
<p class="medallion-name">PHILANTHROPIST</p>
|
||||
</div>
|
||||
<div class="medallion">
|
||||
<div class="medallion-outer"></div>
|
||||
<div class="medallion-inner">⚜</div>
|
||||
<p class="medallion-name">ARISTOCRAT</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Connections Constellation -->
|
||||
<section class="connections-section">
|
||||
<h2 class="achievements-title" style="text-align: center; margin-bottom: 30px;">SOCIAL CONSTELLATION</h2>
|
||||
<div class="constellation-canvas" id="constellation">
|
||||
<!-- Connection nodes will be dynamically positioned -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Toggle luxury switches
|
||||
function toggleSwitch(element) {
|
||||
element.classList.toggle('active');
|
||||
}
|
||||
|
||||
// Adjust luxury sliders
|
||||
function adjustSlider(event, slider) {
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const percentage = ((event.clientX - rect.left) / rect.width) * 100;
|
||||
const fill = slider.querySelector('.slider-fill');
|
||||
fill.style.width = Math.max(0, Math.min(100, percentage)) + '%';
|
||||
}
|
||||
|
||||
// Animate stat meters on load
|
||||
window.addEventListener('load', () => {
|
||||
const meters = document.querySelectorAll('.meter-fill');
|
||||
meters.forEach((meter, index) => {
|
||||
const targetWidth = meter.style.width;
|
||||
meter.style.width = '0%';
|
||||
setTimeout(() => {
|
||||
meter.style.width = targetWidth;
|
||||
}, 100 + index * 200);
|
||||
});
|
||||
});
|
||||
|
||||
// Create connections constellation
|
||||
function createConstellation() {
|
||||
const canvas = document.getElementById('constellation');
|
||||
const connections = [
|
||||
{ id: 'CM', x: 20, y: 30 },
|
||||
{ id: 'JG', x: 80, y: 20 },
|
||||
{ id: 'DF', x: 50, y: 50 },
|
||||
{ id: 'NK', x: 30, y: 70 },
|
||||
{ id: 'TW', x: 70, y: 80 },
|
||||
{ id: 'ES', x: 90, y: 60 },
|
||||
{ id: 'RL', x: 10, y: 50 }
|
||||
];
|
||||
|
||||
// Create nodes
|
||||
connections.forEach(conn => {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'connection-node';
|
||||
node.textContent = conn.id;
|
||||
node.style.left = conn.x + '%';
|
||||
node.style.top = conn.y + '%';
|
||||
node.style.transform = 'translate(-50%, -50%)';
|
||||
canvas.appendChild(node);
|
||||
});
|
||||
|
||||
// Create connection lines
|
||||
for (let i = 0; i < connections.length; i++) {
|
||||
for (let j = i + 1; j < connections.length; j++) {
|
||||
if (Math.random() > 0.5) { // Random connections
|
||||
const line = document.createElement('div');
|
||||
line.className = 'connection-line';
|
||||
|
||||
const x1 = connections[i].x;
|
||||
const y1 = connections[i].y;
|
||||
const x2 = connections[j].x;
|
||||
const y2 = connections[j].y;
|
||||
|
||||
const distance = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
||||
|
||||
line.style.width = distance + '%';
|
||||
line.style.left = x1 + '%';
|
||||
line.style.top = y1 + '%';
|
||||
line.style.transform = `rotate(${angle}deg)`;
|
||||
|
||||
canvas.appendChild(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createConstellation();
|
||||
|
||||
// Add hover effects to medallions
|
||||
document.querySelectorAll('.medallion').forEach(medallion => {
|
||||
medallion.addEventListener('click', function() {
|
||||
this.style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
this.style.animation = '';
|
||||
}, 10);
|
||||
});
|
||||
});
|
||||
|
||||
// Parallax effect for background pattern
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
|
||||
document.body.style.backgroundPosition = `${x * 20}px ${y * 20}px`;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,969 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Steampunk Machinery Communication Hub</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
background: #1a1511;
|
||||
color: #d4af37;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(212, 175, 55, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 50%, rgba(184, 134, 11, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 50% 20%, rgba(218, 165, 32, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #daa520;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.3);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.communication-hub {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 3px solid #8b6914;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
box-shadow:
|
||||
inset 0 0 50px rgba(212, 175, 55, 0.1),
|
||||
0 0 30px rgba(212, 175, 55, 0.2),
|
||||
0 10px 20px rgba(0, 0, 0, 0.5);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative gears */
|
||||
.gear {
|
||||
position: absolute;
|
||||
opacity: 0.1;
|
||||
animation: rotate 20s linear infinite;
|
||||
}
|
||||
|
||||
.gear-1 {
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
animation-duration: 30s;
|
||||
}
|
||||
|
||||
.gear-2 {
|
||||
bottom: -30px;
|
||||
left: -30px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Main layout grid */
|
||||
.hub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr 250px;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Participant Manifest (Left Panel) */
|
||||
.passenger-manifest {
|
||||
background: #1f1611;
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.manifest-header {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
color: #daa520;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.participant {
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 1px solid #8b6914;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.participant:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 0 15px rgba(212, 175, 55, 0.3);
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #8b6914;
|
||||
background: #2a2217;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
color: #daa520;
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
font-weight: 600;
|
||||
color: #daa520;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.participant-status {
|
||||
font-size: 0.8rem;
|
||||
color: #b8860b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.steam-indicator {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #4ade80;
|
||||
box-shadow: 0 0 10px #4ade80;
|
||||
animation: steam-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes steam-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
/* Center Panel */
|
||||
.center-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Projection Apparatus (Screen Share) */
|
||||
.projection-apparatus {
|
||||
background: #0a0908;
|
||||
border: 3px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
height: 400px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 0 50px rgba(0, 0, 0, 0.8),
|
||||
0 0 20px rgba(212, 175, 55, 0.2);
|
||||
}
|
||||
|
||||
.projection-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: radial-gradient(ellipse at center, #1a1511 0%, #0a0908 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.projection-content {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.projection-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
animation: projection-flicker 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes projection-flicker {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.projection-controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Brass Valves (Video Controls) */
|
||||
.brass-valves {
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.valve-control {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.valve-control:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.valve-wheel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, #daa520 0%, #8b6914 50%, #654321 100%);
|
||||
border: 3px solid #654321;
|
||||
position: relative;
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.5),
|
||||
0 5px 10px rgba(0, 0, 0, 0.5);
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.valve-control.active .valve-wheel {
|
||||
transform: rotate(180deg);
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(212, 175, 55, 0.5),
|
||||
0 5px 10px rgba(0, 0, 0, 0.5),
|
||||
0 0 20px rgba(212, 175, 55, 0.5);
|
||||
}
|
||||
|
||||
.valve-spokes {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
}
|
||||
|
||||
.valve-spoke {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 50%;
|
||||
background: #654321;
|
||||
left: 50%;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
.valve-spoke:nth-child(1) { transform: translateX(-50%) rotate(0deg); }
|
||||
.valve-spoke:nth-child(2) { transform: translateX(-50%) rotate(45deg); }
|
||||
.valve-spoke:nth-child(3) { transform: translateX(-50%) rotate(90deg); }
|
||||
.valve-spoke:nth-child(4) { transform: translateX(-50%) rotate(135deg); }
|
||||
|
||||
.valve-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 30% 30%, #daa520 0%, #8b6914 100%);
|
||||
border: 2px solid #654321;
|
||||
}
|
||||
|
||||
.valve-label {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.8rem;
|
||||
color: #b8860b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Right Panel */
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* Phonograph Cylinder (Recording) */
|
||||
.phonograph-cylinder {
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cylinder-mechanism {
|
||||
width: 120px;
|
||||
height: 40px;
|
||||
background: linear-gradient(90deg, #8b6914 0%, #daa520 25%, #8b6914 50%, #daa520 75%, #8b6914 100%);
|
||||
border-radius: 20px;
|
||||
margin: 0 auto 15px;
|
||||
position: relative;
|
||||
box-shadow:
|
||||
inset 0 0 10px rgba(0, 0, 0, 0.5),
|
||||
0 5px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.cylinder-mechanism.recording {
|
||||
animation: cylinder-spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes cylinder-spin {
|
||||
from { background-position: 0 0; }
|
||||
to { background-position: 100px 0; }
|
||||
}
|
||||
|
||||
.record-button {
|
||||
background: radial-gradient(circle at 30% 30%, #dc2626 0%, #991b1b 100%);
|
||||
border: 3px solid #7f1d1d;
|
||||
border-radius: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(0, 0, 0, 0.5),
|
||||
0 5px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.record-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow:
|
||||
inset 0 0 20px rgba(220, 38, 38, 0.5),
|
||||
0 5px 10px rgba(0, 0, 0, 0.5),
|
||||
0 0 20px rgba(220, 38, 38, 0.5);
|
||||
}
|
||||
|
||||
.record-button.recording {
|
||||
animation: record-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes record-pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
/* Steam Puffs (Reactions) */
|
||||
.steam-reactions {
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.reactions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.steam-button {
|
||||
background: radial-gradient(circle at 30% 30%, #654321 0%, #3e2723 100%);
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.steam-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 15px rgba(212, 175, 55, 0.5);
|
||||
}
|
||||
|
||||
.steam-puff {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.steam-button:active .steam-puff {
|
||||
animation: steam-rise 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes steam-rise {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(0) scale(0.5);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-50px) scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pneumatic Tube Messages (Chat) */
|
||||
.pneumatic-chat {
|
||||
grid-column: 1 / -1;
|
||||
background: linear-gradient(145deg, #1f1611, #141210);
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tube-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 15px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.tube-messages::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.tube-messages::-webkit-scrollbar-track {
|
||||
background: #1f1611;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.tube-messages::-webkit-scrollbar-thumb {
|
||||
background: #8b6914;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.message-capsule {
|
||||
background: linear-gradient(145deg, #2a2217, #1f1813);
|
||||
border: 1px solid #8b6914;
|
||||
border-radius: 20px;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
animation: tube-arrive 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes tube-arrive {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px) scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-weight: 600;
|
||||
color: #daa520;
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
color: #d4af37;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 15px;
|
||||
font-size: 0.7rem;
|
||||
color: #8b6914;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tube-input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tube-input {
|
||||
flex: 1;
|
||||
background: #1f1611;
|
||||
border: 2px solid #8b6914;
|
||||
border-radius: 20px;
|
||||
padding: 10px 20px;
|
||||
color: #d4af37;
|
||||
font-family: 'Libre Baskerville', serif;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tube-input:focus {
|
||||
box-shadow: 0 0 15px rgba(212, 175, 55, 0.3);
|
||||
border-color: #daa520;
|
||||
}
|
||||
|
||||
.tube-send {
|
||||
background: radial-gradient(circle at 30% 30%, #8b6914 0%, #654321 100%);
|
||||
border: 2px solid #654321;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #daa520;
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tube-send:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 15px rgba(212, 175, 55, 0.5);
|
||||
}
|
||||
|
||||
/* Decorative elements */
|
||||
.brass-plate {
|
||||
position: absolute;
|
||||
background: linear-gradient(145deg, #daa520 0%, #8b6914 100%);
|
||||
border: 1px solid #654321;
|
||||
border-radius: 5px;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.7rem;
|
||||
color: #1f1611;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.hub-label {
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
/* Gauge decorations */
|
||||
.pressure-gauge {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: radial-gradient(circle at 30% 30%, #2a2217 0%, #1f1813 100%);
|
||||
border: 3px solid #8b6914;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.gauge-needle {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
background: #dc2626;
|
||||
transform-origin: bottom;
|
||||
animation: gauge-wobble 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes gauge-wobble {
|
||||
0%, 100% { transform: rotate(-30deg); }
|
||||
50% { transform: rotate(30deg); }
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 1200px) {
|
||||
.hub-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.passenger-manifest,
|
||||
.right-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Communication Hub - Steampunk Machinery Theme</h1>
|
||||
|
||||
<div class="communication-hub">
|
||||
<!-- Decorative gears -->
|
||||
<svg class="gear gear-1" viewBox="0 0 100 100">
|
||||
<path d="M50 15 L55 5 L45 5 Z M50 85 L55 95 L45 95 Z M85 50 L95 55 L95 45 Z M15 50 L5 55 L5 45 Z" fill="#8b6914"/>
|
||||
<circle cx="50" cy="50" r="35" fill="none" stroke="#8b6914" stroke-width="3"/>
|
||||
<circle cx="50" cy="50" r="20" fill="none" stroke="#8b6914" stroke-width="2"/>
|
||||
</svg>
|
||||
|
||||
<svg class="gear gear-2" viewBox="0 0 100 100">
|
||||
<path d="M50 15 L55 5 L45 5 Z M50 85 L55 95 L45 95 Z M85 50 L95 55 L95 45 Z M15 50 L5 55 L5 45 Z" fill="#8b6914"/>
|
||||
<circle cx="50" cy="50" r="35" fill="none" stroke="#8b6914" stroke-width="3"/>
|
||||
</svg>
|
||||
|
||||
<!-- Brass plate label -->
|
||||
<div class="brass-plate hub-label">Victorian Comm Station</div>
|
||||
|
||||
<!-- Pressure gauge decoration -->
|
||||
<div class="pressure-gauge">
|
||||
<div class="gauge-needle"></div>
|
||||
</div>
|
||||
|
||||
<div class="hub-grid">
|
||||
<!-- Passenger Manifest (Participant List) -->
|
||||
<div class="passenger-manifest">
|
||||
<h2 class="manifest-header">Passenger Manifest</h2>
|
||||
<div class="participant">
|
||||
<div class="participant-avatar">VW</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">Victoria Whitmore</div>
|
||||
<div class="participant-status">Chief Engineer</div>
|
||||
</div>
|
||||
<div class="steam-indicator"></div>
|
||||
</div>
|
||||
<div class="participant">
|
||||
<div class="participant-avatar">AT</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">Augustus Thornbury</div>
|
||||
<div class="participant-status">Navigator</div>
|
||||
</div>
|
||||
<div class="steam-indicator"></div>
|
||||
</div>
|
||||
<div class="participant">
|
||||
<div class="participant-avatar">ES</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">Eleanor Sterling</div>
|
||||
<div class="participant-status">Communications</div>
|
||||
</div>
|
||||
<div class="steam-indicator"></div>
|
||||
</div>
|
||||
<div class="participant">
|
||||
<div class="participant-avatar">RB</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">Reginald Blackwood</div>
|
||||
<div class="participant-status">Guest Observer</div>
|
||||
</div>
|
||||
<div class="steam-indicator" style="background: #fbbf24;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Panel -->
|
||||
<div class="center-panel">
|
||||
<!-- Projection Apparatus (Screen Share) -->
|
||||
<div class="projection-apparatus">
|
||||
<div class="projection-screen">
|
||||
<div class="projection-content">
|
||||
<div class="projection-icon">🎭</div>
|
||||
<h3 style="color: #daa520; margin-bottom: 10px;">Projection Chamber Active</h3>
|
||||
<p style="color: #b8860b; font-style: italic;">Awaiting visual transmission...</p>
|
||||
</div>
|
||||
<div class="projection-controls">
|
||||
<button class="tube-send" style="width: 50px; height: 50px;">
|
||||
<span>📽️</span>
|
||||
</button>
|
||||
<button class="tube-send" style="width: 50px; height: 50px;">
|
||||
<span>🔍</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Brass Valves (Video Controls) -->
|
||||
<div class="brass-valves">
|
||||
<div class="valve-control" id="camera-valve">
|
||||
<div class="valve-wheel">
|
||||
<div class="valve-spokes">
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
</div>
|
||||
<div class="valve-center"></div>
|
||||
</div>
|
||||
<div class="valve-label">Camera</div>
|
||||
</div>
|
||||
|
||||
<div class="valve-control active" id="audio-valve">
|
||||
<div class="valve-wheel">
|
||||
<div class="valve-spokes">
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
</div>
|
||||
<div class="valve-center"></div>
|
||||
</div>
|
||||
<div class="valve-label">Audio</div>
|
||||
</div>
|
||||
|
||||
<div class="valve-control" id="screen-valve">
|
||||
<div class="valve-wheel">
|
||||
<div class="valve-spokes">
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
<div class="valve-spoke"></div>
|
||||
</div>
|
||||
<div class="valve-center"></div>
|
||||
</div>
|
||||
<div class="valve-label">Screen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel -->
|
||||
<div class="right-panel">
|
||||
<!-- Phonograph Cylinder (Recording) -->
|
||||
<div class="phonograph-cylinder">
|
||||
<h3 style="color: #daa520; margin-bottom: 15px; font-size: 1rem;">Recording Chamber</h3>
|
||||
<div class="cylinder-mechanism"></div>
|
||||
<button class="record-button" id="record-btn"></button>
|
||||
<p style="color: #b8860b; margin-top: 10px; font-size: 0.8rem;">00:00:00</p>
|
||||
</div>
|
||||
|
||||
<!-- Steam Puffs (Reactions) -->
|
||||
<div class="steam-reactions">
|
||||
<h3 style="color: #daa520; margin-bottom: 15px; font-size: 1rem; text-align: center;">Steam Signals</h3>
|
||||
<div class="reactions-grid">
|
||||
<button class="steam-button">
|
||||
👍
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
<button class="steam-button">
|
||||
❤️
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
<button class="steam-button">
|
||||
😂
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
<button class="steam-button">
|
||||
👏
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
<button class="steam-button">
|
||||
🎩
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
<button class="steam-button">
|
||||
⚙️
|
||||
<div class="steam-puff"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pneumatic Tube Messages (Chat) -->
|
||||
<div class="pneumatic-chat">
|
||||
<div class="tube-messages" id="chat-messages">
|
||||
<div class="message-capsule">
|
||||
<div class="message-time">14:32</div>
|
||||
<div class="message-sender">Victoria Whitmore</div>
|
||||
<div class="message-text">Greetings, fellow inventors! The atmospheric pressure readings are optimal for today's demonstration.</div>
|
||||
</div>
|
||||
<div class="message-capsule">
|
||||
<div class="message-time">14:33</div>
|
||||
<div class="message-sender">Augustus Thornbury</div>
|
||||
<div class="message-text">Splendid! I've calibrated the projection apparatus to maximum clarity. The aetheric interference should be minimal.</div>
|
||||
</div>
|
||||
<div class="message-capsule">
|
||||
<div class="message-time">14:35</div>
|
||||
<div class="message-sender">Eleanor Sterling</div>
|
||||
<div class="message-text">All pneumatic tubes are functioning at peak efficiency. Message velocity has increased by 23% since last fortnight.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tube-input-container">
|
||||
<input type="text" class="tube-input" id="chat-input" placeholder="Compose your pneumatic message...">
|
||||
<button class="tube-send" id="send-btn">➤</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Valve controls
|
||||
const valves = document.querySelectorAll('.valve-control');
|
||||
valves.forEach(valve => {
|
||||
valve.addEventListener('click', function() {
|
||||
this.classList.toggle('active');
|
||||
|
||||
// Add steam effect
|
||||
const steamEffect = document.createElement('div');
|
||||
steamEffect.style.cssText = `
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
animation: steam-expand 1s ease-out forwards;
|
||||
pointer-events: none;
|
||||
`;
|
||||
this.appendChild(steamEffect);
|
||||
|
||||
setTimeout(() => steamEffect.remove(), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// Add CSS for steam expansion
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes steam-expand {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Recording functionality
|
||||
const recordBtn = document.getElementById('record-btn');
|
||||
const cylinderMech = document.querySelector('.cylinder-mechanism');
|
||||
let isRecording = false;
|
||||
let recordingTime = 0;
|
||||
let recordingInterval;
|
||||
|
||||
recordBtn.addEventListener('click', function() {
|
||||
isRecording = !isRecording;
|
||||
this.classList.toggle('recording');
|
||||
cylinderMech.classList.toggle('recording');
|
||||
|
||||
if (isRecording) {
|
||||
recordingInterval = setInterval(() => {
|
||||
recordingTime++;
|
||||
const hours = Math.floor(recordingTime / 3600).toString().padStart(2, '0');
|
||||
const minutes = Math.floor((recordingTime % 3600) / 60).toString().padStart(2, '0');
|
||||
const seconds = (recordingTime % 60).toString().padStart(2, '0');
|
||||
this.nextElementSibling.textContent = `${hours}:${minutes}:${seconds}`;
|
||||
}, 1000);
|
||||
} else {
|
||||
clearInterval(recordingInterval);
|
||||
}
|
||||
});
|
||||
|
||||
// Chat functionality
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
|
||||
function sendMessage() {
|
||||
const text = chatInput.value.trim();
|
||||
if (text) {
|
||||
const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
const messageHtml = `
|
||||
<div class="message-capsule">
|
||||
<div class="message-time">${time}</div>
|
||||
<div class="message-sender">You</div>
|
||||
<div class="message-text">${text}</div>
|
||||
</div>
|
||||
`;
|
||||
chatMessages.insertAdjacentHTML('beforeend', messageHtml);
|
||||
chatInput.value = '';
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
// Add pneumatic tube sound effect simulation
|
||||
sendBtn.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => sendBtn.style.transform = 'scale(1)', 200);
|
||||
}
|
||||
}
|
||||
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
chatInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') sendMessage();
|
||||
});
|
||||
|
||||
// Steam reactions
|
||||
const steamButtons = document.querySelectorAll('.steam-button');
|
||||
steamButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// Create multiple steam puffs
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setTimeout(() => {
|
||||
const puff = document.createElement('div');
|
||||
puff.className = 'steam-puff';
|
||||
puff.style.left = `${40 + (Math.random() * 20)}%`;
|
||||
puff.style.animation = 'steam-rise 1s ease-out forwards';
|
||||
this.appendChild(puff);
|
||||
setTimeout(() => puff.remove(), 1000);
|
||||
}, i * 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Participant status updates
|
||||
setInterval(() => {
|
||||
const indicators = document.querySelectorAll('.steam-indicator');
|
||||
indicators.forEach(indicator => {
|
||||
if (Math.random() > 0.7) {
|
||||
indicator.style.animationDuration = Math.random() * 2 + 1 + 's';
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Add some ambient gauge movements
|
||||
const gaugeNeedle = document.querySelector('.gauge-needle');
|
||||
setInterval(() => {
|
||||
const rotation = -30 + Math.random() * 60;
|
||||
gaugeNeedle.style.transform = `rotate(${rotation}deg)`;
|
||||
}, 2000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,896 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Handcrafted Paper Content Card</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Kalam:wght@300;400;700&family=Patrick+Hand&display=swap');
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Patrick Hand', cursive;
|
||||
background: linear-gradient(135deg, #f5f1e8 0%, #fafaf0 100%);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Wood texture background */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image:
|
||||
repeating-linear-gradient(90deg, rgba(139, 90, 43, 0.03) 0px, transparent 1px, transparent 2px, rgba(139, 90, 43, 0.03) 3px),
|
||||
repeating-linear-gradient(0deg, rgba(139, 90, 43, 0.03) 0px, transparent 1px, transparent 2px, rgba(139, 90, 43, 0.03) 3px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Kalam', cursive;
|
||||
font-size: 2.5rem;
|
||||
color: #4a3f36;
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
h1::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 200px;
|
||||
height: 2px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 2"><path d="M0,1 Q25,0 50,1 T100,1" stroke="%234a3f36" fill="none" stroke-width="0.5"/></svg>');
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2.5rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.paper-card {
|
||||
position: relative;
|
||||
background: #fefef4;
|
||||
padding: 1.5rem;
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
|
||||
transition: transform 0.3s ease, filter 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.paper-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background: #fefef4;
|
||||
z-index: -1;
|
||||
clip-path: polygon(
|
||||
0% 2%, 2% 0%, 15% 1%, 30% 0%, 45% 2%, 60% 0%, 75% 1%, 90% 0%, 100% 2%,
|
||||
99% 15%, 100% 30%, 98% 45%, 100% 60%, 99% 75%, 100% 90%, 98% 100%,
|
||||
85% 99%, 70% 100%, 55% 98%, 40% 100%, 25% 99%, 10% 100%, 0% 98%,
|
||||
1% 85%, 0% 70%, 2% 55%, 0% 40%, 1% 25%, 0% 10%
|
||||
);
|
||||
}
|
||||
|
||||
.paper-card:hover {
|
||||
transform: translateY(-4px) rotate(0.5deg);
|
||||
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
/* Paper texture overlay */
|
||||
.paper-texture {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.3;
|
||||
background-image:
|
||||
repeating-linear-gradient(45deg, transparent, transparent 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 70px),
|
||||
repeating-linear-gradient(-45deg, transparent, transparent 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 70px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Bookmark ribbon */
|
||||
.bookmark-ribbon {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: 20px;
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
background: #d32f2f;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 85%, 50% 100%, 0 85%);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bookmark-ribbon::before {
|
||||
content: '★';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.bookmark-ribbon.active {
|
||||
background: #fbc02d;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
/* Content preview */
|
||||
.card-preview {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: #f5f5dc;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
clip-path: polygon(
|
||||
0% 1%, 1% 0%, 99% 0%, 100% 1%,
|
||||
100% 99%, 99% 100%, 1% 100%, 0% 99%
|
||||
);
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: sepia(0.1) contrast(0.9);
|
||||
}
|
||||
|
||||
.sketch-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M10,10 Q50,5 90,10 M10,90 Q50,95 90,90 M10,10 Q5,50 10,90 M90,10 Q95,50 90,90" stroke="%23000" fill="none" stroke-width="0.2" opacity="0.3"/></svg>');
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: 'Kalam', cursive;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
color: #5a6c7d;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Handwritten metadata */
|
||||
.metadata {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: #7a8694;
|
||||
}
|
||||
|
||||
.metadata span {
|
||||
position: relative;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.metadata span::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 1"><path d="M0,0.5 Q5,0 10,0.5 T20,0.5" stroke="%237a8694" fill="none" stroke-width="0.5"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Paper cutout action buttons */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.paper-button {
|
||||
padding: 0.7rem 1.2rem;
|
||||
background: #fefef4;
|
||||
border: none;
|
||||
font-family: 'Patrick Hand', cursive;
|
||||
font-size: 1rem;
|
||||
color: #4a3f36;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.paper-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background: inherit;
|
||||
z-index: -1;
|
||||
clip-path: polygon(
|
||||
0% 5%, 5% 0%, 95% 0%, 100% 5%,
|
||||
100% 95%, 95% 100%, 5% 100%, 0% 95%
|
||||
);
|
||||
}
|
||||
|
||||
.paper-button:hover {
|
||||
transform: translateY(-2px) rotate(-1deg);
|
||||
filter: drop-shadow(4px 4px 8px rgba(0,0,0,0.15));
|
||||
}
|
||||
|
||||
.paper-button:active {
|
||||
transform: translateY(0) rotate(0);
|
||||
filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.1));
|
||||
}
|
||||
|
||||
.paper-button.primary {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.paper-button.secondary {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
}
|
||||
|
||||
/* Share paper airplane */
|
||||
.share-button {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.paper-airplane {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.paper-airplane.flying {
|
||||
animation: fly-away 1.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fly-away {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) rotate(0deg) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(200px, -200px) rotate(45deg) scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal as unfolding paper */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.paper-modal {
|
||||
background: #fefef4;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
position: relative;
|
||||
filter: drop-shadow(0 10px 30px rgba(0,0,0,0.3));
|
||||
transform: scale(0.7) rotateX(90deg);
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-style: preserve-3d;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.modal-overlay.active .paper-modal {
|
||||
transform: scale(1) rotateX(0);
|
||||
}
|
||||
|
||||
.paper-modal::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -3px;
|
||||
background: #fefef4;
|
||||
z-index: -1;
|
||||
clip-path: polygon(
|
||||
0% 3%, 3% 0%, 20% 2%, 40% 0%, 60% 2%, 80% 0%, 97% 3%, 100% 3%,
|
||||
98% 20%, 100% 40%, 98% 60%, 100% 80%, 97% 97%, 97% 100%,
|
||||
80% 98%, 60% 100%, 40% 98%, 20% 100%, 3% 97%, 0% 97%,
|
||||
2% 80%, 0% 60%, 2% 40%, 0% 20%
|
||||
);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 2.5rem;
|
||||
overflow-y: auto;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-family: 'Kalam', cursive;
|
||||
font-size: 2rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
color: #7a8694;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
font-family: 'Patrick Hand', cursive;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
line-height: 1.8;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Fold lines on modal */
|
||||
.fold-lines {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.1;
|
||||
background-image:
|
||||
linear-gradient(90deg, #000 1px, transparent 1px),
|
||||
linear-gradient(0deg, #000 1px, transparent 1px);
|
||||
background-size: 200px 200px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Coffee stain decoration */
|
||||
.coffee-stain {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(139, 90, 43, 0.1) 0%, rgba(139, 90, 43, 0.05) 50%, transparent 70%);
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* Pencil sketch decorations */
|
||||
.pencil-sketch {
|
||||
position: absolute;
|
||||
opacity: 0.2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sketch-1 {
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50"><circle cx="25" cy="25" r="20" fill="none" stroke="%234a3f36" stroke-width="0.5" stroke-dasharray="1,2"/><path d="M15,25 Q25,15 35,25 T25,35" fill="none" stroke="%234a3f36" stroke-width="0.5"/></svg>');
|
||||
}
|
||||
|
||||
.sketch-2 {
|
||||
top: 50%;
|
||||
right: -20px;
|
||||
width: 40px;
|
||||
height: 100px;
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 100"><path d="M20,10 Q10,30 20,50 T20,90" fill="none" stroke="%234a3f36" stroke-width="0.5"/><circle cx="20" cy="30" r="5" fill="none" stroke="%234a3f36" stroke-width="0.5"/><circle cx="20" cy="70" r="5" fill="none" stroke="%234a3f36" stroke-width="0.5"/></svg>');
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading-sketch {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: 0.5rem;
|
||||
animation: sketch-draw 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes sketch-draw {
|
||||
0%, 100% {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M5,10 L10,10" stroke="%234a3f36" fill="none" stroke-width="1"/></svg>');
|
||||
}
|
||||
50% {
|
||||
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M5,10 L15,10" stroke="%234a3f36" fill="none" stroke-width="1"/></svg>');
|
||||
}
|
||||
}
|
||||
|
||||
/* Share popup */
|
||||
.share-popup {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #fefef4;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.share-popup.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.share-popup::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background: #fefef4;
|
||||
z-index: -1;
|
||||
clip-path: polygon(
|
||||
0% 5%, 5% 0%, 95% 0%, 100% 5%,
|
||||
100% 85%, 90% 90%, 50% 100%, 10% 90%, 0% 85%
|
||||
);
|
||||
}
|
||||
|
||||
.share-options {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.share-option {
|
||||
padding: 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.share-option:hover {
|
||||
transform: scale(1.2) rotate(5deg);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Content Card - Handcrafted Paper Theme</h1>
|
||||
|
||||
<div class="content-grid">
|
||||
<!-- Article Card -->
|
||||
<div class="paper-card" data-id="1">
|
||||
<div class="paper-texture"></div>
|
||||
<div class="bookmark-ribbon" onclick="toggleBookmark(event, this)"></div>
|
||||
<div class="coffee-stain"></div>
|
||||
|
||||
<div class="card-preview">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 200'><rect fill='%23e8dcc6' width='400' height='200'/><text x='50%' y='50%' text-anchor='middle' dy='.3em' fill='%234a3f36' font-family='Patrick Hand' font-size='24'>Mountain Sketch</text><path d='M50,150 L150,50 L250,150 M200,100 L300,150' stroke='%234a3f36' fill='none' stroke-width='2'/><circle cx='320' cy='40' r='20' fill='none' stroke='%234a3f36' stroke-width='1.5'/></svg>" alt="Mountain landscape">
|
||||
<div class="sketch-overlay"></div>
|
||||
</div>
|
||||
<h3 class="card-title">The Art of Mountain Sketching</h3>
|
||||
<p class="card-description">Exploring the timeless techniques of capturing mountain landscapes with pencil and paper...</p>
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
<span>📅 March 15, 2024</span>
|
||||
<span>✏️ 5 min read</span>
|
||||
<span>👁️ 1.2k views</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="paper-button primary" onclick="openModal(1)">
|
||||
Read More
|
||||
</button>
|
||||
<button class="paper-button secondary share-button" onclick="toggleShare(event, this)">
|
||||
Share ✈️
|
||||
<svg class="paper-airplane" viewBox="0 0 20 20">
|
||||
<path d="M2,2 L18,10 L2,18 L5,10 Z" fill="#e65100"/>
|
||||
</svg>
|
||||
<div class="share-popup">
|
||||
<div class="share-options">
|
||||
<button class="share-option" onclick="shareVia('twitter')">🐦</button>
|
||||
<button class="share-option" onclick="shareVia('facebook')">📘</button>
|
||||
<button class="share-option" onclick="shareVia('email')">📧</button>
|
||||
<button class="share-option" onclick="shareVia('copy')">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pencil-sketch sketch-1"></div>
|
||||
</div>
|
||||
|
||||
<!-- Recipe Card -->
|
||||
<div class="paper-card" data-id="2">
|
||||
<div class="paper-texture"></div>
|
||||
<div class="bookmark-ribbon" onclick="toggleBookmark(event, this)"></div>
|
||||
|
||||
<div class="card-preview">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 200'><rect fill='%23f5e6d3' width='400' height='200'/><text x='50%' y='30%' text-anchor='middle' dy='.3em' fill='%234a3f36' font-family='Patrick Hand' font-size='24'>Grandma's Recipe</text><circle cx='200' cy='120' r='40' fill='none' stroke='%234a3f36' stroke-width='2'/><path d='M160,120 Q200,80 240,120' fill='none' stroke='%234a3f36' stroke-width='1.5'/><circle cx='180' cy='110' r='3' fill='%234a3f36'/><circle cx='220' cy='110' r='3' fill='%234a3f36'/></svg>" alt="Recipe illustration">
|
||||
<div class="sketch-overlay"></div>
|
||||
</div>
|
||||
<h3 class="card-title">Handwritten Apple Pie Recipe</h3>
|
||||
<p class="card-description">A treasured family recipe passed down through generations, written on aged paper...</p>
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
<span>🥧 Dessert</span>
|
||||
<span>⏱️ 2 hours</span>
|
||||
<span>⭐ 4.9 rating</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="paper-button primary" onclick="openModal(2)">
|
||||
View Recipe
|
||||
</button>
|
||||
<button class="paper-button secondary share-button" onclick="toggleShare(event, this)">
|
||||
Share ✈️
|
||||
<svg class="paper-airplane" viewBox="0 0 20 20">
|
||||
<path d="M2,2 L18,10 L2,18 L5,10 Z" fill="#e65100"/>
|
||||
</svg>
|
||||
<div class="share-popup">
|
||||
<div class="share-options">
|
||||
<button class="share-option" onclick="shareVia('twitter')">🐦</button>
|
||||
<button class="share-option" onclick="shareVia('facebook')">📘</button>
|
||||
<button class="share-option" onclick="shareVia('email')">📧</button>
|
||||
<button class="share-option" onclick="shareVia('copy')">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pencil-sketch sketch-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Journal Entry Card -->
|
||||
<div class="paper-card" data-id="3">
|
||||
<div class="paper-texture"></div>
|
||||
<div class="bookmark-ribbon active" onclick="toggleBookmark(event, this)"></div>
|
||||
|
||||
<div class="card-preview">
|
||||
<div class="card-image">
|
||||
<img src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 200'><rect fill='%23ede4d3' width='400' height='200'/><text x='50%' y='30%' text-anchor='middle' dy='.3em' fill='%234a3f36' font-family='Patrick Hand' font-size='20'>Travel Journal</text><path d='M100,100 Q150,80 200,100 T300,100' stroke='%234a3f36' fill='none' stroke-width='1' stroke-dasharray='2,3'/><path d='M150,130 L170,110 L190,120 L210,105 L230,115 L250,100' stroke='%234a3f36' fill='none' stroke-width='1.5'/></svg>" alt="Journal sketch">
|
||||
<div class="sketch-overlay"></div>
|
||||
</div>
|
||||
<h3 class="card-title">Adventures in Tuscany</h3>
|
||||
<p class="card-description">Hand-drawn maps and stories from a summer journey through Italian countryside...</p>
|
||||
</div>
|
||||
|
||||
<div class="metadata">
|
||||
<span>📍 Travel</span>
|
||||
<span>📖 8 pages</span>
|
||||
<span>💭 32 comments</span>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<button class="paper-button primary" onclick="openModal(3)">
|
||||
Open Journal
|
||||
</button>
|
||||
<button class="paper-button secondary share-button" onclick="toggleShare(event, this)">
|
||||
Share ✈️
|
||||
<svg class="paper-airplane" viewBox="0 0 20 20">
|
||||
<path d="M2,2 L18,10 L2,18 L5,10 Z" fill="#e65100"/>
|
||||
</svg>
|
||||
<div class="share-popup">
|
||||
<div class="share-options">
|
||||
<button class="share-option" onclick="shareVia('twitter')">🐦</button>
|
||||
<button class="share-option" onclick="shareVia('facebook')">📘</button>
|
||||
<button class="share-option" onclick="shareVia('email')">📧</button>
|
||||
<button class="share-option" onclick="shareVia('copy')">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Modal Overlay -->
|
||||
<div class="modal-overlay" onclick="closeModal(event)">
|
||||
<div class="paper-modal" onclick="event.stopPropagation()">
|
||||
<div class="fold-lines"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Content Title</h2>
|
||||
<button class="close-button" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Content will be dynamically inserted -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Content data
|
||||
const contentData = {
|
||||
1: {
|
||||
title: "The Art of Mountain Sketching",
|
||||
content: `
|
||||
<p>There's something magical about sitting on a mountainside with nothing but a sketchbook and pencil. The rough texture of handmade paper beneath your fingers connects you to centuries of artists who've attempted to capture nature's grandeur.</p>
|
||||
|
||||
<p>I learned this technique from an old artist in the Alps, who showed me how to use quick, confident strokes to suggest the massive scale of mountain peaks. "Don't try to draw every rock," he said, his weathered hands moving the pencil with practiced ease. "Let the paper breathe."</p>
|
||||
|
||||
<p>The key is in the observation. Watch how shadows play across the ridges at different times of day. Notice how atmospheric perspective makes distant peaks appear lighter, almost ghostlike. These subtleties, captured in graphite on paper, can convey more emotion than any photograph.</p>
|
||||
|
||||
<p>My favorite papers for mountain sketching are those with a bit of tooth - rough enough to catch the graphite and create texture, but smooth enough for fine details. I often carry several weights, switching between them as the drawing develops.</p>
|
||||
|
||||
<p>Remember, the goal isn't perfection. It's about capturing a moment, a feeling, the essence of standing small before nature's monuments. Let your lines be loose, your shading suggestive. The best mountain sketches feel alive, as if the wind might blow through them at any moment.</p>
|
||||
`
|
||||
},
|
||||
2: {
|
||||
title: "Grandma's Handwritten Apple Pie Recipe",
|
||||
content: `
|
||||
<p><em>Found tucked between the pages of her cookbook, written in fountain pen on paper now yellowed with age...</em></p>
|
||||
|
||||
<p><strong>For the Crust (makes enough for top and bottom):</strong><br>
|
||||
2½ cups flour<br>
|
||||
1 tsp salt<br>
|
||||
1 cup cold butter (Grandma's note: "must be COLD!")<br>
|
||||
¼ to ½ cup ice water</p>
|
||||
|
||||
<p><strong>For the Filling:</strong><br>
|
||||
6-8 tart apples (she preferred Granny Smith)<br>
|
||||
¾ cup sugar (adjust to taste)<br>
|
||||
2 tbsp flour<br>
|
||||
1 tsp cinnamon<br>
|
||||
¼ tsp nutmeg<br>
|
||||
Pinch of salt<br>
|
||||
2 tbsp butter</p>
|
||||
|
||||
<p><em>Her handwritten notes in the margin:</em> "The secret is in the apples - they should make you pucker just a little when raw. And always, always use real butter. None of that margarine nonsense."</p>
|
||||
|
||||
<p><strong>Instructions:</strong><br>
|
||||
Mix flour and salt. Cut in cold butter until mixture resembles coarse crumbs. Add ice water gradually until dough comes together. Divide in half, wrap, and chill for at least an hour.</p>
|
||||
|
||||
<p>Peel and slice apples thin. Toss with sugar, flour, and spices. Roll out bottom crust, fill with apples, dot with butter. Cover with top crust, seal edges, cut vents.</p>
|
||||
|
||||
<p>Bake at 425°F for 15 minutes, then reduce to 350°F for 35-45 minutes until golden brown. <em>(Another note: "If the edges brown too quick, cover with foil")</em></p>
|
||||
|
||||
<p><em>At the bottom, in shaky handwriting:</em> "Made this for your grandfather every Sunday. Now it's your turn to carry on the tradition. All my love, Grandma"</p>
|
||||
`
|
||||
},
|
||||
3: {
|
||||
title: "Adventures in Tuscany - Travel Journal",
|
||||
content: `
|
||||
<p><strong>Day 1 - Arrival in Florence</strong><br>
|
||||
<em>Sketched from the Piazzale Michelangelo at sunset</em></p>
|
||||
|
||||
<p>The paper of this journal already feels different here - maybe it's the Tuscan air, or maybe it's just my imagination. The city spreads below like a Renaissance painting come to life. Terra cotta roofs catch the golden hour light, and I try to capture it with quick pencil strokes.</p>
|
||||
|
||||
<p><strong>Day 3 - The Hills of Chianti</strong><br>
|
||||
<em>Wine stains on this page are intentional... mostly</em></p>
|
||||
|
||||
<p>Spent the morning sketching vineyards. The way the vines create these perfect lines across the hillsides - it's like nature's own ruled paper. Met an old vintner who laughed at my attempts to draw his ancient olive trees. "You cannot draw age," he said, "you must feel it." I think I understand.</p>
|
||||
|
||||
<p><strong>Day 5 - Siena's Secret Gardens</strong><br>
|
||||
<em>Pressed flowers between these pages</em></p>
|
||||
|
||||
<p>Found a hidden garden behind the cathedral. The kind of place that makes you want to fill every page with drawings. Stone benches worn smooth by centuries, fountains singing softly, cats lounging in patches of sun. My sketches can't capture the smell of jasmine or the sound of church bells, but they try.</p>
|
||||
|
||||
<p><strong>Day 8 - The Coast at Cinque Terre</strong><br>
|
||||
<em>Salt spray has warped these pages slightly</em></p>
|
||||
|
||||
<p>Different paper would have been better for the coast - this handmade stock doesn't love the humidity. But there's something perfect about the way the moisture makes my ink lines bleed slightly, like the boundaries between sea and sky in the morning mist.</p>
|
||||
|
||||
<p><strong>Last Day - Reflections</strong><br>
|
||||
This journal is fuller than just sketches and words. It holds pressed flowers, wine stains, smudges of local soil, even a few tears of joy. The paper has absorbed this journey, becoming part of the story itself. That's the magic of keeping a handwritten travel journal - it becomes an artifact of the adventure, not just a record of it.</p>
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle bookmark
|
||||
function toggleBookmark(event, element) {
|
||||
event.stopPropagation();
|
||||
element.classList.toggle('active');
|
||||
|
||||
const isActive = element.classList.contains('active');
|
||||
const cardTitle = element.closest('.paper-card').querySelector('.card-title').textContent;
|
||||
|
||||
if (isActive) {
|
||||
showNotification(`"${cardTitle}" added to bookmarks`);
|
||||
} else {
|
||||
showNotification(`"${cardTitle}" removed from bookmarks`);
|
||||
}
|
||||
}
|
||||
|
||||
// Open modal
|
||||
function openModal(contentId) {
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
const modalTitle = modal.querySelector('.modal-title');
|
||||
const modalBody = modal.querySelector('.modal-body');
|
||||
|
||||
const content = contentData[contentId];
|
||||
modalTitle.textContent = content.title;
|
||||
modalBody.innerHTML = content.content;
|
||||
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal(event) {
|
||||
if (event && event.target !== event.currentTarget) return;
|
||||
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
modal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// Toggle share popup
|
||||
function toggleShare(event, button) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Close all other share popups
|
||||
document.querySelectorAll('.share-popup').forEach(popup => {
|
||||
if (popup !== button.querySelector('.share-popup')) {
|
||||
popup.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
const popup = button.querySelector('.share-popup');
|
||||
popup.classList.toggle('active');
|
||||
}
|
||||
|
||||
// Share functionality
|
||||
function shareVia(platform) {
|
||||
event.stopPropagation();
|
||||
|
||||
const card = event.target.closest('.paper-card');
|
||||
const title = card.querySelector('.card-title').textContent;
|
||||
const shareButton = card.querySelector('.share-button');
|
||||
const airplane = shareButton.querySelector('.paper-airplane');
|
||||
|
||||
// Close popup
|
||||
const popup = shareButton.querySelector('.share-popup');
|
||||
popup.classList.remove('active');
|
||||
|
||||
// Animate paper airplane
|
||||
airplane.classList.add('flying');
|
||||
setTimeout(() => {
|
||||
airplane.classList.remove('flying');
|
||||
}, 1500);
|
||||
|
||||
// Show notification
|
||||
const messages = {
|
||||
twitter: `Shared "${title}" on Twitter`,
|
||||
facebook: `Shared "${title}" on Facebook`,
|
||||
email: `Email composed for "${title}"`,
|
||||
copy: `Link copied for "${title}"`
|
||||
};
|
||||
|
||||
showNotification(messages[platform]);
|
||||
}
|
||||
|
||||
// Show notification
|
||||
function showNotification(message) {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'notification';
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #fefef4;
|
||||
padding: 1rem 2rem;
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
||||
z-index: 2000;
|
||||
font-family: 'Patrick Hand', cursive;
|
||||
color: #4a3f36;
|
||||
clip-path: polygon(
|
||||
0% 5%, 5% 0%, 95% 0%, 100% 5%,
|
||||
100% 95%, 95% 100%, 5% 100%, 0% 95%
|
||||
);
|
||||
animation: slide-up 0.3s ease-out;
|
||||
`;
|
||||
|
||||
// Add animation keyframes if not already present
|
||||
if (!document.querySelector('#notification-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'notification-styles';
|
||||
style.textContent = `
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slide-up 0.3s ease-out reverse';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Close share popups when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.share-popup.active').forEach(popup => {
|
||||
popup.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Add hover effect to cards for subtle animation
|
||||
document.querySelectorAll('.paper-card').forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
this.style.transform = `translateY(-4px) rotate(${Math.random() * 2 - 1}deg)`;
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
this.style.transform = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
document.querySelectorAll('.share-popup.active').forEach(popup => {
|
||||
popup.classList.remove('active');
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,853 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Digital Minimalism Form Wizard</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #000000;
|
||||
--secondary: #666666;
|
||||
--tertiary: #999999;
|
||||
--background: #ffffff;
|
||||
--surface: #fafafa;
|
||||
--accent: #0066ff;
|
||||
--error: #ff3333;
|
||||
--success: #00cc88;
|
||||
--border: #e5e5e5;
|
||||
--shadow: rgba(0, 0, 0, 0.05);
|
||||
--transition: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: var(--background);
|
||||
color: var(--primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 3rem;
|
||||
text-align: center;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hybrid-component {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
padding: 3rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Progress Line - Minimal */
|
||||
.progress-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
width: 0%;
|
||||
transition: width var(--transition);
|
||||
}
|
||||
|
||||
/* Step Indicators - Geometric Dots */
|
||||
.step-indicators {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--background);
|
||||
border: 2px solid var(--border);
|
||||
transition: all var(--transition);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.step-indicator.active .step-dot {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.step-indicator.completed .step-dot {
|
||||
background: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.step-indicator.active .step-label,
|
||||
.step-indicator.completed .step-label {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Step Connection Line */
|
||||
.step-connection {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Form Steps */
|
||||
.form-steps {
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-step {
|
||||
display: none;
|
||||
animation: fadeIn var(--transition) ease-out;
|
||||
}
|
||||
|
||||
.form-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 0;
|
||||
font-size: 1rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: transparent;
|
||||
transition: border-color var(--transition);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.form-input.error {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
/* Validation Messages */
|
||||
.validation-message {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
height: 1rem;
|
||||
color: var(--error);
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.validation-message.show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Radio Groups */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
position: relative;
|
||||
transition: border-color var(--transition);
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"]:checked + .radio-input {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.radio-option input[type="radio"]:checked + .radio-input::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.form-navigation {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
padding: 0.75rem 2rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
font-family: inherit;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: var(--background);
|
||||
}
|
||||
|
||||
.nav-button.primary {
|
||||
background: var(--primary);
|
||||
color: var(--background);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.nav-button.primary:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-button:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nav-button:disabled:hover {
|
||||
background: none;
|
||||
color: var(--primary);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* Arrow Icons */
|
||||
.arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arrow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-right: 2px solid currentColor;
|
||||
border-bottom: 2px solid currentColor;
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
.arrow.left::before {
|
||||
transform: translate(-50%, -50%) rotate(135deg);
|
||||
}
|
||||
|
||||
/* Save State Indicator */
|
||||
.save-state {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--tertiary);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.save-state.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--success);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Summary Step */
|
||||
.summary-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 1rem;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Success State */
|
||||
.success-message {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
animation: fadeIn var(--transition) ease-out;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.success-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 25%;
|
||||
width: 10px;
|
||||
height: 16px;
|
||||
border-right: 2px solid var(--success);
|
||||
border-bottom: 2px solid var(--success);
|
||||
transform: translateY(-60%) rotate(45deg);
|
||||
}
|
||||
|
||||
.success-icon::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--success);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.hybrid-component {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
padding: 0.625rem 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Form Wizard - Digital Minimalism Theme</h1>
|
||||
|
||||
<div class="hybrid-component">
|
||||
<!-- Progress Line -->
|
||||
<div class="progress-line">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
|
||||
<!-- Save State Indicator -->
|
||||
<div class="save-state" id="saveState">
|
||||
<div class="save-indicator"></div>
|
||||
<span>Auto-saved</span>
|
||||
</div>
|
||||
|
||||
<!-- Step Indicators -->
|
||||
<div class="step-indicators">
|
||||
<div class="step-connection"></div>
|
||||
<div class="step-indicator active" data-step="1">
|
||||
<div class="step-dot"></div>
|
||||
<span class="step-label">Personal</span>
|
||||
</div>
|
||||
<div class="step-indicator" data-step="2">
|
||||
<div class="step-dot"></div>
|
||||
<span class="step-label">Account</span>
|
||||
</div>
|
||||
<div class="step-indicator" data-step="3">
|
||||
<div class="step-dot"></div>
|
||||
<span class="step-label">Preferences</span>
|
||||
</div>
|
||||
<div class="step-indicator" data-step="4">
|
||||
<div class="step-dot"></div>
|
||||
<span class="step-label">Review</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Steps -->
|
||||
<div class="form-steps">
|
||||
<!-- Step 1: Personal Information -->
|
||||
<div class="form-step active" data-step="1">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="firstName">First Name</label>
|
||||
<input type="text" class="form-input" id="firstName" required>
|
||||
<div class="validation-message" id="firstNameError">Please enter your first name</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="lastName">Last Name</label>
|
||||
<input type="text" class="form-input" id="lastName" required>
|
||||
<div class="validation-message" id="lastNameError">Please enter your last name</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">Email Address</label>
|
||||
<input type="email" class="form-input" id="email" required>
|
||||
<div class="validation-message" id="emailError">Please enter a valid email address</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Account Setup -->
|
||||
<div class="form-step" data-step="2">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Username</label>
|
||||
<input type="text" class="form-input" id="username" required>
|
||||
<div class="validation-message" id="usernameError">Username must be at least 3 characters</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input type="password" class="form-input" id="password" required>
|
||||
<div class="validation-message" id="passwordError">Password must be at least 8 characters</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="confirmPassword">Confirm Password</label>
|
||||
<input type="password" class="form-input" id="confirmPassword" required>
|
||||
<div class="validation-message" id="confirmPasswordError">Passwords do not match</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Preferences -->
|
||||
<div class="form-step" data-step="3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notification Frequency</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="notifications" value="daily" checked>
|
||||
<div class="radio-input"></div>
|
||||
<span>Daily</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="notifications" value="weekly">
|
||||
<div class="radio-input"></div>
|
||||
<span>Weekly</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="notifications" value="never">
|
||||
<div class="radio-input"></div>
|
||||
<span>Never</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Theme Preference</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="theme" value="light" checked>
|
||||
<div class="radio-input"></div>
|
||||
<span>Light</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="theme" value="dark">
|
||||
<div class="radio-input"></div>
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
<label class="radio-option">
|
||||
<input type="radio" name="theme" value="auto">
|
||||
<div class="radio-input"></div>
|
||||
<span>Auto</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Review -->
|
||||
<div class="form-step" data-step="4">
|
||||
<div class="summary-section">
|
||||
<div class="summary-label">Personal Information</div>
|
||||
<div class="summary-value" id="summaryName">-</div>
|
||||
<div class="summary-value" id="summaryEmail">-</div>
|
||||
</div>
|
||||
<div class="summary-section">
|
||||
<div class="summary-label">Account Details</div>
|
||||
<div class="summary-value" id="summaryUsername">-</div>
|
||||
</div>
|
||||
<div class="summary-section">
|
||||
<div class="summary-label">Preferences</div>
|
||||
<div class="summary-value" id="summaryNotifications">-</div>
|
||||
<div class="summary-value" id="summaryTheme">-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success State -->
|
||||
<div class="form-step" data-step="success">
|
||||
<div class="success-message">
|
||||
<div class="success-icon"></div>
|
||||
<h2 style="font-weight: 300; margin-bottom: 0.5rem;">Account Created</h2>
|
||||
<p style="color: var(--secondary);">Welcome to your minimal digital experience.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="form-navigation">
|
||||
<button class="nav-button" id="prevBtn" disabled>
|
||||
<span class="arrow left"></span>
|
||||
Previous
|
||||
</button>
|
||||
<button class="nav-button primary" id="nextBtn">
|
||||
Next
|
||||
<span class="arrow"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Form Wizard State
|
||||
const formWizard = {
|
||||
currentStep: 1,
|
||||
totalSteps: 4,
|
||||
formData: {},
|
||||
saveTimeout: null
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const progressFill = document.getElementById('progressFill');
|
||||
const saveState = document.getElementById('saveState');
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
// Initialize form data from localStorage
|
||||
function initializeFormData() {
|
||||
const savedData = localStorage.getItem('formWizardData');
|
||||
if (savedData) {
|
||||
formWizard.formData = JSON.parse(savedData);
|
||||
restoreFormData();
|
||||
}
|
||||
}
|
||||
|
||||
// Save form data
|
||||
function saveFormData() {
|
||||
// Collect current step data
|
||||
collectStepData();
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('formWizardData', JSON.stringify(formWizard.formData));
|
||||
|
||||
// Show save indicator
|
||||
saveState.classList.add('show');
|
||||
clearTimeout(formWizard.saveTimeout);
|
||||
formWizard.saveTimeout = setTimeout(() => {
|
||||
saveState.classList.remove('show');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Restore form data
|
||||
function restoreFormData() {
|
||||
// Restore all form fields
|
||||
Object.keys(formWizard.formData).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'radio') {
|
||||
const radio = document.querySelector(`input[name="${element.name}"][value="${formWizard.formData[key]}"]`);
|
||||
if (radio) radio.checked = true;
|
||||
} else {
|
||||
element.value = formWizard.formData[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Collect data from current step
|
||||
function collectStepData() {
|
||||
const currentStepElement = document.querySelector(`.form-step[data-step="${formWizard.currentStep}"]`);
|
||||
if (!currentStepElement) return;
|
||||
|
||||
// Text inputs
|
||||
currentStepElement.querySelectorAll('.form-input').forEach(input => {
|
||||
if (input.value) {
|
||||
formWizard.formData[input.id] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Radio inputs
|
||||
currentStepElement.querySelectorAll('input[type="radio"]:checked').forEach(radio => {
|
||||
formWizard.formData[radio.name] = radio.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Validate current step
|
||||
function validateStep() {
|
||||
const currentStepElement = document.querySelector(`.form-step[data-step="${formWizard.currentStep}"]`);
|
||||
if (!currentStepElement) return true;
|
||||
|
||||
let isValid = true;
|
||||
|
||||
// Validate text inputs
|
||||
currentStepElement.querySelectorAll('.form-input[required]').forEach(input => {
|
||||
const errorElement = document.getElementById(input.id + 'Error');
|
||||
|
||||
if (!input.value.trim()) {
|
||||
input.classList.add('error');
|
||||
if (errorElement) errorElement.classList.add('show');
|
||||
isValid = false;
|
||||
} else if (input.type === 'email' && !isValidEmail(input.value)) {
|
||||
input.classList.add('error');
|
||||
if (errorElement) errorElement.classList.add('show');
|
||||
isValid = false;
|
||||
} else if (input.id === 'username' && input.value.length < 3) {
|
||||
input.classList.add('error');
|
||||
if (errorElement) errorElement.classList.add('show');
|
||||
isValid = false;
|
||||
} else if (input.id === 'password' && input.value.length < 8) {
|
||||
input.classList.add('error');
|
||||
if (errorElement) errorElement.classList.add('show');
|
||||
isValid = false;
|
||||
} else if (input.id === 'confirmPassword') {
|
||||
const password = document.getElementById('password').value;
|
||||
if (input.value !== password) {
|
||||
input.classList.add('error');
|
||||
if (errorElement) errorElement.classList.add('show');
|
||||
isValid = false;
|
||||
} else {
|
||||
input.classList.remove('error');
|
||||
if (errorElement) errorElement.classList.remove('show');
|
||||
}
|
||||
} else {
|
||||
input.classList.remove('error');
|
||||
if (errorElement) errorElement.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function isValidEmail(email) {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
}
|
||||
|
||||
// Update progress
|
||||
function updateProgress() {
|
||||
const progress = ((formWizard.currentStep - 1) / formWizard.totalSteps) * 100;
|
||||
progressFill.style.width = progress + '%';
|
||||
}
|
||||
|
||||
// Update step indicators
|
||||
function updateStepIndicators() {
|
||||
document.querySelectorAll('.step-indicator').forEach((indicator, index) => {
|
||||
const stepNum = index + 1;
|
||||
indicator.classList.remove('active', 'completed');
|
||||
|
||||
if (stepNum < formWizard.currentStep) {
|
||||
indicator.classList.add('completed');
|
||||
} else if (stepNum === formWizard.currentStep) {
|
||||
indicator.classList.add('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show step
|
||||
function showStep(stepNumber) {
|
||||
// Hide all steps
|
||||
document.querySelectorAll('.form-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
});
|
||||
|
||||
// Show current step
|
||||
const currentStepElement = document.querySelector(`.form-step[data-step="${stepNumber}"]`);
|
||||
if (currentStepElement) {
|
||||
currentStepElement.classList.add('active');
|
||||
}
|
||||
|
||||
// Update navigation buttons
|
||||
prevBtn.disabled = stepNumber === 1;
|
||||
|
||||
if (stepNumber === formWizard.totalSteps) {
|
||||
nextBtn.textContent = 'Submit';
|
||||
nextBtn.innerHTML = 'Submit <span class="arrow"></span>';
|
||||
} else {
|
||||
nextBtn.textContent = 'Next';
|
||||
nextBtn.innerHTML = 'Next <span class="arrow"></span>';
|
||||
}
|
||||
|
||||
// Update progress and indicators
|
||||
updateProgress();
|
||||
updateStepIndicators();
|
||||
|
||||
// Update summary if on review step
|
||||
if (stepNumber === 4) {
|
||||
updateSummary();
|
||||
}
|
||||
}
|
||||
|
||||
// Update summary
|
||||
function updateSummary() {
|
||||
document.getElementById('summaryName').textContent =
|
||||
`${formWizard.formData.firstName || ''} ${formWizard.formData.lastName || ''}`;
|
||||
document.getElementById('summaryEmail').textContent =
|
||||
formWizard.formData.email || '-';
|
||||
document.getElementById('summaryUsername').textContent =
|
||||
`Username: ${formWizard.formData.username || '-'}`;
|
||||
document.getElementById('summaryNotifications').textContent =
|
||||
`Notifications: ${formWizard.formData.notifications || 'daily'}`;
|
||||
document.getElementById('summaryTheme').textContent =
|
||||
`Theme: ${formWizard.formData.theme || 'light'}`;
|
||||
}
|
||||
|
||||
// Next step
|
||||
function nextStep() {
|
||||
if (formWizard.currentStep === formWizard.totalSteps) {
|
||||
// Submit form
|
||||
if (validateStep()) {
|
||||
collectStepData();
|
||||
saveFormData();
|
||||
showStep('success');
|
||||
progressFill.style.width = '100%';
|
||||
// Clear saved data after successful submission
|
||||
localStorage.removeItem('formWizardData');
|
||||
// Hide navigation
|
||||
document.querySelector('.form-navigation').style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
if (validateStep()) {
|
||||
collectStepData();
|
||||
saveFormData();
|
||||
formWizard.currentStep++;
|
||||
showStep(formWizard.currentStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Previous step
|
||||
function prevStep() {
|
||||
if (formWizard.currentStep > 1) {
|
||||
formWizard.currentStep--;
|
||||
showStep(formWizard.currentStep);
|
||||
}
|
||||
}
|
||||
|
||||
// Event Listeners
|
||||
nextBtn.addEventListener('click', nextStep);
|
||||
prevBtn.addEventListener('click', prevStep);
|
||||
|
||||
// Auto-save on input change
|
||||
document.querySelectorAll('.form-input, input[type="radio"]').forEach(input => {
|
||||
input.addEventListener('change', () => {
|
||||
saveFormData();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear validation on input
|
||||
document.querySelectorAll('.form-input').forEach(input => {
|
||||
input.addEventListener('input', () => {
|
||||
const errorElement = document.getElementById(input.id + 'Error');
|
||||
if (input.value.trim()) {
|
||||
input.classList.remove('error');
|
||||
if (errorElement) errorElement.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initializeFormData();
|
||||
showStep(formWizard.currentStep);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,691 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Playful Animation Media Player</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-pink: #FF6B9D;
|
||||
--primary-yellow: #FFC857;
|
||||
--primary-blue: #4ECDC4;
|
||||
--primary-purple: #A8E6CF;
|
||||
--primary-orange: #FF6F61;
|
||||
--bg-color: #FAF3F0;
|
||||
--text-dark: #2D3436;
|
||||
--white: #FFFFFF;
|
||||
--shadow: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0) scale(1); }
|
||||
50% { transform: translateY(-10px) scale(1.05); }
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(-5deg); }
|
||||
75% { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
@keyframes dance {
|
||||
0%, 100% { transform: translateX(0) rotate(0deg); }
|
||||
25% { transform: translateX(-5px) rotate(-2deg); }
|
||||
75% { transform: translateX(5px) rotate(2deg); }
|
||||
}
|
||||
|
||||
@keyframes rainbow {
|
||||
0% { filter: hue-rotate(0deg); }
|
||||
100% { filter: hue-rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Comic Sans MS', cursive, sans-serif;
|
||||
background: var(--bg-color);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
animation: wiggle 2s ease-in-out infinite;
|
||||
text-shadow: 3px 3px 0px var(--primary-pink);
|
||||
}
|
||||
|
||||
.media-player {
|
||||
background: var(--white);
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 20px 40px var(--shadow);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Visualizer Section */
|
||||
.visualizer {
|
||||
height: 200px;
|
||||
background: linear-gradient(135deg, var(--primary-blue) 0%, var(--primary-purple) 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.wave-bar {
|
||||
width: 6%;
|
||||
background: var(--white);
|
||||
border-radius: 10px 10px 0 0;
|
||||
transition: height 0.2s ease;
|
||||
animation: dance 1s ease-in-out infinite;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.wave-bar:nth-child(odd) {
|
||||
animation-delay: 0.1s;
|
||||
background: var(--primary-yellow);
|
||||
}
|
||||
|
||||
.wave-bar:nth-child(even) {
|
||||
animation-delay: 0.2s;
|
||||
background: var(--primary-pink);
|
||||
}
|
||||
|
||||
/* Current Song Display */
|
||||
.current-song {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background: var(--white);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.song-title {
|
||||
font-size: 1.5em;
|
||||
color: var(--text-dark);
|
||||
margin-bottom: 5px;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.song-artist {
|
||||
color: #7D8E95;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Controls Section */
|
||||
.controls {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: var(--primary-pink);
|
||||
border: none;
|
||||
color: var(--white);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
font-size: 1.5em;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 15px rgba(255, 107, 157, 0.3);
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
animation: bounce 0.5s ease-in-out;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 10px 25px rgba(255, 107, 157, 0.5);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: var(--primary-blue);
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 2em;
|
||||
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.3);
|
||||
}
|
||||
|
||||
.play-btn:hover {
|
||||
box-shadow: 0 10px 25px rgba(78, 205, 196, 0.5);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
padding: 0 20px 20px;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: #F0F0F0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-pink) 0%, var(--primary-yellow) 50%, var(--primary-blue) 100%);
|
||||
width: 35%;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
transition: width 0.3s ease;
|
||||
animation: rainbow 3s linear infinite;
|
||||
}
|
||||
|
||||
.progress-handle {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: var(--white);
|
||||
border: 4px solid var(--primary-pink);
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
box-shadow: 0 2px 10px var(--shadow);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Playlist Section */
|
||||
.playlist-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: #F8F8F8;
|
||||
}
|
||||
|
||||
.playlist-item {
|
||||
background: var(--white);
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playlist-item:hover {
|
||||
transform: translateX(10px);
|
||||
animation: dance 0.5s ease-in-out;
|
||||
box-shadow: 0 5px 15px var(--shadow);
|
||||
}
|
||||
|
||||
.playlist-item.active {
|
||||
background: linear-gradient(135deg, var(--primary-pink) 0%, var(--primary-purple) 100%);
|
||||
color: var(--white);
|
||||
animation: dance 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.playlist-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--primary-yellow);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
color: var(--text-dark);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.playlist-item.active .playlist-number {
|
||||
background: var(--white);
|
||||
animation: bounce 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Quality Selector */
|
||||
.quality-selector {
|
||||
padding: 20px;
|
||||
background: var(--white);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quality-bubble {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-purple);
|
||||
color: var(--text-dark);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9em;
|
||||
position: relative;
|
||||
box-shadow: 0 5px 10px var(--shadow);
|
||||
}
|
||||
|
||||
.quality-bubble:hover {
|
||||
animation: float 1s ease-in-out;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.quality-bubble.active {
|
||||
background: var(--primary-orange);
|
||||
color: var(--white);
|
||||
animation: pulse 1s ease-in-out infinite;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
.quality-bubble.low { width: 50px; height: 50px; font-size: 0.8em; }
|
||||
.quality-bubble.medium { width: 60px; height: 60px; }
|
||||
.quality-bubble.high { width: 70px; height: 70px; font-size: 1em; }
|
||||
.quality-bubble.ultra { width: 80px; height: 80px; font-size: 1.1em; }
|
||||
|
||||
/* Share Button */
|
||||
.share-section {
|
||||
padding: 20px;
|
||||
background: var(--white);
|
||||
text-align: center;
|
||||
border-top: 2px dashed #E0E0E0;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
background: linear-gradient(135deg, var(--primary-orange) 0%, var(--primary-pink) 100%);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
padding: 15px 40px;
|
||||
border-radius: 30px;
|
||||
font-size: 1.2em;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 5px 15px rgba(255, 111, 97, 0.3);
|
||||
}
|
||||
|
||||
.share-btn:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(255, 111, 97, 0.5);
|
||||
animation: wiggle 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Confetti */
|
||||
.confetti {
|
||||
position: fixed;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -10px;
|
||||
animation: confetti-fall 3s linear forwards;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes confetti-fall {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
h1 { font-size: 2em; }
|
||||
.control-btn { width: 50px; height: 50px; font-size: 1.2em; }
|
||||
.play-btn { width: 70px; height: 70px; font-size: 1.8em; }
|
||||
.quality-bubble { width: 50px; height: 50px; font-size: 0.8em; }
|
||||
.quality-bubble.low { width: 40px; height: 40px; font-size: 0.7em; }
|
||||
.quality-bubble.medium { width: 50px; height: 50px; }
|
||||
.quality-bubble.high { width: 60px; height: 60px; font-size: 0.9em; }
|
||||
.quality-bubble.ultra { width: 70px; height: 70px; font-size: 1em; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>🎵 Bouncy Music Box 🎵</h1>
|
||||
|
||||
<div class="media-player">
|
||||
<!-- Visualizer -->
|
||||
<div class="visualizer">
|
||||
<div class="wave" id="waveContainer">
|
||||
<!-- Wave bars will be generated by JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Song Display -->
|
||||
<div class="current-song">
|
||||
<div class="song-title" id="songTitle">Happy Dance Time</div>
|
||||
<div class="song-artist" id="songArtist">The Joyful Beats</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" id="progressBar">
|
||||
<div class="progress-fill" id="progressFill">
|
||||
<div class="progress-handle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<button class="control-btn" id="prevBtn">⏮️</button>
|
||||
<button class="control-btn play-btn" id="playBtn">▶️</button>
|
||||
<button class="control-btn" id="nextBtn">⏭️</button>
|
||||
</div>
|
||||
|
||||
<!-- Quality Selector -->
|
||||
<div class="quality-selector">
|
||||
<div class="quality-bubble low" data-quality="low">128k</div>
|
||||
<div class="quality-bubble medium" data-quality="medium">256k</div>
|
||||
<div class="quality-bubble high active" data-quality="high">320k</div>
|
||||
<div class="quality-bubble ultra" data-quality="ultra">FLAC</div>
|
||||
</div>
|
||||
|
||||
<!-- Playlist -->
|
||||
<div class="playlist-container">
|
||||
<div class="playlist-item active" data-index="0">
|
||||
<div class="playlist-number">1</div>
|
||||
<div>
|
||||
<div>Happy Dance Time</div>
|
||||
<small>The Joyful Beats</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-item" data-index="1">
|
||||
<div class="playlist-number">2</div>
|
||||
<div>
|
||||
<div>Bouncy Castle Dreams</div>
|
||||
<small>Rainbow Unicorns</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-item" data-index="2">
|
||||
<div class="playlist-number">3</div>
|
||||
<div>
|
||||
<div>Bubble Pop Symphony</div>
|
||||
<small>Giggle Factory</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-item" data-index="3">
|
||||
<div class="playlist-number">4</div>
|
||||
<div>
|
||||
<div>Marshmallow Clouds</div>
|
||||
<small>Sweet Melodies</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist-item" data-index="4">
|
||||
<div class="playlist-number">5</div>
|
||||
<div>
|
||||
<div>Confetti Carnival</div>
|
||||
<small>Party Pirates</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Section -->
|
||||
<div class="share-section">
|
||||
<button class="share-btn" id="shareBtn">🎉 Share the Joy! 🎉</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Playlist data
|
||||
const playlist = [
|
||||
{ title: 'Happy Dance Time', artist: 'The Joyful Beats' },
|
||||
{ title: 'Bouncy Castle Dreams', artist: 'Rainbow Unicorns' },
|
||||
{ title: 'Bubble Pop Symphony', artist: 'Giggle Factory' },
|
||||
{ title: 'Marshmallow Clouds', artist: 'Sweet Melodies' },
|
||||
{ title: 'Confetti Carnival', artist: 'Party Pirates' }
|
||||
];
|
||||
|
||||
let currentSongIndex = 0;
|
||||
let isPlaying = false;
|
||||
let progress = 35;
|
||||
let animationFrame;
|
||||
let currentQuality = 'high';
|
||||
|
||||
// Create wave bars
|
||||
const waveContainer = document.getElementById('waveContainer');
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'wave-bar';
|
||||
bar.style.height = '20%';
|
||||
bar.style.animationDelay = `${i * 0.1}s`;
|
||||
waveContainer.appendChild(bar);
|
||||
}
|
||||
|
||||
// Visualizer animation
|
||||
function animateVisualizer() {
|
||||
const bars = document.querySelectorAll('.wave-bar');
|
||||
bars.forEach((bar, index) => {
|
||||
if (isPlaying) {
|
||||
const height = Math.random() * 80 + 20;
|
||||
bar.style.height = `${height}%`;
|
||||
|
||||
// More complex animations for higher quality
|
||||
if (currentQuality === 'ultra') {
|
||||
bar.style.transform = `scaleX(${1 + Math.random() * 0.2}) rotate(${Math.random() * 5 - 2.5}deg)`;
|
||||
} else if (currentQuality === 'high') {
|
||||
bar.style.transform = `scaleX(${1 + Math.random() * 0.1})`;
|
||||
}
|
||||
} else {
|
||||
bar.style.height = '20%';
|
||||
bar.style.transform = 'scaleX(1) rotate(0deg)';
|
||||
}
|
||||
});
|
||||
|
||||
animationFrame = requestAnimationFrame(animateVisualizer);
|
||||
}
|
||||
|
||||
// Play/Pause functionality
|
||||
const playBtn = document.getElementById('playBtn');
|
||||
playBtn.addEventListener('click', () => {
|
||||
isPlaying = !isPlaying;
|
||||
playBtn.textContent = isPlaying ? '⏸️' : '▶️';
|
||||
|
||||
if (isPlaying) {
|
||||
animateVisualizer();
|
||||
simulateProgress();
|
||||
// Make playlist item dance more when playing
|
||||
document.querySelector('.playlist-item.active').style.animationDuration = '0.5s';
|
||||
} else {
|
||||
document.querySelector('.playlist-item.active').style.animationDuration = '2s';
|
||||
}
|
||||
});
|
||||
|
||||
// Progress simulation
|
||||
function simulateProgress() {
|
||||
if (isPlaying && progress < 100) {
|
||||
progress += 0.1;
|
||||
document.getElementById('progressFill').style.width = `${progress}%`;
|
||||
requestAnimationFrame(simulateProgress);
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bar click
|
||||
document.getElementById('progressBar').addEventListener('click', (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
progress = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
document.getElementById('progressFill').style.width = `${progress}%`;
|
||||
});
|
||||
|
||||
// Song navigation
|
||||
function changeSong(index) {
|
||||
// Update current song index
|
||||
currentSongIndex = index;
|
||||
if (currentSongIndex < 0) currentSongIndex = playlist.length - 1;
|
||||
if (currentSongIndex >= playlist.length) currentSongIndex = 0;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('songTitle').textContent = playlist[currentSongIndex].title;
|
||||
document.getElementById('songArtist').textContent = playlist[currentSongIndex].artist;
|
||||
|
||||
// Update playlist active state
|
||||
document.querySelectorAll('.playlist-item').forEach((item, i) => {
|
||||
item.classList.toggle('active', i === currentSongIndex);
|
||||
});
|
||||
|
||||
// Reset progress
|
||||
progress = 0;
|
||||
document.getElementById('progressFill').style.width = '0%';
|
||||
|
||||
// Bounce effect on song change
|
||||
const songDisplay = document.querySelector('.current-song');
|
||||
songDisplay.style.animation = 'bounce 0.5s ease-in-out';
|
||||
setTimeout(() => {
|
||||
songDisplay.style.animation = '';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Previous/Next buttons
|
||||
document.getElementById('prevBtn').addEventListener('click', () => {
|
||||
changeSong(currentSongIndex - 1);
|
||||
});
|
||||
|
||||
document.getElementById('nextBtn').addEventListener('click', () => {
|
||||
changeSong(currentSongIndex + 1);
|
||||
});
|
||||
|
||||
// Playlist item clicks
|
||||
document.querySelectorAll('.playlist-item').forEach((item, index) => {
|
||||
item.addEventListener('click', () => {
|
||||
changeSong(index);
|
||||
});
|
||||
});
|
||||
|
||||
// Quality selector
|
||||
document.querySelectorAll('.quality-bubble').forEach(bubble => {
|
||||
bubble.addEventListener('click', () => {
|
||||
document.querySelectorAll('.quality-bubble').forEach(b => b.classList.remove('active'));
|
||||
bubble.classList.add('active');
|
||||
currentQuality = bubble.dataset.quality;
|
||||
|
||||
// Bounce all quality bubbles for fun
|
||||
document.querySelectorAll('.quality-bubble').forEach(b => {
|
||||
b.style.animation = 'bounce 0.5s ease-in-out';
|
||||
setTimeout(() => {
|
||||
b.style.animation = '';
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Update visualizer complexity based on quality
|
||||
const bars = document.querySelectorAll('.wave-bar');
|
||||
bars.forEach((bar, i) => {
|
||||
if (currentQuality === 'ultra') {
|
||||
bar.style.width = '5%';
|
||||
} else if (currentQuality === 'high') {
|
||||
bar.style.width = '6%';
|
||||
} else if (currentQuality === 'medium') {
|
||||
bar.style.width = '7%';
|
||||
} else {
|
||||
bar.style.width = '8%';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Share button with confetti
|
||||
document.getElementById('shareBtn').addEventListener('click', () => {
|
||||
// Create confetti burst
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const confetti = document.createElement('div');
|
||||
confetti.className = 'confetti';
|
||||
confetti.style.left = Math.random() * window.innerWidth + 'px';
|
||||
confetti.style.backgroundColor = ['#FF6B9D', '#FFC857', '#4ECDC4', '#A8E6CF', '#FF6F61'][Math.floor(Math.random() * 5)];
|
||||
confetti.style.animationDelay = Math.random() * 0.5 + 's';
|
||||
confetti.style.animationDuration = (Math.random() * 2 + 2) + 's';
|
||||
document.body.appendChild(confetti);
|
||||
|
||||
setTimeout(() => confetti.remove(), 4000);
|
||||
}
|
||||
|
||||
// Bounce the share button
|
||||
const btn = document.getElementById('shareBtn');
|
||||
btn.style.animation = 'bounce 0.5s ease-in-out';
|
||||
setTimeout(() => {
|
||||
btn.style.animation = '';
|
||||
}, 500);
|
||||
|
||||
// Show mock share message
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '🎊 Shared! 🎊';
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Start visualizer animation
|
||||
animateVisualizer();
|
||||
|
||||
// Add some fun interactions
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (isPlaying) {
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
|
||||
// Subtle tilt effect on the player
|
||||
const player = document.querySelector('.media-player');
|
||||
const tiltX = (y - 0.5) * 5;
|
||||
const tiltY = (x - 0.5) * -5;
|
||||
player.style.transform = `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg)`;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset tilt on mouse leave
|
||||
document.addEventListener('mouseleave', () => {
|
||||
document.querySelector('.media-player').style.transform = '';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue