Demon888
9/29/2016 - 1:21 AM

BetterExplained Fourier Example

BetterExplained Fourier Example

<html>
<head>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.2/underscore-min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js"></script>
<script src="//ajax.cdnjs.com/ajax/libs/json2/20110223/json2.js"></script>

<!--
TODO:

DONE: Have a "details mode" where we see how we got the frequencies.
- In details mode, have a table / dropdown for you to pick what frequency to analyze
-->
<script type="text/javascript">

	// from view-source:http://treeblurb.com/dev_math/sin_canv00.html
	var x_size = 150;
	var y_size = 100;

    var settings = {

        canvas: {
            width: 0,       // autodetected
            height: 0,
        },

        timegraph_center_x: 150,
        timegraph_center_y: 100,
        timegraph_height: 60,
        timegraph_width: 360,

        circle_radius: 60,
        circle_center_x: 80,
        circle_center_y: 100,

        axis_margin_left: 15,
        axis_margin_top: 15,
        axis_margin_bottom: 15,
        axis_margin_right: 25,

        refresh: 50,    // interval refresh in ms
        steps: 60,      // # of intervals to divide wave into

        cyclegraph_dot: {
            strokeStyle: "#ccc",
            lineWidth: 1.5,
            radius: 3.5,
            fillStyle: "Orange"
        },

        timegraph_dot: {
            strokeStyle: "#ccccff",
            lineWidth: 1.0,
            radius: 3.5,
            fillStyle: "Orange"
        },

        axes: {
            strokeStyle: "#999",
            lineWidth: 0.5
        },

        wave: {
            fillStyle: "rgba(0,0,0,0)",
            strokeStyle: "#C2A7DD",
            lineWidth: 1.5
        },

        combined: {
            color: "#4A93FA"
        },

        interval: {
            dotcolor: "rgba(255, 165, 0, 0.8)",
            lineStyle: "rgba(255, 165, 0, 0.5)",
            lineWidth: 1.0,
        },

        unitcircle: {
            fillStyle: "rgba(0,0,0,0)",
            strokeStyle: "#999",
            lineWidth: 0.5
        },

        cycle: {
            poscolor: "#37B610",
            negcolor: "#E95C59",
            zerocolor: "#999"
        },

        text: {
            font: "normal 12px Courier",
            fillStyle: "#888"
        }
    };

    function getURLParameter(name) {
        return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search)||[,""])[1].replace(/\+/g, '%20'))||null;
    }

    function roundTo(n, digits) {
        return Math.round(n * Math.pow(10, digits)) / Math.pow(10, digits);
    }

    // TODO: perhaps unnecessary, but good for learning: load canvas caches (if possible) and draw into the original
    var cachedCanvas = {};
    function getCachedCanvas(key) {
        return cachedCanvas[key] || null;
    }

    function setCachedCanvas(key, ctx) {

        // flush cache
        if (_(cachedCanvas).keys().length > 25) {
            cachedCanvas = {};
        }

        cachedCanvas[key] = ctx;
    }

    function generateCanvas() {
        var canvas = document.createElement('canvas');
        canvas.width = settings.canvas.width;
        canvas.height = settings.canvas.height;
        return canvas;
    }

    function cachedRender(key, ctx, renderfn) {
        key = JSON.stringify(key);
        var cachedCanvas = getCachedCanvas(key);

        if (!cachedCanvas) {
            cachedCanvas = generateCanvas();
            cached_ctx = cachedCanvas.getContext("2d");
            resetCanvas(cached_ctx);
            renderfn(cached_ctx);
            setCachedCanvas(key, cachedCanvas);
        }

        ctx.drawImage(cachedCanvas, 0, 0);
    }

    // cycleFn: function(x) that returns value for a time point
    // key: unique key for this call, used for caching
    function timegraph_path(ctx, cycleFn, strokeStyle)
    {
        var N = settings.timegraph_width; // buttery-smooth, pixel-by-pixel
        var dx = 2 * (Math.PI) / N;;
        var x = 0;
        var px = settings.timegraph_center_x;
        var px_orig = px;
        var py = settings.timegraph_center_y;

        ctx.beginPath();
        ctx.lineWidth = settings.wave.lineWidth;
        ctx.strokeStyle = strokeStyle || settings.wave.strokeStyle;

        // have one extra point so curves wrap nicely
        for (var i = 0; i <= N; i++) {
            var x = 2 * Math.PI * i/N;
            y = cycleFn(x);

            var px = settings.timegraph_center_x + x * (180 / Math.PI) * settings.timegraph_width / 360;
            var py = settings.timegraph_center_y - settings.timegraph_height*y;

            if (i == 0) {
                ctx.moveTo(px, py);
            }
            else {
                ctx.lineTo(px, py);
            }
        }

        ctx.stroke();
        ctx.closePath();
    }

    function path_circ(ctx, x, y, r)
    {
        ctx.beginPath();
        ctx.arc(x, y, r, 0, Math.PI * 2, true);     //arc(x, y, radius, startAngle, endAngle, anticlockwise)
        ctx.stroke(); 
        ctx.closePath();
    }

    function path_line(ctx, x0, y0, x1, y1)
    {
        ctx.beginPath();
        ctx.moveTo(x0, y0);
        ctx.lineTo(x1, y1);
        ctx.stroke(); 
        ctx.closePath();
    }

    // place circle on canvas 
    function path_dot(ctx, x, y, radius)
    {
        radius = radius || 3.5;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2, true); // arc(x, y, radius, startAngle, endAngle, anticlockwise)
        ctx.fill(); 
        ctx.closePath();
    }

    // dot on cycle chart
    function cyclegraph_dot(ctx, x, y, fillStyle)
    {
        var x = settings.circle_center_x + settings.circle_radius*x;
        var y = settings.circle_center_y - settings.circle_radius*y;

        ctx.strokeStyle = settings.cyclegraph_dot.strokeStyle;
        ctx.lineWidth = settings.cyclegraph_dot.lineWidth;

        // line to origin
        path_line(ctx, settings.circle_center_x, settings.circle_center_y, x, y);

        // draw circle itself
        ctx.fillStyle = fillStyle || settings.cyclegraph_dot.fillStyle;
        path_dot(ctx, x, y, settings.cyclegraph_dot.radius);
    }

    // draw interval marker
    function cyclegraph_interval(ctx, x, y, color)
    {
        var x = settings.circle_center_x + settings.circle_radius*x;
        var y = settings.circle_center_y - settings.circle_radius*y;

        ctx.strokeStyle = color || settings.interval.fillStyle;
        ctx.lineWidth = settings.interval.lineWidth;

        // line to origin
        path_line(ctx, settings.circle_center_x, settings.circle_center_y, x, y);
    }

    function timegraph_interval(ctx, t, color)
    {
        var x = settings.timegraph_center_x + t * 180/Math.PI * settings.timegraph_width / 360;
        var min_y = settings.axis_margin_top;
        var max_y = settings.canvas.height - settings.axis_margin_bottom;

        ctx.fillStyle = color || settings.interval.fillStyle;
        ctx.lineWidth = settings.interval.lineWidth;

        // drop line to baseline
        path_line(ctx, x, min_y, x, max_y);
    }

    // dot on time chart
    function timegraph_dot(ctx, t, height, fillStyle)
    {
    	var x = settings.timegraph_center_x + t * 180/Math.PI * settings.timegraph_width / 360;
    	var y = settings.timegraph_center_y - settings.timegraph_height * height;

        ctx.fillStyle = fillStyle || settings.timegraph_dot.fillStyle;
        path_dot(ctx, x, y, settings.timegraph_dot.radius);

        ctx.strokeStyle = settings.timegraph_dot.strokeStyle;
        ctx.lineWidth = settings.timegraph_dot.lineWidth;
    }

    // parse cycle text and return array of cycle objects (amp, freq, phase)
    // cycles are 0th 1st 2nd 3rd... or 0th 1st & -1st 2nd & -2nd
    function getCycles() {
		$cycleInput = $('input[name=data-cycles]');
		var text = $cycleInput.val();

		text = text.replace(/\s+&\s+/g, '&');
	    var strings = _(text.split(/[\s,]+/)).reject(function(i){ return i == null || i == "";});

	    var index = 0;
	    var cycles = [];

	    function parseCycle(str, freq){
    		var matches = str.split(/[@:]/);
    		return {
    			freq: freq,
    			amp: parseFloat(matches[0]),
    			phase: parseFloat(matches[1] || 0)
    		};
	    }

    	_(strings).each(function(i){
    		var posneg = i.split('&');
    		cycles.push(parseCycle(posneg[0], index));

    		// specified negative cycle too
    		if (posneg[1]) {
    			cycles.push(parseCycle(posneg[1], -1 * index));
    		}

   			index++;
    	});

        cycles = _(cycles).reject(function(i){ return _.isNaN(i.amp); });

    	return cycles;
    }

    function resetCanvas(ctx) {
        ctx.clearRect(0, 0, settings.canvas.width, settings.canvas.height);
    }

    function drawAxes(ctx, scale) {

        // style axes
        ctx.strokeStyle = settings.axes.strokeStyle;
        ctx.lineWidth = settings.axes.lineWidth;

        // x-axis both graphs
        path_line(ctx,
            settings.circle_center_x - settings.circle_radius - settings.axis_margin_left,
            settings.circle_center_y,
            settings.timegraph_center_x + settings.timegraph_width,
            settings.timegraph_center_y);

        // y-axis for circle
        path_line(ctx, settings.circle_center_x, settings.axis_margin_top, settings.circle_center_x,settings.canvas.height - settings.axis_margin_bottom);

        // y-axis for time series
        path_line(ctx, settings.timegraph_center_x, settings.axis_margin_top, settings.timegraph_center_x, settings.canvas.height - settings.axis_margin_bottom);

        // unit circle
        ctx.fillStyle = settings.unitcircle.fillStyle;
        ctx.strokeStyle = settings.unitcircle.strokeStyle;
        ctx.lineWidth = settings.unitcircle.lineWidth;
        path_circ(ctx, settings.circle_center_x, settings.circle_center_y, settings.circle_radius * scale);

        // line for the wave itself
        ctx.fillStyle = settings.wave.fillStyle;
        ctx.strokeStyle = settings.wave.strokeStyle;
        ctx.lineWidth = settings.wave.lineWidth;
    }

    function drawFourier(ctx, options)
    {
        var start = new Date();

        options = options || {};

        // position to move to has been scaled along a circle
        var r = (step/settings.steps) * 2.0 * Math.PI;

        var cycles = getCycles();
        var N = cycles.length;
        var timeseries = Fourier.InverseTransform(cycles);
        var combined = Fourier.totalValue(r, cycles);
        var max_amplitude_time = _(_(timeseries).pluck('amp')).max();
        var max_real = _(_(timeseries).pluck('real')).max();

        // adjust scale if we are hiding the total
        if (!$('#showcombined').is(':checked')) {
            max_amplitude = _(_(cycles).pluck('amp')).max();
        }

        var scale = max_amplitude_time > 0 ? 1 / max_amplitude_time : 1;
        if (scale > 1) {
            scale = 1;
        }

        resetCanvas(ctx);
        drawAxes(ctx, scale);

        function getCycleColor(cycle) {
            var color = cycle.freq > 0 ? settings.cycle.poscolor : settings.cycle.negcolor;
            if (cycle.freq == 0){
                color = settings.cycle.zerocolor;
            }
            return color;
        }

        function drawStatus(text, color) {
            ctx.font = settings.text.font;
            ctx.fillStyle = color || settings.text.fillStyle;
            ctx.fillText(text, settings.timegraph_center_x + 10, canvas.height - settings.axis_margin_bottom);
        }

        function drawIntervals(){
            // draw lines showing the intervals
            _(timeseries).each(function(point){

                // ignore first interval, there's already an x-axis
                if (point.x > 0) {
                    timegraph_interval(ctx, point.x, settings.interval.lineStyle);
                }

                var value = Fourier.totalValue(point.x, {freq: 1, phase: 0, amp: 1});
                cyclegraph_interval(ctx, value.real * scale, value.im * scale, settings.interval.lineStyle);
            });
        }

        if (!$('input[name=data-time]').is(':focus')) {
            var str = _(timeseries).map(function(point){return Math.round(point.real * 10) / 10;}).join(" ");
            $('input[name=data-time]').val(str);
        }

        if ($('#showreverse').is(':checked')) {

            // show only the total and the cycle we want
            var soloFreq = parseInt($('#reversefreq').val());
            scale = 1 / max_amplitude_time;

            var cycle = {
                freq: -1 * soloFreq,
                phase: 0,
                amp: 1
            };

            var color = getCycleColor(cycle);
            var cycleTotal = Fourier.totalValue(r, cycle);

            drawIntervals();

            cyclegraph_dot(ctx, cycleTotal.real * scale, cycleTotal.im * scale, color);
            timegraph_dot(ctx, r, cycleTotal.real * scale, color);

            cachedRender(["timegraph_path", cycle, scale, color], ctx, function(ctx){
                timegraph_path(ctx, function(x){ return Fourier.totalValue(x, cycle).real * scale;}, color);
            });

            var totalReal = 0;
            var totalIm = 0;

            _(timeseries).each(function(point, i){

                // draw the multiplied signal
                var thisCycle = Fourier.totalValue(point.x, cycle);

                if (point.x < r) {
                    timegraph_dot(ctx, point.x, point.real * thisCycle.real * scale, settings.cycle.negcolor);
                    cyclegraph_dot(ctx, point.real * thisCycle.real * scale, point.real * thisCycle.im * scale, settings.cycle.negcolor);
                }

                totalReal += point.real * thisCycle.real;
                totalIm += point.real * thisCycle.im;
            });

            if (timeseries.length > 0 && r > timeseries[timeseries.length - 1].x) {
                // show the final average
                var avgReal = totalReal / N;
                var avgIm = totalIm / N;
                var avgRealRounded = roundTo(avgReal, 2);
                var avgImRounded = roundTo(avgIm, 2);
                var ampRounded = Math.round(Math.sqrt(avgReal * avgReal + avgIm * avgIm), 2);
                var phase = roundTo(Math.atan2(avgImRounded, avgRealRounded) * 180/Math.PI, 0);

                cyclegraph_dot(ctx, avgReal * scale, avgIm * scale, settings.combined.color);

                var text = "avg: " + " re: " + avgRealRounded + " im: " + avgImRounded;
                // text += " [" + ampRounded + (ampRounded != 0 && phase != 0 ? phase : '' ) +  "]";
                drawStatus(text, settings.combined.color);
            }

            return;
        }

        if ($('#showparts').is(':checked')) {
            _(cycles).each(function(cycle){
                var color = getCycleColor(cycle);
                var cycleTotal = Fourier.totalValue(r, cycle);

                cyclegraph_dot(ctx, cycleTotal.real * scale, cycleTotal.im * scale, color);
                timegraph_dot(ctx, r, cycleTotal.real * scale, color);
                timegraph_path(ctx, function(x){ return Fourier.totalValue(x, cycle).real * scale;}, color, cycle);
            });
        }

        // ticks
        if ($('#showdiscrete').is(':checked')){
            drawIntervals();
        }

        // current combined point
        if ($('#showcombined').is(':checked')) {
            cyclegraph_dot(ctx, combined.real * scale, combined.im * scale, settings.combined.color);
            timegraph_path(ctx, function(x){return Fourier.totalValue(x, cycles).real * scale;}, settings.combined.color, cycles);
            timegraph_dot(ctx, r, combined.real * scale, settings.combined.color);

            _(timeseries).each(function(point){
                timegraph_dot(ctx, point.x, point.real * scale, settings.interval.dotcolor);
                cyclegraph_dot(ctx, point.real * scale, point.im * scale, settings.interval.dotcolor);
            });
        }

        // label values
        if ($('#running').is(':checked') == false) {
            var text = "t: " + roundTo(r, 1) + " re: " + roundTo(combined.real, 1) + " im: " + roundTo(combined.im, 1);
        }
    }

    function init()
    {
        var canvas = $('#canvas').get(0);
        var ctx = canvas.getContext("2d");

        settings.canvas.width = canvas.width;
        settings.canvas.height = canvas.height;

        setInterval(function () {
            if ($('#running').is(':checked')) {
                drawFourier(ctx);
                advanceTime();
            }
        }, settings.refresh);

        step = 0;
        function advanceTime(){
            step++;

            if (step > settings.steps){
                step=0;
            }
        }

        $('#reset').click(function(e){
            e.preventDefault();
            step = 0;
            drawFourier(ctx);
        });

        $('input').not('.nochange').change(function(){drawFourier(ctx);}).keyup(function(){drawFourier(ctx);});

        $('#time').css('visibility', 'hidden').change(function(){
            step = ($(this).val() / 100) * settings.steps;
            drawFourier(ctx);
        });

        $('#running').change(function(){
            $('#time').css('visibility', $(this).is(':checked') ? 'hidden' : '');
            $('#time').val((step / settings.steps) * 100);
        });

        if (!Modernizr.inputtypes.range){
            $('#time').css('width', '30px');
        }

        $('input[name=data-time]').keyup(function(){
            var timeseries = Fourier.parseTimeSeries($(this).val());
            var transform = Fourier.Transform(timeseries);
            var newCycles = Fourier.getCyclesFromData(transform);
            var newString = Fourier.getStringFromCycles(newCycles);
            $('input[name=data-cycles]').val(newString);
            drawFourier(ctx, {dataupdate: false});
        });

        $('#canvas').click(function(){
            $('#running').click();
        })

        $('.mrfourier').click(function(e){
            e.preventDefault();
            $('.fourierchart').toggleClass('theme-dark');
        });
    };

    $(function(){
        init();

        var cycles = getURLParameter("cycles");
        if (cycles) {
            cycles = cycles.replace(/,/g, " ");
            $('input[name=data-cycles]').val(cycles);
        } else {
            var time = getURLParameter("time");
            if (time) {
                time = time.replace(/,/g, " ");
                $('input[name=data-cycles]').val("");
                $('input[name=data-time]').val(time).trigger('keyup');
                $('#running').trigger('click');
            }
        }
    });


