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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment