Для написания нам будет необходимо немного вспомнить курс математики. Основы 3D графики.
Набросаем план работы нашего скрипта:
1. Запускаемся после загрузки страницы
2. Инициализируем положение ссылок
3. Обеспечиваем непрерывное функционирование. Т.е. при наведении на ссылку вращаем сферу до попадания ссылки на передний план.
Для начала напишем несколько вспомогательных функций, которые облегчат нам жизнь в дальнейшем.
Это функция получения элемента по его id, в аргументе функции el - id элемента
function did(el)
{
return document.getElementById(el);
}
И функция установки стилей элемента. Здесь используется перебор ассоциативного массива styles и el - сам элемент. И устанавливаются значения el.syles[]. Так как для организации прозрачности в IE приходится устанавливать фильтр, то определяем наличие IE и устанавливаем фильтр и убираем его для уменьшения нагрузки на процессор при значениях прозрачности > 0.99. Так же умножаем на 100, в IE прозрачность от 0 до 100, тогда как в других браузерах от 0.0 до 1.0.
var is_ie = (document.all && !window.opera);
function setStyles (el, styles) {
for (var x in styles) {
if (is_ie && x == 'opacity') {
if (styles[x] > 0.99) el.style.removeAttribute('filter');
else el.style.filter = 'alpha(opacity='+ (styles[x] * 100) +')';
}
else el.style[x] = styles[x];
}
}
Для автоматического запуска скрипта добавим обработчик события onload, где вызовем функцию инициализации сферы.
onload=function()
{
setTimeout(function(){initSphere(_cnt,_Rad);},100);
}
Вот теперь пишем функцию инициализации сферы:
numpnt - количество точек в сфере, radius - соответственно радиус сферы.
Здесь нам пригодятся знания математики. Вспомним уравнение сферы и формулы перехода из полярных в декартовы координаты. Точка на поверхности сферы задается двумя углами и радиусом сферы. Один угол задает луч в плоскости XY, а второй поворот этого луча относительно оси Y.
x = Math.round(radius * Math.sin(f) * Math.cos(t));
y = Math.round(radius * Math.sin(f) * Math.sin(t));
z = Math.round(radius * Math.cos(f));
Углы пока установим сучайными. Но надо помнить что тригонометрические функции работают в не в градусах, а в радианах, поэтому и углы
мы будем устанавливать в радианах, тем более что мы поним, что 0 - 180 градусов соответствуют 0 - PI радиан, где PI - число пи. Поэтому
Воспользуемся функцией Math.random() для получения случайного числа от 0.0 до 1.0 и умножим на 360 градусов или 2*PI.
Получаемые координаты соберем в массиве xyz. Пройдемся циклом по количеству точек и все готово.
Теперь у нас есть кооординаты точек расположенных на поверхности сферы радиусом radius. Но нам это как-то нужно увидеть, для этого динамически создадим какой-нибудь элемент с координатами в нужных нам точках. Например <div>. Воспользуемся функцией
var el=document.createElement('div');
Установим у созданного элемента el стили с помощью нашей функции setStyles(). И добавим этот элемент в DOM - структуру к телу нашей страницы: document.body.appendChild(el). После всего запустим цикл трансформации в процедуре rotateth().
var xyz=Array();
function initSphere(numpnt,radius)
{
var x,y,z,f,t,c;
num_pnt=numpnt;
for(n=1;n<=numpnt;n++){
f=Math.random()*Math.PI*2;
t=Math.random()*Math.PI*2;
x = Math.round(radius * Math.sin(f) * Math.cos(t));
y = Math.round(radius * Math.sin(f) * Math.sin(t));
z = Math.round(radius * Math.cos(f));
xyz[n]={x:x,y:y,z:z};
var el=document.createElement('div');
el.id="pnt"+n;el.innerHTML="<a href=''><b>Ссылка"+n+"</b></a>";
setStyles(el,{border:'1px solid black',background:'red',fontFamily:'Arial',
position:'absolute', left:(100+x)+"px", top:(100+y)+"px",zIndex:z});
document.body.appendChild(el);
}
setTimeout(function(){rotateth();},100);
}
В нашем цикле трансформации выполняются какие-то действия с точками сферы. Мы проходим циклом по всем точкам и выполним операцию вращения сферы на какой-либо угол. Необходимо создать функцию обеспечивающую поворот. Назовем ее rotate(). Мы можем реализовать ее в виде функции изменяющей координаты в массиве xyz и указывать в параметре поворот на единицу поворота, например на 1 градус или если преобразовать в радианы 2*PI/360: var ada=(2*Math.PI / 360). Но здесь существует опасность накапливания ошибок при повороте, обусловленных неточностями тригонометрических функций. Т.е. при длительном цикле работы координаты точек не останутся на поверхности сферы, а уйдут куда-либо. Поэтому мы будем задавать полный угол на который будут поворачиваться неизменные координаты точек в массиве xyz. И делать приращение этого угла на единичный угол ada. При переходе через 360 градусов сделаем перенос в 0 градусов. В конце зацикливаем функцию запуская ее на выполнение через 50мс.
function rotateth()
{
za += ada; if(za > 2*Math.PI) za = za - 2*Math.PI;
for(i=0;i<_cnt;i++)
rotate(i,0,za,0);
setTimeout(function(){rotateth();},50);
}
Теперь займемся процедурой поворота rotate(n,a,b,c).
Здесь n - номер точки, a,b,c - углы относительно осей координат, соответственно ox,oy и oz.
Для поворота вокруг осей можно использовать простые формулы, получаемые из уравнения окружности, обеспечивающие поворот точки в одной плоскости,
оставляю третью координату неизменной.
ox=x;oy=y;oz=z;
if(c!=0)//around z
{
x = xo*Math.cos(c)-yo*Math.sin(c);
y = xo*Math.sin(c)+yo*Math.cos(c);
z = zo;
}
Поворот вокруг Y:
ox=x;oy=y;oz=z;
if(b!=0)//around y
{
x = xo*Math.cos(b)+z*Math.sin(b);
y = yo;
z = -xo*Math.sin(b)+zo*Math.cos(b);
}
Поворот вокруг X:
ox=x;oy=y;oz=z;
if(a!=0)//around x
{
x = xo;
y = yo*Math.cos(a)-zo*Math.sin(a);
z = yo*Math.sin(a)+zo*Math.cos(a);
}
Примечание: ox=x;oy=y;oz=z; необходимо выполнять каждый раз, иначе наступим на грабли :)
Или использовать сложную формулу поворота на любой угол вокруг каждой из осей (которую я и выберу). Для таких трансформаций конечно лучше использовать матричные преобразования, но в нашей задаче это не целесообразно, поэтому будем применять формулы. Сделаем небольшую оптимизацию, чтобы не нагружать процессор однотипными вычислениями и упросить запись этих монстроидальных формул. Сделаем предварительное вычисление вех синусов и косинусов. Можно вообще сделать табличку значений синусов и косинусов, тогда нагрузка на процессор будет вообще минимальная, но такая оптимизация думается будет излишней.
var sa=Math.sin(a),ca=Math.cos(a),sb=Math.sin(b),cb=Math.cos(b),sc=Math.sin(c),cc=Math.cos(c);
и собственно формула:
x = xo * cb * cc - yo * cb * sc + zo * sb;
y = xo * (cc * sa * sb + ca * sc)+
yo * (ca * cc - sa * sb * sc)-
zo * (cb * sa);
z = -xo * (ca * cc * sb + sa * sc)+
yo * (cc * sa + ca * sb * sc)+
zo * (ca * cb);
Далее получим элемент с нужным нам номером и установим стили. CL,CT,CZ - центр по x,y и z. Добавим эффект прозрачности при удалении вглубь картинки. OB - минимальная прозрачность. Также добавим эффект уменьшения шрифта, в зависимости от дальности его расположения от зрителя. FB - минимальный размер шрифта, FE - максимальный размер шрифта минус минимальный размер шрифта.
function rotate(n,a,b,c)
{
x=xyz[n].x;
y=xyz[n].y;
z=xyz[n].z;
var sa=Math.sin(a),ca=Math.cos(a),sb=Math.sin(b),cb=Math.cos(b),sc=Math.sin(c),cc=Math.cos(c);
var ox=x,oy=y,oz=z;
x = xo*cb*cc - yo*cb*sc+zo*sb;
y = xo*(cc*sa*sb+ca*sc)+
yo*(ca*cc-sa*sb*sc)-
zo*(cb*sa);
z =-xo*(ca*cc*sb+sa*sc)+
yo*(cc*sa+ca*sb*sc)+
zo*(ca*cb);
el=did("pnt"+n);
if(el){
var o=(z+_Rad)/(2*_Rad);
var fs = FB+o*FE;
o+=OB;
setStyles(el,{left:x+CL+"px",top:y+CT+"px",zIndex:(CZ+z)*2,fontSize: fs+"px"});
setStyles(el,{opacity:o});
}
}
В общем все готово. Можно смотреть, что получилось. Здесь можно поиграться с различным расположением точек и заданием различных углов поворотов и их комбинаций. Например, задав вместо случайного распределения точек в функции InitSphere
f = Math.random() * Math.PI * 2;
t = Math.random() * Math.PI * 2;
вот такие
t=0;
f=(2*Math.PI/numpnt)*n;
получим равномерное распределение точек по окружности на поверхности сферы, то есть вертикальное кольцо. Поменяв местами значения t и f получим горизонтальное кольцо.
Задавая в rotateth() различные комбинации и приращений углов (в параметрах к функции rotate()) получим различные направления вращений нашей сферы.
Напимер:
rotate(i,za,za,za); -вращение вокруг всех осей одновременно
Меняя знак угла получим (т.е. -za) получим вращение в обратную сторону.
Расположение точек по сфере, которое мы применяли ранее не соответствует задаче. Так как случайное распределение выглядит не лучшим образом. Существуют большие скопления и разреженности. Поэтому нужно более правильное распределение точек по поверхности. Поэтому воспользуемся вот таким распределением:
f = Math.acos(-1 + (2 * n - 1) / numpnt);
t = Math.sqrt(numpnt * Math.PI) * f;
.sptag{
font-family:Arial;
position:absolute;
text-decoration:none;
color:'gray';
}
.sptag a,a:link ,a:visited ,a:active,a:hover {
text-decoration:none;
font-weight:bold;
font-family:Arial;
color: gray;
}
Установим имя класса для вновь созданного элемента
el.className="sptag";
и добавим в свойства элемента его номер
el.n = n;
А в стилях элемента будем устанавливать только положение на странице и прозрачность. Кстати я заметил, что наша сфера какая-то кособокая, это вызывается тем, что наши div-ы устанавливаются левым верхним краем в точку на сфере. Это исправляется смещением на половину ширины div-а влево.
left:(CL+x-el.style.width/2)+"px"
Также необходимо сделать механизм получения данных тегов. Т.е. текста и адреса ссылки. Их будем получать из массива taga вида
{h:"#",t:"Ссылка1"}
Этот массив заполняем в html файле или здесь же в скрипте. Например так:
<script>
taga=[
{h:"#",t:"Ссылка1"},{h:"#",t:"Ссылка2"},{h:"#",t:"Ссылка3"},{h:"#",t:"Ссылка4"},{h:"#",t:"Ссылка5"},
{h:"#",t:"Ссылка6"},{h:"#",t:"Ссылка7"},{h:"#",t:"Ссылка8"},{h:"#",t:"Ссылка9"},{h:"#",t:"Ссылка10"},
{h:"#",t:"Ссылка11"},{h:"#",t:"Ссылка12"},{h:"#",t:"Ссылка13"},{h:"#",t:"Ссылка14"},{h:"#",t:"Ссылка15"},
{h:"#",t:"Ссылка16"},{h:"#",t:"Ссылка17"},{h:"#",t:"Ссылка18"},{h:"#",t:"Ссылка19"},{h:"#",t:"Ссылка20"}
];
</script>
В результате получим:
unction initSphere(numpnt,radius)
{
var x,y,z,f,t,c;
for(n=1;n<=numpnt;n++){
//f=Math.random()*Math.PI*2;
//t=Math.random()*Math.PI*2;
//t=0;
//f=(2*Math.PI/numpnt)*n;
f = Math.acos(-1 + (2 * n - 1) / numpnt);
t = Math.sqrt(numpnt * Math.PI) * f;
x = Math.round(radius * Math.sin(f) * Math.cos(t));
y = Math.round(radius * Math.sin(f) * Math.sin(t));
z = Math.round(radius * Math.cos(f));
xyz[n]={x:x,y:y,z:z};
var el=document.createElement('div');
el.id="pnt"+n;
el.innerHTML="<a href='"+taga[n-1].h+"'>"+taga[n-1].t+"</a>";
el.n=n;
el.className="sptag";
setStyles(el,{ left:(CL+x-el.style.width/2)+"px",top:(CT+y)+"px",zIndex:z});
el.onmouseover = function(){mouseover(this);};
el.onmouseout = function(){mouseout(this)};
document.body.appendChild(el);
}
state=0;
rotateth();
}
Здесь у нас появилась переменная state=0; Она будет показывать режим выполнямых действий. 0 - автоматический выбор углов и скоростей вращения, 1 - углы задаются пользователем, 2 - остановка вращения. В начале работы программы устанавливается режим 0. Режимы работы опишем в rotateth().
Для режима 0 введем таймер tmr, в котором будет изменяться скорость и направление вращения.
В режиме 1 задается скорость вращения по умолчанию в 1 градус и не изменяется направление, его зададим ниже. Также введем таймер который по прошествии некоторого времени обеспечит остановку сферы на некоторое время и переход в режим 0.
Здесь мы запоминаем установленные таймоуты для их сброса, в lto и to. Иначе может появиться эффект наложения вызываемых из таймоутов функци rotateth() и вследствии этого увеличение скорости. Кстати вращение будем выполнять не так как описывалось ранее. Когда я хотел уйти от накопления ошибок поворота. Так как при управлении пользователем будет меняться направления вращения. Поэтому будем в поворачивать сферу на определенный угол и запоминать новые координаты в xyz.
function rotate(n,a,b,c)
{
var x,y,z;
x=xyz[n].x;
y=xyz[n].y;
z=xyz[n].z;
var sa=Math.sin(a),ca=Math.cos(a),sb=Math.sin(b),cb=Math.cos(b),sc=Math.sin(c),cc=Math.cos(c);
var ox=x,oy=y,oz=z;
x = ox*cb*cc - oy*cb*sc + oz*sb;
y = ox*(cc*sa*sb+ca*sc) + oy*(ca*cc-sa*sb*sc) - oz*(cb*sa);
z = ox*(sa*sc-ca*cc*sb) + oy*(cc*sa+ca*sb*sc) + oz*(ca*cb);
xyz[n]={x:x,y:y,z:z};
var el=did("pnt"+n);
if(el){
x=Math.round(x);
y=Math.round(y);
z=Math.round(z);
var o = (z+_Rad)/(2*_Rad);
var fs = FB+o*FE;
o+=OB;
setStyles(el,{left:x+CL-el.style.width/2+"px",top:y+CT+"px",zIndex:(CZ+z)*2,fontSize: fs+"px"});
setStyles(el,{opacity:o});
}
}
function rotateth()
{
switch(state)
{
case 0:
if(tmr>0)tmr--;else{
ada=0.0001+ADA/2*Math.random()*((Math.random()>0.7)?1:-1);
tmr=TMR;
}
dax=day=daz=ada;
break;
case 1:
ada=ADA;
if(tmr>=100){
state=2;
to=setTimeout(function(){state=0;tmr=0;ada=ADA;rotateth();},5000);
return;
}
tmr++;
break;
}
for(i=1;i<=_cnt;i++)
rotate(i,dax,day,daz);
lto=setTimeout(function(){rotateth();},50);
}
Для реакции на действия пользователя, которое выражаетс в наведении курсора мыши на тэг, введены обработчики событий мыши
el.onmouseover = function(){mouseover(this);};
el.onmouseout = function(){mouseout(this)};
Основная функция управления mouseover. Функция mouseout нужна для восстановления вида ссылок после ухода курсора мыши с тэга. В ней мы убираем бордюр выводимый при наведении на ссылку указателя мыши.
function mouseout(el)
{
setStyles(el,{border:'none'});
}
В mouseover, выполняемой при наведении курсора на тег установим тот самый бордюр. Далее расчитаем положение левого края ссылки относительно центра сферы и нормализуем полученный резултат разделив его на радиус нашей сферы. Вращать будем вокруг 2 осей ох и оу поэтому третий угол поворота (daz) установим в 0. Сбросим таймер tmr также в 0. Очистим таймоуты и переведем режим в 1.
function mouseover(el)
{
setStyles(el,{border:'1px solid black'});
var ll=(CL - parseInt(el.style.left))/_Rad, lt=(parseInt(el.style.top) - CT)/_Rad;
daz=0; dax=(ADA / 2) * lt; day=(ADA/2)*ll;
tmr=0;
clearTimeout(to);clearTimeout(lto); lto=null; to=null;
setTimeout(function(){rotateth();},50);
state=1;
}
Вот как-то так. Смотрим. Вроде похоже :)
Можно доработать скрипт, убрав прозрачность и заменив ее изменением цвета текста.
Исходники можно взять здесь.
Демо - здесь.