0
# Visual Effects
1
2
Visual enhancement components including ripple effects and animations that provide tactile feedback and smooth transitions. These effects enhance the user experience by providing immediate visual feedback for interactions.
3
4
## Capabilities
5
6
### Material Ripple
7
8
Touch ripple effect component that creates expanding circular animations on user interactions.
9
10
```javascript { .api }
11
/**
12
* Material Design ripple effect component
13
* CSS Class: mdl-js-ripple-effect
14
* Widget: false
15
*/
16
interface MaterialRipple {
17
/**
18
* Get current animation frame count
19
* @returns Current frame count number
20
*/
21
getFrameCount(): number;
22
23
/**
24
* Set animation frame count
25
* @param frameCount - New frame count value
26
*/
27
setFrameCount(frameCount: number): void;
28
29
/**
30
* Get the DOM element used for ripple effect
31
* @returns HTMLElement representing the ripple
32
*/
33
getRippleElement(): HTMLElement;
34
35
/**
36
* Set ripple animation coordinates
37
* @param x - X coordinate for ripple center
38
* @param y - Y coordinate for ripple center
39
*/
40
setRippleXY(x: number, y: number): void;
41
42
/**
43
* Set ripple styling for animation phase
44
* @param start - Whether this is the start or end of animation
45
*/
46
setRippleStyles(start: boolean): void;
47
48
/** Handle animation frame updates */
49
animFrameHandler(): void;
50
}
51
```
52
53
**HTML Structure:**
54
55
```html
56
<!-- Button with ripple effect -->
57
<button class="mdl-button mdl-js-button mdl-js-ripple-effect">
58
Ripple Button
59
</button>
60
61
<!-- Checkbox with ripple effect -->
62
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="checkbox-ripple">
63
<input type="checkbox" id="checkbox-ripple" class="mdl-checkbox__input">
64
<span class="mdl-checkbox__label">Check with ripple</span>
65
</label>
66
67
<!-- Menu with ripple effect -->
68
<ul class="mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect"
69
for="demo-menu-button">
70
<li class="mdl-menu__item">Menu Item 1</li>
71
<li class="mdl-menu__item">Menu Item 2</li>
72
</ul>
73
74
<!-- Custom element with ripple -->
75
<div class="custom-element mdl-js-ripple-effect" tabindex="0">
76
Click me for ripple effect
77
</div>
78
```
79
80
**Usage Examples:**
81
82
Since ripple effects are largely automatic, direct API usage is rare, but here are some advanced use cases:
83
84
```javascript
85
// Access ripple instance (rarely needed)
86
const rippleElement = document.querySelector('.mdl-js-ripple-effect');
87
// Note: MaterialRipple instances are typically managed internally
88
89
// Programmatically trigger ripple effect
90
function triggerRipple(element, x, y) {
91
// Create a synthetic mouse event at specific coordinates
92
const event = new MouseEvent('mousedown', {
93
clientX: x,
94
clientY: y,
95
bubbles: true
96
});
97
98
element.dispatchEvent(event);
99
100
// Clean up with mouseup
101
setTimeout(() => {
102
const upEvent = new MouseEvent('mouseup', {
103
bubbles: true
104
});
105
element.dispatchEvent(upEvent);
106
}, 100);
107
}
108
109
// Add ripple effect to custom elements
110
function addRippleToElement(element) {
111
if (!element.classList.contains('mdl-js-ripple-effect')) {
112
element.classList.add('mdl-js-ripple-effect');
113
componentHandler.upgradeElement(element);
114
}
115
}
116
117
// Remove ripple effect
118
function removeRippleFromElement(element) {
119
element.classList.remove('mdl-js-ripple-effect');
120
// Remove ripple container if it exists
121
const rippleContainer = element.querySelector('.mdl-ripple-container');
122
if (rippleContainer) {
123
rippleContainer.remove();
124
}
125
}
126
127
// Custom ripple colors
128
function setRippleColor(element, color) {
129
const style = document.createElement('style');
130
const className = 'ripple-' + Math.random().toString(36).substr(2, 9);
131
132
element.classList.add(className);
133
134
style.textContent = `
135
.${className} .mdl-ripple {
136
background: ${color};
137
}
138
`;
139
140
document.head.appendChild(style);
141
}
142
143
// Usage examples
144
const customButton = document.querySelector('#custom-button');
145
addRippleToElement(customButton);
146
setRippleColor(customButton, '#ff4081');
147
148
// Trigger ripple on center of element
149
const rect = customButton.getBoundingClientRect();
150
const centerX = rect.left + rect.width / 2;
151
const centerY = rect.top + rect.height / 2;
152
triggerRipple(customButton, centerX, centerY);
153
```
154
155
### Ripple Effect Customization
156
157
```javascript
158
// Custom ripple implementation for non-standard elements
159
class CustomRippleManager {
160
constructor() {
161
this.ripples = new Map();
162
this.setupGlobalListeners();
163
}
164
165
setupGlobalListeners() {
166
document.addEventListener('mousedown', (event) => {
167
if (event.target.matches('[data-custom-ripple]')) {
168
this.createRipple(event.target, event);
169
}
170
});
171
172
document.addEventListener('mouseup', () => {
173
this.fadeAllRipples();
174
});
175
176
document.addEventListener('mouseleave', () => {
177
this.fadeAllRipples();
178
});
179
}
180
181
createRipple(element, event) {
182
const rect = element.getBoundingClientRect();
183
const size = Math.max(rect.width, rect.height);
184
const x = event.clientX - rect.left - size / 2;
185
const y = event.clientY - rect.top - size / 2;
186
187
const ripple = document.createElement('div');
188
ripple.className = 'custom-ripple';
189
ripple.style.cssText = `
190
position: absolute;
191
width: ${size}px;
192
height: ${size}px;
193
left: ${x}px;
194
top: ${y}px;
195
background: rgba(255, 255, 255, 0.3);
196
border-radius: 50%;
197
transform: scale(0);
198
pointer-events: none;
199
transition: transform 0.6s, opacity 0.6s;
200
`;
201
202
// Ensure element has relative positioning
203
if (getComputedStyle(element).position === 'static') {
204
element.style.position = 'relative';
205
}
206
207
// Ensure element has overflow hidden
208
element.style.overflow = 'hidden';
209
210
element.appendChild(ripple);
211
212
// Store ripple reference
213
this.ripples.set(ripple, { element, startTime: Date.now() });
214
215
// Trigger animation
216
requestAnimationFrame(() => {
217
ripple.style.transform = 'scale(2)';
218
});
219
}
220
221
fadeAllRipples() {
222
this.ripples.forEach((info, ripple) => {
223
const elapsed = Date.now() - info.startTime;
224
225
// Only fade if ripple has been visible for minimum time
226
if (elapsed > 100) {
227
ripple.style.opacity = '0';
228
229
setTimeout(() => {
230
if (ripple.parentNode) {
231
ripple.parentNode.removeChild(ripple);
232
}
233
this.ripples.delete(ripple);
234
}, 600);
235
}
236
});
237
}
238
}
239
240
// Initialize custom ripple manager
241
const customRippleManager = new CustomRippleManager();
242
243
// Usage: Add data-custom-ripple attribute to elements
244
// <div data-custom-ripple class="my-button">Custom Ripple</div>
245
```
246
247
### Performance Optimization
248
249
```javascript
250
// Optimized ripple effect with requestAnimationFrame
251
class OptimizedRipple {
252
constructor(element) {
253
this.element = element;
254
this.isAnimating = false;
255
this.setupListeners();
256
}
257
258
setupListeners() {
259
this.element.addEventListener('mousedown', (event) => {
260
if (!this.isAnimating) {
261
this.startRipple(event);
262
}
263
});
264
265
this.element.addEventListener('mouseup', () => {
266
this.endRipple();
267
});
268
269
this.element.addEventListener('mouseleave', () => {
270
this.endRipple();
271
});
272
}
273
274
startRipple(event) {
275
this.isAnimating = true;
276
277
const rect = this.element.getBoundingClientRect();
278
const rippleContainer = this.getRippleContainer();
279
280
const ripple = document.createElement('div');
281
ripple.className = 'optimized-ripple';
282
283
const size = Math.max(rect.width, rect.height) * 2;
284
const x = event.clientX - rect.left - size / 2;
285
const y = event.clientY - rect.top - size / 2;
286
287
ripple.style.cssText = `
288
position: absolute;
289
width: ${size}px;
290
height: ${size}px;
291
left: ${x}px;
292
top: ${y}px;
293
background: rgba(255, 255, 255, 0.3);
294
border-radius: 50%;
295
transform: scale(0);
296
opacity: 1;
297
pointer-events: none;
298
`;
299
300
rippleContainer.appendChild(ripple);
301
this.currentRipple = ripple;
302
303
// Use requestAnimationFrame for smooth animation
304
this.animateRipple(ripple, 0);
305
}
306
307
animateRipple(ripple, startTime) {
308
if (!startTime) startTime = performance.now();
309
310
const elapsed = performance.now() - startTime;
311
const duration = 600;
312
const progress = Math.min(elapsed / duration, 1);
313
314
// Easing function
315
const easeOut = 1 - Math.pow(1 - progress, 3);
316
317
ripple.style.transform = `scale(${easeOut})`;
318
319
if (progress < 1 && this.isAnimating) {
320
requestAnimationFrame(() => this.animateRipple(ripple, startTime));
321
}
322
}
323
324
endRipple() {
325
if (this.currentRipple && this.isAnimating) {
326
this.isAnimating = false;
327
328
// Fade out
329
this.currentRipple.style.transition = 'opacity 0.3s';
330
this.currentRipple.style.opacity = '0';
331
332
setTimeout(() => {
333
if (this.currentRipple && this.currentRipple.parentNode) {
334
this.currentRipple.parentNode.removeChild(this.currentRipple);
335
}
336
this.currentRipple = null;
337
}, 300);
338
}
339
}
340
341
getRippleContainer() {
342
let container = this.element.querySelector('.ripple-container');
343
344
if (!container) {
345
container = document.createElement('div');
346
container.className = 'ripple-container';
347
container.style.cssText = `
348
position: absolute;
349
top: 0;
350
left: 0;
351
right: 0;
352
bottom: 0;
353
overflow: hidden;
354
pointer-events: none;
355
`;
356
357
this.element.appendChild(container);
358
359
// Ensure parent has relative positioning
360
if (getComputedStyle(this.element).position === 'static') {
361
this.element.style.position = 'relative';
362
}
363
}
364
365
return container;
366
}
367
}
368
369
// Apply optimized ripple to elements
370
function addOptimizedRipple(element) {
371
if (!element.optimizedRipple) {
372
element.optimizedRipple = new OptimizedRipple(element);
373
}
374
}
375
376
// Usage
377
document.querySelectorAll('[data-optimized-ripple]').forEach(addOptimizedRipple);
378
```
379
380
## Ripple Constants
381
382
```javascript { .api }
383
/**
384
* Material Ripple constants and configuration
385
*/
386
interface RippleConstants {
387
/** Initial scale transform for ripple start */
388
INITIAL_SCALE: 'scale(0.0001, 0.0001)';
389
390
/** Initial size for ripple element */
391
INITIAL_SIZE: '1px';
392
393
/** Initial opacity for ripple start */
394
INITIAL_OPACITY: '0.4';
395
396
/** Final opacity for ripple end */
397
FINAL_OPACITY: '0';
398
399
/** Final scale transform for ripple end */
400
FINAL_SCALE: '';
401
}
402
```
403
404
### Animation Utilities
405
406
```javascript
407
// Utility functions for working with animations
408
class AnimationUtils {
409
static easeOutCubic(t) {
410
return 1 - Math.pow(1 - t, 3);
411
}
412
413
static easeInOutCubic(t) {
414
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
415
}
416
417
static animate(element, properties, duration, easing = 'easeOut') {
418
const startTime = performance.now();
419
const startValues = {};
420
421
// Get initial values
422
Object.keys(properties).forEach(prop => {
423
const currentValue = this.getNumericValue(element, prop);
424
startValues[prop] = currentValue;
425
});
426
427
const easingFunction = typeof easing === 'string' ?
428
this[easing] || this.easeOutCubic : easing;
429
430
const step = (currentTime) => {
431
const elapsed = currentTime - startTime;
432
const progress = Math.min(elapsed / duration, 1);
433
const easedProgress = easingFunction(progress);
434
435
Object.keys(properties).forEach(prop => {
436
const startValue = startValues[prop];
437
const endValue = properties[prop];
438
const currentValue = startValue + (endValue - startValue) * easedProgress;
439
440
this.setProperty(element, prop, currentValue);
441
});
442
443
if (progress < 1) {
444
requestAnimationFrame(step);
445
}
446
};
447
448
requestAnimationFrame(step);
449
}
450
451
static getNumericValue(element, property) {
452
const style = getComputedStyle(element);
453
const value = style[property];
454
return parseFloat(value) || 0;
455
}
456
457
static setProperty(element, property, value) {
458
switch (property) {
459
case 'scale':
460
element.style.transform = `scale(${value})`;
461
break;
462
case 'opacity':
463
element.style.opacity = value;
464
break;
465
default:
466
element.style[property] = value + 'px';
467
}
468
}
469
}
470
471
// Usage with ripple effects
472
function createAnimatedRipple(element, event) {
473
const rect = element.getBoundingClientRect();
474
const ripple = document.createElement('div');
475
476
// Setup ripple
477
const size = Math.max(rect.width, rect.height) * 2;
478
const x = event.clientX - rect.left - size / 2;
479
const y = event.clientY - rect.top - size / 2;
480
481
ripple.style.cssText = `
482
position: absolute;
483
width: ${size}px;
484
height: ${size}px;
485
left: ${x}px;
486
top: ${y}px;
487
background: rgba(255, 255, 255, 0.3);
488
border-radius: 50%;
489
transform: scale(0);
490
opacity: 0.4;
491
pointer-events: none;
492
`;
493
494
element.appendChild(ripple);
495
496
// Animate with custom easing
497
AnimationUtils.animate(ripple, { scale: 1 }, 600, AnimationUtils.easeOutCubic);
498
499
// Fade out after delay
500
setTimeout(() => {
501
AnimationUtils.animate(ripple, { opacity: 0 }, 300, (t) => t);
502
503
setTimeout(() => {
504
if (ripple.parentNode) {
505
ripple.parentNode.removeChild(ripple);
506
}
507
}, 300);
508
}, 400);
509
}
510
```
511
512
### Accessibility Considerations
513
514
```javascript
515
// Respect user preferences for reduced motion
516
function respectMotionPreferences() {
517
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
518
519
if (prefersReducedMotion) {
520
// Disable ripple effects
521
document.querySelectorAll('.mdl-js-ripple-effect').forEach(element => {
522
element.classList.remove('mdl-js-ripple-effect');
523
element.classList.add('mdl-js-ripple-effect--disabled');
524
});
525
526
// Add CSS to disable animations
527
const style = document.createElement('style');
528
style.textContent = `
529
.mdl-js-ripple-effect--disabled .mdl-ripple,
530
.custom-ripple,
531
.optimized-ripple {
532
animation: none !important;
533
transition: none !important;
534
}
535
`;
536
document.head.appendChild(style);
537
}
538
}
539
540
// Initialize on page load
541
document.addEventListener('DOMContentLoaded', respectMotionPreferences);
542
543
// Handle dynamic preference changes
544
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', respectMotionPreferences);
545
```
546
547
### Ripple Effect Themes
548
549
```javascript
550
// Different ripple themes for various contexts
551
const RippleThemes = {
552
light: {
553
background: 'rgba(0, 0, 0, 0.1)',
554
duration: 600
555
},
556
dark: {
557
background: 'rgba(255, 255, 255, 0.3)',
558
duration: 600
559
},
560
accent: {
561
background: 'rgba(255, 64, 129, 0.3)',
562
duration: 800
563
},
564
success: {
565
background: 'rgba(76, 175, 80, 0.3)',
566
duration: 600
567
},
568
warning: {
569
background: 'rgba(255, 152, 0, 0.3)',
570
duration: 600
571
},
572
error: {
573
background: 'rgba(244, 67, 54, 0.3)',
574
duration: 600
575
}
576
};
577
578
function applyRippleTheme(element, theme) {
579
const themeConfig = RippleThemes[theme];
580
if (!themeConfig) return;
581
582
element.addEventListener('mousedown', (event) => {
583
createThemedRipple(element, event, themeConfig);
584
});
585
}
586
587
function createThemedRipple(element, event, theme) {
588
const rect = element.getBoundingClientRect();
589
const ripple = document.createElement('div');
590
591
const size = Math.max(rect.width, rect.height) * 2;
592
const x = event.clientX - rect.left - size / 2;
593
const y = event.clientY - rect.top - size / 2;
594
595
ripple.style.cssText = `
596
position: absolute;
597
width: ${size}px;
598
height: ${size}px;
599
left: ${x}px;
600
top: ${y}px;
601
background: ${theme.background};
602
border-radius: 50%;
603
transform: scale(0);
604
opacity: 1;
605
pointer-events: none;
606
transition: transform ${theme.duration}ms cubic-bezier(0.4, 0, 0.2, 1),
607
opacity ${theme.duration * 0.5}ms ease-out;
608
`;
609
610
element.appendChild(ripple);
611
612
requestAnimationFrame(() => {
613
ripple.style.transform = 'scale(1)';
614
});
615
616
setTimeout(() => {
617
ripple.style.opacity = '0';
618
setTimeout(() => {
619
if (ripple.parentNode) {
620
ripple.parentNode.removeChild(ripple);
621
}
622
}, theme.duration * 0.5);
623
}, theme.duration * 0.7);
624
}
625
626
// Usage
627
applyRippleTheme(document.querySelector('#success-button'), 'success');
628
applyRippleTheme(document.querySelector('#error-button'), 'error');
629
```