В одном из проектов заказчик попросил сделать реалистичную анимацию ёлочных шаров, которые начинали бы раскачиваться при касании мышью. Вот что из этого получилось.
Проведите мышкой по шарам, и они начнут качаться со все уменьшающейся амплитудой, пока не остановятся через некоторое время.
JS-код оказался не слишком простым в реализации. Возможно, кому-то пригодятся заложенные в него идеи.
При наведении указателя мыши на шар инициализируется длительность раскачивания (от 3 до 7 секунд) и амплитуды раскачивания в двух плоскостях — плоскости экрана (maxAngle) и перпендикулярной плоскости (maxScale).
var params = {
init: 0,
duration: 5000 + ( Math.random() * 2 - 1 ) * 2000,
maxScale: 1 + ( Math.random() * 2 - 1 ) * 0.25,
maxAngle: 10 * ( Math.random() * 2 - 1 )
};
Вычисление положения на экране производится по формуле затухающего математического маятника:
function calcTransform( arrParams, percent ) {
var valS = 0;
var valR = 0;
var valO = 0;
var length = arrParams.length;
for ( var i = 0; i < length; i ++ ) {
var params = arrParams[i];
var time = ( params.init + percent ) * 100;
if ( time > 100 ) {
continue;
}
var cycles = 3;
var attenuation = 0.025;
var sin = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles );
var amplitude = sin * Math.exp( - attenuation * time );
valS += amplitude * ( params.maxScale - 1 );
valR += amplitude * params.maxAngle;
var sin_90 = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles - 90 * Math.PI / 180 );
valO = ( sin_90 + 1 ) / 2;
}
valO = valO / length;
valS = 1 + valS;
return {
transform: 'scale(' + valS + ') rotate(' + valR + 'deg)',
opacity: valO
};
}
Полный html, css, js код приведён ниже:
<style>
.ny-2018-wrapper {
position: relative;
}
.ny-2018-balls {
position: absolute;
right: 0;
top: 0;
height: 50vh;
width: 44vh;
z-index: 1;
-webkit-animation: slide-in-from-top 1s ease-in-out forwards;
-moz-animation: slide-in-from-top 1s ease-in-out forwards;
-o-animation: slide-in-from-top 1s ease-in-out forwards;
animation: slide-in-from-top 1s ease-in-out forwards;
}
.ny-2018-balls img {
cursor: pointer;
height: 100%;
position: absolute;
}
.ball {
transform-origin: center top;
}
.glow {
transform-origin: center center;
pointer-events: none;
-webkit-animation: glow 1.67s ease-in-out infinite;
-moz-animation: glow 1.67s ease-in-out infinite;
-o-animation: glow 1.67s ease-in-out infinite;
animation: glow 1.67s ease-in-out infinite;
}
#ball1 {
left: 0;
top: 9%;
height: 46%;
}
#ball2 {
left: 15%;
top: 3%;
height: 97%;
}
#ball3 {
left: 31%;
top: 0;
height: 71%;
}
#ball4 {
left: 51%;
top: 13%;
height: 87%;
}
#ball5 {
left: 72%;
top: 8%;
height: 61%;
}
#glow1 {
height: 14%;
left: -1.1%;
top: 42%;
transform-origin: center -231%;
opacity: 0;
animation-delay: 0.2s;
}
#glow2 {
height: 18.5%;
left: 13.5%;
top: 82.8%;
transform-origin: center -438%;
opacity: 0;
animation-delay: 0s;
}
#glow3 {
height: 23%;
left: 29.1%;
top: 49.6%;
transform-origin: center -213%;
opacity: 0;
animation-delay: 0.4s;
}
#glow4 {
height: 25.7%;
left: 49.3%;
top: 75.7%;
transform-origin: center -244%;
opacity: 0;
animation-delay: 1s;
}
#glow5 {
height: 28%;
left: 70%;
top: 43%;
transform-origin: center -124%;
opacity: 0;
animation-delay: 0.7s;
}
@-webkit-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-moz-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-o-keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@keyframes slide-in-from-top {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@-webkit-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-moz-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-o-keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes glow {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
<script>
jQuery(
function( $ ) {
// Анимация шариков
function calcTransform( arrParams, percent ) {
var valS = 0;
var valR = 0;
var valO = 0;
var length = arrParams.length;
for ( var i = 0; i < length; i ++ ) {
var params = arrParams[i];
var time = ( params.init + percent ) * 100;
if ( time > 100 ) {
continue;
}
var cycles = 3;
var attenuation = 0.025;
var sin = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles );
var amplitude = sin * Math.exp( - attenuation * time );
valS += amplitude * ( params.maxScale - 1 );
valR += amplitude * params.maxAngle;
var sin_90 = Math.sin( time / 100 * 360 * Math.PI / 180 * cycles - 90 * Math.PI / 180 );
valO = ( sin_90 + 1 ) / 2;
}
valO = valO / length;
valS = 1 + valS;
return {
transform: 'scale(' + valS + ') rotate(' + valR + 'deg)',
opacity: valO
};
}
$( '.ny-2018-balls .ball' ).hover(
function() {
if ( ! $( this ).hasClass( 'deaf' ) ) {
$( this ).addClass( 'deaf' );
var params = {
init: 0,
duration: 5000 + + ( Math.random() * 2 - 1 ) * 2000,
maxScale: 1 + ( Math.random() * 2 - 1 ) * 0.25,
maxAngle: 10 * ( Math.random() * 2 - 1 )
};
var arrParams = $( this ).data( 'arrParams' );
if ( typeof arrParams === 'undefined' ) {
arrParams = [];
} else {
var currentPercent = parseInt( $( this ).css( 'border-spacing' ) );
var length = arrParams.length;
for ( var i = 0; i < length - 1; i ++ ) {
arrParams[i].init += currentPercent;
if ( arrParams[i].init > 100 ) {
arrParams[i].init = 100;
}
}
arrParams[length - 1].init = currentPercent;
}
arrParams.push( params );
$( this ).data( 'arrParams', arrParams );
if ( ! $( this ).hasClass( 'animating' ) ) {
$( this ).addClass( 'animating' );
$( this ).dequeue().stop().animate(
{
// fake property, just to call step function
borderSpacing: 100 // must be 100, used as % of animation completed
},
{
step: function( now, fx ) {
var angle = now;
var percent = now / Math.abs( fx.end - fx.start );
var values = calcTransform( arrParams, percent );
$( this ).css( '-webkit-transform', values.transform );
$( this ).css( '-ms-transform', values.transform );
$( this ).css( 'transform', values.transform );
$( this ).next().css( '-webkit-transform', values.transform );
$( this ).next().css( '-ms-transform', values.transform );
$( this ).next().css( 'transform', values.transform );
},
duration: params.duration,
easing: 'linear',
complete: function() {
$( this ).removeClass( 'animating' ).dequeue();
$( this ).removeClass( 'deaf' );
$( this ).removeData( 'arrParams' );
$( this ).css( 'border-spacing', 0 );
}
}
);
}
}
}
);
}
);
</script>
<div class="ny-2018-wrapper">
<img src="https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape.jpg" alt="XMas Landscape"
srcset="https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-960x541.jpg 960w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-200x113.jpg 200w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-540x304.jpg 540w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape-768x433.jpg 768w, https://kagg.eu/wp-content/uploads/2019/01/xmas-landscape.jpg 1005w"
sizes="(max-width: 1005px) 100vw, 1005px">
<div class="ny-2018-balls">
<img class="ball" id="ball1" alt="ball1" src="https://kagg.eu/wp-content/uploads/2019/01/ball1.png">
<img class="glow" id="glow1" alt="glow1" src="https://kagg.eu/wp-content/uploads/2019/01/glow1.png">
<img class="ball" id="ball2" alt="ball2" src="https://kagg.eu/wp-content/uploads/2019/01/ball2.png">
<img class="glow" id="glow2" alt="glow2" src="https://kagg.eu/wp-content/uploads/2019/01/glow2.png">
<img class="ball" id="ball3" alt="ball3" src="https://kagg.eu/wp-content/uploads/2019/01/ball3.png">
<img class="glow" id="glow3" alt="glow3" src="https://kagg.eu/wp-content/uploads/2019/01/glow3.png">
<img class="ball" id="ball4" alt="ball4" src="https://kagg.eu/wp-content/uploads/2019/01/ball4.png">
<img class="glow" id="glow4" alt="glow4" src="https://kagg.eu/wp-content/uploads/2019/01/glow4.png">
<img class="ball" id="ball5" alt="ball5" src="https://kagg.eu/wp-content/uploads/2019/01/ball5.png">
<img class="glow" id="glow5" alt="glow5" src="https://kagg.eu/wp-content/uploads/2019/01/glow5.png">
</div>
</div>