Archive for January, 2009

Acceleration: slow down problem

Jan 20 2009 Published by Eneko Alonso under uncategorized

Moving objects in computing can be handled easily keeping a speed variable, let’s say in pixels per second (object.speed.x = 10). Then, on every step of the animation, we change the object position with this amount divided by the number of frames per second we are processing (object.x += object.speed.x / fps). Acceleration can be achieved easily to. On every step of the animation we can modify the speed with an acceleration factor (object.speed.x += object.acceleration.x).

Slow down problem

Today at work I’ve been trying to solve this problem: Giving an object moving at 50 px/s I want it to slow down to 0 px/s in exactly 400px. How can we calculate the acceleration for this? I need an amount to subtract to the speed on every step, so by the time the object has moved 400px the speed is 0.

There are a few equations of motion that could be applied. In this case, we don’t care about how much time does it take to stop the object. So the formula is:

a=\frac{v^2 - v_0^2}{2*s}

a=\frac{-50^2}{2*400}=-3.13

In Javascript:

  1. this.acceleration = (Math.pow(this.finalSpeed,2) – Math.pow(this.speed,2)) / (2*this.brakingSpace);

Well, still it doesn’t work perfectly on my demo. The object, a 10px square div, decelerates properly, but stops at 375px instead of 400px. I’ll keep working on it.

The code

  1. var Brake = new Class({
  2.   Extends: Thread,
  3.  
  4.   initialize: function(options) {
  5.     this.parent(options);
  6.     this.item = $('item');
  7.     this.speed = 50;
  8.     this.finalSpeed = 0;
  9.     this.brakingSpace = 400;
  10.     this.currentPos = 0;
  11.  
  12.     this.acceleration = (Math.pow(this.finalSpeed,2) – Math.pow(this.speed,2)) / (2*this.brakingSpace);
  13.     $('acceleration').set('html', 'Acceleration: ' + this.acceleration.toFixed(2) + ' px/s<sup>2</sup>');
  14.   },
  15.  
  16.   execute: function() {
  17.     if (this.speed < 0) {
  18.       this.stop();
  19.       return;
  20.     }
  21.     this.item.setStyles({left: this.currentPos});
  22.     $('position').set('text', 'Position: ' + this.currentPos.toFixed(2) + ' px');
  23.     $('speed').set('text', 'Speed: ' + this.speed.toFixed(2) + ' px/s');
  24.  
  25.     this.speed      += this.acceleration;
  26.     this.currentPos += this.speed;
  27.   }
  28.  
  29. });

If I modify the position before adjusting the speed, the object goes farther than 400px. If I update the speed first (like above), the object stops before reaching 400px.

Problem solved

Turns out I wasn’t calculating the new position properly. The formula to calculate the new position is:

s=v - \frac{1}{2}a

Or in Javascript:

  1. this.currentPos += this.speed0.5*this.acceleration;

Here is the modified code:

  1. execute: function() {
  2.   this.speed += this.acceleration;
  3.   this.currentPos += this.speed0.5*this.acceleration;
  4.  
  5.   if (this.speed < 0) {
  6.     this.stop();
  7.     return;
  8.   }
  9.   this.item.setStyles({left: this.currentPos});
  10.   $('position').set('text', 'Position: ' + this.currentPos.toFixed(2) + ' px');
  11.   $('speed').set('text', 'Speed: ' + this.speed.toFixed(2) + ' px/s');
  12. }

Of course, now the demo is accurate :) Thanks Jacob.

2 responses so far

Spinning Color Wheel: Mootools + Raphael + SVG

Jan 18 2009 Published by Eneko Alonso under uncategorized

A couple of days ago I was playing at work with the RaphaelJS library to do some image rotation that we may end up using on a new project we are working on. At the end I ended up creating a spinning object with variable speed, similar to this demo I show you here.

Using RaphaelJS it is very easy to embed any image into an SVG object and rotate it. With a little bit of animation, we can set angular speeds and have some fun :)

Spinning color wheel

