I'm currently having difficulties at visualizing Fourier series. I tried the same thing about three times in order to find errors but in vain.
Now I even don't know what is wrong with my code or understanding of Fourier series.
What I'm trying to make is a thing like shown in the following Youtube video: https://youtu.be/r6sGWTCMz2k
I think I know what is Fourier series a bit. I can prove this by showing my previous works:
(1) square wave approximation
(2) parameter
So now I would like to draw more complicated thing in a parametric way. Please let me show the process I've walked.
① From svg path, get coordinates. For example,
// svg path
const d = 'M 0 0 L 20 30 L 10 20 ... ... ... Z';
↓
↓ convert with some processing...
↓
const cx = [0, 20, 10, ...]; // function Fx(t)
const cy = [0, 30, 20, ...]; // function Fy(t)
② Get Fourier coefficients from Fx(t), Fy(t), respectively. After that, I can get approximated coordinates by calculating Fourier series respectively by using the coefficients I got. For example,
Let's say I have a0_x, an_x, bn_x, a0_y, an_y, bn_y.
Then, Fx(t) = a0_x + an_x[1] * cos(1wt) + bn_x[1] * cos(1wt)
+ an_x[2] * cos(2wt) + bn_x[2] * cos(2wt) + ...;
Fy(t) = a0_y + an_y[1] * cos(1wt) + bn_y[1] * cos(1wt)
+ an_y[2] * cos(2wt) + bn_y[2] * cos(2wt) + ...;
Therefore a set of points (Fx(t), Fy(t)) is an approximated path!
This is all! Only thing left is just drawing!
Meanwhile, I processed the data in the following way:
const d = [svg path data];
const split = d.split(/[, ]/);
const points = get_points(split);
const normalized = normalize(points);
const populated = populate(normalized, 8);
const cx = populated.x; // Fx(t)
const cy = populated.y; // Fy(t)
/**
* This function does the below job.
* populate([0,3,6], 2) => output 0 12 3 45 6
* populate([0,4,8], 3) => output 0 123 4 567 8
*/
function populate(data, n) {
if (data.x.length <= 1) throw new Error('NotEnoughData');
if (n < 1) throw new Error('InvalidNValue');
const arr_x = new Array(data.x.length + (data.x.length - 1) * n);
const arr_y = new Array(data.y.length + (data.y.length - 1) * n);
for (let i = 0; i < data.x.length; i++) {
arr_x[i * (n + 1)] = data.x[i];
arr_y[i * (n + 1)] = data.y[i];
}
for (let i = 0; i <= arr_x.length - n - 1 - 1; i += (n + 1)) {
const x_interpolation = (arr_x[i + n + 1] - arr_x[i]) / (n + 1);
const y_interpolation = (arr_y[i + n + 1] - arr_y[i]) / (n + 1);
for (let j = 1; j <= n; j++) {
arr_x[i + j] = arr_x[i] + x_interpolation * j;
arr_y[i + j] = arr_y[i] + y_interpolation * j;
}
}
return { x: arr_x, y: arr_y };
}
// This function makes all values are in range of [-1, 1].
// I just did it... because I don't want to deal with big numbers (and not want numbers having different magnitude depending on data).
function normalize(obj) {
const _x = [];
const _y = [];
const biggest_x = Math.max(...obj.x);
const smallest_x = Math.min(...obj.x);
const final_x = Math.max(Math.abs(biggest_x), Math.abs(smallest_x));
const biggest_y = Math.max(...obj.y);
const smallest_y = Math.min(...obj.y);
const final_y = Math.max(Math.abs(biggest_y), Math.abs(smallest_y));
for (let i = 0; i < obj.x.length; i++) {
_x[i] = obj.x[i] / final_x;
_y[i] = obj.y[i] / final_y;
}
return { x: _x, y: _y };
}
// returns Fx(t) and Fy(t) from svg path data
function get_points(arr) {
const x = [];
const y = [];
let i = 0;
while (i < arr.length) {
const path_command = arr[i];
if (path_command === "M") {
x.push(Number(arr[i + 1]));
y.push(Number(arr[i + 2]));
i += 3;
} else if (path_command === 'm') {
if (i === 0) {
x.push(Number(arr[i + 1]));
y.push(Number(arr[i + 2]));
i += 3;
} else {
x.push(x.at(-1) + Number(arr[i + 1]));
y.push(y.at(-1) + Number(arr[i + 2]));
i += 3;
}
} else if (path_command === 'L') {
x.push(Number(arr[i + 1]));
y.push(Number(arr[i + 2]));
i += 3;
} else if (path_command === 'l') {
x.push(x.at(-1) + Number(arr[i + 1]));
y.push(y.at(-1) + Number(arr[i + 2]));
i += 3;
} else if (path_command === 'H') {
x.push(Number(arr[i + 1]));
y.push(y.at(-1));
i += 2;
} else if (path_command === 'h') {
x.push(x.at(-1) + Number(arr[i + 1]));
y.push(y.at(-1));
i += 2;
} else if (path_command === 'V') {
x.push(x.at(-1));
y.push(Number(arr[i + 1]));
i += 2;
} else if (path_command === 'v') {
x.push(x.at(-1));
y.push(y.at(-1) + Number(arr[i + 1]));
i += 2;
} else if (path_command === 'Z' || path_command === 'z') {
i++;
console.log('reached to z/Z, getting points done');
} else if (path_command === 'C' || path_command === 'c' || path_command === 'S' || path_command === 's' || path_command === 'Q' || path_command === 'q' || path_command === 'T' || path_command === 't' || path_command === 'A' || path_command === 'a') {
throw new Error('unsupported path command, getting points aborted');
} else {
x.push(x.at(-1) + Number(arr[i]));
y.push(y.at(-1) + Number(arr[i + 1]));
i += 2;
}
}
return { x, y };
}
Meanwhile, in order to calculate Fourier coefficients, I used numerical integration. This is the code.
/**
* This function calculates Riemann sum (area approximation using rectangles).
* #param {Number} div division number (= number of rectangles to be used)
* #param {Array | Function} subject subject of integration
* #param {Number} start where to start integration
* #param {Number} end where to end integration
* #param {Number} nth this parameter will be passed to 'subject'
* #param {Function} paramFn this parameter will be passed to 'subject'
* #returns {Number} numerical-integrated value
*/
function numerical_integration(div, subject, start, end, nth = null, paramFn = null) {
if (div < 1) throw new Error(`invalid div; it can't be 0 or 0.x`);
let sum = 0;
const STEP = 1 / div;
const isSubjectArray = Array.isArray(subject);
if (isSubjectArray) {
for (let t = start; t < end; t++) {
for (let u = 0; u < div; u++) {
sum += subject[t + 1] * STEP;
}
}
} else {
for (let t = start; t < end; t++) {
for (let u = 0; u < div; u++) {
const period = end - start;
const isParamFnArray = Array.isArray(paramFn);
if (isParamFnArray) sum += subject((t + 1), period, nth, paramFn) * STEP;
else sum += subject(((t + STEP) + STEP * u), period, nth, paramFn) * STEP;
}
}
}
return sum;
// console.log(numerical_integration(10, (x) => x ** 3, 0, 2));
}
The approximation is near. For (x) => x, division 10, from 0 to 2, the approximation is 2.1 while actual answer is 2. For (x) => x ** 2, division 10, from 0 to 2, the approximation is 2.87, while actual answer is 2.67. For (x) => x ** 3, division 10, from 0 to 2, the approximation is 4.41, while actual answer is 4.
And I found a0, an, bn by the following: (※ You can find Fourier coefficients formulas in my previous question)
/**
* This function will be passed to 'getAn' function.
* #param {Number} t this function is a function of time
* #param {Number} period period of a function to be integrated
* #param {Number} nth integer multiple
* #param {Array | Function} paramFn
* #returns {Number} computed value
*/
function fc(t, period, nth, paramFn) {
const isParamFnArray = Array.isArray(paramFn);
const w = 2 * Math.PI / period;
if (isParamFnArray) return paramFn[t] * Math.cos(nth * w * t);
else return paramFn(t) * Math.cos(nth * w * t);
}
// This function will be passed to 'getBn' function.
function fs(t, period, nth, paramFn) {
const isParamFnArray = Array.isArray(paramFn);
const w = 2 * Math.PI / period;
if (isParamFnArray) return paramFn[t] * Math.sin(nth * w * t);
else return paramFn(t) * Math.sin(nth * w * t);
}
/**
* This function returns a0 value.
* #param {Number} period period of a function to be integrated
* #param {Array | Function} intgFn function to be intergrated
* #param {Number} div number of rectangles to use
* #returns {Number} a0 value
*/
// Why * 30? in order to scale up
// Why - 1? because arr[arr.length] is undefined.
function getA0(period, intgFn, div) {
return 30 * numerical_integration(div, intgFn, 0, period - 1) / period;
}
/**
* This function returns an values.
* #param {Number} period period of a function to be integrated
* #param {Number} div number of rectangles to use
* #param {Number} howMany number of an values to be calculated
* #param {Array | Function} paramFn function to be integrated
* #returns {Array} an values
*/
function getAn(period, div, howMany, paramFn) {
const an = [];
for (let n = 1; n <= howMany; n++) {
const value = 30 * numerical_integration(div, fc, 0, period - 1, n, paramFn) * 2 / period;
an.push(value);
}
return an;
}
// This function returns bn values.
function getBn(period, div, howMany, paramFn) {
const bn = [];
for (let n = 1; n <= howMany; n++) {
const value = 30 * numerical_integration(div, fs, 0, period - 1, n, paramFn) * 2 / period;
bn.push(value);
}
return bn;
}
const xa0 = getA0(cx.length, cx, 10);
const xan = getAn(cx.length, 10, 100, cx);
const xbn = getBn(cx.length, 10, 100, cx);
const ya0 = getA0(cy.length, cy, 10);
const yan = getAn(cy.length, 10, 100, cy);
const ybn = getBn(cy.length, 10, 100, cy);
However, the result was not a thing I wanted... It was a weird shape... Maybe this is life...
The below is the canvas drawing code:
const $cvs = document.createElement('canvas');
const cctx = $cvs.getContext('2d');
$cvs.setAttribute('width', 1000);
$cvs.setAttribute('height', 800);
$cvs.setAttribute('style', 'border: 1px solid black;');
document.body.appendChild($cvs);
window.requestAnimationFrame(draw_tick);
// offset
const xoo = { x: 200, y: 600 }; // x oscillator offset
const yoo = { x: 600, y: 200 }; // y ~
// path
const path = [];
// drawing function
let deg = 0;
function draw_tick() {
const rAF = window.requestAnimationFrame(draw_tick);
// initialize
cctx.clearRect(0, 0, 1000, 800);
// y oscillator
const py = { x: 0, y: 0 };
// a0
// a0 circle
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(yoo.x + py.x, yoo.y + py.y, Math.abs(ya0), 0, 2 * Math.PI);
cctx.stroke();
// a0 line
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(yoo.x + py.x, yoo.y + py.y);
py.x += ya0 * Math.cos(0 * deg * Math.PI / 180);
py.y += ya0 * Math.sin(0 * deg * Math.PI / 180);
cctx.lineTo(yoo.x + py.x, yoo.y + py.y);
cctx.stroke();
// an
for (let i = 0; i < yan.length; i++) {
const radius = yan[i];
// an circles
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(yoo.x + py.x, yoo.y + py.y, Math.abs(radius), 0, 2 * Math.PI);
cctx.stroke();
// an lines
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(yoo.x + py.x, yoo.y + py.y);
py.x += radius * Math.cos((i+1) * deg * Math.PI / 180);
py.y += radius * Math.sin((i+1) * deg * Math.PI / 180);
cctx.lineTo(yoo.x + py.x, yoo.y + py.y);
cctx.stroke();
}
// bn
for (let i = 0; i < ybn.length; i++) {
const radius = ybn[i];
// bn circles
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(yoo.x + py.x, yoo.y + py.y, Math.abs(radius), 0, 2 * Math.PI);
cctx.stroke();
// bn lines
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(yoo.x + py.x, yoo.y + py.y);
py.x += radius * Math.cos((i+1) * deg * Math.PI / 180);
py.y += radius * Math.sin((i+1) * deg * Math.PI / 180);
cctx.lineTo(yoo.x + py.x, yoo.y + py.y);
cctx.stroke();
}
// x oscillator
const px = { x: 0, y: 0 };
// a0
// a0 circle
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(yoo.x + py.x, yoo.y + py.y, Math.abs(xa0), 0, 2 * Math.PI);
cctx.stroke();
// a0 line
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(yoo.x + py.x, yoo.y + py.y);
py.x += xa0 * Math.cos(0 * deg * Math.PI / 180);
py.y += xa0 * Math.sin(0 * deg * Math.PI / 180);
cctx.lineTo(yoo.x + py.x, yoo.y + py.y);
cctx.stroke();
// an
for (let i = 0; i < xan.length; i++) {
const radius = xan[i];
// an circles
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(xoo.x + px.x, xoo.y + px.y, Math.abs(radius), 0, 2 * Math.PI);
cctx.stroke();
// an lines
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(xoo.x + px.x, xoo.y + px.y);
px.x += radius * Math.cos((i+1) * deg * Math.PI / 180);
px.y += radius * Math.sin((i+1) * deg * Math.PI / 180);
cctx.lineTo(xoo.x + px.x, xoo.y + px.y);
cctx.stroke();
}
// bn
for (let i = 0; i < xbn.length; i++) {
const radius = xbn[i];
// bn circles
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.arc(xoo.x + px.x, xoo.y + px.y, Math.abs(radius), 0, 2 * Math.PI);
cctx.stroke();
// bn lines
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(xoo.x + px.x, xoo.y + px.y);
px.x += radius * Math.cos((i+1) * deg * Math.PI / 180);
px.y += radius * Math.sin((i+1) * deg * Math.PI / 180);
cctx.lineTo(xoo.x + px.x, xoo.y + px.y);
cctx.stroke();
}
// y oscillator line
cctx.strokeStyle = 'black';
cctx.beginPath();
cctx.moveTo(yoo.x + py.x, yoo.y + py.y);
cctx.lineTo(xoo.x + px.x, yoo.y + py.y);
cctx.stroke();
// x oscillator line
cctx.strokeStyle = 'black';
cctx.beginPath();
cctx.moveTo(xoo.x + px.x, xoo.y + px.y);
cctx.lineTo(xoo.x + px.x, yoo.y + py.y);
cctx.stroke();
// path
path.push({ x: px.x, y: py.y });
cctx.beginPath();
cctx.strokeStyle = 'black';
cctx.moveTo(200 + path[0].x, 200 + path[0].y);
for (let i = 0; i < path.length; i++) {
cctx.lineTo(200 + path[i].x, 200 + path[i].y);
}
cctx.stroke();
// degree update
if (deg === 359) {
window.cancelAnimationFrame(rAF);
} else {
deg++;
}
}
So! I decided to be logical. First, I checked whether the converted path data is correct by drawing it at canvas. The below is the canvas code and the data.
let count = 0;
function draw_tick2() {
const rAF = window.requestAnimationFrame(draw_tick2);
const s = 100; // scale up
// initialize
cctx.clearRect(0, 0, 1000, 800);
cctx.beginPath();
// 200 has no meaning I just added it to move the path.
for (let i = 0; i < count; i++) {
if (i === 0) cctx.moveTo(200 + s * cx[i], 200 + s * cy[i]);
else cctx.lineTo(200 + s * cx[i], 200 + s * cy[i]);
}
cctx.stroke();
if (count < cx.length - 1) {
count++;
} else {
window.cancelAnimationFrame(rAF);
}
}
const paimon = 'm 0,0 -2.38235,-2.87867 -1.58823,-1.29045 -1.9853,-0.893384 -3.17647,-0.39706 1.58824,-1.98529 1.09191,-2.08456 v -2.38235 l -0.79412,-2.87868 1.88603,2.18383 1.6875,1.88602 1.78677,0.99265 1.78676,0.39706 1.78676,-0.19853 -1.6875,1.58824 -0.69485,1.68749 -0.0993,2.084564 0.39706,2.18383 9.62867,3.87132 2.77941,1.9853 4.66544,-1.09192 3.07721,-1.88603 1.9853,-2.58088 -3.97059,0.49633 -3.375,-0.79412 -2.87868,-2.58088 -2.08456,-3.077214 2.38235,1.48897 2.08456,0.19853 3.57353,-0.89338 2.58089,-2.48162 -3.07721,0.39706 -3.87132,-1.88603 -2.97794,-2.08456 -2.48162,-2.87868 -3.87133,-4.06985 -4.06985,-2.68015 -5.95588,-2.58088 -5.85662,-0.79412 -5.45956,0.99265 0.59559,1.6875 -0.99265,1.09191 -0.79412,3.47427 -1.29044,-2.97794 -0.89338,-1.19118 0.79412,-1.48897 1.6875,-0.79412 0.39706,-3.772057 1.48897,1.290441 1.78676,0.09926 -2.08456,-1.985293 1.78677,-0.893382 4.36765,-0.19853 4.86397,0.992648 1.19117,1.091912 -2.38235,1.985301 3.17647,-0.49633 2.87868,-2.680149 -3.57353,-2.580881 -5.45956,-1.488972 h -4.46691 l -3.6728,-3.176471 -0.79412,1.389706 -0.79411,-1.488969 0.69485,-0.595588 -1.58824,-3.871325 -0.39706,3.672795 -0.69485,0.297794 0.89338,1.091911 v 1.091912 h -1.19113 l -0.59559,-0.992648 -1.98529,2.878677 -4.06986,1.588236 -4.26838,1.985293 3.27574,3.871329 2.87867,1.88603 2.58088,0.29779 -2.58088,-1.58823 -0.89338,-2.084566 4.86397,-0.992645 -1.19118,2.382351 h 1.58824 l 1.48897,-1.88603 0.29779,2.77942 -2.38235,2.38235 -3.57353,2.87868 -3.97059,4.86397 -2.08456,3.67279 -2.58088,2.58088 -2.68015,1.09192 -3.17647,0.0993 -1.3897,-0.69485 1.09191,3.17647 2.18382,3.573534 3.375,2.38235 -1.78676,5.85662 -1.38971,6.05514 0.39706,4.36765 1.38971,4.66544 3.87132,4.46691 -0.79412,-3.57352 -0.49632,-4.06986 v -2.48162 l 1.78676,5.85662 3.07721,3.17647 3.07721,1.29044 3.37499,0.79412 2.28309,-0.89338 0.69486,-1.48897 -1.19118,0.49632 -2.48162,-1.98529 -2.28309,-2.87868 2.28309,2.48162 h 0.99265 l 0.69485,-0.49632 0.2978,-1.19118 0.0993,-0.79412 -0.89339,0.59559 -1.58823,-0.99265 -1.29044,-1.3897 -1.19118,-2.38236 -0.89338,-4.86397 -0.0993,-4.56617 0.29779,-4.96324 0.39706,0.89338 1.19118,-0.44669 0.0496,-0.89338 1.09191,0.69485 1.48897,0.2978 1.53861,0.89338 0.99264,0.64522 h -0.79411 l 0.49632,2.43199 -0.44669,1.58823 -1.78676,0.39706 -1.24081,-1.24081 -0.24817,-1.43934 0.84375,-0.94301 1.19118,-0.49633 1.14154,0.94302 0.24816,1.14154 -0.0993,1.48897 -1.83639,0.64523 -1.58824,-1.53861 -0.44669,-1.48897 -0.24816,-2.18382 -1.43934,0.99264 0.0496,-0.99264 -0.44669,1.78676 0.69485,3.12684 1.09192,4.26838 1.78676,1.78677 6.89889,3.02757 -2.53124,0.99265 -3.17647,1.3897 -0.79412,0.39706 0.59559,0.39706 1.34007,-0.69485 0.0496,1.19117 1.98529,-0.39705 2.68015,-0.44669 -0.2978,-1.93567 0.79412,1.58824 2.82905,-0.44669 4.06985,-1.34008 1.04229,-0.59559 -0.2978,-1.78676 -0.34743,-1.73713 -4.9136,2.48162 -2.58088,0.94301 -3.17648,-4.81434 1.53861,0.49633 1.3897,0.0496 1.43935,-0.24816 -1.34008,0.24816 h -1.58824 l -1.41452,-0.54596 3.12684,4.78953 2.63052,-0.89339 4.86397,-2.4568 2.65533,-2.08456 0.39706,-5.90625 -0.84375,1.5386 -1.14155,0.54596 -1.5386,0.19853 -1.29044,-0.89338 -0.59559,-1.09191 -0.24816,-1.73714 0.24816,-1.3897 -2.08456,0.54595 -0.29779,-0.34742 0.34743,-0.49633 0.64522,-0.39706 1.5386,-0.39705 2.18382,-0.19853 1.24081,0.0993 1.14154,0.54596 0.4467,1.43934 -0.19853,1.63786 -0.59559,1.29044 -1.24081,0.89339 -1.43934,-0.39706 -0.99264,-1.09191 -0.0496,-1.19118 0.79412,-0.89338 0.89338,-0.44669 1.19118,-0.0496 0.64522,1.04228 0.34742,0.79412 -0.14889,1.14155 0.99265,-0.4467 0.29779,-1.34007 -0.19853,-4.06985 -1.93566,-0.44669 -2.53125,-1.6875 -2.23346,-1.88603 -2.23345,-4.069864 -0.44669,3.920964 0.64522,4.21875 1.5386,3.92096 0.74448,0.44669 h -1.73713 l -2.18383,-0.54596 -3.12684,-2.08456 -1.58823,-2.28309 -1.14154,-2.08456 -1.29044,-3.871324 -1.38971,2.481624 -1.48897,2.63051 -0.94302,1.9853 3.8217,-6.948534 1.29044,3.672794 2.33272,3.92096 2.9283,2.13419 0.49633,0.44669 2.28309,0.49632 h 1.63787 l -0.69485,-0.69485 -0.84375,-1.93566 -1.34008,-5.80698 0.44669,-3.970594 2.33273,4.069854 4.56617,3.47426 2.08456,0.59559 0.19853,2.82905 -0.0496,3.97058 -0.0993,6.00552 -0.54595,3.02757 -1.58824,2.77941 -1.5386,0.89339 -1.19118,0.24816 -1.48897,-0.69485 -0.69485,-0.1489 0.69485,1.24081 1.43934,1.6875 2.68015,1.19117 3.17647,0.2978 3.77206,-2.23346 1.3897,-2.77941 0.89339,-3.82169 0.0496,-3.375 0.14889,6.25368 -1.14154,5.11213 -2.08456,3.27573 -2.08456,1.6875 -1.88603,0.59559 -2.28308,-0.79412 1.78676,1.6875 4.9136,1.88603 2.43199,0.2978 2.68015,-0.39706 2.72977,-1.09191 3.62317,-3.27574 0.89338,-3.97059 0.49632,-3.57353 -0.0993,-2.87867 -0.39706,-3.17647 -0.49632,-3.07721 1.98529,3.47427 1.19117,2.18382 0.39706,1.29044 0.39706,-2.28309 -0.39706,-3.0772 -1.29044,-3.77206 -1.29044,-2.87868 -1.6875,-3.27573 -10.125,-4.16912 z';
This is ★Paimon chan★ from a computer game 'Genshin Impact'. Thus it is proved that there are no flaws at the data, since all the data is plotted correctly.
Next, I plotted the approximated (Fx(t), Fy(t)) points so that I can check whether there is a problem. And It turned out that there was a problem. But I don't understand what is the problem. At the same time this path is interesting; The beginning part of the path seems like the hairpin.
This is the drawing code:
function approxFn(t) {
let x = xa0;
let y = ya0;
for (let i = 0; i < xan.length; i++) {
x += xan[i] * Math.cos(2 * Math.PI * i * t / cx.length);
x += xbn[i] * Math.sin(2 * Math.PI * i * t / cx.length);
y += yan[i] * Math.cos(2 * Math.PI * i * t / cx.length);
y += ybn[i] * Math.sin(2 * Math.PI * i * t / cx.length);
}
return { x, y };
}
function draw_tick3() {
const rAF = window.requestAnimationFrame(draw_tick3);
const s = 5;
// initialize
cctx.clearRect(0, 0, 1000, 800);
cctx.beginPath();
for (let t = 0; t < count; t++) {
if (count === 0) cctx.moveTo(200 + s * approxFn(t).x, 200 + s * approxFn(t).y);
else cctx.lineTo(200 + s * approxFn(t).x, 200 + s * approxFn(t).y);
}
cctx.stroke();
if (count < cx.length - 1) {
count++;
} else {
window.cancelAnimationFrame(rAF);
}
}
The above is all the code in my js file. In where I made a mistake? It's a mystery! I know this question is exceptionally seriously long question. But please help me! I want to realize Paimon chan! ㅠwㅠ
※ (This section is irrelevant with the question) Meanwhile I made a success to draw the path in a complex number plane. If you're interested, please see my work... I would like to add circle things to this but I have no idea what is 'radius' in this case.
// You can see that I used real part for x and imaginary part for y.
for (let i = 0; i <= count; i++) {
if (i === 0) {
cctx.moveTo(coords[i].real * scaler + paimonPosition, coords[i].imag * scaler + paimonPosition);
} else {
cctx.lineTo(coords[i].real * scaler + paimonPosition, coords[i].imag * scaler + paimonPosition);
}
}
And this is the result. But what makes me confused is a case of cn = -5000 ~ 5000. As far as I understand, more cn, more accurate as original wave. But why it crashes when cn is so big?
Anyways, thank you very much for reading this long question!
(the character shown: Paimon from Genshin Impact)
Hello myself!
First, errors in your code...
You did not consider a case where sequence of values come after drawing command. For example, your get_points function can't handle a case like h 0 1 2.
Current get_points function can't handle second m drawing command. You need to manually join strings if you have multiple paths.
You need to manually set m x y to m 0 0. Otherwise you can't see canvas drawing. (Maybe values are too too small to draw)
Second, in brief, you can't draw a shape with rotating vectors having fixed magnitude, if you approximate f(t) in a xy plane. It's because what you approximated is not a shape itself, but shape's coordinates.
Third, the reason you got weird shape when you tried to plot approximated data is at your approxFn() function.
x += xan[i] * Math.cos(2 * Math.PI * i * t / cx.length);
x += xbn[i] * Math.sin(2 * Math.PI * i * t / cx.length);
y += yan[i] * Math.cos(2 * Math.PI * i * t / cx.length);
y += ybn[i] * Math.sin(2 * Math.PI * i * t / cx.length);
not t, (t + 1) is correct. Your approximated data has no problem.
Fourth, so you need to take a complex plane approach if you want rotating vectors. In this case, the radius of circles are the magnitude of a sum vector of a real part vector and an imaginary part vector (Pythagorean theorem).
Fifth, In Cn formula, you missed 1 / T.
Sixth, The reason it crashed is... I don't know the exact reason but I think numerical integration and/or finding Cn is wrong. The new code I wrote don't crash at high Cn.
p.s. I wrote some writings about Fourier series. Please see if you are interested: https://logic-finder.github.io/memory/FourierSeriesExploration/opening/opening-en.html
Having to give the idea of an ever changing network of nodes (each with different impact and possibly more than one color) connecting each other to create something.
I wanted to give it depth perception, so I ended up using two canvases around the title: one in the foreground, even over the words, and the other in background, with slightly larger and blurred elements.
Demo here, full JavaScript code at the moment:
// min and max radius, radius threshold and percentage of filled circles
var radMin = 5,
radMax = 125,
filledCircle = 60, //percentage of filled circles
concentricCircle = 30, //percentage of concentric circles
radThreshold = 25; //IFF special, over this radius concentric, otherwise filled
//min and max speed to move
var speedMin = 0.3,
speedMax = 2.5;
//max reachable opacity for every circle and blur effect
var maxOpacity = 0.6;
//default palette choice
var colors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
bgColors = ['52,168,83', '117,95,147', '199,108,23', '194,62,55', '0,172,212', '120,120,120'],
circleBorder = 10,
backgroundLine = bgColors[0];
var backgroundMlt = 0.85;
//min distance for links
var linkDist = Math.min(canvas.width, canvas.height) / 2.4,
lineBorder = 2.5;
//most importantly: number of overall circles and arrays containing them
var maxCircles = 12,
points = [],
pointsBack = [];
//populating the screen
for (var i = 0; i < maxCircles * 2; i++) points.push(new Circle());
for (var i = 0; i < maxCircles; i++) pointsBack.push(new Circle(true));
//experimental vars
var circleExp = 1,
circleExpMax = 1.003,
circleExpMin = 0.997,
circleExpSp = 0.00004,
circlePulse = false;
//circle class
function Circle(background) {
//if background, it has different rules
this.background = (background || false);
this.x = randRange(-canvas.width / 2, canvas.width / 2);
this.y = randRange(-canvas.height / 2, canvas.height / 2);
this.radius = background ? hyperRange(radMin, radMax) * backgroundMlt : hyperRange(radMin, radMax);
this.filled = this.radius < radThreshold ? (randint(0, 100) > filledCircle ? false : 'full') : (randint(0, 100) > concentricCircle ? false : 'concentric');
this.color = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.borderColor = background ? bgColors[randint(0, bgColors.length - 1)] : colors[randint(0, colors.length - 1)];
this.opacity = 0.05;
this.speed = (background ? randRange(speedMin, speedMax) / backgroundMlt : randRange(speedMin, speedMax)); // * (radMin / this.radius);
this.speedAngle = Math.random() * 2 * Math.PI;
this.speedx = Math.cos(this.speedAngle) * this.speed;
this.speedy = Math.sin(this.speedAngle) * this.speed;
var spacex = Math.abs((this.x - (this.speedx < 0 ? -1 : 1) * (canvas.width / 2 + this.radius)) / this.speedx),
spacey = Math.abs((this.y - (this.speedy < 0 ? -1 : 1) * (canvas.height / 2 + this.radius)) / this.speedy);
this.ttl = Math.min(spacex, spacey);
};
Circle.prototype.init = function() {
Circle.call(this, this.background);
}
//support functions
//generate random int a<=x<=b
function randint(a, b) {
return Math.floor(Math.random() * (b - a + 1) + a);
}
//generate random float
function randRange(a, b) {
return Math.random() * (b - a) + a;
}
//generate random float more likely to be close to a
function hyperRange(a, b) {
return Math.random() * Math.random() * Math.random() * (b - a) + a;
}
//rendering function
function drawCircle(ctx, circle) {
//circle.radius *= circleExp;
var radius = circle.background ? circle.radius *= circleExp : circle.radius /= circleExp;
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius * circleExp, 0, 2 * Math.PI, false);
ctx.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', circle.opacity, ')'].join('');
if (circle.filled == 'full') {
ctx.fillStyle = ['rgba(', circle.borderColor, ',', circle.background ? circle.opacity * 0.8 : circle.opacity, ')'].join('');
ctx.fill();
ctx.lineWidth=0;
ctx.strokeStyle = ['rgba(', circle.borderColor, ',', 0, ')'].join('');
}
ctx.stroke();
if (circle.filled == 'concentric') {
ctx.beginPath();
ctx.arc(circle.x, circle.y, radius / 2, 0, 2 * Math.PI, false);
ctx.lineWidth = Math.max(1, circleBorder * (radMin - circle.radius) / (radMin - radMax));
ctx.strokeStyle = ['rgba(', circle.color, ',', circle.opacity, ')'].join('');
ctx.stroke();
}
circle.x += circle.speedx;
circle.y += circle.speedy;
if (circle.opacity < (circle.background ? maxOpacity : 1)) circle.opacity += 0.01;
circle.ttl--;
}
//initializing function
function init() {
window.requestAnimationFrame(draw);
}
//rendering function
function draw() {
if (circlePulse) {
if (circleExp < circleExpMin || circleExp > circleExpMax) circleExpSp *= -1;
circleExp += circleExpSp;
}
var ctxfr = document.getElementById('canvas').getContext('2d');
var ctxbg = document.getElementById('canvasbg').getContext('2d');
ctxfr.globalCompositeOperation = 'destination-over';
ctxfr.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
ctxbg.globalCompositeOperation = 'destination-over';
ctxbg.clearRect(0, 0, canvas.width, canvas.height); // clear canvas
ctxfr.save();
ctxfr.translate(canvas.width / 2, canvas.height / 2);
ctxbg.save();
ctxbg.translate(canvas.width / 2, canvas.height / 2);
//function to render each single circle, its connections and to manage its out of boundaries replacement
function renderPoints(ctx, arr) {
for (var i = 0; i < arr.length; i++) {
var circle = arr[i];
//checking if out of boundaries
if (circle.ttl<0) {}
var xEscape = canvas.width / 2 + circle.radius,
yEscape = canvas.height / 2 + circle.radius;
if (circle.ttl < -20) arr[i].init(arr[i].background);
//if (Math.abs(circle.y) > yEscape || Math.abs(circle.x) > xEscape) arr[i].init(arr[i].background);
drawCircle(ctx, circle);
}
for (var i = 0; i < arr.length - 1; i++) {
for (var j = i + 1; j < arr.length; j++) {
var deltax = arr[i].x - arr[j].x;
var deltay = arr[i].y - arr[j].y;
var dist = Math.pow(Math.pow(deltax, 2) + Math.pow(deltay, 2), 0.5);
//if the circles are overlapping, no laser connecting them
if (dist <= arr[i].radius + arr[j].radius) continue;
//otherwise we connect them only if the dist is < linkDist
if (dist < linkDist) {
var xi = (arr[i].x < arr[j].x ? 1 : -1) * Math.abs(arr[i].radius * deltax / dist);
var yi = (arr[i].y < arr[j].y ? 1 : -1) * Math.abs(arr[i].radius * deltay / dist);
var xj = (arr[i].x < arr[j].x ? -1 : 1) * Math.abs(arr[j].radius * deltax / dist);
var yj = (arr[i].y < arr[j].y ? -1 : 1) * Math.abs(arr[j].radius * deltay / dist);
ctx.beginPath();
ctx.moveTo(arr[i].x + xi, arr[i].y + yi);
ctx.lineTo(arr[j].x + xj, arr[j].y + yj);
var samecolor = arr[i].color == arr[j].color;
ctx.strokeStyle = ["rgba(", arr[i].borderColor, ",", Math.min(arr[i].opacity, arr[j].opacity) * ((linkDist - dist) / linkDist), ")"].join("");
ctx.lineWidth = (arr[i].background ? lineBorder * backgroundMlt : lineBorder) * ((linkDist - dist) / linkDist); //*((linkDist-dist)/linkDist);
ctx.stroke();
}
}
}
}
var startTime = Date.now();
renderPoints(ctxfr, points);
renderPoints(ctxbg, pointsBack);
deltaT = Date.now() - startTime;
ctxfr.restore();
ctxbg.restore();
window.requestAnimationFrame(draw);
}
init();
I asked around and ctx.save() and ctx.restore() are in the top list of suspects, but I wouldn't know how to do this without them.
This is my first animation with canvas, which AFAIK should have been the best option in terms of cross-browser support and (decent) performances, but any advice on this side is still welcome; also, seems to slow down significantly on FF, but just on some machines where hardware acceleration does not work properly (or at all).
From what I read here (and basically everywhere else), FF seems to have serious issues dealing with canvas, but maybe I can optimize things a bit more.
Should I use something other than canvas to do the animation? But also consider that other options (like using SVG) seem to have less support, not to mention it would mean redoing most of the work.
Notes: The first part with the general variables might not be the best practice, but it worked to let a non-technical staff member (UI designer) play on the variables to see different results.