FEAT: 🎉 Added <LightPillar /> background

This commit is contained in:
Utkarsh-Singhal-26
2025-12-04 14:17:04 +05:30
parent 937c5bc71e
commit 654b9672a5
9 changed files with 603 additions and 3 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -19,10 +19,10 @@
/>
<div class="hero-main-content">
<router-link to="/backgrounds/floating-lines" class="hero-new-badge-container">
<router-link to="/backgrounds/light-pillar" class="hero-new-badge-container">
<span class="hero-new-badge">New 🎉</span>
<div class="hero-new-badge-text">
<span>Floating Lines</span>
<span>Light Pillar</span>
<i class="pi-arrow-right pi" style="font-size: 0.8rem"></i>
</div>
</router-link>

View File

@@ -1,5 +1,5 @@
// Highlighted sidebar items
export const NEW = ['Color Bends', 'Ghost Cursor', 'Laser Flow', 'Liquid Ether', 'Pixel Blast', 'Floating Lines'];
export const NEW = ['Color Bends', 'Ghost Cursor', 'Laser Flow', 'Liquid Ether', 'Pixel Blast', 'Floating Lines', 'Light Pillar'];
export const UPDATED = [];
// Used for main sidebar navigation
@@ -123,6 +123,7 @@ export const CATEGORIES = [
'Iridescence',
'Letter Glitch',
'Lightning',
'Light Pillar',
'Light Rays',
'Liquid Chrome',
'Liquid Ether',

View File

@@ -121,6 +121,7 @@ const backgrounds = {
'liquid-ether': () => import('../demo/Backgrounds/LiquidEtherDemo.vue'),
'color-bends': () => import('../demo/Backgrounds/ColorBendsDemo.vue'),
'floating-lines': () => import('../demo/Backgrounds/FloatingLinesDemo.vue'),
'light-pillar': () => import('../demo/Backgrounds/LightPillarDemo.vue'),
};
export const componentMap = {

View File

@@ -948,5 +948,13 @@ export const componentMetadata: ComponentMetadata = {
name: 'LiquidEther',
docsUrl: 'https://vue-bits.dev/backgrounds/liquid-ether',
tags: []
},
'Backgrounds/LightPillar': {
videoUrl: '/assets/videos/lightpillar.webm',
description: 'Vertical pillar of light with glow effects.',
category: 'Backgrounds',
name: 'LightPillar',
docsUrl: 'https://vue-bits.dev/backgrounds/light-pillar',
tags: []
}
};

View File

@@ -0,0 +1,27 @@
import code from '@content/Backgrounds/LightPillar/LightPillar.vue?raw';
import { createCodeObject } from '../../../types/code';
export const lightPillar = createCodeObject(code, 'Backgrounds/LightPillar', {
installation: `npm install three`,
usage: `<template>
<div style="width: 100%; height: 600px; position: relative;">
<LightPillar
topColor="#48FF28"
bottomColor="#9EF19E"
:intensity="1.0"
:rotationSpeed="0.3"
:glowAmount="0.005"
:pillarWidth="3.0"
:pillarHeight="0.4"
:noiseIntensity="0.5"
:pillarRotation="0"
:interactive="false"
mixBlendMode="normal"
/>
</div>
</template>
<script setup>
import LightPillar from './LightPillar.vue'
</script>`
});

View File

@@ -0,0 +1,402 @@
<script setup lang="ts">
import * as THREE from 'three';
import {
onBeforeMount,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
useTemplateRef,
watch,
type CSSProperties
} from 'vue';
interface LightPillarProps {
topColor?: string;
bottomColor?: string;
intensity?: number;
rotationSpeed?: number;
interactive?: boolean;
className?: string;
glowAmount?: number;
pillarWidth?: number;
pillarHeight?: number;
noiseIntensity?: number;
mixBlendMode?: CSSProperties['mixBlendMode'];
pillarRotation?: number;
}
const props = withDefaults(defineProps<LightPillarProps>(), {
topColor: '#48FF28',
bottomColor: '#9EF19E',
intensity: 1.0,
rotationSpeed: 0.3,
interactive: false,
className: '',
glowAmount: 0.005,
pillarWidth: 3.0,
pillarHeight: 0.4,
noiseIntensity: 0.5,
mixBlendMode: 'screen',
pillarRotation: 0
});
const containerRef = useTemplateRef('containerRef');
const rafRef = ref<number | null>(null);
const rendererRef = shallowRef<THREE.WebGLRenderer | null>(null);
const materialRef = shallowRef<THREE.ShaderMaterial | null>(null);
const sceneRef = shallowRef<THREE.Scene | null>(null);
const cameraRef = shallowRef<THREE.OrthographicCamera | null>(null);
const geometryRef = shallowRef<THREE.PlaneGeometry | null>(null);
const mouseRef = ref<THREE.Vector2>(new THREE.Vector2(0, 0));
const timeRef = ref<number>(0);
const webGLSupported = ref<boolean>(true);
onBeforeMount(() => {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
webGLSupported.value = false;
console.warn('WebGL is not supported in this browser');
}
canvas.remove();
});
let cleanup: (() => void) | null = null;
const setup = () => {
if (!containerRef.value || !webGLSupported.value) return;
const container = containerRef.value;
const width = container.clientWidth;
const height = container.clientHeight;
// Scene setup
const scene = new THREE.Scene();
sceneRef.value = scene;
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
cameraRef.value = camera;
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({
antialias: false,
alpha: true,
powerPreference: 'high-performance',
precision: 'lowp',
stencil: false,
depth: false
});
} catch (error) {
console.error('Failed to create WebGL renderer:', error);
webGLSupported.value = false;
return;
}
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
container.appendChild(renderer.domElement);
rendererRef.value = renderer;
// Convert hex colors to RGB
const parseColor = (hex: string): THREE.Vector3 => {
const color = new THREE.Color(hex);
return new THREE.Vector3(color.r, color.g, color.b);
};
// Shader material
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform vec3 uTopColor;
uniform vec3 uBottomColor;
uniform float uIntensity;
uniform bool uInteractive;
uniform float uGlowAmount;
uniform float uPillarWidth;
uniform float uPillarHeight;
uniform float uNoiseIntensity;
uniform float uPillarRotation;
varying vec2 vUv;
const float PI = 3.141592653589793;
const float EPSILON = 0.001;
const float E = 2.71828182845904523536;
const float HALF = 0.5;
mat2 rot(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat2(c, -s, s, c);
}
// Procedural noise function
float noise(vec2 coord) {
float G = E;
vec2 r = (G * sin(G * coord));
return fract(r.x * r.y * (1.0 + coord.x));
}
// Apply layered wave deformation to position
vec3 applyWaveDeformation(vec3 pos, float timeOffset) {
float frequency = 1.0;
float amplitude = 1.0;
vec3 deformed = pos;
for(float i = 0.0; i < 4.0; i++) {
deformed.xz *= rot(0.4);
float phase = timeOffset * i * 2.0;
vec3 oscillation = cos(deformed.zxy * frequency - phase);
deformed += oscillation * amplitude;
frequency *= 2.0;
amplitude *= HALF;
}
return deformed;
}
// Polynomial smooth blending between two values
float blendMin(float a, float b, float k) {
float scaledK = k * 4.0;
float h = max(scaledK - abs(a - b), 0.0);
return min(a, b) - h * h * 0.25 / scaledK;
}
float blendMax(float a, float b, float k) {
return -blendMin(-a, -b, k);
}
void main() {
vec2 fragCoord = vUv * uResolution;
vec2 uv = (fragCoord * 2.0 - uResolution) / uResolution.y;
// Apply 2D rotation to UV coordinates
float rotAngle = uPillarRotation * PI / 180.0;
uv *= rot(rotAngle);
vec3 origin = vec3(0.0, 0.0, -10.0);
vec3 direction = normalize(vec3(uv, 1.0));
float maxDepth = 50.0;
float depth = 0.1;
mat2 rotX = rot(uTime * 0.3);
if(uInteractive && length(uMouse) > 0.0) {
rotX = rot(uMouse.x * PI * 2.0);
}
vec3 color = vec3(0.0);
for(float i = 0.0; i < 100.0; i++) {
vec3 pos = origin + direction * depth;
pos.xz *= rotX;
// Apply vertical scaling and wave deformation
vec3 deformed = pos;
deformed.y *= uPillarHeight;
deformed = applyWaveDeformation(deformed + vec3(0.0, uTime, 0.0), uTime);
// Calculate distance field using cosine pattern
vec2 cosinePair = cos(deformed.xz);
float fieldDistance = length(cosinePair) - 0.2;
// Radial boundary constraint
float radialBound = length(pos.xz) - uPillarWidth;
fieldDistance = blendMax(radialBound, fieldDistance, 1.0);
fieldDistance = abs(fieldDistance) * 0.15 + 0.01;
vec3 gradient = mix(uBottomColor, uTopColor, smoothstep(15.0, -15.0, pos.y));
color += gradient * pow(1.0 / fieldDistance, 1.0);
if(fieldDistance < EPSILON || depth > maxDepth) break;
depth += fieldDistance;
}
// Normalize by pillar width to maintain consistent glow regardless of size
float widthNormalization = uPillarWidth / 3.0;
color = tanh(color * uGlowAmount / widthNormalization);
// Add noise postprocessing
float rnd = noise(gl_FragCoord.xy);
color -= rnd / 15.0 * uNoiseIntensity;
gl_FragColor = vec4(color * uIntensity, 1.0);
}
`;
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector2(width, height) },
uMouse: { value: mouseRef.value },
uTopColor: { value: parseColor(props.topColor) },
uBottomColor: { value: parseColor(props.bottomColor) },
uIntensity: { value: props.intensity },
uInteractive: { value: props.interactive },
uGlowAmount: { value: props.glowAmount },
uPillarWidth: { value: props.pillarWidth },
uPillarHeight: { value: props.pillarHeight },
uNoiseIntensity: { value: props.noiseIntensity },
uPillarRotation: { value: props.pillarRotation }
},
transparent: true,
depthWrite: false,
depthTest: false
});
materialRef.value = material;
const geometry = new THREE.PlaneGeometry(2, 2);
geometryRef.value = geometry;
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Mouse interaction - throttled for performance
let mouseMoveTimeout: number | null = null;
const handleMouseMove = (event: MouseEvent) => {
if (!props.interactive) return;
if (mouseMoveTimeout) return;
mouseMoveTimeout = window.setTimeout(() => {
mouseMoveTimeout = null;
}, 16); // ~60fps throttle
const rect = container.getBoundingClientRect();
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
mouseRef.value.set(x, y);
};
if (props.interactive) {
container.addEventListener('mousemove', handleMouseMove, { passive: true });
}
// Animation loop with fixed timestep
let lastTime = performance.now();
const targetFPS = 60;
const frameTime = 1000 / targetFPS;
const animate = (currentTime: number) => {
if (!materialRef.value || !rendererRef.value || !sceneRef.value || !cameraRef.value) return;
const deltaTime = currentTime - lastTime;
if (deltaTime >= frameTime) {
timeRef.value += 0.016 * props.rotationSpeed;
materialRef.value.uniforms.uTime.value = timeRef.value;
rendererRef.value.render(sceneRef.value, cameraRef.value);
lastTime = currentTime - (deltaTime % frameTime);
}
rafRef.value = requestAnimationFrame(animate);
};
rafRef.value = requestAnimationFrame(animate);
// Handle resize with debouncing
let resizeTimeout: number | null = null;
const handleResize = () => {
if (resizeTimeout) {
clearTimeout(resizeTimeout);
}
resizeTimeout = window.setTimeout(() => {
if (!rendererRef.value || !materialRef.value || !containerRef.value) return;
const newWidth = containerRef.value.clientWidth;
const newHeight = containerRef.value.clientHeight;
rendererRef.value.setSize(newWidth, newHeight);
materialRef.value.uniforms.uResolution.value.set(newWidth, newHeight);
}, 150);
};
window.addEventListener('resize', handleResize, { passive: true });
// Cleanup
cleanup = () => {
window.removeEventListener('resize', handleResize);
if (props.interactive) {
container.removeEventListener('mousemove', handleMouseMove);
}
if (rafRef.value) {
cancelAnimationFrame(rafRef.value);
}
if (rendererRef.value) {
rendererRef.value.dispose();
rendererRef.value.forceContextLoss();
if (container.contains(rendererRef.value.domElement)) {
container.removeChild(rendererRef.value.domElement);
}
}
if (materialRef.value) {
materialRef.value.dispose();
}
if (geometryRef.value) {
geometryRef.value.dispose();
}
rendererRef.value = null;
materialRef.value = null;
sceneRef.value = null;
cameraRef.value = null;
geometryRef.value = null;
rafRef.value = null;
};
};
onMounted(() => {
setup();
});
onBeforeUnmount(() => {
cleanup?.();
});
watch(
() => [
props.topColor,
props.bottomColor,
props.intensity,
props.rotationSpeed,
props.interactive,
props.glowAmount,
props.pillarWidth,
props.pillarHeight,
props.noiseIntensity,
props.pillarRotation,
webGLSupported.value
],
() => {
cleanup?.();
setup();
},
{
deep: true
}
);
</script>
<template>
<div
v-if="!webGLSupported"
:class="`w-full h-full absolute top-0 left-0 flex items-center justify-center bg-black/10 text-gray-500 text-sm ${className}`"
:style="{ mixBlendMode }"
>
WebGL not supported
</div>
<div
v-else
ref="containerRef"
:class="`w-full h-full absolute top-0 left-0 ${className}`"
:style="{ mixBlendMode }"
/>
</template>

View File

@@ -0,0 +1,161 @@
<template>
<TabbedLayout>
<template #preview>
<div class="relative p-0 h-[600px] overflow-hidden demo-container">
<LightPillar
:top-color="topColor"
:bottom-color="bottomColor"
:intensity="intensity"
:rotation-speed="rotationSpeed"
:interactive="interactive"
:glow-amount="glowAmount"
:pillar-width="pillarWidth"
:pillar-height="pillarHeight"
:noise-intensity="noiseIntensity"
:pillar-rotation="pillarRotation"
:mix-blend-mode="mixBlendMode"
/>
<BackgroundContent pill-text="New Background" headline="Ethereal light pillar for your hero sections." />
</div>
<Customize>
<PreviewColor v-model="topColor" title="Top Color" />
<PreviewColor v-model="bottomColor" title="Bottom Color" class="mt-4" />
<PreviewSlider :min="0.1" :max="3" :step="0.1" v-model="intensity" title="Intensity" />
<PreviewSlider :min="0" :max="2" :step="0.1" v-model="rotationSpeed" title="Rotation Speed" />
<PreviewSlider :min="0.001" :max="0.02" :step="0.001" v-model="glowAmount" title="Glow Amount" />
<PreviewSlider :min="1" :max="10" :step="0.1" v-model="pillarWidth" title="Pillar Width" />
<PreviewSlider :min="0.1" :max="2" :step="0.1" v-model="pillarHeight" title="Pillar Height" />
<PreviewSlider :min="0" :max="2" :step="0.1" v-model="noiseIntensity" title="Noise Intensity" />
<PreviewSlider :min="0" :max="360" :step="1" v-model="pillarRotation" title="Pillar Rotation" />
<PreviewSwitch title="Interactive Rotation" v-model="interactive" />
<PreviewSelect :options="blendModeOptions" v-model="mixBlendMode" title="Mix Blend Mode" />
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['three']" />
</template>
<template #code>
<CodeExample :code-object="lightPillar" />
</template>
<template #cli>
<CliInstallation :command="lightPillar.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { lightPillar } from '@/constants/code/Backgrounds/lightPillarCode';
import { ref, type CSSProperties } from 'vue';
import CliInstallation from '../../components/code/CliInstallation.vue';
import CodeExample from '../../components/code/CodeExample.vue';
import Dependencies from '../../components/code/Dependencies.vue';
import BackgroundContent from '../../components/common/BackgroundContent.vue';
import Customize from '../../components/common/Customize.vue';
import PreviewSlider from '../../components/common/PreviewSlider.vue';
import PreviewColor from '../../components/common/PreviewColor.vue';
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
import PreviewSelect from '../../components/common/PreviewSelect.vue';
import PropTable from '../../components/common/PropTable.vue';
import TabbedLayout from '../../components/common/TabbedLayout.vue';
import LightPillar from '../../content/Backgrounds/LightPillar/LightPillar.vue';
const topColor = ref('#48FF28');
const bottomColor = ref('#9EF19E');
const intensity = ref(1.0);
const rotationSpeed = ref(0.3);
const interactive = ref(false);
const glowAmount = ref(0.002);
const pillarWidth = ref(3.0);
const pillarHeight = ref(0.4);
const noiseIntensity = ref(0.5);
const pillarRotation = ref(25);
const mixBlendMode = ref<CSSProperties['mixBlendMode']>('screen');
const blendModeOptions = [
{ value: 'normal', label: 'Normal' },
{ value: 'screen', label: 'Screen' },
{ value: 'darken', label: 'Darken' },
{ value: 'lighten', label: 'Lighten' },
{ value: 'color-dodge', label: 'Color Dodge' },
{ value: 'luminosity', label: 'Luminosity' }
];
const propData = [
{
name: 'topColor',
type: 'string',
default: "'#48FF28'",
description: 'Hex color string for the top gradient color of the light pillar.'
},
{
name: 'bottomColor',
type: 'string',
default: "'#9EF19E'",
description: 'Hex color string for the bottom gradient color of the light pillar.'
},
{
name: 'intensity',
type: 'number',
default: '1.0',
description: 'Controls the overall brightness and intensity of the effect.'
},
{
name: 'rotationSpeed',
type: 'number',
default: '0.3',
description: 'Speed multiplier for the pillar rotation animation.'
},
{
name: 'interactive',
type: 'boolean',
default: 'false',
description: 'Enable mouse interaction to control the pillar rotation.'
},
{
name: 'glowAmount',
type: 'number',
default: '0.005',
description: 'Controls the glow intensity and spread of the light effect.'
},
{
name: 'pillarWidth',
type: 'number',
default: '3.0',
description: 'Width/radius of the light pillar.'
},
{
name: 'pillarHeight',
type: 'number',
default: '0.4',
description: 'Height scaling factor for the pillar distortion.'
},
{
name: 'noiseIntensity',
type: 'number',
default: '0.5',
description: 'Intensity of the film grain noise postprocessing effect.'
},
{
name: 'className',
type: 'string',
default: "''",
description: 'Additional CSS class names to apply to the container element.'
},
{
name: 'mixBlendMode',
type: 'string',
default: "'screen'",
description: 'CSS mix-blend-mode property to control how the component blends with its background.'
},
{
name: 'pillarRotation',
type: 'number',
default: '0',
description: 'Rotation angle of the pillar in degrees (0-360).'
}
];
</script>