Skip to main content
Search IntMath
Close

Home | Math art in code | Animated Lissajous figures

Page by Murray Bourne, IntMath.com. Last updated: 05 Dec 2019

Animated Lissajous figures

As a child, I spent many hours drawing curves with my Spirograph:

Spirograph
The Spirograph allows you to draw complex spirals [Image source]

By moving the smaller disks within a larger cogged circle, you were effectively combining the ratios between the two circles, and the distance from the center of the smaller circle, resulting in quite complex shapes.

The following interactive graph involving Lissajous curves works on a similar principle. We combine two parametric trigonometric curves, one given by the x-expression, and the other by the y-expression. The animations show how the curve grows, as the angle t increases over time.

General form of Lissajous curves

For the following interactive graph, we are using the following general form of Lissajous figures, a pair of parametric tigonometric equations:

`x = cos(t/a + delta)`

`y = sin(t/b)`

The value δ in the expression for x produces a phase shift, which you can vary below using a slider.

Things to do

You can:

  1. Try some values of a or b (using the sliders below the graph) and see the effect on the resulting curve.
  2. Change the phase shift (using the "ps" slider) for the given values of a and b to explore what happens.
  3. Select Show circles (using the check box) to see how Lissajous curves are the result of combining two signals.

Show circles

a =
b =
ps =

Resulting parametric equations, with phase shift:

Copyright © www.intmath.com Frame rate: 0

See also...

See the section on Lissajous Figures in the chapter on trigonometric graphs.

The complete javascript


