tsparticles
Version:
Easily create highly customizable particle, confetti and fireworks animations and use them as animated backgrounds for your website. Ready to use components available also for React, Vue.js (2.x and 3.x), Angular, Svelte, jQuery, Preact, Riot.js, Inferno.
405 lines (404 loc) • 16.5 kB
JavaScript
import { calcClosestPtOnSegment, drawPolygonMask, drawPolygonMaskPath, parsePaths, segmentBounce } from "./Utils";
import { deepExtend, getDistance, getDistances, itemFromArray } from "../../Utils";
import { Constants } from "../../Core";
import { PolygonMask } from "./Options/Classes/PolygonMask";
/**
* Polygon Mask manager
* @category Polygon Mask Plugin
*/
export class PolygonMaskInstance {
constructor(container) {
this.container = container;
this.dimension = {
height: 0,
width: 0,
};
this.path2DSupported = !!window.Path2D;
this.options = new PolygonMask();
this.polygonMaskMoveRadius = this.options.move.radius * container.retina.pixelRatio;
}
async initAsync(options) {
this.options.load(options === null || options === void 0 ? void 0 : options.polygon);
const polygonMaskOptions = this.options;
this.polygonMaskMoveRadius = polygonMaskOptions.move.radius * this.container.retina.pixelRatio;
/* If is set the url of svg element, load it and parse into raw polygon data */
if (polygonMaskOptions.enable) {
await this.initRawData();
}
}
resize() {
const container = this.container;
const options = this.options;
if (!(options.enable && options.type !== "none" /* none */)) {
return;
}
if (this.redrawTimeout) {
clearTimeout(this.redrawTimeout);
}
this.redrawTimeout = window.setTimeout(async () => {
await this.initRawData(true);
await container.particles.redraw();
}, 250);
}
stop() {
delete this.raw;
delete this.paths;
}
particlesInitialization() {
const options = this.options;
if (options.enable &&
options.type === "inline" /* inline */ &&
(options.inline.arrangement === "one-per-point" /* onePerPoint */ ||
options.inline.arrangement === "per-point" /* perPoint */)) {
this.drawPoints();
return true;
}
return false;
}
particlePosition(position) {
var _a, _b;
const options = this.options;
if (!(options.enable && ((_b = (_a = this.raw) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0)) {
return;
}
return deepExtend({}, position ? position : this.randomPoint());
}
particleBounce(particle, delta, direction) {
return this.polygonBounce(particle, delta, direction);
}
clickPositionValid(position) {
const options = this.options;
return (options.enable &&
options.type !== "none" /* none */ &&
options.type !== "inline" /* inline */ &&
this.checkInsidePolygon(position));
}
draw(context) {
var _a;
if (!((_a = this.paths) === null || _a === void 0 ? void 0 : _a.length)) {
return;
}
const options = this.options;
const polygonDraw = options.draw;
if (!(options.enable && polygonDraw.enable)) {
return;
}
const rawData = this.raw;
for (const path of this.paths) {
const path2d = path.path2d;
const path2dSupported = this.path2DSupported;
if (!context) {
continue;
}
if (path2dSupported && path2d && this.offset) {
drawPolygonMaskPath(context, path2d, polygonDraw.stroke, this.offset);
}
else if (rawData) {
drawPolygonMask(context, rawData, polygonDraw.stroke);
}
}
}
polygonBounce(particle, _delta, direction) {
const options = this.options;
if (!this.raw || !options.enable || direction !== "top" /* top */) {
return false;
}
if (options.type === "inside" /* inside */ || options.type === "outside" /* outside */) {
let closest, dx, dy;
const pos = particle.getPosition(), radius = particle.getRadius();
for (let i = 0, j = this.raw.length - 1; i < this.raw.length; j = i++) {
const pi = this.raw[i], pj = this.raw[j];
closest = calcClosestPtOnSegment(pi, pj, pos);
const dist = getDistances(pos, closest);
[dx, dy] = [dist.dx, dist.dy];
if (dist.distance < radius) {
segmentBounce(pi, pj, particle.velocity);
return true;
}
}
if (closest && dx !== undefined && dy !== undefined && !this.checkInsidePolygon(pos)) {
const factor = { x: 1, y: 1 };
if (particle.position.x >= closest.x) {
factor.x = -1;
}
if (particle.position.y >= closest.y) {
factor.y = -1;
}
particle.position.x = closest.x + radius * 2 * factor.x;
particle.position.y = closest.y + radius * 2 * factor.y;
particle.velocity.mult(-1);
return true;
}
}
else if (options.type === "inline" /* inline */ && particle.initialPosition) {
const dist = getDistance(particle.initialPosition, particle.getPosition());
if (dist > this.polygonMaskMoveRadius) {
particle.velocity.x = particle.velocity.y / 2 - particle.velocity.x;
particle.velocity.y = particle.velocity.x / 2 - particle.velocity.y;
return true;
}
}
return false;
}
checkInsidePolygon(position) {
var _a, _b;
const container = this.container;
const options = this.options;
if (!options.enable || options.type === "none" /* none */ || options.type === "inline" /* inline */) {
return true;
}
// https://github.com/substack/point-in-polygon
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
if (!this.raw) {
throw new Error(Constants.noPolygonFound);
}
const canvasSize = container.canvas.size;
const x = (_a = position === null || position === void 0 ? void 0 : position.x) !== null && _a !== void 0 ? _a : Math.random() * canvasSize.width;
const y = (_b = position === null || position === void 0 ? void 0 : position.y) !== null && _b !== void 0 ? _b : Math.random() * canvasSize.height;
let inside = false;
// if (this.path2DSupported && this.polygonPath && position) {
// inside = container.canvas.isPointInPath(this.polygonPath, position);
// } else {
for (let i = 0, j = this.raw.length - 1; i < this.raw.length; j = i++) {
const pi = this.raw[i];
const pj = this.raw[j];
const intersect = pi.y > y !== pj.y > y && x < ((pj.x - pi.x) * (y - pi.y)) / (pj.y - pi.y) + pi.x;
if (intersect) {
inside = !inside;
}
}
// }
return options.type === "inside" /* inside */
? inside
: options.type === "outside" /* outside */
? !inside
: false;
}
parseSvgPath(xml, force) {
var _a, _b, _c;
const forceDownload = force !== null && force !== void 0 ? force : false;
if (this.paths !== undefined && !forceDownload) {
return this.raw;
}
const container = this.container;
const options = this.options;
const parser = new DOMParser();
const doc = parser.parseFromString(xml, "image/svg+xml");
const svg = doc.getElementsByTagName("svg")[0];
let svgPaths = svg.getElementsByTagName("path");
if (!svgPaths.length) {
svgPaths = doc.getElementsByTagName("path");
}
this.paths = [];
for (let i = 0; i < svgPaths.length; i++) {
const path = svgPaths.item(i);
if (path) {
this.paths.push({
element: path,
length: path.getTotalLength(),
});
}
}
const pxRatio = container.retina.pixelRatio;
const scale = options.scale / pxRatio;
this.dimension.width = parseFloat((_a = svg.getAttribute("width")) !== null && _a !== void 0 ? _a : "0") * scale;
this.dimension.height = parseFloat((_b = svg.getAttribute("height")) !== null && _b !== void 0 ? _b : "0") * scale;
const position = (_c = options.position) !== null && _c !== void 0 ? _c : {
x: 50,
y: 50,
};
/* centering of the polygon mask */
this.offset = {
x: (container.canvas.size.width * position.x) / (100 * pxRatio) - this.dimension.width / 2,
y: (container.canvas.size.height * position.y) / (100 * pxRatio) - this.dimension.height / 2,
};
return parsePaths(this.paths, scale, this.offset);
}
/**
* Deprecate SVGPathElement.getPathSegAtLength removed in:
* Chrome for desktop release 62
* Chrome for Android release 62
* Android WebView release 62
* Opera release 49
* Opera for Android release 49
*/
async downloadSvgPath(svgUrl, force) {
const options = this.options;
const url = svgUrl || options.url;
const forceDownload = force !== null && force !== void 0 ? force : false;
// Load SVG from file on server
if (!url || (this.paths !== undefined && !forceDownload)) {
return this.raw;
}
const req = await fetch(url);
if (!req.ok) {
throw new Error("tsParticles Error - Error occurred during polygon mask download");
}
return this.parseSvgPath(await req.text(), force);
}
drawPoints() {
if (!this.raw) {
return;
}
for (const item of this.raw) {
this.container.particles.addParticle({
x: item.x,
y: item.y,
});
}
}
randomPoint() {
const container = this.container;
const options = this.options;
let position;
if (options.type === "inline" /* inline */) {
switch (options.inline.arrangement) {
case "random-point" /* randomPoint */:
position = this.getRandomPoint();
break;
case "random-length" /* randomLength */:
position = this.getRandomPointByLength();
break;
case "equidistant" /* equidistant */:
position = this.getEquidistantPointByIndex(container.particles.count);
break;
case "one-per-point" /* onePerPoint */:
case "per-point" /* perPoint */:
default:
position = this.getPointByIndex(container.particles.count);
}
}
else {
position = {
x: Math.random() * container.canvas.size.width,
y: Math.random() * container.canvas.size.height,
};
}
if (this.checkInsidePolygon(position)) {
return position;
}
else {
return this.randomPoint();
}
}
getRandomPoint() {
if (!this.raw || !this.raw.length) {
throw new Error(Constants.noPolygonDataLoaded);
}
const coords = itemFromArray(this.raw);
return {
x: coords.x,
y: coords.y,
};
}
getRandomPointByLength() {
var _a, _b, _c;
const options = this.options;
if (!this.raw || !this.raw.length || !((_a = this.paths) === null || _a === void 0 ? void 0 : _a.length)) {
throw new Error(Constants.noPolygonDataLoaded);
}
const path = itemFromArray(this.paths);
const distance = Math.floor(Math.random() * path.length) + 1;
const point = path.element.getPointAtLength(distance);
return {
x: point.x * options.scale + (((_b = this.offset) === null || _b === void 0 ? void 0 : _b.x) || 0),
y: point.y * options.scale + (((_c = this.offset) === null || _c === void 0 ? void 0 : _c.y) || 0),
};
}
getEquidistantPointByIndex(index) {
var _a, _b, _c, _d, _e, _f, _g;
const options = this.container.actualOptions;
const polygonMaskOptions = this.options;
if (!this.raw || !this.raw.length || !((_a = this.paths) === null || _a === void 0 ? void 0 : _a.length))
throw new Error(Constants.noPolygonDataLoaded);
let offset = 0;
let point;
const totalLength = this.paths.reduce((tot, path) => tot + path.length, 0);
const distance = totalLength / options.particles.number.value;
for (const path of this.paths) {
const pathDistance = distance * index - offset;
if (pathDistance <= path.length) {
point = path.element.getPointAtLength(pathDistance);
break;
}
else {
offset += path.length;
}
}
return {
x: ((_b = point === null || point === void 0 ? void 0 : point.x) !== null && _b !== void 0 ? _b : 0) * polygonMaskOptions.scale + ((_d = (_c = this.offset) === null || _c === void 0 ? void 0 : _c.x) !== null && _d !== void 0 ? _d : 0),
y: ((_e = point === null || point === void 0 ? void 0 : point.y) !== null && _e !== void 0 ? _e : 0) * polygonMaskOptions.scale + ((_g = (_f = this.offset) === null || _f === void 0 ? void 0 : _f.y) !== null && _g !== void 0 ? _g : 0),
};
}
getPointByIndex(index) {
if (!this.raw || !this.raw.length) {
throw new Error(Constants.noPolygonDataLoaded);
}
const coords = this.raw[index % this.raw.length];
return {
x: coords.x,
y: coords.y,
};
}
createPath2D() {
var _a, _b;
const options = this.options;
if (!this.path2DSupported || !((_a = this.paths) === null || _a === void 0 ? void 0 : _a.length)) {
return;
}
for (const path of this.paths) {
const pathData = (_b = path.element) === null || _b === void 0 ? void 0 : _b.getAttribute("d");
if (pathData) {
const path2d = new Path2D(pathData);
const matrix = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix();
const finalPath = new Path2D();
const transform = matrix.scale(options.scale);
if (finalPath.addPath) {
finalPath.addPath(path2d, transform);
path.path2d = finalPath;
}
else {
delete path.path2d;
}
}
else {
delete path.path2d;
}
if (path.path2d || !this.raw) {
continue;
}
path.path2d = new Path2D();
path.path2d.moveTo(this.raw[0].x, this.raw[0].y);
this.raw.forEach((pos, i) => {
var _a;
if (i > 0) {
(_a = path.path2d) === null || _a === void 0 ? void 0 : _a.lineTo(pos.x, pos.y);
}
});
path.path2d.closePath();
}
}
async initRawData(force) {
const options = this.options;
if (options.url) {
this.raw = await this.downloadSvgPath(options.url, force);
}
else if (options.data) {
const data = options.data;
let svg;
if (typeof data !== "string") {
const path = data.path instanceof Array
? data.path.map((t) => `<path d="${t}" />`).join("")
: `<path d="${data.path}" />`;
const namespaces = 'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"';
svg = `<svg ${namespaces} width="${data.size.width}" height="${data.size.height}">${path}</svg>`;
}
else {
svg = data;
}
this.raw = this.parseSvgPath(svg, force);
}
this.createPath2D();
}
}