Главная » Полезные статьи » Язык JavaScript » Быстрый взгляд на математику анимации в JavaScript
Распечатать статью

Быстрый взгляд на математику анимации в JavaScript

В школе я ненавидел математику. Мне она казалась надоедливой, сухой и скучной штукой с кучей старых книг, решающих очень теоретические проблемы. Даже хуже, многие задачи повторялись, с простым логическим изменением в каждой итерации (деление чисел, дифференциалы и т.д) Именно поэтому мы и придумали компьютеры. Достаточно сказать, что очень много домашних заданий по математике сделал мой старый верный Commodore 64 и Basic. Мне же оставалось просто скопировать результаты.

Эти инструменты и несколько уроков геометрии вдохновили меня сделать математику интересной для себя. Я сделал это в первую очередь путем создания визуальных эффектов, которые следуют правилам математических правил в демо, заставках и других, казалось бы бессмысленных вещах.

На самом деле очень много математики присутствует во всяких визуальных штуках, которые мы делаем, даже если мы это и не осознаем. Если вы хотите, чтоб что-то выглядело естественно и двигалось естественно, нужно добавить ему немного физики и скругления. Природа не построена на идеально прямых углах и линейных ускорениях. Вот почему зомби в фильмах выглядят такими жуткими.

Переходим с 0 к 1 без скуки

Если вы только-только начали программировать и вас просят сделать переход от 0 к 1 в несколько шагов, вы, вероятно, обратитесь к такому циклу:

for ( i = 0; i <= 1; i += 0.1 ) {
  x = i;
  y = i;
…
}

Это приведет к прямой линии на графике, то есть 45 градусов. Ничего в природе не движется с такой точностью:

Простым решением для того, чтобы сделать это движение более натуральным, стало бы умножение значения самого на себя:

for ( i = 0; i <= 1; i += 0.1 ) {
        x = i;
        y = i * i;
}

Это значит, что 0.1 будет 0.01, 0.2 будет 0.04, 0.3 будет 0.09, 0.4 будет 0.16, 0.5 будет 0.25 и так далее. В результате получается кривая, которая будет плавной вначале и все более крутой ближе к концу:

 

Вы можете сделать её еще более выраженной, продолжая умножать, либо используя силу функции Math.pow():

for ( i = 0; i <= 1; i += 0.1 ) {
        x = i;
        y = Math.pow( i, 4 );
}

Это один из приемов easing функций, используемых в таких библиотеках, как jQuery и YUI, а также в CSS переходах и анимации в современных браузерах.

Вы можете использовать этот способ, но есть еще более простой вариант для получения значений между 0 и 1 естественным путем.

Никаких нарушений, просто естественное движение

Синусоидные волны, вероятно, лучшее решение для плавной анимации. Они случаются в природе – волны океана, звук и свет. В нашем случае мы хотим добиться плавного продвижения от 0 к 1.

Для создания плавности движения от 0 к 1 и обратно к 0, мы можем использовать синусоиду, которая проходит от 0 до π с несколькими шагами. Полной синусоидой будет движение от 0 к π×2 (то есть весь круг), и это приведет в результате к значениям от -1 до 1, а мы этого не хотим (пока):

var counter = 0;

// 100 iterations
var increase = Math.PI / 100;

for ( i = 0; i <= 1; i += 0.01 ) {
  x = i;
  y = Math.sin(counter);
  counter += increase;
}

Обе функции Math.sin() и Math.cos() принимают в качестве параметра угол, который исчисляется в радианах. Нам же, простым людям, гораздо легче считать градусы в диапазоне от 0 к 360. Поэтому можно и нужно конвертировать радианы в градусы с помощью простой формулы:

var toRadian = Math.PI / 180;
var toDegree = 180 / Math.PI;

var angle = 30;

var angleInRadians = angle * toRadian;
var angleInDegrees = angleInRadians * toDegree;

Вернемся к нашим синусоидам. Вы можете экспериментировать с ними как угодно. К примеру, можно использовать абсолютное значение полного 2 × π цикла:

var counter = 0;
// 100 iterations
var increase = Math.PI * 2 / 100;

for ( i = 0; i <= 1; i += 0.01 ) {
  x = i;
  y = Math.abs( Math.sin( counter ) );
  counter += increase;
}