I have added an acceleration parameter which will make the speed changes very smooth, although it the new speed is very different than the current one, it will take some time to reach it.

See it in action: http://enekoalonso.com/research/color-wheel.php

The code

  1. var ColorWheel = new Class({
  2.   Extends: Thread,
  3.  
  4.   initialize: function(options) {
  5.     this.parent(options);
  6.     this.raphael = Raphael('wheel', 400, 400);
  7.     this.wheel = this.raphael.image('media/svg/colorwheel.svg.png', 0,0,400, 400);
  8.     this.lblSpeed = $('speed');
  9.     this.lblRPM = $('rpm');
  10.     this.lblFPS = $('fps');
  11.    
  12.     $('btnSetSpeed').addEvent('click', function(event) {
  13.       this.setSpeed($('targetSpeed').get('value'));
  14.     }.bind(this));
  15.   },
  16.  
  17.   execute: function() {
  18.     var now = new Date().getTime();
  19.     this.fps = (1000 / (now – this.lastTime)).toFixed(2);
  20.     this.lastTime = now;
  21.  
  22.     if (this.targetSpeed == 0) { // stop
  23.       this.angularSpeed *= 0.99;
  24.       if (this.angularSpeed < 0.1) this.stop();
  25.     }
  26.  
  27.     else if (this.angularSpeed < this.targetSpeed) { // accelerate
  28.       this.angularSpeed = Math.min(this.angularSpeed + this.acceleration, this.targetSpeed);
  29.     }
  30.  
  31.     else if (this.angularSpeed > this.targetSpeed) { // brake
  32.       this.angularSpeed = Math.max(this.angularSpeedthis.acceleration, this.targetSpeed);
  33.     }
  34.  
  35.     var currentRPM = 60*(this.angularSpeed/360)*(1000/|>this.options.interval);
  36.     this.lblSpeed.set('text', 'Angular speed: ' + this.angularSpeed.toFixed(2) + ' deg per frame');
  37.     this.lblRPM.set('text', 'Current speed: ' + currentRPM.toFixed(2) + ' rpm');
  38.     this.lblFPS.set('text', 'Frames: ' + this.fps + ' fps');
  39.  
  40.     this.wheel.rotate(this.angularSpeed);
  41.   },
  42.  
  43.   start: function() {
  44.     this.angularSpeed = 0;
  45.     this.setSpeed($('targetSpeed').get('value'));
  46.     this.acceleration = 0.1;
  47.     this.lastTime = new Date().getTime();
  48.     this.parent();
  49.   },
  50.  
  51.   setSpeed: function(rpm) {
  52.     if (this.stopped) this.start();
  53.     this.targetSpeed = (rpm/60)*(360)*(this.options.interval/|>1000);
  54.     console.log('setSpeed (rpm,anglespeed): ', rpm, this.targetSpeed);
  55.   }
  56. });
  57.  
  58. window.addEvent('domready', function() {
  59.   new ColorWheel({interval:1000/30}).start(); // 30 fps
  60. });

Enjoy!

PS: I took the color wheel from Wikipedia, although I couldn’t figure out how to embed an SVG graphic into a Raphael SVG container.

4 responses so far

Moved research to enekoalonso.com

Jan 11 2009 Published by Eneko Alonso under uncategorized

You shouldn’t notice it, but I have moved all my research demos from dev.enekoalonso.com/research to enekoalonso.com/research. It is just a little change, but will help keeping things more organized, I think.

To avoid breaking old links, I have added a 301 redirect:

  1. Options +FollowSymLinks
  2. RewriteEngine on
  3.  
  4. # For http://dev.enekoalonso.com/research
  5. RewriteCond %{REQUEST_URI} ^/research$
  6. RewriteRule .* http://enekoalonso.com/research [R=301,L]
  7.  
  8. # For http://dev.enekoalonso.com/research/*
  9. RewriteRule (.*) http://enekoalonso.com/research/$1 [R=301,L]

Let me know if you find any issues.

No responses yet

Repulsive force in Javascript with Mootools draggables