var Fourier = {};

/*
    Transform a discrete time series to frequency components
    @param data (array): time-series numbers
    @returns frequencies: array of frequency objects, indexed by frequency (f=0 ... N-1):
        {real part, imaginary part, magnitude (computed), phase in degrees (computed) }
*/

Fourier.Transform = function(data) {
    var N = data.length;
    var frequencies = [];

    // for every frequency...
    for (var freq = 0; freq < N; freq++) {     
        var re = 0;
        var im = 0;

        // for every point in time...
        for (var t = 0; t < N; t++) {

            // Spin the signal _backwards_ at each frequency (as radians/s, not Hertz)
            var rate = -1 * (2 * Math.PI) * freq;

            // How far around the circle have we gone at time=t?
            var time = t / N;
            var distance = rate * time;

            // datapoint * e^(-i*2*pi*f) is complex, store each part
            var re_part = data[t] * Math.cos(distance);
            var im_part = data[t] * Math.sin(distance);

            // add this data point's contribution
            re += re_part;
            im += im_part;
        }

        // Close to zero? You're zero.
        if (Math.abs(re) < 1e-10) { re = 0; }
        if (Math.abs(im) < 1e-10) { im = 0; }

        // Average contribution at this frequency
        re = re / N;
        im = im / N;

        frequencies[freq] = {
            re: re,
            im: im,
            freq: freq,
            amp: Math.sqrt(re*re + im*im),
            phase: Math.atan2(im, re) * 180 / Math.PI     // in degrees
        };
    }

    return frequencies;
}

