884 lines
31 KiB
HTML
884 lines
31 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: 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> |