Следим за фигурой не отрываясь от компьютера

в 4:06, , рубрики: bookmarklets, fun, jquery, букмарклеты, Веб-разработка, метки: , , ,

С вашего позволения, публикую топик от имени моего безвременно read-only друга и коллеги Terion. Возможно, мы промахнулись с хабом – сообщите нам об этом.

Следим за фигурой не отрываясь от компьютера

Нет, речь в посте пойдет не о гипножабе, которая через монитор закодирует вас на похудение, или еще о чем-то таком. Речь пойдет о счетчике потраченных за компьютером калорий. Точнее, потраченных на сайтах. А также о некоторых tips-n-tricks с анимацией.

Мы давно хотели сделать нечто подобное, а тут подвернулась возможность и мы с радостью принялись за дело. И получился тренажерный зал онлайн LazyS. Конечно, никто не претендует на абсолютную точность и, безусловно, это фановая игрушка, но цифры взяты и не с потолка. К тому же, там довольно забавные тренажеры.

Как это работает


Человек в среднем в сутки тратит 1200 ккал. Это показатель не для спортсменов или работников физического труда, а как раз для человека среднестатистического (работающего в офисе). На активную фазу суток (т.е. на бодрствование), в качестве расчетной точки, мы выделили 1000 ккал.

С ростом пульса — увеличивается и расход энергии. Соответственно, дальше считать довольно просто: замеряем скорость движения, не влияющую на пульс, берем её за опорную, приводим коэффициент для бОльших скоростей, замеряем время и умножаем на средний расход калорий за это время.

Конечно, это все очень приблизительно, но для поверхностной оценки — вполне пойдет.

Где это работает

Во-первых, на самом сайте есть забавные тренажеры. Во-вторых, есть букмарклет, который лучше всего работает на ajax-driven сайтах (в частности, отлично получается на vk.com). Заходите и смотрите, сколько тратите энергии на движения мышкой).

Немного о коде

Расчет дистанции, пройденной курсором — достаточно тривиальная задача (привет, теорема Пифагора). А вот о чем было бы интересно рассказать, так это о реализации гири (или «офисных вертолетов»), которая, на мой взгляд, получилась очень интересной.

Следим за фигурой не отрываясь от компьютера

Гиря должна вращаться по кругу, захваченная мышкой. Конечно, она должна удерживаться, когда мышка покидает гирю. А еще нам нужно считать угловую скорость и рассчитывать тень под ней.