// return data point for all cycles {x, real, im, amp}
Fourier.totalValue = function(x, cycles) {
    cycles = _.isArray(cycles) ? cycles : [cycles];

    var real = 0;
    var im = 0;

    _(cycles).each(function(cycle){
        real += cycle.amp * Math.cos(x * cycle.freq + cycle.phase * Math.PI/180);
        im += cycle.amp * Math.sin(x * cycle.freq + cycle.phase * Math.PI/180);
    });

    return {
        x: x,
        real: real,
        im: im,
        amp: Math.sqrt(real*real + im*im)
    };
};

Fourier.realValue = function(x, cycle) {
    return Fourier.totalValue(x, cycle).real;
};

Fourier.imaginaryValue = function(x, cycle) {
    return Fourier.totalValue(x, cycle).im;
};

// return time series of data points {x, real, im, amp}
Fourier.InverseTransform = function(cycles) {

    var timeseries = [];
    var len = cycles.length;
    for (var i = 0; i < len; i++) {
        var pos = i/len * 2 * Math.PI;
        var total = Fourier.totalValue(pos, cycles);
        timeseries.push(total);
    }
    return timeseries;
};

// Do a fourier transform on this data string
Fourier.getCyclesFromData = function(data, rounding){
    rounding = rounding || 2;
    return _(data).map(function(i){
        return {
            freq: i.freq,
            phase: Math.round(i.phase * Math.pow(10, 1)) / Math.pow(10, 1),
            amp: Math.round(i.amp * Math.pow(10, rounding)) / Math.pow(10, rounding)
        };
    });
};

