Intersection of 2 SVG Paths - javascript

I need to check if two SVG Path elements intersect. Checking for intersection of the bounding boxes with .getBBox() is too inaccurate.
What I'm currently doing is iterating both paths with .getTotalLength() and then checking if two points .getPointAtLength() are equal. Below is a snippet, but as you can see this is very slow and blocks the browser tab.
There must be a more efficient method to check for intersections between two paths.
var path1 = document.getElementById("p1");
var path2 = document.getElementById("p2");
var time = document.getElementById("time");
var btn = document.getElementById("start");
btn.addEventListener("click", getIntersection);
function getIntersection() {
var start = Date.now();
for (var i = 0; i < path1.getTotalLength(); i++) {
for (var j = 0; j < path2.getTotalLength(); j++) {
var point1 = path1.getPointAtLength(i);
var point2 = path2.getPointAtLength(j);
if (pointIntersect(point1, point2)) {
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}
}
}
}
function pointIntersect(p1, p2) {
p1.x = Math.round(p1.x);
p1.y = Math.round(p1.y);
p2.x = Math.round(p2.x);
p2.y = Math.round(p2.y);
return p1.x === p2.x && p1.y === p2.y;
}
svg {
fill: none;
stroke: black;
}
#start {
border: 1px solid;
display: inline-block;
position: absolute;
}
<div id="start">Start
</div>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 10 c 120 120 120 120 120 20 z" id="p1"></path>
<path d="M 150 10 c 120 120 120 120 120 20 z" id="p2"></path>
</svg>
<div id="time"></div>

I'm not sure but it may be possible to solve this mathematically if you could extract the vectors and curves from the paths. However, your function can be optimized by caching the points from one path, and reducing the number of calls to getTotalLength and getPointAtLength.
function getIntersection() {
var start = Date.now(),
path1Length = path1.getTotalLength(),
path2Length = path2.getTotalLength(),
path2Points = [];
for (var j = 0; j < path2Length; j++) {
path2Points.push(path2.getPointAtLength(j));
}
for (var i = 0; i < path1Length; i++) {
var point1 = path1.getPointAtLength(i);
for (var j = 0; j < path2Points.length; j++) {
if (pointIntersect(point1, path2Points[j])) {
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}
}
}
}
This can calculate the example paths in around 0.07 seconds instead of 4-5 seconds.
jsfiddle

time 0.027s
function getIntersection2() {
function Intersect(p1, p2) {
return p1.z!==p2.z && p1.x === p2.x && p1.y === p2.y;
}
var paths = [path1,path2];
var start = Date.now(),
pathLength = [path1.getTotalLength(),path2.getTotalLength()],
pathPoints = [],
inters = [];
for (var i = 0; i < 2; i++) {
for (var j = 0; j < pathLength[i]; j++) {
var p = paths[i].getPointAtLength(j);
p.z=i;
p.x=Math.round(p.x);
p.y=Math.round(p.y);
pathPoints.push(p);
}
}
pathPoints.sort((a,b)=>a.x!=b.x?a.x-b.x:a.y!=b.y?a.y-b.y:0)
// todos os pontos
.forEach((a,i,m)=>i&&Intersect(m[i-1],a)?inters.push([a.x,a.y]):0)
// somente o primeiro
//.some((a,i,m)=>i&&Intersect(m[i-1],a)?inters.push([a.x,a.y]):0);
result.innerHTML = inters;
var end = Date.now();
time.innerHTML = (end - start) / 1000 + "s";
return;
}

And this, while totally not being what the OP asked for, is what I was looking for.
A way to detect intersections over a large number of paths by sampling:
function pointIntersects(p1, p2) {
return (Math.abs(p1.x - p2.x) > 10)
? false
: (Math.abs(p1.y - p2.y) < 10)
}
function pointsIntersect(points, point) {
return _.some(points, p => pointIntersects(p, point))
}
function samplePathPoints(path) {
const pathLength = path.getTotalLength()
const points = []
for (let i = 0; i < pathLength; i += 10)
points.push(path.getPointAtLength(i))
return points
}
let pointCloud = []
_(document.querySelectorAll('svg path'))
.filter(path => {
const points = samplePathPoints(path)
if (_.some(pointCloud, point => pointsIntersect(points, point)))
return true
points.forEach(p => pointCloud.push(p))
})
.each(path => path.remove())
note: underscore/lodash has been used for brevity

