Интерактивный глобус — SVG versus Canvas

от автора

Доброго времени суток, уважаемый читатель! В прошлый раз мы изучали процесс создания интерактивной карты-хороплета, теперь предлагаю немного усложнить задачу и перейти к трёхмерной модели Земли, именуемой в народе глобусом. Глобус делать будем двух видов: 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 — название страны. Внешний файл с названиями используется для удобства, так как в этом случае можно легко перевести названия на любой язык. Всё, можно приступать непосредственно к созданию глобуса.

Замечание:

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

Рисуем интерактивный глобус

Нашей целью будет глобус, который можно:

  • вращать мышкой «хватая» за сушу
  • центрировать глобус на страну, выбранную из списка

Шаблон, для тех кому нужно.

<!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!

ссылка на оригинал статьи http://habrahabr.ru/post/186532/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *