D3.js — это прежде всего библиотека визуализации данных. Вот для чего люди его используют. Делать блестящие вещи, которые поражают всех.
Но D3 — это нечто большее. Это мощная библиотека манипуляций с SVG . Да, некоторые люди скажут: «Но вам не нужна библиотека манипуляций с SVG! Вы можете просто написать SVG, как вы делаете HTML » . Эти люди глупы и, вероятно, пишут свои собственные функции манипулирования временем.
Недавно я сделал простую игру Pong с использованием D3. Ничего особенного, только два весла, которые вы можете перетаскивать, шар, который отскакивает туда-сюда, два счетчика очков и реагирует на изменения ориентации на мобильных устройствах. Нет даже кнопки запуска или остановки.
Вы можете поиграть в игру здесь , и увидеть код здесь .
Хотя это был быстрый проект, поэтому он не работает в Firefox из-за странной ошибки в размере холста SVG (мне нужно было это распространить по всему экрану), и некоторые люди говорили мне, что перетаскивание весла не работает на рабочем столе. Работал на меня. пожимание плечами
Положить его вместе
Сделать такую игру с помощью D3 не так уж и много.
Для начала нам нужен минимальный HTML:
01.
<!DOCTYPE html>
02.
<
meta
charset
=
"utf-8"
>
03.
<
meta
name
=
"viewport"
content
=
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"
/>
04.
<
title
>D3 pong</
title
>
06.
<
link
rel
=
"stylesheet"
href
=
"style.css"
>
07.
08.
<
main
></
main
>
09.
11.
<
script
src
=
"d3-pong.js"
></
script
>
HTML не интересен. Наша игра перейдет в <main></main>
тег. Остальное касается загрузки необходимых файлов и указания мобильным браузерам не вести себя смешно.
Нам также нужно немного CSS, чтобы наша игра занимала весь экран и все выглядело прилично.
01.
html, body, main {
02.
height
:
100%
;
03.
padding
:
0
;
04.
margin
:
0
;
05.
-webkit-box-sizing: border-box;
06.
-moz-box-sizing: border-box;
07.
box-sizing: border-box;
08.
}
09.
10.
svg {
11.
width
:
100%
;
12.
height
:
99%
;
/* gets rid of scrollbar */
13.
}
14.
15.
text {
16.
font-family
:
'Overlock'
,
cursive
;
17.
font-size
:
1.5em
;
18.
}
19.
20.
line {
21.
stroke:
black
;
22.
stroke-
width
:
2
;
23.
}
24.
25.
.area {
26.
fill:
white
;
27.
stroke:
"red"
;
28.
}
Базовые элементы
После этой наземной работы начинается самое интересное.
Мы создаем элемент SVG, определяем некоторые полезные поля и вспомогательную функцию, которая превращает свойства css как "10px"
числа.
01.
var
svg = d3.select(
"main"
)
02.
.append(
"svg"
),
03.
margin = {top: 10,
04.
right: 10,
05.
bottom: 10,
06.
left: 10},
07.
parse =
function
(N) {
08.
return
Number(N.replace(
"px"
,
""
));
09.
};
Просто.
Screen
Функция всегда будет сказать нам , сколько места у нас есть.
1.
var
Screen =
function
() {
2.
return
{
3.
width: parse(svg.style(
"width"
)),
4.
height: parse(svg.style(
"height"
))
5.
};
6.
};
Затем сделаем весло. Для дополнительного удовольствия, я сделал это так, что создание весла возвращает функцию, которую мы можем вызвать всякий раз, когда мы хотим обновить положение весла.
01.
Paddle =
function
(which) {
02.
var
width = 5,
03.
area = svg.append(
'rect'
)
04.
.classed(
'area'
,
true
)
05.
.attr({width: width*7}),
06.
paddle = svg.append(
'rect'
)
07.
.classed(
'paddle'
,
true
)
08.
.classed(which+
"_paddle"
,
true
)
09.
.attr({width: 5}),
10.
update =
function
(x, y) {
11.
var
height = Screen().height*0.15;
12.
13.
paddle.attr({
14.
x: x,
15.
y: y,
16.
height: height
17.
});
18.
area.attr({
19.
x: x-width*5/2,
20.
y: y,
21.
height: height
22.
});
23.
return
update;
24.
};
Поскольку пальцы толстые, а весла тонкие, мы определили, area
что это больше, чем фактическое весло. Это будет использоваться в качестве ручки перетаскивания. Тогда у нас есть paddle
само по себе — оба они просто SVG-прямоугольники.
update
Функция си немного более интересно, но не так много. Просто занимает x, y
позицию и обновления paddle
и area
.
Чтобы сделать весла перетаскиваемыми с помощью мыши или пальца, мы будем использовать перетаскиваемое поведение D3. Предварительно созданная функция, которую мы можем вызвать для любого элемента SVG, чтобы создать прослушиватели для всех соответствующих событий.
Все, что от нас требуется, это определить, что на самом деле происходит, когда что-то затягивают
Это идет внутри Paddle
функции.
// make paddle draggable var drag = d3.behavior.drag() .on("drag", function () { var y = parse(area.attr("y")), height = Screen().height*0.1; update(parse(paddle.attr("x")), Math.max(margin.top, Math.min(parse(paddle.attr("y"))+d3.event.dy, Screen().height-margin.bottom-height))); }) .origin(function () { return {x: parse(area.attr("x")), y: parse(area.attr("y"))}; }); area.call(drag); return update; },
"drag"
Событие представляет собой любой тип либо мыши или сенсорного события , которые могут представлять перетаскивание. В обратном вызове мы просто вызываем update
функцию с новыми координатами. Все это Math.max
и Math.min
чепуха гарантируют, что весла не могут быть вытащены из экрана.
.origin
это то, что нужно для правильного расчета позиций. Лучше всего просто установить его на текущую позицию нашего элемента.
area.call(drag);
активирует перетаскиваемое поведение на нашем перетаскиваемом area
.
Далее — функция, которая ведет счет.
// generates a score, returns function for updating value and repositioning score Score = function (x) { var value = 0, score = svg.append('text') .text(value); return function f(inc) { value += inc; score.text(value) .attr({x: Screen().width*x, y: margin.top*3}); return f; }; },
Мы собираемся снова с возвращением функций обновления. Но кроме этого это действительно просто, просто добавьте text
элемент в область рисования и присвойте ему значение. Не беспокойтесь об этом репозиционном материале.
Почти то же самое относится и к средней линии — добавьте строку, убедитесь, что она может быть перемещена при необходимости.
// generates middle line, returns function for updating position Middle = function () { var line = svg.append('line'); return function f() { var screen = Screen(); line.attr({ x1: screen.width/2, y1: margin.top, x2: screen.width/2, y2: screen.height-margin.bottom }); return f; };
Мяч будет немного веселее. Он не только должен нарисовать простой круг и иметь возможность перемещать его, он также должен реагировать на столкновение с препятствиями и обновление счета.
На этот раз мы собираемся вернуть функцию, которая делает полный шаг основной анимации. Вещи могут стать волосатыми.
Ball = function () { var R = 5, ball = svg.append('circle') .classed("ball", true) .attr({r: R, cx: Screen().width/2, cy: Screen().height/2}), scale = d3.scale.linear().domain([0, 1]).range([-1, 1]), vector = {x: scale(Math.random()), y: scale(Math.random())}, speed = 7;
Мы начали с простых вещей — рисование мяча, определение случайного вектора и увеличение скорости, которая хорошо смотрелась на моем экране.
Логика столкновений более привлекательна.
var hit_paddle = function (y, paddle) { return y-R > parse(paddle.attr("y")) && y+R < parse(paddle.attr("y"))+parse(paddle.attr("height")); }, collisions = function () { var x = parse(ball.attr("cx")), y = parse(ball.attr("cy")), left_p = d3.select(".left_paddle"), right_p = d3.select(".right_paddle"); // collision with top or bottom if (y-R < margin.top || y+R > Screen().height-margin.bottom) { vector.y = -vector.y; } // bounce off right paddle or score if (x+R > parse(right_p.attr("x"))) { if (hit_paddle(y, right_p)) { vector.x = -vector.x; }else{ return "left"; } } // bounce off left paddle or score if (x-R < parse(left_p.attr("x"))+parse(left_p.attr("width"))) { if (hit_paddle(y, left_p)) { vector.x = -vector.x; }else{ return "right"; } } return false; };
Hokay.
hit_paddle
это вспомогательная функция, которая сообщает нам, касается ли мяч весла — положение весла минус радиус шара. Просто.
collisions
выглядит волосатым, но очень повторяющимся
- если мяч касается верхнего или нижнего края, его вертикальное положение должно перевернуться.
- если мяч достаточно прав, чтобы поразить весло, он либо перевернет свое горизонтальное направление, либо сообщит вызывающий код, который
"right"
испортил - то же самое слева
- если ничего не происходит, вернуть false
Последняя часть Paddle
функции — это функция, которая выполняет шаг анимации.
return function f(left, right, delta_t) { var screen = Screen(), // this should pretend we have 100 fps fps = delta_t > 0 ? (delta_t/1000)/100 : 1; ball.attr({ cx: parse(ball.attr("cx"))+vector.x*speed*fps, cy: parse(ball.attr("cy"))+vector.y*speed*fps }); var scored = collisions(); if (scored) { if (scored == "left") { left.score(1); }else{ right.score(1); } return true; } return false; }; };
Это довольно просто. Сначала мы обновляем позицию мяча в соответствии с текущим вектором, затем проверяем наличие столкновений и обновляем оценки, если это необходимо. Наконец, мы возвращаемся true
или в false
зависимости от того, хотим ли мы продолжать текущую анимацию или нет.
Основной бит
Теперь, когда у нас есть все элементы, которые незаметно содержат большую часть нашего кода, пришло время привести их в движение.
// generate starting scene var left = {score: Score(0.25)(0), paddle: Paddle("left")(margin.left, Screen().height/2)}, right = {score: Score(0.75)(0), paddle: Paddle("right")(Screen().width-margin.right, Screen().height/2)}, middle = Middle()(), ball = Ball();
left
и right
удерживайте счет каждого игрока и функции обновления весла, и middle
и ball
являются средней линией и мячом.
Мы также должны реагировать на изменение размеров окна. Это скрытно отражает и изменения ориентации.
// detect window resize events (also captures orientation changes) d3.select(window).on('resize', function () { var screen = Screen(); left.score(0); left.paddle(margin.left, screen.height/2); right.score(0); right.paddle(screen.width-margin.right, screen.height/2); middle(); });
Когда экран меняется, мы просто обновляем положение всего. Посмотрите, как всегда возвращение функции обновления облегчало нашу жизнь?
И наконец, мы запускаем анимацию.
// start animation timer that runs until a player scores // then reset ball and start again function run() { var last_time = Date.now(); d3.timer(function () { var now = Date.now(), scored = ball(left, right, now-last_time), last_time = now; if (scored) { d3.select(".ball").remove(); ball = Ball(); run(); } return scored; }, 500); }; run();
Мы использовали d3.timer
для создания пользовательского цикла анимации, привязанного к скорости графики устройства пользователя. Чтобы противостоять этому, мы вводим дельту времени в нашу ball
функцию анимации, чтобы создать видимость постоянной скорости.
Фактические разработчики игр сказали мне, что я должен сделать это, и я сделал это.
Когда пользователь забивает, мы сбрасываем мяч в его текущую позицию и перезапускаем все. d3.timer
Основной цикл работает до тех пор, пока функция продолжает возвращаться false
. Мы позаботились об этом, вернувшись scored
.
Плавник
Вот и все. (ab) Использование D3 для создания простой игры в понг, потому что мы можем. Это был забавный хак, есть миллионы лучших инструментов, которые вы могли бы использовать, но мне было весело.