Как стать автором
Обновить

Сказка о Box2D, Canvas,Twitter, и о том, как все это черт возьми связано

Время на прочтение9 мин
Количество просмотров2.3K
Однажды, убивая вечер чтением однообразных твитов с каким-то тегом, который в тот момент был на первом месте в топ-10, и думая о бессмысленности этого занятия, мне пришла в голову потрясающе разумная мысль, что пора бы с этим завязывать, и наконец сделать хоть что-нибудь интересное.

В качестве первого пришедшего в голову «интересного» была идея как-то визуализироать весь этот поток абсолютно бесполезной информации, который создается большинством пользователей твиттера, а тут еще как раз удачно в памяти всплыл Ball Pool.



Глава 1. О том как мы познакомились с Box2DJS
Итак, мне нужен физический движок. После недолгих поисков в мои лапы попал Box2D. Его версии есть для целой кучи языков и платформ, версия для Javascript тоже нашлась, примеры на её домашней страничке работали отлично, а инструкция по установке была простой и понятной:

  1. Скачать и распаковать архив.
  2. Перенести скрипты в свой проект
  3. Подключить библиотеку на свою страничку, скопировав содержимое тега <head> из примера.

Радовался я только пока не дошел до третьего пункта, где на страницу мне предлагали скопировать следующее:

	
<!--=============================-->
		<!-- Copy this part to your app. -->
		<!-- START -->
		<!--=============================-->
		<!-- libs -->
		<!--[if IE]><script type="text/javascript" src="lib/excanvas.js"></script><![endif]-->
<script src="lib/prototype-1.6.0.2.js"></script>

		<!-- box2djs -->
<script src='js/box2d/common/b2Settings.js'></script>
<script src='js/box2d/common/math/b2Vec2.js'></script>
<script src='js/box2d/common/math/b2Mat22.js'></script>
<script src='js/box2d/common/math/b2Math.js'></script>
<script src='js/box2d/collision/b2AABB.js'></script>
<script src='js/box2d/collision/b2Bound.js'></script>
<script src='js/box2d/collision/b2BoundValues.js'></script>
<script src='js/box2d/collision/b2Pair.js'></script>
<script src='js/box2d/collision/b2PairCallback.js'></script>
<script src='js/box2d/collision/b2BufferedPair.js'></script>
<script src='js/box2d/collision/b2PairManager.js'></script>
<script src='js/box2d/collision/b2BroadPhase.js'></script>
<script src='js/box2d/collision/b2Collision.js'></script>
<script src='js/box2d/collision/Features.js'></script>
<script src='js/box2d/collision/b2ContactID.js'></script>
<script src='js/box2d/collision/b2ContactPoint.js'></script>
<script src='js/box2d/collision/b2Distance.js'></script>
<script src='js/box2d/collision/b2Manifold.js'></script>
<script src='js/box2d/collision/b2OBB.js'></script>
<script src='js/box2d/collision/b2Proxy.js'></script>
<script src='js/box2d/collision/ClipVertex.js'></script>
<script src='js/box2d/collision/shapes/b2Shape.js'></script>
<script src='js/box2d/collision/shapes/b2ShapeDef.js'></script>
<script src='js/box2d/collision/shapes/b2BoxDef.js'></script>
<script src='js/box2d/collision/shapes/b2CircleDef.js'></script>
<script src='js/box2d/collision/shapes/b2CircleShape.js'></script>
<script src='js/box2d/collision/shapes/b2MassData.js'></script>
<script src='js/box2d/collision/shapes/b2PolyDef.js'></script>
<script src='js/box2d/collision/shapes/b2PolyShape.js'></script>
<script src='js/box2d/dynamics/b2Body.js'></script>
<script src='js/box2d/dynamics/b2BodyDef.js'></script>
<script src='js/box2d/dynamics/b2CollisionFilter.js'></script>
<script src='js/box2d/dynamics/b2Island.js'></script>
<script src='js/box2d/dynamics/b2TimeStep.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactNode.js'></script>
<script src='js/box2d/dynamics/contacts/b2Contact.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactConstraint.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactConstraintPoint.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactRegister.js'></script>
<script src='js/box2d/dynamics/contacts/b2ContactSolver.js'></script>
<script src='js/box2d/dynamics/contacts/b2CircleContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2Conservative.js'></script>
<script src='js/box2d/dynamics/contacts/b2NullContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2PolyAndCircleContact.js'></script>
<script src='js/box2d/dynamics/contacts/b2PolyContact.js'></script>
<script src='js/box2d/dynamics/b2ContactManager.js'></script>
<script src='js/box2d/dynamics/b2World.js'></script>
<script src='js/box2d/dynamics/b2WorldListener.js'></script>
<script src='js/box2d/dynamics/joints/b2JointNode.js'></script>
<script src='js/box2d/dynamics/joints/b2Joint.js'></script>
<script src='js/box2d/dynamics/joints/b2JointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2DistanceJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2DistanceJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2Jacobian.js'></script>
<script src='js/box2d/dynamics/joints/b2GearJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2GearJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2MouseJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2MouseJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2PrismaticJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2PrismaticJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2PulleyJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2PulleyJointDef.js'></script>
<script src='js/box2d/dynamics/joints/b2RevoluteJoint.js'></script>
<script src='js/box2d/dynamics/joints/b2RevoluteJointDef.js'></script>
		<!--=============================-->
		<!-- Copy this part to your app. -->
		<!-- END -->
		<!--=============================-->