// convert cycles into parseable string
Fourier.getStringFromCycles = function(cycles){
    var str = "";
    _(cycles).each(function(i){
        str += i.amp;
        if (i.phase != 0 && i.amp != 0){
            str += ":" + i.phase;
        }
        str += " ";
    });
    return str;
}

// Return array of numbers given a time-series string ("1 2.3 -4"). Comma or space separated
Fourier.parseTimeSeries = function(text) {
    var strings = _(text.split(/[\s,]+/)).reject(function(i){ return i == null || i == "";});
    return _(strings).map(function(i){return parseFloat(i);});
}
</script>
</head>

<style type="text/css">
input[type=text] {
	font-family: monospace;
    width: 240px;
}

input[name=data-time] {
    width: 160px;
}

#time {
    width: 100px;
}

.fourierchart {
    width: 520px;
    border: 1px solid #ccc;
    position: relative;
}

label[for=showcombined] {
    color: #4A93FA;
}

label[for=showparts] {
    color: #37B610;
}

label[for=showdiscrete]{
    color: #E7A020;
}

.commands {
    font-family: 'Lucida Console', 'Courier New', monospace;
    font-size: 11px;
    background: #eaeaea;
    color: #333;
    padding: 1px 4px;
}

.gradient {
    background: rgb(238,238,238); /* Old browsers */
    background: -moz-linear-gradient(top,  rgba(238,238,238,1) 0%, rgba(204,204,204,1) 100%); /* FF3.6+ */
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(238,238,238,1)), color-stop(100%,rgba(204,204,204,1))); /* Chrome,Safari4+ */
    background: -webkit-linear-gradient(top,  rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%); /* Chrome10+,Safari5.1+ */
    background: -o-linear-gradient(top,  rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%); /* Opera 11.10+ */
    background: -ms-linear-gradient(top,  rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%); /* IE10+ */
    background: linear-gradient(to bottom,  rgba(238,238,238,1) 0%,rgba(204,204,204,1) 100%); /* W3C */
    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#cccccc',GradientType=0 ); /* IE6-9 */
}

