Home | Math art in code | Animated Lissajous figures
Page by Murray Bourne, IntMath.com. Last updated: 05 Dec 2019Animated Lissajous figures
As a child, I spent many hours drawing curves with my 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:
- Try some values of a or b (using the sliders below the graph) and see the effect on the resulting curve.
- Change the phase shift (using the "ps" slider) for the given values of a and b to explore what happens.
- Select Show circles (using the check box) to see how Lissajous curves are the result of combining two signals.
Show circles
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 = "► 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 = "► 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 = "► resume"; } else { anim0 = true; if(rot) { init(); rot = false; } if(completed) { init(); completed = false; } doAnim(); this.innerHTML = "<small>▌▌</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.