See as: raw | code
terms of service | imprint
gibney.org is powered by m1d1
|
related.js pre object oriented (Entry Nr. 499, by user 39 | edit) |
|
|
/******************************************************************************
* animated item relationship distance map
*
* Part of Gnod, the Global Network of Dreams. (c) Marek Gibney
*
* Animates html elements with id's "s0", "s1",...
*
* Tries to keep the distances between them as given by an array Aid[][],
* where Aid[4][7] is the proposed distance "s4" should have to "s7".
*
* The element with id "s0" allways stays in the center.
*
* settings can be set via a global object "gnodMapSettings":
* * minX, maxX, minY, maxY: boundaries of drawing area
* * offsetX, offsetY: move center out of the drawing area's center.
* * nrItems: maximum number items to be displayed, even if Aid[][] is larger
*
*****************************************************************************/
// adjustable constants ------------------------------------------------------
// timing
var frameDelayInitial=25; // initial value of increasing delay at each timestep
var slowdownCycle =300; // number of timesteps after that delay is increased
// physics
var scaleFactor =1.4; // scaling in respect to calculated window space
var scaleBase =0; // scaling offset
var scaleBySprings =0.0; // scaling in respect to nr. of springs (Aid[][]>0) vs. possible nr. of springs
var scaleByCenterDist=-1; // scaling in respect to mean target center distance (mean Aid[i][0])
var inertia =0.7; // part of velocity kept in one timestep
var damperInitial =1; // initial value of increasing damper that cool down motion with time
var damperFactor =1.002; // factor the damper is increased by in one timestep
var damperMax =100; // max. value of damper
var springForce0 =0.025; // force between each item and the central item s0 trying to keep them at Aid[][]-distance
var springForce =0.005; // force between each item pair except the central item s0
var centeringForce =0.1; // force that pulls the center of gravity towards the central item
// overlap avoiding
var repelDelay =200; // nr. of timesteps without repulsion
var repelIncrease =0.001; // amount the repelling force that keeps items non overlapping increases each timestep
var repelMax =0.15; // max amount of repelling force.
var paddingFactor =1; // factor by which an item is enlarged while avoiding overlap
// ----------------------------------------------------------------------------
var nrItems; // number of items to layout
var nrSprings; // number of Aid[][] values>0
var maxX, maxY, minX, minY; // bounds of drawing area
var scaleX,scaleY; // scale to make anything fit
var cogX, cogY; // center of gravity of all items
var cycle=0; // refresh cycle counter
var frameDelay; // delay between redraws
var damper; // increasing damper to cool down motion
var repel; // increasing force that repells items to not overlap each other
/**
* A moving HTML element
*/
function mg_2d_element(id)
{
this.id=id;
this.x=0;
this.y=0;
this.speedX=0;
this.speedY=0;
this.element=document.getElementById("s"+this.id);
this.name=this.element.innerHTML;
this.width =this.element.offsetWidth;
this.height=this.element.offsetHeight;
this.update=function()
{
this.element.style.left=this.x-this.width /2;
this.element.style.top =this.y-this.height/2;
this.x+=this.speedX/damper;
this.y+=this.speedY/damper;
this.speedX*=inertia;
this.speedY*=inertia;
}
}
/**
* update minX,minY,maxX,maxY to window size
* may be overridden by gnodMapSettings
*/
function updateBoundaries()
{
minX=gnodMapSettings.minX;
minY=gnodMapSettings.minY;
var haveIE4orNewer = (document.all) ? 1 : 0;
if (haveIE4orNewer)
{
maxX = document.body.clientWidth;
maxY = document.body.clientHeight;
}
else
{
maxX = window.innerWidth;
maxY = window.innerHeight;
}
if (gnodMapSettings.maxX) maxX=gnodMapSettings.maxX;
if (gnodMapSettings.maxY) maxY=gnodMapSettings.maxY;
}
/**
* returns the mean item dimensions (width, height)
*/
function getMeanItemSize()
{
var meanW=0, meanH=0;
for (i=1;i<nrItems;i++)
{
meanW+=items[i].width;
meanH+=items[i].height;
}
meanW/=nrItems;
meanH/=nrItems;
return {width: meanW, height: meanH};
}
/**
* sets scaleX, scaleY in relation to window size, mean item size, density of springs.
*/
function updateScale()
{
var nrSpringsPossible=nrItems*(nrItems-1);
var springDensity =nrSprings/nrSpringsPossible;
var scale =scaleFactor*(1+scaleBySprings*springDensity)*(1+scaleByCenterDist*meanTargetCenterDistance());
// var scale =scaleFactor*(1+scaleBySprings*springDensity)/meanTargetCenterDistance();
var meanSize =getMeanItemSize();
// items[0].element.innerHTML=springDensity+" "+scale;
scaleX=scaleBase+(maxX-minX-meanSize.width )*scale;
scaleY=scaleBase+(maxY-minY-meanSize.height)*scale;
}
/**
* update boundaries to window size and place items at initial positions
*/
function resetItemPositions()
{
updateBoundaries();
items=new Array();
items[0]=new mg_2d_element(0);
// center item0
items[0].x=minX+(maxX-minX)/2;
items[0].y=minY+(maxY-minY)/2;
if (gnodMapSettings.offsetX) items[0].x+=gnodMapSettings.offsetX;
if (gnodMapSettings.offsetY) items[0].y+=gnodMapSettings.offsetY;
for (i=1;i<nrItems;i++)
{
items[i]=new mg_2d_element(i);
//initially place elements on a small circle:
items[i].x=items[0].x+Math.sin(i);
items[i].y=items[0].y+Math.cos(i);
}
updateScale();
cogX=items[0].x;
cogY=items[0].y;
cycle=0;
frameDelay=frameDelayInitial;
damper=damperInitial;
repel=0;
}
/**
* recenter items by moving the cog towards item[0].
*/
function recenterItems()
{
var forceX=(items[0].x-cogX)*centeringForce;
var forceY=(items[0].y-cogY)*centeringForce;
for (var i=1; i<nrItems; i++)
{
items[i].x+=forceX;
items[i].y+=forceY;
}
}
/**
* update item positions
*
* first checks all items against boundaries and push them back if needed
*/
function updateItems()
{
cogX=0; cogY=0;
for (var i=0; i<nrItems; i++)
{
var w=items[i].width /2;
var h=items[i].height/2;
if (items[i].x+w>maxX) items[i].x=maxX-w;
if (items[i].x-w<minX) items[i].x=minX+w;
if (items[i].y+h>maxY) items[i].y=maxY-h;
if (items[i].y-h<minY) items[i].y=minY+h;
cogX+=items[i].x;
cogY+=items[i].y;
items[i].update();
}
cogX/=nrItems;
cogY/=nrItems;
}
/**
* calculate min of all positive values of array values[]
*/
function positiveMin(values)
{
var r=Number.MAX_VALUE;
for (var i=0; i<values.length; i++)
if (values[i]>=0 && values[i]<r) r=values[i];
return r;
}
/**
* update position of all items
*
* * pull items toward target distance stored in Aim[][]
* * push items to not overlap each other.
*/
function layoutItems()
{
for(i1=1; i1<nrItems; i1++)
{
for(i2=0; i2<nrItems; i2++)
{
if (i2==i1) continue;
adjustItemDistance(items[i1], items[i2]);
repelItems (items[i1], items[i2]);
}
}
}
function adjustItemDistance(item1, item2, targetDistance, forceFactor, dx, dy)
{
var targetDistance=Aid[item1.id][item2.id];
if(targetDistance<=0) return;
var dx=item1.x-item2.x;
var dy=item1.y-item2.y;
var forceFactor;
if (item2.id==0) forceFactor=springForce0;
else forceFactor=springForce;
// measure distance in both axes independently in respect to window width and height
var wdx=dx/scaleX;
var wdy=dy/scaleY;
var distanceInWindowSpace=Math.sqrt(wdx*wdx+wdy*wdy);
var force=(targetDistance-distanceInWindowSpace)*forceFactor;
item1.speedX+=dx/distanceInWindowSpace*force;
item1.speedY+=dy/distanceInWindowSpace*force;
}
function repelItems(item1, item2)
{
var dx=item1.x-item2.x;
var dy=item1.y-item2.y;
//calculate extents of the items from their centers
var extentsSumX=(item1.width +item2.width) *paddingFactor/2;
var extentsSumY=(item1.height+item2.height)*paddingFactor/2;
// calculate overlapping on all four borders
var oLeft =-dx+extentsSumX;
var oRight = dx+extentsSumX;
var oTop =-dy+extentsSumY;
var oBottom= dy+extentsSumY;
var no_overlap = oLeft<0 || oRight<0 || oTop<0 || oBottom<0;
if (repel>0 && !no_overlap)
{
// calculate minimal overlapping
var oMin=positiveMin(Array(oLeft, oRight, oTop, oBottom));
var distance=Math.sqrt(dx*dx+dy*dy);
// repel item in respect to current overlapping
// we assume that dx,dy is a useful direction to decrease the overlapping
var repelScaler=repel*oMin/distance;
item1.x+=dx*repelScaler;
item1.y+=dy*repelScaler;
}
}
/**
* do one layout step.
*/
function layoutStep()
{
layoutItems();
recenterItems();
updateItems();
if (damper<damperMax) damper=damper*damperFactor;
if (cycle>repelDelay && repel<repelMax) repel+=repelIncrease;
if (cycle>slowdownCycle) frameDelay++;
cycle++;
setTimeout("layoutStep()",frameDelay);
}
function sum_of_squared_errors()
{
var error0=0, error=0;
for(i1=0; i1<nrItems; i1++)
{
for(i2=i1+1; i2<nrItems; i2++)
{
var targetDistance=Aid[i1][i2];
if(targetDistance<=0) continue;
var dx=items[i1].x-items[i2].x;
var dy=items[i1].y-items[i2].y;
// measure distance in both axes independently in respect to window width and height
var wdx=dx/scaleX;
var wdy=dy/scaleY;
var distanceInWindowSpace=Math.sqrt(wdx*wdx+wdy*wdy);
var tmp=targetDistance-distanceInWindowSpace;
error+=tmp*tmp;
if (i1==0) error0+=tmp*tmp;
}
}
return {error: error, error0: error0};
}
function meanTargetCenterDistance()
{
var r=0;
for(i1=1; i1<nrItems; i1++)
{
r+=Aid[i1][0];
}
r/=(nrItems-1);
return r;
}
function keyPress(event)
{
if (!event) event=window.event;
if (!event.ctrlKey) return;
if (event.which) key=event.which;
else key=event.keyCode;
if (key!=105) return;
var errors=sum_of_squared_errors();
var info="Infos:\n";
info+="Cycle: "+cycle+"\n";
info+="Number of active springs: "+nrSprings+"\n";
info+="Mean target center distance: "+meanTargetCenterDistance()+"\n";
info+="Sum of Squared Errors of all items: "+errors.error+"\n";
info+="Sum of Squared Errors of center item: "+errors.error0+"\n";
alert (info);
return false;
}
/**
* find needle in sorted array haystack
*/
function search(needle, haystack) {
var low=0;
var high=haystack.length-1;
while (low<=high)
{
var mid = parseInt((low+high)/2);
var value = haystack[mid];
if (value>needle) high=mid-1;
else if (value<needle) low =mid+1;
else return mid;
}
return -1;
}
/**
* replace values of Aid[][] with equally dense values in the range 0.0-1.0
* the smallest item becomes 1.0, the largest 0.0.
*
* sets nrSprings.
*/
function equalizeAid()
{
var values=new Array();
//build array of all used values
for (var i=0; i<nrItems; i++)
for (var j=0; j<nrItems; j++)
if (Aid[i][j]>0) values.push(Aid[i][j]);
nrSprings=values.length;
values.sort();
//assign each Aid[][] the rank of its value in values[] normalized to 0.0...1.0
for (var i=0; i<nrItems; i++)
for (var j=0; j<nrItems; j++)
if (Aid[i][j]>0)
{
Aid[i][j]=1-(search(Aid[i][j], values)/values.length);
}
}
/**
* initially place every item and start refresh timer.
*/
function init()
{
// Even though init() gets called on window.onload, in IE it sometimes
// gets started with a screen size of 0. If so, we try again later:
updateBoundaries();
if (maxX<1 || maxY<1)
{
setTimeout("init()",500);
return;
}
nrItems=Aid[0].length;
if (gnodMapSettings.nrItems && nrItems>gnodMapSettings.nrItems)
{
nrItems=gnodMapSettings.nrItems;
}
equalizeAid();
resetItemPositions();
window.onresize=resetItemPositions;
setTimeout("layoutStep()",10); // let's go.
}
// ----------------------------------------------------------------------------
// instead of calling init directy, we attach it to the window.onload event
// especially when the map is loaded, the screensize might not be set
// before that event
window.onload=init;
document.onkeypress = keyPress; |
|
|
Create a new entry at this position
|
|
|
|