Replaced tsparticles with pixi-emitter
This commit is contained in:
parent
69468d88c0
commit
ecc4b9fb5f
4 changed files with 879 additions and 848 deletions
1546
package-lock.json
generated
1546
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue