|
| 1 | +/** |
| 2 | + * Run animation. |
| 3 | + */ |
1 | 4 | window.onload = () => { |
2 | 5 | 'use strict'; |
3 | 6 |
|
4 | | - const canvas = document.getElementById('canvas') as HTMLCanvasElement; |
5 | | - const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; |
| 7 | + const canvas = document.getElementById('canvas'); |
| 8 | + const effectDisplacement = document.querySelector('filter feDisplacementMap'); |
| 9 | + const effectOffset = document.querySelector('filter feOffset'); |
6 | 10 |
|
7 | | - const effectDisplacement = document.querySelector('filter feDisplacementMap') as SVGFEDisplacementMapElement; |
8 | | - const effectOffset = document.querySelector('filter feOffset') as SVGFEOffsetElement; |
| 11 | + const animation = new SvgCanvasAnimation({ |
| 12 | + canvasElement: canvas, |
| 13 | + effectDisplacementElement: effectDisplacement, |
| 14 | + effectOffsetElement: effectOffset, |
| 15 | + }); |
9 | 16 |
|
10 | | - const centerX: number = canvas.width / 2; |
11 | | - const centerY: number = canvas.height / 2; |
12 | | - let offsetFactor: number = 0; |
13 | | - let radians: number = 0; |
14 | | - let i: number = 0; |
| 17 | + animation.start(); |
| 18 | +}; |
| 19 | + |
| 20 | +/** |
| 21 | + * SVG + Canvas animation class. |
| 22 | + */ |
| 23 | +class SvgCanvasAnimation { |
| 24 | + canvas: HTMLCanvasElement; |
| 25 | + ctx: CanvasRenderingContext2D; |
| 26 | + effectDisplacement: SVGFEDisplacementMapElement; |
| 27 | + effectOffset: SVGFEOffsetElement; |
| 28 | + centerX: number; |
| 29 | + centerY: number; |
| 30 | + offsetFactor: number = 0; |
| 31 | + rotation: number = 0; // Radians |
| 32 | + frame: number = 0; |
15 | 33 |
|
16 | 34 | /** |
17 | | - * Bad math - Values are ok for this screen size. |
18 | | - * Moving mouse to center (on X) increases factor; boundaries will become close to 0. |
19 | | - * |
20 | | - * - scaleFactor: Determines max. possible result. |
21 | | - * - offset: Left or right from center (must be positive). |
22 | | - * Higher offset will cancel out with scale factor and become close to 0. |
| 35 | + * Constructor sets canvas and filter effect elements |
| 36 | + * plus initial values for starting the animation. |
23 | 37 | */ |
24 | | - function onMouseMove(event: MouseEvent) { |
25 | | - const scaleFactor = centerX / 100; |
26 | | - const offset = (event.offsetX - centerX) / 100; |
| 38 | + constructor({ canvasElement, effectDisplacementElement, effectOffsetElement }) { |
| 39 | + this.canvas = canvasElement as HTMLCanvasElement; |
| 40 | + this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D; |
| 41 | + |
| 42 | + this.effectDisplacement = effectDisplacementElement as SVGFEDisplacementMapElement; |
| 43 | + this.effectOffset = effectOffsetElement as SVGFEOffsetElement; |
| 44 | + |
| 45 | + this.centerX = this.canvas.width / 2; |
| 46 | + this.centerY = this.canvas.height / 2; |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Public API – |
| 51 | + * Bind mouse event and start animation. |
| 52 | + */ |
| 53 | + start() { |
| 54 | + this.canvas.addEventListener('mousemove', this.onMouseMove); |
27 | 55 |
|
28 | | - offsetFactor = Math.abs(Math.abs(offset) - scaleFactor); |
| 56 | + this.loop(); |
29 | 57 | } |
30 | 58 |
|
31 | 59 | /** |
32 | 60 | * Frame-by-frame render process. |
| 61 | + * |
| 62 | + * 1) Optional feature demonstrated – Artificial delay for better visibility: |
| 63 | + * To render every N-th frame only; change expression 'i % 1' to higher numbers. |
| 64 | + * |
| 65 | + * @private |
33 | 66 | */ |
34 | | - function loop() { |
35 | | - // Artificial delay for better visibility: |
36 | | - // To render every N-th frame only; change expression 'i % 1' to higher numbers. |
37 | | - const renderFrame = i === 0 || i % 1 === 0; |
| 67 | + loop() { |
| 68 | + const frame = this.frame; |
| 69 | + const renderFrame = frame === 0 || frame % 1 === 0; // *1 |
38 | 70 |
|
39 | 71 | if (renderFrame) { |
40 | | - update(); |
41 | | - reset(); |
42 | | - draw(); |
| 72 | + this.update(); |
| 73 | + this.reset(); |
| 74 | + this.draw(); |
43 | 75 | } |
44 | 76 |
|
45 | | - window.requestAnimationFrame(loop); |
46 | | - i++; |
| 77 | + window.requestAnimationFrame(this.loop.bind(this)); |
| 78 | + this.frame++; |
47 | 79 | } |
48 | 80 |
|
49 | 81 | /** |
50 | 82 | * Update effect values. |
| 83 | + * |
| 84 | + * @private |
51 | 85 | */ |
52 | | - function update() { |
| 86 | + update() { |
| 87 | + const effectDisplacement = this.effectDisplacement; |
| 88 | + const effectOffset = this.effectOffset; |
| 89 | + |
| 90 | + const rnd: string = Math.random().toString(); |
53 | 91 | let scale: string = effectDisplacement.getAttribute('scale') + ''; |
54 | | - let rnd: string = Math.random().toString(); |
55 | 92 |
|
56 | | - // Effect size - Added random values = Stronger distortion and 'vibration' effect |
57 | | - scale = (16 + Math.sin(+scale) * offsetFactor).toString(); |
| 93 | + // Effect size: |
| 94 | + // - Define minimum size (arbitrary number, what looks good) |
| 95 | + // - Add random values for distortion and 'vibration' effect |
| 96 | + scale = (16 + Math.sin(+scale) * this.offsetFactor).toString(); |
58 | 97 |
|
59 | | - // Rotation speed |
60 | | - radians += 0.05; |
| 98 | + // Rotation movement |
| 99 | + this.rotation += 0.05; |
61 | 100 |
|
62 | 101 | effectOffset.setAttribute('dx', rnd); |
63 | 102 | effectOffset.setAttribute('dy', rnd); |
64 | 103 | effectDisplacement.setAttribute('scale', scale); |
65 | 104 | } |
66 | 105 |
|
67 | | - /** |
68 | | - * Reset screen by partly transparent filling for a fading effect. |
69 | | - */ |
70 | | - function reset() { |
71 | | - ctx.fillStyle = 'rgba(255, 255, 255, 0.25)'; |
72 | | - |
73 | | - ctx.fillRect(0, 0, canvas.width, canvas.height); |
74 | | - } |
75 | | - |
76 | 106 | /** |
77 | 107 | * Draw rectangle at screen center. |
| 108 | + * |
| 109 | + * @private |
78 | 110 | */ |
79 | | - function draw() { |
| 111 | + draw() { |
| 112 | + const ctx = this.ctx; |
| 113 | + |
80 | 114 | ctx.save(); |
81 | 115 |
|
82 | 116 | ctx.lineWidth = 24; |
83 | 117 | ctx.strokeStyle = 'rgba(255, 192, 0, 1)'; |
84 | 118 |
|
85 | | - ctx.translate(centerX, centerY); |
86 | | - ctx.rotate(radians); |
| 119 | + ctx.translate(this.centerX, this.centerY); |
| 120 | + ctx.rotate(this.rotation); |
87 | 121 | ctx.beginPath(); |
88 | 122 | ctx.rect(-100, -100, 200, 200); |
89 | 123 | ctx.stroke(); |
90 | 124 |
|
91 | 125 | ctx.restore(); |
92 | 126 | } |
93 | 127 |
|
94 | | - // Start |
95 | | - canvas.addEventListener('mousemove', onMouseMove); |
| 128 | + /** |
| 129 | + * Reset screen by partly transparent filling for a fading effect. |
| 130 | + * |
| 131 | + * @private |
| 132 | + */ |
| 133 | + reset() { |
| 134 | + const canvas = this.canvas; |
| 135 | + const ctx = this.ctx; |
96 | 136 |
|
97 | | - loop(); |
98 | | -}; |
| 137 | + ctx.fillStyle = 'rgba(255, 255, 255, 0.25)'; |
| 138 | + |
| 139 | + ctx.fillRect(0, 0, canvas.width, canvas.height); |
| 140 | + } |
| 141 | + |
| 142 | + /** |
| 143 | + * Moving mouse to center (on X) increases effect factor; boundaries will become close to 0. |
| 144 | + * Values are ok for this screen size. Bad math ;) |
| 145 | + * |
| 146 | + * - scaleFactor: Determines max. possible effect result. |
| 147 | + * - offset: Left or right from center (must be positive in all cases). |
| 148 | + * - Higher offset will cancel out with scale factor and become close to 0. |
| 149 | + * |
| 150 | + * @private |
| 151 | + */ |
| 152 | + onMouseMove(event: MouseEvent) { |
| 153 | + const centerX = this.centerX; |
| 154 | + |
| 155 | + const scaleFactor = centerX / 100; |
| 156 | + const offset = (event.offsetX - centerX) / 100; |
| 157 | + |
| 158 | + this.offsetFactor = Math.abs(Math.abs(offset) - scaleFactor); |
| 159 | + } |
| 160 | +} |
0 commit comments