You can further optimize performance by reducing the amount of sample points.
getPointAtLength() is rather expensive especially when run >100 times.
The following examples should usually need only a few milliseconds.
Example 1: Boolean result (intersecting true/false)
let svg = document.querySelector("svg");
function check() {
perfStart();
let intersections = checkPathIntersections(p0, p1, 24);
time.textContent = '1. stroke intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
//render indtersection point
gInter.innerHTML = '';
renderPoint(gInter, intersections[0], 'red', '2%');
}
function checkPathIntersections(path0, path1, checksPerPath = 24, threshold = 2) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 - threshold &&
top <= bottom1 - threshold &&
bottom >= top1 - threshold &&
right >= left1 - threshold ?
true :
false;
if (!bboxIntersection) {
return false;
}
// path0
let pathLength0 = path0.getTotalLength();
// set temporary stroke
let style0 = window.getComputedStyle(path0);
let fill0 = style0.fill;
let strokeWidth0 = style0.strokeWidth;
path0.style.strokeWidth = threshold;
// path1
let pathLength1 = path1.getTotalLength();
// set temporary stroke
let style1 = window.getComputedStyle(path1);
let fill1 = style1.fill;
let strokeWidth1 = style1.strokeWidth;
path1.style.strokeWidth = threshold;
/**
* 1. check sample point intersections
*/
let checks = 0;
let intersections = [];
/**
* 1.1 compare path0 against path1
*/
for (let c = 0; c < checksPerPath && !intersections.length; c++) {
let pt = path1.getPointAtLength((pathLength1 / checksPerPath) * c);
let inStroke = path0.isPointInStroke(pt);
let inFill = path0.isPointInFill(pt);
// check path 1 against path 2
if (inStroke || inFill) {
intersections.push(pt)
} else {
/**
* no intersections found:
* check path1 sample points against path0
*/
let pt1 = path0.getPointAtLength(
(pathLength0 / checksPerPath) * c
);
let inStroke1 = path1.isPointInStroke(pt1);
let inFill1 = path1.isPointInFill(pt1);
if (inStroke1 || inFill1) {
intersections.push(pt1)
}
}
// just for benchmarking
checks++;
}
// reset styles
path0.style.fill = fill0;
path0.style.strokeWidth = strokeWidth0;
path1.style.fill = fill1;
path1.style.strokeWidth = strokeWidth1;
console.log('sample point checks:', checks);
return intersections;
}
/**
* simple performance test
*/
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = "") {
t1 = performance.now();
total = t1 - t0;
console.log(`excecution time ${text}: ${total} ms`);
return total;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
svg {
fill: none;
stroke: black;
}
<p><button onclick="check()">Check intersection</button></p>
<svg xmlns="http://www.w3.org/2000/svg">
<path d="M 50 10 c 120 120 120 120 120 20 z" id="p0"></path>
<path d="M 150 10 c 120 120 120 120 120 20 z" id="p1"></path>
<g id="gInter"></g>
</svg>
<p id="time"></p>
How it works
1. Check BBox intersections
Checking for intersection of the bounding boxes with .getBBox() is too
inaccurate.
That's true, however we should always start with a bbox intersection test to avoid unnecessary calculations.
2. Check intersection via isPointInStroke() and isPointInFill()
These natively supported methods are well optimized so we don't need to compare retrieved point arrays against each other.
By increasing the stroke-width of the paths we can also increase the tolerance threshold for intersections.
3. Reduce sample points
If we don't need all intersecting points but only a boolean value, we can drastically reduce the number of intersection checks by creating them progressively within the testing loop.
Once we found any intersection (in stroke or fill) – we stop the loop and return true.
Besides we can usually reduce the number by splitting the path length in e.g 24-100 steps.
Example 2: get all intersection points
let svg = document.querySelector("svg");
let paths = svg.querySelectorAll("path");
function check() {
// reset results
gInter2.innerHTML = '';
gInter.innerHTML = '';
time.textContent = '';
/**
* Boolean check
*/
perfStart();
let intersections = checkPathIntersections(p0, p1, 24);
time.textContent += '1. stroke intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections[0]);
perfStart();
let intersections1 = checkPathIntersections(p2, p3, 24);
time.textContent += '2. fill intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections1[0])
/**
* Precise check
*/
perfStart();
let intersections3 = checkIntersectionPrecise(p4, p5, 100, 1);
time.textContent += '3. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections3.length) {
intersections3.forEach(p => {
renderPoint(svg, p, 'red')
})
}
// no bbox intersection
perfStart();
let intersections4 = checkPathIntersections(p5, p6, 24);
time.textContent += '4. no bbBox intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
perfStart();
let intersections5 = checkIntersectionPrecise(p8, p9, 1200, 0);
time.textContent += '5. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections5.length) {
intersections5.forEach(p => {
renderPoint(gInter2, p, 'green', '0.25%');
})
}
}
function checkIntersectionPrecise(path0, path1, split = 1000, decimals = 0) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 &&
top <= bottom1 &&
bottom >= top1 &&
right >= left1 ?
true :
false;
if (!bboxIntersection) {
console.log('no intersections at all');
return false;
}
// path0
let pathData0 = path0.getPathData({
normalize: true
})
let points0 = pathDataToPolygonPoints(pathData0, true, split);
let points0Strings = points0.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
// filter duplicates
points0Strings = [...new Set(points0Strings)];
// path1
let pathLength1 = path1.getTotalLength();
let pathData1 = path1.getPathData({
normalize: true
})
let points1 = pathDataToPolygonPoints(pathData1, true, split);
let points1Strings = points1.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
points1Strings = [...new Set(points1Strings)];
// 1. compare
let intersections = [];
let intersectionsFilter = [];
for (let i = 0; i < points0Strings.length; i++) {
let p0Str = points0Strings[i];
let index = points1Strings.indexOf(p0Str);
if (index !== -1) {
let p1 = p0Str.split('_');
intersections.push({
x: +p1[0],
y: +p1[1]
});
}
}
// filter nearby points
if (intersections.length) {
intersectionsFilter = [intersections[0]];
let length = intersections.length;
for (let i = 1; i < length; i += 1) {
let p = intersections[i];
let pPrev = intersections[i - 1];
let diffX = Math.abs(pPrev.x - p.x);
let diffY = Math.abs(pPrev.y - p.y);
let diff = diffX + diffY;
if (diff > 1) {
intersectionsFilter.push(p)
}
}
} else {
return false
}
return intersectionsFilter;
}
/**
* convert path d to polygon point array
*/
function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
let points = [];
pathData.forEach((com, c) => {
let type = com.type;
let values = com.values;
let valL = values.length;
// optional splitting
let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
let split = splitStep;
// M
if (c === 0) {
let M = {
x: pathData[0].values[valL - 2],
y: pathData[0].values[valL - 1]
};
points.push(M);
}
if (valL && c > 0) {
let prev = pathData[c - 1];
let prevVal = prev.values;
let prevValL = prevVal.length;
let p0 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
};
// cubic curves
if (type === "C") {
if (prevValL) {
let cp1 = {
x: values[valL - 6],
y: values[valL - 5]
};
let cp2 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// linetos
else if (type === "L") {
if (splitLines) {
//let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
let p1 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
}
let p2 = {
x: values[valL - 2],
y: values[valL - 1]
}
if (addControlPointsMid && split) {
for (let s = 0; split < 1; s++) {
let midPoint = interpolatedPoint(p1, p2, split);
points.push(midPoint);
split += splitStep
}
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
});
return points;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t=0.5) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
function checkPathIntersections(path0, path1, checksPerPath = 24, threshold = 2) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 - threshold &&
top <= bottom1 - threshold &&
bottom >= top1 - threshold &&
right >= left1 - threshold ?
true :
false;
if (!bboxIntersection) {
return false;
}
// path0
let pathLength0 = path0.getTotalLength();
// set temporary stroke
let style0 = window.getComputedStyle(path0);
let fill0 = style0.fill;
let strokeWidth0 = style0.strokeWidth;
path0.style.strokeWidth = threshold;
// path1
let pathLength1 = path1.getTotalLength();
// set temporary stroke
let style1 = window.getComputedStyle(path1);
let fill1 = style1.fill;
let strokeWidth1 = style1.strokeWidth;
path1.style.strokeWidth = threshold;
/**
* 1. check sample point intersections
*/
let checks = 0;
let intersections = [];
/**
* 1.1 compare path0 against path1
*/
for (let c = 0; c < checksPerPath && !intersections.length; c++) {
let pt = path1.getPointAtLength((pathLength1 / checksPerPath) * c);
let inStroke = path0.isPointInStroke(pt);
let inFill = path0.isPointInFill(pt);
// check path 1 against path 2
if (inStroke || inFill) {
intersections.push(pt)
} else {
/**
* no intersections found:
* check path1 sample points against path0
*/
let pt1 = path0.getPointAtLength(
(pathLength0 / checksPerPath) * c
);
let inStroke1 = path1.isPointInStroke(pt1);
let inFill1 = path1.isPointInFill(pt1);
if (inStroke1 || inFill1) {
intersections.push(pt1)
}
}
// just for benchmarking
checks++;
}
// reset styles
path0.style.fill = fill0;
path0.style.strokeWidth = strokeWidth0;
path1.style.fill = fill1;
path1.style.strokeWidth = strokeWidth1;
console.log('sample point checks:', checks);
return intersections;
}
/**
* simple performance test
*/
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = "") {
t1 = performance.now();
total = t1 - t0;
console.log(`excecution time ${text}: ${total} ms`);
return total;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
body {
font-family: sans-serif;
}
svg {
width: 100%;
}
path {
fill: none;
stroke: #000;
stroke-width: 1px;
}
p {
white-space: pre-line;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 150">
<path id="p0" d="M27.357,21.433c13.373,3.432,21.433,17.056,18.001,30.43
c-3.432,13.374-17.057,21.434-30.43,18.002" />
<path id="p1" d="M80.652,80.414c-12.205,6.457-27.332,1.8-33.791-10.403
c-6.458-12.204-1.8-27.333,10.404-33.791" />
<path id="p2"
d="M159.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p3"
d="M191.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p4"
d="M259.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p5"
d="M291.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p6"
d="M359.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p7"
d="M420 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<g id="gInter"></g>
</svg>
<p>Based on #Netsi1964's codepen:
https://codepen.io/netsi1964/pen/yKagwx/</p>
<svg id="svg2" viewBox="0 0 2000 700">
<path d=" M 529 664 C 93 290 616 93 1942 385 C 1014 330 147 720 2059 70 C 1307 400 278 713 1686 691 " style="stroke:orange!important" stroke="orange" id="p8"/>
<path d=" M 1711 363 C 847 15 1797 638 1230 169 C 1198 443 1931 146 383 13 C 1103 286 1063 514 521 566 " id="p9"/>
<g id="gInter2"></g>
</svg>
<p><button onclick="check()">Check intersection</button></p>
<p id="time"></p>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
This example calculates sample points from a parsed pathData array - retrieved with getPathData() (needs a polyfill).
All commands are normalized/converted via
path.getPathData({normalize:true})
to absolute coordinates using only M,C,L and Z.
We can now easily calculate points on bézier C commands with an interpolation helper.
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t=0.5) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
p0 = previous commands last point
cp1 = first C control point
cp2 = second control point
p = C control end point
t = split position: t=0.5 => middle of the curve
Admittedly, quite a chunk of code.
However way faster for calculating hundreds of sample points than using getPointAtLength().
Codepen example.

Related

How to visualize Fourier series / Fourier coefficients?

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

SVG & JS - Light stopped by obstacle

I'm actually working on a little game and i'm trying to block map reveal with obstacle. Currently i have this :
And i of course want this :
I tried SAT.js library which let me know when there is a collision but i don't really know what yo do with this. My first idea was to create a black (or white just for hide) polygon behind the obstacle but i'm sure the is a better solution.
For this "light" effect i use trick like this in SVG :
<clippath id="clips" >
<path class="view" tokenname="" d="M 1 1000 L 250 1 L 500 1000 z"/>
</clippath>
<image xlink:href="https://i.pinimg.com/originals/46/3a/12/463a1244e2e2627c53ff9806e2012c84.jpg" width="1920" height="1080" x="0" y="0" class="one"/>
<image xlink:href="https://i.pinimg.com/originals/46/3a/12/463a1244e2e2627c53ff9806e2012c84.jpg" width="1920" height="1080" x="0" y="0" clip-path="url(#clips)"/>
Thank you for your help.
The next demo is inspired by this tutorial: 2D Raycasting;
The main idea is using the white rays as a mask for your image. Please change the stroke-width in css for a dimmer / clearer image.
Please move the mouse over the svg canvas to see it changing.
Also read the comments in the code and do not forget to see Daniel Shiffman's tutorial.
let SVG_NS = "http://www.w3.org/2000/svg";
let SVG_XLINK = "http://www.w3.org/1999/xlink";
let svg = document.querySelector("svg");
let m = { x: 0, y: 0 };// the initial mouse position
let record = 600;//the maxim length of a ray
let walls = [];//the array of the walls
//list of points for the boundary (walls)
let p1 = {
x: 300,
y: 100
};
let p2 = {
x: 200,
y: 300
};
let p3 = {
x: 10,
y: 300
};
let p4 = {
x: 300,
y: 200
};
class Particle {
constructor(pos) {
this.pos = pos;
this.rays = [];
for (let a = 0; a < 2 * Math.PI; a += Math.PI / 360) {
this.rays.push(new Ray(this.pos, a));
}
// Uncomment to visualize the particle
//let o = { cx: this.pos.x, cy: this.pos.y, r: 2, fill: "red" };
//this.element = drawSVGelmt(o, "circle", svg);
}
show(m) {
//update the position of the mouse
this.update(m.x, m.y);
//empty the group of rays
rys.innerHTML = "";
// Uncomment to visualize the particle
//let o = { cx: this.pos.x, cy: this.pos.y };
//this.element = updateSVGElmt(o, this.element);
//first cast the rays
for (let i = 0; i < this.rays.length; i++) {
for (let w = 0; w < walls.length; w++) {
this.rays[i].cast(walls[w]);
}
}
//next draw the rays and append them to the rys group
for (let i = 0; i < this.rays.length; i++) {
//if the ray is intersecting one of the walls
if (this.rays[i].intersection) {
//set the attributes of the ray line
var l = {};
l.x1 = this.pos.x;
l.y1 = this.pos.y;
l.x2 = this.rays[i].intersection.x;
l.y2 = this.rays[i].intersection.y;
//draw ray and append it to the rys group
this.line = drawSVGelmt(l, "line", rys);
}
}
}
update(x, y) {
//update all the rays inside the rys
this.rays.map((r) => {
r.update();
});
//reset the position of the particle
this.pos.x = x;
this.pos.y = y;
}
}
class Ray {
constructor(pos, a) {
this.pos = pos;//the starting point
this.angle = a;//the angle of the ray
this.maxLength = record;
this.dir = {//the direction of a ray with an initial length of 1 unit
x: Math.cos(this.angle),
y: Math.sin(this.angle)
};
}
cast(wall) {//cast the ray against the wall
let p4 = {};
p4.x = this.pos.x + this.dir.x;
p4.y = this.pos.y + this.dir.y;
// see if the ray is intersecting the wall
let Intersection = Intersect(wall.a, wall.b, this.pos, p4);
if (Intersection) {
let length = dist(this.pos, Intersection);
if (length < this.maxLength) {
this.maxLength = length;
this.intersection = Intersection;
}
}
}
// update the ray when the mouse (m) is moving
update() {
this.pos = { x: m.x, y: m.y };
this.intersection = false;
this.maxLength = record;
}
}
//the walls
class Boundary {
constructor(a, b) {
this.a = a;
this.b = b;
}
show() {
// the attributes for the line
let o = {};
o.x1 = this.a.x;
o.y1 = this.a.y;
o.x2 = this.b.x;
o.y2 = this.b.y;
o.class = "boundary";// a class to style the walls
//draw and append the wall line
this.line = drawSVGelmt(o, "line", wls);
}
}
walls.push(new Boundary(p1, p2));
walls.push(new Boundary(p3, p4));
walls.forEach((w) => {
w.show();
});
let p = new Particle(m);
p.show(m);
//HELPERS
// a function to get the intersection point of 2 lines, Returns the point of intersection or false if there is no intersection point
function Intersect(p1, p2, p3, p4) {
var denominator =
(p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);
var ua =
((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) /
denominator;
var ub =
((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) /
denominator;
var x = p1.x + ua * (p2.x - p1.x);
var y = p1.y + ua * (p2.y - p1.y);
if (ua > 0 && ua < 1 && ub > 0 /*&& ub < 1*/) {
return { x: x, y: y };
} else {
return false;
}
}
// a function to draw an svg element
function drawSVGelmt(o, tag, parent) {
let elmt = document.createElementNS(SVG_NS, tag);
for (let name in o) {
if (o.hasOwnProperty(name)) {
elmt.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(elmt);
return elmt;
}
// a function to update an svg element
function updateSVGElmt(o, element) {
for (var name in o) {
if (o.hasOwnProperty(name)) {
element.setAttribute(name, o[name]);
}
}
return element;
}
//a function to get the angle of a line from p1 to p2
function getAngle(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let angle = Math.atan2(dy, dx);
return angle < 0 ? 2 * Math.PI + angle : angle;
}
//a function to get the distance between 2 points: p1 & p2
function dist(p1, p2) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
//a function to get the mouse position inside an svg element
function oMousePosSVG(e) {
let p = svg.createSVGPoint();
p.x = e.clientX;
p.y = e.clientY;
let ctm = svg.getScreenCTM().inverse();
p = p.matrixTransform(ctm);
return p;
}
svg.addEventListener("mousemove", function (e) {
m = oMousePosSVG(e);
p.show(m);
});
*{margin:0;padding:0;}
body{background:black;}
svg{border:1px solid silver;width:min(100vw,100vh)}
line{stroke:white;stroke-width:1px}
.boundary{stroke:white;stroke-width:2px}
<svg viewBox="0 0 400 400">
<g id="wls"></g>
<mask id="m">
<g id="rys"></g>
</mask>
<image xlink:href="https://assets.codepen.io/222579/darwin300.jpg" height="400" width="400" mask="url(#m)"></image>
</svg>

Determine bounding box of svg element from string in javascript

If I load an SVG in chrome, I am able to get bounding box of it's elements like so:
document.getElementById('char1').getBBox()
->
SVGRect {x: 348.5628356933594, y: 78.95916748046875, width: 202.74807739257812, height: 845.1696166992188}
However if i load the same SVG using DOMParser, i get a bounding box with 0 values.
EG:
const svgString = document.documentElement.outerHTML // in practice this would be an svg fetched from elsewhere
let svgElement = new DOMParser().parseFromString(svgString, 'text/xml').documentElement
svgElement.getElementById('char1').getBBox()
->
SVGRect {x: 0, y: 0, width: 0, height: 0}
a) why does this happen?
b) how do i get the correct bounding box value by using the svg string and without loading it into the document?
It isn't as pretty as it ought to be, but the solution is to temporarily add, measure, then remove it from the DOM. Because execution never returns while running these three lines of JS, the element won't actually appear on the page. Therefore, this should most likely have no side effects (unless, perhaps, you're doing something on the body with mutation observers or similar).
const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
const bounds = svgElement.getBBox();
toRemove?.remove();
As commented by #Robert Longson: getBBox() requires your element to be appended to DOM. Temporarily appending a svg (and eventually removing it) is usually the most convenient approach.
If you can't do this ...
Calculate approximated bounding box from pathData
We can get a bounding box approximation by calculating points from a pathData array
parse pathData via (polyfilled) getPathData() method. We also use the normalize:true parameter to convert all commands to absolute coordinates and concert A (arcs) to c (cubic béziers)
loop through command array to get segment end points as well as interpolated points on curves via custom pathDataToPolygonPoints() helper
We need to calculate interpolated points on curve segments to get x/y extrema.
The more interpolated points are calculated - the higher the accuracy.
loop through point array to find min/max x and y coordinates
let svgMarkup = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<path fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
</svg>`;
let svg = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml').querySelector('svg');
let path = svg.querySelector('path');
let pathData = path.getPathData({
normalize: true
});
// init
upDataBBox();
inputAccuracy.addEventListener('input', e => {
upDataBBox()
})
function upDataBBox() {
/**
* calculate points from pathData
* interpolate additional points for curve segements
* to increase accuracy
*/
let accuracy = +inputAccuracy.value;
let polypoints = pathDataToPolygonPoints(pathData, true, accuracy);
// approximated bbox
let polyBBox = getPolygonBBox(polypoints);
// compare with getBBox()
let bb = pathInline.getBBox();
let bbox = {
x: bb.x,
y: bb.y,
width: bb.width,
height: bb.height
};
bboxOrig.textContent = beautify(JSON.stringify(bbox));
bboxPoly.textContent = beautify(JSON.stringify(polyBBox));
points.innerHTML = '';
polypoints.forEach(point => {
renderPoint(points, point)
})
}
function beautify(str) {
str = str.replaceAll('{', '').replaceAll('}', '').split(',').join('\n')
return str
}
function getPolygonBBox(polyPoints) {
let xArr = [];
let yArr = [];
polyPoints.forEach(point => {
xArr.push(point.x);
yArr.push(point.y);
})
let xmin = Math.min(...xArr);
let xmax = Math.max(...xArr);
let ymin = Math.min(...yArr);
let ymax = Math.max(...yArr);
return {
x: xmin,
y: ymin,
width: xmax - xmin,
height: ymax - ymin
}
}
/**
* convert path d to polygon point array
*/
function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
let points = [];
// close path fix
pathData = addClosePathLineto(pathData);
pathData.forEach((com, c) => {
let type = com.type;
let values = com.values;
let valL = values.length;
// optional splitting
let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
let split = splitStep;
// M
if (c === 0) {
let M = {
x: pathData[0].values[valL - 2],
y: pathData[0].values[valL - 1]
};
points.push(M);
}
if (valL && c > 0) {
let prev = pathData[c - 1];
let prevVal = prev.values;
let prevValL = prevVal.length;
let p0 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
};
// cubic curves
if (type === "C") {
if (prevValL) {
let cp1 = {
x: values[valL - 6],
y: values[valL - 5]
};
let cp2 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// quadratic curves
else if (type === "Q") {
if (prevValL) {
let cp1 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
//let coords = prevCoords.concat(values);
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtQuadraticSegmentLength(p0, cp1, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// linetos
else if (type === "L") {
if (splitLines) {
//let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
let p1 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
}
let p2 = {
x: values[valL - 2],
y: values[valL - 1]
}
if (addControlPointsMid && split) {
for (let s = 0; split < 1; s++) {
let midPoint = interpolatedPoint(p1, p2, split);
points.push(midPoint);
split += splitStep
}
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
});
return points;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
function getPointAtQuadraticSegmentLength(p0, cp1, p, t = 0.5) {
let t1 = 1 - t;
return {
x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
}
}
/**
* Add closing lineto:
* needed for path reversing or adding points
*/
function addClosePathLineto(pathData) {
let pathDataL = pathData.length;
let closed = pathData[pathDataL - 1]["type"] == "Z" ? true : false;
let M = pathData[0];
let [x0, y0] = [M.values[0], M.values[1]];
let lastCom = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
let lastComL = lastCom.values.length;
let [xE, yE] = [lastCom.values[lastComL - 2], lastCom.values[lastComL - 1]];
if (closed && (x0 != xE || y0 != yE)) {
//console.log('add final lineto')
pathData.pop();
pathData.push({
type: "L",
values: [x0, y0]
}, {
type: "Z",
values: []
});
}
return pathData;
}
/**
* render point
* accepts coordinate array and point object
**/
function renderPoint(svg, coords, fill = "red", r = "2") {
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
body {
font-family: sans-serif
}
svg {
overflow: visible;
border: 1px solid #ccc;
width: 20em;
}
p {
white-space: pre-line;
}
<p>Accuracy: <input type="range" id="inputAccuracy" min="1" max="10" steps="1" value="1"></p>
<p><strong>BBox exact: </strong>
<span id="bboxOrig"></span></p>
<p><strong>BBox poly:</strong>
<span id="bboxPoly"></span></p>
<svg id="svgInline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="36.3 40.73 385.5 461.5">
<path id="pathInline" fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
<g id="points"></g>
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill#1.0.4/path-data-polyfill.min.js"></script>
This approach works pretty well for all geometry elements, since getpathData() can also retrieve a pathData array from primitives:
<circle>, <ellipse>,
<polygon>, <polyline>
<rect>, <line>
Doesn't work for <text> or <use> elements.

p5.js - Low FPS for some basic animations

I'm having really bad performance on a project i wrote in Javascript (with the p5.js library)
Here is the code:
const fps = 60;
const _width = 400;
const _height = 300;
const firePixelChance = 1;
const coolingRate = 1;
const heatSourceSize = 10;
const noiseIncrement = 0.02;
const fireColor = [255, 100, 0, 255];
const bufferWidth = _width;
const bufferHeight = _height;
let buffer1;
let buffer2;
let coolingBuffer;
let ystart = 0.0;
function setup() {
createCanvas(_width, _height);
frameRate(fps);
buffer1 = createGraphics(bufferWidth, bufferHeight);
buffer2 = createGraphics(bufferWidth, bufferHeight);
coolingBuffer = createGraphics(bufferWidth, bufferHeight);
}
// Draw a line at the bottom
function heatSource(buffer, rows, _color) {
const start = bufferHeight - rows;
for (let x = 0; x < bufferWidth; x++) {
for (let y = start; y < bufferHeight; y++) {
if(Math.random() >= firePixelChance)
continue;
buffer.pixels[(x + (y * bufferWidth)) * 4] = _color[0]; // Red
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = _color[1]; // Green
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = _color[2]; // Blue
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = 255; // Alpha
}
}
}
// Produces the 'smoke'
function coolingMap(buffer){
let xoff = 0.0;
for(x = 0; x < bufferWidth; x++){
xoff += noiseIncrement;
yoff = ystart;
for(y = 0; y < bufferHeight; y++){
yoff += noiseIncrement;
n = noise(xoff, yoff);
bright = pow(n, 3) * 20;
buffer.pixels[(x + (y * bufferWidth)) * 4] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +1] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +2] = bright;
buffer.pixels[(x + (y * bufferWidth)) * 4 +3] = bright;
}
}
ystart += noiseIncrement;
}
// Change color of a pixel so it looks like its smooth
function smoothing(buffer, _buffer2, _coolingBuffer) {
for (let x = 0; x < bufferWidth; x++) {
for (let y = 0; y < bufferHeight; y++) {
// Get all 4 neighbouring pixels
const left = getColorFromPixelPosition(x+1,y,buffer.pixels);
const right = getColorFromPixelPosition(x-1,y,buffer.pixels);
const bottom = getColorFromPixelPosition(x,y+1,buffer.pixels);
const top = getColorFromPixelPosition(x,y-1,buffer.pixels);
// Set this pixel to the average of those neighbours
let sumRed = left[0] + right[0] + bottom[0] + top[0];
let sumGreen = left[1] + right[1] + bottom[1] + top[1];
let sumBlue = left[2] + right[2] + bottom[2] + top[2];
let sumAlpha = left[3] + right[3] + bottom[3] + top[3];
// "Cool down" color
const coolingMapColor = getColorFromPixelPosition(x,y,_coolingBuffer.pixels)
sumRed = (sumRed / 4) - (Math.random() * coolingRate) - coolingMapColor[0];
sumGreen = (sumGreen / 4) - (Math.random() * coolingRate) - coolingMapColor[1];
sumBlue = (sumBlue / 4) - (Math.random() * coolingRate) - coolingMapColor[2];
sumAlpha = (sumAlpha / 4) - (Math.random() * coolingRate) - coolingMapColor[3];
// Make sure we dont get negative numbers
sumRed = sumRed > 0 ? sumRed : 0;
sumGreen = sumGreen > 0 ? sumGreen : 0;
sumBlue = sumBlue > 0 ? sumBlue : 0;
sumAlpha = sumAlpha > 0 ? sumAlpha : 0;
// Update this pixel
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4] = sumRed; // Red
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +1] = sumGreen; // Green
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +2] = sumBlue; // Blue
_buffer2.pixels[(x + ((y-1) * bufferWidth)) * 4 +3] = sumAlpha; // Alpha
}
}
}
function draw() {
background(0);
text("FPS: "+Math.floor(frameRate()), 10, 20);
fill(0,255,0,255);
buffer1.loadPixels();
buffer2.loadPixels();
coolingBuffer.loadPixels();
heatSource(buffer1, heatSourceSize, fireColor);
coolingMap(coolingBuffer);
smoothing(buffer1, buffer2, coolingBuffer);
buffer1.updatePixels();
buffer2.updatePixels();
coolingBuffer.updatePixels();
let temp = buffer1;
buffer1 = buffer2;
buffer2 = temp;
image(buffer2, 0, 0); // Draw buffer to screen
// image(coolingBuffer, 0, bufferHeight); // Draw buffer to screen
}
function mousePressed() {
buffer1.fill(fireColor);
buffer1.noStroke();
buffer1.ellipse(mouseX, mouseY, 100, 100);
}
function getColorFromPixelPosition(x, y, pixels) {
let _color = [];
for (let i = 0; i < 4; i++)
_color[i] = pixels[(x + (y * bufferWidth)) * 4 + i];
return _color;
}
function getRandomColorValue() {
return Math.floor(Math.random() * 255);
}
I'm getting ~12 FPS on chrome and ~1 FPS on any other browser and i cant figure out why..
Resizing my canvas to make it bigger also impacts the fps negatively...
In the devtools performance tab i noticed that both my smoothing and coolingMap functions are the things slowing it down, but i cant figure out what part of them are so heavy..
You've pretty much answered this for yourself already:
i'm starting to think this is normal and i should work on caching stuff and maybe use pixel groups instead of single pixels
Like you're discovering, doing some calculation for every single pixel is pretty slow. Computers only have finite resources, and there's going to be a limit to what you can throw at them.
In your case, you might consider drawing the whole thing to a canvas once at startup, and then moving the canvas up over the life of the program.

Canvas problems. Not able to reproduce design

I need to build canvas animation like design requires. I spend almost 3 days but I'm not able to do anything like in design. Here a REQUESTED design!. And here - what I've got for now: current implementation which definitely not what requested from design .I need only animation of planet from particles at background (also whole process of animation changes in time, it starts from few particles but then amount growing and movings directions of particles changes)
here my current code:
export class CanvasComponent implements OnInit {
sphereRad = 280;
radius_sp = 1;
distance = 600;
particle_size = 0.7;
constructor() { }
ngOnInit() {
this.canvasApp();
}
canvasApp () {
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let displayWidth;
let displayHeight;
let wait;
let count;
let numToAddEachFrame;
let particleList;
let recycleBin;
let particleAlpha;
let r, g, b;
let fLen;
let m;
let projCenterX;
let projCenterY;
let zMax;
let turnAngle;
let turnSpeed;
let sphereCenterX, sphereCenterY, sphereCenterZ;
let particleRad;
let zeroAlphaDepth;
let randAccelX, randAccelY, randAccelZ;
let gravity;
let rgbString;
// we are defining a lot of letiables used in the screen update functions globally so that they don't have to be redefined every frame.
let p;
let outsideTest;
let nextParticle;
let sinAngle;
let cosAngle;
let rotX, rotZ;
let depthAlphaFactor;
let i;
let theta, phi;
let x0, y0, z0;
// INITIALLI
const init = () => {
wait = 1;
count = wait - 1;
numToAddEachFrame = 30;
// particle color
r = 255;
g = 255;
b = 255;
rgbString = 'rgba(' + r + ',' + g + ',' + b + ','; // partial string for color which will be completed by appending alpha value.
particleAlpha = 1; // maximum alpha
displayWidth = canvas.width;
displayHeight = canvas.height;
fLen = this.distance; // represents the distance from the viewer to z=0 depth.
// projection center coordinates sets location of origin
projCenterX = displayWidth / 2;
projCenterY = displayHeight / 2;
// we will not draw coordinates if they have too large of a z-coordinate (which means they are very close to the observer).
zMax = fLen - 2;
particleList = {};
recycleBin = {};
// random acceleration factors - causes some random motion
randAccelX = 0.1;
randAccelY = 0.1;
randAccelZ = 0.1;
gravity = -0; // try changing to a positive number (not too large, for example 0.3), or negative for floating upwards.
particleRad = this.particle_size;
sphereCenterX = 0;
sphereCenterY = 0;
sphereCenterZ = -3 - this.sphereRad;
// alpha values will lessen as particles move further back, causing depth-based darkening:
zeroAlphaDepth = 0;
turnSpeed = 2 * Math.PI / 1200; // the sphere will rotate at this speed (one complete rotation every 1600 frames).
turnAngle = 0; // initial angle
// timer = setInterval(onTimer, 10 / 24);
onTimer();
}
const onTimer = () => {
// if enough time has elapsed, we will add new particles.
count++;
if (count >= wait) {
count = 0;
for (i = 0; i < numToAddEachFrame; i++) {
theta = Math.random() * 2 * Math.PI;
phi = Math.acos(Math.random() * 2 - 1);
x0 = this.sphereRad * Math.sin(phi) * Math.cos(theta);
y0 = this.sphereRad * Math.sin(phi) * Math.sin(theta);
z0 = this.sphereRad * Math.cos(phi);
// We use the addParticle function to add a new particle. The parameters set the position and velocity components.
// Note that the velocity parameters will cause the particle to initially fly outwards away from the sphere center (after
// it becomes unstuck).
const p = addParticle(x0, sphereCenterY + y0, sphereCenterZ + z0, 0.002 * x0, 0.002 * y0, 0.002 * z0);
// we set some 'envelope' parameters which will control the evolving alpha of the particles.
p.attack = 50;
p.hold = 50;
p.decay = 100;
p.initValue = 0;
p.holdValue = particleAlpha;
p.lastValue = 0;
// the particle will be stuck in one place until this time has elapsed:
p.stuckTime = 90 + Math.random() * 20;
p.accelX = 0;
p.accelY = gravity;
p.accelZ = 0;
}
}
// update viewing angle
turnAngle = (turnAngle + turnSpeed) % (2 * Math.PI);
sinAngle = Math.sin(turnAngle);
cosAngle = Math.cos(turnAngle);
// background fill
context.fillStyle = '#000000';
context.fillRect(0, 0, displayWidth, displayHeight);
// update and draw particles
p = particleList.first;
while (p != null) {
// before list is altered record next particle
nextParticle = p.next;
// update age
p.age++;
// if the particle is past its 'stuck' time, it will begin to move.
if (p.age > p.stuckTime) {
p.velX += p.accelX + randAccelX * (Math.random() * 2 - 1);
p.velY += p.accelY + randAccelY * (Math.random() * 2 - 1);
p.velZ += p.accelZ + randAccelZ * (Math.random() * 2 - 1);
p.x += p.velX;
p.y += p.velY;
p.z += p.velZ;
}
/*
We are doing two things here to calculate display coordinates.
The whole display is being rotated around a vertical axis, so we first calculate rotated coordinates for
x and z (but the y coordinate will not change).
Then, we take the new coordinates (rotX, y, rotZ), and project these onto the 2D view plane.
*/
rotX = cosAngle * p.x + sinAngle * (p.z - sphereCenterZ);
rotZ = -sinAngle * p.x + cosAngle * (p.z - sphereCenterZ) + sphereCenterZ;
// m = this.radius_sp * fLen / (fLen - rotZ);
m = this.radius_sp;
p.projX = rotX * m + projCenterX;
p.projY = p.y * m + projCenterY;
p.projZ = rotZ * m + projCenterX;
// update alpha according to envelope parameters.
if (p.age < p.attack + p.hold + p.decay) {
if (p.age < p.attack) {
p.alpha = (p.holdValue - p.initValue) / p.attack * p.age + p.initValue;
} else if (p.age < p.attack + p.hold) {
p.alpha = p.holdValue;
} else if (p.age < p.attack + p.hold + p.decay) {
p.alpha = (p.lastValue - p.holdValue) / p.decay * (p.age - p.attack - p.hold) + p.holdValue;
}
} else {
p.dead = true;
}
// see if the particle is still within the viewable range.
if ((p.projX > displayWidth) || (p.projX < 0) || (p.projY < 0) || (p.projY > displayHeight) || (rotZ > zMax)) {
outsideTest = true;
} else {
outsideTest = false;
}
if (outsideTest || p.dead ||
(p.projX > displayWidth / (2 + (1 - Math.random())) && p.projZ + displayWidth * 0.1 > displayWidth / 2) ||
(p.projX < displayWidth / (2 - (1 - Math.random())) && p.projZ + displayWidth * 0.25 < displayWidth / 2)
) {
recycle(p);
} else {
// depth-dependent darkening
// console.log(turnAngle, rotZ)
depthAlphaFactor = 1;
// depthAlphaFactor = (1 - (1.5 + rotZ / 100));
depthAlphaFactor = (depthAlphaFactor > 1) ? 1 : ((depthAlphaFactor < 0) ? 0 : depthAlphaFactor);
context.fillStyle = rgbString + depthAlphaFactor * p.alpha + ')';
// draw
context.beginPath();
context.arc(p.projX, p.projY, m * particleRad, 0, 2 * Math.PI, false);
context.closePath();
context.fill();
}
p = nextParticle;
}
window.requestAnimationFrame(onTimer);
}
const addParticle = (x0, y0, z0, vx0, vy0, vz0) => {
let newParticle;
// const color;
// check recycle bin for available drop:
if (recycleBin.first != null) {
newParticle = recycleBin.first;
// remove from bin
if (newParticle.next != null) {
recycleBin.first = newParticle.next;
newParticle.next.prev = null;
} else {
recycleBin.first = null;
}
} else {
newParticle = {};
}
// if the recycle bin is empty, create a new particle (a new empty object):
// add to beginning of particle list
if (particleList.first == null) {
particleList.first = newParticle;
newParticle.prev = null;
newParticle.next = null;
} else {
newParticle.next = particleList.first;
particleList.first.prev = newParticle;
particleList.first = newParticle;
newParticle.prev = null;
}
// initialize
newParticle.x = x0;
newParticle.y = y0;
newParticle.z = z0;
newParticle.velX = vx0;
newParticle.velY = vy0;
newParticle.velZ = vz0;
newParticle.age = 0;
newParticle.dead = false;
if (Math.random() < 0.5) {
newParticle.right = true;
} else {
newParticle.right = false;
}
return newParticle;
}
const recycle = (p) => {
// remove from particleList
if (particleList.first === p) {
if (p.next != null) {
p.next.prev = null;
particleList.first = p.next;
} else {
particleList.first = null;
}
} else {
if (p.next == null) {
p.prev.next = null;
} else {
p.prev.next = p.next;
p.next.prev = p.prev;
}
}
// add to recycle bin
if (recycleBin.first == null) {
recycleBin.first = p;
p.prev = null;
p.next = null;
} else {
p.next = recycleBin.first;
recycleBin.first.prev = p;
recycleBin.first = p;
p.prev = null;
}
};
init();
}
}
So I will be happy with any help also REWARD(for full implementation) is possible (ETH, BTC any currency you wish).

Categories

Resources