Jan 04 2009 Published by Eneko Alonso under uncategorized

A couple of days ago I was reading about the Boids demo made by Ben from Coderholic. I though that was very interesting, specially since I hadn’t heard about boids before.

I wanted to create my own version but instead I decided to play only with the repulsive force, making it interactive using Mootools drag & drop functionality. When the demo loads, 20 elements will be created and they will start moving on their own, trying to keep the minimum distance with any other element. This distance is configurable, as it is the option to keep the element speed or to make them slow down until they stop. At any time, the user can move elements on the screen dragging them with the mouse.

Repulsive Force Demo

See the demo in action.

Source code

  1. var Item = new Class({
  2.   Implements: Options,
  3.  
  4.   options: {
  5.     radius: 10,
  6.     position: { x: window.clientWidth/2, y: window.clientHeight/|>2 },
  7.     speed: { x: 0, y: 0 },
  8.     color: '#333'
  9.   },
  10.  
  11.   initialize: function(options) {
  12.     this.setOptions(options);
  13.     this.addToDOM();
  14.    
  15.     var self = this;
  16.     this.dragController = new Drag(this.element, {
  17.       onStart: function() {
  18.         self.dragging = true;
  19.       },
  20.       onDrag: function() {
  21.         self.options.position = self.element.getPosition();
  22.       },
  23.       onComplete: function() {
  24.         self.options.position = self.element.getPosition();
  25.         self.speed = {x:0,y:0};
  26.         self.dragging = false;
  27.       }
  28.     });
  29.     this.dragging = false;
  30.   },
  31.  
  32.   destroy: function() {
  33.     this.element.destroy();
  34.     delete this;
  35.   },
  36.  
  37.   addToDOM: function() {
  38.     this.element = new Element('div', { 'class': 'item' });
  39.     this.element.setStyles({
  40.       background: this.options.color,
  41.       height: 2*this.options.radius,
  42.       width:  2*this.options.radius,
  43.       left: this.options.position.x,
  44.       top:  this.options.position.y,
  45.       '-moz-border-radius': Math.round(this.options.radius) + 'px',
  46.       '-webkit-border-radius': Math.round(this.options.radius) + 'px',
  47.       'border-radius': Math.round(this.options.radius) + 'px'
  48.     }).inject(document.body);
  49.   },
  50.  
  51.   moveTo: function(x, y) {
  52.     if (this.dragging) return;
  53.  
  54.     this.options.position.x = x;
  55.     this.options.position.y = y;
  56.  
  57.     // Check bounds
  58.     if (this.options.position.x < 0) {
  59.       this.options.position.x = 0;
  60.       this.options.speed.x = 0;
  61.     }
  62.     if (this.options.position.x > window.getWidth()2*this.options.radius) {
  63.       this.options.position.x = window.getWidth()2*this.options.radius;
  64.       this.options.speed.x = 0;
  65.     }
  66.     if (this.options.position.y < $('header').getHeight()) {
  67.       this.options.position.y = $('header').getHeight();
  68.       this.options.speed.y = 0;
  69.     }
  70.     if (this.options.position.y > window.getHeight()2*this.options.radius) {
  71.       this.options.position.y = window.getHeight()2*this.options.radius;
  72.       this.options.speed.y = 0;
  73.     }
  74.  
  75.     // Slow down
  76.     if ($('chk-slowdown').get('checked')) {
  77.       this.options.speed.x *= 0.7;
  78.       this.options.speed.y *= 0.7;
  79.     }
  80.    
  81.     if (this.options.speed.x < 0.001) this.options.speed.x = 0;
  82.     if (this.options.speed.y < 0.001) this.options.speed.y = 0;
  83.   },
  84.  
  85.   draw: function() {
  86.     if (this.dragging) return;
  87.     this.element.setStyles({
  88.       left: Math.round(this.options.position.x),
  89.       top:  Math.round(this.options.position.y)
  90.     });
  91.   }
  92. })
  93.  
  94. var ItemController = new Class({
  95.   Extends: Thread,
  96.   items: [],
  97.  
  98.   initialize: function(options) {
  99.     this.parent(options);
  100.   },
  101.  
  102.   addItem: function(options) {
  103.     this.items.push(new Item(options));
  104.   },
  105.  
  106.   execute: function(){
  107.     this.compute();
  108.     this.draw();
  109.   },
  110.  
  111.   compute: function() {
  112.     var self = this;
  113.     self.items.each(function(item1, index1) {
  114.       self.items.each(function(item2, index2) {
  115.         if (index1 != index2) {
  116.           var distance = {}
  117.           distance.x = (item2.options.position.x + item2.options.radius) -
  118.                  (item1.options.position.x + item1.options.radius);
  119.           distance.y = (item2.options.position.y + item2.options.radius) -
  120.                   (item1.options.position.y + item1.options.radius);
  121.  
  122.           distance.total = Math.sqrt(Math.pow(distance.x, 2) + Math.pow(distance.y, 2));
  123.           if (distance.total < $('distance').get('value')) {
  124.        
  125.             var angle = Math.atan2(distance.y, distance.x);
  126.             var speed = 1000 / distance.total;
  127.  
  128.             item1.options.speed.x += -speed * Math.cos(angle);
  129.             item1.options.speed.y += -speed * Math.sin(angle);
  130.           }
  131.         }
  132.       });
  133.      
  134.       if (item1.options.speed.x || item1.options.speed.y) {
  135.         item1.moveTo(item1.options.position.x + item1.options.speed.x * (self.options.interval/1000),
  136.                item1.options.position.y + item1.options.speed.y * (self.options.interval/1000));
  137.       }
  138.     });
  139.   },
  140.  
  141.   draw: function() {
  142.     this.items.each(function(item) { item.draw(); });
  143.   },
  144.  
  145.   addRandom: function() {
  146.     this.addItem({
  147.       position: { x: Random.get(100,500), y: Random.get(100,400) },
  148.       color: 'rgb('+Random.get(0,255)+','+Random.get(0,255)+','+Random.get(0,255)+')'
  149.     });
  150.   }
  151. });
  152.  
  153. window.addEvent('domready', function() {
  154.   window.system = new ItemController({autostart: true, interval: 50});
  155.   for (var i=0;i<20;i++)
  156.     system.addRandom();
  157.  
  158.   $('btnAddRandom').addEvent('click', function(){
  159.     system.addRandom();
  160.   });
  161. });

