mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Added <Orb /> background
This commit is contained in:
@@ -88,7 +88,8 @@ export const CATEGORIES = [
|
|||||||
'Squares',
|
'Squares',
|
||||||
'Iridescence',
|
'Iridescence',
|
||||||
'Threads',
|
'Threads',
|
||||||
'Grid Motion'
|
'Grid Motion',
|
||||||
|
'Orb',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ 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')
|
||||||
};
|
};
|
||||||
|
|
||||||
export const componentMap = {
|
export const componentMap = {
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
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>
|
||||||
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>
|
||||||
Reference in New Issue
Block a user