Skip to content

Commit

Permalink
feat: interactivity options overrides in particles options, closes #4120
Browse files Browse the repository at this point in the history
  • Loading branch information
matteobruni committed May 24, 2022
1 parent 0678014 commit 309afb5
Show file tree
Hide file tree
Showing 17 changed files with 100 additions and 58 deletions.
3 changes: 2 additions & 1 deletion engine/src/Core/Interfaces/IExternalInteractor.ts
@@ -1,14 +1,15 @@
import type { ClickMode } from "../../Enums/Modes/ClickMode";
import type { IDelta } from "./IDelta";
import type { IInteractor } from "./IInteractor";
import type { Particle } from "../Particle";

/**
* @category Interfaces
*/
export interface IExternalInteractor extends IInteractor {
handleClickMode?: (mode: ClickMode | string) => void;

isEnabled(): boolean;
isEnabled(particle?: Particle): boolean;

interact(delta: IDelta): Promise<void>;
}
2 changes: 2 additions & 0 deletions engine/src/Core/Interfaces/IParticle.ts
Expand Up @@ -12,6 +12,7 @@ import type { IParticleWobble } from "./IParticleWobble";
import type { IParticlesOptions } from "../../Options/Interfaces/Particles/IParticlesOptions";
import type { IShapeValues } from "./IShapeValues";
import type { IStroke } from "../../Options/Interfaces/Particles/IStroke";
import type { Interactivity } from "../../Options/Classes/Interactivity/Interactivity";
import type { Vector } from "../Utils/Vector";