Но опять же, это не совсем то, что нужно. Если вам нужно полное продвижение вверх и вниз, без прерываний в середине, то надо немного сместить значения. Вы должны взять половину синусоиды и затем добавить 0.5 к ней:

var counter = 0;
// 100 iterations
var increase = Math.PI * 2 / 100;

for ( i = 0; i <= 1; i += 0.01 ) {
  x = i;
  y = Math.sin( counter ) / 2 + 0.5;
  counter += increase;
}

 

Итак, как можно использовать это? Имея функцию, которая возвращает значения от -1 к 1, что бы вы ей не скормили – получится круто. Все, что вам нужно сделать – это умножить её на те значения, которые вы хотите и добавить смещение для избежания отрицательных чисел.

Для примера посмотрите этот пример движения по синусоиде.

Выглядит аккуратно, не так ли? Большая часть трюка заложена уже в CSS:

.stage {
        width:200px;
        height:200px;
        margin:2em;
        position:relative;
        background:#6cf;
        overflow:hidden;
}
.stage div {
        line-height:40px;
        width:100%;
        text-align:center;
        background:#369;
        color:#fff;
        font-weight:bold;
        position:absolute;
}

Элемент stage имеет фиксированный размер и позиционируется относительно. Это означает, что все, что позиционируется абсолютно внутри него, будет относительно самого элемента.

Блок внутри элемента stage имеет высоту 40 пикселей и свойство position: absolute. Теперь все, что нам нужно сделать – это перемещать блок по синусоиде:

var banner = document.querySelector( '.stage div' ),
start = 0;
function sine(){
        banner.style.top = 50 * Math.sin( start ) + 80 + 'px';
        start += 0.05;
}
window.setInterval( sine, 1000/30 );

Начальное значение меняется постоянно, а с Math.sin() мы получаем хорошее волнообразное движение. Умножим его на 50, чтобы получить более широкую волну, и добавим еще 80 пикселей, чтобы центрировать её в элементе stage. Да, высота элемента 200 пикселей, и 100 – это ровно половина. Но высота нашего баннера 40 пикселей, поэтому нам нужно вычесть половину для центрирования.

Сейчас это просто движения вверх-вниз. Никто не мешает нам, однако, сделать его более интересным. Коэффициент умножения 50 например:

var banner = document.querySelector( '.stage div' ),
start = 0,
multiplier = 0;
function sine(){
        multiplier = 50 * Math.sin( start * 2 );
        banner.style.top = multiplier * Math.sin( start ) + 80 + 'px';
        start += 0.05;
}
window.setInterval( sine, 1000/30 );

Результатом этого является баннер, который, кажется, «осторожно, с сомнением» движется вверх-вниз. Возвращаясь в дни очень медленных Commodore 64, расчет живой синусоиды был слишком длительным процессом. Вместо этого, у нас имелись инструменты для создания таблиц синусов (массивы, если хотите). Одним из таких инструментов для создания прекрасной синусоиды, например подпрыгивающей бегущей строки, был Wix Bouncer:

 

Круги на песке, круг за кругом…

Круговое движение – это красиво. Оно радует глаз, напоминает нам о земле, на которой мы стоим, и в целом нам кажется, что это «не компьютерная штука». Математика рисования чего-либо по кругу в общем то не сложная вещь.

Своими корнями она восходит к Пифагору, который, по слухам, начертил много кругов на песке, прежде чем вывести свою знаменитую теорему. Если вы хотите использовать все хорошее, что есть в этой теореме, то попробуйте обратиться к треугольнику с прямым углом. Если его гипотенуза равна 1, то вы легко подсчитаете, что горизонтальный катет будет равняться косинусу угла, а вертикальный – синусу:

Как все это относится к кругу? Ну, довольно просто вписать треугольник с прямым углом в круг:

Это означает, что если вам надо вписать что-либо в круг (либо просто нарисовать его), то это можно сделать с помощью цикла и синуса с косинусом. Полный круг составляет 360 градусов, или 2 × π в радианах. Мы могли бы сразу приступить к этому делу, но для начала давайте подготовим кое-какой код.

Шаблон для рисования с помощью DOM

Обычно, мой выбор пал бы на canvas, но давайте используем простой DOM для кроссбраузерности. Эта функция-хелпер добавляет div-элементы в элемент stage и позволяет нам позиционировать их, менять их размеры, задавать цвет, менять их контент, вращать, не прибегая к помощи надоедливых свойств стиля элементов DOM.

