Circular Wander

Size
11,203 Kb
Views
26,312

How do I make an circular wander?

Step by Step visualization for a smooth random motion. Be sure to click on the "See one" and "See many" buttons.. What is a circular wander? How do you make a circular wander? This script and codes were developed by Sakri Rosenstrom on 13 September 2022, Tuesday.

Circular Wander Previews

Circular Wander - Script Codes HTML Codes

<!DOCTYPE html>
<html >
<head> <meta charset="UTF-8"> <title>Circular Wander</title> <link rel="stylesheet" href="css/style.css">
</head>
<body> <div id="canvasContainer"></div>
<div id="buttons"> <button type="button" onclick="explainMode()" >Explanation</button> <button type="button" onclick="singleMode()" >See one</button> <button type="button" onclick="manyMode()" >See many</button>
</div> <script src="js/index.js"></script>
</body>
</html>

Circular Wander - Script Codes CSS Codes

html, body{ margin : 0px; width : 100%; height : 100%; overflow: hidden; background-color:#FFFFFF;
}
#canvasContainer{ position: absolute; margin : 0px; width : 100%; height : 100%;
}
#controls{ position: absolute;
}
#buttons{ position: absolute; bottom: 40px; left: 40px;
}

Circular Wander - Script Codes JS Codes

/* * * @author Sakri Rosenstrom * http://www.sakri.net * https://twitter.com/sakri * http://www.devstate.net * Sources for this can be found at: * https://github.com/sakri/sakriNetCommonJS */
(function (window){ var Sakri = window.Sakri || {}; window.Sakri = window.Sakri || Sakri;	Sakri.MathUtil = {};	//used for radiansToDegrees and degreesToRadians	Sakri.MathUtil.PI_180 = Math.PI/180;	Sakri.MathUtil.ONE80_PI = 180/Math.PI;	//precalculations for values of 90, 270 and 360 in radians	Sakri.MathUtil.PI2 = Math.PI*2;	Sakri.MathUtil.HALF_PI = Math.PI/2;	Sakri.MathUtil.PI_AND_HALF = Math.PI+ Math.PI/2;	Sakri.MathUtil.NEGATIVE_HALF_PI = -Math.PI/2; //keep degrees between 0 and 360 Sakri.MathUtil.constrainDegreeTo360 = function(degree){ return (360 + degree % 360) % 360;//hmmm... looks a bit weird?! }; Sakri.MathUtil.constrainRadianTo2PI = function(rad){ return (Sakri.MathUtil.PI2 + rad % Sakri.MathUtil.PI2) % Sakri.MathUtil.PI2;//equally so... }; Sakri.MathUtil.radiansToDegrees = function(rad){ return rad*Sakri.MathUtil.ONE80_PI; }; Sakri.MathUtil.degreesToRadians = function(degree){ return degree * Sakri.MathUtil.PI_180; };	//return number between 1 and 0	Sakri.MathUtil.normalize = function(value, minimum, maximum){	return (value - minimum) / (maximum - minimum);	};	//map normalized number to values	Sakri.MathUtil.interpolate = function(normValue, minimum, maximum){	return minimum + (maximum - minimum) * normValue;	};	//map a value from one set to another	Sakri.MathUtil.map = function(value, min1, max1, min2, max2){	return Sakri.MathUtil.interpolate( Sakri.MathUtil.normalize(value, min1, max1), min2, max2);	}; Sakri.MathUtil.clamp = function(min,max,value){ if(value < min){ return min; } if(value > max){ return max; } return value; }; Sakri.MathUtil.clampRGB = function(value){ return Sakri.MathUtil.clamp(0, 255, value); };	Sakri.MathUtil.getRandomNumberInRange = function(min, max){	return min + Math.random() * (max - min);	};	Sakri.MathUtil.getRandomIntegerInRange = function(min, max){	return Math.round(Sakri.MathUtil.getRandomNumberInRange(min, max));	}; //Move to geom?	Sakri.MathUtil.getCircumferenceOfEllipse = function(width,height){	return ((Math.sqrt(.5 * ((width * width) + (height * height)))) * (Math.PI * 2)) / 2;	}; //from : http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb Sakri.MathUtil.rgbToHex = function(r, g, b) { return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); } Sakri.MathUtil.hexToRgb = function(hex) { // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function(m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; }
}(window));
//has a dependency on Sakri.MathUtil
(function (window){ var Sakri = window.Sakri || {}; window.Sakri = window.Sakri || Sakri;	Sakri.Geom = {}; //================================================== //=====================::POINT::==================== //================================================== Sakri.Geom.Point = function (x,y){ this.x = isNaN(x) ? 0 : x; this.y = isNaN(y) ? 0 : y; }; Sakri.Geom.Point.prototype.clone = function(){ return new Sakri.Geom.Point(this.x,this.y); }; Sakri.Geom.Point.prototype.update = function(x, y){ this.x = isNaN(x) ? this.x : x; this.y = isNaN(y) ? this.y : y; }; Sakri.Geom.Point.prototype.add = function(x, y){ this.x += isNaN(x) ? 0 : x; this.y += isNaN(y) ? 0 : y; }; Sakri.Geom.Point.prototype.equals = function(point){ return this.x==point.x && this.y==point.y; }; Sakri.Geom.Point.prototype.toString = function(){ return "{x:"+this.x+" , y:"+this.y+"}"; }; Sakri.Geom.Point.interpolate = function(pointA, pointB, normal){ return new Sakri.Geom.Point(Sakri.MathUtil.interpolate(normal, pointA.x, pointB.x) , Sakri.MathUtil.interpolate(normal, pointA.y, pointB.y)); }; Sakri.Geom.Point.distanceBetweenTwoPoints = function( point1, point2 ){ //console.log("Math.pow(point2.x - point1.x,2) : ",Math.pow(point2.x - point1.x,2)); return Math.sqrt( Math.pow(point2.x - point1.x,2) + Math.pow(point2.y - point1.y,2) ); }; Sakri.Geom.Point.angleBetweenTwoPoints = function(p1,p2){ return Math.atan2(p1.y-p2.y, p1.x-p2.x); }; Sakri.Geom.mirrorPointInRectangle = function(point,rect){ return new Sakri.Geom.Point(rect.width-point.x,rect.height-point.y); }; Sakri.Geom.randomizePoint = function(point,randomValue){ return new Sakri.Geom.Point(-randomValue+Math.random()*randomValue+point.x,-randomValue+Math.random()*randomValue+point.y); }; //================================================== //=====================::LINE::==================== //================================================== Sakri.Geom.Line = function(pointA, pointB){ this.pointA = pointA ? pointA : new Sakri.Geom.Point(); this.pointB = pointB ? pointB : new Sakri.Geom.Point(); this.updateLineProperties(); }; //=================:: Static methods //first check for horizontal+vertical lines? no division by 0?! Sakri.Geom.Line.getSlope = function(pointA,pointB){ return (pointA.y - pointB.y) / (pointA.x - pointB.x); }; Sakri.Geom.Line.getYIntercept = function(point,slope){ //slope intercept : y = mx + b return point.y - point.x * slope; }; //checks for parallel lines Sakri.Geom.Line.intersects = function(line1, line2){ if(line1.isVertical() && line2.isVertical()){ //if(line1.this.pointB.equals(line2.this.pointA))return line1.this.pointB; //throw new Error("Line.getIntersection() ERROR Two vertical lines cannot intersect"); console.log("Line.getIntersection() ERROR Two vertical lines cannot intersect"); return false; } if(line1.isHorizontal() && line2.isHorizontal()){ //if(line1.this.pointB.equals(line2.this.pointA))return line1.this.pointB; //throw new Error("Line.getIntersection() ERROR Two horizontal lines cannot intersect"); console.log("Line.getIntersection() ERROR Two horizontal lines cannot intersect"); return false; } if(line1.slope == line2.slope){ //throw new Error("Line.getIntersection() ERROR Two Parallel lines cannot intersect"); console.log("Line.getIntersection() ERROR Two Parallel lines cannot intersect"); return false; } return true; }; //returns a Sakri.Geom.Point or null //TODO : create alternative where new Points are not instantiated on every call, maybe pass a Point as an arg? Sakri.Geom.Line.getIntersection = function(line1, line2){ //slope intercept : y = mx + b //m1 * x + b1 - y = m2 * x + b2 - y //m1 * x + b1 - b2 = m2 * x; //m1 * x - m2 * x = b2 - b1; //x * (m1 - m2) = b2 - b1 if(!Sakri.Geom.Line.intersects(line1, line2)){ return null; } //PERPENDICULAR LINES if(line1.isHorizontal() && line2.isVertical()){ return new Sakri.Geom.Point(line2.pointA.x, line1.pointA.y); } if(line2.isHorizontal() && line1.isVertical()){ return new Sakri.Geom.Point(line1.pointA.x, line2.pointA.y); } //ONE HORIZONTAL OR VERTICAL if(line1.isHorizontal()){ return new Sakri.Geom.Point(line2.getXatY(line1.pointA.y), line1.pointA.y); } if(line1.isVertical()){ return new Sakri.Geom.Point(line1.pointA.x, line2.getYatX(line1.pointA.x)); } if(line2.isHorizontal()){ return new Sakri.Geom.Point(line1.getXatY(line2.pointA.y),line2.pointA.y); } if(line2.isVertical()){ return new Sakri.Geom.Point(line2.pointA.x,line1.getYatX(line2.pointA.x)); } var p = new Sakri.Geom.Point(); //console.log("line1.slope : "+line1.slope); //console.log("line2.slope : "+line2.slope); p.x = (line2.yIntercept - line1.yIntercept) / (line1.slope - line2.slope); p.y = line1.getYatX(p.x); return p; }; //=================:: public methods Sakri.Geom.Line.prototype.isHorizontal = function(){ return this.pointA.y == this.pointB.y; }; Sakri.Geom.Line.prototype.isVertical = function(){ return this.pointA.x == this.pointB.x; }; Sakri.Geom.Line.prototype.getYatX = function(x){ if(this.isHorizontal()){ return this.pointA.y; } if(this.isVertical()){ return NaN; }//throw error? return this.slope * x + this.yIntercept; }; Sakri.Geom.Line.prototype.getXatY = function(y){ if(this.isVertical()){ return this.pointA.x; } if(this.isHorizontal()){ return NaN; }//throw error? return (y - this.yIntercept)/this.slope; }; Sakri.Geom.Line.prototype.update = function(pointA, pointB){ if(pointA){ this.pointA.x = isNaN(pointA.x) ? this.pointA.x : pointA.x; this.pointA.y = isNaN(pointA.y) ? this.pointA.y : pointA.y; } if(pointB){ this.pointB.x = isNaN(pointB.x) ? this.pointB.x : pointB.x; this.pointB.y = isNaN(pointB.y) ? this.pointB.y : pointB.y; } this.updateLineProperties(); }; Sakri.Geom.Line.prototype.updateLineProperties = function(){ this.slope = Sakri.Geom.Line.getSlope(this.pointA, this.pointB); this.yIntercept = Sakri.Geom.Line.getYIntercept(this.pointA, this.slope); }; Sakri.Geom.Line.prototype.toString = function(){ return "Line{a : "+this.pointA.toString()+" , b : "+this.pointB.toString()+" , slope : "+this.slope+" , yIntercept : "+this.yIntercept+"}"; };	//==================================================	//===================::RECTANGLE::==================	//==================================================	Sakri.Geom.Rectangle = function (x, y, width, height){	this.update(x, y, width, height);	};	Sakri.Geom.Rectangle.prototype.update = function(x, y, width, height){	this.x = isNaN(x) ? 0 : x;	this.y = isNaN(y) ? 0 : y;	this.width = isNaN(width) ? 0 : width;	this.height = isNaN(height) ? 0 : height;	}; //TODO : doesn't work Sakri.Geom.Rectangle.prototype.inflate = function(x, y){ this.x -= isNaN(x) ? 0 : x; this.y -= isNaN(y) ? 0 : y; this.width += isNaN(x) ? 0 : x * 2; this.height += isNaN(y) ? 0 : y * 2; };	Sakri.Geom.Rectangle.prototype.updateToRect = function(rect){	this.x = rect.x;	this.y = rect.y;	this.width = rect.width;	this.height = rect.height;	};	Sakri.Geom.Rectangle.prototype.scaleX = function(scaleBy){	this.width *= scaleBy;	};	Sakri.Geom.Rectangle.prototype.scaleY = function(scaleBy){	this.height *= scaleBy;	};	Sakri.Geom.Rectangle.prototype.scale = function(scaleBy){	this.scaleX(scaleBy);	this.scaleY(scaleBy);	};	Sakri.Geom.Rectangle.prototype.getRight = function(){	return this.x + this.width;	};	Sakri.Geom.Rectangle.prototype.getBottom = function(){	return this.y + this.height;	}; Sakri.Geom.Rectangle.prototype.getCenter = function(){ return new Sakri.Geom.Point(this.getCenterX(), this.getCenterY()); }; Sakri.Geom.Rectangle.prototype.getCenterX = function(){ return this.x + this.width/2; }; Sakri.Geom.Rectangle.prototype.getCenterY=function(){ return this.y + this.height/2; }; Sakri.Geom.Rectangle.prototype.containsPoint = function(x, y){ return x >= this.x && y >= this.y && x <= this.getRight() && y <= this.getBottom(); }; Sakri.Geom.Rectangle.prototype.containsRect = function(rect){ return this.containsPoint(rect.x, rect.y) && this.containsPoint(rect.getRight(), rect.getBottom()); };	Sakri.Geom.Rectangle.prototype.isSquare = function(){	return this.width == this.height;	};	Sakri.Geom.Rectangle.prototype.isLandscape = function(){	return this.width > this.height;	};	Sakri.Geom.Rectangle.prototype.isPortrait = function(){	return this.width < this.height;	};	Sakri.Geom.Rectangle.prototype.getSmallerSide = function(){	return Math.min(this.width, this.height);	};	Sakri.Geom.Rectangle.prototype.getBiggerSide = function(){	return Math.max(this.width,this.height);	};	Sakri.Geom.Rectangle.prototype.getArea = function(){	return this.width * this.height;	};	Sakri.Geom.Rectangle.prototype.floor = function(){	this.x = Math.floor(this.x);	this.y = Math.floor(this.y);	this.width = Math.floor(this.width);	this.height = Math.floor(this.height);	};	Sakri.Geom.Rectangle.prototype.ceil = function(){	this.x = Math.ceil(this.x);	this.y = Math.ceil(this.y);	this.width = Math.ceil(this.width);	this.height = Math.ceil(this.height);	};	Sakri.Geom.Rectangle.prototype.round = function(){	this.x=Math.round(this.x);	this.y=Math.round(this.y);	this.width=Math.round(this.width);	this.height=Math.round(this.height);	};	Sakri.Geom.Rectangle.prototype.roundIn = function(){	this.x = Math.ceil(this.x);	this.y = Math.ceil(this.y);	this.width = Math.floor(this.width);	this.height = Math.floor(this.height);	};	Sakri.Geom.Rectangle.prototype.roundOut = function(){	this.x = Math.floor(this.x);	this.y = Math.floor(this.y);	this.width = Math.ceil(this.width);	this.height = Math.ceil(this.height);	};	Sakri.Geom.Rectangle.prototype.clone = function(){	return new Sakri.Geom.Rectangle(this.x, this.y, this.width, this.height);	};	Sakri.Geom.Rectangle.prototype.toString = function(){	return "Rectangle{x:"+this.x+" , y:"+this.y+" , width:"+this.width+" , height:"+this.height+"}";	}; //================================================== //=====================::CIRCLE::=================== //================================================== Sakri.Geom.Circle = function (x,y,radius){ Sakri.Geom.Point.call(this,x,y); //call super constructor. this.radius = isNaN(radius) ? 10 : radius;// not sure if this should just be 0? }; //subclass extends superclass Sakri.Geom.Circle.prototype = Object.create(Sakri.Geom.Point.prototype); Sakri.Geom.Circle.prototype.constructor = Sakri.Geom.Point; Sakri.Geom.Circle.prototype.getRandomPointInCircle = function(){ var radius = Math.random() * this.radius; var radian = Math.random() * SimpleGeometry.PI2; var x = this.x + Math.cos(radian) * radius; var y = this.y + Math.sin(radian) * radius; return new Sakri.Geom.Point(x, y); }; Sakri.Geom.Circle.prototype.update = function(x,y,radius){ this.x = isNaN(x) ? this.x : x; this.y = isNaN(y) ? this.y : y; this.radius = isNaN(radius) ? this.radius : radius; } Sakri.Geom.Circle.prototype.clone = function(){ return new Sakri.Geom.Circle(this.x,this.y,this.radius); }; Sakri.Geom.Circle.prototype.containsPoint = function(point){ return Sakri.Geom.Point.distanceBetweenTwoPoints(point, this) <= this.radius; }; Sakri.Geom.Circle.prototype.toString = function(){ return "Circle{x : "+this.x+" , y : "+this.y+" , radius : "+this.radius+"}"; };
}(window));
/** * Created by sakri on 15-1-14. */
//has a dependency on MathUtil, Geom,
(function (window){ var Sakri = window.Sakri || {}; window.Sakri = window.Sakri || Sakri; //bounds is a Rectangle, startingPoint is a Point //TODO : speed should have some documentation? in pixels? Sakri.CircularWander = function(bounds, speed, startingPoint ){ this.minimumRadius = 10; //circles that particles travel along cannot be smaller than this this.minCircleRotation = Math.PI/10;//minimum number of radians that a particle travels along a circle before switching to a new one this.maxCircleRotation = Sakri.MathUtil.PI2;//max number of radians that a particle travels along a circle before switching to a new one this.updates = 0;//current number of "moves" this.bounds = bounds; this.speed = speed; this.currentCircle = new Sakri.Geom.Circle(startingPoint.x, startingPoint.y, this.minimumRadius);//TODO: random starting point ensuring circle is within bounds this.currentRadian = Math.random() * Sakri.MathUtil.PI2; // TODO: rename, not immediately clear what the purpose of this property is? this.position = new Sakri.Geom.Point(startingPoint.x, startingPoint.y); this.radiusLine = new Sakri.Geom.Line();//used for creating circles this.circleFinder = new Sakri.LargestCircleInBoundsForRadiusFinder(); this.setNextChangeTarget(); }; Sakri.CircularWander.prototype.update = function(){ this.currentRadian += this.updateAngleIncrement; this.currentRadian = Sakri.MathUtil.constrainRadianTo2PI(this.currentRadian); this.position.x = this.currentCircle.x + Math.cos(this.currentRadian) * this.currentCircle.radius; this.position.y = this.currentCircle.y + Math.sin(this.currentRadian) * this.currentCircle.radius; if(this.shiftToCenter){ this.currentCircle.radius.x += this.bounds.getCenterX() < this.position.x ? -2 : 2; this.currentCircle.radius.y += this.bounds.getCenterY() < this.position.y ? -2 : 2; } this.updates++; if(this.updates >= this.updatesBeforeNextCircle){ this.setNextCircle(); } }; Sakri.CircularWander.prototype.setNextCircle = function(){ this.radiusLine.pointA.update(this.currentCircle.x, this.currentCircle.y); this.radiusLine.pointB.update(this.position.x, this.position.y); this.radiusLine.updateLineProperties(); var nextCircle = this.circleFinder.findLargestFittingCircle(this.bounds, this.radiusLine, this.currentRadian); //if the radius is too small, just keep rotating until an option big enough becomes available if(nextCircle.radius < this.minimumRadius){ this.updatesBeforeNextCircle = 10; this.shiftToCenter = true; this.updates = 0; return; } //determine direction if(!nextCircle.containsPoint(this.currentCircle) && !this.currentCircle.containsPoint(nextCircle)){ this.speed *= -1; this.currentRadian -= Math.PI; this.currentRadian = Sakri.MathUtil.constrainRadianTo2PI(this.currentRadian); } //set to a random size within the circle nextCircle.radius = Sakri.MathUtil.getRandomNumberInRange(this.minimumRadius, nextCircle.radius); //var angle = Math.atan2( this.position.y - nextCircle.y, this.position.x - nextCircle.x ); var angle = Math.atan2( nextCircle.y-this.position.y, nextCircle.x - this.position.x ); nextCircle.x = this.position.x + Math.cos(angle)*nextCircle.radius; nextCircle.y = this.position.y + Math.sin(angle)*nextCircle.radius; this.currentCircle.update(nextCircle.x, nextCircle.y, nextCircle.radius); this.setNextChangeTarget(); }; //sets speed and duration of stay within current circle Sakri.CircularWander.prototype.setNextChangeTarget = function(){ var circumference = Math.PI * (this.currentCircle.radius * 2); var moveDistance = Sakri.MathUtil.getRandomNumberInRange(this.minCircleRotation, this.maxCircleRotation); var totalDist = (moveDistance / Sakri.MathUtil.PI2) * circumference; this.updatesBeforeNextCircle = Math.ceil(totalDist / Math.abs(this.speed));//number of moves before switching circles, TODO rename this.updateAngleIncrement = (moveDistance / this.updatesBeforeNextCircle) * (this.speed > 0 ? 1 : -1); this.updates = 0; //console.log("CircularWander.setNextChangeTarget" , circumference, moveDistance, totalDist, this.updatesBeforeNextCircle, this.updateAngleIncrement); }; //============================================================================= //===================::LARGEST CIRCLE IN BOUNDS FOR RADIUS FINDER::============ //============================================================================= //I know I know, lame ass class name... Sakri.LargestCircleInBoundsForRadiusFinder = function(){ this.nextCircleOption1 = new Sakri.Geom.Circle(); this.nextCircleOption2 = new Sakri.Geom.Circle(); this.candidateCircleA= new Sakri.Geom.Circle(); this.candidateCircleB = new Sakri.Geom.Circle(); this.angleZone = 0;//all options can be calculated within 90° "zones" this.vertexAngle = 0;//isosceles triangles have one unique angle called the vertex angle this.baseAngle = 0;//isosceles triangles have two equal angles called the base angles this.baseRadian = 0;//angle of isosceles triangle base line, used to calculate intersection with a boundary wall this.candidateDiameter = 0;//diameter of maximum size circle between "point" and a wall this.smaller = 0; }; Sakri.LargestCircleInBoundsForRadiusFinder.prototype.findLargestFittingCircle = function(bounds, radiusLine, radian){ var nextCircle = this.findLargestFittingCircleForRadian(bounds, radiusLine, radian); if(nextCircle){ this.nextCircleOption1.update(nextCircle.x, nextCircle.y, nextCircle.radius); }else{ this.nextCircleOption1.radius = 0;//0 is used to check if valid option TODO: change, not immediately clear why this is done } nextCircle = this.findLargestFittingCircleForRadian(bounds, radiusLine, Sakri.MathUtil.constrainRadianTo2PI(radian-Math.PI)); if(nextCircle){ this.nextCircleOption2.update(nextCircle.x, nextCircle.y, nextCircle.radius); }else{ this.nextCircleOption2.radius = 0;//0 is used to check if valid option } if(!this.nextCircleOption1.radius && !this.nextCircleOption2.radius){ throw new Error("NO CIRCLE CANDIDATES AVAILABLE?!");//obviously this should never happen. If it does then I have some bug huntin' to do } if(!this.nextCircleOption1.radius){ return this.nextCircleOption2; } if(!this.nextCircleOption2.radius){ return this.nextCircleOption1; } return this.nextCircleOption1.radius>this.nextCircleOption2.radius ? this.nextCircleOption1 : this.nextCircleOption2;//bigger one } Sakri.LargestCircleInBoundsForRadiusFinder.prototype.findLargestFittingCircleForRadian = function(bounds, radiusLine, radian){ var point = radiusLine.pointB;//pointB is travelling particle, pointA is the current center Point //HORIZONTAL AND VERTICAL CASES switch(radian){ //horizontal case 0: this.candidateCircleA.x = point.x + (bounds.getRight() - point.x)/2; this.candidateCircleA.y = point.y; this.candidateCircleA.radius = bounds.getRight() - this.candidateCircleA.x; return; case Math.PI: this.candidateCircleA.x = bounds.x + (point.x-bounds.x)/2; this.candidateCircleA.y = point.y; this.candidateCircleA.radius = this.candidateCircleA.x-bounds.x; return; //vertical case Sakri.MathUtil.HALF_PI: this.candidateCircleA.x = point.x; this.candidateCircleA.y = point.y + (bounds.getBottom() - point.y)/2; this.candidateCircleA.radius = bounds.getBottom() - this.candidateCircleA.y; return; case Sakri.MathUtil.PI_AND_HALF: this.candidateCircleA.x = point.x; this.candidateCircleA.y = bounds.y + (point.y-bounds.y)/2; this.candidateCircleA.radius = this.candidateCircleA.y - bounds.y; return; } //ALL OTHER ANGLES this.angleZone = Math.floor(radian / Sakri.MathUtil.HALF_PI);//all options can be calculated within 90° "zones" switch(this.angleZone){ case 0: //Candidate from right wall this.vertexAngle = Math.PI - radian; //180 - radian this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.getRight() - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point //Candidate from bottom wall this.vertexAngle = Sakri.MathUtil.HALF_PI + radian; //90 + radian this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.getBottom() - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point break; case 1: //Candidate from left wall this.vertexAngle = radian; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.x - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point //Candidate from bottom wall this.vertexAngle = Math.PI-radian + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.getBottom() - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point break; case 2: //Candidate from left wall this.vertexAngle = Sakri.MathUtil.PI2 - radian;//360 - angle this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.x - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point //Candidate from top wall this.vertexAngle = radian - Math.PI + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.y - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point break; case 3: //Candidate from right wall this.vertexAngle = Math.PI - (Sakri.MathUtil.PI2-radian); this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.getRight() - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point //Candidate from top wall this.vertexAngle = Sakri.MathUtil.PI2 - radian + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.y - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point break; } this.candidateCircleA.radius *= .95;//small margin to make sure the circle doesn't exceed borders this.candidateCircleB.radius *= .95;//small margin to make sure the circle doesn't exceed borders var containsA = Sakri.LargestCircleInBoundsForRadiusFinder.rectangleContainsCircle(bounds, this.candidateCircleA); var containsB = Sakri.LargestCircleInBoundsForRadiusFinder.rectangleContainsCircle(bounds, this.candidateCircleB); if(!containsA && !containsB){ return null; } if(!containsA){ return this.candidateCircleB; } if(!containsB){ return this.candidateCircleA; } return this.candidateCircleA.radius > this.candidateCircleB.radius ? this.candidateCircleA : this.candidateCircleB; }; Sakri.LargestCircleInBoundsForRadiusFinder.rectangleContainsCircle = function(rectangle, circle){ //0° if(!rectangle.containsPoint(circle.x+circle.radius, circle.y)){ return false; } //90° if(!rectangle.containsPoint(circle.x, circle.y+circle.radius)){ return false; } //180° if(!rectangle.containsPoint(circle.x-circle.radius, circle.y)){ return false; } //270° if(!rectangle.containsPoint(circle.x, circle.y-circle.radius)){ return false; } return true; }
}(window));
/** * Created by sakri on 15-1-14. */
//has a dependency on MathUtil, Geom,
(function (window){ var Sakri = window.Sakri || {}; window.Sakri = window.Sakri || Sakri; //bounds is a Rectangle, startingPoint is a Point //TODO : speed should have some documentation? in pixels? Sakri.CircularWanderExp = function(bounds, speed, startingPoint, context ){ this.minimumRadius = 10; //circles that particles travel along cannot be smaller than this this.minCircleRotation = Math.PI/3;//minimum number of radians that a particle travels along a circle before switching to a new one this.maxCircleRotation = Sakri.MathUtil.PI2;//max number of radians that a particle travels along a circle before switching to a new one this.updates = 0;//current number of "moves" this.bounds = bounds; this.speed = speed; this.currentCircle = new Sakri.Geom.Circle(startingPoint.x, startingPoint.y, 50); this.context = context; this.currentRadian = Math.random() * Sakri.MathUtil.PI2; // TODO: rename, not immediately clear what the purpose of this property is? this.position = new Sakri.Geom.Point(startingPoint.x, startingPoint.y); this.radiusLine = new Sakri.Geom.Line();//used for creating circles this.circleFinder = new Sakri.LargestCircleInBoundsForRadiusFinderExp(); this.leftColor = "#FF0000"; this.topColor = "#00FF00"; this.rightColor = "#0000FF"; this.bottomColor = "#00FFFF"; this.createExplanationObjects(); this.explanationObjectIndex = 0; this.setNextChangeTarget(); this.updatesBeforeNextCircle = 80; this.startMovementAlongCircle(); }; Sakri.CircularWanderExp.prototype.createExplanationObjects = function(){ this.nextCircleExp = new Sakri.Geom.Circle(); this.nextCircleUseExp = new Sakri.Geom.Circle(); this.explanationObjects = []; for(var i=0; i<4; i++){ this.explanationObjects[i] = {circle:new Sakri.Geom.Circle()}; } } Sakri.CircularWanderExp.prototype.startMovementAlongCircle = function(){ var scope = this; this.moveIntervalId = setInterval(function(){scope.update()}, 20); }; Sakri.CircularWanderExp.prototype.stopMovementAlongCircle = function(){ clearInterval(this.moveIntervalId); }; Sakri.CircularWanderExp.prototype.setResumeTimeOut = function(){ var scope = this; this.resumeTimeOutId = setTimeout(function(){scope.startMovementAlongCircle()}, 100); }; Sakri.CircularWanderExp.prototype.stopExplaining = function(){ console.log("CircularWanderExp.stopExplaining()"); clearInterval(this.moveIntervalId); clearTimeout(this.resumeTimeOutId); clearTimeout(this.renderExpObjTimeoutId); }; Sakri.CircularWanderExp.prototype.update = function(){ this.currentRadian += this.updateAngleIncrement; this.currentRadian = Sakri.MathUtil.constrainRadianTo2PI(this.currentRadian); this.position.x = this.currentCircle.x + Math.cos(this.currentRadian) * this.currentCircle.radius; this.position.y = this.currentCircle.y + Math.sin(this.currentRadian) * this.currentCircle.radius; if(this.shiftToCenter){ this.currentCircle.radius.x += this.bounds.getCenterX() < this.position.x ? -2 : 2; this.currentCircle.radius.y += this.bounds.getCenterY() < this.position.y ? -2 : 2; } if(!this.bounds.containsPoint(this.position.x, this.position.y)){ console.log("OUT OF BOUNDS!"); this.stopExplaining(); return; } this.updates++; this.context.clearRect(0,0,this.bounds.width, this.bounds.height); //render current circle this.context.lineWidth = 1; this.context.strokeStyle = "#FFFF00"; this.context.beginPath(); this.context.arc(this.currentCircle.x, this.currentCircle.y, this.currentCircle.radius, 0, Sakri.MathUtil.PI2); this.context.closePath(); this.context.stroke(); this.renderCurrentPosition(); if(this.updates >= this.updatesBeforeNextCircle){ this.stopMovementAlongCircle(); this.setNextCircle(); } }; Sakri.CircularWanderExp.prototype.renderCurrentPosition = function(){ //render current position this.renderBorders(); this.context.fillStyle = "#FF0000"; this.context.beginPath(); this.context.arc(this.position.x, this.position.y, 10, 0, Sakri.MathUtil.PI2); this.context.closePath(); this.context.fill(); } Sakri.CircularWanderExp.prototype.renderBorders = function(){ this.renderTopBorder(1); this.renderRightBorder(1); this.renderBottomBorder(1); this.renderLeftBorder(1); } Sakri.CircularWanderExp.prototype.renderTopBorder = function(lineThickness){ this.context.beginPath(); this.context.strokeStyle = this.topColor; this.context.lineWidth = lineThickness; this.context.moveTo(this.bounds.x, this.bounds.y); this.context.lineTo(this.bounds.getRight(), this.bounds.y); this.context.closePath(); this.context.stroke(); } Sakri.CircularWanderExp.prototype.renderRightBorder = function(lineThickness){ this.context.beginPath(); this.context.strokeStyle = this.rightColor; this.context.lineWidth = lineThickness; this.context.moveTo(this.bounds.getRight(), this.bounds.y); this.context.lineTo(this.bounds.getRight(), this.bounds.getBottom()); this.context.closePath(); this.context.stroke(); } Sakri.CircularWanderExp.prototype.renderBottomBorder = function(lineThickness){ this.context.beginPath(); this.context.strokeStyle = this.bottomColor; this.context.lineWidth = lineThickness; this.context.moveTo(this.bounds.getRight(), this.bounds.getBottom()); this.context.lineTo(this.bounds.x, this.bounds.getBottom()); this.context.closePath(); this.context.stroke(); } Sakri.CircularWanderExp.prototype.renderLeftBorder = function(lineThickness){ this.context.beginPath(); this.context.strokeStyle = this.leftColor; this.context.lineWidth = lineThickness; this.context.moveTo(this.bounds.x, this.bounds.getBottom()); this.context.lineTo(this.bounds.x, this.bounds.y); this.context.closePath(); this.context.stroke(); } Sakri.CircularWanderExp.prototype.startExplanationRenders = function(){ this.explanationObjectIndex = 0; this.renderNextExplanationObject(); } Sakri.CircularWanderExp.prototype.renderNextExplanationObject = function(){ if(this.explanationObjectIndex>=4){ this.renderSelection1(); return; } this.renderExplanationObject(this.explanationObjects[this.explanationObjectIndex]); var scope = this; this.renderExpObjTimeoutId = setTimeout(function(){scope.renderNextExplanationObject()}, 1000); this.explanationObjectIndex++; } Sakri.CircularWanderExp.prototype.renderSelection1 = function(){ this.renderNextCircle(); } Sakri.CircularWanderExp.prototype.setExplanationObject = function(vertexAngle, baseAngle, baseRadian, candidateDiameter, circle, radian, line, color){ this.explanationObjects[this.explanationObjectIndex].circle.update(circle.x, circle.y, circle.radius); this.explanationObjects[this.explanationObjectIndex].vertexAngle = vertexAngle; this.explanationObjects[this.explanationObjectIndex].baseAngle = baseAngle; this.explanationObjects[this.explanationObjectIndex].baseRadian = baseRadian; this.explanationObjects[this.explanationObjectIndex].candidateDiameter = candidateDiameter; this.explanationObjects[this.explanationObjectIndex].radian = radian; this.explanationObjects[this.explanationObjectIndex].line = line; this.explanationObjects[this.explanationObjectIndex].color = color; this.explanationObjectIndex++; } Sakri.CircularWanderExp.prototype.renderExplanationObject = function(expObj){ this.context.beginPath(); this.context.lineWidth = 1; this.context.strokeStyle = expObj.color; this.context.arc(expObj.circle.x, expObj.circle.y, expObj.circle.radius, 0, Sakri.MathUtil.PI2); this.context.closePath(); this.context.stroke(); switch(expObj.color) { case this.topColor: this.renderTopBorder(8); break; case this.rightColor: this.renderRightBorder(8); break; case this.bottomColor: this.renderBottomBorder(8); break; case this.leftColor: this.renderLeftBorder(8); break; } this.context.beginPath(); this.context.strokeStyle = "#FFFF00"; this.context.lineWidth = 3; this.context.moveTo(expObj.line.pointA.x, expObj.line.pointA.y); this.context.lineTo(expObj.line.pointB.x, expObj.line.pointB.y); this.context.closePath(); this.context.stroke(); this.context.beginPath(); this.context.strokeStyle = expObj.color; this.context.lineWidth = 1; this.context.moveTo(expObj.line.pointA.x, expObj.line.pointA.y); this.context.lineTo(expObj.line.pointB.x, expObj.line.pointB.y); this.context.lineTo(expObj.line.pointB.x + Math.cos(expObj.baseRadian) * expObj.candidateDiameter, expObj.line.pointB.y + Math.sin(expObj.baseRadian) * expObj.candidateDiameter); this.context.lineTo(expObj.circle.x, expObj.circle.y); this.context.closePath(); this.context.stroke(); this.renderCurrentPosition(); } Sakri.CircularWanderExp.prototype.renderNextCircle = function(){ for(var i = 0; i<4; i++){ var expObj = this.explanationObjects[i]; if(expObj.circle.equals(this.nextCircleExp)){ this.context.beginPath(); this.context.fillStyle = expObj.color; this.context.arc(expObj.circle.x, expObj.circle.y, expObj.circle.radius, 0, Sakri.MathUtil.PI2); this.context.closePath(); this.context.globalAlpha = .2; this.context.fill(); this.context.globalAlpha = 1; break; } } var scope = this; this.renderExpObjTimeoutId = setTimeout(function(){scope.renderNextUseCircle()}, 1000); this.renderCurrentPosition(); } Sakri.CircularWanderExp.prototype.renderNextUseCircle = function(){ this.context.beginPath(); this.context.lineWidth = 1; this.context.fillStyle = "#FFFFFF"; this.context.strokeStyle = "#FFFF00"; this.context.arc(this.nextCircleUseExp.x, this.nextCircleUseExp.y, this.nextCircleUseExp.radius, 0, Sakri.MathUtil.PI2); this.context.closePath(); this.context.globalAlpha = .9; this.context.fill(); this.context.globalAlpha = 1; this.context.stroke(); var scope = this; this.renderExpObjTimeoutId = setTimeout(function(){scope.startMovementAlongCircle()}, 1000); this.renderCurrentPosition(); } Sakri.CircularWanderExp.prototype.setNextCircle = function(){ this.explanationObjectIndex = 0; this.radiusLine.pointA.update(this.currentCircle.x, this.currentCircle.y); this.radiusLine.pointB.update(this.position.x, this.position.y); this.radiusLine.updateLineProperties(); var nextCircle = this.circleFinder.findLargestFittingCircle(this.bounds, this.radiusLine, this.currentRadian, this.context, this); //if the radius is too small, just keep rotating until an option big enough becomes available if(nextCircle.radius < this.minimumRadius){ console.log("small radius"); this.updatesBeforeNextCircle = 10; this.shiftToCenter = true; this.updates = 0; this.startMovementAlongCircle(); return; } this.shiftToCenter = false; if(!nextCircle.containsPoint(this.currentCircle) && !this.currentCircle.containsPoint(nextCircle)){ //console.log("switch speed : ", this.speed); this.speed *= -1; this.currentRadian -= Math.PI; this.currentRadian = Sakri.MathUtil.constrainRadianTo2PI(this.currentRadian); } this.nextCircleExp.update(nextCircle.x, nextCircle.y, nextCircle.radius); nextCircle.radius = Sakri.MathUtil.getRandomNumberInRange(this.minimumRadius, nextCircle.radius); //var angle = Math.atan2( this.position.y - nextCircle.y, this.position.x - nextCircle.x ); var angle = Math.atan2( nextCircle.y-this.position.y, nextCircle.x - this.position.x ); nextCircle.x = this.position.x + Math.cos(angle)*nextCircle.radius; nextCircle.y = this.position.y + Math.sin(angle)*nextCircle.radius; this.currentCircle.update(nextCircle.x, nextCircle.y, nextCircle.radius); this.nextCircleUseExp.update(nextCircle.x, nextCircle.y, nextCircle.radius); this.setNextChangeTarget(); this.startExplanationRenders(); }; //TODO : this needs to be revisited... calculations seem a bit half baked... Sakri.CircularWanderExp.prototype.setNextChangeTarget = function(){ var circumference = Math.PI * (this.currentCircle.radius * 2); var moveDistance = Sakri.MathUtil.getRandomNumberInRange(this.minCircleRotation, this.maxCircleRotation); var totalDist = (moveDistance / Sakri.MathUtil.PI2) * circumference; this.updatesBeforeNextCircle = Math.ceil(totalDist / Math.abs(this.speed));//number of moves before switching circles, TODO rename this.updateAngleIncrement = (moveDistance / this.updatesBeforeNextCircle) * (this.speed > 0 ? 1 : -1); this.updates = 0; //console.log("CircularWanderExp.setNextChangeTarget" , circumference, moveDistance, totalDist, this.updatesBeforeNextCircle, this.updateAngleIncrement); }; //============================================================================= //=========================::AND NOW, SHOWING MY TRUE COLORS::================= //======================::IN THE ANCIENT ART OF CLASS NAMING,::================ //==================================::I PRESENT::============================== //============================================================================= //===================::LARGEST CIRCLE IN BOUNDS FOR RADIUS FINDER::============ //============================================================================= Sakri.LargestCircleInBoundsForRadiusFinderExp = function(){ //these should be private variables. The idea is to not have to instantiate these variables on every single call //originally I had these as Static variables, which they could still be given a situation where there are a lot of wanderers this.nextCircleOption1 = new Sakri.Geom.Circle(); this.nextCircleOption2 = new Sakri.Geom.Circle(); this.candidateCircleA= new Sakri.Geom.Circle(); this.candidateCircleB = new Sakri.Geom.Circle(); this.angleZone = 0;//all options can be calculated within 90° "zones" this.vertexAngle = 0;//isosceles triangles have one unique angle called the vertex angle this.baseAngle = 0;//isosceles triangles have two equal angles called the base angles this.baseRadian = 0;//angle of isosceles triangle base line, used to calculate intersection with a boundary wall this.candidateDiameter = 0;//diameter of maximum size circle between "point" and a wall this.smaller = 0; }; Sakri.LargestCircleInBoundsForRadiusFinderExp.prototype.findLargestFittingCircle = function(bounds, radiusLine, radian, context, wander){ var nextCircle = this.findLargestFittingCircleForRadian(bounds, radiusLine, radian, context, wander); if(nextCircle){ this.nextCircleOption1.update(nextCircle.x, nextCircle.y, nextCircle.radius); }else{ this.nextCircleOption1.radius = 0;//0 is used to check if valid option TODO: change, not immediately clear why this is done } nextCircle = this.findLargestFittingCircleForRadian(bounds, radiusLine, Sakri.MathUtil.constrainRadianTo2PI(radian-Math.PI), context, wander); if(nextCircle){ this.nextCircleOption2.update(nextCircle.x, nextCircle.y, nextCircle.radius); }else{ this.nextCircleOption2.radius = 0;//0 is used to check if valid option } if(!this.nextCircleOption1.radius && !this.nextCircleOption2.radius){ throw new Error("NO CIRCLE CANDIDATES AVAILABLE?!");//obviously this should never happen. If it does then I have some bug huntin' to do } if(!this.nextCircleOption1.radius){ return this.nextCircleOption2; } if(!this.nextCircleOption2.radius){ return this.nextCircleOption1; } return this.nextCircleOption1.radius>this.nextCircleOption2.radius ? this.nextCircleOption1 : this.nextCircleOption2;//bigger one //return Math.random()>.5 ? this.nextCircleOption1 : this.nextCircleOption2;//either one } Sakri.LargestCircleInBoundsForRadiusFinderExp.prototype.findLargestFittingCircleForRadian = function(bounds, radiusLine, radian, context, wander){ //console.log("findLargestFittingCircleAndUpdate() ", bounds.toString(), radiusLine.toString(), radian); var point = radiusLine.pointB;//pointB is travelling particle, pointA is the current center Point //HORIZONTAL AND VERTICAL CASES switch(radian){ //horizontal case 0: this.candidateCircleA.x = point.x + (bounds.getRight() - point.x)/2; this.candidateCircleA.y = point.y; this.candidateCircleA.radius = bounds.getRight() - this.candidateCircleA.x; return; case Math.PI: this.candidateCircleA.x = bounds.x + (point.x-bounds.x)/2; this.candidateCircleA.y = point.y; this.candidateCircleA.radius = this.candidateCircleA.x-bounds.x; return; //vertical case Sakri.MathUtil.HALF_PI: this.candidateCircleA.x = point.x; this.candidateCircleA.y = point.y + (bounds.getBottom() - point.y)/2; this.candidateCircleA.radius = bounds.getBottom() - this.candidateCircleA.y; return; case Sakri.MathUtil.PI_AND_HALF: this.candidateCircleA.x = point.x; this.candidateCircleA.y = bounds.y + (point.y-bounds.y)/2; this.candidateCircleA.radius = this.candidateCircleA.y - bounds.y; return; } //ALL OTHER ANGLES this.angleZone = Math.floor(radian / Sakri.MathUtil.HALF_PI);//all options can be calculated within 90° "zones" //console.log("\tthis.angleZone : ", this.angleZone); switch(this.angleZone){ case 0: //Candidate from right wall this.vertexAngle = Math.PI - radian; //180 - radian this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.getRight() - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleA, radian, radiusLine, wander.rightColor); //Candidate from bottom wall this.vertexAngle = Sakri.MathUtil.HALF_PI + radian; //90 + radian this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.getBottom() - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleB, radian, radiusLine, wander.bottomColor); break; case 1: //Candidate from left wall this.vertexAngle = radian; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.x - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleA, radian, radiusLine, wander.leftColor); //Candidate from bottom wall this.vertexAngle = Math.PI-radian + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.getBottom() - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleB, radian, radiusLine, wander.bottomColor); break; case 2: //Candidate from left wall this.vertexAngle = Sakri.MathUtil.PI2 - radian;//360 - angle this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.x - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleA, radian, radiusLine, wander.leftColor); //Candidate from top wall this.vertexAngle = radian - Math.PI + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.y - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleB, radian, radiusLine, wander.topColor); break; case 3: //Candidate from right wall this.vertexAngle = Math.PI - (Sakri.MathUtil.PI2-radian); this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian + this.baseAngle; this.candidateDiameter = (bounds.getRight() - point.x) / Math.cos(this.baseRadian); this.candidateCircleA.y = point.y + Math.sin(this.baseRadian) * this.candidateDiameter; this.candidateCircleA.x = radiusLine.getXatY(this.candidateCircleA.y); this.candidateCircleA.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleA);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleA, radian, radiusLine, wander.rightColor); //Candidate from top wall this.vertexAngle = Sakri.MathUtil.PI2 - radian + Sakri.MathUtil.HALF_PI; this.baseAngle = (Math.PI - this.vertexAngle) / 2;//180 minus vertex angle divided by 2 this.baseRadian = radian - this.baseAngle; this.candidateDiameter = (bounds.y - point.y) / Math.sin(this.baseRadian); this.candidateCircleB.x = point.x + Math.cos(this.baseRadian) * this.candidateDiameter; this.candidateCircleB.y = radiusLine.getYatX(this.candidateCircleB.x); this.candidateCircleB.radius = Sakri.Geom.Point.distanceBetweenTwoPoints(point, this.candidateCircleB);//Circle extends point wander.setExplanationObject(this.vertexAngle, this.baseAngle, this.baseRadian, this.candidateDiameter, this.candidateCircleB, radian, radiusLine, wander.topColor); break; } this.candidateCircleA.radius *= .95;//small margin to make sure the circle doesn't exceed borders this.candidateCircleB.radius *= .95;//small margin to make sure the circle doesn't exceed borders var containsA = Sakri.LargestCircleInBoundsForRadiusFinderExp.rectangleContainsCircle(bounds, this.candidateCircleA); var containsB = Sakri.LargestCircleInBoundsForRadiusFinderExp.rectangleContainsCircle(bounds, this.candidateCircleB); if(!containsA && !containsB){ return null; } if(!containsA){ return this.candidateCircleB; } if(!containsB){ return this.candidateCircleA; } return this.candidateCircleA.radius > this.candidateCircleB.radius ? this.candidateCircleA : this.candidateCircleB; }; Sakri.LargestCircleInBoundsForRadiusFinderExp.rectangleContainsCircle = function(rectangle, circle){ //0° if(!rectangle.containsPoint(circle.x+circle.radius, circle.y)){ return false; } //90° if(!rectangle.containsPoint(circle.x, circle.y+circle.radius)){ return false; } //180° if(!rectangle.containsPoint(circle.x-circle.radius, circle.y)){ return false; } //270° if(!rectangle.containsPoint(circle.x, circle.y-circle.radius)){ return false; } return true; }
}(window)); //======================== //general properties for demo set up //======================== var canvas; var context; var canvasContainer; var htmlBounds; var bounds; var minimumStageWidth = 250; var minimumStageHeight = 250; var resizeTimeoutId = -1; var readyStateCheckInterval = setInterval( function() { if (document.readyState === "complete") { clearInterval(readyStateCheckInterval); init(); } }, 10); function init(){ canvas = document.createElement('canvas'); canvas.style.position = "absolute"; context = canvas.getContext("2d"); canvasContainer = document.getElementById("canvasContainer"); canvasContainer.appendChild(canvas); window.onresize = resizeHandler; commitResize(); } function getWidth( element ){return Math.max(element.scrollWidth,element.offsetWidth,element.clientWidth );} function getHeight( element ){return Math.max(element.scrollHeight,element.offsetHeight,element.clientHeight );} //avoid running resize scripts repeatedly if a browser window is being resized by dragging function resizeHandler(){ context.clearRect(0,0,canvas.width, canvas.height); clearInterval(intervalId); clearTimeoutsAndIntervals(); resizeTimeoutId = setTimeout(function(){commitResize();}, 300 ); } function commitResize(){ htmlBounds = new Sakri.Geom.Rectangle(0,0, getWidth(this.canvasContainer) , getHeight(canvasContainer)); if(htmlBounds.width>=800){ canvas.width = 800; canvas.style.left = htmlBounds.getCenterX() - 400+"px"; }else{ canvas.width = htmlBounds.width; canvas.style.left ="0px"; } if(htmlBounds.height>600){ canvas.height = 600; canvas.style.top = htmlBounds.getCenterY() - 300+"px"; }else{ canvas.height = htmlBounds.height; canvas.style.top ="0px"; } bounds = new Sakri.Geom.Rectangle(0,0, canvas.width, canvas.height); context.clearRect(0,0,canvas.width, canvas.height); if(bounds.width < minimumStageWidth || bounds.height < minimumStageHeight){ stageTooSmallHandler(); return; } startDemo(); } function stageTooSmallHandler(){ var warning = "Sorry, bigger screen required :("; var props = new Sakri.CanvasTextProperties(null,null,24); context.font = props.getFontString(); context.fillText(warning, bounds.getCenterX() - context.measureText(warning).width/2, bounds.getCenterY()-12); } //======================== //Demo specific properties //======================== var circularWander; var circularWanderExp; var circularWanderers; var wanderIntervalId = -1; function clearTimeoutsAndIntervals(){ context.clearRect(0,0,canvas.width, canvas.height); if(circularWanderExp){ circularWanderExp.stopExplaining(); } circularWanderExp = null; circularWander = null; if(circularWanderers){ for(var i = 0; i<circularWanderers.length; i++){ circularWanderers[i] = null; } } circularWanderers = null; clearInterval(wanderIntervalId); } function startDemo(){ explainMode(); } function explainMode(){ if(circularWanderExp){ console.log("explainMode() skip"); return; } clearTimeoutsAndIntervals(); circularWanderExp = new Sakri.CircularWanderExp(bounds, 4, bounds.getCenter() , context);//assume no character will be less wide than "speed" circularWanderExp.maxCircleRotation = Math.PI; circularWanderExp.minimumRadius = 20; } function singleMode(){ if(circularWander){ console.log("singleMode() skip"); return; } clearTimeoutsAndIntervals(); circularWander = new Sakri.CircularWander(bounds, 4, bounds.getCenter() , context);//assume no character will be less wide than "speed" circularWander.maxCircleRotation = Math.PI; circularWander.minimumRadius = 20; wanderIntervalId = setInterval(updateSingleMode, 20); } function updateSingleMode(){ circularWander.update(); //render current position context.globalAlpha = .1; context.fillStyle = "#FFFFFF"; context.fillRect(0, 0, canvas.width, canvas.height); context.globalAlpha = 1; context.fillStyle = "#FF0000"; context.beginPath(); context.arc(circularWander.position.x, circularWander.position.y, 10, 0, Sakri.MathUtil.PI2); context.closePath(); context.fill(); } function getWanderColor(base){ return base - 15 + Math.floor(Math.random()*30); } function manyMode(){ if(circularWanderers){ console.log("manyMode() skip"); } clearTimeoutsAndIntervals(); var p = bounds.getCenter(); circularWanderers = []; var r = Sakri.MathUtil.getRandomIntegerInRange(100, 200); var g = Sakri.MathUtil.getRandomIntegerInRange(100, 200); var b = Sakri.MathUtil.getRandomIntegerInRange(100, 200); for(var i = 0; i < 50; i++){ circularWanderers[i] = new Sakri.CircularWander(bounds, 4, p);//assume no character will be less wide than "speed" circularWanderers[i].maxCircleRotation = Math.PI; circularWanderers[i].minimumRadius = 20; circularWanderers[i].color = Sakri.MathUtil.rgbToHex( getWanderColor(b), getWanderColor(g), getWanderColor(b) ); circularWanderers[i].renderRadius = 1 + Math.round(Math.random() * 25); } wanderIntervalId = setInterval(updateManyMode, 20); } function updateManyMode(){ context.globalAlpha = .1; context.fillStyle = "#FFFFFF"; context.fillRect(0, 0, canvas.width, canvas.height); context.globalAlpha = 1; var wander, i; for(i = 0; i < circularWanderers.length; i++){ context.beginPath(); wander = circularWanderers[i]; wander.update(); context.fillStyle = wander.color; context.arc(wander.position.x, wander.position.y, wander.renderRadius, 0, Sakri.MathUtil.PI2); context.closePath(); context.fill(); } }
Circular Wander - Script Codes
Circular Wander - Script Codes
Home Page Home
Developer Sakri Rosenstrom
Username sakri
Uploaded September 13, 2022
Rating 3
Size 11,203 Kb
Views 26,312
Do you need developer help for Circular Wander?

Find the perfect freelance services for your business! Fiverr's mission is to change how the world works together. Fiverr connects businesses with freelancers offering digital services in 500+ categories. Find Developer!

Sakri Rosenstrom (sakri) Script Codes
Create amazing blog posts with AI!

Jasper is the AI Content Generator that helps you and your team break through creative blocks to create amazing, original content 10X faster. Discover all the ways the Jasper AI Content Platform can help streamline your creative workflows. Start For Free!