Аниматор jQuery по координатам гири тут не поможет, пришлось искать другое решение. И оно нашлось довольно интересное: в качестве основы для вращения гири мы будем использовать отдельный элемент (#center) и манипулировать его margin-bottom.

При захвате гири мышкой — начинаем фиксировать угол, образующийся между курсором и центром круга и записываем его значение в margin-bottom элемента #center.

var a = centerPos.left - e.pageX;
var b = centerPos.top - e.pageY;
var c = Math.sqrt( Math.pow(a,2)+Math.pow(b,2) );
var angle = Math.asin(b/c) * -1;

Одновременно с этим рассчитываем положение гири, исходя из полученного угла (простым прямоугольным треугольником, где радиус трэка — его гипотенуза):

var a2 = R * Math.cos(angle);
var b2 = R * Math.sin(angle);
girja.css({'top': b2, 'left': a2}); // гиря позиционирована абсолютно относительно контейнера с position:relative, поэтому компенсировать ничего не нужно

При отрыве мышки, или когда её плечо относительно центра становится менее 40 точек (чтобы нельзя было крутить вокруг центра вплотную и накручивать счетчик) в действие вступает аниматор jQuery, который анимирует margin-bottom элемента #center до значения pi/2 (в радианах). Вся соль в двух вещах:

  • easeOutElastic easing, дающий красивый инерционный возврат
  • step-функция аниматора.

Степ-функция как раз и анимирует саму гирю. На каждом кадре анимации мы получаем текущий угол и по нему выставляем координаты гири:

step: function(now, fx){
	var a2 = R * Math.cos(now);
	var b2 = R * Math.sin(now);
	if (aUp < 0) { a2 = a2 * -1 };
	girja.css({'top': b2, 'left': a2});
}

Отдельно про тень: ее хотелось сделать живой и красивой. После того, что вышло с расчетом координат гири, отрисовать тень должным образом не составило труда. Но есть одна хитрость:

shad.css({'left': a2, 'opacity': Math.pow(b2 / R,15)});

Горизонтальное смещение тени равно смещению гири. А вот прозрачность считается делением вертикального сдвига гири на радиус трека, где оба члена возведены в 15ю степень.

Зачем такое приведение? Во-первых, степень должна быть нечетной, чтобы сохранить знак (когда гиря поднимается выше центра — смещение становится отрицательным и прозрачность не станет нарастать). Во-вторых степени «регулируют скорость приближения к границе». В кавычках, потому что описание совсем не математическое, но как это правильнее сказать — не знаю.

Когда b2 и R равны, деление, независимо от степени, даст 1. А вот по мере уменьшения b2 (горизонтального сдвига гири), чем выше степень обоих членов — тем быстрее результат деления приблизится к 0 и тень пропадет.

Из этого всего в итоге получился такой код:

$('#karusel-machine').each(function(){
		var girja = $('#girja');
		var shad = $('#shadow');
		var center = $('#center');
		var angle = 0;
		var R = 193;
		var win = $(window);
		var bottomRad = (Math.PI / 2).toFixed(3);
		
		girja.on('mousedown', function(e){
			center.stop(true, false);
			win.off('mouseup.girja');
			e.preventDefault();
			
			var centerPos = {left:center.offset().left, top: center.offset().top};
			
			var baseAngle = parseFloat(center.css('margin-bottom'));
			var startAngle = baseAngle;
			var moveStart = new Date().getTime();
			var moveEnd = 0;
			
	
			win.on('mousemove.girja', function(e){
				var a = centerPos.left - e.pageX;
				var b = centerPos.top - e.pageY;
				var c = Math.sqrt( Math.pow(a,2)+Math.pow(b,2) );
				
				if ( c < 40 ) {
					win.trigger('mouseup.girja', {pageX:e.pageX, pageY: e.pageY});
				}
				
				var angle = Math.asin(b/c) * -1;
				var delta = Math.abs(angle - startAngle);
				moveEnd = new Date().getTime();
				var t = moveEnd-moveStart;
				var V = delta/t;
				
				var k = V/0.005;
				if (k<1) k=1;
				var spentCals = calsPerMs*k*t;
				
				if (typeof(spentCals) == "number" && spentCals > 0) {
					window.calsSpent += spentCals;
					var kilocals = window.calsSpent / 1000;
					calsSpantInformer.text(kilocals.toFixed(2));
				}
				
				center.css({'margin-bottom':angle});
				
				startAngle = angle;
				moveStart = moveEnd;
				
				var a2 = R * Math.cos(angle);
				var b2 = R * Math.sin(angle);
				
				if (a > 0) {
					a2 = a2 * -1;
				}
				
				girja.css({'top': b2, 'left': a2});
				shad.css({'left': a2, 'opacity': Math.pow((b2/R),15)});
			});
			
			win.on('mouseup.girja', function(e){
				win.off('mouseup.girja');
				var aUp = parseFloat(girja.css('left'));
				var bUp = parseFloat(girja.css('top'));
				
				win.off('mousemove.girja');
				center.animate(
					{'margin-bottom': bottomRad },
					{
						duration: bUp>0?2500:2000,
						easing: 'easeOutElastic',
						complete: function(){
							center.stop(true, true).css({'margin-bottom': bottomRad });
							win.off('mouseup.girja');
						},
						step: function(now, fx){
							var a2 = R * Math.cos(now);
							var b2 = R * Math.sin(now);
							if (aUp < 0) { a2 = a2 * -1 };
							girja.css({'top': b2, 'left': a2});
							shad.css({'left': a2, 'opacity': Math.pow((b2/R),15)});
						},
						queue: false
					}
				)
			});
			
		});
		
	});

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

Автор: ckald

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js