Статьи

JavaScript анимация, которая работает (часть 4 из 4)

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

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

Если вы посмотрите на наш код с прошлого раза, вы увидите, что, хотя код работает хорошо (и с несколькими роботами), нет очень простого способа заставить код работать.

Обработчики событий — это команды, которые предписывают определенному коду запускаться при возникновении определенных событий. Например, вы можете запустить my_function() всякий раз, когда пользователь нажимает на ваш div с идентификатором 'my_div' . Или вы можете my_other_function() всякий раз, когда пользователь 'my_other_div' указатель мыши на 'my_other_div' .

В теории это довольно простая и понятная идея. К сожалению, как только вы начнете подключать разные браузеры, это может немного запутать. В идеальном мире каждый веб-браузер интерпретирует один и тот же код и HTML одинаково, а разработчики пишут код один раз, и он будет работать одинаково для каждого пользователя. В реальном мире разные браузеры могут иметь совершенно разные команды для выполнения одного и того же действия ( * кашель * * кашель * Internet Explorer ), и поэтому иногда попытка заставить один кусок кода работать одинаково во всех браузерах может показаться стадным кошки. В последнее время ситуация становится намного лучше, поскольку Chrome, Firefox, Safari и Opera очень схожи с кодом, Internet Explorer 9 и 10 стали в большей степени соответствовать стандартам, чем более ранние версии, и почти никто не использует Internet Explorer. 7 или 6 больше. Итак, для нашего кода мы получим обработчики событий для работы как с современными браузерами, так и с Internet Explorer 8.

Как примечание, это тот случай, когда действительно стоит использовать надежную библиотеку JavaScript, такую ​​как jQuery. jQuery выполняет всю работу за вас в кросс-браузерном тестировании, поэтому вам нужно будет ввести только одну команду, а библиотека jQuery переведет ее для каждого браузера за кулисами. Кроме того, многие из команд в jQuery намного более интуитивно понятны и проще, чем основной JavaScript.

Но, поскольку я упрям, и поскольку это возможность для обучения, мы продолжим трудный путь и будем делать все это исключительно с помощью JavaScript и без каких-либо зависимостей!

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

1
2
3
stage.addEventListener(‘mousemove’, stage_mousemove_listener, false);
robot.addEventListener(‘mouseover’, robot_mouseover_listener, false);
stage.addEventListener(‘mouseout’, stage_mouseout_listener, false);

Итак, в приведенных выше строках мы говорили, что всякий раз, когда пользователь перемещает мышь внутри элемента stage, мы запускаем функцию stage_mousemove_listener() (обратите внимание, мы не включаем скобки в команду). Аналогично, когда пользователь robot_mouseover_listener() указатель мыши на элемент робота, он запускает robot_mouseover_listener() , а когда пользователь перемещает мышь за пределы stage_mouseout_listener() , он вызывает stage_mouseout_listener() .

К сожалению, как мы упоминали ранее, Internet Explorer 8 и ниже имеет (похожую, но) другую команду для выполнения одной и той же операции, поэтому нам нужно проверить, какая команда будет понимать браузер пользователя, и выполнять этот метод.

1
2
3
4
5
6
7
8
9
if (stage.addEventListener){ // We will test to see if this command is available
  stage.addEventListener(‘mousemove’, stage_mousemove_listener, false);
  robot.addEventListener(‘mouseover’, robot_mouseover_listener, false);
  stage.addEventListener(‘mouseout’, stage_mouseout_listener, false);
} else { // If not, we have to use IE commands
  stage.attachEvent(‘onmousemove’, stage_mousemove_listener);
  robot.attachEvent(‘onmouseover’, robot_mouseover_listener);
  stage.attachEvent(‘onmouseout’, stage_mouseout_listener);
}

Вы можете заметить, что формат команд очень похож, но имеет некоторые существенные различия: один говорит 'addEventListener' а другой — 'attachEvent' . Один говорит 'mousemove' а другой — 'onmousemove' . Один требует третьего параметра, а другой использует только два. Смешивание любого из них приведет к тому, что команда не запустится. Это те вещи, которые заставят вас биться головой об стену. К сожалению, это не конец дополнительного кодирования, которое мы должны будем сделать для кросс-браузерной возможности.

Далее мы собираемся написать функции прослушивания. Мы начнем с функции, которая срабатывает, когда пользователь наводит курсор на сцену. Так как это слушатель mousemove , эта функция будет срабатывать каждый раз, когда мышь перемещается внутри области сцены (то есть она будет срабатывать несколько раз в секунду, пока мышь движется). Эта функция должна будет сравнить местоположение робота с местоположением мыши, и заставить робота вести себя соответственно. Каждый раз, когда функция запускается, она будет проверять, должен ли робот продолжать движение в том же направлении или изменить поведение. Итак, это должно быть что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Inside of RobotMaker
 
// We will need to introduce a few extra variables to track
var mouseX;
var running_dir = »;
var stageOffset;
 
function stage_mousemove_listener(e){
   
  // Find the horizontal position of the mouse inside of the stage …
  // That position will be saved in ‘mouseX’
   
  // Then we compare ‘mouseX’ to the robot, and decide if we need to run differently
  if (((robot.offsetLeft + (15 * run_speed)) < (mouseX — robot.offsetWidth)) && running_dir !== ‘r’ && (!jump_timer || jump_timer === undefined)){
    // If the mouse is in the stage and to the right of the robot, make run right, if not already
    running_dir = ‘r’;
    clearTimeout(run_timer);
    run_r(1, robot.offsetLeft);
  } else if ((mouseX < robot.offsetLeft — (15 * run_speed)) && running_dir !== ‘l’ && (!jump_timer || jump_timer === undefined)) {
    // If the mouse is in the stage and to the left of the robot, make run left, if not already
    running_dir = ‘l’;
    clearTimeout(run_timer);
    run_l(1, robot.offsetLeft);
  } else if ((robot.offsetLeft < mouseX) && ((robot.offsetLeft + robot.offsetWidth) > mouseX) && running_dir !== » && (!jump_timer || jump_timer === undefined)) {
    // If the mouse is in the stage and over a robot, stop and clear running_dir
    running_dir = »;
    clearTimeout(run_timer);
    if (face_right){
      robot.style.backgroundPosition = «0px 0px»;
    } else {
      robot.style.backgroundPosition = «0px -50px»;
    }
  }
  // If none of the above is true, then we let our current behavior continue
}

Итак, в приведенной выше функции, как только мы сможем найти mouseX , мы сравниваем его с тем, где находится робот, и при необходимости запускаем или останавливаем различные запущенные функции. К сожалению, найти mouseX немного сложно, так как положение мыши — это еще одна вещь, которую разные браузеры делают по-разному. Вместо (более) сложных и многословных объяснений, здесь кросс-браузерный метод поиска mouseX , вдохновленный отличным блогом Quirksmode (который является отличным источником для более углубленного изучения JavaScript).

01
02
03
04
05
06
07
08
09
10
11
12
13
function stage_mousemove_listener(e){
  var posX = 0;
  if (!e){
    var e = window.event;
  }
  
  if (e.pageX) {
    posX = e.pageX;
  } else if (e.clientX) {
    posX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  }
  mouseX = posX — stageOffset.xpos;
}

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

Наконец, мы находим mouseX , находя posX (который является x-положением мыши на странице) и вычитая, как далеко сцена находится от левого края страницы (хранится в stageOffset.xpos ). Это дает нам информацию о том, как далеко от левого края сцены находится мышь, что мы можем напрямую сравнить с robot.offsetLeft . Поскольку сцена может располагаться по-разному вокруг страницы в зависимости от макета, нам также необходимо найти точное смещение пикселя сцены, чтобы функция была точной, и сохранить эту информацию в stageOffset . К счастью, есть полезная хитрость, которую мы можем использовать, чтобы найти абсолютное смещение элемента с помощью этой функции из блога Вишала Астика .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
// Inside RobotMaker
var x = 0;
var y = 0;
function find_stage_offset (el){
  x = el.offsetLeft;
  y = el.offsetTop;
  el = el.offsetParent;
     
  while(el !== null) {
    x = parseInt(x) + parseInt(el.offsetLeft);
    y = parseInt(y) + parseInt(el.offsetTop);
    el = el.offsetParent;
  }
 
  return {xpos: x, ypos: y};
}
var stageOffset = find_stage_offset(stage);

Итак, теперь, когда мы написали слушатель mousemove , остальные будут намного проще. Для слушателя при mouseover нам нужно только проверить, прыгает ли робот, и если нет, остановить таймер запуска и заставить его прыгать.

