С вашего позволения, публикую топик от имени моего безвременно 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