.theme-dark #canvas {
    background: #0F2338;
}

.help {
    font-family: Verdana;
    font-size: 12px;
    position: absolute;
    background: #fafafa;
    padding: 5px;
    color: #333;
    border: 1px solid #ccc;
    top: -150px;
    left: -420px;
    display: none;
    width: 400px;
}

.mrfourier:hover .help {
    xdisplay: block;
}

.mrfourier {
    float: right;
    display: inline-block;
    width: 40px;
    height: 40px;
    /* image from Wikipedia: http://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Joseph_Fourier.jpg/490px-Joseph_Fourier.jpg; */
    background: url(http://betterexplained.com/examples/fourier/fourier.png);
    position: relative;
    top: -24px;
    left: -3px;
    background-size: 100%;
    background-repeat-x: no-repeat;
    background-position-x: 0px;
    border-radius: 81px;
    border: 1px solid #E5E5E5;
}
</style>

<body>
<div class="fourierchart">
    <div class="commands gradient">
	   Cycles <input type="text" name="data-cycles" value = "1 0 0"></input>
        Time <input type="text" name="data-time" class="nochange">
    </div>

    <canvas id="canvas" width="520px" height="200px" style="">
        This browser doesn't support canvas! Try <a href="http://google.com/chrome">Google Chrome</a>.
    </canvas>

    <div class="commands gradient2">
        <input type="checkbox" id="showcombined" checked="checked" name="showcombined"><label for="showcombined">Total</label> 
        <input type="checkbox" id="showparts" checked="checked" name="showparts"><label for="showparts">Parts</label>

        <input type="checkbox" id="showdiscrete" name="showdiscrete"><label for="showdiscrete">Ticks</label>

        <input type="checkbox" id="showreverse" name="showreverse"><label for="showreverse">Derive</label>

        <input type="text" id="reversefreq" style="width: 20px" value="1">
        </input>

        <input type="checkbox" id="running" checked="checked" name="running" class="nochange"><label for="running">Running?</label>
        <input type="range" id="time" min="0" max="100"></input>

        <a href="" class="mrfourier">
            <div class="help">
                Enter frequencies (cycles/sec aka Hz) and see their time values, or vice-versa

                <ul>
                    <li>Frequency input: <code>1 0 2:45</code> is 0Hz (size 1) + 1Hz (size 0) + 2Hz (size 2, phase-shifted 45-degrees) </li>
                    <li>Time input: <code>1 2 3</code> generates a wave that hits 1 2 3
                </ul>
                Click Mr. Fourier for night mode. Have fun!
                <br/>
            </div>
        </a>
    </div>
</div>
</body>
</html>