What do you want to graph? Create a parametric function in JavaScript.
Name it f()
. For each input value t
, between 0
and 1, f()
returns a point like
{ x: 1, y: -0.08333333 }
. You can do one time setup before
defining f()
. See the examples and
additional instructions, below.
You can access these sliders from inside your code. The name of each
slider is written next to it in blue
. The samples will
update immediately as you move the slider.
The first image shows your function tracing out the path. The second image shows how well each approximation matches the original. The table details the progress in the second image.
If you see a dark red or
pink line, that means that there's a
problem drawing the red line. If you see
these extra lines, the normal solution is to ramp up the
support.sampleCount
.
Circles | Amplitude | |
---|---|---|
Using | ||
Adding | ||
Available |
My take on the classic problem.
In this context “complex” means 2 dimensional. This page will convert any function into a series of circles, called a Fourier series. Add the circles back together to get the original function. Circles (or sines and cosines) are often essential because they easier to use than the original function.
This page lets you explore the Fourier series for a function. You can see how well the series approximates the original function, and how quickly it converges. And you can tweak the function and its parameterization to see the result.
The input to this page is a parametric function written in JavaScript.
Your function takes one input, t
, and returns a point. As
t
goes from 0 to 1, your function will trace out a path.
Different functions can trace out the same path. These are called
different “parameterizations.” The small,
light blue dot traces out the
results of your function as t
increases at a constant rate.
The red dot always moves at a constant
speed, as a point of comparison.
The “Square” and “Square with Easing” examples show two parameterizations of the same shape. The former uses the simplest parameterization where the small blue dot is always moving at a constant speed. In the latter example the blue dot slows down around each of the corners, like a physical object would have to, speeding up through the straightaways. The latter example converges faster than the former, perhaps because sharp corners are difficult for a Fourier series, and this function spent more time around those trouble spots.
You have access to all of JavaScript. You also have access to a variable
called support
which provides access to the following:
support.input()
— You can call
support.input(n)
to read a value from one of the sliders on
this page. 0 for the first slider, 1 for the second, etc. The value will
always come back in the range 0-1. (I’m looking at an improved UI, but
this works for now.)
support.ease()
— You can say
t = support.ease(t)
to convert the default linear timing
function into a timing function that eases in and out. The derivative of
this function is 0 around t=0 and t=1. This function is shaped like half
of a sine wave.
support.makeTSplitter()
— This is way to split a
parametric function into smaller parts. Typically you’d call this once
in the setup part of the script, before defining f()
. This
takes a list of bin sizes as an input. This returns a function
that you will call inside of f()
.
Full documentation.
support.makeTSplitterA()
— This is way to split a
parametric function into smaller parts. Typically you’d call this once
in the setup part of the script, before defining f()
. This
takes a number of bins as an input and it assumes
they are all the same size.
This will return a function that you will call inside of
f()
.
Full documentation.
support.lerpPoints()
—
support.lerpPoints({x: x1, y: y1}, {x: x2, y: y2}, t)
will
return a point on the line connecting the two input points. If
t
is 0, lerpPoints()
will return its first
input. If t
is 1, lerpPoints()
will return its
second input. For all other values of t
,
lerpPoints()
will linearly interpolate to create a new
point.
support.lerp()
— Linear interpolation.
lerp(at0, at1, 0)
→ at0
.
lerp(at0, at1, 1)
→ at1
. E.g.
const randomValue = lerp(lowestLegalValue, HighestLegalValue,
Math.random());
.
support.makeLinear()
— A more powerful approach to
linear interpolation. This function returns another function.
Full documentation.
support.random()
— Given a string to seed it, returns
a random number generator. The new random number generator can be a drop
in replacement for Math.random()
. See it in use in the
“Polygons and Stars” example.
Full documentation.
support.referencePath
— This describes the background
image that I am drawing in light blue. Initially it is empty. You can
set this in your one time initialization. If you don't, the software
will run your function at multiple places and create this path for you.
In either case, you can access the referencePath
in the
function. support.referencePath.d
accesses the
path string. support.referencePath.length
gives you the length of the
path. support.referencePath.getPoint()
gives you the x,y of
the path at a given distance along the path.
support.samples
—
support.samples.hilbert[0]
...
support.samples.hilbert[3]
and
support.samples.peanocurve[0]
...
support.samples.peanocurve[2]
contain interesting path
strings.
support.sampleCount
— How much detail to use when
displaying the output. If you see pink and dark red lines, try using
more samples. The current default of 200 is more than enough most of the
time. But in the "Like share and subscribe" example I set this to 2000.
support.maxKeyframes
— If we have more than this many
circles, combine some of them to make this many bins.
const numberOfPoints = 5;
/**
* 0 to make a polygon.
* 1 to make a star, if numberOfPoints is odd and at least 5.
* 2 to make a different star, if numberOfPoints is odd and at least 7.
*/
const skip = 1;
const rotate = 2 * Math.PI / numberOfPoints * (1 + skip);
/**
* Create a random number generator.
* Change the seed to get different values.
* random() will return a number between 0 and 1.
*/
const random = support.random("My seed 2025");
/**
* How much effect does the random number generator have.
* Far left → no randomness at all.
*/
const amplitude = support.input(0);
function jiggle() {
return (random()-0.5) * amplitude;
}
const corners = [];
for (let i = 0; i < numberOfPoints; i++) {
const θ = i * rotate;
corners.push({x: Math.cos(θ) + jiggle(), y: Math.sin(θ) + jiggle()});
}
//console.log(corners);
const tSplitter = support.makeTSplitterA(0, corners.length, 0);
function f(t) {
const segment = tSplitter(t);
return support.lerpPoints(corners[segment.index], corners[(segment.index+1)%corners.length], segment.t);
}
const corners = [{x: -0.5, y: -0.5}, {x: 0.5, y: -0.5}, {x: 0.5, y: 0.5}, {x: -0.5, y: 0.5} ];
const tSplitter = support.makeTSplitterA(0, corners.length, 0);
function f(t) {
const segment = tSplitter(t);
return support.lerpPoints(corners[segment.index], corners[(segment.index+1)%corners.length], segment.t);
}
const corners = [{x: -0.5, y: -0.5}, {x: 0.5, y: -0.5}, {x: 0.5, y: 0.5}, {x: -0.5, y: 0.5} ];
const tSplitter = support.makeTSplitterA(0, corners.length, 0);
function f(t) {
const segment = tSplitter(t);
return support.lerpPoints(corners[segment.index], corners[(segment.index+1)%corners.length], support.ease(segment.t));
}
// This pushes the limits of my graphing software.
// This software is aimed at smooth curves.
// The first input is the number of cusps, 1-10
const cuspCount = Math.round(support.input(0)*9.999+0.5);
// Second input:
// Far left looks like a cloud, cusps pointing inward.
// Far right looks like a star, cusps pointing outward.
// Dead center is smooth, no cusps.
const amplitude = 2 * (support.input(1) - 0.5);
function f(t) {
// Once around the circle.
const θ = t * Math.PI * 2;
const r = 2 - amplitude * Math.abs(Math.sin(t * Math.PI * cuspCount));
const x = r * Math.cos(θ);
const y = r * Math.sin(θ);
return { x, y };
}
// Also consider support.samples.hilbert[0] ... support.samples.hilbert[3]
// and support.samples.peanocurve[0] ... support.samples.peanocurve[2]
support.referencePath.d = support.samples.likeShareAndSubscribe;
support.sampleCount = 2000;
support.maxKeyframes = 30;
const length = support.referencePath.length;
console.log({length});
function f(t) {
// Copy the path as is.
return support.referencePath.getPoint(t * length);
}
// The height can be anything convenient to you.
// This software will automatically zoom and pan to show off your work.
const height = 1;
// Use the first slider to change the width of the ellipse.
const width = height * support.input(0) * 2;
function f(t) {
// Use the second slider to change the starting point on the ellipse.
// This doesn't matter in a static ellipse, but it can be important in some animations and other special cases.
const angle = (t + support.input(1)) * 2 * Math.PI;
const x = width * Math.cos(angle);
const y = height * Math.sin(angle);
return {x, y};}
const height = 1;
const width = height;
function f(t) {
const angle = t * 2 * Math.PI;
const adjustmentAngle = angle * 8;
const adjustmentFactor = Math.sin(adjustmentAngle)/10+1;
const x = width * Math.cos(angle) * adjustmentFactor;
const y = height * Math.sin(angle) * adjustmentFactor;
return {x, y};}
const a = 1; // Amplitude in x-direction
const b = 1; // Amplitude in y-direction
const freqX = 3; // Frequency in x-direction
const freqY = 2; // Frequency in y-direction
const phase = Math.PI / 2; // Phase difference
function f(t) {
const angle = t * 2 * Math.PI;
const x = a * Math.sin(freqX * angle + phase);
const y = b * Math.sin(freqY * angle);
return {x, y};}
const R = 1; // Radius of the large circle
const r = R / 4; // Radius of the small circle (astroid case)
function f(t) {
const angle = t * 2 * Math.PI;
const x = (R - r) * Math.cos(angle) + r * Math.cos((R - r) / r * angle);
const y = (R - r) * Math.sin(angle) - r * Math.sin((R - r) / r * angle);
return {x, y};}
// Number of standard deviations in each direction:
const right = support.input(0) * 5;
const left = - right;
const width = right - left;
const height = support.input(1) * 4 + 1;
function f(t) {
const x = t * width + left;
// Negate this.
// This program works with normal graphics notation where lower values of y are higher on the display.
// Normal algebra-class graphs show lower values of y lower on the screen.
const y = - height * Math.exp(-x*x);
return {x, y};}
const scale = 1; // Overall scale of the spiral
const turns = 3; // Number of full rotations
const waveFreq = 10; // Frequency of the oscillation
const waveAmp = 0.1; // Amplitude of the oscillation
function f(t) {
const angle = t * 2 * Math.PI * turns;
const radius = scale * t; // Linear growth for Archimedean spiral
const wave = waveAmp * Math.sin(t * 2 * Math.PI * waveFreq);
const x = radius * Math.cos(angle) * (1 + wave);
const y = radius * Math.sin(angle) * (1 + wave);
return {x, y};}
function f(t) {
const angle = t * 2 * Math.PI;
const x = 16 * Math.pow(Math.sin(angle), 3);
const algebraClassY = (13 * Math.cos(angle) - 5 * Math.cos(2 * angle) - 2 * Math.cos(3 * angle) - Math.cos(4 * angle));
const y = - algebraClassY;
return {x, y};}
const scale = 0.2;
function f(t) {
const angle = t * 24 * Math.PI * support.input(0); // More rotations for complexity
const e = Math.exp(1);
const x = scale * Math.sin(angle) * (e ** Math.cos(angle) - 2 * Math.cos(4 * angle) - Math.pow(Math.sin(angle / 12), 5));
const y = - scale * Math.cos(angle) * (e ** Math.cos(angle) - 2 * Math.cos(4 * angle) - Math.pow(Math.sin(angle / 12), 5));
return {x, y};}
const scale = 1; // Overall scale of the star
const points = 5; // Number of star points
const innerRadius = 0.4; // Radius of the inner points (controls star shape)
const roundness = 0.1; // Amplitude of the oscillation for rounding
function f(t) {
const angle = t * 2 * Math.PI; // Full circle
const starAngle = angle * points; // Angle scaled for 5 points
const radius = scale * (1 - innerRadius * (Math.cos(starAngle) + 1) / 2); // Base star shape
const rounding = roundness * Math.sin(starAngle); // Oscillation for rounding
const x = (radius + rounding) * Math.cos(angle);
const y = (radius + rounding) * Math.sin(angle);
return {x, y};}
// According to Wikipedia, if it's hollow inside, it's a star.
// If you can see the lines crossing each other, it's a pentagram.
const r1 = 0.5; // Short radius of the ellipse
const r2 = 1.0; // Long radius of the ellipse
const phase = support.input(0) * Math.PI; // First slider: Rotation angle in radians (0 to π)
function f(t) {
const angle = t * 2 * Math.PI; // Full circle
// Basic ellipse centered at the origin
const xEllipse = r1 * Math.cos(angle);
const yEllipse = r2 * Math.sin(angle);
// Rotate the ellipse by the phase angle
const x = xEllipse * Math.cos(phase) - yEllipse * Math.sin(phase);
const y = xEllipse * Math.sin(phase) + yEllipse * Math.cos(phase);
return {x, y};}
// I used this formula as a starting place for the rounded pentagram.
const r1 = 0.5 * support.input(0); // Short radius of the ellipse. Top slider will adjust it.
const r2 = 1.0; // Long radius of the ellipse
function f(t) {
const phase = Math.PI * t; // The reference ellipse will make one half complete rotation during the tracing process.
const numberOfTrips = support.input(1) * 10; // Effective range is 0 to 10
const angle = t * 2 * Math.PI * numberOfTrips; // Basic ellipse centered at the origin
const xEllipse = r1 * Math.cos(angle);
const yEllipse = r2 * Math.sin(angle);// Rotate the ellipse by the phase angle
const x = xEllipse * Math.cos(phase) - yEllipse * Math.sin(phase);
const y = xEllipse * Math.sin(phase) + yEllipse * Math.cos(phase);
return {x, y};}
// The top slider controls the amount of curvature in the output.
// The second slider controls the number of lobes.
// Try values like 0.05, 0.15, 0.25, …, 0.95 for closed shapes.
// This will trace out the shape of a dog tag using epicycles.
// Use the first slider to choose how many circles to use in
// this approximation, from 1 to 20.
// I was originally trying to use epicycles to create a square.
// But I ran into some problems,
// so this a square where two of the sides bulge out some.
const numberOfCircles = 1 + 19 * support.input(0);
const circlesToConsider = Math.ceil(numberOfCircles);
const attenuation = numberOfCircles - Math.floor(numberOfCircles);
function f(t) {
let x = 0;
let y = 0;
for (let k = 0; k < circlesToConsider; k++) {
const n = 2 * k + 1; // Odd frequencies: 1, 3, 5, ...
const radius = (4 * Math.sqrt(2)) / (Math.PI * Math.PI * n * n);
const phase = k % 2 === 0 ? -Math.PI / 4 : Math.PI / 4;
const factor = (k === circlesToConsider - 1 && attenuation > 0) ? attenuation : 1;
const baseAngle = t * 2 * Math.PI;
x += factor * radius * Math.cos(n * baseAngle + phase);
y += factor * radius * Math.sin(n * baseAngle + phase);
}
return {x, y};}
// Inspired by https://www.youtube.com/watch?v=t99CmgJAXbg
// Square Orbits Part 1: Moon Orbits
const R = 0.573; // Match our first circle's radius
const moonRadius = (7 / 45) * R;
function f(t) {
const planetAngle = t * 2 * Math.PI; // Frequency 1
const moonAngle = -3 * planetAngle; // Frequency 3, opposite direction
const planetX = R * Math.cos(planetAngle);
const planetY = R * Math.sin(planetAngle);
const moonX = moonRadius * Math.cos(moonAngle);
const moonY = moonRadius * Math.sin(moonAngle);
const x = (planetX + moonX) * 1.2;
const y = (planetY + moonY) * 1.2;
return {x, y};}
// Use the first slider to choose how many sine waves to use in
// this approximation, from 1 to 20.
const numberOfCircles = 1 + 19 * support.input(0);
const circlesToConsider = Math.ceil(numberOfCircles);
const attenuation = numberOfCircles - Math.floor(numberOfCircles);
function f(t) {
let ySum = 0;
for (let k = 0; k < circlesToConsider; k++) {
const n = 2 * k + 1; // Odd frequencies: 1, 3, 5, ...
const amplitude = (4 / Math.PI) / n;
const factor = (k === circlesToConsider - 1 && attenuation > 0) ? attenuation : 1;
const baseAngle = 2 * Math.PI * 2.5 * t + Math.PI / 2; // 2.5 cycles, shift for vertical center
ySum += factor * amplitude * Math.sin(n * baseAngle);
}
const x = (t * 5) - 2.5; // Span x from -2.5 to 2.5
const y = ySum;
return {x, y};}