Added support for non-fullscreen particles
This also drastically improves positioning effects based on elements
This commit is contained in:
parent
8d0489c8e1
commit
887002e95b
2 changed files with 176 additions and 41 deletions
|
@ -1,11 +1,19 @@
|
|||
onMounted,
|
||||
<template>
|
||||
<Particles
|
||||
id="particles"
|
||||
:id="id"
|
||||
:class="{
|
||||
'not-fullscreen': !fullscreen
|
||||
}"
|
||||
:style="{
|
||||
zIndex
|
||||
}"
|
||||
ref="particles"
|
||||
:particlesInit="particlesInit"
|
||||
:particlesLoaded="particlesLoaded"
|
||||
:options="{
|
||||
fpsLimit: 60,
|
||||
fullScreen: { zIndex: 1 },
|
||||
fullScreen: { enable: fullscreen, zIndex },
|
||||
particles: {
|
||||
number: {
|
||||
value: 0
|
||||
|
@ -15,40 +23,105 @@
|
|||
autoPlay: false
|
||||
}
|
||||
}"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<div ref="resizeListener" class="resize-listener" />
|
||||
</template>
|
||||
|
||||
<script lang="tsx">
|
||||
import { loadFull } from "tsparticles";
|
||||
import { Engine } from "tsparticles-engine";
|
||||
import { Engine, Container } from "tsparticles-engine";
|
||||
import { Emitters } from "tsparticles-plugin-emitters/Emitters";
|
||||
import { defineComponent, nextTick, ref } from "vue";
|
||||
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
|
||||
import { defineComponent, inject, nextTick, onMounted, PropType, ref } from "vue";
|
||||
import { ParticlesComponent } from "particles.vue3";
|
||||
import { NodesInjectionKey } from "game/layers";
|
||||
|
||||
// TODO get typing support on the Particles component
|
||||
export default defineComponent({
|
||||
emits: ["init"],
|
||||
props: {
|
||||
zIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
onInit: {
|
||||
type: Function as PropType<(container: EmitterContainer & Container) => void>,
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
onContainerResized: Function as PropType<(rect: DOMRect) => void>
|
||||
},
|
||||
components: { Particles: ParticlesComponent },
|
||||
setup(props, { emit }) {
|
||||
setup(props) {
|
||||
const particles = ref<null | { particles: { container: Emitters } }>(null);
|
||||
|
||||
async function particlesInit(engine: Engine) {
|
||||
await loadFull(engine);
|
||||
finishInit(engine);
|
||||
}
|
||||
|
||||
function finishInit(engine: Engine) {
|
||||
if (engine.domArray.length) {
|
||||
emit("init", engine.domItem(0));
|
||||
} else {
|
||||
nextTick(() => finishInit(engine));
|
||||
function particlesLoaded(container: EmitterContainer & Container) {
|
||||
props.onInit(container);
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateBounds);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const nodes = inject(NodesInjectionKey)!;
|
||||
|
||||
const resizeListener = ref<Element | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||
const resListener = resizeListener.value;
|
||||
if (resListener != null) {
|
||||
resizeObserver.observe(resListener);
|
||||
}
|
||||
updateBounds();
|
||||
});
|
||||
|
||||
let isDirty = true;
|
||||
function updateBounds() {
|
||||
if (isDirty) {
|
||||
isDirty = false;
|
||||
nextTick(() => {
|
||||
if (resizeListener.value != null && props.onContainerResized) {
|
||||
// TODO don't overlap with Links.vue
|
||||
Object.values(nodes.value).forEach(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
node => (node!.rect = node?.element.getBoundingClientRect())
|
||||
);
|
||||
props.onContainerResized(resizeListener.value.getBoundingClientRect());
|
||||
}
|
||||
isDirty = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
particles,
|
||||
particlesInit
|
||||
particlesInit,
|
||||
particlesLoaded,
|
||||
resizeListener
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.not-fullscreen,
|
||||
.resize-listener {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,39 +1,101 @@
|
|||
import ParticlesComponent from "./Particles.vue";
|
||||
import ParticlesComponent from "features/particles/Particles.vue";
|
||||
import { Container } from "tsparticles-engine";
|
||||
import { IEmitter } from "tsparticles-plugin-emitters/Options/Interfaces/IEmitter";
|
||||
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance";
|
||||
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
|
||||
import { Ref, shallowRef } from "vue";
|
||||
import { registerGameComponent } from "game/settings";
|
||||
import { jsx } from "features/feature";
|
||||
import { Component, GatherProps, getUniqueID, Replace, setDefault } from "features/feature";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
|
||||
registerGameComponent(jsx(() => <ParticlesComponent onInit={onInit} />));
|
||||
export const ParticlesType = Symbol("Particles");
|
||||
|
||||
const containerRef: Ref<null | EmitterContainer> = shallowRef(null);
|
||||
export interface ParticlesOptions {
|
||||
fullscreen?: boolean;
|
||||
zIndex?: number;
|
||||
onContainerResized?: (boundingRect: DOMRect) => void;
|
||||
}
|
||||
|
||||
let emittersToAdd: {
|
||||
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void;
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] };
|
||||
}[] = [];
|
||||
export interface BaseParticles {
|
||||
id: string;
|
||||
containerRef: Ref<null | (EmitterContainer & Container)>;
|
||||
addEmitter: (
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
||||
) => Promise<EmitterInstance>;
|
||||
removeEmitter: (emitter: EmitterInstance) => void;
|
||||
type: typeof ParticlesType;
|
||||
[Component]: typeof ParticlesComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function addEmitter(
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
||||
): Promise<EmitterInstance> {
|
||||
if (containerRef.value) {
|
||||
// TODO why does addEmitter require a position parameter
|
||||
return Promise.resolve(containerRef.value.addEmitter(options));
|
||||
export type Particles<T extends ParticlesOptions> = Replace<
|
||||
T & BaseParticles,
|
||||
{
|
||||
fullscreen: undefined extends T["fullscreen"] ? true : T["fullscreen"];
|
||||
zIndex: undefined extends T["zIndex"] ? 1 : T["zIndex"];
|
||||
}
|
||||
return new Promise<EmitterInstance>(resolve => {
|
||||
emittersToAdd.push({ resolve, options });
|
||||
>;
|
||||
|
||||
export type GenericParticles = Replace<
|
||||
Particles<ParticlesOptions>,
|
||||
{
|
||||
fullscreen: boolean;
|
||||
zIndex: number;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createParticles<T extends ParticlesOptions>(
|
||||
optionsFunc: () => T & ThisType<Particles<T>>
|
||||
): Particles<T> {
|
||||
return createLazyProxy(() => {
|
||||
const particles: T & Partial<BaseParticles> = optionsFunc();
|
||||
particles.id = getUniqueID("particles-");
|
||||
particles.type = ParticlesType;
|
||||
particles[Component] = ParticlesComponent;
|
||||
|
||||
particles.containerRef = shallowRef(null);
|
||||
particles.addEmitter = (
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
||||
): Promise<EmitterInstance> => {
|
||||
const genericParticles = particles as GenericParticles;
|
||||
if (genericParticles.containerRef.value) {
|
||||
// TODO why does addEmitter require a position parameter
|
||||
return Promise.resolve(genericParticles.containerRef.value.addEmitter(options));
|
||||
}
|
||||
return new Promise<EmitterInstance>(resolve => {
|
||||
emittersToAdd.push({ resolve, options });
|
||||
});
|
||||
};
|
||||
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: {
|
||||
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void;
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] };
|
||||
}[] = [];
|
||||
|
||||
function onInit(container: EmitterContainer & Container) {
|
||||
(particles as GenericParticles).containerRef.value = container;
|
||||
emittersToAdd.forEach(({ resolve, options }) => resolve(container.addEmitter(options)));
|
||||
emittersToAdd = [];
|
||||
}
|
||||
|
||||
setDefault(particles, "fullscreen", true);
|
||||
setDefault(particles, "zIndex", 1);
|
||||
particles.onContainerResized = particles.onContainerResized?.bind(particles);
|
||||
|
||||
particles[GatherProps] = function (this: GenericParticles) {
|
||||
const { id, fullscreen, zIndex, onContainerResized } = this;
|
||||
return {
|
||||
id,
|
||||
fullscreen,
|
||||
zIndex,
|
||||
onContainerResized,
|
||||
onInit
|
||||
};
|
||||
};
|
||||
|
||||
return particles as unknown as Particles<T>;
|
||||
});
|
||||
}
|
||||
|
||||
export function removeEmitter(emitter: EmitterInstance) {
|
||||
// TODO I can't find a proper way to remove an emitter without accessing private functions
|
||||
emitter.emitters.removeEmitter(emitter);
|
||||
}
|
||||
|
||||
function onInit(container: EmitterContainer) {
|
||||
containerRef.value = container;
|
||||
emittersToAdd.forEach(({ resolve, options }) => resolve(container.addEmitter(options)));
|
||||
emittersToAdd = [];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue