785 lines
29 KiB
HTML
785 lines
29 KiB
HTML
<!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> |