Ёлочные шары как затухающий маятник

В одном из проектов заказчик попросил сделать реалистичную анимацию ёлочных шаров, которые начинали бы раскачиваться при касании мышью. Вот что из этого получилось.

Проведите мышкой по шарам, и они начнут качаться со все уменьшающейся амплитудой, пока не остановятся через некоторое время.

.ny-2018-wrapper {
position: relative;
overflow: hidden;
}

.ny-2018-balls {
position: absolute;
right: 0;
top: 0;
height: 50%;
width: 25%;
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;
}
}


jQuery(
function( $ ) {
// Анимация шариков
function calcTransform( arrParams, percent ) {
var valS = 0;
var valR = 0;
var valO = 0;
var length = arrParams.length;
for ( var i = 0; i 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 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 );
}
}
);
}
}
}
);
}
);

XMas Landscape
ball1
glow1
ball2
glow2
ball3
glow3
ball4
glow4
ball5
glow5

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://wordpressify.ru/wp-content/uploads/2021/03/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://wordpressify.ru/wp-content/uploads/2021/03/xmas-landscape.jpg 1005w"
		 sizes="(max-width: 1005px) 100vw, 1005px">
	<div class="ny-2018-balls">
		<img class="ball" id="ball1" alt="ball1" src="https://wordpressify.ru/wp-content/uploads/2021/03/ball1.png">
		<img class="glow" id="glow1" alt="glow1" src="https://wordpressify.ru/wp-content/uploads/2021/03/glow1.png">
		<img class="ball" id="ball2" alt="ball2" src="https://wordpressify.ru/wp-content/uploads/2021/03/ball2.png">
		<img class="glow" id="glow2" alt="glow2" src="https://wordpressify.ru/wp-content/uploads/2021/03/glow2.png">
		<img class="ball" id="ball3" alt="ball3" src="https://wordpressify.ru/wp-content/uploads/2021/03/ball3.png">
		<img class="glow" id="glow3" alt="glow3" src="https://wordpressify.ru/wp-content/uploads/2021/03/glow3.png">
		<img class="ball" id="ball4" alt="ball4" src="https://wordpressify.ru/wp-content/uploads/2021/03/ball4.png">
		<img class="glow" id="glow4" alt="glow4" src="https://wordpressify.ru/wp-content/uploads/2021/03/glow4.png">
		<img class="ball" id="ball5" alt="ball5" src="https://wordpressify.ru/wp-content/uploads/2021/03/ball5.png">
		<img class="glow" id="glow5" alt="glow5" src="https://wordpressify.ru/wp-content/uploads/2021/03/glow5.png">
	</div>
</div>

Источник: KAGG Design

Игорь Гергель

Единственный обладатель значков золотой WordPress и бронзовый WooCommerce на StackOverflow RU. WordPress Core contributor. Работал ведущим девелопером в команде WPML.

%d такие блоггеры, как: