mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-09 00:19:31 -06:00
Merge branch 'main' into feat/defineModel-support
This commit is contained in:
@@ -124,7 +124,7 @@ const { command } = defineProps<{
|
|||||||
.code-block {
|
.code-block {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-accordionpanel) {
|
:deep(.p-accordionpanel) {
|
||||||
@@ -133,19 +133,19 @@ const { command } = defineProps<{
|
|||||||
|
|
||||||
:deep(.p-accordion-header) {
|
:deep(.p-accordion-header) {
|
||||||
background: #0b0b0b !important;
|
background: #0b0b0b !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
border-radius: 20px !important;
|
border-radius: 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader) {
|
:deep(.p-accordionpanel:not(.p-disabled).p-accordionpanel-active > .p-accordionheader) {
|
||||||
background: #0b0b0b !important;
|
background: #0b0b0b !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
border-radius: 15px 15px 0 0;
|
border-radius: 15px 15px 0 0;
|
||||||
border-bottom: none !important;
|
border-bottom: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-accordionpanel:not(.p-disabled) > .p-accordionheader) {
|
:deep(.p-accordionpanel:not(.p-disabled) > .p-accordionheader) {
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
background: #0b0b0b !important;
|
background: #0b0b0b !important;
|
||||||
}
|
}
|
||||||
@@ -171,14 +171,14 @@ const { command } = defineProps<{
|
|||||||
|
|
||||||
:deep(.p-accordion-content) {
|
:deep(.p-accordion-content) {
|
||||||
background: #0b0b0b !important;
|
background: #0b0b0b !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-radius: 0 0 15px 15px;
|
border-radius: 0 0 15px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-accordioncontent-content) {
|
:deep(.p-accordioncontent-content) {
|
||||||
background: #0b0b0b !important;
|
background: #0b0b0b !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
border-radius: 0 0 15px 15px;
|
border-radius: 0 0 15px 15px;
|
||||||
border-top: none !important;
|
border-top: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="preview-color">
|
<div class="preview-color">
|
||||||
<span class="color-label">{{ title }}</span>
|
<span class="color-label">{{ title }}</span>
|
||||||
<input :value="color" @input="handleColorChange" type="color" :disabled="props.disabled" class="color-input" />
|
<input :value="color" @input="handleColorChange" type="color" :disabled="disabled" class="color-input" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{ title: string; disabled?: boolean }>();
|
defineProps<{
|
||||||
|
title?: string;
|
||||||
|
modelValue: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
const color = defineModel<string>();
|
const color = defineModel<string>();
|
||||||
|
|
||||||
const handleColorChange = (event: Event) => {
|
const handleColorChange = (event: Event) => {
|
||||||
@@ -19,7 +24,6 @@ const handleColorChange = (event: Event) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.color-label {
|
.color-label {
|
||||||
|
|||||||
@@ -92,14 +92,14 @@ defineProps<{
|
|||||||
|
|
||||||
:deep(.p-datatable-header) {
|
:deep(.p-datatable-header) {
|
||||||
background: #111;
|
background: #111;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-datatable-thead > tr > th) {
|
:deep(.p-datatable-thead > tr > th) {
|
||||||
background: #111;
|
background: #111;
|
||||||
border-right: 1px solid #142216;
|
border-right: 1px solid #333;
|
||||||
border-bottom: 1px solid #142216;
|
border-bottom: 1px solid #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
@@ -114,8 +114,8 @@ defineProps<{
|
|||||||
|
|
||||||
:deep(.p-datatable-tbody > tr > td) {
|
:deep(.p-datatable-tbody > tr > td) {
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
border-right: 1px solid #142216;
|
border-right: 1px solid #333;
|
||||||
border-bottom: 1px solid #142216;
|
border-bottom: 1px solid #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ defineEmits<{
|
|||||||
top: 0.75rem;
|
top: 0.75rem;
|
||||||
right: 0.75rem;
|
right: 0.75rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: #111;
|
background-color: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
color: white;
|
color: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
@@ -29,11 +29,11 @@ defineEmits<{
|
|||||||
.refresh-button:hover {
|
.refresh-button:hover {
|
||||||
background-color: #222 !important;
|
background-color: #222 !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
outline: 1px solid transparent !important;
|
outline: 1px solid transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh-button:active {
|
.refresh-button:active {
|
||||||
background-color: #142216;
|
background-color: #0b0b0b;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ import ContributionSection from './ContributionSection.vue';
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-tab[aria-selected='true'] .tab-header) {
|
:deep(.p-tab[aria-selected='true'] .tab-header) {
|
||||||
background: #333333;
|
background: #222;
|
||||||
color: #a7ef9e;
|
color: #27ff64;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.p-tabpanels) {
|
:deep(.p-tabpanels) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
split-type="words"
|
split-type="words"
|
||||||
:delay="10"
|
:delay="10"
|
||||||
:duration="1"
|
:duration="1"
|
||||||
text="Eighty-plus snippets, ready to be dropped into your Vue projects"
|
text="Sixty-plus snippets, ready to be dropped into your Vue projects"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<router-link to="/text-animations/split-text" class="landing-button">
|
<router-link to="/text-animations/split-text" class="landing-button">
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ export const CATEGORIES = [
|
|||||||
'Squares',
|
'Squares',
|
||||||
'Iridescence',
|
'Iridescence',
|
||||||
'Threads',
|
'Threads',
|
||||||
'Grid Motion'
|
'Grid Motion',
|
||||||
|
'Orb',
|
||||||
|
'Ballpit'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ const backgrounds = {
|
|||||||
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.vue'),
|
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.vue'),
|
||||||
'hyperspeed': () => import('../demo/Backgrounds/HyperspeedDemo.vue'),
|
'hyperspeed': () => import('../demo/Backgrounds/HyperspeedDemo.vue'),
|
||||||
'shape-blur': () => import('../demo/Backgrounds/ShapeBlurDemo.vue'),
|
'shape-blur': () => import('../demo/Backgrounds/ShapeBlurDemo.vue'),
|
||||||
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue')
|
'balatro': () => import('../demo/Backgrounds/BalatroDemo.vue'),
|
||||||
|
'orb': () => import('../demo/Backgrounds/OrbDemo.vue'),
|
||||||
|
'ballpit': () => import('../demo/Backgrounds/BallpitDemo.vue'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentMap = {
|
export const componentMap = {
|
||||||
|
|||||||
26
src/constants/code/Backgrounds/ballpitCode.ts
Normal file
26
src/constants/code/Backgrounds/ballpitCode.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import code from '@content/Backgrounds/Ballpit/Ballpit.vue?raw';
|
||||||
|
import type { CodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const ballpit: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Ballpit`,
|
||||||
|
installation: `npm i three`,
|
||||||
|
usage: `//Component inspired by Kevin Levron:
|
||||||
|
//https://x.com/soju22/status/1858925191671271801
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full h-[500px] overflow-hidden">
|
||||||
|
<Ballpit
|
||||||
|
:count="200"
|
||||||
|
:gravity="0.7"
|
||||||
|
:friction="0.8"
|
||||||
|
:wallBounce="0.95"
|
||||||
|
:followCursor="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Ballpit from "./Ballpit.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
};
|
||||||
17
src/constants/code/Backgrounds/orbCode.ts
Normal file
17
src/constants/code/Backgrounds/orbCode.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import code from '@content/Backgrounds/Orb/Orb.vue?raw';
|
||||||
|
import type { CodeObject } from '../../../types/code';
|
||||||
|
|
||||||
|
export const orb: CodeObject = {
|
||||||
|
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Orb`,
|
||||||
|
installation: `npm i ogl`,
|
||||||
|
usage: `<template>
|
||||||
|
<div class="relative w-full h-[600px]">
|
||||||
|
<Orb :hoverIntensity="0.5" :rotateOnHover="true" :hue="0" :forceHoverState="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Orb from "./Orb.vue";
|
||||||
|
</script>`,
|
||||||
|
code
|
||||||
|
};
|
||||||
909
src/content/Backgrounds/Ballpit/Ballpit.vue
Normal file
909
src/content/Backgrounds/Ballpit/Ballpit.vue
Normal file
@@ -0,0 +1,909 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { gsap } from 'gsap';
|
||||||
|
import { Observer } from 'gsap/Observer';
|
||||||
|
import {
|
||||||
|
ACESFilmicToneMapping,
|
||||||
|
AmbientLight,
|
||||||
|
Clock,
|
||||||
|
Color,
|
||||||
|
InstancedMesh,
|
||||||
|
MathUtils,
|
||||||
|
MeshPhysicalMaterial,
|
||||||
|
Object3D,
|
||||||
|
PerspectiveCamera,
|
||||||
|
Plane,
|
||||||
|
PMREMGenerator,
|
||||||
|
PointLight,
|
||||||
|
Raycaster,
|
||||||
|
Scene,
|
||||||
|
ShaderChunk,
|
||||||
|
SphereGeometry,
|
||||||
|
SRGBColorSpace,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
WebGLRenderer,
|
||||||
|
type MeshPhysicalMaterialParameters,
|
||||||
|
type WebGLRendererParameters
|
||||||
|
} from 'three';
|
||||||
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
|
||||||
|
import { defineProps, onMounted, onUnmounted, ref } from 'vue';
|
||||||
|
|
||||||
|
gsap.registerPlugin(Observer);
|
||||||
|
|
||||||
|
interface MaterialParams extends MeshPhysicalMaterialParameters {
|
||||||
|
metalness?: number;
|
||||||
|
roughness?: number;
|
||||||
|
clearcoat?: number;
|
||||||
|
clearcoatRoughness?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
followCursor?: boolean;
|
||||||
|
count?: number;
|
||||||
|
colors?: number[];
|
||||||
|
ambientColor?: number;
|
||||||
|
ambientIntensity?: number;
|
||||||
|
lightIntensity?: number;
|
||||||
|
materialParams?: MaterialParams;
|
||||||
|
minSize?: number;
|
||||||
|
maxSize?: number;
|
||||||
|
size0?: number;
|
||||||
|
gravity?: number;
|
||||||
|
friction?: number;
|
||||||
|
wallBounce?: number;
|
||||||
|
maxVelocity?: number;
|
||||||
|
maxX?: number;
|
||||||
|
maxY?: number;
|
||||||
|
maxZ?: number;
|
||||||
|
controlSphere0?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
className: '',
|
||||||
|
followCursor: true,
|
||||||
|
count: 200,
|
||||||
|
colors: () => [0, 0, 0],
|
||||||
|
ambientColor: 0xffffff,
|
||||||
|
ambientIntensity: 1,
|
||||||
|
lightIntensity: 200,
|
||||||
|
materialParams: () => ({
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.5,
|
||||||
|
clearcoat: 1,
|
||||||
|
clearcoatRoughness: 0.15
|
||||||
|
}),
|
||||||
|
minSize: 0.5,
|
||||||
|
maxSize: 1,
|
||||||
|
size0: 1,
|
||||||
|
gravity: 0.5,
|
||||||
|
friction: 0.9975,
|
||||||
|
wallBounce: 0.95,
|
||||||
|
maxVelocity: 0.15,
|
||||||
|
maxX: 5,
|
||||||
|
maxY: 5,
|
||||||
|
maxZ: 2,
|
||||||
|
controlSphere0: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
|
const spheresInstanceRef = ref<CreateBallpitReturn | null>(null);
|
||||||
|
|
||||||
|
interface PostProcessing {
|
||||||
|
setSize: (width: number, height: number) => void;
|
||||||
|
render: () => void;
|
||||||
|
dispose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface XConfig {
|
||||||
|
canvas?: HTMLCanvasElement;
|
||||||
|
id?: string;
|
||||||
|
rendererOptions?: Partial<WebGLRendererParameters>;
|
||||||
|
size?: 'parent' | { width: number; height: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SizeData {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
wWidth: number;
|
||||||
|
wHeight: number;
|
||||||
|
ratio: number;
|
||||||
|
pixelRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class X {
|
||||||
|
#config: XConfig;
|
||||||
|
#postprocessing: PostProcessing | null = null;
|
||||||
|
#resizeObserver?: ResizeObserver;
|
||||||
|
#intersectionObserver?: IntersectionObserver;
|
||||||
|
#resizeTimer?: number;
|
||||||
|
#animationFrameId: number = 0;
|
||||||
|
#clock: Clock = new Clock();
|
||||||
|
#animationState = { elapsed: 0, delta: 0 };
|
||||||
|
#isAnimating: boolean = false;
|
||||||
|
#isVisible: boolean = false;
|
||||||
|
|
||||||
|
canvas!: HTMLCanvasElement;
|
||||||
|
camera!: PerspectiveCamera;
|
||||||
|
cameraMinAspect?: number;
|
||||||
|
cameraMaxAspect?: number;
|
||||||
|
cameraFov!: number;
|
||||||
|
maxPixelRatio?: number;
|
||||||
|
minPixelRatio?: number;
|
||||||
|
scene!: Scene;
|
||||||
|
renderer!: WebGLRenderer;
|
||||||
|
size: SizeData = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
wWidth: 0,
|
||||||
|
wHeight: 0,
|
||||||
|
ratio: 0,
|
||||||
|
pixelRatio: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
render: () => void = this.#render.bind(this);
|
||||||
|
onBeforeRender: (state: { elapsed: number; delta: number }) => void = () => {};
|
||||||
|
onAfterRender: (state: { elapsed: number; delta: number }) => void = () => {};
|
||||||
|
onAfterResize: (size: SizeData) => void = () => {};
|
||||||
|
isDisposed: boolean = false;
|
||||||
|
|
||||||
|
constructor(config: XConfig) {
|
||||||
|
this.#config = { ...config };
|
||||||
|
this.#initCamera();
|
||||||
|
this.#initScene();
|
||||||
|
this.#initRenderer();
|
||||||
|
this.resize();
|
||||||
|
this.#initObservers();
|
||||||
|
}
|
||||||
|
|
||||||
|
#initCamera() {
|
||||||
|
this.camera = new PerspectiveCamera();
|
||||||
|
this.cameraFov = this.camera.fov;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initScene() {
|
||||||
|
this.scene = new Scene();
|
||||||
|
}
|
||||||
|
|
||||||
|
#initRenderer() {
|
||||||
|
if (this.#config.canvas) {
|
||||||
|
this.canvas = this.#config.canvas;
|
||||||
|
} else if (this.#config.id) {
|
||||||
|
const elem = document.getElementById(this.#config.id);
|
||||||
|
if (elem instanceof HTMLCanvasElement) {
|
||||||
|
this.canvas = elem;
|
||||||
|
} else {
|
||||||
|
console.error('Three: Missing canvas or id parameter');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Three: Missing canvas or id parameter');
|
||||||
|
}
|
||||||
|
this.canvas!.style.display = 'block';
|
||||||
|
const rendererOptions: WebGLRendererParameters = {
|
||||||
|
canvas: this.canvas,
|
||||||
|
powerPreference: 'high-performance',
|
||||||
|
...(this.#config.rendererOptions ?? {})
|
||||||
|
};
|
||||||
|
this.renderer = new WebGLRenderer(rendererOptions);
|
||||||
|
this.renderer.outputColorSpace = SRGBColorSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initObservers() {
|
||||||
|
if (!(this.#config.size instanceof Object)) {
|
||||||
|
window.addEventListener('resize', this.#onResize.bind(this));
|
||||||
|
if (this.#config.size === 'parent' && this.canvas.parentNode) {
|
||||||
|
this.#resizeObserver = new ResizeObserver(this.#onResize.bind(this));
|
||||||
|
this.#resizeObserver.observe(this.canvas.parentNode as Element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#intersectionObserver = new IntersectionObserver(this.#onIntersection.bind(this), {
|
||||||
|
root: null,
|
||||||
|
rootMargin: '0px',
|
||||||
|
threshold: 0
|
||||||
|
});
|
||||||
|
this.#intersectionObserver.observe(this.canvas);
|
||||||
|
document.addEventListener('visibilitychange', this.#onVisibilityChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
#onResize() {
|
||||||
|
if (this.#resizeTimer) clearTimeout(this.#resizeTimer);
|
||||||
|
this.#resizeTimer = window.setTimeout(this.resize.bind(this), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
let w: number, h: number;
|
||||||
|
if (this.#config.size instanceof Object) {
|
||||||
|
w = this.#config.size.width;
|
||||||
|
h = this.#config.size.height;
|
||||||
|
} else if (this.#config.size === 'parent' && this.canvas.parentNode) {
|
||||||
|
w = (this.canvas.parentNode as HTMLElement).offsetWidth;
|
||||||
|
h = (this.canvas.parentNode as HTMLElement).offsetHeight;
|
||||||
|
} else {
|
||||||
|
w = window.innerWidth;
|
||||||
|
h = window.innerHeight;
|
||||||
|
}
|
||||||
|
this.size.width = w;
|
||||||
|
this.size.height = h;
|
||||||
|
this.size.ratio = w / h;
|
||||||
|
this.#updateCamera();
|
||||||
|
this.#updateRenderer();
|
||||||
|
this.onAfterResize(this.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateCamera() {
|
||||||
|
this.camera.aspect = this.size.width / this.size.height;
|
||||||
|
if (this.camera.isPerspectiveCamera && this.cameraFov) {
|
||||||
|
if (this.cameraMinAspect && this.camera.aspect < this.cameraMinAspect) {
|
||||||
|
this.#adjustFov(this.cameraMinAspect);
|
||||||
|
} else if (this.cameraMaxAspect && this.camera.aspect > this.cameraMaxAspect) {
|
||||||
|
this.#adjustFov(this.cameraMaxAspect);
|
||||||
|
} else {
|
||||||
|
this.camera.fov = this.cameraFov;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.camera.updateProjectionMatrix();
|
||||||
|
this.updateWorldSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
#adjustFov(aspect: number) {
|
||||||
|
const tanFov = Math.tan(MathUtils.degToRad(this.cameraFov / 2));
|
||||||
|
const newTan = tanFov / (this.camera.aspect / aspect);
|
||||||
|
this.camera.fov = 2 * MathUtils.radToDeg(Math.atan(newTan));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWorldSize() {
|
||||||
|
if (this.camera.isPerspectiveCamera) {
|
||||||
|
const fovRad = (this.camera.fov * Math.PI) / 180;
|
||||||
|
this.size.wHeight = 2 * Math.tan(fovRad / 2) * this.camera.position.length();
|
||||||
|
this.size.wWidth = this.size.wHeight * this.camera.aspect;
|
||||||
|
} else {
|
||||||
|
const cam = this.camera as unknown as {
|
||||||
|
top: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
isOrthographicCamera: boolean;
|
||||||
|
};
|
||||||
|
if (cam.isOrthographicCamera) {
|
||||||
|
this.size.wHeight = cam.top - cam.bottom;
|
||||||
|
this.size.wWidth = cam.right - cam.left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateRenderer() {
|
||||||
|
this.renderer.setSize(this.size.width, this.size.height);
|
||||||
|
this.#postprocessing?.setSize(this.size.width, this.size.height);
|
||||||
|
let pr = window.devicePixelRatio;
|
||||||
|
if (this.maxPixelRatio && pr > this.maxPixelRatio) {
|
||||||
|
pr = this.maxPixelRatio;
|
||||||
|
} else if (this.minPixelRatio && pr < this.minPixelRatio) {
|
||||||
|
pr = this.minPixelRatio;
|
||||||
|
}
|
||||||
|
this.renderer.setPixelRatio(pr);
|
||||||
|
this.size.pixelRatio = pr;
|
||||||
|
}
|
||||||
|
|
||||||
|
get postprocessing() {
|
||||||
|
return this.#postprocessing;
|
||||||
|
}
|
||||||
|
set postprocessing(value: PostProcessing | null) {
|
||||||
|
this.#postprocessing = value;
|
||||||
|
if (value) {
|
||||||
|
this.render = value.render.bind(value);
|
||||||
|
} else {
|
||||||
|
this.render = this.#render.bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onIntersection(entries: IntersectionObserverEntry[]) {
|
||||||
|
this.#isAnimating = entries[0].isIntersecting;
|
||||||
|
if (this.#isAnimating) {
|
||||||
|
this.#startAnimation();
|
||||||
|
} else {
|
||||||
|
this.#stopAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onVisibilityChange() {
|
||||||
|
if (this.#isAnimating) {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.#stopAnimation();
|
||||||
|
} else {
|
||||||
|
this.#startAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#startAnimation() {
|
||||||
|
if (this.#isVisible) return;
|
||||||
|
const animateFrame = () => {
|
||||||
|
this.#animationFrameId = requestAnimationFrame(animateFrame);
|
||||||
|
this.#animationState.delta = this.#clock.getDelta();
|
||||||
|
this.#animationState.elapsed += this.#animationState.delta;
|
||||||
|
this.onBeforeRender(this.#animationState);
|
||||||
|
this.render();
|
||||||
|
this.onAfterRender(this.#animationState);
|
||||||
|
};
|
||||||
|
this.#isVisible = true;
|
||||||
|
this.#clock.start();
|
||||||
|
animateFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
#stopAnimation() {
|
||||||
|
if (this.#isVisible) {
|
||||||
|
cancelAnimationFrame(this.#animationFrameId);
|
||||||
|
this.#isVisible = false;
|
||||||
|
this.#clock.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#render() {
|
||||||
|
this.renderer.render(this.scene, this.camera);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.scene.traverse(obj => {
|
||||||
|
const mesh = obj as unknown as {
|
||||||
|
isMesh?: boolean;
|
||||||
|
material?: {
|
||||||
|
dispose: () => void;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
geometry?: {
|
||||||
|
dispose: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (mesh.isMesh && mesh.material && mesh.geometry) {
|
||||||
|
if (typeof mesh.material === 'object' && mesh.material !== null) {
|
||||||
|
Object.keys(mesh.material).forEach(key => {
|
||||||
|
const matProp = mesh.material![key] as unknown;
|
||||||
|
if (matProp && typeof matProp === 'object' && matProp !== null) {
|
||||||
|
const disposable = matProp as { dispose?: () => void };
|
||||||
|
if (typeof disposable.dispose === 'function') {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mesh.material.dispose();
|
||||||
|
mesh.geometry.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.scene.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.#onResizeCleanup();
|
||||||
|
this.#stopAnimation();
|
||||||
|
this.clear();
|
||||||
|
this.#postprocessing?.dispose();
|
||||||
|
this.renderer.dispose();
|
||||||
|
this.isDisposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#onResizeCleanup() {
|
||||||
|
window.removeEventListener('resize', this.#onResize.bind(this));
|
||||||
|
this.#resizeObserver?.disconnect();
|
||||||
|
this.#intersectionObserver?.disconnect();
|
||||||
|
document.removeEventListener('visibilitychange', this.#onVisibilityChange.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WConfig {
|
||||||
|
count: number;
|
||||||
|
maxX: number;
|
||||||
|
maxY: number;
|
||||||
|
maxZ: number;
|
||||||
|
maxSize: number;
|
||||||
|
minSize: number;
|
||||||
|
size0: number;
|
||||||
|
gravity: number;
|
||||||
|
friction: number;
|
||||||
|
wallBounce: number;
|
||||||
|
maxVelocity: number;
|
||||||
|
controlSphere0?: boolean;
|
||||||
|
followCursor?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class W {
|
||||||
|
config: WConfig;
|
||||||
|
positionData: Float32Array;
|
||||||
|
velocityData: Float32Array;
|
||||||
|
sizeData: Float32Array;
|
||||||
|
center: Vector3 = new Vector3();
|
||||||
|
|
||||||
|
constructor(config: WConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.positionData = new Float32Array(3 * config.count).fill(0);
|
||||||
|
this.velocityData = new Float32Array(3 * config.count).fill(0);
|
||||||
|
this.sizeData = new Float32Array(config.count).fill(1);
|
||||||
|
this.center = new Vector3();
|
||||||
|
this.#initializePositions();
|
||||||
|
this.setSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
#initializePositions() {
|
||||||
|
const { config, positionData } = this;
|
||||||
|
this.center.toArray(positionData, 0);
|
||||||
|
for (let i = 1; i < config.count; i++) {
|
||||||
|
const idx = 3 * i;
|
||||||
|
positionData[idx] = MathUtils.randFloatSpread(2 * config.maxX);
|
||||||
|
positionData[idx + 1] = MathUtils.randFloatSpread(2 * config.maxY);
|
||||||
|
positionData[idx + 2] = MathUtils.randFloatSpread(2 * config.maxZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSizes() {
|
||||||
|
const { config, sizeData } = this;
|
||||||
|
sizeData[0] = config.size0;
|
||||||
|
for (let i = 1; i < config.count; i++) {
|
||||||
|
sizeData[i] = MathUtils.randFloat(config.minSize, config.maxSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaInfo: { delta: number }) {
|
||||||
|
const { config, center, positionData, sizeData, velocityData } = this;
|
||||||
|
let startIdx = 0;
|
||||||
|
if (config.controlSphere0) {
|
||||||
|
startIdx = 1;
|
||||||
|
const firstVec = new Vector3().fromArray(positionData, 0);
|
||||||
|
firstVec.lerp(center, 0.1).toArray(positionData, 0);
|
||||||
|
new Vector3(0, 0, 0).toArray(velocityData, 0);
|
||||||
|
}
|
||||||
|
for (let idx = startIdx; idx < config.count; idx++) {
|
||||||
|
const base = 3 * idx;
|
||||||
|
const pos = new Vector3().fromArray(positionData, base);
|
||||||
|
const vel = new Vector3().fromArray(velocityData, base);
|
||||||
|
vel.y -= deltaInfo.delta * config.gravity * sizeData[idx];
|
||||||
|
vel.multiplyScalar(config.friction);
|
||||||
|
vel.clampLength(0, config.maxVelocity);
|
||||||
|
pos.add(vel);
|
||||||
|
pos.toArray(positionData, base);
|
||||||
|
vel.toArray(velocityData, base);
|
||||||
|
}
|
||||||
|
for (let idx = startIdx; idx < config.count; idx++) {
|
||||||
|
const base = 3 * idx;
|
||||||
|
const pos = new Vector3().fromArray(positionData, base);
|
||||||
|
const vel = new Vector3().fromArray(velocityData, base);
|
||||||
|
const radius = sizeData[idx];
|
||||||
|
for (let jdx = idx + 1; jdx < config.count; jdx++) {
|
||||||
|
const otherBase = 3 * jdx;
|
||||||
|
const otherPos = new Vector3().fromArray(positionData, otherBase);
|
||||||
|
const otherVel = new Vector3().fromArray(velocityData, otherBase);
|
||||||
|
const diff = new Vector3().copy(otherPos).sub(pos);
|
||||||
|
const dist = diff.length();
|
||||||
|
const sumRadius = radius + sizeData[jdx];
|
||||||
|
if (dist < sumRadius) {
|
||||||
|
const overlap = sumRadius - dist;
|
||||||
|
const correction = diff.normalize().multiplyScalar(0.5 * overlap);
|
||||||
|
const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 1));
|
||||||
|
pos.sub(correction);
|
||||||
|
vel.sub(velCorrection);
|
||||||
|
pos.toArray(positionData, base);
|
||||||
|
vel.toArray(velocityData, base);
|
||||||
|
otherPos.add(correction);
|
||||||
|
otherVel.add(correction.clone().multiplyScalar(Math.max(otherVel.length(), 1)));
|
||||||
|
otherPos.toArray(positionData, otherBase);
|
||||||
|
otherVel.toArray(velocityData, otherBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.controlSphere0) {
|
||||||
|
const diff = new Vector3().copy(new Vector3().fromArray(positionData, 0)).sub(pos);
|
||||||
|
const d = diff.length();
|
||||||
|
const sumRadius0 = radius + sizeData[0];
|
||||||
|
if (d < sumRadius0) {
|
||||||
|
const correction = diff.normalize().multiplyScalar(sumRadius0 - d);
|
||||||
|
const velCorrection = correction.clone().multiplyScalar(Math.max(vel.length(), 2));
|
||||||
|
pos.sub(correction);
|
||||||
|
vel.sub(velCorrection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Math.abs(pos.x) + radius > config.maxX) {
|
||||||
|
pos.x = Math.sign(pos.x) * (config.maxX - radius);
|
||||||
|
vel.x = -vel.x * config.wallBounce;
|
||||||
|
}
|
||||||
|
if (config.gravity === 0) {
|
||||||
|
if (Math.abs(pos.y) + radius > config.maxY) {
|
||||||
|
pos.y = Math.sign(pos.y) * (config.maxY - radius);
|
||||||
|
vel.y = -vel.y * config.wallBounce;
|
||||||
|
}
|
||||||
|
} else if (pos.y - radius < -config.maxY) {
|
||||||
|
pos.y = -config.maxY + radius;
|
||||||
|
vel.y = -vel.y * config.wallBounce;
|
||||||
|
}
|
||||||
|
const maxBoundary = Math.max(config.maxZ, config.maxSize);
|
||||||
|
if (Math.abs(pos.z) + radius > maxBoundary) {
|
||||||
|
pos.z = Math.sign(pos.z) * (config.maxZ - radius);
|
||||||
|
vel.z = -vel.z * config.wallBounce;
|
||||||
|
}
|
||||||
|
pos.toArray(positionData, base);
|
||||||
|
vel.toArray(velocityData, base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShaderUniforms {
|
||||||
|
[key: string]: { value: number | Vector2 | Vector3 | Color | boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShaderObject {
|
||||||
|
uniforms: ShaderUniforms;
|
||||||
|
fragmentShader: string;
|
||||||
|
vertexShader: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UniformValue {
|
||||||
|
value: number | Vector2 | Vector3 | Color | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Y extends MeshPhysicalMaterial {
|
||||||
|
uniforms: { [key: string]: UniformValue } = {
|
||||||
|
thicknessDistortion: { value: 0.1 },
|
||||||
|
thicknessAmbient: { value: 0 },
|
||||||
|
thicknessAttenuation: { value: 0.1 },
|
||||||
|
thicknessPower: { value: 2 },
|
||||||
|
thicknessScale: { value: 10 }
|
||||||
|
};
|
||||||
|
|
||||||
|
declare defines: { [key: string]: string };
|
||||||
|
|
||||||
|
constructor(params: MaterialParams) {
|
||||||
|
super(params);
|
||||||
|
this.defines = { USE_UV: '' };
|
||||||
|
this.onBeforeCompile = shader => {
|
||||||
|
Object.assign(shader.uniforms, this.uniforms);
|
||||||
|
shader.fragmentShader =
|
||||||
|
`
|
||||||
|
uniform float thicknessPower;
|
||||||
|
uniform float thicknessScale;
|
||||||
|
uniform float thicknessDistortion;
|
||||||
|
uniform float thicknessAmbient;
|
||||||
|
uniform float thicknessAttenuation;
|
||||||
|
` + shader.fragmentShader;
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace(
|
||||||
|
'void main() {',
|
||||||
|
`
|
||||||
|
void RE_Direct_Scattering(const in IncidentLight directLight, const in vec2 uv, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, inout ReflectedLight reflectedLight) {
|
||||||
|
vec3 scatteringHalf = normalize(directLight.direction + (geometryNormal * thicknessDistortion));
|
||||||
|
float scatteringDot = pow(saturate(dot(geometryViewDir, -scatteringHalf)), thicknessPower) * thicknessScale;
|
||||||
|
#ifdef USE_COLOR
|
||||||
|
vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * vColor;
|
||||||
|
#else
|
||||||
|
vec3 scatteringIllu = (scatteringDot + thicknessAmbient) * diffuse;
|
||||||
|
#endif
|
||||||
|
reflectedLight.directDiffuse += scatteringIllu * thicknessAttenuation * directLight.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
`
|
||||||
|
);
|
||||||
|
const lightsChunk = ShaderChunk.lights_fragment_begin.replace(
|
||||||
|
/RE_Direct\( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight \);/g,
|
||||||
|
`
|
||||||
|
RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight );
|
||||||
|
RE_Direct_Scattering(directLight, vUv, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, reflectedLight);
|
||||||
|
`
|
||||||
|
);
|
||||||
|
shader.fragmentShader = shader.fragmentShader.replace('#include <lights_fragment_begin>', lightsChunk);
|
||||||
|
if (this.onBeforeCompile2) this.onBeforeCompile2(shader);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
onBeforeCompile2?: (shader: ShaderObject) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const XConfig = {
|
||||||
|
count: 200,
|
||||||
|
colors: [0, 0, 0],
|
||||||
|
ambientColor: 0xffffff,
|
||||||
|
ambientIntensity: 1,
|
||||||
|
lightIntensity: 200,
|
||||||
|
materialParams: {
|
||||||
|
metalness: 0.5,
|
||||||
|
roughness: 0.5,
|
||||||
|
clearcoat: 1,
|
||||||
|
clearcoatRoughness: 0.15
|
||||||
|
},
|
||||||
|
minSize: 0.5,
|
||||||
|
maxSize: 1,
|
||||||
|
size0: 1,
|
||||||
|
gravity: 0.5,
|
||||||
|
friction: 0.9975,
|
||||||
|
wallBounce: 0.95,
|
||||||
|
maxVelocity: 0.15,
|
||||||
|
maxX: 5,
|
||||||
|
maxY: 5,
|
||||||
|
maxZ: 2,
|
||||||
|
controlSphere0: false,
|
||||||
|
followCursor: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const U = new Object3D();
|
||||||
|
|
||||||
|
let globalPointerActive = false;
|
||||||
|
const pointerPosition = new Vector2();
|
||||||
|
|
||||||
|
interface PointerData {
|
||||||
|
position: Vector2;
|
||||||
|
nPosition: Vector2;
|
||||||
|
hover: boolean;
|
||||||
|
onEnter: (data: PointerData) => void;
|
||||||
|
onMove: (data: PointerData) => void;
|
||||||
|
onClick: (data: PointerData) => void;
|
||||||
|
onLeave: (data: PointerData) => void;
|
||||||
|
dispose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointerMap = new Map<HTMLElement, PointerData>();
|
||||||
|
|
||||||
|
function createPointerData(options: Partial<PointerData> & { domElement: HTMLElement }): PointerData {
|
||||||
|
const defaultData: PointerData = {
|
||||||
|
position: new Vector2(),
|
||||||
|
nPosition: new Vector2(),
|
||||||
|
hover: false,
|
||||||
|
onEnter: () => {},
|
||||||
|
onMove: () => {},
|
||||||
|
onClick: () => {},
|
||||||
|
onLeave: () => {},
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
if (!pointerMap.has(options.domElement)) {
|
||||||
|
pointerMap.set(options.domElement, defaultData);
|
||||||
|
if (!globalPointerActive) {
|
||||||
|
document.body.addEventListener('pointermove', onPointerMove as EventListener);
|
||||||
|
document.body.addEventListener('pointerleave', onPointerLeave as EventListener);
|
||||||
|
document.body.addEventListener('click', onPointerClick as EventListener);
|
||||||
|
globalPointerActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defaultData.dispose = () => {
|
||||||
|
pointerMap.delete(options.domElement);
|
||||||
|
if (pointerMap.size === 0) {
|
||||||
|
document.body.removeEventListener('pointermove', onPointerMove as EventListener);
|
||||||
|
document.body.removeEventListener('pointerleave', onPointerLeave as EventListener);
|
||||||
|
document.body.removeEventListener('click', onPointerClick as EventListener);
|
||||||
|
globalPointerActive = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return defaultData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e: PointerEvent) {
|
||||||
|
pointerPosition.set(e.clientX, e.clientY);
|
||||||
|
for (const [elem, data] of pointerMap) {
|
||||||
|
const rect = elem.getBoundingClientRect();
|
||||||
|
if (isInside(rect)) {
|
||||||
|
updatePointerData(data, rect);
|
||||||
|
if (!data.hover) {
|
||||||
|
data.hover = true;
|
||||||
|
data.onEnter(data);
|
||||||
|
}
|
||||||
|
data.onMove(data);
|
||||||
|
} else if (data.hover) {
|
||||||
|
data.hover = false;
|
||||||
|
data.onLeave(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerClick(e: PointerEvent) {
|
||||||
|
pointerPosition.set(e.clientX, e.clientY);
|
||||||
|
for (const [elem, data] of pointerMap) {
|
||||||
|
const rect = elem.getBoundingClientRect();
|
||||||
|
updatePointerData(data, rect);
|
||||||
|
if (isInside(rect)) data.onClick(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerLeave() {
|
||||||
|
for (const data of pointerMap.values()) {
|
||||||
|
if (data.hover) {
|
||||||
|
data.hover = false;
|
||||||
|
data.onLeave(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePointerData(data: PointerData, rect: DOMRect) {
|
||||||
|
data.position.set(pointerPosition.x - rect.left, pointerPosition.y - rect.top);
|
||||||
|
data.nPosition.set((data.position.x / rect.width) * 2 - 1, (-data.position.y / rect.height) * 2 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInside(rect: DOMRect) {
|
||||||
|
return (
|
||||||
|
pointerPosition.x >= rect.left &&
|
||||||
|
pointerPosition.x <= rect.left + rect.width &&
|
||||||
|
pointerPosition.y >= rect.top &&
|
||||||
|
pointerPosition.y <= rect.top + rect.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Z extends InstancedMesh {
|
||||||
|
config: typeof XConfig;
|
||||||
|
physics: W;
|
||||||
|
ambientLight: AmbientLight | undefined;
|
||||||
|
light: PointLight | undefined;
|
||||||
|
|
||||||
|
constructor(renderer: WebGLRenderer, params: Partial<typeof XConfig> = {}) {
|
||||||
|
const config = { ...XConfig, ...params };
|
||||||
|
const roomEnv = new RoomEnvironment();
|
||||||
|
const pmrem = new PMREMGenerator(renderer);
|
||||||
|
const envTexture = pmrem.fromScene(roomEnv).texture;
|
||||||
|
const geometry = new SphereGeometry();
|
||||||
|
const material = new Y({ envMap: envTexture, ...config.materialParams });
|
||||||
|
material.envMapRotation.x = -Math.PI / 2;
|
||||||
|
super(geometry, material, config.count);
|
||||||
|
this.config = config;
|
||||||
|
this.physics = new W(config);
|
||||||
|
this.#setupLights();
|
||||||
|
this.setColors(config.colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setupLights() {
|
||||||
|
this.ambientLight = new AmbientLight(this.config.ambientColor, this.config.ambientIntensity);
|
||||||
|
this.add(this.ambientLight);
|
||||||
|
this.light = new PointLight(this.config.colors[0], this.config.lightIntensity);
|
||||||
|
this.add(this.light);
|
||||||
|
}
|
||||||
|
|
||||||
|
setColors(colors: number[]) {
|
||||||
|
if (Array.isArray(colors) && colors.length > 1) {
|
||||||
|
const colorUtils = (function (colorsArr: number[]) {
|
||||||
|
let baseColors: number[] = colorsArr;
|
||||||
|
let colorObjects: Color[] = [];
|
||||||
|
baseColors.forEach(col => {
|
||||||
|
colorObjects.push(new Color(col));
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
setColors: (cols: number[]) => {
|
||||||
|
baseColors = cols;
|
||||||
|
colorObjects = [];
|
||||||
|
baseColors.forEach(col => {
|
||||||
|
colorObjects.push(new Color(col));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getColorAt: (ratio: number, out: Color = new Color()) => {
|
||||||
|
const clamped = Math.max(0, Math.min(1, ratio));
|
||||||
|
const scaled = clamped * (baseColors.length - 1);
|
||||||
|
const idx = Math.floor(scaled);
|
||||||
|
const start = colorObjects[idx];
|
||||||
|
if (idx >= baseColors.length - 1) return start.clone();
|
||||||
|
const alpha = scaled - idx;
|
||||||
|
const end = colorObjects[idx + 1];
|
||||||
|
out.r = start.r + alpha * (end.r - start.r);
|
||||||
|
out.g = start.g + alpha * (end.g - start.g);
|
||||||
|
out.b = start.b + alpha * (end.b - start.b);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(colors);
|
||||||
|
for (let idx = 0; idx < this.count; idx++) {
|
||||||
|
this.setColorAt(idx, colorUtils.getColorAt(idx / this.count));
|
||||||
|
if (idx === 0) {
|
||||||
|
this.light!.color.copy(colorUtils.getColorAt(idx / this.count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.instanceColor) return;
|
||||||
|
this.instanceColor.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(deltaInfo: { delta: number }) {
|
||||||
|
this.physics.update(deltaInfo);
|
||||||
|
for (let idx = 0; idx < this.count; idx++) {
|
||||||
|
U.position.fromArray(this.physics.positionData, 3 * idx);
|
||||||
|
if (idx === 0 && this.config.followCursor === false) {
|
||||||
|
U.scale.setScalar(0);
|
||||||
|
} else {
|
||||||
|
U.scale.setScalar(this.physics.sizeData[idx]);
|
||||||
|
}
|
||||||
|
U.updateMatrix();
|
||||||
|
this.setMatrixAt(idx, U.matrix);
|
||||||
|
if (idx === 0) this.light!.position.copy(U.position);
|
||||||
|
}
|
||||||
|
this.instanceMatrix.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateBallpitReturn {
|
||||||
|
three: X;
|
||||||
|
spheres: Z;
|
||||||
|
setCount: (count: number) => void;
|
||||||
|
togglePause: () => void;
|
||||||
|
dispose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBallpit(canvas: HTMLCanvasElement, config: Partial<typeof XConfig> = {}): CreateBallpitReturn {
|
||||||
|
const threeInstance = new X({
|
||||||
|
canvas,
|
||||||
|
size: 'parent',
|
||||||
|
rendererOptions: { antialias: true, alpha: true }
|
||||||
|
});
|
||||||
|
let spheres: Z;
|
||||||
|
threeInstance.renderer.toneMapping = ACESFilmicToneMapping;
|
||||||
|
threeInstance.camera.position.set(0, 0, 20);
|
||||||
|
threeInstance.camera.lookAt(0, 0, 0);
|
||||||
|
threeInstance.cameraMaxAspect = 1.5;
|
||||||
|
threeInstance.resize();
|
||||||
|
initialize(config);
|
||||||
|
const raycaster = new Raycaster();
|
||||||
|
const plane = new Plane(new Vector3(0, 0, 1), 0);
|
||||||
|
const intersectionPoint = new Vector3();
|
||||||
|
let isPaused = false;
|
||||||
|
const pointerData = createPointerData({
|
||||||
|
domElement: canvas,
|
||||||
|
onMove() {
|
||||||
|
raycaster.setFromCamera(pointerData.nPosition, threeInstance.camera);
|
||||||
|
threeInstance.camera.getWorldDirection(plane.normal);
|
||||||
|
raycaster.ray.intersectPlane(plane, intersectionPoint);
|
||||||
|
spheres.physics.center.copy(intersectionPoint);
|
||||||
|
spheres.config.controlSphere0 = true;
|
||||||
|
},
|
||||||
|
onLeave() {
|
||||||
|
spheres.config.controlSphere0 = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function initialize(cfg: Partial<typeof XConfig>) {
|
||||||
|
if (spheres) {
|
||||||
|
threeInstance.clear();
|
||||||
|
threeInstance.scene.remove(spheres);
|
||||||
|
}
|
||||||
|
spheres = new Z(threeInstance.renderer, cfg);
|
||||||
|
threeInstance.scene.add(spheres);
|
||||||
|
}
|
||||||
|
threeInstance.onBeforeRender = deltaInfo => {
|
||||||
|
if (!isPaused) spheres.update(deltaInfo);
|
||||||
|
};
|
||||||
|
threeInstance.onAfterResize = size => {
|
||||||
|
spheres.config.maxX = size.wWidth / 2;
|
||||||
|
spheres.config.maxY = size.wHeight / 2;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
three: threeInstance,
|
||||||
|
get spheres() {
|
||||||
|
return spheres;
|
||||||
|
},
|
||||||
|
setCount(count: number) {
|
||||||
|
initialize({ ...spheres.config, count });
|
||||||
|
},
|
||||||
|
togglePause() {
|
||||||
|
isPaused = !isPaused;
|
||||||
|
},
|
||||||
|
dispose() {
|
||||||
|
pointerData.dispose?.();
|
||||||
|
threeInstance.dispose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const { followCursor, ...restProps } = props;
|
||||||
|
|
||||||
|
const safeMaterialParams = {
|
||||||
|
metalness: props.materialParams.metalness ?? 0.5,
|
||||||
|
roughness: props.materialParams.roughness ?? 0.5,
|
||||||
|
clearcoat: props.materialParams.clearcoat ?? 1,
|
||||||
|
clearcoatRoughness: props.materialParams.clearcoatRoughness ?? 0.15
|
||||||
|
};
|
||||||
|
|
||||||
|
spheresInstanceRef.value = createBallpit(canvas, {
|
||||||
|
...restProps,
|
||||||
|
followCursor,
|
||||||
|
materialParams: safeMaterialParams
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (spheresInstanceRef.value) {
|
||||||
|
spheresInstanceRef.value.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<canvas ref="canvasRef" :class="['w-full', 'h-full', props.className]" />
|
||||||
|
</template>
|
||||||
310
src/content/Backgrounds/Orb/Orb.vue
Normal file
310
src/content/Backgrounds/Orb/Orb.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
import { Renderer, Program, Mesh, Triangle, Vec3 } from 'ogl';
|
||||||
|
|
||||||
|
interface OrbProps {
|
||||||
|
hue?: number;
|
||||||
|
hoverIntensity?: number;
|
||||||
|
rotateOnHover?: boolean;
|
||||||
|
forceHoverState?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<OrbProps>(), {
|
||||||
|
hue: 0,
|
||||||
|
hoverIntensity: 0.2,
|
||||||
|
rotateOnHover: true,
|
||||||
|
forceHoverState: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctnDom = ref<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const vert = /* glsl */ `
|
||||||
|
precision highp float;
|
||||||
|
attribute vec2 position;
|
||||||
|
attribute vec2 uv;
|
||||||
|
varying vec2 vUv;
|
||||||
|
void main() {
|
||||||
|
vUv = uv;
|
||||||
|
gl_Position = vec4(position, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const frag = /* glsl */ `
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform float iTime;
|
||||||
|
uniform vec3 iResolution;
|
||||||
|
uniform float hue;
|
||||||
|
uniform float hover;
|
||||||
|
uniform float rot;
|
||||||
|
uniform float hoverIntensity;
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
vec3 rgb2yiq(vec3 c) {
|
||||||
|
float y = dot(c, vec3(0.299, 0.587, 0.114));
|
||||||
|
float i = dot(c, vec3(0.596, -0.274, -0.322));
|
||||||
|
float q = dot(c, vec3(0.211, -0.523, 0.312));
|
||||||
|
return vec3(y, i, q);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 yiq2rgb(vec3 c) {
|
||||||
|
float r = c.x + 0.956 * c.y + 0.621 * c.z;
|
||||||
|
float g = c.x - 0.272 * c.y - 0.647 * c.z;
|
||||||
|
float b = c.x - 1.106 * c.y + 1.703 * c.z;
|
||||||
|
return vec3(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 adjustHue(vec3 color, float hueDeg) {
|
||||||
|
float hueRad = hueDeg * 3.14159265 / 180.0;
|
||||||
|
vec3 yiq = rgb2yiq(color);
|
||||||
|
float cosA = cos(hueRad);
|
||||||
|
float sinA = sin(hueRad);
|
||||||
|
float i = yiq.y * cosA - yiq.z * sinA;
|
||||||
|
float q = yiq.y * sinA + yiq.z * cosA;
|
||||||
|
yiq.y = i;
|
||||||
|
yiq.z = q;
|
||||||
|
return yiq2rgb(yiq);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hash33(vec3 p3) {
|
||||||
|
p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
|
||||||
|
p3 += dot(p3, p3.yxz + 19.19);
|
||||||
|
return -1.0 + 2.0 * fract(vec3(
|
||||||
|
p3.x + p3.y,
|
||||||
|
p3.x + p3.z,
|
||||||
|
p3.y + p3.z
|
||||||
|
) * p3.zyx);
|
||||||
|
}
|
||||||
|
|
||||||
|
float snoise3(vec3 p) {
|
||||||
|
const float K1 = 0.333333333;
|
||||||
|
const float K2 = 0.166666667;
|
||||||
|
vec3 i = floor(p + (p.x + p.y + p.z) * K1);
|
||||||
|
vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
|
||||||
|
vec3 e = step(vec3(0.0), d0 - d0.yzx);
|
||||||
|
vec3 i1 = e * (1.0 - e.zxy);
|
||||||
|
vec3 i2 = 1.0 - e.zxy * (1.0 - e);
|
||||||
|
vec3 d1 = d0 - (i1 - K2);
|
||||||
|
vec3 d2 = d0 - (i2 - K1);
|
||||||
|
vec3 d3 = d0 - 0.5;
|
||||||
|
vec4 h = max(0.6 - vec4(
|
||||||
|
dot(d0, d0),
|
||||||
|
dot(d1, d1),
|
||||||
|
dot(d2, d2),
|
||||||
|
dot(d3, d3)
|
||||||
|
), 0.0);
|
||||||
|
vec4 n = h * h * h * h * vec4(
|
||||||
|
dot(d0, hash33(i)),
|
||||||
|
dot(d1, hash33(i + i1)),
|
||||||
|
dot(d2, hash33(i + i2)),
|
||||||
|
dot(d3, hash33(i + 1.0))
|
||||||
|
);
|
||||||
|
return dot(vec4(31.316), n);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 extractAlpha(vec3 colorIn) {
|
||||||
|
float a = max(max(colorIn.r, colorIn.g), colorIn.b);
|
||||||
|
return vec4(colorIn.rgb / (a + 1e-5), a);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
|
||||||
|
const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
|
||||||
|
const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
|
||||||
|
const float innerRadius = 0.6;
|
||||||
|
const float noiseScale = 0.65;
|
||||||
|
|
||||||
|
float light1(float intensity, float attenuation, float dist) {
|
||||||
|
return intensity / (1.0 + dist * attenuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
float light2(float intensity, float attenuation, float dist) {
|
||||||
|
return intensity / (1.0 + dist * dist * attenuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 draw(vec2 uv) {
|
||||||
|
vec3 color1 = adjustHue(baseColor1, hue);
|
||||||
|
vec3 color2 = adjustHue(baseColor2, hue);
|
||||||
|
vec3 color3 = adjustHue(baseColor3, hue);
|
||||||
|
|
||||||
|
float ang = atan(uv.y, uv.x);
|
||||||
|
float len = length(uv);
|
||||||
|
float invLen = len > 0.0 ? 1.0 / len : 0.0;
|
||||||
|
|
||||||
|
float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
|
||||||
|
float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
|
||||||
|
float d0 = distance(uv, (r0 * invLen) * uv);
|
||||||
|
float v0 = light1(1.0, 10.0, d0);
|
||||||
|
v0 *= smoothstep(r0 * 1.05, r0, len);
|
||||||
|
float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
float a = iTime * -1.0;
|
||||||
|
vec2 pos = vec2(cos(a), sin(a)) * r0;
|
||||||
|
float d = distance(uv, pos);
|
||||||
|
float v1 = light2(1.5, 5.0, d);
|
||||||
|
v1 *= light1(1.0, 50.0, d0);
|
||||||
|
|
||||||
|
float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
|
||||||
|
float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
|
||||||
|
|
||||||
|
vec3 col = mix(color1, color2, cl);
|
||||||
|
col = mix(color3, col, v0);
|
||||||
|
col = (col + v1) * v2 * v3;
|
||||||
|
col = clamp(col, 0.0, 1.0);
|
||||||
|
|
||||||
|
return extractAlpha(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 mainImage(vec2 fragCoord) {
|
||||||
|
vec2 center = iResolution.xy * 0.5;
|
||||||
|
float size = min(iResolution.x, iResolution.y);
|
||||||
|
vec2 uv = (fragCoord - center) / size * 2.0;
|
||||||
|
|
||||||
|
float angle = rot;
|
||||||
|
float s = sin(angle);
|
||||||
|
float c = cos(angle);
|
||||||
|
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
|
||||||
|
|
||||||
|
uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
|
||||||
|
uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
|
||||||
|
|
||||||
|
return draw(uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 fragCoord = vUv * iResolution.xy;
|
||||||
|
vec4 col = mainImage(fragCoord);
|
||||||
|
gl_FragColor = vec4(col.rgb * col.a, col.a);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
let cleanupAnimation: (() => void) | null = null;
|
||||||
|
|
||||||
|
const setupAnimation = () => {
|
||||||
|
const container = ctnDom.value;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
|
||||||
|
const gl = renderer.gl;
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
container.appendChild(gl.canvas);
|
||||||
|
|
||||||
|
const geometry = new Triangle(gl);
|
||||||
|
const program = new Program(gl, {
|
||||||
|
vertex: vert,
|
||||||
|
fragment: frag,
|
||||||
|
uniforms: {
|
||||||
|
iTime: { value: 0 },
|
||||||
|
iResolution: {
|
||||||
|
value: new Vec3(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
||||||
|
},
|
||||||
|
hue: { value: props.hue },
|
||||||
|
hover: { value: 0 },
|
||||||
|
rot: { value: 0 },
|
||||||
|
hoverIntensity: { value: props.hoverIntensity }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (!container) return;
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
renderer.setSize(width * dpr, height * dpr);
|
||||||
|
gl.canvas.style.width = width + 'px';
|
||||||
|
gl.canvas.style.height = height + 'px';
|
||||||
|
program.uniforms.iResolution.value.set(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height);
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
resize();
|
||||||
|
|
||||||
|
let targetHover = 0;
|
||||||
|
let lastTime = 0;
|
||||||
|
let currentRot = 0;
|
||||||
|
const rotationSpeed = 0.3;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const width = rect.width;
|
||||||
|
const height = rect.height;
|
||||||
|
const size = Math.min(width, height);
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = height / 2;
|
||||||
|
const uvX = ((x - centerX) / size) * 2.0;
|
||||||
|
const uvY = ((y - centerY) / size) * 2.0;
|
||||||
|
|
||||||
|
if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
|
||||||
|
targetHover = 1;
|
||||||
|
} else {
|
||||||
|
targetHover = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
targetHover = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener('mousemove', handleMouseMove);
|
||||||
|
container.addEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|
||||||
|
let rafId: number;
|
||||||
|
const update = (t: number) => {
|
||||||
|
rafId = requestAnimationFrame(update);
|
||||||
|
const dt = (t - lastTime) * 0.001;
|
||||||
|
lastTime = t;
|
||||||
|
program.uniforms.iTime.value = t * 0.001;
|
||||||
|
program.uniforms.hue.value = props.hue;
|
||||||
|
program.uniforms.hoverIntensity.value = props.hoverIntensity;
|
||||||
|
|
||||||
|
const effectiveHover = props.forceHoverState ? 1 : targetHover;
|
||||||
|
program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
|
||||||
|
|
||||||
|
if (props.rotateOnHover && effectiveHover > 0.5) {
|
||||||
|
currentRot += dt * rotationSpeed;
|
||||||
|
}
|
||||||
|
program.uniforms.rot.value = currentRot;
|
||||||
|
|
||||||
|
renderer.render({ scene: mesh });
|
||||||
|
};
|
||||||
|
rafId = requestAnimationFrame(update);
|
||||||
|
|
||||||
|
cleanupAnimation = () => {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
window.removeEventListener('resize', resize);
|
||||||
|
container.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
container.removeChild(gl.canvas);
|
||||||
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setupAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (cleanupAnimation) {
|
||||||
|
cleanupAnimation();
|
||||||
|
cleanupAnimation = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props,
|
||||||
|
() => {
|
||||||
|
if (cleanupAnimation) {
|
||||||
|
cleanupAnimation();
|
||||||
|
cleanupAnimation = null;
|
||||||
|
}
|
||||||
|
setupAnimation();
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="ctnDom" class="w-full h-full" />
|
||||||
|
</template>
|
||||||
@@ -29,7 +29,7 @@ body {
|
|||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 2.3em;
|
right: 2.3em;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
margin-top: 1.4rem;
|
margin-top: 1.4rem;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div:has(> .props-table) {
|
div:has(> .props-table) {
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ div:has(> .props-table) {
|
|||||||
.star-border-demo .inner-content {
|
.star-border-demo .inner-content {
|
||||||
padding: 1em 3em;
|
padding: 1em 3em;
|
||||||
background-color: #0b0b0b !important;
|
background-color: #0b0b0b !important;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-bounceCards {
|
.custom-bounceCards {
|
||||||
@@ -204,7 +204,7 @@ div:has(> .props-table) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -214,7 +214,7 @@ div:has(> .props-table) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-header:hover {
|
.tab-header:hover {
|
||||||
background: #142216;
|
background: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-example {
|
.code-example {
|
||||||
@@ -233,7 +233,7 @@ div:has(> .props-table) {
|
|||||||
.code-wrapper {
|
.code-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ div:has(> .props-table) {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background-color: #0b0b0b;
|
background-color: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -291,7 +291,7 @@ div:has(> .props-table) {
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ div:has(> .props-table) {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
background: #333333;
|
background: #333333;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
@@ -353,7 +353,7 @@ div:has(> .props-table) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contribute-button:hover {
|
.contribute-button:hover {
|
||||||
background: #142216;
|
background: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contribute-separator {
|
.contribute-separator {
|
||||||
@@ -364,7 +364,7 @@ div:has(> .props-table) {
|
|||||||
|
|
||||||
.contribute-container {
|
.contribute-container {
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 46px;
|
padding: 46px;
|
||||||
margin-top: 1.4rem;
|
margin-top: 1.4rem;
|
||||||
@@ -376,7 +376,7 @@ div:has(> .props-table) {
|
|||||||
|
|
||||||
.contribute-button {
|
.contribute-button {
|
||||||
background-color: #0b0b0b !important;
|
background-color: #0b0b0b !important;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
transition: 0.5s ease;
|
transition: 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,7 @@ div:has(> .props-table) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contribute-button:hover {
|
.contribute-button:hover {
|
||||||
background-color: #142216 !important;
|
background-color: #222 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-details {
|
.demo-details {
|
||||||
@@ -395,14 +395,15 @@ div:has(> .props-table) {
|
|||||||
gap: 0.3em;
|
gap: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-details span {
|
.dependency-tag {
|
||||||
background-color: #111;
|
background-color: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: #a7ef9e;
|
color: #27ff64;
|
||||||
border-radius: 10px;
|
border-radius: 50px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 0.5em 1em;
|
font-weight: 400;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
transition: 0.3s ease;
|
transition: 0.3s ease;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
@@ -424,7 +425,7 @@ div:has(> .props-table) {
|
|||||||
margin: 2em auto 2em;
|
margin: 2em auto 2em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, transparent, #142216, transparent);
|
background: linear-gradient(to right, transparent, #333, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cli-divider::before {
|
.cli-divider::before {
|
||||||
@@ -509,7 +510,7 @@ div:has(> .props-table) {
|
|||||||
|
|
||||||
.custom-spotlight-card {
|
.custom-spotlight-card {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border: 1px solid #142216 !important;
|
border: 1px solid #333 !important;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
background: #142216;
|
background: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
padding: 0 0.5rem 0 0.75rem;
|
padding: 0 0.5rem 0 0.75rem;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
background: #0b0b0b;
|
background: #0b0b0b;
|
||||||
border: 1px solid #142216;
|
border: 1px solid #333;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -366,7 +366,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.search-icon {
|
||||||
color: #142216;
|
color: #333;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,18 +14,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<div class="space-y-2">
|
<div class="flex gap-2">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Color Stops</h3>
|
|
||||||
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<PreviewColor
|
<PreviewColor
|
||||||
v-for="(color, index) in colorStops"
|
v-for="(_, index) in colorStops"
|
||||||
:key="index"
|
:key="index"
|
||||||
:title="`Color ${index + 1}`"
|
:title="`Color ${index + 1}`"
|
||||||
v-model="colorStops[index]"
|
v-model="colorStops[index]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<PreviewSlider title="Amplitude" v-model="amplitude" :min="0" :max="2" :step="0.1" />
|
<PreviewSlider title="Amplitude" v-model="amplitude" :min="0" :max="2" :step="0.1" />
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Customize>
|
<Customize>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-2">
|
||||||
<PreviewColor
|
<PreviewColor
|
||||||
v-for="(_, index) in colorStops"
|
v-for="(_, index) in colorStops"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PreviewSlider title="Pixelation" v-model="pixelation" :min="0" :max="2000" :step="10" />
|
<PreviewSlider title="Pixelation" v-model="pixelation" :min="100" :max="2000" :step="10" />
|
||||||
|
|
||||||
<PreviewSwitch title="Enable Mouse Interaction" v-model="mouseInteractionEnabled" />
|
<PreviewSwitch title="Enable Mouse Interaction" v-model="mouseInteractionEnabled" />
|
||||||
|
|
||||||
|
|||||||
220
src/demo/Backgrounds/BallpitDemo.vue
Normal file
220
src/demo/Backgrounds/BallpitDemo.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative p-0 h-[500px] overflow-hidden demo-container">
|
||||||
|
<RefreshButton @click="forceRerender" />
|
||||||
|
<p class="z-0 absolute font-black text-[#271e37] text-[200px]">Balls.</p>
|
||||||
|
<Ballpit
|
||||||
|
className="relative"
|
||||||
|
:key="key"
|
||||||
|
:count="count"
|
||||||
|
:gravity="gravity"
|
||||||
|
:friction="friction"
|
||||||
|
:wallBounce="wallBounce"
|
||||||
|
:followCursor="followCursor"
|
||||||
|
:colors="colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSwitch title="Display Cursor" v-model="followCursor" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Ball Count"
|
||||||
|
:min="50"
|
||||||
|
:max="500"
|
||||||
|
:step="10"
|
||||||
|
v-model="count"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
count = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Gravity"
|
||||||
|
:min="0.1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.1"
|
||||||
|
v-model="gravity"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
gravity = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Friction"
|
||||||
|
:min="0.9"
|
||||||
|
:max="1"
|
||||||
|
:step="0.001"
|
||||||
|
v-model="friction"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
friction = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Wall Bounce"
|
||||||
|
:min="0.1"
|
||||||
|
:max="1"
|
||||||
|
:step="0.05"
|
||||||
|
v-model="wallBounce"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
wallBounce = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['three']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="ballpit" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="ballpit.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import RefreshButton from '../../components/common/RefreshButton.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import { ballpit } from '../../constants/code/Backgrounds/ballpitCode';
|
||||||
|
import Ballpit from '../../content/Backgrounds/Ballpit/Ballpit.vue';
|
||||||
|
|
||||||
|
const { rerenderKey: key, forceRerender } = useForceRerender();
|
||||||
|
|
||||||
|
const count = ref(100);
|
||||||
|
const gravity = ref(0.5);
|
||||||
|
const friction = ref(0.9975);
|
||||||
|
const wallBounce = ref(0.95);
|
||||||
|
const followCursor = ref(false);
|
||||||
|
|
||||||
|
const colors = [0xffffff, 0x000000, 0x27ff64];
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[count, gravity, friction, wallBounce, followCursor],
|
||||||
|
() => {
|
||||||
|
forceRerender();
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'count',
|
||||||
|
type: 'number',
|
||||||
|
default: '200',
|
||||||
|
description: 'Sets the number of balls in the ballpit.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gravity',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Controls the gravity affecting the balls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'friction',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.9975',
|
||||||
|
description: 'Sets the friction applied to the ball movement.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wallBounce',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.95',
|
||||||
|
description: 'Determines how much balls bounce off walls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'followCursor',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Enables or disables the sphere following the cursor.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'colors',
|
||||||
|
type: 'array',
|
||||||
|
default: '[0, 0, 0]',
|
||||||
|
description: 'Defines the colors of the balls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ambientColor',
|
||||||
|
type: 'number',
|
||||||
|
default: '16777215',
|
||||||
|
description: 'Sets the ambient light color.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ambientIntensity',
|
||||||
|
type: 'number',
|
||||||
|
default: '1',
|
||||||
|
description: 'Controls the intensity of ambient light.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lightIntensity',
|
||||||
|
type: 'number',
|
||||||
|
default: '200',
|
||||||
|
description: 'Sets the intensity of the main light source.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minSize',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.5',
|
||||||
|
description: 'Specifies the minimum size of the balls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxSize',
|
||||||
|
type: 'number',
|
||||||
|
default: '1',
|
||||||
|
description: 'Specifies the maximum size of the balls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'size0',
|
||||||
|
type: 'number',
|
||||||
|
default: '1',
|
||||||
|
description: 'Initial size value for the cursor ball.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxVelocity',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.15',
|
||||||
|
description: 'Limits the maximum velocity of the balls.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxX',
|
||||||
|
type: 'number',
|
||||||
|
default: '5',
|
||||||
|
description: 'Defines the maximum X-coordinate boundary.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxY',
|
||||||
|
type: 'number',
|
||||||
|
default: '5',
|
||||||
|
description: 'Defines the maximum Y-coordinate boundary.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxZ',
|
||||||
|
type: 'number',
|
||||||
|
default: '2',
|
||||||
|
description: 'Defines the maximum Z-coordinate boundary.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
126
src/demo/Backgrounds/OrbDemo.vue
Normal file
126
src/demo/Backgrounds/OrbDemo.vue
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<TabbedLayout>
|
||||||
|
<template #preview>
|
||||||
|
<div class="relative p-0 h-[500px] overflow-hidden demo-container">
|
||||||
|
<Orb
|
||||||
|
:hue="debouncedHue"
|
||||||
|
:hoverIntensity="debouncedHoverIntensity"
|
||||||
|
:rotateOnHover="rotateOnHover"
|
||||||
|
:forceHoverState="forceHoverState"
|
||||||
|
/>
|
||||||
|
<p class="z-0 absolute mb-0 font-black text-[clamp(2rem,2vw,6rem)] mix-blend-difference">Hover.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Customize>
|
||||||
|
<PreviewSlider
|
||||||
|
title="Hue Shift"
|
||||||
|
:min="0"
|
||||||
|
:max="360"
|
||||||
|
:step="1"
|
||||||
|
v-model="hue"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
hue = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSlider
|
||||||
|
title="Hover Intensity"
|
||||||
|
:min="0"
|
||||||
|
:max="5"
|
||||||
|
:step="0.01"
|
||||||
|
v-model="hoverIntensity"
|
||||||
|
@onChange="
|
||||||
|
(val: number) => {
|
||||||
|
hoverIntensity = val;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PreviewSwitch title="Rotate on Hover" v-model="rotateOnHover" @update:model-value="forceRerender" />
|
||||||
|
|
||||||
|
<PreviewSwitch title="Force Hover State" v-model="forceHoverState" @update:model-value="forceRerender" />
|
||||||
|
</Customize>
|
||||||
|
|
||||||
|
<PropTable :data="propData" />
|
||||||
|
<Dependencies :dependency-list="['ogl']" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #code>
|
||||||
|
<CodeExample :code-object="orb" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cli>
|
||||||
|
<CliInstallation :command="orb.cli" />
|
||||||
|
</template>
|
||||||
|
</TabbedLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForceRerender } from '@/composables/useForceRerender';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||||
|
import CodeExample from '../../components/code/CodeExample.vue';
|
||||||
|
import Dependencies from '../../components/code/Dependencies.vue';
|
||||||
|
import Customize from '../../components/common/Customize.vue';
|
||||||
|
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||||
|
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||||
|
import PropTable from '../../components/common/PropTable.vue';
|
||||||
|
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||||
|
import { orb } from '../../constants/code/Backgrounds/orbCode';
|
||||||
|
import Orb from '../../content/Backgrounds/Orb/Orb.vue';
|
||||||
|
|
||||||
|
const { forceRerender } = useForceRerender();
|
||||||
|
|
||||||
|
const hue = ref(100);
|
||||||
|
const hoverIntensity = ref(0.5);
|
||||||
|
const rotateOnHover = ref(true);
|
||||||
|
const forceHoverState = ref(false);
|
||||||
|
|
||||||
|
const debouncedHue = ref(100);
|
||||||
|
const debouncedHoverIntensity = ref(0.5);
|
||||||
|
|
||||||
|
let hueTimeout: number;
|
||||||
|
let hoverIntensityTimeout: number;
|
||||||
|
|
||||||
|
watch(hue, newValue => {
|
||||||
|
clearTimeout(hueTimeout);
|
||||||
|
hueTimeout = setTimeout(() => {
|
||||||
|
debouncedHue.value = newValue;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
watch(hoverIntensity, newValue => {
|
||||||
|
clearTimeout(hoverIntensityTimeout);
|
||||||
|
hoverIntensityTimeout = setTimeout(() => {
|
||||||
|
debouncedHoverIntensity.value = newValue;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
const propData = [
|
||||||
|
{
|
||||||
|
name: 'hue',
|
||||||
|
type: 'number',
|
||||||
|
default: '100',
|
||||||
|
description: 'The base hue for the orb (in degrees).'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hoverIntensity',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.2',
|
||||||
|
description: 'Controls the intensity of the hover distortion effect.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rotateOnHover',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
description: 'Toggle to enable or disable continuous rotation on hover.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'forceHoverState',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
description: 'Force hover animations even when the orb is not actually hovered.'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -40,7 +40,7 @@ export const customToastStyles = {
|
|||||||
message: {
|
message: {
|
||||||
style: {
|
style: {
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
border: '1px solid #142216',
|
border: '1px solid #333',
|
||||||
backgroundColor: '#0b0b0b'
|
backgroundColor: '#0b0b0b'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user