Created
June 25, 2014 19:00
-
-
Save kazad/8bb682da198db597558c to your computer and use it in GitHub Desktop.
BetterExplained Fourier Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you sir