Pull to refresh

Анимация перехода от глобуса к двумерной карте

Reading time5 min
Views18K
Хочу поделиться с хабром своим картографическим экспериментом, а именно анимацией перехода от Ортографической проекции (глобус) к Равнопромежуточной (одна из проекций обычных двумерных карт). Также этот способ подойдёт и для любых других проекций. Результатом экспериментов стала вот такая анимация:

От глобуса к карте


Как и прежде будем использовать библиотеку d3.js, как и прежде сделаем несколько реализаций: SVG и Canvas. Оба варианта можно будет эффектно использовать для интерактивной инфографики. Ну что, начнём?

Начало


Увидел я как-то по телевизору рекламу (вроде Газпрома), там как раз глобус крутится и потом разворачивается в карту. Красивый мультик, мне понравилось, ну я и решил сделать нечто похожее, только интерактивное и для интернета. Challenge accepted.

От простого к сложному


Поскольку с картами мы уже работали (Интерактивная SVG картограмма с помощью d3.js), да и с глобусом тоже (Интерактивный глобус — SVG versus Canvas), то начнём сразу с реализации перехода от одной проекции к другой. Первое же, что приходит на ум — отдать всё на откуп библиотеке, ведь там уже реализована анимация переходов: transitions. Так я и сделал. Полный код первого примера можно посмотреть на GitHub: Globe to Map, пощупать на bl.ocks.org: Globe to Map. Да, если на bl.ocks.org щёлкнуть по номеру блока в левом верхнем углу, то вы перейдёте к соответствующему gist'у на GitHub, поэтому в дальнейшем буду давать только одну ссылку.

Итак, что есть что: focused — индикатор фокусировки на страну, ortho — индикатор проекции, speed — скорость вращения, start — начало вращения, corr — переменная для сохранения фазы поворота.

Функция endall(transition, callback) считает количество элементов, к которым будет применяться переход (анимация), и, когда всё будет закончено, выполняет скормленную ей функцию (callback). В принципе, может быть заменена с помощью SetTimeout, но лучше всё же использовать endall(transition, callback).

//Starter for function AFTER All transitions

function endall(transition, callback) { 
  var n = 0; 
  transition 
  .each(function() { ++n; }) 
  .each("end", function() { if (!--n) callback.apply(this, arguments); }); 
}

Вращение реализовано с помощью d3.timer и применяется только к path с классом ortho.

  //Globe rotating via timer

  d3.timer(function() {
    var λ = speed * (Date.now() - start),
    φ = -5;

    projection.rotate([λ + corr, φ]);
    g.selectAll(".ortho").attr("d", path);

  });

Переменная corr позволяет нам вернуться к тому же углу поворота глобуса, который был до смены проекции. С долготой λ и широтой φ поможет разобраться следующая картинка из вики-статьи Географические координаты:

Координатная сфера


Анимация перехода от глобуса к карте:

//Transforming Globe to Map

if (ortho === true) {
  corr = projection.rotate()[0]; // <- save last rotation angle      
  g.selectAll(".ortho").classed("ortho", ortho = false);
  projection = projectionMap;
  path.projection(projection);
  g.selectAll("path").transition().duration(3000).attr("d", path);
}

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

Обратный переход отличается добавлением класса ortho для всех path по окончании перехода и сбросом угла поворота глобуса (таймер то тикал всё это время).

//Transforming Map to Globe

projection = projectionGlobe;
path.projection(projection);
g.selectAll("path").transition()
.duration(3000).attr("d", path)
.call(endall, function() {
  g.selectAll("path").classed("ortho", ortho = true);
  start = Date.now(); // <- reset start for rotation
});

Остальная часть кода вопросов, по идее, вызывать не должна, особенно если вы читали предыдущие мои статьи.