try {
	var reducer = 1;

	function createBrd() { // Create board
		removeEle("asvg0SVG");
		boardID = "asvg0";
		if(brdType === 0) {
			xMin=-1; xMax=1; yMin=-1;  yMax=1;
		} else {
			xMin=-3; xMax=1; yMin=-3;  yMax=1;
		}
		padding = 3;
		boardWidthToHeight = 1;
		initBoard(boardID, xMin, xMax, yMin);
		doGrids = 0;
		//axes(2,2,"labels",2,2); // for testing
		stroke = corpColor;

		if(brdType === 1) {
			strokewidth = 1;
			strokeopacity = 0.4;
			circle([-2,0],reducer,"circ0");
			circle([0,-2],reducer,"circ1");

			ASdot([-2+reducer,0], 2, corpColor, corpColor, "ptOnCirc0");
			ASdot([reducer,-2], 2, corpColor, corpColor, "ptOnCirc1");

			segment([-2-reducer,0], [reducer,0], "seg0");
			segment([reducer,-2-reducer], [reducer,reducer], "seg1");
		}
		strokewidth = 2;
		strokeopacity = 0.4;
	}

	brdType = 0;
	createBrd();

	// Fractions cancelling
	function reduce(n, d) {
	  // Based on: http://tiny.cc/facCancel
	  var gcd = function(a, b) {
		return b ? gcd(b, a % b) : a
	  }
	  gcd = gcd(n, d)
	  N = n/gcd;
	  D = d/gcd;
	  return [N, D]
	}

	// Variables
	var p, n0, d0, N, D, reduceArr, inc, anim0, t, domFactor, raf0;
	var j=0, del=0, rot = false;
	var completed = false;
	var frameTime0 = 100000,
		lastLoop0 = new Date(),
		thisLoop0;
	var filterStrength = 20;
	var ox = brdPropsArr[brdID]["ox"], oy = brdPropsArr[brdID]["oy"];

	// Initial values
	function init() {
		window.cancelAnimationFrame(raf0);
		for(k=0;k<j+1;k++){
			removeEle("greenCurve"+k);
		}
		removeEle("ptOnCurv");
		removeEle("greenCurve");
		p = [[reducer*cos(del),0]];
		n0 = 1*slider0.noUiSlider.get();
		d0 = 1*slider1.noUiSlider.get();

		// Remove strange negative on 0.00
		if( Math.sign(1*slider2.noUiSlider.get() ) === -0) {
			gebi( "slider2" ).getElementsByClassName("noUi-tooltip")[0].innerHTML = "0.00";
		}

		if(del < 0) {
			plusSgn = "";
		} else {
			plusSgn = "+";
		}
		plusSgn = ( del < 0 ? "" : "+" );
		gebi("theFn").innerHTML = "`x = cos(t/"+n0+ plusSgn +del.toFixed(2)+"),~ ~ ~ y = sin(t/"+d0+")`";
		AMfunc(true);

		reduceArr = reduce(n0,d0);
		N = reduceArr[0];
		D = reduceArr[1];
		//inc = (N + D)/100;
		inc = (n0 + d0)/100;
		anim0 = true;
		j=0;
		t=0;
		minn0d0 = Math.min(n0,d0);
		domFactor = ( (N == n0 && D == d0) ? 1 : minn0d0);
	}

	function doAnim() {
		if( t > 2*N*D*pi * domFactor ) {
			t=0;
			curveLength=0;
			curveLength1=0;
			anim0 = false;
			ptOnCurv.setAttribute("cx", origin[0] + xunitlength*reducer*cos(del));
			ptOnCurv.setAttribute("cy", (boardHeight - origin[1]) );
			if(brdType === 1 ) {
				ptOnCirc1.setAttribute( "transform", "rotate("+ -del*180/pi+", " + (origin[0]).toFixed(1)  + ","+(boardHeight - (origin[1] - 2*yunitlength)).toFixed(1)+")");
				seg1.setAttribute("transform", "translate("+ -xunitlength*reducer*(1-cos(del))+")");
			}
			window.cancelAnimationFrame(raf0);
			completed = true;
			gebi("pause").innerHTML = "&#9658; start";
			fps0Span.innerText = 0;

		// Split curve after every pi, so repaint area is small

		} else if(t > (j+1)*pi) {
			j++;
			p = [[reducer*cos(t/(n0) + del), reducer*sin(t/(d0))]];
			raf0 = window.requestAnimationFrame(doAnim);
		} else {
			if (anim0) {
				t += inc;
				if( typeof(ptOnCurv) == "undefined") {
					ASdot([reducer*cos(t/(n0) + del), reducer*sin(t/(d0))], 2, corpColor, corpColor, "ptOnCurv");
					gebi("ptOnCurv");
				}
				ptOnCurv.setAttribute("cx", origin[0] + xunitlength*reducer*cos(t/(n0) + del) );
				ptOnCurv.setAttribute("cy", (boardHeight - origin[1]) - yunitlength*reducer*sin(t/(d0) ));


				if(brdType === 1 ) {
					ptOnCirc0.setAttribute( "transform", "rotate(" + -(t*180/(d0*pi)).toFixed(1) + ", " + (origin[0] - 2*xunitlength).toFixed(1)  + ","+(boardHeight - origin[1]).toFixed(1)+")");


					ptOnCirc1.setAttribute( "transform", "rotate(" + -(( t/n0+del)*180/pi ).toFixed(1) + ", " + (origin[0]).toFixed(1)  + ","+(boardHeight - (origin[1] - 2*yunitlength)).toFixed(1) + ")" );



					seg0.setAttribute("transform", "translate(0 "+(- yunitlength*reducer*(sin(t/(d0))))+")");
					seg1.setAttribute("transform", "translate("+(-xunitlength*reducer*(1-cos(t/(n0)+del)))+")");
				}
				p.push([reducer*cos(t/(n0) + del), reducer*sin(t/(d0))]);
				path(p, "greenCurve"+j);
				raf0 = window.requestAnimationFrame(doAnim);
			}
		}
		// Frames per second
		if (anim0) {
			var thisFrameTime0=(thisLoop0=new Date())-lastLoop0;
			frameTime0+=(thisFrameTime0-frameTime0)/filterStrength;
			lastLoop0=thisLoop0;
			if(10*t.toFixed(1) % 10 == 0)  {
				fps0 = (1000/frameTime0).toFixed(1);
				fps0Span.innerText = fps0;
			}
		}
	}

	function doRot() {
		removeEle("greenCurve");
		for (t = 0; 2*N*D*pi*domFactor + 2*inc > t; t += inc){
			p.push([reducer*cos(t/N + del), reducer*sin(t/D)]);
		}
		// Plot the array
		path(p, "greenCurve");
		rot = true;
	}


	if(slider0.noUiSlider) {
		slider0.noUiSlider.destroy();
	}
	noUiSlider.create(slider0, {
		start: 4,
		step: 1,
		range: {
			"min": 1,
			"max": 20
		},
		tooltips: [true]
	});
	slider0.noUiSlider.on("slide", function(values, handle){
		var leftBy = (1*values[0] < 10 ? 200 : -100 );
		document.getElementsByClassName( "noUi-tooltip")[0].style.left = leftBy+"%";
		plusSgn = ( 1*values[0] < 0 ? "" : "+" );
		gebi("theFn").innerHTML = "`x = cos(t/"+Number(values[0]).toFixed(0)+ plusSgn + Math.abs(del).toFixed(2)+"),~ ~ ~ y = sin(t/"+d0+")`";
		AMfunc(true);
		anim0 = false;
	});

	slider0.noUiSlider.on("set", function(values, handle){
		init();
		doAnim();
	});


	if(slider1.noUiSlider) {
		slider1.noUiSlider.destroy();
	}
	noUiSlider.create(slider1, {
		start: 3,
		step: 1,
		range: {
			"min": 1,
			"max": 20
		},
		tooltips: [true]
	});
	slider1.noUiSlider.on("slide", function(values, handle){
		var leftBy = (1*values[0] < 10 ? 200 : -100 );
		document.getElementsByClassName( "noUi-tooltip")[1].style.left = leftBy+"%";
		plusSgn = ( del < 0 ? "" : "+" );		
		gebi("theFn").innerHTML = "`x = cos(t/"+Number(values[0]).toFixed(0)+ plusSgn +del.toFixed(2)+"),~ ~ ~ y = sin(t/"+d0+")`";
		AMfunc(true);
		anim0 = false;
	});

	slider1.noUiSlider.on("set", function(values, handle){
		init();
		doAnim();
	});

	if(slider2.noUiSlider) {
		slider2.noUiSlider.destroy();
	}

	noUiSlider.create(slider2, {
		start: 0.001,
		step: 0.01,
		range: {
			"min": -pi,
			"max": pi
		},
		tooltips: [true]
	});

	slider2.noUiSlider.on("slide", function(values, handle){
		del = 1*values[0];
		var leftBy = (del < 0 ? 200 : -100 );
		document.getElementsByClassName( "noUi-tooltip")[2].style.left = leftBy+"%";
		gebi("pause").innerHTML = "&#9658; start";
		init();
		anim0 = false;
		fps0Span.innerText = 0;
		doRot();
	});

	// Call these after defining sliders
	init();
	doAnim();

	gebi("pause").addEventListener( "click", function() {
		if(anim0 == true) {
			anim0 = false;
			this.innerHTML = "&#9658; resume";
		} else {
			anim0 = true;
			if(rot) {
				init();
				rot = false;
			}
			if(completed) {
				init();
				completed = false;
			}
			doAnim();
			this.innerHTML = "<small>&#9612;&#9612;</small> pause";
		}
	});

	actualResizeHandler = function() {
		createBrd();
		init();
		doAnim();
	}

	gebi("chk").addEventListener("change", function() {
		brdType = ( brdType == 0 ? 1 : 0 );
		reducer = ( brdType == 0 ? 1 : 0.9 );

		t = 0;
		createBrd();
		setBoardParams("asvg0");
		init();
		doAnim();
	});


} catch(err) {
	gebi("err").innerHTML = err.message;
}

Credit

Based loosely on a script by Jase Smith.

Tips, tricks, lessons, and tutoring to help reduce test anxiety and move to the top of the class.