Related
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
So I've wrote a microbe animation.
It's all cool, but I think that it would be even better, if the microbe would be able to eat diatoms, and to destroy bubbles.
The issue is that the microbe is made of bezier curves.
I have no idea how to check collision between object made of bezier curves, and a circle in a reasonable way.
The only thing that comes to my mind, is to paint the microbe shape and bubbles a hidden canvas, and then check if they paint to the same pixels. But that would cause big performance issues IMHO.
Code: https://codepen.io/michaelKurowski/pen/opWeKY
class Cell is the cell, while class CellWallNode is a node of bezier curve, in case if somebody needs to look up the implementation.
The bubbles and diatoms can be easily simplified to circles.
Solution to bounds testing object defined by beziers
Below is an example solution to finding if a circle is inside an object defined by a center point and a set of beziers defining the perimeter.
The solution has only been tested for non intersecting cubic beziers. Also will not work if there are more than two intercepts between the object being tested and the center of the cell. However all you need to solve for the more complex bounds is there in the code.
The method
Define a center point to test from as a 2D point
Define the test point as a 2D point
Define a line from the center to the test point
For each bezier
Translate bezier so first point is at start of line
Rotate the bezier such that the line is aligned to the x axis
Solve the bezier polynomials to find the roots (location of x axis intercepts)
Use the roots to find position on bezier curve of line intercept.
Use the closest intercept to the point to find distance from center to perimeter.
If perimeter distance is greater than test point distance plus radius then inside.
Notes
The test is to a point along a line to the center not to a circle which would be a area defined by a triangle. As long as the circle radius is small compared to the size of the beziers the approximation works well.
Not sure if you are using cubic or quadratic beziers so the solution covers both cubic and quadratic beziers.
Example
The snippet creates a set of beziers (cubic) around a center point. the object theBlob holds the animated beziers. The function testBlob tests the mouse position and returns true if inside theBlob. The object bezHelper contains all the functionality needed to solve the problem.
The cubic root solver was derived from github intersections cube root solver.
const bezHelper = (()=>{
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// To prevent heap thrashing close over some pre defined 2D points
const v1 = P2();
const v2 = P2();
const v3 = P2();
const v4 = P2();
var u,u1,u2;
// solves quadratic for bezier 2 returns first root
function solveBezier2(A, B, C){
// solve the 2nd order bezier equation.
// There can be 2 roots, u,u1 hold the results;
// 2nd order function a+2(-a+b)x+(a-2b+c)x^2
a = (A - 2 * B + C);
b = 2 * ( - A + B);
c = A;
a1 = 2 * a;
c = b * b - 4 * a * c;
if(c < 0){
u = Infinity;
u1 = Infinity;
return u;
}else{
b1 = Math.sqrt(c);
}
u = (-b + b1) / a1;
u1 = (-b - b1) / a1;
return u;
}
// solves cubic for bezier 3 returns first root
function solveBezier3(A, B, C, D){
// There can be 3 roots, u,u1,u2 hold the results;
// Solves 3rd order a+(-2a+3b)t+(2a-6b+3c)t^2+(-a+3b-3c+d)t^3 Cardano method for finding roots
// this function was derived from http://pomax.github.io/bezierinfo/#intersections cube root solver
// Also see https://en.wikipedia.org/wiki/Cubic_function#Cardano.27s_method
function crt(v) {
if(v<0) return -Math.pow(-v,1/3);
return Math.pow(v,1/3);
}
function sqrt(v) {
if(v<0) return -Math.sqrt(-v);
return Math.sqrt(v);
}
var a, b, c, d, p, p3, q, q2, discriminant, U, v1, r, t, mp3, cosphi,phi, t1, sd;
u2 = u1 = u = -Infinity;
d = (-A + 3 * B - 3 * C + D);
a = (3 * A - 6 * B + 3 * C) / d;
b = (-3 * A + 3 * B) / d;
c = A / d;
p = (3 * b - a * a) / 3;
p3 = p / 3;
q = (2 * a * a * a - 9 * a * b + 27 * c) / 27;
q2 = q / 2;
a /= 3;
discriminant = q2 * q2 + p3 * p3 * p3;
if (discriminant < 0) {
mp3 = -p / 3;
r = sqrt(mp3 * mp3 * mp3);
t = -q / (2 * r);
cosphi = t < -1 ? -1 : t > 1 ? 1 : t;
phi = Math.acos(cosphi);
t1 = 2 * crt(r);
u = t1 * Math.cos(phi / 3) - a;
u1 = t1 * Math.cos((phi + 2 * Math.PI) / 3) - a;
u2 = t1 * Math.cos((phi + 4 * Math.PI) / 3) - a;
return u;
}
if(discriminant === 0) {
U = q2 < 0 ? crt(-q2) : -crt(q2);
u = 2 * U - a;
u1 = -U - a;
return u;
}
sd = sqrt(discriminant);
u = crt(sd - q2) - crt(sd + q2) - a;
return u;
}
// get a point on the bezier at pos ( from 0 to 1 values outside this range will be outside the bezier)
// p1, p2 are end points and cp1, cp2 are control points.
// ret is the resulting point. If given it is set to the result, if not given a new point is created
function getPositionOnBez(pos,p1,p2,cp1,cp2,ret = P2()){
if(pos === 0){
ret.x = p1.x;
ret.y = p1.y;
return ret;
}else
if(pos === 1){
ret.x = p2.x;
ret.y = p2.y;
return ret;
}
v1.x = p1.x;
v1.y = p1.y;
var c = pos;
if(cp2 === undefined){
v2.x = cp1.x;
v2.y = cp1.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (p2.x - v2.x) * c;
v2.y += (p2.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
v2.x = cp1.x;
v2.y = cp1.y;
v3.x = cp2.x;
v3.y = cp2.y;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (p2.x - v3.x) * c;
v3.y += (p2.y - v3.y) * c;
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
ret.x = v1.x + (v2.x - v1.x) * c;
ret.y = v1.y + (v2.y - v1.y) * c;
return ret;
}
const cubicBez = 0;
const quadraticBez = 1;
const none = 2;
var type = none;
// working bezier
const p1 = P2();
const p2 = P2();
const cp1 = P2();
const cp2 = P2();
// rotated bezier
const rp1 = P2();
const rp2 = P2();
const rcp1 = P2();
const rcp2 = P2();
// translate and rotate bezier
function transformBez(pos,rot){
const ax = Math.cos(rot);
const ay = Math.sin(rot);
var x = p1.x - pos.x;
var y = p1.y - pos.y;
rp1.x = x * ax - y * ay;
rp1.y = x * ay + y * ax;
x = p2.x - pos.x;
y = p2.y - pos.y;
rp2.x = x * ax - y * ay;
rp2.y = x * ay + y * ax;
x = cp1.x - pos.x;
y = cp1.y - pos.y;
rcp1.x = x * ax - y * ay;
rcp1.y = x * ay + y * ax;
if(type === cubicBez){
x = cp2.x - pos.x;
y = cp2.y - pos.y;
rcp2.x = x * ax - y * ay;
rcp2.y = x * ay + y * ax;
}
}
function getPosition2(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
}
function getPosition3(pos,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
}
const API = {
getPosOnQBez(pos,p1,cp1,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,undefined,ret);
},
getPosOnCBez(pos,p1,cp1,cp2,p2,ret){
return getPositionOnBez(pos,p1,p2,cp1,cp2,ret);
},
set bezQ(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(p2, points[2]);
type = quadraticBez;
},
set bezC(points){
setP2As(p1, points[0]);
setP2As(cp1, points[1]);
setP2As(cp2, points[2]);
setP2As(p2, points[3]);
type = cubicBez;
},
isInside(center, testPoint, pointRadius){
drawLine(testPoint , center);
v1.x = (testPoint.x - center.x);
v1.y = (testPoint.y - center.y);
const pointDist = Math.sqrt(v1.x * v1.x + v1.y * v1.y)
const dir = -Math.atan2(v1.y,v1.x);
transformBez(center,dir);
if(type === cubicBez){
solveBezier3(rp1.y, rcp1.y, rcp2.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { u = u2 }
if (u < 0 || u > 1) { return }
getPosition3(u, v4);
}else{
solveBezier2(rp1.y, rcp1.y, rp2.y);
if (u < 0 || u > 1) { u = u1 }
if (u < 0 || u > 1) { return }
getPosition2(u, v4);
}
drawCircle(v4);
const dist = Math.sqrt((v4.x - center.x) ** 2 + (v4.y - center.y) ** 2);
const dist1 = Math.sqrt((v4.x - testPoint.x) ** 2 + (v4.y - testPoint.y) ** 2);
return dist1 < dist && dist > pointDist - pointRadius;
}
}
return API;
})();
const ctx = canvas.getContext("2d");
const m = {x : 0, y : 0};
document.addEventListener("mousemove",e=>{
var b = canvas.getBoundingClientRect();
m.x = e.pageX - b.left - scrollX - 2;
m.y = e.pageY - b.top - scrollY - 2;
});
function drawCircle(p,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.arc(p.x,p.y,r,0,Math.PI*2)
ctx.stroke();
}
function drawLine(p1,p2,r = 5,col = "black"){
ctx.beginPath();
ctx.strokeStyle = col;
ctx.lineTo(p1.x,p1.y);
ctx.lineTo(p2.x,p2.y);
ctx.stroke();
}
const w = 400;
const h = 400;
const diag = Math.sqrt(w * w + h * h);
// creates a 2D point
const P2 = (x=0, y= x === 0 ? 0 : x.y + (x = x.x, 0)) => ({x, y});
const setP2As = (p,pFrom) => (p.x = pFrom.x, p.y = pFrom.y, p);
// random int and double
const randI = (min, max = min + (min = 0)) => (Math.random()*(max - min) + min) | 0;
const rand = (min = 1, max = min + (min = 0)) => Math.random() * (max - min) + min;
const theBlobSet = [];
const theBlob = [];
function createCubicBlob(segs){
const step = Math.PI / segs;
for(var i = 0; i < Math.PI * 2; i += step){
const dist = rand(diag * (1/6), diag * (1/5));
const ang = i + rand(-step * 0.2,step * 0.2);
const p = P2(
w / 2 + Math.cos(ang) * dist,
h / 2 + Math.sin(ang) * dist
);
theBlobSet.push(p);
theBlob.push(P2(p));
}
theBlobSet[theBlobSet.length -1] = theBlobSet[0];
theBlob[theBlobSet.length -1] = theBlob[0];
}
createCubicBlob(8);
function animateTheBlob(time){
for(var i = 0; i < theBlobSet.length-1; i++){
const ang = Math.sin(time + i) * 6;
theBlob[i].x = theBlobSet[i].x + Math.cos(ang) * diag * 0.04;
theBlob[i].y = theBlobSet[i].y + Math.sin(ang) * diag * 0.04;
}
}
function drawTheBlob(){
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
var i = 0;
ctx.moveTo(theBlob[i].x,theBlob[i++].y);
while(i < theBlob.length){
ctx.bezierCurveTo(
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y,
theBlob[i].x,theBlob[i++].y
);
}
ctx.stroke();
}
var center = P2(w/2,h/2);
function testBlob(){
var i = 0;
while(i < theBlob.length-3){
bezHelper.bezC = [theBlob[i++], theBlob[i++], theBlob[i++], theBlob[i]];
if(bezHelper.isInside(center,m,6)){
return true;
}
}
return false;
}
// main update function
function update(timer){
ctx.clearRect(0,0,w,h);
animateTheBlob(timer/1000)
drawTheBlob();
if(testBlob()){
ctx.strokeStyle = "red";
}else{
ctx.strokeStyle = "black";
}
ctx.beginPath();
ctx.arc(m.x,m.y,5,0,Math.PI*2)
ctx.stroke();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas { border : 2px solid black; }
<canvas id="canvas" width = "400" height = "400"></canvas>
I had created an animation of bubbles in which al the circle will expand which are 50px neer to the mouse.
so here is the trick. you can just simply change mouseX,mouseY with your microbe's X and Y coordinates and 50 to the radius of your microbe.
And when my bubbles get bigger, so there you can destroy you air bubbles.
here is the link to my Animation.
https://ankittorenzo.github.io/canvasAnimations/Elements/Bubbles/
here is the link to my GitHub Code.
https://github.com/AnkitTorenzo/canvasAnimations/blob/master/Elements/Bubbles/js/main.js
Let Me Know if you have any problem.
I'm trying to generate an elliptical arc by approximating a bezier curve as in the post https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
However my implementation doesn't seem to fetch the right result. (Red line is SVG and black line is canvas path)
This is my code
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y
};
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
cp = generateBezierPoints(
45,35, // Radii
-30 * Math.PI / 180, // xAngle
0, // Large arc flag
1, // Sweep flag
100,100, // Endpoint1
100 + 50, 100 - 25 // Endpoint2
);
ctx.bezierCurveTo(cp.cpx1,cp.cpy1,cp.cpx2,cp.cpy2,150,75);
ctx.stroke()
I need help with understanding where I'm going wrong
UPDATE:
I went through the post a couple more times and there is one part of the post that I don't quite understand which may also be lacking in my implementation.
All I had to do was subdivide the angle range into small sections to get a good approximation. I didn’t quite understand the paper’s error calculations, but I found another paper by Joe Cridge indicating divisions of π/2 provides a potential one pixel error on a fairly high resolution device. So I chose π/4 to ensure smooth animation, even for partial arcs on high density mobile devices.
I don't understand what the author means by subdividing the angles...
So apparently an elliptical arc cannot be approximated with a single bezier curve, so it takes multiple bezier curves by dividing the two angles into ranges.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var n = [];
n.push(n1);
var interval = Math.PI/4;
while(n[n.length - 1] + interval < n2)
n.push(n[n.length - 1] + interval)
n.push(n2);
function getCP(n1, n2) {
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y,
en1: en1,
en2: en2
};
}
var cps = []
for(var i = 0; i < n.length - 1; i++) {
cps.push(getCP(n[i],n[i+1]));
}
return cps;
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
var rx = 45, ry=35,phi = -30 * Math.PI / 180, fa = 0, fs = 1, x = 100, y = 100, x1 = x + 50, y1 = y - 25;
var cps = generateBezierPoints(rx, ry, phi, fa, fs, x, y, x1, y1);
var limit = 2;
for(var i = 0; i < limit && i < cps.length; i++) {
ctx.bezierCurveTo(cps[i].cpx1, cps[i].cpy1,
cps[i].cpx2, cps[i].cpy2,
i < limit - 1 ? cps[i].en2.x : x1, i < limit - 1 ? cps[i].en2.y : y1);
}
ctx.stroke()
I have a orbit of length 200. But it is centered around a sun of radius 0 (length 0). Now I want to expand the sun to have a radius of 1 and "push" out the outer orbits as well.
The XYZ coordinates look like this:
[-6.76, 5.75, -1.06],
[-6.95, 5.54, -0.91],
[-7.13, 5.33, -0.75],
[-7.31, 5.11, -0.58]
... followed by 196 more coordinates
I tried tried a lot of things to make the circle bigger * radius and / someNumbers. To at least try to do it myself.
But i lost it when i made an if like this:
If(the x coordination > 0)
the x coordination += 1;
}
Else{
the x coordination += 1;
}
And also for Y and Z but when they came close to the 1 and -1 position of that axis they skipped to the other side.
Creating a line (with the width of 1 on both sides) of emptiness along the axis.
Result of MBo's awnser(view from above):
// arrayIndex is a number to remember at which point it is in the orbit array
satellites.forEach(function (element) {
if (element.arrayIndex>= element.satellite.coordinates.length) {
element.arrayIndex= 0;
}
var posX = element.satellite.coordinates[element.arrayIndex][0];
var posY = element.satellite.coordinates[element.arrayIndex][1];
var posZ = element.satellite.coordinates[element.arrayIndex][2];
R = Math.sqrt(posX^2 + posY^2 + posZ^2);
cf = (R + earthRadius) / R;
xnew = posX * cf;
ynew = posY * cf;
znew = posZ * cf;
// var posX = earthRadius * (element.satellite.coordinates[element.test][0] / (200 * earthRadius) * earthRadius);
// var posY = earthRadius * (element.satellite.coordinates[element.test][1] / (200 * earthRadius) * earthRadius);
// var posZ = earthRadius * (element.satellite.coordinates[element.test][2] / (200 * earthRadius) * earthRadius);
// divide by 100 to scale it down some more
element.position.x = xnew / 100;
element.position.y = ynew / 100;
element.position.z = znew / 100;
element.arrayIndex= element.arrayIndex+ 1;
});
You have orbit radius
/////////R = Sqrt(x^2 + y^2 + z^2)
Edit to avoid confusion:
R = Sqrt(x * x + y * y + z * z)
You need to modify coordinates to make orbit radius R+r. To preserve orbit form, for every point find it's R, and multiply all components by coefficient (R+r)/R
R = Sqrt(x^2 + y^2 + z^2)
cf = (R + r) / R
xnew = x * cf
ynew = y * cf
znew = z * cf
So I have an imaginary circle divided into multiple parts (I use 8 for simplicity, but in the end, I would like to divide it to 16 or 32 parts).
Then I have N number of quadratic bezier curves, that is between 2 nearest segments. It may rest upon the circle or further from the center, but not nearer than the circle.
I know how to find, what in witch line I should look for intersection in, but I do not know how to split it into two parts... I know, that if I looked for intersection of the line and curve I should get the point that the previous curve should end and the next should start, and that by derivation I may be able to get the vector, but I do not know how to do it.
Example image where I have only 8 parts for easier problem solving.
The point is, to make "progress" bar using bezier curves. Side note: The curves will change every frame, as they are part of music visualization.
If there is a better way to spit color a curve, I am all for it!
Spliting cubic and quadratic Beziers
Splitting a bezier is relatively easy. As there is already an answer I will just copy the functions needed to split a single bezier, cubic or quadratic at a position along its path range from 0 to 1. The function Bezier.splitAt takes a position (0 to 1) and depending on start = true returns the from 0 to position or the if start = false returns the bezier from position to 1. It will handle both 2nd order (quadratic) and 3rd order (cubic) Beziers
Example usage
var bezier = createBezierCubic( 146, 146, 134, 118, 184, 103, 217, 91 );
// split in two
var startingHalf = bezier.splitAt(0.5, true);
var endingHalf = bezier.splitAt(0.5, false);
// split into four.
var quart1 = startingHalf.splitAt(0.5, true)
var quart2 = startingHalf.splitAt(0.5, false)
var quart3 = endingHalf.splitAt(0.5, true)
var quart4 = endingHalf.splitAt(0.5, false)
// getting a segment
var startFrom = 0.3;
var endAt = 0.8;
var section = bezier.splitAt(startFrom, false).splitAt((endAt - startFrom) / (1 - startFrom), true);
The bezier is made up of a start and end point p1, p2 and one or two control points cp1, cp2. If the bezier is 2nd order then cp2 is undefined. The points are Vec and take the from Vec.x, Vec.y
To render a 2nd order
ctx.moveTo(bezier.p1.x, bezier.p1.y);
ctx.quadraticCurveTo(bezier.cp1.x, bezier.cp1.y, bezier.p2.x, bezier.p2.y);
To render the 3rd order
ctx.moveTo(bezier.p1.x, bezier.p1.y);
ctx.bezierCurveTo(bezier.cp1.x, bezier.cp1.y, bezier.cp2.x, bezier.cp2.y, bezier.p2.x, bezier.p2.y);
The code with dependencies.
As you are all programmers see the code for more info in usage. Warning there could be typos as this has been pulled from a more extensive geometry interface.
var geom = (function(){
function Vec(x,y){ // creates a vector
if(x === undefined){
x = 1;
y = 0;
}
this.x = x;
this.y = y;
}
Vec.prototype.set = function(x,y){
this.x = x;
this.y = y;
return this;
};
// closure vars to stop constant GC
var v1 = Vec();
var v2 = Vec();
var v3 = Vec();
var v4 = Vec();
var v5 = Vec();
const BEZIER_TYPES = {
cubic : "cubic",
quadratic : "quadratic",
};
// creates a bezier p1 and p2 are the end points as vectors.
// if p1 is a string then returns a empty bezier object.
// with the type as quadratic (default) or cubic
// cp1, [cp2] are the control points. cp2 is optional and if omitted will create a quadratic
function Bezier(p1,p2,cp1,cp2){
if(typeof p1 === 'string'){
this.p1 = new Vec();
this.p2 = new Vec();
this.cp1 = new Vec();
if(p1 === BEZIER_TYPES.cubic){
this.cp2 = new Vec();
}
}else{
this.p1 = p1 === undefined ? new Vec() : p1;
this.p2 = p2 === undefined ? new Vec() : p2;
this.cp1 = cp1 === undefined ? new Vec() : cp1;
this.cp2 = cp2;
}
}
Bezier.prototype.type = function(){
if(this.cp2 === undefined){
return BEZIER_TYPES.quadratic;
}
return BEZIER_TYPES.cubic;
}
Bezier.prototype.splitAt = function(position,start){ // 0 <= position <= 1 where to split. Start if true returns 0 to position and else from position to 1
var retBezier,c;
if(this.cp2 !== undefined){ retBezier = new Bezier(BEZIER_TYPES.cubic); }
else{ retBezier = new Bezier(BEZIER_TYPES.quadratic); }
v1.x = this.p1.x;
v1.y = this.p1.y;
c = Math.max(0, Math.min(1, position)); // clamp for safe use in Stack Overflow answer
if(start === true){
retBezier.p1.x = this.p1.x;
retBezier.p1.y = this.p1.y;
}else{
retBezier.p2.x = this.p2.x;
retBezier.p2.y = this.p2.y;
}
if(this.cp2 === undefined){ // returns a quadratic
v2.x = this.cp1.x;
v2.y = this.cp1.y;
if(start){
retBezier.cp1.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp1.y = (v1.y += (v2.y - v1.y) * c);
v2.x += (this.p2.x - v2.x) * c;
v2.y += (this.p2.y - v2.y) * c;
retBezier.p2.x = v1.x + (v2.x - v1.x) * c;
retBezier.p2.y = v1.y + (v2.y - v1.y) * c;
retBezier.cp2 = undefined;
}else{
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
retBezier.cp1.x = (v2.x += (this.p2.x - v2.x) * c);
retBezier.cp1.y = (v2.y += (this.p2.y - v2.y) * c);
retBezier.p1.x = v1.x + (v2.x - v1.x) * c;
retBezier.p1.y = v1.y + (v2.y - v1.y) * c;
retBezier.cp2 = undefined;
}
return retBezier;
}
v2.x = this.cp1.x;
v3.x = this.cp2.x;
v2.y = this.cp1.y;
v3.y = this.cp2.y;
if(start){
retBezier.cp1.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp1.y = (v1.y += (v2.y - v1.y) * c);
v2.x += (v3.x - v2.x) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
v3.x += (this.p2.x - v3.x) * c;
v3.y += (this.p2.y - v3.y) * c;
retBezier.cp2.x = (v1.x += (v2.x - v1.x) * c);
retBezier.cp2.y = (v1.y += (v2.y - v1.y) * c);
retBezier.p2.y = v1.y + (v2.y - v1.y) * c;
retBezier.p2.x = v1.x + (v2.x - v1.x) * c;
}else{
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
v2.x += (v3.x - v2.x) * c;
v2.y += (v3.y - v2.y) * c;
retBezier.cp2.x = (v3.x += (this.p2.x - v3.x) * c);
retBezier.cp2.y = (v3.y += (this.p2.y - v3.y) * c);
v1.x += (v2.x - v1.x) * c;
v1.y += (v2.y - v1.y) * c;
retBezier.cp1.x = (v2.x += (v3.x - v2.x) * c);
retBezier.cp1.y = (v2.y += (v3.y - v2.y) * c);
retBezier.p1.x = v1.x + (v2.x - v1.x) * c;
retBezier.p1.y = v1.y + (v2.y - v1.y) * c;
}
return retBezier;
}
return {
Vec : Vec,
Bezier : Bezier,
bezierTypes : BEZIER_TYPES,
};
})();
// helper function
// Returns second order quadratic from points in the same order as most rendering api take then
// The second two coordinates x1,y1 are the control points
function createBezierQuadratic(x, y, x1, y1, x2, y2){
var b = new geom.Bezier(geom.bezierTypes.quadratic);
b.p1.set(x, y);
b.p2.set(x2, y2);
b.cp1.set(x1, y1);
return b;
}
// Returns third order cubic from points in the same order as most rendering api take then
// The coordinates x1, y1 and x2, y2 are the control points
function createBezierCubic(x, y, x1, y1, x2, y2, x3, y3){
var b = new geom.Bezier(geom.bezierTypes.cubic);
b.p1.set(x, y);
b.p2.set(x3, y3);
b.cp1.set(x1, y1);
b.cp2.set(x2, y2);
return b;
}
[Edit]
The algo for getting the length is still not working, it seems I forgot to calculate the last path, if someone wants to point me to the solution that would be very nice since I don't have time right now. (Otherwise, I'll try to find it in the weekend...)
Since you don't need support for older IE (<=11), one easy way is to use the setLineDash() method.
This will allow you to only draw your path once, and to only have to get the full length of your path.
To do so, I use a js implementation of this algo made by tunght13488. There may be better implementations of it.
var ctx = c.getContext('2d');
var percent = 90;
var length = 0;
// all our quadraticCurves points
var curves = [
[146, 146, 134, 118, 184, 103],
[217, 91, 269, 81, 271, 107],
[263, 155, 381, 158, 323, 173],
[279, 182, 314, 225, 281, 223],
[246, 219, 247, 274, 207, 236],
[177, 245, 133, 248, 137, 211],
[123, 238, 10, 145, 130, 150]
];
// get the full length of our spline
curves.forEach(function(c) {
length += quadraticBezierLength.apply(null, c);
});
// that's still not it...
length += quadraticBezierLength.apply(null,curves[curves.length-1]);
var anim = function() {
var offset = (percent / 100) * length;
ctx.clearRect(0, 0, c.width, c.height);
ctx.beginPath();
ctx.moveTo(133, 150);
// draw our splines
curves.forEach(function(c) {
ctx.bezierCurveTo.apply(ctx, c);
})
ctx.closePath();
// the non completed part
ctx.strokeStyle = "gray";
// this will make the part from 0 to offset non drawn
ctx.setLineDash([0, offset, length]);
ctx.stroke();
// the completed part
ctx.setLineDash([offset, length]);
ctx.strokeStyle = "blue";
ctx.stroke();
percent = (percent + .25) % 100;
requestAnimationFrame(anim);
}
// modified from https://gist.github.com/tunght13488/6744e77c242cc7a94859
function Point(x, y) {
this.x = x;
this.y = y;
}
function quadraticBezierLength(p0x, p0y, p1x, p1y, p2x, p2y) {
var a = new Point(
p0x - 2 * p1x + p2x,
p0y - 2 * p1y + p2y
);
var b = new Point(
2 * p1x - 2 * p0x,
2 * p1y - 2 * p0y
);
var A = 4 * (a.x * a.x + a.y * a.y);
var B = 4 * (a.x * b.x + a.y * b.y);
var C = b.x * b.x + b.y * b.y;
var Sabc = 2 * Math.sqrt(A + B + C);
var A_2 = Math.sqrt(A);
var A_32 = 2 * A * A_2;
var C_2 = 2 * Math.sqrt(C);
var BA = B / A_2;
return (A_32 * Sabc + A_2 * B * (Sabc - C_2) + (4 * C * A - B * B) * Math.log((2 * A_2 + BA + Sabc) / (BA + C_2))) / (4 * A_32);
};
anim();
<canvas width="500" height="500" id="c"></canvas>
To anyone still landing on this page, take a look at Bezier.js (https://github.com/Pomax/bezierjs), especially at the API: https://pomax.github.io/bezierjs/
You can extract a quadratic Bezier curve between t = 0.25 and t = 0.75 like so:
var curve = new Bezier(150,40 , 80,30 , 105,150);
var segment_curve = curve.split(0.25, 0.75);
context.moveTo(segment_curve.points[0].x, segment_curve.points[0].y);
context.quadraticCurveTo(segment_curve.points[1].x, segment_curve.points[1].y, segment_curve.points[2].x, segment_curve.points[2].y);
context.stroke();