Доброго времени суток, уважаемый читатель! В прошлый раз мы изучали процесс создания интерактивной карты-хороплета, теперь предлагаю немного усложнить задачу и перейти к трёхмерной модели Земли, именуемой в народе глобусом. Глобус делать будем двух видов: SVG версия и Canvas версия. В обоих случаях будем использовать JavaScript библиотеку d3.js. У каждого варианта свои преимущества. В моём исполнении Голубая планета выглядит следующим образом:
А как создать свой собственный Мир с материками и океанами можно узнать под катом.
Начало
Сперва нам нужно найти геоданные. Как и в прошлый раз мы будем использовать TopoJSON для этих целей. О том как его получить можно прочитать в предыдущей статье в разделе «Дела картографические». И так у нас есть TopoJSON файл world-110m.json
для карты с масштабом 1:110,000,000, или 1 см = 1,100 км (1″ = 1,736 миль) и файл world-110m-country-names.tsv
с названиями стран вида id
— название страны. Внешний файл с названиями используется для удобства, так как в этом случае можно легко перевести названия на любой язык. Всё, можно приступать непосредственно к созданию глобуса.
Рисуем интерактивный глобус
Нашей целью будет глобус, который можно:
- вращать мышкой «хватая» за сушу
- центрировать глобус на страну, выбранную из списка
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Nice title</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/queue.v1.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
</head>
<style>
Your awesome CSS
</style>
<body>
<h1>Cool Header</h1>
<script>
Your awesome d3.js code
</script>
</body>
</html>
Определим основные переменные и добавим DOM элементы.
var width = 600,
height = 500,
sens = 0.25,
focused;
//Setting projection
var projection = d3.geo.orthographic()
.scale(245)
.rotate([0, 0])
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
//SVG container
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
//Adding water
svg.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path);
var countryTooltip = d3.select("body").append("div").attr("class", "countryTooltip"),
countryList = d3.select("body").append("select").attr("name", "countries");
Переменная sens
отвечает за точность при вращении мышкой, а focused
используется как триггер для выбранной (центрированной) страны. Про используемую проекцию можно почитать на wikipedia: Orthographic projection. Метод .clipAngle() определяет какую часть сферы мы будем отображать (а точнее видеть), про это опять же можно почитать на wikipedia: small-circle clipping. Остальное вроде в разъяснениях не нуждается.
Далее мы загружаем наши файлы при помощи библиотеки queue.js, которая позволяет делать нам это асинхронно.
queue()
.defer(d3.json, "data/world-110m.json")
.defer(d3.tsv, "data/world-110m-country-names.tsv")
.await(ready);
Теперь перейдём к главной функции, в нашем случае она называется ready
. Вначале, мы добавляем названия стран в наш dropdown list и отрисовываем страны на глобусе.
function ready(error, world, countryData) {
var countryById = {},
countries = topojson.feature(world, world.objects.countries).features;
//Adding countries to select
countryData.forEach(function(d) {
countryById[d.id] = d.name;
option = countryList.append("option");
option.text(d.name);
option.property("value", d.id);
});
//Drawing countries on the globe
var world = svg.selectAll("path.land")
.data(countries)
.enter().append("path")
.attr("class", "land")
.attr("d", path)
Перейдём к обработке событий мыши. Здесь пояснений требует drag.origin(), он позволяет нам задать «оригинальные» (действительные) стартовые координаты при захвате элемента, в нашем случае широту и долготу.
//Drag event
.call(d3.behavior.drag()
.origin(function() { var r = projection.rotate(); return {x: r[0] / sens, y: -r[1] / sens}; })
.on("drag", function() {
var rotate = projection.rotate();
projection.rotate([d3.event.x * sens, -d3.event.y * sens, rotate[2]]);
svg.selectAll("path.land").attr("d", path);
svg.selectAll(".focused").classed("focused", focused = false);
}))
//Mouse events
.on("mouseover", function(d) {
countryTooltip.text(countryById[d.id])
.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("display", "block")
.style("opacity", 1);
})
.on("mouseout", function(d) {
countryTooltip.style("opacity", 0)
.style("display", "none");
})
.on("mousemove", function(d) {
countryTooltip.style("left", (d3.event.pageX + 7) + "px")
.style("top", (d3.event.pageY - 15) + "px");
});
Для реализации фокусировки на стране нам необходимо написать функцию, которая бы возвращала нам геоданные для страны по её id
'шнику. Собственно вот она.
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
if(cnt[i].id == sel.value) {return cnt[i];}
}
};
Теперь можно непосредственно перейти к реализации фокусировки (центровки) на стране, выбранной из списка.
//Country focus on option select
d3.select("select").on("change", function() {
var rotate = projection.rotate(),
focusedCountry = country(countries, this),
p = d3.geo.centroid(focusedCountry);
svg.selectAll(".focused").classed("focused", focused = false);
//Globe rotating
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
svg.selectAll("path").attr("d", path)
.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
.transition();
})();
});
Здесь вся соль кроется в transition.tween(), который позволяет нам вызывать заданную функцию (поворот) для каждого интерполированного значения.
Крутится, вертится шар голубой.
Всё — SVG глобус готов. Исходники можно найти на GitHub (там же можно задать вопросы тем у кого read-only на Хабрахабре), а пощупать результат можно через сервис bl.ocks.org.
Давайте рассмотрим преимущества SVG:
- Возможность взаимодействовать с DOM элементами, в частности
path
- Возможность использовать CSS (как следствие из предыдущего пункта)
- Текст является текстом, со всеми вытекающими отсюда плюсами
Анимация планеты Земля
С SVG реализацией вроде разобрались, давайте посмотрим как сделать что-то подобное на canvas. Создадим простую анимацию вращения Земли. Тут многое будет аналогично предыдущему примеру. Кода мало, поэтому приведу его весь сразу.
var width = 800,
height = 500;
var projection = d3.geo.orthographic()
.scale(245)
.rotate([180, 0])
.translate([width / 2, height / 2])
.clipAngle(90);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var c = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(projection)
.context(c);
function getImage(path, callback) {
var img = new Image();
img.src = path;
img.onload = callback(null, img);
}
queue()
.defer(d3.json, "data/world-110m.json")
.defer(d3.tsv, "data/world-110m-country-names.tsv")
.defer(getImage, "data/space.jpg")
.await(ready);
//Main function
function ready(error, world, countryData, space) {
var globe = {type: "Sphere"},
land = topojson.feature(world, world.objects.land),
borders = topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; });
//Earth rotating
(function transition() {
d3.transition()
.duration(15000)
.ease("linear")
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-180, 0]);
return function(t) {
projection.rotate(r(t));
c.clearRect(0, 0, width, height);
c.drawImage(space, 0, 0);
c.fillStyle = "#00006B", c.beginPath(), path(globe), c.fill();
c.fillStyle = "#29527A", c.beginPath(), path(land), c.fill();
c.strokeStyle = "#fff", c.lineWidth = .5, c.beginPath(), path(borders), c.stroke();
projection.rotate([180, 0]);
};
})
.transition().duration(30).ease("linear")
.each("end", transition);
})();
};
Вращение реализовано как поворот из точки [180, 0]
в точку [-180, 0]
, которые совпадают. Таким образом, «интерполятор», не заметив подвоха, сделает то что нам нужно. Потом мы начинаем рисовать на canvas, предварительно очистив его. Рисуем фон, сферу, материки и границы стран. Бесконечное вращение получаем за счёт рекурсивного вызова функции transition
.
«И всё-таки она вертится!»
Ну вот мы и создали анимацию. Исходники можно найти на GitHub, а полюбоваться космическими видами можно через сервис bl.ocks.org.
Рассмотрим преимущества Canvas:
- Более быстрая/плавная работа по сравнению с SVG
- Возможность интеграции с анимацией, видео, играми и прочими штуками, которые реализуют сегодня с его помощью
Заключение
Вот мы и рассмотрели ещё пару интересных примеров созданных с помощью замечательной библиотеки d3.js. Я старался, чтобы примеры были в меру просты для понимания, наглядны и довольно-таки интересны. В борьбе SVG и Canvas в итоге победила дружба, так использование той или иной технологии зависит от типа вашего проекта. Например, если ваш проект связан с картографией, то целесообразно использовать SVG, если же вы работаете с мультимедиа, то Canvas вам в помощь. Надеюсь, вам было интересно. Удачи и успехов в дальнейшем освоении d3.js!
Автор: KoGor