Если вы поигрались некоторое время с первым примером, то должны были заметить, что карта каждый раз выглядит по-разному. Это связано с тем, что мы инициируем переход при различных углах поворота глобуса (λ), поэтому карта разрезается по разным значениям долготы (меридианам). Чтобы получить привычный вид карты (разрез по антимеридиану), нам необходимо перед переходом довернуть глобус до нулевого меридиана. Также вместо доворота можно менять параметры проекции двумерной карты, но я выбрал первый вариант. Во втором примере реализовано разрезание по антимеридиану, а глобус вращается мышкой (drag event). За доворот отвечает функция defaultRotate():

//Rotate to default before animation

function defaultRotate() {
  d3.transition()
  .duration(1500)
  .tween("rotate", function() {
    var r = d3.interpolate(projection.rotate(), [0, 0]);
    return function(t) {
      projection.rotate(r(t));
      g.selectAll("path").attr("d", path);
    };
  })
};

Подобную функцию я уже описывал в статье Интерактивный глобус — SVG versus Canvas, так что не буду повторяться. Код второго примера на bl.ocks.org: Globe to Map II.

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

Если красота спасёт мир, то почему она постоянно требует каких-то жертв? ©


Под жертвами в данном случае имеется ввиду усложнение кода. Итак, чтобы переход от одной проекции к другой проходил не «тупо» по кратчайшему пути, а красиво, нам необходимо создать собственную интерполяцию проекций. Мне повезло, я нашёл подходящий пример у Mike'а: Orthographic to Equirectangular. С минимальными доработками его можно использовать для переходов в обе стороны, как раз то что нам нужно. Вот собственно его доработанная реализация:

//Unreelling transformation

function animation(interProj) {
  defaultRotate();
  g.transition()
  .duration(7500)
  .tween("projection", function() {
    return function(_) {
      interProj.alpha(_);
      g.selectAll("path").attr("d", path);
    };
  })
}

function interpolatedProjection(a, b) {
  var projection = d3.geo.projection(raw).scale(1),
  center = projection.center,
  translate = projection.translate,
  clip = projection.clipAngle,
  α;

  function raw(λ, φ) {
    var pa = a([λ *= 180 / Math.PI, φ *= 180 / Math.PI]), pb = b([λ, φ]);
    return [(1 - α) * pa[0] + α * pb[0], (α - 1) * pa[1] - α * pb[1]];
  }

  projection.alpha = function(_) {
    if (!arguments.length) return α;
    α = +_;
    var ca = a.center(), cb = b.center(),
    ta = a.translate(), tb = b.translate();
    center([(1 - α) * ca[0] + α * cb[0], (1 - α) * ca[1] + α * cb[1]]);
    translate([(1 - α) * ta[0] + α * tb[0], (1 - α) * ta[1] + α * tb[1]]);
    if (ortho === true) {clip(180 - α * 90);}
    return projection;
  };

  delete projection.scale;
  delete projection.translate;
  delete projection.center;
  return projection.alpha(0);
}

На первый взгляд выглядит сложно, но на самом деле это не так. Что же тут происходит? Мы скармливаем функции interpolatedProjection(a, b) две проекции, они нормируются (один масштаб, координаты в радианах), затем создаётся комбинированная проекция, которая есть попарная сумма параметров (центр, смещение) исходных проекций с коэффициентом α (шаг интерполяции). И на каждом шаге нам отдаётся комбинированная проекция, зависящая от α. С увеличением шага α уменьшается вес первой проекции, и усиливается вес второй. Таким образом, мы получаем красивую анимацию.
Финальная версия на bl.ocks.org: Globe to Map III.

В конце я также решил сделать версию с использованием canvas. Эта версия отличается тем, что все функции переехали внутрь ready(error, world, countryData), поскольку должны непосредственно манипулировать геоданными, в остальном логика работы та же. Комментировать особо нечего, так что вот сразу код на bl.ocks.org: Globe to Map IV.

Результаты


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


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

На этом наш эксперимент заканчивается. Спасибо тем, кто дочитал до конца. Всем удачи и интересных проектов.
Tags:
Hubs:
+48
Comments17

Articles

Change theme settings