Replaced tsparticles with pixi-emitter

This commit is contained in:
thepaperpilot 2022-04-01 01:01:52 -05:00
parent 69468d88c0
commit ecc4b9fb5f
4 changed files with 879 additions and 848 deletions

1546
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,11 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"particles.vue3": "^2.0.3", "pixi.js": "^6.3.0",
"tsparticles": "^2.0.3",
"vue": "^3.2.26", "vue": "^3.2.26",
"vue-next-select": "^2.10.2", "vue-next-select": "^2.10.2",
"vue-panzoom": "^1.1.6", "vue-panzoom": "^1.1.6",

View file

@ -1,89 +1,74 @@
onMounted, onMounted,
<template> <template>
<Particles <div
:id="id" ref="resizeListener"
:class="{ class="resize-listener"
'not-fullscreen': !fullscreen :style="unref(style)"
}" :class="unref(classes)"
:style="{
zIndex
}"
ref="particles"
:particlesInit="particlesInit"
:particlesLoaded="particlesLoaded"
:options="{
fpsLimit: 60,
fullScreen: { enable: fullscreen, zIndex },
particles: {
number: {
value: 0
}
},
emitters: {
autoPlay: false
}
}"
v-bind="$attrs"
/> />
<div ref="resizeListener" class="resize-listener" />
</template> </template>
<script lang="tsx"> <script lang="tsx">
import { loadFull } from "tsparticles"; import { StyleValue } from "features/feature";
import { Engine, Container } from "tsparticles-engine";
import { Emitters } from "tsparticles-plugin-emitters/Emitters";
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
import { defineComponent, inject, nextTick, onMounted, PropType, ref } from "vue";
import { ParticlesComponent } from "particles.vue3";
import { FeatureNode, NodesInjectionKey } from "game/layers"; import { FeatureNode, NodesInjectionKey } from "game/layers";
import { Application } from "pixi.js";
import { processedPropType } from "util/vue";
import {
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
PropType,
ref,
unref
} from "vue";
// TODO get typing support on the Particles component // TODO get typing support on the Particles component
export default defineComponent({ export default defineComponent({
props: { props: {
zIndex: { style: processedPropType<StyleValue>(String, Object, Array),
type: Number, classes: processedPropType<Record<string, boolean>>(Object),
required: true
},
fullscreen: {
type: Boolean,
required: true
},
onInit: { onInit: {
type: Function as PropType<(container: EmitterContainer & Container) => void>, type: Function as PropType<(app: Application) => void>,
required: true required: true
}, },
id: { id: {
type: String, type: String,
required: true required: true
}, },
onContainerResized: Function as PropType<(rect: DOMRect) => void> onContainerResized: Function as PropType<(rect: DOMRect) => void>,
onHotReload: Function as PropType<VoidFunction>
}, },
components: { Particles: ParticlesComponent },
setup(props) { setup(props) {
const particles = ref<null | { particles: { container: Emitters } }>(null); const app = ref<null | Application>(null);
async function particlesInit(engine: Engine) {
await loadFull(engine);
}
function particlesLoaded(container: EmitterContainer & Container) {
props.onInit(container);
}
const resizeObserver = new ResizeObserver(updateBounds); const resizeObserver = new ResizeObserver(updateBounds);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodes = inject(NodesInjectionKey)!; const nodes = inject(NodesInjectionKey)!;
const resizeListener = ref<Element | null>(null); const resizeListener = ref<HTMLElement | null>(null);
onMounted(() => { onMounted(() => {
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
const resListener = resizeListener.value; const resListener = resizeListener.value;
if (resListener != null) { if (resListener != null) {
resizeObserver.observe(resListener); resizeObserver.observe(resListener);
app.value = new Application({
resizeTo: resListener,
backgroundAlpha: 0
});
resizeListener.value?.appendChild(app.value.view);
props.onInit(app.value as Application);
} }
updateBounds(); updateBounds();
if (module.hot?.status() === "apply" && props.onHotReload) {
nextTick(props.onHotReload);
}
});
onBeforeUnmount(() => {
app.value?.destroy();
}); });
let isDirty = true; let isDirty = true;
@ -93,20 +78,20 @@ export default defineComponent({
nextTick(() => { nextTick(() => {
if (resizeListener.value != null && props.onContainerResized) { if (resizeListener.value != null && props.onContainerResized) {
// TODO don't overlap with Links.vue // TODO don't overlap with Links.vue
(Object.values(nodes.value) as FeatureNode[]).forEach( (Object.values(nodes.value).filter(n => n) as FeatureNode[]).forEach(
node => (node.rect = node.element.getBoundingClientRect()) node => (node.rect = node.element.getBoundingClientRect())
); );
props.onContainerResized(resizeListener.value.getBoundingClientRect()); props.onContainerResized(resizeListener.value.getBoundingClientRect());
app.value?.resize();
} }
isDirty = true; isDirty = true;
}); });
} }
} }
document.fonts.ready.then(updateBounds);
return { return {
particles, unref,
particlesInit,
particlesLoaded,
resizeListener resizeListener
}; };
} }

View file

@ -1,27 +1,24 @@
import ParticlesComponent from "features/particles/Particles.vue"; import ParticlesComponent from "features/particles/Particles.vue";
import { Container } from "tsparticles-engine"; import { Ref, shallowRef, unref } from "vue";
import { IEmitter } from "tsparticles-plugin-emitters/Options/Interfaces/IEmitter"; import { Component, GatherProps, getUniqueID, Replace, StyleValue } from "features/feature";
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance";
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
import { Ref, shallowRef } from "vue";
import { Component, GatherProps, getUniqueID, Replace, setDefault } from "features/feature";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Application } from "pixi.js";
import { Emitter, EmitterConfigV3, upgradeConfig } from "@pixi/particle-emitter";
import { Computable, GetComputableType } from "util/computed";
export const ParticlesType = Symbol("Particles"); export const ParticlesType = Symbol("Particles");
export interface ParticlesOptions { export interface ParticlesOptions {
fullscreen?: boolean; classes?: Computable<Record<string, boolean>>;
zIndex?: number; style?: Computable<StyleValue>;
onContainerResized?: (boundingRect: DOMRect) => void; onContainerResized?: (boundingRect: DOMRect) => void;
onHotReload?: VoidFunction;
} }
export interface BaseParticles { export interface BaseParticles {
id: string; id: string;
containerRef: Ref<null | (EmitterContainer & Container)>; app: Ref<null | Application>;
addEmitter: ( addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
options: IEmitter & { particles: Required<IEmitter>["particles"] }
) => Promise<EmitterInstance>;
removeEmitter: (emitter: EmitterInstance) => void;
type: typeof ParticlesType; type: typeof ParticlesType;
[Component]: typeof ParticlesComponent; [Component]: typeof ParticlesComponent;
[GatherProps]: () => Record<string, unknown>; [GatherProps]: () => Record<string, unknown>;
@ -30,18 +27,12 @@ export interface BaseParticles {
export type Particles<T extends ParticlesOptions> = Replace< export type Particles<T extends ParticlesOptions> = Replace<
T & BaseParticles, T & BaseParticles,
{ {
fullscreen: undefined extends T["fullscreen"] ? true : T["fullscreen"]; classes: GetComputableType<T["classes"]>;
zIndex: undefined extends T["zIndex"] ? 1 : T["zIndex"]; style: GetComputableType<T["style"]>;
} }
>; >;
export type GenericParticles = Replace< export type GenericParticles = Particles<ParticlesOptions>;
Particles<ParticlesOptions>,
{
fullscreen: boolean;
zIndex: number;
}
>;
export function createParticles<T extends ParticlesOptions>( export function createParticles<T extends ParticlesOptions>(
optionsFunc: () => T & ThisType<Particles<T>> optionsFunc: () => T & ThisType<Particles<T>>
@ -52,46 +43,38 @@ export function createParticles<T extends ParticlesOptions>(
particles.type = ParticlesType; particles.type = ParticlesType;
particles[Component] = ParticlesComponent; particles[Component] = ParticlesComponent;
particles.containerRef = shallowRef(null); particles.app = shallowRef(null);
particles.addEmitter = ( particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
options: IEmitter & { particles: Required<IEmitter>["particles"] }
): Promise<EmitterInstance> => {
const genericParticles = particles as GenericParticles; const genericParticles = particles as GenericParticles;
if (genericParticles.containerRef.value) { if (genericParticles.app.value) {
// TODO why does addEmitter require a position parameter return Promise.resolve(new Emitter(genericParticles.app.value.stage, config));
return Promise.resolve(genericParticles.containerRef.value.addEmitter(options));
} }
return new Promise<EmitterInstance>(resolve => { return new Promise<Emitter>(resolve => {
emittersToAdd.push({ resolve, options }); emittersToAdd.push({ resolve, config });
}); });
}; };
particles.removeEmitter = (emitter: EmitterInstance) => {
// TODO I can't find a proper way to remove an emitter without accessing private functions
emitter.emitters.removeEmitter(emitter);
};
let emittersToAdd: { let emittersToAdd: {
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void; resolve: (value: Emitter | PromiseLike<Emitter>) => void;
options: IEmitter & { particles: Required<IEmitter>["particles"] }; config: EmitterConfigV3;
}[] = []; }[] = [];
function onInit(container: EmitterContainer & Container) { function onInit(app: Application) {
(particles as GenericParticles).containerRef.value = container; (particles as GenericParticles).app.value = app;
emittersToAdd.forEach(({ resolve, options }) => resolve(container.addEmitter(options))); emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
emittersToAdd = []; emittersToAdd = [];
} }
setDefault(particles, "fullscreen", true);
setDefault(particles, "zIndex", 1);
particles.onContainerResized = particles.onContainerResized?.bind(particles); particles.onContainerResized = particles.onContainerResized?.bind(particles);
particles[GatherProps] = function (this: GenericParticles) { particles[GatherProps] = function (this: GenericParticles) {
const { id, fullscreen, zIndex, onContainerResized } = this; const { id, style, classes, onContainerResized, onHotReload } = this;
return { return {
id, id,
fullscreen, style: unref(style),
zIndex, classes,
onContainerResized, onContainerResized,
onHotReload,
onInit onInit
}; };
}; };
@ -99,3 +82,10 @@ export function createParticles<T extends ParticlesOptions>(
return particles as unknown as Particles<T>; return particles as unknown as Particles<T>;
}); });
} }
declare global {
interface Window {
upgradeConfig: typeof upgradeConfig;
}
}
window.upgradeConfig = upgradeConfig;