Может и найдется кто-нибудь, кто правда готов подключить на страницу более пятидесяти скриптов, но я так делать точно не буду.
Наверно, можно подгрузить только то что нужно, подумал я, но, еще немного побродив по сайту, наткнулся на объявление:

«Каждый скрипт имеет сложную систему зависимостей, и даже подгрузка их в неправильном порядке может вызвать Fatal error»

Окей, ясно, это была плохая идея, и мы так делать не будем. Сразу появился второй вариант — слить весь box2d код в один файл и минимизировать по самые гланды.
Эта идея мне понравилась намного больше первой, и, как оказалось, не мне одному.
Нужные исходники уже лежали вот здесь, а в их тексте встретился знакомый ник mr.Doob( это именно он делал Ball pool ), а значит я на правильном пути.
Скачал поставил и подключил. Ураа, теперь у меня на страничке есть физика!

Глава 2. Физика есть, а что с ней собственно делать?
Домашняя страница Box2DJs ограничивается скромными примерами общим размером строк в 50, и ссылкой на документацию Action Script версии. Последнее, конечно, очень полезно, но все равно как-то негусто. Впрочем, оказалось, что негусто с этим вобще везде, а кроме тех примеров которые в комплекте, можно найти что-нибудь вроде вот этого маятника, что достаточно скромно, или чуть менее скромного Ball Pool, да-да, того самого, из начала статьи.
В исходники последнего заглядывать сначала не хотелось, но спустя какое-то время прагматичность все-таки победила :) и уже через пару часов я сделал свой маленький мирок.
P.s. Уже после того как техническая часть была законченна, вот тут появились очень классные статьи.

Глава 3. Невидимый мир это круто, но все равно почему-то хочется его нарисовать
На той самой страничке mr.Doob я заметил, что каждый мячик рисуется на своем канвасе, а потом уже, с помощью сss, передвигается. Смысл этого я понял немножко позже. После того, как отображение было написано с использованием только канваса, где-то на 10 мячике страничка без моего ведома предлагала мне вместо какого-никакого экшена полюбоваться на слайд шоу.
Спасибо тебе Mr.Doob — уже через полчаса отображение было сделано точно так же, ну только с jquery и jCanvas.

Окей, кажется все работает — мячики создаются, прыгают, исчезают, и их даже можно таскать по экрану, все, конечно, круто, если не обращать внимание на безумную утечку памяти в ff, привет С++.
Уже предвкушая вечер в компании какого-нибудь js профайлера, я все-таки решил порефакторить код в надежде, что поможет, а если и нет, то покрасней мере все будет аккуратно.
Неожиданно, но это правда помогло, да и работать все стало быстрее. Получилось вот так:



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

Глава 4. Конец света. Мир схлопывается в ноль по высоте. Мячики в панике :)
Так, кажется зря я это сделал. А если задать высоту div в пикселах? Мир вернулся — все наверно не так уж и плохо, только я так делать не хочу — не люблю скроллбары, или кучу лишнего свободного места.
Решение проблемы скоро нашлось на каком-то из из форумов и до него в общем-то можно было додуматься логически:

( ниже представлен отрывок диалога между воображаемым и куда более сообразительным мной, который не смотрел на форум и моим же внутренним голосом )

голос(г): что значит высота 70%? 70% от чего?
я: от высоты родительского элемента.
г: Ага, и какая у него сейчас высота?
я: Так родитель у нас <body> контента в нем нет, а так как размер, если он не задан, зависит от контента, то сейчас он = 0;

В общем, решается все это так:

html{ height: 100% }
body{ height: 100% }


Тогда высота html будет равна высоте вьюпорта ну а дальше все ясно. Проверка, запуск, и вот такая вот картинка:



Ага, ясно, у нас тут нашествие гигантских мячиков — в другой раз не забуду менять их размер в зависимости от размеров мира:



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



