Grid-based lighting engine - javascript

I am trying to create a grid-based shadow engine using JavaScript. My algorithm shades squares based on whether their position is 'behind' a block, relative to a light source.
This is my algorithm so far: https://jsfiddle.net/jexqpfLf/
var canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
var light_x = 90;
var light_y = 110;
var block_x = 120;
var block_y = 120;
requestAnimationFrame(render);
function render() {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
var vec1_x = block_x - light_x;
var vec1_y = block_y - light_y;
var vec1_mag = Math.sqrt(vec1_x * vec1_x + vec1_y * vec1_y);
ctx.fillStyle = 'black';
for (var x = 0; x < canvas.width; x += 10)
for (var y = 0; y < canvas.width; y += 10) {
var vec2_x = x - light_x;
var vec2_y = y - light_y;
var vec2_mag = Math.sqrt(vec2_x * vec2_x + vec2_y * vec2_y);
var dotproduct = vec1_x * vec2_x + vec1_y * vec2_y;
var angle = Math.acos(dotproduct / (vec1_mag * vec2_mag));
if (vec2_mag > vec1_mag && angle < Math.PI / 8 / vec1_mag * 10)
ctx.fillRect(x, y, 10, 10);
}
ctx.fillStyle = 'green';
ctx.fillRect(light_x, light_y, 10, 10);
ctx.fillStyle = 'red';
ctx.fillRect(block_x, block_y, 10, 10);
requestAnimationFrame(render);
}
onkeydown = function (e) {
if (e.which == 65)
light_x -= 10;
if (e.which == 68)
light_x += 10;
if (e.which == 87)
light_y -= 10;
if (e.which == 83)
light_y += 10;
}
Unfortunately, as you can see in the demonstration, I'm finding some angles problematic. Some squares which should be shaded are left unshaded. This happens for some angles and distances (between the light source and the block) but not others. For example, placing the light source at (60, 90) shows these artifacts as well.
I am using the vectors LP (from light to point) and LB (from light to block), taking their dot product and dividing by the product of their magnitudes to find the shading angle, then scaling this angle depending on the distance between the block and light source.
Could these artifacts be due to rounding errors? Or is there a problem with the algorithm itself? Any help would be appreciated :-)

