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>
|
<template>
|
||||||
<Particles
|
<Particles
|
||||||
id="particles"
|
:id="id"
|
||||||
|
:class="{
|
||||||
|
'not-fullscreen': !fullscreen
|
||||||
|
}"
|
||||||
|
:style="{
|
||||||
|
zIndex
|
||||||
|
}"
|
||||||
ref="particles"
|
ref="particles"
|
||||||
:particlesInit="particlesInit"
|
:particlesInit="particlesInit"
|
||||||
|
:particlesLoaded="particlesLoaded"
|
||||||
:options="{
|
:options="{
|
||||||
fpsLimit: 60,
|
fpsLimit: 60,
|
||||||
fullScreen: { zIndex: 1 },
|
fullScreen: { enable: fullscreen, zIndex },
|
||||||
particles: {
|
particles: {
|
||||||
number: {
|
number: {
|
||||||
value: 0
|
value: 0
|
||||||
|
@ -15,40 +23,105 @@
|
||||||
autoPlay: false
|
autoPlay: false
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
|
v-bind="$attrs"
|
||||||
/>
|
/>
|
||||||
|
<div ref="resizeListener" class="resize-listener" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { loadFull } from "tsparticles";
|
import { loadFull } from "tsparticles";
|
||||||
import { Engine } from "tsparticles-engine";
|
import { Engine, Container } from "tsparticles-engine";
|
||||||
import { Emitters } from "tsparticles-plugin-emitters/Emitters";
|
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 { ParticlesComponent } from "particles.vue3";
|
||||||
|
import { NodesInjectionKey } from "game/layers";
|
||||||
|
|
||||||
// TODO get typing support on the Particles component
|
// TODO get typing support on the Particles component
|
||||||
export default defineComponent({
|
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 },
|
components: { Particles: ParticlesComponent },
|
||||||
setup(props, { emit }) {
|
setup(props) {
|
||||||
const particles = ref<null | { particles: { container: Emitters } }>(null);
|
const particles = ref<null | { particles: { container: Emitters } }>(null);
|
||||||
|
|
||||||
async function particlesInit(engine: Engine) {
|
async function particlesInit(engine: Engine) {
|
||||||
await loadFull(engine);
|
await loadFull(engine);
|
||||||
finishInit(engine);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishInit(engine: Engine) {
|
function particlesLoaded(container: EmitterContainer & Container) {
|
||||||
if (engine.domArray.length) {
|
props.onInit(container);
|
||||||
emit("init", engine.domItem(0));
|
}
|
||||||
} else {
|
|
||||||
nextTick(() => finishInit(engine));
|
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 {
|
return {
|
||||||
particles,
|
particles,
|
||||||
particlesInit
|
particlesInit,
|
||||||
|
particlesLoaded,
|
||||||
|
resizeListener
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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 { IEmitter } from "tsparticles-plugin-emitters/Options/Interfaces/IEmitter";
|
||||||
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance";
|
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance";
|
||||||
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
|
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
|
||||||
import { Ref, shallowRef } from "vue";
|
import { Ref, shallowRef } from "vue";
|
||||||
import { registerGameComponent } from "game/settings";
|
import { Component, GatherProps, getUniqueID, Replace, setDefault } from "features/feature";
|
||||||
import { jsx } 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: {
|
export interface BaseParticles {
|
||||||
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void;
|
id: string;
|
||||||
options: IEmitter & { particles: Required<IEmitter>["particles"] };
|
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(
|
export type Particles<T extends ParticlesOptions> = Replace<
|
||||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
T & BaseParticles,
|
||||||
): Promise<EmitterInstance> {
|
{
|
||||||
if (containerRef.value) {
|
fullscreen: undefined extends T["fullscreen"] ? true : T["fullscreen"];
|
||||||
// TODO why does addEmitter require a position parameter
|
zIndex: undefined extends T["zIndex"] ? 1 : T["zIndex"];
|
||||||
return Promise.resolve(containerRef.value.addEmitter(options));
|
|
||||||
}
|
}
|
||||||
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