Как оптимизировать или исправить анимацию волны?
Делаю примитивную анимацию волны. Но столкнулся с проблемой. Первая кривая Безье не согласуется с последующими, из-за этого пришлось спрятать этот участок за холстом.
Как можно упростить анимацию, ведь сейчас каждый раз создается новый путь?
Подозреваю, что можно отрисовать одиночный элемент, полуволну, которую повторить с помощью pattern, например. Но высота, ширина волны, а в дальнейшем и наклон могут меняться. Поэтому не хочу делать этот tile "в камне". Как можно сделать этот повторяющийся элемент стыкуемым и одновременно программно изменяемым? Можно ли решить с помощью SVG и CSS-анимаций?
Пример со спрятанными волнами в начале:
const SPEED = 0.05;
const WAVE_WIDTH = 100;
const WAVE_HEIGHT = 50;
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 200;
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width = CANVAS_WIDTH;
let h = canvas.height = CANVAS_HEIGHT;
ctx.fillStyle = 'blue';
let wave_fase = 0;
draw();
function draw(){
ctx.clearRect(0,0,w,h);
let waves_path_string = `M ${WAVE_WIDTH*(wave_fase - 3)} ${h}
V ${h/2 - WAVE_HEIGHT/2}
q ${WAVE_WIDTH/2} ${-WAVE_HEIGHT/2} ${WAVE_WIDTH} ${WAVE_HEIGHT/2} `;
waves_path_string += 't' + ` ${WAVE_WIDTH} 0`.repeat(w/WAVE_WIDTH + 3) + ` V ${h} z`;
ctx.fill(new Path2D(waves_path_string));
wave_fase = (wave_fase + SPEED)%2;
requestAnimationFrame(draw);
}
<canvas></canvas>
А здесь видно первую волну, которая не хочет встраиваться в общий ряд.
const SPEED = 0.01;
const WAVE_WIDTH = 60;
const WAVE_HEIGHT = 40;
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 200;
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width = CANVAS_WIDTH;
let h = canvas.height = CANVAS_HEIGHT;
ctx.fillStyle = 'blue';
let wave_fase = 0;
draw();
function draw(){
ctx.clearRect(0,0,w,h);
let waves_path_string = `M ${WAVE_WIDTH*wave_fase} ${h}
V ${h/2 - WAVE_HEIGHT/2}
q ${WAVE_WIDTH/2} ${-WAVE_HEIGHT/2} ${WAVE_WIDTH} ${WAVE_HEIGHT/2} `;
waves_path_string += 't' + ` ${WAVE_WIDTH} 0`.repeat(w/WAVE_WIDTH) + ` V ${h} z`;
ctx.fill(new Path2D(waves_path_string));
wave_fase = (wave_fase + SPEED)%2;
requestAnimationFrame(draw);
}
<canvas></canvas>
А здесь тот же код на SVG,- с манипулированием атрибута d в <path>:
const SPEED = 0.05;
const WAVE_WIDTH = 100;
const WAVE_HEIGHT = 50;
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 200;
let svg = document.querySelector('svg');
svg.setAttribute('width',CANVAS_WIDTH);
svg.setAttribute('height',CANVAS_WIDTH);
let wave_fase = 0;
draw();
function draw(){
let waves_path_string = `M ${WAVE_WIDTH*(wave_fase - 3)} ${CANVAS_HEIGHT}
V ${CANVAS_HEIGHT/2 - WAVE_HEIGHT/2}
q ${WAVE_WIDTH/2} ${-WAVE_HEIGHT/2} ${WAVE_WIDTH} ${WAVE_HEIGHT/2} `;
waves_path_string += 't' + ` ${WAVE_WIDTH} 0`.repeat(CANVAS_WIDTH/WAVE_WIDTH + 3) + ` V ${CANVAS_HEIGHT} z`;
svg.querySelector('path').setAttribute('d', waves_path_string);
wave_fase = (wave_fase + SPEED)%2;
requestAnimationFrame(draw);
}
<svg>
<path d="" fill="blue"/>
</svg>
Ответы (4 шт):
Попробуйте такой способ, он больше на волну похож.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const draw = (delta) => {
requestAnimationFrame(draw);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(0, 0, 255, 0.4)";
const randomLeft = Math.abs(Math.pow(Math.sin(delta / 1000), 2)) * 100;
const randomRight = Math.abs(Math.pow(Math.sin((delta / 1000) + 10), 2)) * 100;
const randomLeftConstraint = Math.abs(Math.pow(Math.sin((delta / 1000) + 2), 2)) * 100;
const randomRightConstraint = Math.abs(Math.pow(Math.sin((delta / 1000) + 1), 2)) * 100;
ctx.beginPath();
ctx.moveTo(0, randomLeft);
ctx.bezierCurveTo(canvas.width / 3, randomLeftConstraint, canvas.width / 3 * 2, randomRightConstraint, canvas.width, randomRight);
ctx.lineTo(canvas.width, canvas.height);
ctx.lineTo(0, canvas.height);
ctx.lineTo(0, randomLeft);
ctx.closePath();
ctx.fill();
}
requestAnimationFrame(draw);
html,
body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<canvas id="canvas"></canvas>
Подозреваю, что можно отрисовать одиночный элемент, полуволну, которую повторить с помощью pattern, например.
Первая кривая Безье не согласуется с последующими, из-за этого пришлось спрятать этот участок за холстом.
- Создаем одну волну с помощью кривых Безье
<svg class="editorial"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 24 150 28"
preserveAspectRatio="none">
<path
id="gentle-wave" fill="#4579e2"
d="m -160,44.4 c 30,0 58,
-18 87.7,-18 30.3,0 58.3,
18 87.3,18 30,0 58,-18 88,
-18 30,0 58,18 88,18 l 0,
34.5 -351,0 z" />
</svg>
- Клонируем эту волну три раза с разным цветом закраски
<svg class="editorial"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 24 150 28"
preserveAspectRatio="none">
<defs>
<path
id="gentle-wave"
d="m -160,44.4 c 30,0 58,
-18 87.7,-18 30.3,0 58.3,
18 87.3,18 30,0 58,-18 88,
-18 30,0 58,18 88,18 l 0,
34.5 -351,0 z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="50" y="0" fill="#4579e2"/>
<use xlink:href="#gentle-wave" x="50" y="3" fill="#3461c1"/>
<use xlink:href="#gentle-wave" x="50" y="6" fill="#2d55aa"/>
</g>
</svg>
- Добавляем анимацию волн CSS
@keyframes move-forever {
0% {
transform: translate(-90px, 0%);
}
100% {
transform: translate(85px, 0%);
}
}
Ниже полный код:
.parallax > use {
animation: move-forever 12s linear infinite;
}
.parallax > use:nth-child(1) {
animation-delay: -2s;
}
.parallax > use:nth-child(2) {
animation-delay: -2s;
animation-duration: 5s;
}
.parallax > use:nth-child(3) {
animation-delay: -4s;
animation-duration: 3s;
}
@keyframes move-forever {
0% {
transform: translate(-90px, 0%);
}
100% {
transform: translate(85px, 0%);
}
}
.editorial {
display: block;
width: 100%;
height: 17em;
margin: 0;
}
body {
background-color: #323232;
margin: 0;
padding: 9em 0 0 0;
}
.content {
font-family: 'Lato',sans-serif;
text-align: center;
background-color: #2d55aa;
text-align: center;
min-height: 75vh;
margin: -.1em 0 0 0;
padding: 1em;
color: #eee;
font-size: 2em;
font-weight: 300;
}
<svg class="editorial"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 24 150 28"
preserveAspectRatio="none">
<defs>
<path
id="gentle-wave"
d="m -160,44.4 c 30,0 58,
-18 87.7,-18 30.3,0 58.3,
18 87.3,18 30,0 58,-18 88,
-18 30,0 58,18 88,18 l 0,
34.5 -351,0 z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="50" y="0" fill="#4579e2"/>
<use xlink:href="#gentle-wave" x="50" y="3" fill="#3461c1"/>
<use xlink:href="#gentle-wave" x="50" y="6" fill="#2d55aa"/>
</g>
</svg>
За основу взят и переработан топик @Andy Fitzsimon на codepen
Update
По результатам обсуждения в комментариях, я понял, что автору вопроса нужно какое-то универсальное решение, где ничего не надо считать и получать результат, изменяя только входные данные. Вот великолепный ответ @Stranger in the Q
for (var i = 0; i < 5; i++)
wave(20 + i * 3, 70 - i * 10, 2 + 1 * Math.random(), 0.4 + i * 0.05, 40 + i * 25)
function wave(amp, freq, time, scaleY, translateY) {
let pts = [];
for (var x = 0; x < 2000; x += 10)
pts.push([x, (Math.sin(x / freq) * amp + translateY).toFixed(1)])
svg.innerHTML += `<path class="wave" d="M${pts.join('L')}v1000h-2000"
style="--t:${time}s;--sy:${scaleY};--tx:-${freq*2*Math.PI}px" />`;
}
.wave {
fill: steelblue;
stroke: white;
opacity: 0.5;
stroke-width: 2px;
animation: wave var(--t) linear infinite;
}
@keyframes wave {
0% {transform: scale(1, var(--sy)) translate(0px, 0px)}
50% {transform: translate(calc(var(--tx)*0.5), 0px)}
100% {transform: scale(1, var(--sy)) translate(var(--tx), 0px)}
}
<svg id=svg viewBox="0 0 1000 175" preserveAspectRatio="none"></svg>
Надеюсь это поможет в решении задачи.
Мне подход автора вопроса нравится. Почему первая кривая не стыкуется с последующими?
Потому что q задает относительные координаты, а первая точка - на вершине волны. Последующие "полупериоды" достраиваются относительно средней линии. Т.е. получается, что начальная и управляющая точка первого гребня задраны вверх.
Кривые Безье хорошо стыкуются, когда смежные управляющие точки являются отражениями друг друга, что в решении у @Leonid достигается командой t, и в этом прелесть подхода.
Небольшая корректировка исправляет проблему с первым гребнем:
const SPEED = 0.01;
const WAVE_WIDTH = 60;
const WAVE_HEIGHT = 80;
const CANVAS_WIDTH = 600;
const CANVAS_HEIGHT = 200;
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
let w = canvas.width = CANVAS_WIDTH;
let h = canvas.height = CANVAS_HEIGHT;
ctx.fillStyle = 'blue';
let wave_fase = 1;
draw();
function draw(){
ctx.clearRect(0,0,w,h);
let waves_path_string = `M ${WAVE_WIDTH*wave_fase} ${h}
V ${h/2}
q ${WAVE_WIDTH/2} ${WAVE_HEIGHT} ${WAVE_WIDTH} 0 `;
waves_path_string += 't' + ` ${WAVE_WIDTH} 0`.repeat(w/WAVE_WIDTH) + ` V ${h} z`;
ctx.fill(new Path2D(waves_path_string));
wave_fase = (wave_fase + SPEED) % 2;
requestAnimationFrame(draw);
}
<canvas id="canvas"></canvas>
Чтобы не создавать Path каждый раз в функции draw(), можно построить путь один раз и использовать "циклический" translate(), но я не пробовал.
Наверстал вам «бурю в стакане» на CSS:
body {
background: #172b3c;
margin: 0;
}
.sea {
width: 100vh;
height: 100vh;
position: relative;
background: #fff;
overflow: hidden;
margin: auto;
}
.wave {
position: absolute;
width: 200%;
height: 200%;
left: -40%;
top: 56%;
background-color: #57d0ff;
border-radius: 40%;
animation: wave 7s linear infinite;
}
@keyframes wave {
100% {
transform: rotate(360deg);
}
}
<div class="sea">
<div class="wave"></div>
</div>