Great question. You're not gonna like this one.
It's a floating point math issue.
What's the value of Math.acos(1.000000000000000)?
0.
Whats the value of Math.acos(1.0000000000000003)?
NaN.
That's annoying, isn't it?
At some values, your dotproduct is 6000 and your (vec1_mag * vec2_mag) is 5999.999999999999, leading to the issue above.
Changing (vec1_mag * vec2_mag) to Math.round(vec1_mag * vec2_mag) will solve your problem.
While we're staring at this fiddle together you should know that there's another bug:
for (var x = 0; x < canvas.width; x += 10) {
for (var y = 0; y < canvas.width; y += 10) {
You use canvas.width here twice. I imagine the second one ought to be canvas.height, so make sure what you wrote there is what you want.
Working fiddle for you!

Related

Trimming length of a random drawn line in HTML canvas over time

I have the code below that is drawing a continue line on to a canvas. The line is using live input from sliders as seen here My test page, this simulates a Spirograph with 3 axis. (EDIT! you have to move slider to start)
I want to keep the lines a set length removing the tail as i go, but because the sliders update the line in real time I am not sure how best to do this and i cant simple recalculate the line unless i record the time the values change.
I was thinking that I could store a list of all the point in an array to make up the of the length of line I am interested in and then clear and redraw each time, but this seems like a lot of duplication. it would be an array of several 100 to 1000 points.
I think this is the way to go and just push the old points out the bottom as new ones are calculated but does any one have any better solutions.
// Your code here!
var globalID;
var r1 = 80;
var ang1 = 0;
var ang2 = 0;
var ang3 = 0;
var flag = null;
var x2 = 0;
var y2 = 0
function drawpatten() {
var canvas = document.getElementById("graphicsView");
var ctx = canvas.getContext('2d');
ctx.strokeStyle = "#0000FF";
ctx.beginPath();
// move the start if the line to the last know point caculated
ctx.moveTo(x2 + 200, y2 + 200);
// get current value of sliders and devide the value by 1000 (sliders are -100 to + 100 so this gives a value of 0.1 to 0.0001 for each ajustment of angle)
S1 = document.getElementById("slider1");
angm1 = S1.value / 1000;
S2 = document.getElementById("slider2");
angm2 = S2.value / 1000;
S3 = document.getElementById("slider3");
angm3 = S3.value / 1000;
// we are only going to draw to screen for each 10 points we caculate, this allows us to have finer resolutions with out the over head of writing to screen so often
for (i = 0; i < 10; i++) {
//increments the angle and reset after 360 full circle
ang1 = ang1 + angm1;
ang2 = ang2 + angm2;
ang3 = ang3 + angm3;
if (ang1 > 360) { ang1 = ang1 - 360 };
if (ang2 > 360) { ang2 = ang2 - 360 };
if (ang3 > 360) { ang3 = ang3 - 360 };
// caculate the x y cordinates the points on each circle and of sets them
x = (Math.cos(ang1) * r1);
y = (Math.sin(ang1) * r1);
x1 = (Math.cos(ang2) * r1) + x;
y1 = (Math.sin(ang2) * r1) + y;
x2 = (Math.cos(ang3) * r1) + x1;
y2 = (Math.sin(ang3) * r1) + y1;
// draws the next sections of the line
ctx.lineTo(x2 + 200, y2 + 200);
}
// better way to do this but this flag just skips drawing the first time, this is becasue the first step will have a line from 0,0 to first cacualted point)
if (flag > 0) {
ctx.stroke();
}
// set flag after first caculate and stroke
flag = 1
// recussivaly call function
globalID = requestAnimationFrame(drawpatten);
}
As always, clear all and redraw all every frame.
Keep all your points in an Array, iterate through them to create a new Path at every frame, and draw that.
var r1 = 80;
var ang1 = 0;
var ang2 = 0;
var ang3 = 0;
var points = [];
// the anim loop
function anim()  {
// push new points
makepattern();
// remove old points
cleanoldies();
// draw all
draw();
// do it again
requestAnimationFrame(anim);
}
anim();
function cleanoldies() {
var max_length = slider4.value * 2;
while(points.length > max_length) {
points.shift();
}
}
function draw() {
//Here we'll only draw
var canvas = document.getElementById("graphicsView");
var ctx = canvas.getContext('2d');
ctx.strokeStyle = "#0000FF";
// clear all
ctx.clearRect(0, 0, canvas.width, canvas.height);
// a single Path
ctx.beginPath();
// points are stored in a flat array [x, y, x, y, x...]
for (let i = 0; i < points.length; i += 2)
ctx.lineTo(points[i], points[i + 1]);
ctx.stroke();
}
function makepattern() {
// push new points
S1 = document.getElementById("slider1");
angm1 = S1.value / 1000;
S2 = document.getElementById("slider2");
angm2 = S2.value / 1000;
S3 = document.getElementById("slider3");
angm3 = S3.value / 1000;
for (i = 0; i < 10; i++) {
ang1 = ang1 + angm1;
ang2 = ang2 + angm2;
ang3 = ang3 + angm3;
if (ang1 > 360) {
ang1 = ang1 - 360
};
if (ang2 > 360) {
ang2 = ang2 - 360
};
if (ang3 > 360) {
ang3 = ang3 - 360
};
var x = (Math.cos(ang1) * r1),
y = (Math.sin(ang1) * r1),
x1 = (Math.cos(ang2) * r1) + x,
y1 = (Math.sin(ang2) * r1) + y,
x2 = (Math.cos(ang3) * r1) + x1,
y2 = (Math.sin(ang3) * r1) + y1;
// store the next sections of the line
points.push(x2 + 200, y2 + 200);
}
}
<input type="range" min="-100" max="100" value="10" id="slider1"><br>
<input type="range" min="-100" max="100" value="20" id="slider2"><br>
<input type="range" min="-100" max="100" value="10" id="slider3"><br>
<label>length<input type="range" min="0" max="10000" id="slider4" value="300"></label><br>
<canvas id="graphicsView" height="400" width="500"></canvas>
I think the easy way to do this would be, as you suggest, maintain an array of the line segments, pushing newly calculated segments onto the end of the array, and shifting the oldest segments from the beginning. Then, paint the old segment white. This has the side effect of also erasing little bits and pieces of line that shouldn't be erased, but the line moves so fast in your example that it shouldn't be too noticeable.
If that imperfection is not acceptable, then I can't see any other way than to redraw the whole curve every frame. Painting in HTML canvases is nice because the browser won't update the screen until the frame is completely drawn (so you don't have to worry about managing another framebuffer.)

Dot-motion task implementation in JavaScript/React

I am looking for a "random-dot kinematograms" implementation in javascript/HTML (preferably in ReactJS) that I can use in web-based experiments.
Basically, dots moving (arrows indicate motion direction) within a visual field (the circle canvas). Signal dots (shown as filled circles) all move in the same direction, while the noise dots can move in random directions. In the actual display, the correlated and uncorrelated dots in a frame appear the same; the filled and open dots are used in the figure only to explain the principle.
Where can I find an implementation for something like this, where the user can specify the direction using a mouse or slider? Or how would one approach implementing this task in ReactJS? (new to javascript, so tips would be highly appreciated).
I've build you simple canvas based kinematogram that you should extend to your needs. What I've done so far is:
added bucket of balls (total:100) = 50 black, 50 white
blacks are going in strait direction
whites are going randomly and they also bounce from walls
whites are going in random speed
blacks are going in constant speed
To adapt blacks directions you could start by looking in this answer to determine the mouse direction and patch my balls.push loop for that.
To be able to vary balls ratio, I'd add input field somewhere on page and replace my hardcoded noise variable.. something like:
<input type="text" id="t" />
and in javascript pick it like:
var t = document.getElementById("t");
t.addEventListener('keyup', function(ev){ /* update value */ }, false);
hope this helps you in your research and I do encourage you to take a look into the code that I'm posting so try to learn it and extend it :)
;(function() {
'use strict';
var c = document.getElementById('c');
var t = document.getElementById('t');
var ctx = c.getContext('2d');
var w = c.width = window.innerWidth;
var h = c.height = window.innerHeight;
// current dots
var balls=[];
var total = 100;
var noise = 50; // here we could pick the value from user input
var bounce = -1;
for(var i=0 ; i<total ; i++){
balls.push({
x : Math.random() * w,
y : Math.random() * h,
vx : ( i < noise ) ? (Math.random() * (2.5 - 1 + 1) + 1) : 2,
vy : ( i < noise ) ? (Math.random() * (2.5 - 1 + 1) + 1) : 2,
})
}
// draw all balls each frame
function draw() {
ctx.clearRect(0, 0, w, h);
var j, dot;
for(j = 0; j < total; j++) {
dot = balls[j];
ctx.beginPath();
ctx.arc(dot.x, dot.y, 4, 0, Math.PI * 2, false);
ctx.fillStyle = (j > noise) ? "rgb(0,0,0)" : "#fff";
ctx.fill();
ctx.strokeStyle = 'black';
(j < noise) ? ctx.stroke() : '';
}
}
// movement function
function update(){
var i,dot;
for( i=0 ; i< total ; i++){
dot = balls[i];
dot.x += dot.vx;
dot.y += dot.vy;
// if ball is white, bounce it
if( i < noise){
if(dot.x > w){
dot.x = w;
dot.vx *= bounce;
}else if(dot.x <0){
dot.x = 0;
dot.vx *= bounce;
}
if(dot.y > h){
dot.y = h;
dot.vy *= bounce;
}else if(dot.y<0){
dot.y = 0;
dot.vy *= bounce;
}
// if ball is black do not bounce
} else {
if(dot.x > w){
dot.x = 0;
}else if(dot.x <0){
dot.x = w;
}
if(dot.y > h){
dot.y = 0;
}else if(dot.y<0){
dot.y = h;
}
}
}
}
// loop the animation
requestAnimationFrame(function loop() {
requestAnimationFrame(loop);
update();
draw();
});
})();
html,
body {
padding: 0;
margin: 0;
}
canvas {
display: block;
background: white;
}
<canvas id="c"></canvas>

How to clear the canvas without interrupting animations?

I am visualising flight paths with D3 and Canvas. In short, I have data for each flight's origin and destination
as well as the airport coordinates. The ideal end state is to have an indiviudal circle representing a plane moving
along each flight path from origin to destination. The current state is that each circle gets visualised along the path,
yet the removal of the previous circle along the line does not work as clearRect gets called nearly constantly.
Current state:
Ideal state (achieved with SVG):
The Concept
Conceptually, an SVG path for each flight is produced in memory using D3's custom interpolation with path.getTotalLength() and path.getPointAtLength() to move the circle along the path.
The interpolator returns the points along the path at any given time of the transition. A simple drawing function takes these points and draws the circle.
Key functions
The visualisation gets kicked off with:
od_pairs.forEach(function(el, i) {
fly(el[0], el[1]); // for example: fly('LHR', 'JFK')
});
The fly() function creates the SVG path in memory and a D3 selection of a circle (the 'plane') - also in memory.
function fly(origin, destination) {
var pathElement = document.createElementNS(d3.namespaces.svg, 'path');
var routeInMemory = d3.select(pathElement)
.datum({
type: 'LineString',
coordinates: [airportMap[origin], airportMap[destination]]
})
.attr('d', path);
var plane = custom.append('plane');
transition(plane, routeInMemory.node());
}
The plane gets transitioned along the path by the custom interpolater in the delta() function:
function transition(plane, route) {
var l = route.getTotalLength();
plane.transition()
.duration(l * 50)
.attrTween('pointCoordinates', delta(plane, route))
// .on('end', function() { transition(plane, route); });
}
function delta(plane, path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
draw([p.x, p.y]);
};
};
}
... which calls the simple draw() function
function draw(coords) {
// contextPlane.clearRect(0, 0, width, height); << how to tame this?
contextPlane.beginPath();
contextPlane.arc(coords[0], coords[1], 1, 0, 2*Math.PI);
contextPlane.fillStyle = 'tomato';
contextPlane.fill();
}
This results in an extending 'path' of circles as the circles get drawn yet not removed as shown in the first gif above.
Full code here: http://blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6
My question is, how can I achieve to draw only a single, current circle while the previous circle gets removed without interrupting other circles being drawn on the same canvas?
Some failed attempts:
The natural answer is of course context.clearRect(), however, as there's a time delay (roughly a milisecond+) for each circle to be drawn as it needs to get through the function pipeline clearRect gets fired almost constantly.
I tried to tame the perpetual clearing of the canvas by calling clearRect only at certain intervals (Date.now() % 10 === 0 or the like) but that leads to no good either.
Another thought was to calculate the previous circle's position and remove the area specifically with a small and specific clearRect definition within each draw() function.
Any pointers very much appreciated.
Handling small dirty regions, especially if there is overlap between objects quickly becomes very computationally heavy.
As a general rule, a average Laptop/desktop can easily handle 800 animated objects if the computation to calculate position is simple.
This means that the simple way to animate is to clear the canvas and redraw every frame. Saves a lot of complex code that offers no advantage over the simple clear and redraw.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x += p.ax * p.speed;
p.y += p.ay * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(800,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
As pointed out in the comments some machines may be able to draw a circle if one colour and all as one path slightly quicker (not all machines). The point of rendering an image is that it is invariant to the image complexity. Image rendering is dependent on the image size but colour and alpha setting per pixel have no effect on rendering speed. Thus I have changed the circle to show the direction of each point via a little plane icon.
Path follow example
I have added a way point object to each plane that in the demo has a random set of way points added. I called it path (could have used a better name) and a unique path is created for each plane.
The demo is to just show how you can incorporate the D3.js interpolation into the plane update function. The plane.update now calls the path.getPos(time) which returns true if the plane has arrived. If so the plane is remove. Else the new plane coordinates are used (stored in the path object for that plane) to set the position and direction.
Warning the code for path does little to no vetting and thus can easily be made to throw an error. It is assumed that you write the path interface to the D3.js functionality you want.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const path = {
wayPoints : null, // holds way points
nextTarget : null, // holds next target waypoint
current : null, // hold previously passed way point
x : 0, // current pos x
y : 0, // current pos y
addWayPoint(x,y,time){
this.wayPoints.push({x,y,time});
},
start(){
if(this.wayPoints.length > 1){
this.current = this.wayPoints.shift();
this.nextTarget = this.wayPoints.shift();
}
},
getNextTarget(){
this.current = this.nextTarget;
if(this.wayPoints.length === 0){ // no more way points
return;
}
this.nextTarget = this.wayPoints.shift(); // get the next target
},
getPos(time){
while(this.nextTarget.time < time && this.wayPoints.length > 0){
this.getNextTarget(); // get targets untill the next target is ahead in time
}
if(this.nextTarget.time < time){
return true; // has arrivecd at target
}
// get time normalised ove time between current and next
var timeN = (time - this.current.time) / (this.nextTarget.time - this.current.time);
this.x = timeN * (this.nextTarget.x - this.current.x) + this.current.x;
this.y = timeN * (this.nextTarget.y - this.current.y) + this.current.y;
return false; // has not arrived
}
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
var p;
planes.items.push(p = {
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
path : Object.assign({},path,{wayPoints : []}),
})
return p; // return the plane
},
update(time){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
if(p.path.getPos(time)){ // target reached
planes.items.splice(i--,1); // remove
}else{
p.dir = Math.atan2(p.y - p.path.y, p.x - p.path.x) + Math.PI; // add 180 because i drew plane wrong way around.
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x = p.path.x;
p.y = p.path.y;
}
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(time){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(810,()=>{
var p = planes.add(Math.random() * canvas.width, Math.random() * canvas.height);
// now add random number of way points
var timeP = time;
// info to create a random path
var dir = Math.random() * Math.PI * 2;
var x = p.x;
var y = p.y;
doFor(Math.floor(Math.random() * 80 + 12),()=>{
var dist = Math.random() * 5 + 4;
x += Math.cos(dir) * dist;
y += Math.sin(dir) * dist;
dir += (Math.random()-0.5)*0.3;
timeP += Math.random() * 1000 + 500;
p.path.addWayPoint(x,y,timeP);
});
// last waypoin at center of canvas.
p.path.addWayPoint(canvas.width / 2,canvas.height / 2,timeP + 5000);
p.path.start();
})
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update(time);
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
#Blindman67 is correct, clear and redraw everything, every frame.
I'm here just to say that when dealing with such primitive shapes as arc without too many color variations, it's actually better to use the arc method than drawImage().
The idea is to wrap all your shapes in a single path declaration, using
ctx.beginPath(); // start path declaration
for(i; i<shapes.length; i++){ // loop through our points
ctx.moveTo(pt.x + pt.radius, pt.y); // default is lineTo and we don't want it
// Note the '+ radius', arc starts at 3 o'clock
ctx.arc(pt.x, pt.y, pt.radius, 0, Math.PI*2);
}
ctx.fill(); // a single fill()
This is faster than drawImage, but the main caveat is that it works only for single-colored set of shapes.
I've made an complex plotting app, where I do draw a lot (20K+) of entities, with animated positions. So what I do, is to store two sets of points, one un-sorted (actually sorted by radius), and one
sorted by color. I then do use the sorted-by-color one in my animations loop, and when the animation is complete, I draw only the final frame with the sorted-by-radius (after I filtered the non visible entities). I achieve 60fps on most devices. When I tried with drawImage, I was stuck at about 10fps for 5K points.
Here is a modified version of Blindman67's good answer's snippet, using this single-path approach.
/* All credits to SO user Blindman67 */
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
const planes = {
items : [],
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
rad: 2,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.x += Math.cos(p.dir) * p.speed;
p.y += Math.sin(p.dir) * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
ctx.beginPath();
ctx.fillStyle = 'red';
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.moveTo(x + p.rad, y)
ctx.arc(x, y, p.rad, 0, Math.PI*2);
}
ctx.fill();
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(8000,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index: -1;
}
<canvas id=canvas></canvas>
8000 animated points
Not directly related but in case you've got part of your drawings that don't update at the same rate as the rest (e.g if you want to highlight an area of your map...) then you might also consider separating your drawings in different layers, on offscreen canvases. This way you'd have one canvas for the planes, that you'd clear every frame, and other canvas for other layers that you would update at different rate. But that's an other story.

How to get a tangent of a point in a golden spiral with HTML5 canvas

I've got a problem with some html5 canvas stuff...
With a lot of patience I've draw a golden spiral with a X number of dots on it (equally distributed).
This dots must become a little perpendicular lines.
Perpendicular to what?
In my mind, the line must be perpendicular to the tangent line of that point of a spiral.
So, that's the questions:
- How can I get the tangent line of a point in a golden spiral?
- Once I find the tangent, how can I draw a line perpendicular to it starting from my point xy?
The main problem is that, in canvas (as far I know), a line must have a start and end already known, while I have only the xystart
This is the jfiddle with the spiral code.
https://jsfiddle.net/MasterFO/bho3v6hs/2/
function spiral_render(){
var c = document.getElementById('c');
var context = c.getContext("2d");
var centerx = (context.canvas.width / 2)+5;
var centery = (context.canvas.height / 2)+100;
context.clearRect(0, 0, 582, 620);
context.moveTo(centerx, centery);
context.beginPath();
var dots_coordinates = [];
a = parseFloat(0.41);
b = parseFloat(0.23);
var dots = 20; //How many dots
rounds = parseFloat(180);
strength = parseFloat(30);
sample = strength * 360;
start = -rounds*(3.14);
end = rounds*(3.14);
step = (end - start) / sample;
var distance = parseInt((sample/dots)/2.6); //Distance between dots
k = 0;
//Draw the golden spiral
for (var i = 1; i <= sample; i++) {
r = start+i*step;
t = (1/b)*Math.log(r/a); //Phi angle of spiral
x = centerx + r* Math.cos(t);
y = centery + r* Math.sin(t);
if (i % distance == 0 && k <=dots && i > 5600) {
if(x && y){
dots_coordinates.push([x,y]);
k++;
}
}
context.lineTo(x, y);
}
context.lineWidth = 0.5;
context.strokeStyle = "#000";
context.stroke();
//Draw the dots in sequential mode
context.moveTo(centerx, centery);
var i = 0;
inter = setInterval(function() {
xp = Math.floor( dots_coordinates[i][0] );
yp = Math.floor( dots_coordinates[i][1] );
context.beginPath();
context.lineTo(xp, yp);
context.arc(xp, yp, 4, 0, 2 * Math.PI , false);
context.fillStyle = '#C4071A';
context.fill();
i++;
if (i == (dots_coordinates.length)) {
clearInterval(inter);
}
}, 50);
}
Someone have any idea?
This is the code to draw a perpendicular line at a dot of your choise. This code will not work on the first and last dot unfortunately. Credits to Spencer Wieczorek who suggested this method in his comment above.
In order to overcome only having a starting point is to use Math.sin() and Math.cos() multiplied with the length that you want your line to have.
var dotIndex = 11;
var lineLength = 50;
var myX = dots_coordinates[dotIndex-1][0]-dots_coordinates[dotIndex+1][0];
var myY = dots_coordinates[dotIndex-1][1]-dots_coordinates[dotIndex+1][1];
var theta = Math.atan2(-myY, myX);
context.beginPath();
context.moveTo(dots_coordinates[dotIndex][0],dots_coordinates[dotIndex][1]);
context.lineTo(dots_coordinates[dotIndex][0]+Math.sin(theta)*lineLength,dots_coordinates[dotIndex][1]+Math.cos(theta)*lineLength);
context.closePath();
context.stroke();

Position different size circles around a circular path with no gaps

I'm creating a Canvas animation, and have managed to position x number of circles in a circular path. Here's a simplified version of the code I'm using:
var total = circles.length,
i = 0,
radius = 500,
angle = 0,
step = (2*Math.PI) / total;
for( i; i < total; i++ ) {
var circle = circles[i].children[0],
cRadius = circle[i].r,
x = Math.round(winWidth/2 + radius * Math.cos( angle ) - cRadius),
y = Math.round(winHeight/2 + radius * Math.sin( angle ) - cRadius);
circle.x = x;
circle.y = y;
angle += step;
}
Which results in this:
What I am trying to achieve is for all the circles to be next to each other with no gap between them (except the first and last). The circles sizes (radius) are generated randomly and shouldn't adjust to fit the circular path:
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
I expect there to be a gap between the first and last circle.
I'm struggling to get my head around this so any help would be much appreciated :)
Cheers!
Main creation loop :
• take a current radius
• compute the angles it cover ( = atan2(smallRadius/bigRadius) )
• move base angle by this latest angle.
http://jsfiddle.net/dj2v7mbw/9/
function CircledCircle(x, y, radius, margin, subCircleCount, subRadiusMin, subRadiusMax) {
this.x = x;
this.y = y;
this.radius = radius;
this.subCircleCount = subCircleCount;
var circles = this.circles = [];
// build top sub-circles
var halfCount = Math.floor(subCircleCount / 2);
var totalAngle = addCircles(halfCount);
// re-center top circles
var correction = totalAngle / 2 + Math.PI / 2;
for (var i = 0; i < halfCount; i++) this.circles[i].angle -= correction;
// build bottom sub-circles
totalAngle = addCircles(subCircleCount - halfCount);
// re-center bottom circles
var correction = totalAngle / 2 - Math.PI / 2;
for (var i = halfCount; i < subCircleCount; i++) this.circles[i].angle -= correction;
// -- draw this C
this.draw = function (angleShift) {
for (var i = 0; i < this.circles.length; i++) drawDistantCircle(this.circles[i], angleShift);
}
//
function drawDistantCircle(c, angleShift) {
angleShift = angleShift || 0;
var thisX = x + radius * Math.cos(c.angle + angleShift);
var thisY = y + radius * Math.sin(c.angle + angleShift);
ctx.beginPath();
ctx.arc(thisX, thisY, c.r, 0, 2 * Math.PI);
ctx.fillStyle = 'hsl(' + (c.index * 15) + ',75%, 75%)';
ctx.fill();
ctx.stroke();
}
//
function addCircles(cnt) {
var currAngle = 0;
for (var i = 0; i < cnt; i++) {
var thisRadius = getRandomInt(subRadiusMin, subRadiusMax);
var thisAngle = Math.atan2(2 * thisRadius + margin, radius);
var thisCircle = new subCircle(thisRadius, currAngle + thisAngle / 2, i);
currAngle += thisAngle;
circles.push(thisCircle);
}
return currAngle;
}
}
with
function subCircle(radius, angle, index) {
this.r = radius;
this.angle = angle;
this.index = index;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
use with
var myCircles = new CircledCircle(winWidth / 2, winHeight / 2, 350, 2, 24, 5, 50);
myCircles.draw();
animate with :
var angleShift = 0;
function draw() {
requestAnimationFrame(draw);
ctx.clearRect(0, 0, winWidth, winHeight);
myCircles.draw(angleShift);
angleShift += 0.010;
}
draw();
It's something like this, but you're gonna have to figure out the last circle's size:
http://jsfiddle.net/rudiedirkx/ufvf62yf/2/
The main logic:
var firstStep = 0, rad = 0, step = 0;
firstStep = step = stepSize();
for ( var i=0; i<30; i++ ) {
draw.radCircle(rad, step);
rad += step;
step = stepSize();
rad += step;
}
stepSize() creates a random rad between Math.PI/48 and Math.PI/48 + Math.PI/36 (no special reason, but it looked good). You can fix that to be the right sizes.
draw.radCircle(rad, step) creates a circle at position rad of size step (also in rad).
step is added twice per iteration: once to step from current circle's center to its edge and once to find the next circle's center
firstStep is important because you have to know where to stop drawing (because the first circle crosses into negative angle)
I haven't figured out how to make the last circle the perfect size yet =)
There's also a bit of magic in draw.radCircle():
var r = rRad * Math.PI/3 * 200 * .95;
The 200 is obviously the big circle's radius
The 95% is because the circle's edge length is slightly longer than the (straight) radius of every circle
I have no idea why Math.PI/3 is that... I figured it had to be Math.PI/2, which is 1 rad, but that didn't work at all. 1/3 for some reason does..... Explain that!
If you want to animate these circle sizes and keep them aligned, you'll have a hard time. They're all random now. In an animation, only the very first iteration can be random, and the rest is a big mess of cache and math =)

Categories