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
I have a basic circle bouncing off the walls of a rectangle canvas (that I adapted from an example).
https://jsfiddle.net/n5stvv52/1/
The code to check for this kind of collision is somewhat crude, like so, but it works:
if (p.x > canvasWidth - p.rad) {
p.x = canvasWidth - p.rad
p.velX *= -1
}
if (p.x < p.rad) {
p.x = p.rad
p.velX *= -1
}
if (p.y > canvasHeight - p.rad) {
p.y = canvasHeight - p.rad
p.velY *= -1
}
if (p.y < p.rad) {
p.y = p.rad
p.velY *= -1
}
Where p is the item moving around.
However, the bounds of my canvas now need to be a circle, so I check collision with the following:
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
}
When my ball hits the edges of the circle, the if (collision) statement executes as true and I see the log. So I can get it detected, but I'm unable to know how to calculate the direction it should then go after that.
Obviously comparing x to the canvas width isn't what I need because that's the rectangle and a circle is cut at the corners.
Any idea how I can update my if statements to account for this newly detected circle?
I'm absolutely terrible with basic trigonometry it seems, so please bear with me! Thank you.
You can use the polar coordinates to normalize the vector:
var theta = Math.atan2(dy, dx)
var R = canvasRadius - p.rad
p.x = canvasRadius + R * Math.cos(theta)
p.y = canvasRadius + R * Math.sin(theta)
p.velX *= -1
p.velY *= -1
https://jsfiddle.net/d3k5pd94/1/
Update: The movement can be more natural if we add randomness to acceleration:
p.velX *= Math.random() > 0.5 ? 1 : -1
p.velY *= Math.random() > 0.5 ? 1 : -1
https://jsfiddle.net/1g9h9jvq/
So in order to do this you will indeed need some good ol' trig. The basic ingredients you'll need are:
The vector that points from the center of the circle to the collision point.
The velocity vector of the ball
Then, since things bounce with roughly an "equal and opposite angle", you'll need to find the angle difference between that velocity vector and the radius vector, which you can get by using a dot product.
Then do some trig to get a new vector that is that much off from the radius vector, in the other direction (this is your equal and opposite). Set that to be the new velocity vector, and you're good to go.
I know that's a bit dense, especially if you're rusty with your trig / vector math, so here's the code to get it going. This code could probably be simplified but it demonstrates the essential steps at least:
function canvasApp (selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles () {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
x: Math.random() * canvasWidth,
y: Math.random() * canvasHeight,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw () {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const collision = Math.sqrt(dx * dx + dy * dy) >= canvasRadius - p.rad
if (collision) {
console.log('Out of circle bounds!')
// Center of circle.
const center = [Math.floor(canvasWidth/2), Math.floor(canvasHeight/2)];
// Vector that points from center to collision point (radius vector):
const radvec = [p.x, p.y].map((c, i) => c - center[i]);
// Inverse vector, this vector is one that is TANGENT to the circle at the collision point.
const invvec = [-p.y, p.x];
// Direction vector, this is the velocity vector of the ball.
const dirvec = [p.velX, p.velY];
// This is the angle in radians to the radius vector (center to collision point).
// Time to rememeber some of your trig.
const radangle = Math.atan2(radvec[1], radvec[0]);
// This is the "direction angle", eg, the DIFFERENCE in angle between the radius vector
// and the velocity vector. This is calculated using the dot product.
const dirangle = Math.acos((radvec[0]*dirvec[0] + radvec[1]*dirvec[1]) / (Math.hypot(...radvec)*Math.hypot(...dirvec)));
// This is the reflected angle, an angle that is "equal and opposite" to the velocity vec.
const refangle = radangle - dirangle;
// Turn that back into a set of coordinates (again, remember your trig):
const refvec = [Math.cos(refangle), Math.sin(refangle)].map(x => x*Math.hypot(...dirvec));
// And invert that, so that it points back to the inside of the circle:
p.velX = -refvec[0];
p.velY = -refvec[1];
// Easy peasy lemon squeezy!
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="500" height="500" style="border: 1px solid red; border-radius: 50%;"></canvas>
DISCLAIMER: Since your initial position is random, this doens't work very well with the ball starts already outside of the circle. So make sure the initial point is within the bounds.
You don't need trigonometry at all. All you need is the surface normal, which is the vector from the point of impact to the center. Normalize it (divide both coordinates by the length), and you get the new velocity using
v' = v - 2 * (v • n) * n
Where v • n is the dot product:
v • n = v.x * n.x + v.y * n.y
Translated to your code example, that's
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
function canvasApp(selector) {
const canvas = document.querySelector(selector)
const context = canvas.getContext('2d')
const canvasWidth = canvas.width
const canvasHeight = canvas.height
const canvasRadius = canvasWidth / 2
const particleList = {}
const numParticles = 1
const initVelMax = 1.5
const maxVelComp = 2.5
const randAccel = 0.3
const fadeColor = 'rgba(255,255,255,0.1)'
let p
context.fillStyle = '#050505'
context.fillRect(0, 0, canvasWidth, canvasHeight)
createParticles()
draw()
function createParticles() {
const minRGB = 16
const maxRGB = 255
const alpha = 1
for (let i = 0; i < numParticles; i++) {
const vAngle = Math.random() * 2 * Math.PI
const vMag = initVelMax * (0.6 + 0.4 * Math.random())
const r = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const g = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const b = Math.floor(minRGB + Math.random() * (maxRGB - minRGB))
const color = `rgba(${r},${g},${b},${alpha})`
const newParticle = {
// start inside circle
x: canvasWidth / 4 + Math.random() * canvasWidth / 2,
y: canvasHeight / 4 + Math.random() * canvasHeight / 2,
velX: vMag * Math.cos(vAngle),
velY: vMag * Math.sin(vAngle),
rad: 15,
color
}
if (i > 0) {
newParticle.next = particleList.first
}
particleList.first = newParticle
}
}
function draw() {
context.fillStyle = fadeColor
context.fillRect(0, 0, canvasWidth, canvasHeight)
// draw circle bounds
context.fillStyle = "black"
context.beginPath()
context.arc(canvasRadius, canvasRadius, canvasRadius, 0, 2 * Math.PI, false)
context.closePath()
context.stroke()
p = particleList.first
// random accleration
p.velX += (1 - 2 * Math.random()) * randAccel
p.velY += (1 - 2 * Math.random()) * randAccel
// don't let velocity get too large
if (p.velX > maxVelComp) {
p.velX = maxVelComp
} else if (p.velX < -maxVelComp) {
p.velX = -maxVelComp
}
if (p.velY > maxVelComp) {
p.velY = maxVelComp
} else if (p.velY < -maxVelComp) {
p.velY = -maxVelComp
}
p.x += p.velX
p.y += p.velY
// boundary
const dx = p.x - canvasRadius
const dy = p.y - canvasRadius
const nl = Math.sqrt(dx * dx + dy * dy)
const collision = nl >= canvasRadius - p.rad
if (collision) {
// the normal at the point of collision is -dx, -dy normalized
var nx = -dx / nl
var ny = -dy / nl
// calculate new velocity: v' = v - 2 * dot(d, v) * n
const dot = p.velX * nx + p.velY * ny
p.velX = p.velX - 2 * dot * nx
p.velY = p.velY - 2 * dot * ny
}
context.fillStyle = p.color
context.beginPath()
context.arc(p.x, p.y, p.rad, 0, 2 * Math.PI, false)
context.closePath()
context.fill()
p = p.next
window.requestAnimationFrame(draw)
}
}
canvasApp('#canvas')
<canvas id="canvas" width="176" height="176"></canvas>