The code is very similar to the Gravity demo, although in this case the attraction becomes repulsion.

3 responses so far

Check your server status with email notification

Jan 03 2009 Published by Eneko Alonso under uncategorized

I wanted to do this for a while and never really sit down and code it. Yesterday I came across this pretty nice script by Ben from Coderholic which does exactly that: check your server status and send you an email if it is other than 200 (OK).

  1. #!/bin/bash  
  2.  
  3. # Query a supplied URL and output an error message if  
  4. # the status code was not 200. Can be used as a cron  
  5. # task to check if a server is up or not.  
  6.  
  7. # Ben Dowling – http://www.coderholic.com  
  8.  
  9. # The URL to query  
  10. if [ $# -ne 1 ]  
  11. then  
  12.     echo "Usage: $0 <URL>";  
  13.     exit;  
  14. fi  
  15.  
  16. url=$1;  
  17. response=$(curl -s -I -L $url | grep HTTP);  
  18.  
  19. status=${response#* }; # Strip off characters up to the first space  
  20. status=${status:0:3}; # Just use the 3 digit status code  
  21.  
  22. if [ "$status" != "200" ]  
  23. then  
  24.     echo "Error fetching $url. Status code '$status'";  
  25. fi

Nice.

No responses yet

Solving the 8 Queen Puzzle in Javascript

Jan 02 2009 Published by Eneko Alonso under uncategorized

Eight Queens is a classic problem which consist on being able to place 8 queens on a chess board without leaving them in position of eating each other. More info on Wikipedia.

8 Queens
See the 8 Queens demo in action.

On this demo I have created 3 different Mootools classes: ChessBoard, Piece and EightQueen. ChessBoard and Piece are generic classes which I plan to reuse in other demos. EightQueen is the brain of the demo and it inherits from Thread, the class I use for javascript animations. Check the source code at the end of the post.

Solving the problem

The easiest way to solve this problem is using backtracking. Once we find we have 2 queens on eating position, we can cancel that path. Since there can be only one Queen per column, we create an array and store the row on each position. Once the array has 8 items (columns) the problem will be solved.

If we can’t find a valid position for the current column, we need to remove the previous column Queen and add it again in the next row. If there are no more rows, we remove the previous column Queen and so on.

Testing if a position is valid: my solution

So the process to resolve the 8 Queen puzzle is easy after all. But we still need to figure out how to check if a position for a Queen is valid or not. I didn’t want to copy this logic from any other demo or site (I bet there are hundreds of ways to do this). I wanted to think my own way, and this is what I came up:

  1. validPosition: function(position) {
  2.   if (this.queens.indexOf(position.y) != -1) return false;
  3.   for (var index=1, length=this.queens.length; index<=length; index++) {
  4.     var queenY = this.queens[index-1];
  5.     if (position.y + (position.x-index) == queenY) return false;
  6.     if (position.y(position.x-index) == queenY) return false;
  7.   };
  8.   return true;
  9. }

First, we know only one Queen can sit on a column, but also only one Queen can sit on a row. So we check if there is already a Queen in the same row (position.y). If there is one, the position is not valid.

Then, we need to check the diagonals. Queens can move on 45° diagonals. This is, the number of squares the Queen moves is the same in both horizontal and vertical directions. So a position will be invalid if there is a Queen one column away and one row away, or two columns away and two rows away, etc. With a simple equation we can check this. We need to do it twice, since it can be one row before or after. We don’t need to check the columns on the right because we know there are no Queens placed on that side yet (on this demo I am placing Queens from left to right).

Source code

  1. var ChessBoard = new Class({
  2.   pieces: [],
  3.  
  4.   initialize: function(container) {
  5.     this.container = container;
  6.     this.element = new Element('div', {'class': 'board'});
  7.     for (var row=8; row>0; row–) {
  8.       for (var col=1; col<9; col++) {
  9.         var square = new Element('div', {'class': 'square'});
  10.         square.id = "square" + String.fromCharCode(96+col) + row;
  11.         square.addClass(this.isDarkSquare(row,col)?'dark':'light');
  12.         this.element.grab(square);
  13.       }
  14.     }
  15.     this.container.grab(this.element);
  16.   },
  17.  
  18.   isDarkSquare: function(row,col) {
  19.     return ((row%2==0 && col%2==0) || (row%2==1 && col%2==1));
  20.   },
  21.  
  22.   addPiece: function(options) {
  23.     var piece = new Piece(options);
  24.     this.pieces.push(piece);
  25.     this.element.grab(piece.element);
  26.   },
  27.  
  28.   removePiece: function(position) {
  29.     var piece;
  30.     $A(this.pieces).each(function(item) {
  31.       if (item.square.x == position.x && item.square.y == position.y)
  32.         piece = item;
  33.     });
  34.     $A(this.pieces).erase(piece);
  35.     piece.destroy();
  36.   },
  37.  
  38.   removeAll: function() {
  39.     while (this.pieces.length > 0) {
  40.       var piece = this.pieces[this.pieces.length-1];
  41.       piece.destroy();
  42.       this.pieces.length–;
  43.     }
  44.   }
  45. });
  46.  
  47.  
  48. var Piece = new Class({
  49.   Extends: Sprite,
  50.   square: {col:1, row:1},
  51.  
  52.   initialize: function(options) {
  53.     this.options.size = {x:32,y:32};
  54.     this.parent(options);
  55.     this.setFigurine();
  56.     this.moveTo(this.options.square);
  57.   },
  58.  
  59.   setFigurine: function() {
  60.     this.setImage('media/images/icons/chess/'+
  61.             this.options.color + '_' +
  62.             this.options.kind + '_32.png');
  63.   },
  64.  
  65.   moveTo: function(position) {
  66.     if (typeof position == 'string') {
  67.       var parts = position.split("");
  68.       parts[0] = parts[0].charCodeAt(0)96;
  69.       this.square.x = parts[1];
  70.       this.square.y = parts[0];
  71.     }
  72.     else if (position instanceof Object) {
  73.       this.square = position;
  74.     }
  75.  
  76.     this.options.position.x = ((this.square.x-1) * 38) + 3;
  77.     this.options.position.y = ((8-this.square.y) * 38) + 3;
  78.     this.animate(this);
  79.   }
  80. });
  81.  
  82.  
  83. var EightQueens = new Class({
  84.   Extends: Thread,
  85.   queens: [],
  86.  
  87.   initialize: function(options) {
  88.     this.parent(options);
  89.     this.board = new ChessBoard($('content'));
  90.     this.currentRow = Random.get(1,8);
  91.     this.logbox = new Element('div', {'class':'log'}).inject($('content'));
  92.  
  93.     this.chkLog = $('chk-log');
  94.  
  95.     $('btn-restart').addEvent('click', function(){
  96.       this.restart();
  97.     }.bind(this));
  98.   },
  99.  
  100.   execute: function() {
  101.     this.addQueen();
  102.     if (this.queens.length == 8) {
  103.       this.stop();
  104.       this.log('Done.');
  105.     }
  106.   },
  107.  
  108.   addQueen: function() {
  109.     var queenAdded = false;
  110.    
  111.     for (var row=this.currentRow; row<=8; row++) {
  112.       var position = {x:this.queens.length+1, y:row};
  113.  
  114.       if (this.validPosition(position)) {
  115.         this.addToBoard(position);
  116.         queenAdded = true;
  117.         this.currentRow = 1;
  118.         break;
  119.       }
  120.     }
  121.    
  122.     if (!queenAdded) {
  123.       this.currentRow = this.queens[this.queens.length-1] + 1;
  124.       this.board.removePiece({x:this.queens.length,y:this.queens[this.queens.length-1]});
  125.       this.queens.length -= 1;
  126.       if (this.queens.length == 0) this.currentRow = Random.get(1,8);
  127.     }
  128.   },
  129.  
  130.   validPosition: function(position) {
  131.     this.log('Testing position: ' + position.x + ',' + position.y);
  132.     if (this.queens.indexOf(position.y) != -1) return false;
  133.     for (var index=1, l=this.queens.length; index<=l; index++) {
  134.       var queenY = this.queens[index-1];
  135.       if (position.y + (position.x-index) == queenY) return false;
  136.       if (position.y(position.x-index) == queenY) return false;
  137.     };
  138.     return true;
  139.   },
  140.  
  141.   addToBoard: function(position) {
  142.     this.log('Queen added: ' + position.x + ',' + position.y +' – '+ this.queens.toString());
  143.     this.queens.push(position.y);
  144.     this.board.addPiece({kind:'Q', square:position, color:'Yellow'});
  145.   },
  146.  
  147.   log: function(text) {
  148.     if (!this.chkLog.get('checked')) return;
  149.     var p = new Element('p', {'text': text});
  150.     this.logbox.grab(p);
  151.     this.logbox.scrollTop = p.getPosition(this.logbox).y296;
  152.   },
  153.  
  154.   restart: function() {
  155.     this.stop();
  156.     this.queens.length = 0;
  157.     this.board.removeAll();
  158.     this.currentRow = Random.get(1,8);
  159.     this.options.interval = $('field-interval').get('value');
  160.     this.logbox.empty();
  161.     this.start();
  162.   }
  163.  
  164. });
  165.  
  166. window.addEvent('domready', function() {
  167.   new EightQueens({interval: $('field-interval').get('value'), autostart: true});
  168. });

Enjoy!

One response so far