• Как создавался WebWeb

    В пятницу, 7 августа, состоялось открытие художественного проекта WebWeb, в котором я принимал участие. Не спрашивайте меня, что это все означает, я и сам не понимаю; это такое высокое искусство. Для меня самым интересным разделом был Death: если подвигать курсором влево-вправо, то можно увидеть, как по лошади ползет тень, описывая ее контуры.

    death

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

    Макет от дизайнера

    Дизайнер прислал мне макет, в котором он нарисовал 33 состояния тени.

    step1

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

    Смотрим внимательно на саму тень. По сути, это две пересеченные линии, одна из них — перекладина — простая, как пять копеек, а вторую можно представить как обычную кривую, отрисованную контуром (stroke). Решение этой задачи будет выглядеть следующим образом: создаем кривые, которые описывают каждое состояние тени…

    step2-1

    …а затем будем рассчитывать между координатами точек новую линию, в зависимости от позиции курсора.

    step2-2

    Важно помнить, что набор точек должен быть одним и тем же (то есть в одной линии не может быть больше или меньше точек, чем в другой). Я делал так: отрисовал первую тень, продублировал путь (path) и передвинул точки в другое положение. И так еще 31 раз.

    Хранение кривых внутри JavaScript

    Линии отрисовали, теперь нужно придумать, как будем их хранить и использовать в JS. У нас возможны два типа линий: обычная, прямая линия и кривая Безье. Сам контур тени представляет собой массив таких линий. Для удобства и краткости записи создаются две прокси-функции, которые просто будут возвращать объект с координатами:

    /**
     * Точка
     */
    function p(x, y) {
    	return {x: x, y: y};
    }
    
    /**
     * Кривая Безье
     */
    function bz(cp1x, cp1y, cp2x, cp2y, x, y) {
    	return {
    		cp1x: cp1x,
    		cp1y: cp1y,
    		cp2x: cp2x,
    		cp2y: cp2y,
    		x: x,
    		y: y
    	};
    }
    
    // пример использования
    var line = [p(381, 289), p(427.33, 254), bz(427, 254, 427, 244, 433, 237)];
    

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

    var shadow = {
    	min: Number.POSITIVE_INFINITY,
    	max: Number.NEGATIVE_INFINITY,
    	lines: {},
    	degs: []
    };
    
    /**
     * Добавляет линию, описывающую тень
     * @param Массив точек
     */
    function addLine() {
    	// преобразуем объект аргументов в настоящий массив
    	var points = Array.prototype.slice.call(arguments, 0);
    
    	/** Первая точка линии */
    	var first = points[0],
    		/** Последняя точка линии */
    		last = points[points.length - 1];
    
    	// считаем угол наклона линии
    	var deg = -Math.atan2(last.y - first.y, last.x - first.x) / Math.PI * 180;
    
    	shadow.degs.push(deg);
    	shadow.lines[deg] = points;
    	if (deg < shadow.min)
    		shadow.min = deg;
    
    	if (deg > shadow.max)
    		shadow.max = deg;
    }
    

    Как видите, я использую массив углов линий shadow.degs и хэш самих линий shadow.lines, где ключом является угол наклона. Все это нужно для того, чтобы быстро находить две соседние линии. Дополнительно я считаю минимальный и максимальный угол чтобы знать, в каких пределах производить расчеты.

    Экспорт данных из фотошопа

    На первый взгяд, это самая рутинная и скучная часть работы: все 33 контура тени теперь нужно перенести из фотошопа в JS, причем в том формате, который мы выбрали. Решение «в лоб» — наводить курсор на каждую точку кривой, запомнить координаты и записать их в JS. Потратил час только на то, чтобы таким образом записать 3–4 линии (и те с ошибками). Так дело не пойдет, нужно как-то автоматизировать этот процесс.

    Я помню, что в адобовском иллюстраторе есть опция экспорта кривых в SVG, а у фотошопа есть функция экспорта кривых в иллюстратор (File → Export → Paths to Illustrator). Что ж, это уже что-то. Так как иллюстратора у меня нет, экспортированный файл высылаю дизайнеру и прошу его пересохранить в SVG.

    Но все равно SVG не самый удобный формат для моей задачи. Во-первых, у его Path Data есть два типа данных — абсолютные и относительные координаты. Например, запись l10,20 означает смещение на 10 пикселей по горизонтали и 20 пикселей по вертикали относительно предыдущей точки, а запись L10,20 уже означает смещение в координату x=10, y=20. Во-вторых, в SVG присутствует короткая запись однотипных инструкций: например, L10,20 L30,40 L0,60 можно записать как L10,20,30,40,0,60 и иллюстратор любит этим пользоваться. В общем, парсинг из SVG далеко не тривиальная задача.

    Оказывается, формат .ai вполне себе текстовый. Более того, координаты кривых там записываются в двольно простом формате и это именно то, что мне нужно.

    ...
    %Adobe_Photoshop_Path_Begin:<< /defaultFill false >>
    *u
    %AI3_Note:<< /operation /xor /defaultFill false >>
    1 XR
    551.5000 248.5000 m
    596.0000 283.5000 L
    601.0000 285.5000 595.5000 299.5000 607.0000 303.0000 C
    616.0000 309.0000 626.0000 317.5000 634.0000 323.5000 c
    642.0000 329.5000 662.5000 339.0000 670.0000 345.5000 c
    677.5000 352.0000 690.5000 376.5000 699.0000 384.5000 C
    711.0000 398.5000 719.0000 379.5000 y
    854.5000 487.0000 l
    N
    *U
    %Adobe_Photoshop_Path_End
    ...
    

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

    Вспомнить школу

    Настало время приступить к программированию самой логики. Алгоритм работы будет следующим: во время движения мышки рассчитываем угол наклона линии, соединяющей курсор и начальную точку (в нашем случае это столб), добавляем к этому углу 180˚ и получаем угол наклона тени. Затем находим два статических контура, между которыми находится полученный угол, строим новый контур и отрисовываем его. Все просто, не так ли? :)

    step3

    Прежде, чем начать, нам нужно вспомнить школьный курс математики и геометрии. У большинства моих преподавателей было типичное совковое мировоззрение: задавая вопрос «зачем мне все это нужно?» я получал ответ вроде «это нужно для поступления в ВУЗ», а о способе применения этих знаний в жизни тактично умалчивали. Если б я знал, что буду заниматься программированием анимации и графики, я бы совсем иначе относился к учебе. Так что если вы еще учитесь — наматывайте на ус и не повторяйте моих ошибок :)

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

    graph

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

    При движении мышки мы знаем ее X и Y координаты, соответственно, можем рассчитать и угол наклона линии, соединяющей курсор и начальную точку, через арктангенс. Затем просто переводим этот угол в другую систему координат с помощью поворота (не претендую на изящность решения, так как всю геометрию и алгебру забыл):

    // считаем угол наклона линии  от курсора до начальной точки
    var cursor_deg = Math.atan2(y, x) + Math.PI / 4 * 3;
    // считаем угол линии контура тени
    var line_deg = 315 - cursor_deg / Math.PI * 180;
    

    Следует помнить, что все вычисления с углами через Math.xxx функции должны вестись в радианах, а не углах.

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

    /**
     * Высчитывает параметры контура для указанного угла
     * @param {Number} deg
     * @return {Array}
     */
    function calculateLine(deg) {
    	// угол должен быть не меньше минимального и не больше максимального
    	deg = Math.min(shadow.max, Math.max(shadow.min, deg));
    
    	var result = [], d1, d2, coeff;
    	for (var i = 1; i < shadow.degs.length; i++) {
    		d1 = shadow.degs[i - 1];
    		d2 = shadow.degs[i];
    
    		if (deg >= d1 && deg <= d2) {
    			// нашли нужный диапазон, считаем коэффициент
    			coeff = 1 - (deg - d1) / (d2 - d1);
    			break;
    		}
    	}
    
    	if (coeff !== null) {
    		var l1 = shadow.lines[d1],
    			l2 = shadow.lines[d2];
    
    		// считаем координаты нового контура
    		for (var j = 0; j < l1.length; j++) {
    			var p1 = l1[j],
    				p2 = l2[j],
    				point = {};
    			for (var p in p1) if (p1.hasOwnProperty(p)) {
    				point[p] = p2[p] + (p1[p] - p2[p]) * coeff;
    			}
    
    			result.push(point);
    		}
    	}
    
    	return result;
    }
    

    Начинаем рисовать

    Основные элементы логики описаны, осталось собрать их вместе. Для рисования я буду использовать Canvas, для IE, соответственно, VML с помощью прослойки excanvas. Пишем промежуточный класс CanvasAdapter, который будет рисовать графику по выбранному ранее формату записи контуров, пишем вспомогательные функции, отслеживающие перемещение курсора и изменения размера экрана (чтобы не тратить лишний раз ресурсы на расчет координат начальной точки контуров) и проверяем:

    step41

    Вполне неплохо. Обращаем внимание на то, что контур тени — так как это обычная кривая — неестественно смотрится на ягодицах лошади:

    step5

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

    Помним, что у нас есть замечательный конвертер, который может из ai-формата сделать наш внутренний формат, поэтому в фотошопе быстренько рисуем маску и экспортируем ее в JS. Особая точность маске не нужна, у меня она получилась вот такой:

    step6

    Собираем все вместе, смотрим:

    step7

    Осталось совсем немного: отрезать верхушку тени, чтобы она смотрелась естественно. Можно воспользоваться методом canvas.clearRect(), однако в IE, из-за ограниченной поддержки excanvas, это не сработает. Поэтому размещаем сверху <div> с белым фоном, который будет скрывать ненужную часть. Просто и эффективно.

    Рисуем перекладину

    Последняя часть марлезонского балета — нарисовать перекладину. Она представляет из себя обычный прямоугольник, который должен правильно искажаться в зависимости от рассчитанного угла наклона тени. Решаем задачу следующим образом: находим пересечение текущего контура тени (достаточно прямой линии, кривые Безье не нужны) и горизонтальной линии перекладины, искажаем текущее координатное пространство матричным преобразованием и рисуем прямоугольник:

    var intersection = lineIntersection(0, line_y, 1000, line_y, last_point.x, last_point.y, first_point.x, first_point.y);
    canvas.transform(1, 0, (line_deg - 90) / 50, 1, intersection.x - 125, intersection.y);
    canvas.fillRect(0, 0, 250, 10);
    

    Вот и все. Делаем окончательный тюнинг, чтобы все работало гладко и смотрим на окончательный результат. Осталось добавить, что excanvas не поддерживает и маски, поэтому тень в IE смотрится не очень красиво, но для проекта это не критично. Интересующиеся могут исследовать исходный код страницы, чтобы увидеть все детали и жуткий баг Оперы, связанный с использованием масок в canvas.

    Метки: , , ,
  • 36 комментариев

    1. 9 августа 2009

      Спасибо! Очень интересно.

    2. Romanoza
      9 августа 2009

      Пипец, ну вы жжоте.
      Это просто круто.

      Интересно было бы применить такое на практике — там, где это нужно. А где — не понятно.

    3. Сергей
      9 августа 2009

      Вот это круто, но надеюсь, что я всё-таки в обозримом будущем смогу выучиться и делать что-то похожее. Общий принцип понятен, но мне не хватает знаний пока что, чтобы полно понимать все написанное… Спасибо Сергей, это здорово!

    4. 9 августа 2009

      Отлично, не зря я начал проходить на интуите canvas (:

      С самого начала статьи думал что вы сделаете немного по другому:
      Ту часть тени, которая объемная, на лошади, можно было все таки сделать пнг спрйтом, думаю при оптимизации графики размер все таки был не большим.
      А затем, крест можно сделать используя свойства border, когда они создают угол. Конечно js при этом будет немного сложнее.

      Ну а это решение невероятно элегантное, браво. Сколько времени потратил на обдумывание и реализацию, если не секрет?

    5. 9 августа 2009

      omfg:
      1) 33 варианта в спрайте и дискретная тень?
      2) Как нарисовать бордером угол 25°?

    6. 9 августа 2009

      razetdinov.ya.ru :

      1)В спрайте делать только тень которая на лошади, т.е. кривая.
      2)У бордеров есть одно хорошее свойство — не зависимо от размера границы, они всегда делять угол между собой. Не знаю как объяснить понятно, вот набросал для вас пару состояний тени от креста при помощи бордера (насчет ие6 не парился, но легко можно исправить):
      http://life4web.ru/example/border_corner.html

      Затем пишем скрипт, который меняет положение спрайта на кривой, а так же рисует крест при помощи бордера (:

    7. Сергей Чикуенок
      9 августа 2009

      Общий принцип понятен, но мне не хватает знаний пока что, чтобы полно понимать все написанное

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

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

      Дело не только в объеме, но и в том, как она будет двигаться. 33 состояния на таком большом сегменте — слишком заметно.

      А затем, крест можно сделать используя свойства border, когда они создают угол. Конечно js при этом будет немного сложнее.

      JS будет намного сложнее :) По сути, я выполнил обычное skew-преобразование одной командой, иначе пришлось бы считать зависимости толщины бордюров от угла наклона. И сглаживание далеко не везде работает.

    8. 9 августа 2009

      (В IE8 маска почему-то не работает)

      Красивое решение. Хотя я бы делал честный расчет проекции тени через 3D->2D. Там не сложно совсем на самом деле, для лошади хватило бы и 20 точек, остальное интерполяцией.

    9. Eos
      9 августа 2009

      Вы маньяк.

    10. SlamJam
      9 августа 2009

      Вы реально круты! Я тут пока читал, подумал, что наверное мне еще не поздно стать кем угодно, но программистом я больше быть не могу. До такого я бы в жизни не дошел.

    11. Vii
      9 августа 2009

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

      Когда курсе на 2-м начал немного разбираться в Direct2D тоже был очень удивлен, что школьная геометрия и те самые матричные преобразования, которые мы проходили (конечно же «проходили мимо» :)) в курсе мат. анализа на 1-м курсе могут применяться в областях которые мне интересны. Сейчас, правда, все забыл без практики.

    12. 9 августа 2009

      Прикольно, но чего-то в FF 3.5 глючит — на некоторых углах появляются какие-то полупрозрачные треугольники. Это баги FF или excanvas?

    13. Дмитрий Чаплинский
      9 августа 2009

      Да, я тоже заметил:
      http://img.skitch.com/20090809-jt57gm2tntgbq2qy79p25nwd3q.png
      http://img.skitch.com/20090809-4733kh326kmetr45y4qh27qid.png

      FF 3.5.2, Mac OS X 10.5.7

      P. S.
      Браво! :)

      SC: спасибо за опечатки

    14. Сергей Чикуенок
      9 августа 2009

      Это баг в отрисовке (в Safari тоже такое есть). К сожалению, не успел отловить его. Похоже, где-то линия неправильно нарисована

    15. 10 августа 2009

      Клёво. Из-за неточной маски, когда тень левого края перекладины только выходит из-за столба, меж задних ног — косячок наложения теней небольшой. В хроме по крайней мере.

    16. mkrn
      10 августа 2009

      Respect! It gives us inspiration
      reminds me of when i’ve been screwing around with 3d transparent stuff on canvas

    17. dgl
      10 августа 2009

      Невероятно круто. Обязательно опубликуй это в Smashing Magazine.

    18. 10 августа 2009

      Круто! Думаю я бы решал такой вопрос с помощью флэша =) Надо расширять кругозор, спасибо за отличный пример использования canvas!

    19. 10 августа 2009

      Поправьте:

      …рассчитываем угол наклона линии, соединяющЕЙ курсор и начальную точку…

    20. 10 августа 2009

      занимательно. можно узнать время исполнения? (от получения макета)

    21. Сергей Чикуенок
      10 августа 2009

      По времени заняло около 15 часов

    22. scorpix
      11 августа 2009

      Была проделана большая работа: сначала разработка, потом статья. Поздравляю, круто :)

    23. 11 августа 2009

      Очень круто, реальни. Интересное решение.

    24. Алексей
      11 августа 2009

      Это настоящий секс.

    25. 12 августа 2009

      Сергей, очень круто. Простые вещи, но использованы красиво. Получил несказанное удовольствие от чтения рассказа, спасибо!

    26. Дмитрий Чаплинский
      13 августа 2009

      Сергей, unrelated вопрос.
      Почему–то у меня блоки кода в FF3.5.2 (mac os x 10.5.7) набраны не моноширинным шрифтом (хотя в стиле указано обратное и computed style в Firebug показывает font-family: monospace). В сафари, тем не менее, все ок.

      Странно, что происходит фолбэк до monospace (хотя есть и монако и курьер), да и у моноспейса в настройках мозиллы указано использовать Courier.
      но это так, заметки на полях :)

    27. Алексей
      13 августа 2009

      Вопрос не по теме, но все же. Установил Aptana на ZendStudio for Eclipse, не могу связать html,css,js файлы с аптаной, т.е. в ассоциациях с файлами установил аптана, но она сбивается по какойто причине:(

    28. Егор
      19 августа 2009

      Сергей, вы, конечно, мегамонстр и заставили меня вспомнить школьный (да и университетский) курс математики. Но я думаю, что тень лучше представить не в виде одной центральной кривой, а в виде двух крайних и заливать фон между ними, тогда она будет выглядеть реалистичнее. Вдоль поверхности коня у неё будет немного отличаться толщина, в зависимости от кривизны этой поверхности, да и ближе к хвосту исчезнет артефакт с «загибом», а то сейчас грубовато.
      http://dl.getdropbox.com/u/789301/webweb-death.png

    29. Сергей Чикуенок
      19 августа 2009

      Какой внимательный читатель, очень приятно :) На самом деле вычисления будут гораздо сложнее, чтобы сделать правильные ограничивающие кривые. А указанные дефекты — не более, чем моя неаккуратность. Все это можно исправить, если правильно расположить точки на кривой. Если бы у меня было еще хотя бы два свободных вечера, я бы использовал и описал способ проверки точности расположения точек.

    30. editor
      15 сентября 2009

      «…Но меня этот вариант не нравится…»

    31. editor
      15 сентября 2009

      «…например 0.2 и так делее…»

    32. 17 сентября 2009

      Господи, как же много секса :)

    33. 20 сентября 2009

      Отличный проект. А почему вы 100% не остановились на js? Художник обязательно хотел использовать видео в последующих скетчах?

    34. Сергей Чикуенок
      20 сентября 2009

      Да, дизайнер хотел видео. Некоторые вещи можно было бы сделать и без флэша, но они принципиально не будут работать в IE

    35. 5 января 2010

      Вот зачем было извращаться с экспортом в SVG, если точки пути можно забрать прямо из ФШ с помощью встроенного скриптования на javascript.
      Выбираем нужный слой и запускаем такой скрипт:

      var docRef = app.activeDocument;
      var points = docRef.pathItems[0].subPathItems[0].pathPoints;
      var a = [];
      for(var p = 0; p < points.length; p++) {
      a.push(points[p].anchor.toString());
      }
      alert(a.join(«; «));

      копи-пастится в:

      —————————
      Script Alert
      —————————
      180,364; 203,334; 437,295; 448,310; 420,423
      —————————
      OK
      —————————

      Я пробовал на простых кусках. Для более сложных, возможно, понадобится пройтись также по индексам pathItems и subPathItems

    36. Сергей Чикуенок
      5 января 2010

      А зачем тратить время на изучения документации по скриптованию фотошопа, если достаточно экспортировать файл в AI и скопировать точки из него?