@tsparticles/plugin-polygon-mask
Version:
tsParticles polygon mask plugin
395 lines (394 loc) • 17.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PolygonMaskInstance = void 0;
const engine_1 = require("@tsparticles/engine");
const utils_js_1 = require("./utils.js");
const PolygonMaskInlineArrangement_js_1 = require("./Enums/PolygonMaskInlineArrangement.js");
const PolygonMaskType_js_1 = require("./Enums/PolygonMaskType.js");
const noPolygonDataLoaded = `${engine_1.errorPrefix} No polygon data loaded.`, noPolygonFound = `${engine_1.errorPrefix} No polygon found, you need to specify SVG url in config.`, origin = {
x: 0,
y: 0,
}, half = 0.5, double = 2;
class PolygonMaskInstance {
constructor(container, engine) {
this._checkInsidePolygon = position => {
const container = this._container, options = container.actualOptions.polygon;
if (!options?.enable || options.type === PolygonMaskType_js_1.PolygonMaskType.none || options.type === PolygonMaskType_js_1.PolygonMaskType.inline) {
return true;
}
if (!this.raw) {
throw new Error(noPolygonFound);
}
const canvasSize = container.canvas.size, x = position?.x ?? (0, engine_1.getRandom)() * canvasSize.width, y = position?.y ?? (0, engine_1.getRandom)() * canvasSize.height, indexOffset = 1;
let inside = false;
for (let i = 0, j = this.raw.length - indexOffset; i < this.raw.length; j = i++) {
const pi = this.raw[i], pj = this.raw[j], 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;
}
}
if (options.type === PolygonMaskType_js_1.PolygonMaskType.inside) {
return inside;
}
else {
return options.type === PolygonMaskType_js_1.PolygonMaskType.outside ? !inside : false;
}
};
this._createPath2D = () => {
const container = this._container, options = container.actualOptions.polygon;
if (!options || !this.paths?.length) {
return;
}
for (const path of this.paths) {
const pathData = path.element?.getAttribute("d");
if (pathData) {
const path2d = new Path2D(pathData), matrix = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix(), finalPath = new Path2D(), transform = matrix.scale(this._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();
const firstIndex = 0, firstPoint = this.raw[firstIndex];
path.path2d.moveTo(firstPoint.x, firstPoint.y);
this.raw.forEach((pos, i) => {
if (i > firstIndex) {
path.path2d?.lineTo(pos.x, pos.y);
}
});
path.path2d.closePath();
}
};
this._downloadSvgPath = async (svgUrl, force) => {
const options = this._container.actualOptions.polygon;
if (!options) {
return;
}
const url = svgUrl ?? options.url, forceDownload = force ?? false;
if (!url || (this.paths !== undefined && !forceDownload)) {
return this.raw;
}
const req = await fetch(url);
if (!req.ok) {
throw new Error(`${engine_1.errorPrefix} occurred during polygon mask download`);
}
return this._parseSvgPath(await req.text(), force);
};
this._drawPoints = () => {
if (!this.raw) {
return;
}
for (const item of this.raw) {
void this._container.particles.addParticle({
x: item.x,
y: item.y,
});
}
};
this._getEquidistantPointByIndex = index => {
const container = this._container, options = container.actualOptions, polygonMaskOptions = options.polygon;
if (!polygonMaskOptions) {
return;
}
if (!this.raw?.length || !this.paths?.length) {
throw new Error(noPolygonDataLoaded);
}
let offset = 0, point;
const baseAccumulator = 0, totalLength = this.paths.reduce((tot, path) => tot + path.length, baseAccumulator), 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;
}
}
const scale = this._scale;
return {
x: (point?.x ?? origin.x) * scale + (this.offset?.x ?? origin.x),
y: (point?.y ?? origin.y) * scale + (this.offset?.y ?? origin.y),
};
};
this._getPointByIndex = index => {
if (!this.raw?.length) {
throw new Error(noPolygonDataLoaded);
}
const coords = this.raw[index % this.raw.length];
return {
x: coords.x,
y: coords.y,
};
};
this._getRandomPoint = () => {
if (!this.raw?.length) {
throw new Error(noPolygonDataLoaded);
}
const coords = (0, engine_1.itemFromArray)(this.raw);
return {
x: coords.x,
y: coords.y,
};
};
this._getRandomPointByLength = () => {
const container = this._container, options = container.actualOptions.polygon;
if (!options) {
return;
}
if (!this.raw?.length || !this.paths?.length) {
throw new Error(noPolygonDataLoaded);
}
const path = (0, engine_1.itemFromArray)(this.paths), offset = 1, distance = Math.floor((0, engine_1.getRandom)() * path.length) + offset, point = path.element.getPointAtLength(distance), scale = this._scale;
return {
x: point.x * scale + (this.offset?.x ?? origin.x),
y: point.y * scale + (this.offset?.y ?? origin.y),
};
};
this._initRawData = async (force) => {
const options = this._container.actualOptions.polygon;
if (!options) {
return;
}
if (options.url) {
this.raw = await this._downloadSvgPath(options.url, force);
}
else if (options.data) {
const data = options.data;
let svg;
if ((0, engine_1.isString)(data)) {
svg = data;
}
else {
const getPath = (p) => `<path d="${p}" />`, path = (0, engine_1.isArray)(data.path) ? data.path.map(getPath).join("") : getPath(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>`;
}
this.raw = this._parseSvgPath(svg, force);
}
this._createPath2D();
this._engine.dispatchEvent("polygonMaskLoaded", {
container: this._container,
});
};
this._parseSvgPath = (xml, force) => {
const forceDownload = force ?? false;
if (this.paths !== undefined && !forceDownload) {
return this.raw;
}
const container = this._container, options = container.actualOptions.polygon;
if (!options) {
return;
}
const parser = new DOMParser(), doc = parser.parseFromString(xml, "image/svg+xml"), firstIndex = 0, svg = doc.getElementsByTagName("svg")[firstIndex];
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 scale = this._scale;
this.dimension.width = parseFloat(svg.getAttribute("width") ?? "0") * scale;
this.dimension.height = parseFloat(svg.getAttribute("height") ?? "0") * scale;
const position = options.position ?? {
x: 50,
y: 50,
}, canvasSize = container.canvas.size;
this.offset = {
x: (canvasSize.width * position.x) / engine_1.percentDenominator - this.dimension.width * half,
y: (canvasSize.height * position.y) / engine_1.percentDenominator - this.dimension.height * half,
};
return (0, utils_js_1.parsePaths)(this.paths, scale, this.offset);
};
this._polygonBounce = (particle, delta, direction) => {
const options = this._container.actualOptions.polygon;
if (!this.raw || !options?.enable || direction !== engine_1.OutModeDirection.top) {
return false;
}
if (options.type === PolygonMaskType_js_1.PolygonMaskType.inside || options.type === PolygonMaskType_js_1.PolygonMaskType.outside) {
let closest, dx, dy;
const pos = particle.getPosition(), radius = particle.getRadius(), offset = 1;
for (let i = 0, j = this.raw.length - offset; i < this.raw.length; j = i++) {
const pi = this.raw[i], pj = this.raw[j];
closest = (0, utils_js_1.calcClosestPointOnSegment)(pi, pj, pos);
const dist = (0, engine_1.getDistances)(pos, closest);
[dx, dy] = [dist.dx, dist.dy];
if (dist.distance < radius) {
(0, utils_js_1.segmentBounce)(pi, pj, particle.velocity);
return true;
}
}
if (closest && dx !== undefined && dy !== undefined && !this._checkInsidePolygon(pos)) {
const factor = { x: 1, y: 1 }, diameter = radius * double, inverse = -1;
if (pos.x >= closest.x) {
factor.x = -1;
}
if (pos.y >= closest.y) {
factor.y = -1;
}
particle.position.x = closest.x + diameter * factor.x;
particle.position.y = closest.y + diameter * factor.y;
particle.velocity.mult(inverse);
return true;
}
}
else if (options.type === PolygonMaskType_js_1.PolygonMaskType.inline && particle.initialPosition) {
const dist = (0, engine_1.getDistance)(particle.initialPosition, particle.getPosition()), { velocity } = particle;
if (dist > this._moveRadius) {
velocity.x = velocity.y * half - velocity.x;
velocity.y = velocity.x * half - velocity.y;
return true;
}
}
return false;
};
this._randomPoint = () => {
const container = this._container, options = container.actualOptions.polygon;
if (!options) {
return;
}
let position;
if (options.type === PolygonMaskType_js_1.PolygonMaskType.inline) {
switch (options.inline.arrangement) {
case PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.randomPoint:
position = this._getRandomPoint();
break;
case PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.randomLength:
position = this._getRandomPointByLength();
break;
case PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.equidistant:
position = this._getEquidistantPointByIndex(container.particles.count);
break;
case PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.onePerPoint:
case PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.perPoint:
default:
position = this._getPointByIndex(container.particles.count);
}
}
else {
const canvasSize = container.canvas.size;
position = {
x: (0, engine_1.getRandom)() * canvasSize.width,
y: (0, engine_1.getRandom)() * canvasSize.height,
};
}
if (this._checkInsidePolygon(position)) {
return position;
}
else {
return this._randomPoint();
}
};
this._container = container;
this._engine = engine;
this.dimension = {
height: 0,
width: 0,
};
this._moveRadius = 0;
this._scale = 1;
}
clickPositionValid(position) {
const options = this._container.actualOptions.polygon;
return (!!options?.enable &&
options.type !== PolygonMaskType_js_1.PolygonMaskType.none &&
options.type !== PolygonMaskType_js_1.PolygonMaskType.inline &&
this._checkInsidePolygon(position));
}
draw(context) {
if (!this.paths?.length) {
return;
}
const options = this._container.actualOptions.polygon;
if (!options?.enable) {
return;
}
const polygonDraw = options.draw;
if (!polygonDraw.enable) {
return;
}
const rawData = this.raw;
for (const path of this.paths) {
const path2d = path.path2d;
if (!context) {
continue;
}
if (path2d && this.offset) {
(0, utils_js_1.drawPolygonMaskPath)(this._engine, context, path2d, polygonDraw.stroke, this.offset);
}
else if (rawData) {
(0, utils_js_1.drawPolygonMask)(this._engine, context, rawData, polygonDraw.stroke);
}
}
}
async init() {
const container = this._container, polygonMaskOptions = container.actualOptions.polygon, pxRatio = container.retina.pixelRatio;
if (!polygonMaskOptions) {
return;
}
this._moveRadius = polygonMaskOptions.move.radius * pxRatio;
this._scale = polygonMaskOptions.scale * pxRatio;
if (polygonMaskOptions.enable) {
await this._initRawData();
}
}
particleBounce(particle, delta, direction) {
return this._polygonBounce(particle, delta, direction);
}
particlePosition(position) {
const options = this._container.actualOptions.polygon, defaultLength = 0;
if (!(options?.enable && (this.raw?.length ?? defaultLength) > defaultLength)) {
return;
}
return (0, engine_1.deepExtend)({}, position ? position : this._randomPoint());
}
particlesInitialization() {
const options = this._container.actualOptions.polygon;
if (options?.enable &&
options.type === PolygonMaskType_js_1.PolygonMaskType.inline &&
(options.inline.arrangement === PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.onePerPoint ||
options.inline.arrangement === PolygonMaskInlineArrangement_js_1.PolygonMaskInlineArrangement.perPoint)) {
this._drawPoints();
return true;
}
return false;
}
resize() {
const container = this._container, options = container.actualOptions.polygon;
if (!(options?.enable && options.type !== PolygonMaskType_js_1.PolygonMaskType.none)) {
return;
}
if (this.redrawTimeout) {
clearTimeout(this.redrawTimeout);
}
const timeout = 250;
this.redrawTimeout = setTimeout(() => {
void (async () => {
await this._initRawData(true);
await container.particles.redraw();
})();
}, timeout);
}
stop() {
delete this.raw;
delete this.paths;
}
}
exports.PolygonMaskInstance = PolygonMaskInstance;