1
2
3
4
5
6
function robot_mouseover_listener(){
  if (!jump_timer || jump_timer === undefined){
    clearTimeout(run_timer);
    jmp(true, robot.offsetTop);
  }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
function stage_mouseout_listener(){
  mouseX = undefined;
  running_dir = »;
  if (!jump_timer || jump_timer === undefined){
    clearTimeout(run_timer);
    if (face_right){
      robot.style.backgroundPosition = «0px 0px»;
    } else {
      robot.style.backgroundPosition = «0px -50px»;
    }
  }
}

Функции, которые оживляют беговые и прыжковые движения, на этот раз не сильно изменились. Мы только что добавили отслеживающую переменную running_dir , running_dir оператор, который проверяет, собирается ли робот поразить стену (поскольку это избыточно с нашей функцией mouseout ), и добавили немного кода в функцию jump, которая снова проверяет, если Робот должен начать бегать, если мышь находится на сцене после того, как приземлится от прыжка. Вот окончательный код (довольно большой):

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
function run_r(phase, left){
  face_right = true;
  running_dir = ‘r’;
  if ((left + (15 * run_speed)) < (mouseX — robot.offsetWidth)){ // if mouse is to the right, run
         
    left = left + (15 * run_speed);
    robot.style.left = left+»px»;
    switch (phase){
      case 1:
        robot.style.backgroundPosition = «-40px 0px»;
        run_timer = setTimeout(function(){run_r(2, left);}, 200);
        break;
      case 2:
        robot.style.backgroundPosition = «-80px 0px»;
        run_timer = setTimeout(function(){run_r(3, left);}, 200);
        break;
      case 3:
        robot.style.backgroundPosition = «-120px 0px»;
        run_timer = setTimeout(function(){run_r(4, left);}, 200);
        break;
      case 4:
        robot.style.backgroundPosition = «-80px 0px»;
        run_timer = setTimeout(function(){run_r(1, left);}, 200);
        break;
    }
} else if ((left + (15 * run_speed)) < mouseX) { // if mouse if above, stop
    robot.style.backgroundPosition = «0px 0px»;
    running_dir = »;
} else { // if mouse is to the left, run left
    running_dir = ‘l’;
    run_l(1, robot.offsetLeft);
  }
}
 
function run_l(phase, left){
  face_right = false;
  running_dir = ‘l’;
  if (mouseX < robot.offsetLeft — (15 * run_speed)){ // if mouse is to the left, run
     
    left = left — (15 * run_speed);
    robot.style.left = left+»px»;
    switch (phase){
      case 1:
        robot.style.backgroundPosition = «-40px -50px»;
        run_timer = setTimeout(function(){run_l(2, left);}, 200);
        break;
      case 2:
        robot.style.backgroundPosition = «-80px -50px»;
        run_timer = setTimeout(function(){run_l(3, left);}, 200);
        break;
      case 3:
        robot.style.backgroundPosition = «-120px -50px»;
        run_timer = setTimeout(function(){run_l(4, left);}, 200);
        break;
      case 4:
        robot.style.backgroundPosition = «-80px -50px»;
        run_timer = setTimeout(function(){run_l(1, left);}, 200);
        break;
    }
} else if (mouseX < (robot.offsetLeft + robot.offsetWidth — (15 * run_speed))){ // if mouse overhead, stop
    robot.style.backgroundPosition = «0px -50px»;
    running_dir = »;
} else { // if mouse is to the right, run right
    running_dir = ‘r’;
    run_r(1, robot.offsetLeft);
  }
}
                 
function jmp(up, top){
  running_dir = »;
  if (face_right){
    robot.style.backgroundPosition = «-160px 0px»;
  } else {
    robot.style.backgroundPosition = «-160px -50px»;
  }
 
  if (up && (robot.offsetTop > (20 * (1 / jump_height)))){
    top = top — (top * 0.1);
    robot.style.top = top+»px»;
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else if (up) {
    up = false;
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else if (!up && (robot.offsetTop < 115)){
    top = top + (top * 0.1);
    robot.style.top = top+»px»;
    jump_timer = setTimeout(function(){jmp(up, top);}, 60);
  } else {
    robot.style.top = «120px»;
    if (face_right){
      robot.style.backgroundPosition = «0px 0px»;
    } else {
      robot.style.backgroundPosition = «0px -50px»;
    }
     
    jump_timer = false;
    if (mouseX !== undefined){
      if (((robot.offsetLeft + (15 * run_speed)) < (mouseX — robot.offsetWidth)) && running_dir !== ‘r’){
        // make run right, if not already
        running_dir = ‘r’;
        clearTimeout(run_timer);
        run_r(1, robot.offsetLeft);
      } else if ((mouseX < robot.offsetLeft — (15 * run_speed)) && running_dir !== ‘l’) {
        // make run left, if not already
        running_dir = ‘l’;
        clearTimeout(run_timer);
        run_l(1, robot.offsetLeft);
      }
    }
  }
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
(function (){
  if (stage.addEventListener){
    stage.addEventListener(‘touchstart’, stage_mousemove_listener, false);
    stage.addEventListener(‘touchmove’, stage_mousemove_listener, false);
    stage.addEventListener(‘touchend’, stage_mouseout_listener, false);
         
    stage.addEventListener(‘mousemove’, stage_mousemove_listener, false);
    robot.addEventListener(‘mouseover’, robot_mouseover_listener, false);
    stage.addEventListener(‘mouseout’, stage_mouseout_listener, false);
  } else {
    stage.attachEvent(‘onmousemove’, stage_mousemove_listener);
    robot.attachEvent(‘onmouseover’, robot_mouseover_listener);
    stage.attachEvent(‘onmouseout’, stage_mouseout_listener);
  }
})();

Нам не нужно беспокоиться о том, что слушатели касания находятся в формате Internet Explorer 8, и если какое-либо устройство не имеет поддержки касания, оно будет игнорировать слушателей. Теперь нам нужно обновить stage_mousemove_listener() чтобы она stage_mousemove_listener() иначе, если браузер имеет сенсорную функцию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function stage_mousemove_listener(e){
/*
 * First we check if this is a touch screen device (if it has e.touches)
 */
  if (e.touches){
    e.preventDefault();
    // If the touch was within the boundaries of the stage…
    if ((e.touches[0].pageX > stageOffset.xpos)
    && (e.touches[0].pageX < (stageOffset.xpos + stage.offsetWidth))
    && (e.touches[0].pageY > stageOffset.ypos)
    && (e.touches[0].pageY < (stageOffset.ypos + stage.offsetHeight))){
      // we set the mouseX to equal the px location inside the stage
      mouseX = e.touches[0].pageX — stageOffset.xpos;
    } else { // if the touch was outside the stage, we call the mouseout listener
      stage_mouseout_listener();
    }
     
    /*
     * If the touch is directly on the robot, then we stop the run timer and make the robot jump
     */
    if ((e.touches[0].pageX > robot.offsetLeft) && (e.touches[0].pageX < (robot.offsetLeft + robot.offsetWidth))
    && (e.touches[0].pageY > (stageOffset.ypos + stage.offsetHeight — robot.offsetHeight))
    && (e.touches[0].pageY < (stageOffset.ypos + stage.offsetHeight))
    && (!jump_timer || jump_timer === undefined)){
      clearTimeout(run_timer);
      jmp(true, robot.offsetTop);
    }
     
  } else { // Finding the mouseX for non-touch devices…
    // All of our non-touch device code here
  }
}

Вы можете заметить, что у нас больше нет «дверей» в нашей функции RobotMaker , но, поскольку мы вызываем весь наш код с помощью обработчиков событий, которые мы назначаем внутри RobotMaker , они нам больше не нужны! Как для нашей сцены, так и для наших персонажей, мы хотим добавить немного CSS специально для сенсорных устройств, чтобы он не пытался вырезать и вставлять какие-либо изображения, когда пользователь нажимает на них пальцем.

1
2
3
#stage, .character {
  -webkit-user-select: none;
}

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

1
2
3
4
5
6
(function(){
  var j = RobotMaker(document.getElementById(‘j’), 1, 1);
  var j2 = RobotMaker(document.getElementById(‘j2’), .8, 5);
  var j3 = RobotMaker(document.getElementById(‘j3’), 1.1, .5);
  var j4 = RobotMaker(document.getElementById(‘j4’), .5, .75);
})();

Пожалуйста, проверьте окончательный результат во всей красе!

Я настоятельно рекомендую вам изучить весь (и полностью прокомментированный!) Код , и вы также можете скачать все четыре спрайта роботов здесь .

Счастливого оживления!