Проверяю, играюсь, радуюсь и…

Глава 5. Экран внезапно уезжает...
В хроме. Нет, правда — вот так:


Происходит это когда мышка уходит вниз за границу браузера. Скорая помощь ввиде overflow:hidden в body или в html положения не спасают, в гугле решение быстро тоже не находится. Окей — будем разбираться.

Спустя полчаса выясняю следующее:
Хром некорректно работает с элементом у которого height:100% и при этом есть вертикальны padding; в принципе даже понятно откуда уши у ошибки растут: из таких стилей следует что высота должна быть вся какая можно + еще 10 пикселей сверху. Странно только что ff работает с этим нормально :)
Проблема эта решается так:

#menu{
…
padding: 0px 10px; /*padding по вертикали заменим на margin у крайних элементов внутри контейнера - визуально эффект тот же*/
…
}
html
{
...
overflow: hidden; /*потому что даже при удалении padding по вертикали chrome в той же ситуации пытается добавить место как будто для невидимых полос прокрутки. ну вот именно так это выглядит*/
...
}

Ура, наконец все работает.

Глава 6. API твиттера и Same origin policy
А теперь идем изучать апи твиттера. Все просто и понятно — подгружаем ответы через jQuery.ajax и любуемся на то, как ничего не работает, потому что тут из за угла появляется Same origin policy.
Как-то я про нее совсем забыл. Вообще никогда не любил эти ограничения — есть 1001 способ их обойти, а жить они все равно мешают.
Разработчики твиттера, в отличии от меня, о ней конечно помнят — их апишки поддерживают jsonp. а, это значит, что мы можем обратиться к ним вот так:

<script src="http://search.twitter.com/search.json?q=%40twitterapi&callback=process"></script>

и в скрипт вернется что-то похожее на

process({ /*data*/ })


А это, между прочим, корректный javascript, и same origin policy на это не распространяется.
Самое приятное, что jQuery умеет хорошо и аккуратно работать с jsonp:

$.getJSON("http://api.twitter.com/1/trends/1.json?callback=?", function () { alert("wooohooo! it works!") });

Фишка в том, что если jQuery видит в строке запроса параметр типа callback=?, значит будет использован jsonp, а вместо знака вопроса jQuery подставляет свою собственную функцию обработки.
Пишем еще немножко кода — ставим отображение крупным шрифтом текста твиттов, еще всякие мелочи, и получается вот так:



Все? ну вообще-то еще нет :) уменьшаем окно, уменьшаем, уменьшаем, и… нет, как-то все это теперь неправильно выглядит:



Жалко, что css-ом нельзя быстро сказать: «пожалуйста, выбери такой размер шрифта, чтобы весь текст в див помещался, но за границы не выходил».
Да даже на человеческом языке это как-то долго говорится. В общем, самый простой способ добиться такого эффекта, если мы используем jQuery — воспользоваться плагином TextFill. Написан он конечно на коленке, но очень даже работает.



Заливаем все на хостинг и проверяем. Блин, как-то все это долго грузится, хостинг конечно небыстрый, но это же не повод так тормозить.
Ага. box2d, jquery, плагины к нему, еще какаято фигня, в общем, файлов както многовато, и с этим надо что-то делать.

Глава 7. Привет SquishIt
Маленький и потрясающе удобный проект для asp.net. от очень колоритного разработчика (его фото есть на сайте — справа:)
Умеет мнимизировать js и css, и сшивать их в один файл — то, что доктор прописал. Иногда конечно SquishIt глючит, но ему можно.
Самое приятное, что работает все это счастье из коробки. Скачал, подключил сборку, и заменил блок со скриптами в <head> на что-то вроде:

<%= Bundle.JavaScript()
.Add("~/Scripts/Shared/jquery-1.6.2.min.js")
.Add("~/Scripts/Shared/jQueryRotate.2.1.js")
.Add("~/Scripts/Shared/JCanvas.js")
.Add("~/Scripts/Shared/jquery-textfill-0.1.js")
.Add("~/Scripts/Shared/Box2d-singlefiled-jsmined.js")
.Add("~/Scripts/Shared/World.js")
.Add("~/Scripts/Tweets_life.js")
.Render("~/Scripts/combined_#.js")
%>

Даже пояснений, думаю, никаких не нужно. Все просто работает. Как у эппл :)

Ну а теперь, осталось только придумать клевое название, добавить немножко глубинного смысла, продумать некоторые мелочи и слегка изменить дизайн. То, что получилось в итоге, потрогать можно тут.



The End
Теги:
Хабы:
Всего голосов 44: ↑42 и ↓2+40
Комментарии16

Публикации