export interface IParticle {
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface IParticle {
readonly strokeWidth?: number;
readonly velocity: Vector;
readonly retina: IParticleRetinaProps;
readonly interactivity: Interactivity;

getPosition(): ICoordinates3d;

Expand Down
8 changes: 8 additions & 0 deletions engine/src/Core/Particle.ts
Expand Up @@ -37,6 +37,7 @@ import type { IParticleWobble } from "./Interfaces/IParticleWobble";
import type { IParticlesOptions } from "../Options/Interfaces/Particles/IParticlesOptions";
import type { IShape } from "../Options/Interfaces/Particles/Shape/IShape";
import type { IShapeValues } from "./Interfaces/IShapeValues";
import { Interactivity } from "../Options/Classes/Interactivity/Interactivity";
import { MoveDirection } from "../Enums/Directions/MoveDirection";
import { ParticleOutType } from "../Enums/Types/ParticleOutType";
import type { RecursivePartial } from "../Types/RecursivePartial";
Expand Down Expand Up @@ -275,6 +276,8 @@ export class Particle implements IParticle {
*/
readonly retina: IParticleRetinaProps;

readonly interactivity: Interactivity;

/**
* Gets the particle containing engine instance
* @private
Expand Down Expand Up @@ -341,6 +344,11 @@ export class Particle implements IParticle {
particlesOptions.load(this.shapeData?.particles);
}

this.interactivity = new Interactivity();

this.interactivity.load(container.actualOptions.interactivity);
this.interactivity.load(particlesOptions.interactivity);

this.fill = this.shapeData?.fill ?? this.fill;
this.close = this.shapeData?.close ?? this.close;
this.options = particlesOptions;
Expand Down
5 changes: 3 additions & 2 deletions engine/src/Core/Utils/ExternalInteractorBase.ts
Expand Up @@ -28,13 +28,14 @@ export abstract class ExternalInteractorBase implements IExternalInteractor {

/**
* Interaction enabled check
* @param particle the particle to check, if null, checks the container
* @returns true or false, checking if the options enable this interaction manager
*/
abstract isEnabled(): boolean;
abstract isEnabled(particle?: Particle): boolean;

/**
* Before interaction reset
* @param particle the particle to be reset
* @param particle the particle to reset
*/
abstract reset(particle: Particle): void;
}
38 changes: 26 additions & 12 deletions engine/src/Core/Utils/QuadTree.ts
Expand Up @@ -91,20 +91,27 @@ export class QuadTree {
* Queries the instance using a [[Circle]] object, with the given position and the given radius
* @param position the circle position
* @param radius the circle radius
* @param check the function to check if the particle can be added to the result
* @returns the particles inside the given circle
*/
queryCircle(position: ICoordinates, radius: number): Particle[] {
return this.query(new Circle(position.x, position.y, radius));
queryCircle(position: ICoordinates, radius: number, check?: (particle: Particle) => boolean): Particle[] {
return this.query(new Circle(position.x, position.y, radius), check);
}

/**
* Queries the instance using a [[CircleWarp]] object, with the given position and the given radius
* @param position the circle position
* @param radius the circle radius
* @param containerOrSize the container canvas size
* @param check the function to check if the particle can be added to the result
* @returns the particles inside the given circle
*/
queryCircleWarp(position: ICoordinates, radius: number, containerOrSize: Container | IDimension): Particle[] {
queryCircleWarp(
position: ICoordinates,
radius: number,
containerOrSize: Container | IDimension,
check?: (particle: Particle) => boolean
): Particle[] {
const container = containerOrSize as Container,
size = containerOrSize as IDimension;

Expand All @@ -114,46 +121,53 @@ export class QuadTree {
position.y,
radius,
container.canvas !== undefined ? container.canvas.size : size
)
),
check
);
}

/**
* Queries the instance using a [[Rectangle]] object, with the given position and the given size
* @param position the rectangle position
* @param size the rectangle size
* @param check the function to check if the particle can be added to the result
* @returns the particles inside the given rectangle
*/
queryRectangle(position: ICoordinates, size: IDimension): Particle[] {
return this.query(new Rectangle(position.x, position.y, size.width, size.height));
queryRectangle(position: ICoordinates, size: IDimension, check?: (particle: Particle) => boolean): Particle[] {
return this.query(new Rectangle(position.x, position.y, size.width, size.height), check);
}

/**
* Queries the instance using a [[Rectangle]] object, with the given position and the given size
* @param range the range to use for querying the tree
* @param check the function to check if the particle can be added to the result
* @param found found particles array, output parameter
* @returns the particles inside the given range
*/
query(range: Range, found?: Particle[]): Particle[] {
query(range: Range, check?: (particle: Particle) => boolean, found?: Particle[]): Particle[] {
const res = found ?? [];

if (!range.intersects(this.rectangle)) {
return [];
}

for (const p of this.points) {
if (!range.contains(p.position) && getDistance(range.position, p.position) > p.particle.getRadius()) {
if (
!range.contains(p.position) &&
getDistance(range.position, p.position) > p.particle.getRadius() &&
(!check || check(p.particle))
) {
continue;
}

res.push(p.particle);
}

if (this.divided) {
this.northEast?.query(range, res);
this.northWest?.query(range, res);
this.southEast?.query(range, res);
this.southWest?.query(range, res);
this.northEast?.query(range, check, res);
this.northWest?.query(range, check, res);
this.southEast?.query(range, check, res);
this.southWest?.query(range, check, res);
}

return res;
Expand Down
6 changes: 6 additions & 0 deletions engine/src/Options/Classes/Particles/ParticlesOptions.ts
Expand Up @@ -2,6 +2,7 @@ import { AnimatableColor } from "../AnimatableColor";
import { AnimatableGradient } from "../Gradients/AnimatableGradient";
import { Collisions } from "./Collisions/Collisions";
import { Destroy } from "./Destroy/Destroy";
import type { IInteractivity } from "../../Interfaces/Interactivity/IInteractivity";
import type { IOptionLoader } from "../../Interfaces/IOptionLoader";
import type { IParticlesOptions } from "../../Interfaces/Particles/IParticlesOptions";
import { Life } from "./Life/Life";
Expand Down Expand Up @@ -38,6 +39,7 @@ export class ParticlesOptions implements IParticlesOptions, IOptionLoader<IParti
destroy;
gradient: SingleOrMultiple<AnimatableGradient>;
groups: ParticlesGroups;
interactivity?: RecursivePartial<IInteractivity>;
life;
links;
move;
Expand Down Expand Up @@ -174,6 +176,10 @@ export class ParticlesOptions implements IParticlesOptions, IOptionLoader<IParti

this.collisions.load(data.collisions);

if (data.interactivity !== undefined) {
this.interactivity = deepExtend({}, data.interactivity) as RecursivePartial<IInteractivity>;
}

const strokeToLoad = data.stroke ?? data.shape?.stroke;

if (strokeToLoad) {
Expand Down
3 changes: 3 additions & 0 deletions engine/src/Options/Interfaces/Particles/IParticlesOptions.ts
Expand Up @@ -6,6 +6,7 @@ import type { IAnimatableColor } from "../IAnimatableColor";
import type { IAnimatableGradient } from "../IAnimatableGradient";
import type { ICollisions } from "./Collisions/ICollisions";
import type { IDestroy } from "./Destroy/IDestroy";
import type { IInteractivity } from "../Interactivity/IInteractivity";
import type { ILife } from "./Life/ILife";
import type { ILinks } from "./Links/ILinks";
import type { IMove } from "./Move/IMove";
Expand All @@ -25,6 +26,7 @@ import type { ITwinkle } from "./Twinkle/ITwinkle";
import type { IWobble } from "./Wobble/IWobble";
import type { IZIndex } from "./ZIndex/IZIndex";
import type { ParticlesGroups } from "../../../Types/ParticlesGroups";
import type { RecursivePartial } from "../../../Types/RecursivePartial";
import type { SingleOrMultiple } from "../../../Types/SingleOrMultiple";

/**
Expand All @@ -48,6 +50,7 @@ export interface IParticlesOptions {
destroy: IDestroy;
gradient: SingleOrMultiple<IAnimatableGradient>;
groups: ParticlesGroups;
interactivity?: RecursivePartial<IInteractivity>;
life: ILife;
links: ILinks;
move: IMove;
Expand Down
18 changes: 11 additions & 7 deletions interactions/external/attract/src/Attractor.ts
Expand Up @@ -11,10 +11,10 @@ import {
isInArray,
mouseMoveEvent,
} from "tsparticles-engine";
import type { Container, ICoordinates, IParticle } from "tsparticles-engine";
import type { Container, ICoordinates, Particle } from "tsparticles-engine";

interface IContainerAttract {
particles: IParticle[];
particles: Particle[];
finish?: boolean;
count?: number;
clicking?: boolean;
Expand Down Expand Up @@ -53,6 +53,10 @@ export class Attractor extends ExternalInteractorBase {
container.attract.count = 0;

for (const particle of container.attract.particles) {
if (!this.isEnabled(particle)) {
continue;
}

particle.velocity.setTo(particle.initialVelocity);
}

Expand All @@ -71,11 +75,11 @@ export class Attractor extends ExternalInteractorBase {
};
}

isEnabled(): boolean {
isEnabled(particle?: Particle): boolean {
const container = this.container,
options = container.actualOptions,
mouse = container.interactivity.mouse,
events = options.interactivity.events;
events = (particle?.interactivity ?? options.interactivity).events;

if ((!mouse.position || !events.onHover.enable) && (!mouse.clickPosition || !events.onClick.enable)) {
return false;
Expand Down Expand Up @@ -122,9 +126,9 @@ export class Attractor extends ExternalInteractorBase {
}

private processAttract(position: ICoordinates, attractRadius: number, area: Range): void {
const container = this.container;
const attractOptions = container.actualOptions.interactivity.modes.attract;
const query = container.particles.quadTree.query(area);
const container = this.container,
attractOptions = container.actualOptions.interactivity.modes.attract,
query = container.particles.quadTree.query(area, (p) => this.isEnabled(p));

for (const particle of query) {
const { dx, dy, distance } = getDistances(particle.position, position);
Expand Down
8 changes: 4 additions & 4 deletions interactions/external/bounce/src/Bouncer.ts
Expand Up @@ -17,18 +17,18 @@ import {
mouseMoveEvent,
rectBounce,
} from "tsparticles-engine";
import type { Container, ICoordinates } from "tsparticles-engine";
import type { Container, ICoordinates, Particle } from "tsparticles-engine";

export class Bouncer extends ExternalInteractorBase {
constructor(container: Container) {
super(container);
}

isEnabled(): boolean {
isEnabled(particle?: Particle): boolean {
const container = this.container,
options = container.actualOptions,
mouse = container.interactivity.mouse,
events = options.interactivity.events,
events = (particle?.interactivity ?? options.interactivity).events,
divs = events.onDiv;

return (
Expand Down Expand Up @@ -101,7 +101,7 @@ export class Bouncer extends ExternalInteractorBase {
}

private processBounce(position: ICoordinates, radius: number, area: Range): void {
const query = this.container.particles.quadTree.query(area);
const query = this.container.particles.quadTree.query(area, (p) => this.isEnabled(p));

for (const particle of query) {
if (area instanceof Circle) {
Expand Down
10 changes: 5 additions & 5 deletions interactions/external/bubble/src/Bubbler.ts
Expand Up @@ -77,11 +77,11 @@ export class Bubbler extends ExternalInteractorBase {
};
}

isEnabled(): boolean {
isEnabled(particle?: Particle): boolean {
const container = this.container,
options = container.actualOptions,
mouse = container.interactivity.mouse,
events = options.interactivity.events,
events = (particle?.interactivity ?? options.interactivity).events,
divs = events.onDiv,
divBubble = isDivModeEnabled(DivMode.bubble, divs);

Expand Down Expand Up @@ -154,7 +154,7 @@ export class Bubbler extends ExternalInteractorBase {
elem.offsetWidth * pxRatio,
elem.offsetHeight * pxRatio
),
query = container.particles.quadTree.query(area);
query = container.particles.quadTree.query(area, (p) => this.isEnabled(p));

for (const particle of query) {
if (!area.contains(particle.getPosition())) {
Expand Down Expand Up @@ -257,7 +257,7 @@ export class Bubbler extends ExternalInteractorBase {
}

const distance = container.retina.bubbleModeDistance,
query = container.particles.quadTree.queryCircle(mouseClickPos, distance);
query = container.particles.quadTree.queryCircle(mouseClickPos, distance, (p) => this.isEnabled(p));

for (const particle of query) {
if (!container.bubble.clicking) {
Expand Down Expand Up @@ -326,7 +326,7 @@ export class Bubbler extends ExternalInteractorBase {
}

const distance = container.retina.bubbleModeDistance,
query = container.particles.quadTree.queryCircle(mousePos, distance);
query = container.particles.quadTree.queryCircle(mousePos, distance, (p) => this.isEnabled(p));

//for (const { distance, particle } of query) {
for (const particle of query) {
Expand Down
8 changes: 4 additions & 4 deletions interactions/external/connect/src/Connector.ts
@@ -1,5 +1,5 @@
import type { Container, Particle } from "tsparticles-engine";
import { ExternalInteractorBase, HoverMode, isInArray } from "tsparticles-engine";
import type { Container } from "tsparticles-engine";

/**
* Particle connection manager
Expand All @@ -10,10 +10,10 @@ export class Connector extends ExternalInteractorBase {
super(container);
}

isEnabled(): boolean {
isEnabled(particle?: Particle): boolean {
const container = this.container,
mouse = container.interactivity.mouse,
events = container.actualOptions.interactivity.events;
events = (particle?.interactivity ?? container.actualOptions.interactivity).events;

if (!(events.onHover.enable && mouse.position)) {
return false;
Expand Down Expand Up @@ -41,7 +41,7 @@ export class Connector extends ExternalInteractorBase {
}

const distance = Math.abs(container.retina.connectModeRadius),
query = container.particles.quadTree.queryCircle(mousePos, distance);
query = container.particles.quadTree.queryCircle(mousePos, distance, (p) => this.isEnabled(p));

let i = 0;

Expand Down

0 comments on commit 309afb5

Please sign in to comment.