Plot = function ( stage ) {
        this.setDimensions = function( x, y ) {
                this.elm.style.width = x + 'px';
                this.elm.style.height = y + 'px';
                this.width = x; this.height = y;
        }
        this.position = function( x, y ) {
                var xoffset = arguments[2] ? 0 : this.width / 2;
                var yoffset = arguments[2] ? 0 : this.height / 2;
                this.elm.style.left = (x - xoffset) + 'px';
                this.elm.style.top = (y - yoffset) + 'px';
                this.x = x; this.y = y;
        };
        this.setbackground = function( col ) {
                this.elm.style.background = col;
        }
        this.kill = function() {
                stage.removeChild( this.elm );
        }
        this.rotate = function( str ) {
                this.elm.style.webkitTransform = this.elm.style.MozTransform = this.elm.style.OTransform = this.elm.style.transform = 'rotate('+str+')';
        }
        this.content = function( content ) {
                this.elm.innerHTML = content;
        }
        this.round = function( round ) {
                this.elm.style.borderRadius = round ? '50%/50%' : '';
        }
        this.elm = document.createElement( 'div' );
        this.elm.style.position = 'absolute';
        stage.appendChild( this.elm );
};

Единственное, что может быть ново здесь – это трансформации с разными вендорными префиксами и позиционирование. Люди часто совершают ошибку, создавая div-ы с заданными размерами w и h, устанавливая их как координаты x и y экрана. Это означает, что вы всегда будете иметь дело со смещением по высоте и ширине. Путем вычитания половины ширины и высоты до позиционирования div, вы тем самым устанавливаете их там, где нужно – независимо от размеров. И вот доказательство:

Теперь давайте используем это, чтобы выстроить 10 прямоугольников в круг:

var stage = document.querySelector('.stage'),
    plots = 10;
    increase = Math.PI * 2 / plots,
    angle = 0,
    x = 0,
    y = 0;

for( var i = 0; i < plots; i++ ) {
  var p = new Plot( stage );
  p.setBackground( 'green' );
  p.setDimensions( 40, 40 );
  x = 100 * Math.cos( angle ) + 200;
  y = 100 * Math.sin( angle ) + 200;
  p.position( x, y );
  angle += increase;

Мы хотим выстроить по кругу 10 элементов. Значит нам нужно найти угол, под которым мы их расположим. Полный круг – это 2 раза по Math.PI. Просто разделим его. Позиции x и y наших прямоугольников могут быть рассчитаны с помощью заданного угла. x – это косинус, а y – синус, об этом нам говорит теорема Пифагора. Все, что нам нужно сделать дальше – отцентрировать круг на полотне (200,200 – координаты центра полотна). Это все, мы нарисовали круг с радиусом 100px за 10 итераций.

Проблема в том, что это выглядит ужасно. Если мы действительно хотим расположить элементы по кругу, то их углы также должны указывать на центр, не так ли? Для этого мы должны высчитать тангенс прямого угла. В JavaScript мы можем использовать Math.atan2(). Результат выглядит намного лучше:

var stage = document.querySelector(".stage"),
plots = 10;
increase = Math.PI * 2 / plots,
angle = 0,
x = 0,
y = 0;
for( var i = 0; i < plots; i++ ) {
        var p = new Plot( stage );
        p.setBackground( "green" );
        p.setDimensions( 40, 40 );
        x = 100 * Math.cos( angle ) + 200; y = 100 * Math.sin( angle ) + 200;
        p.rotate( Math.atan2( y - 200, x - 200 ) + "rad");
        p.position( x, y );
        angle += increase;
}

Обратите внимание, что поворот с помощью трансформации в CSS помогает нам обрабатывать все элементы вместе. В противном случае, математика для вращения наших прямоугольников принесла бы гораздо меньше удовольствия. CSS трансформации принимают радианы либо градусы в качестве своих значений. В данном случае мы используем rad, но если хотите, то можете указывать для поворота градусы, просто используйте deg в качестве значения.

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

var stage = document.querySelector('.stage'),
    plots = 10;
    increase = Math.PI * 2 / plots,
    angle = 0,
    x = 0,
    y = 0,
    plotcache = [];

for( var i = 0; i < plots; i++ ) {
  var p = new Plot( stage );
  p.setBackground( 'green' );
  p.setDimensions( 40, 40 );
  plotcache.push( p );
}

function rotate(){
  for( var i = 0; i < plots; i++ ) {
    x = 100 * Math.cos( angle ) + 200;
    y = 100 * Math.sin( angle ) + 200;
    plotcache[ i ].rotate( Math.atan2( y - 200, x - 200 ) + 'rad' );
    plotcache[ i ].position( x, y );
    angle += increase;
  }
  angle += 0.06;
}

setInterval( rotate, 1000/30 );

Хотите больше? Как на счет вращающегося текстового сообщения? Трюк состоит в том, что мы должны поворачивать символы на 90 градусов при каждой итерации:

var stage = document.querySelector('.stage'),
    message = 'Smashing Magazine '.toUpperCase(),
    plots = message.length;
    increase = Math.PI * 2 / plots,
    angle = -Math.PI,
    turnangle = 0,
    x = 0,
    y = 0,
    plotcache = [];

for( var i = 0; i < plots; i++ ) {
  var p = new Plot( stage );
  p.content( message.substr(i,1) );
  p.setDimensions( 40, 40 );
  plotcache.push( p );
}
function rotate(){
  for( var i = 0; i < plots; i++ ) {
    x = 100 * Math.cos( angle ) + 200;
    y = 100 * Math.sin( angle ) + 200;
    // rotation and rotating the text 90 degrees
    turnangle = Math.atan2( y - 200, x - 200 ) * 180 / Math.PI + 90 + 'deg';
    plotcache[ i ].rotate( turnangle );
    plotcache[ i ].position( x, y );
    angle += increase;
  }
  angle += 0.06;
}

setInterval( rotate, 1000/40 );

Опять же, здесь нет ничего фиксированного. Вы можете сделать так, чтоб радиус круга постоянно менялся, как мы делали уже раньше для подпрыгивающего баннера(ниже только отрывок):

multiplier = 80 * Math.sin( angle );
for( var i = 0; i < plots; i++ ) {
  x = multiplier * Math.cos( angle ) + 200;
  y = multiplier * Math.sin( angle ) + 200;
  turnangle = Math.atan2( y - 200, x - 200 ) * 180 / Math.PI + 90 + 'deg';
  plotcache[ i ].rotate( turnangle );
  plotcache[ i ].position( x, y );
  angle += increase;
}
angle += 0.06;

И, конечно же, вы так же можете смещать и центр круга:

rx = 50 * Math.cos( angle ) + 200;
ry = 50 * Math.sin( angle ) + 200;
for( var i = 0; i < plots; i++ ) {
  x = 100 * Math.cos( angle ) + rx;
  y = 100 * Math.sin( angle ) + ry;
  turnangle = Math.atan2( y - ry, x - rx ) * 180 / Math.PI + 90 + 'deg';
  plotcache[ i ].rotate( turnangle );
  plotcache[ i ].position( x, y );
  angle += increase;
}
angle += 0.06;

Ну и на десерт, как на счет того, чтобы разрешить только определенный диапазон координат?

function rotate() {
  rx = 70 * Math.cos( angle ) + 200;
  ry = 70 * Math.sin( angle ) + 200;
  for( var i = 0; i < plots; i++ ) {
    x = 100 * Math.cos( angle ) + rx;
    y = 100 * Math.sin( angle ) + ry;
    x = contain( 70, 320, x );
    y = contain( 70, 320, y );
    turnangle = Math.atan2( y - ry, x - rx ) * 180 / Math.PI + 90 + 'deg';
    plotcache[ i ].rotate( turnangle );
    plotcache[ i ].position( x, y );
    angle += increase;
  }
  angle += 0.06;
}
function contain( min, max, value ) {
  return Math.min( max, Math.max( min, value ) );
}

Итог

Это было только краткое введение в использование экспонент и синусоид для вписывания элементов в круг. Поэкспериментируйте с кодом и числами. Удивительно, как много крутых эффектов вы можете создать с помощью нескольких изменений радиуса или умножения углов.

Источник:  css-live.ru

Вы можете оставить комментарий, или обратную ссылку на Ваш сайт.

Оставить комментарий

Похожие статьи