Optics simulation in Flash - lens formula


I just created a flash simulation of what happens when you move an object in front of a convergent lens. When the object is moved back and forth its image will react to the movement. It all had to happen with Actionscript 2 too. Check the result here.

Depending on whether the object is more than two focal distances away, between two and one focal distance and closer than one focal distance away from the lens we will get three distinct behaviors in its image.

I’m quite happy with the result which is using the lens formula (and the derived magnification formula). The only “cheating” this time is the fact that I’m preventing infinite values in one place, the drawing API went haywire without these changes.

Let’s take a look at the basic classes:

class Lens{
	
	var f:Number;
	var fLeft:Number;
	var fRight:Number;
	var fLeft2:Number;
	var fRight2:Number;
	var xc:Number;
	var yc:Number;
	var clip:MovieClip;
	
	function Lens(f:Number, clip:MovieClip){
		this.f 			= f;
		this.clip 		= clip;
		this.xc 		= this.clip._x;
		this.yc 		= this.clip._y;
		this.fLeft		= this.xc - this.f;
		this.fRight 	= this.xc + this.f;
		this.fLeft2		= this.fLeft - this.f;
		this.fRight2 	= this.fRight + this.f;
	}
	
	function cXpos(clip:MovieClip){
		return clip._x + (clip._width / 2);
	}
	
	function cYpos(clip:MovieClip){
		return clip._y + (clip._height / 2);
	}
}

Here we set some convenience values, f is of course the focal length, clip is the movieclip representing the lens. The xc and yc values are already legacy stuff, at first I needed them because I wanted to work with x and y position of the clip in the default top-left corner, this proved to be impractical though so I moved the graphics which are now positioned to accomplish having the x and y coordinates in the middle of the clip. The other variables above mark the position of the focal lengths on the x-axis to the left and right, same goes for 2x the focal length.

class ConvergingLens extends Lens{
	
	function ConvergingLens(f:Number, clip:MovieClip){
		super(f, clip);
	}
	
	function calcImgPos(realObj:MovieClip){
		var S1:Number = this.xc - realObj._x;
		return this.xc + this.calcS2(S1);
	}
	
	function calcS2(S1:Number){
		var S2:Number = 1 / ( (1 / this.f) - (1 / S1) ) ;
		return Math.abs(S2) > 2000 ? (S2 / Math.abs(S2)) * 2000 : S2;
	}
	
	function calcMag(realObj:MovieClip, imgObj:MovieClip){
		var S1:Number = this.xc - realObj._x;
		var S2:Number = this.calcS2(S1);
		return -S2 / S1;
	}
	
	function setFleft(fleft:MovieClip, varName:String){
		fleft._x = this[varName];
		this.setY(fleft);
	}
	
	function setFright(fright:MovieClip, varName:String){
		fright._x = this[varName];
		this.setY(fright);
	}
	
	function setY(fclip:MovieClip){
		fclip._y = this.yc;
	}
}

Here is where all the heavy stuff is, making use of the lens formula and the magnification formula. Note the line making use of Math.abs() above, that’s the cheat I referred to earlier, we cap the S2 value to 2000 if it manages to go above that value.

The setFright and setFleft methods are used to position the markers on the x-axis in the correct positions, this makes stuff more dynamic. If new requirements pop up it’s easy to manage if you don’t have to reposition a lot of stuff manually.

Let’s take a look at how we use these classes in the main time line:

var lens:ConvergingLens = new ConvergingLens(150, lensClip);

lens.setFleft(fleft, 'fLeft');
lens.setFleft(fleft2, 'fLeft2');
lens.setFright(fright, 'fRight');
lens.setFright(fright2, 'fRight2');

var candleY = candle._y;

candle._x = lens.fLeft2;

this.createEmptyMovieClip("pen", 1);

candle.onPress = function(){
	this.startDrag();
}

candle.onRelease = function(){
	this.stopDrag();
}

this.onEnterFrame = function(){
	candle._y = candleY; // 1
	if(candle._x > 365) // 2
		candle._x = 365;
	
	if(candle._x < fleft2._x){ // 3
		txt1.text = "Inverted, reduced, real.";
		txt2.text = "This could be used in a camera, big object on small film.";
	}else if(candle._x < fleft._x){
		txt1.text = "Inverted, enlarged, real.";
		txt2.text = "This could be used as a projector, small slide on big screen.";
	}else{
		txt1.text = "Upright, enlarged, virtual.";
		txt2.text = "This is a magnifying glass.";
	}
	
	candle_img._x = lens.calcImgPos(candle); // 4
	var M = lens.calcMag(candle, candle_img) * 50; // 5
	candle_img._xscale = M;
	candle_img._yscale = M;
	
	pen.clear(); // 6
	pen.lineStyle(1, 0xFF0000, 100);
	pen.moveTo(candle._x, candle._y);
	pen.lineTo(lens.xc, candle._y);
	
	if(candle_img._x > lens.xc){ // 7
		candle_img._y = lens.yc + candle_img._height;
		
		pen.lineTo(candle_img._x, candle_img._y);
		pen.lineTo(candle._x, candle._y);
		pen.lineTo(lens.xc, candle_img._y);
		pen.lineTo(candle_img._x, candle_img._y);
	}else{
		candle_img._y = lens.yc - candle_img._height;
		var ciy = candle_img._y;
		candle_img._alpha = 90;
		var y3 = HSMath.getY3(new V2D(lens.xc, candle._y), V2D.fromMc(fright), 1000); // 8
		pen.lineTo(1000, y3);
		var y3 = HSMath.getY3(V2D.fromMc(candle), new V2D(lens.xc, lens.yc), 1000);
		pen.moveTo(candle._x, candle._y);
		pen.lineTo(1000, y3);
		pen.moveTo(candle_img._x, lens.yc);
		pen.lineTo(lens.xc, ciy);
		pen.lineTo(1000, ciy);
		
		pen.lineStyle(1, 0xBBBBBB, 100);
		pen.moveTo(candle_img._x, ciy);
		pen.lineTo(lens.xc, ciy);
		pen.moveTo(candle_img._x, ciy);
		pen.lineTo(lens.xc, candle._y);
		pen.moveTo(candle_img._x, ciy);
		pen.lineTo(candle._x, candle._y);
		
	}
}

We begin with creating the lens object, setting a focal length of 150 pixels, then we position the markers for focal lengths and double focal lengths on both sides of the lens.

Next we position the real candle object at the double focal marker to the left of the lens. We also take note of the start y position of the candle, we will later use this value to prevent movement of the candle on the y-axis.

We initiate the dragging and then it’s time to start the simulation:
1.) We prevent movement on the y-axis of the real candle object.
2.) We limit movement on the x-axis of the real candle object to the left side of the lens.
3.) We determine which comments we are to write in some dynamic text fields based on where the real candle is.
4.) We calculate the position on the x-axis of the candle image.
5.) We determine the magnification to use, note the multiplication of 50 here, that is to account for the fact that the candle clip was scaled before we started the simulation, if you can refrain from scaling stuff clips, ie get them to be the right size before you start you don’t need modifiers like this.
6.) We start drawing the lines, starting with clearing the lines drawn during the prior frame.
7.) As you can see we draw different lines depending on whether the candle image is on the right side (real, inverted) of the lens or left side (virtual, upright).
8.) Here we calculate where to draw lines based on line slope, using V2D and HSMath:

class V2D{
	
	public var x:Number;
	public var y:Number;
	
	function V2D(x:Number, y:Number){
		this.x = x;
		this.y = y;
	}
	
	static function fromMc(mc){
		return new V2D(mc._x, mc._y);
	}
	
	function set(x, y){
		this.x = x;
		this.y = y;
	}
	
	function setVpos(v:V2D){
		this.x = v.x;
		this.y = v.y;
	}
	
	function divideScal(num:Number){
		this.x /= num;
		this.y /= num;
	}
	
	function multi(num:Number){
		this.x *= num;
		this.y *= num;
	}
	
	function multiV(v:V2D){
		this.x *= v.x;
		this.y *= v.y;
	}
	
	function inc(p:V2D){
		this.x += p.x;
		this.y += p.y;
	}
	
	function incScal(num:Number){
		this.x += num;
		this.y += num;
	}
	
	function incScals(x:Number, y:Number){
		this.x += x;
		this.y += y;
	}
	
	function moveMovie(m:MovieClip){
		m._x = this.x;
		m._y = this.y;
	}
	
	function movieSet(m:MovieClip){
		this.x = m._x;
		this.y = m._y;
	}
	
	function getAngle():Number{
		return Math.atan(this.y/this.x);
	}
}

This time basically the only function we use here is the fromMc method.

And HSMath:

class HSMath {
	
	static function getK(mc1:V2D, mc2:V2D){
		return (mc1.y - mc2.y) / (mc1.x - mc2.x);
	}
	
	static function getY3(mc1:V2D, mc2:V2D, x3:Number){
		var k = HSMath.getK(mc1, mc2);
		return mc1.y + (k * (x3 - mc1.x));
	}
}

GetK() is used to get the slope, and getY3() to get the y-coordinate of a third point we want to draw to. We need them to draw the infinite lines going out of the picture when we have a virtual candle image (ie when the real candle is positioned closer to the lens than the focal distance), see #8 above.

Related Posts

Tags: , , , , ,