mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 14:39:30 -07:00
Merge pull request #22 from snepsnepy/feat/balatro-background
Migrated Balatro background component.
This commit is contained in:
@@ -80,6 +80,7 @@ export const CATEGORIES = [
|
||||
'Hyperspeed',
|
||||
'Silk',
|
||||
'Lightning',
|
||||
'Balatro',
|
||||
'Letter Glitch',
|
||||
'Particles',
|
||||
'Shape Blur',
|
||||
|
||||
@@ -73,7 +73,8 @@ const backgrounds = {
|
||||
'beams': () => import('../demo/Backgrounds/BeamsDemo.vue'),
|
||||
'grid-motion': () => import('../demo/Backgrounds/GridMotionDemo.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')
|
||||
};
|
||||
|
||||
export const componentMap = {
|
||||
|
||||
19
src/constants/code/Backgrounds/balatroCode.ts
Normal file
19
src/constants/code/Backgrounds/balatroCode.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import code from '@content/Backgrounds/Balatro/Balatro.vue?raw';
|
||||
import type { CodeObject } from '../../../types/code';
|
||||
|
||||
export const balatro: CodeObject = {
|
||||
cli: `npx jsrepo add https://vue-bits.dev/ui/Backgrounds/Balatro`,
|
||||
installation: `npm install ogl`,
|
||||
usage: `<template>
|
||||
<Balatro
|
||||
:is-rotate="false"
|
||||
:mouse-interaction="true"
|
||||
:pixel-filter="700"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Balatro from "./Balatro.vue";
|
||||
</script>`,
|
||||
code
|
||||
};
|
||||
273
src/content/Backgrounds/Balatro/Balatro.vue
Normal file
273
src/content/Backgrounds/Balatro/Balatro.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div ref="containerRef" :class="className" :style="style" class="w-full h-full" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted, type CSSProperties } from 'vue';
|
||||
import { Renderer, Program, Mesh, Triangle } from 'ogl';
|
||||
|
||||
interface BalatroProps {
|
||||
spinRotation?: number;
|
||||
spinSpeed?: number;
|
||||
offset?: [number, number];
|
||||
color1?: string;
|
||||
color2?: string;
|
||||
color3?: string;
|
||||
contrast?: number;
|
||||
lighting?: number;
|
||||
spinAmount?: number;
|
||||
pixelFilter: number;
|
||||
spinEase?: number;
|
||||
isRotate: boolean;
|
||||
mouseInteraction: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<BalatroProps>(), {
|
||||
spinRotation: -2,
|
||||
spinSpeed: 7,
|
||||
offset: () => [0, 0],
|
||||
color1: '#DE443B',
|
||||
color2: '#006BB4',
|
||||
color3: '#162325',
|
||||
contrast: 3.5,
|
||||
lighting: 0.4,
|
||||
spinAmount: 0.25,
|
||||
pixelFilter: 745,
|
||||
spinEase: 1,
|
||||
isRotate: false,
|
||||
mouseInteraction: true,
|
||||
className: '',
|
||||
style: () => ({})
|
||||
});
|
||||
|
||||
const containerRef = ref<HTMLDivElement>();
|
||||
|
||||
const hexToVec4 = (hex: string): [number, number, number, number] => {
|
||||
const hexStr = hex.replace('#', '');
|
||||
let r = 0,
|
||||
g = 0,
|
||||
b = 0,
|
||||
a = 1;
|
||||
if (hexStr.length === 6) {
|
||||
r = parseInt(hexStr.slice(0, 2), 16) / 255;
|
||||
g = parseInt(hexStr.slice(2, 4), 16) / 255;
|
||||
b = parseInt(hexStr.slice(4, 6), 16) / 255;
|
||||
} else if (hexStr.length === 8) {
|
||||
r = parseInt(hexStr.slice(0, 2), 16) / 255;
|
||||
g = parseInt(hexStr.slice(2, 4), 16) / 255;
|
||||
b = parseInt(hexStr.slice(4, 6), 16) / 255;
|
||||
a = parseInt(hexStr.slice(6, 8), 16) / 255;
|
||||
}
|
||||
return [r, g, b, a];
|
||||
};
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
attribute vec2 position;
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = vec4(position, 0, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = `
|
||||
precision highp float;
|
||||
|
||||
#define PI 3.14159265359
|
||||
|
||||
uniform float iTime;
|
||||
uniform vec3 iResolution;
|
||||
uniform float uSpinRotation;
|
||||
uniform float uSpinSpeed;
|
||||
uniform vec2 uOffset;
|
||||
uniform vec4 uColor1;
|
||||
uniform vec4 uColor2;
|
||||
uniform vec4 uColor3;
|
||||
uniform float uContrast;
|
||||
uniform float uLighting;
|
||||
uniform float uSpinAmount;
|
||||
uniform float uPixelFilter;
|
||||
uniform float uSpinEase;
|
||||
uniform bool uIsRotate;
|
||||
uniform vec2 uMouse;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
vec4 effect(vec2 screenSize, vec2 screen_coords) {
|
||||
float pixel_size = length(screenSize.xy) / uPixelFilter;
|
||||
vec2 uv = (floor(screen_coords.xy * (1.0 / pixel_size)) * pixel_size - 0.5 * screenSize.xy) / length(screenSize.xy) - uOffset;
|
||||
float uv_len = length(uv);
|
||||
|
||||
float speed = (uSpinRotation * uSpinEase * 0.2);
|
||||
if(uIsRotate){
|
||||
speed = iTime * speed;
|
||||
}
|
||||
speed += 302.2;
|
||||
|
||||
float mouseInfluence = (uMouse.x * 2.0 - 1.0);
|
||||
speed += mouseInfluence * 0.1;
|
||||
|
||||
float new_pixel_angle = atan(uv.y, uv.x) + speed - uSpinEase * 20.0 * (uSpinAmount * uv_len + (1.0 - uSpinAmount));
|
||||
vec2 mid = (screenSize.xy / length(screenSize.xy)) / 2.0;
|
||||
uv = (vec2(uv_len * cos(new_pixel_angle) + mid.x, uv_len * sin(new_pixel_angle) + mid.y) - mid);
|
||||
|
||||
uv *= 30.0;
|
||||
float baseSpeed = iTime * uSpinSpeed;
|
||||
speed = baseSpeed + mouseInfluence * 2.0;
|
||||
|
||||
vec2 uv2 = vec2(uv.x + uv.y);
|
||||
|
||||
for(int i = 0; i < 5; i++) {
|
||||
uv2 += sin(max(uv.x, uv.y)) + uv;
|
||||
uv += 0.5 * vec2(
|
||||
cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121),
|
||||
sin(uv2.x - 0.113 * speed)
|
||||
);
|
||||
uv -= cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);
|
||||
}
|
||||
|
||||
float contrast_mod = (0.25 * uContrast + 0.5 * uSpinAmount + 1.2);
|
||||
float paint_res = min(2.0, max(0.0, length(uv) * 0.035 * contrast_mod));
|
||||
float c1p = max(0.0, 1.0 - contrast_mod * abs(1.0 - paint_res));
|
||||
float c2p = max(0.0, 1.0 - contrast_mod * abs(paint_res));
|
||||
float c3p = 1.0 - min(1.0, c1p + c2p);
|
||||
float light = (uLighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + uLighting * max(c2p * 5.0 - 4.0, 0.0);
|
||||
|
||||
return (0.3 / uContrast) * uColor1 + (1.0 - 0.3 / uContrast) * (uColor1 * c1p + uColor2 * c2p + vec4(c3p * uColor3.rgb, c3p * uColor1.a)) + light;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv * iResolution.xy;
|
||||
gl_FragColor = effect(iResolution.xy, uv);
|
||||
}
|
||||
`;
|
||||
|
||||
let renderer: Renderer | null = null;
|
||||
let program: Program | null = null;
|
||||
let animateId = 0;
|
||||
|
||||
const initBalatro = () => {
|
||||
const container = containerRef.value;
|
||||
if (!container) return;
|
||||
|
||||
renderer = new Renderer({
|
||||
alpha: true,
|
||||
antialias: true
|
||||
});
|
||||
|
||||
const gl = renderer.gl;
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.canvas.style.backgroundColor = 'transparent';
|
||||
|
||||
function resize() {
|
||||
renderer?.setSize(container!.offsetWidth, container!.offsetHeight);
|
||||
if (program) {
|
||||
program.uniforms.iResolution.value = [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height];
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
const geometry = new Triangle(gl);
|
||||
program = new Program(gl, {
|
||||
vertex: vertexShader,
|
||||
fragment: fragmentShader,
|
||||
uniforms: {
|
||||
iTime: { value: 0 },
|
||||
iResolution: {
|
||||
value: [gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height]
|
||||
},
|
||||
uSpinRotation: { value: props.spinRotation },
|
||||
uSpinSpeed: { value: props.spinSpeed },
|
||||
uOffset: { value: props.offset },
|
||||
uColor1: { value: hexToVec4(props.color1) },
|
||||
uColor2: { value: hexToVec4(props.color2) },
|
||||
uColor3: { value: hexToVec4(props.color3) },
|
||||
uContrast: { value: props.contrast },
|
||||
uLighting: { value: props.lighting },
|
||||
uSpinAmount: { value: props.spinAmount },
|
||||
uPixelFilter: { value: props.pixelFilter },
|
||||
uSpinEase: { value: props.spinEase },
|
||||
uIsRotate: { value: props.isRotate },
|
||||
uMouse: { value: [0.5, 0.5] }
|
||||
}
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
|
||||
function update(time: DOMHighResTimeStamp) {
|
||||
animateId = requestAnimationFrame(update);
|
||||
program!.uniforms.iTime.value = time * 0.001;
|
||||
renderer!.render({ scene: mesh });
|
||||
}
|
||||
|
||||
animateId = requestAnimationFrame(update);
|
||||
container.appendChild(gl.canvas);
|
||||
gl.canvas.style.width = '100%';
|
||||
gl.canvas.style.height = '100%';
|
||||
gl.canvas.style.display = 'block';
|
||||
gl.canvas.style.position = 'absolute';
|
||||
gl.canvas.style.top = '0';
|
||||
gl.canvas.style.left = '0';
|
||||
gl.canvas.style.zIndex = '100';
|
||||
|
||||
function handleMouseMove(e: MouseEvent) {
|
||||
if (!props.mouseInteraction) return;
|
||||
const rect = container!.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width;
|
||||
const y = 1.0 - (e.clientY - rect.top) / rect.height;
|
||||
program!.uniforms.uMouse.value = [x, y];
|
||||
}
|
||||
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animateId);
|
||||
window.removeEventListener('resize', resize);
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
};
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
if (animateId) {
|
||||
cancelAnimationFrame(animateId);
|
||||
}
|
||||
if (renderer) {
|
||||
const gl = renderer.gl;
|
||||
const container = containerRef.value;
|
||||
if (container && gl.canvas.parentNode === container) {
|
||||
container.removeChild(gl.canvas);
|
||||
}
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
renderer = null;
|
||||
program = null;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initBalatro();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.pixelFilter, props.isRotate, props.color1, props.color2, props.color3],
|
||||
() => {
|
||||
if (!program) return;
|
||||
program.uniforms.uColor1.value = hexToVec4(props.color1);
|
||||
program.uniforms.uColor2.value = hexToVec4(props.color2);
|
||||
program.uniforms.uColor3.value = hexToVec4(props.color3);
|
||||
program.uniforms.uPixelFilter.value = props.pixelFilter;
|
||||
program.uniforms.uIsRotate.value = props.isRotate;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
160
src/demo/Backgrounds/BalatroDemo.vue
Normal file
160
src/demo/Backgrounds/BalatroDemo.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="demo-container">
|
||||
<Balatro
|
||||
:is-rotate="rotate"
|
||||
:mouse-interaction="mouseInteractionEnabled"
|
||||
:pixel-filter="pixelation"
|
||||
:color1="colorStops[0]"
|
||||
:color2="colorStops[1]"
|
||||
:color3="colorStops[2]"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<div class="flex gap-4">
|
||||
<PreviewColor
|
||||
v-for="(color, index) in colorStops"
|
||||
:key="index"
|
||||
:title="`Color ${index + 1}`"
|
||||
:model-value="color"
|
||||
@update:model-value="value => updateColorStop(index, value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PreviewSlider title="Pixelation" v-model="pixelation" :min="0" :max="2000" :step="10" />
|
||||
|
||||
<PreviewSwitch title="Enable Mouse Interaction" v-model="mouseInteractionEnabled" />
|
||||
|
||||
<PreviewSwitch title="Rotate" v-model="rotate" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
<Dependencies :dependency-list="['ogl']" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="balatro" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="balatro.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import Dependencies from '../../components/code/Dependencies.vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import PreviewColor from '@/components/common/PreviewColor.vue';
|
||||
import Balatro from '../../content/Backgrounds/Balatro/Balatro.vue';
|
||||
import { balatro } from '@/constants/code/Backgrounds/balatroCode';
|
||||
|
||||
const colorStops = ref(['#DE443B', '#006BB4', '#162325']);
|
||||
const pixelation = ref(745);
|
||||
const mouseInteractionEnabled = ref(true);
|
||||
const rotate = ref(false);
|
||||
|
||||
const updateColorStop = (index: number, color: string) => {
|
||||
colorStops.value[index] = color;
|
||||
};
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'spinRotation',
|
||||
type: 'number',
|
||||
default: '-2.0',
|
||||
description: 'Base rotation amount affecting the shader effect.'
|
||||
},
|
||||
{
|
||||
name: 'spinSpeed',
|
||||
type: 'number',
|
||||
default: '7.0',
|
||||
description: 'Speed of the spin animation.'
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
type: '[number, number]',
|
||||
default: '[0.0, 0.0]',
|
||||
description: 'Offset for the shader effect.'
|
||||
},
|
||||
{
|
||||
name: 'color1',
|
||||
type: 'string',
|
||||
default: '"#DE443B"',
|
||||
description: 'Primary color in HEX format.'
|
||||
},
|
||||
{
|
||||
name: 'color2',
|
||||
type: 'string',
|
||||
default: '"#006BB4"',
|
||||
description: 'Secondary color in HEX format.'
|
||||
},
|
||||
{
|
||||
name: 'color3',
|
||||
type: 'string',
|
||||
default: '"#162325"',
|
||||
description: 'Tertiary color in HEX format.'
|
||||
},
|
||||
{
|
||||
name: 'contrast',
|
||||
type: 'number',
|
||||
default: '3.5',
|
||||
description: 'Contrast value affecting color blending.'
|
||||
},
|
||||
{
|
||||
name: 'lighting',
|
||||
type: 'number',
|
||||
default: '0.4',
|
||||
description: 'Lighting factor affecting brightness.'
|
||||
},
|
||||
{
|
||||
name: 'spinAmount',
|
||||
type: 'number',
|
||||
default: '0.25',
|
||||
description: 'Amount of spin influence based on UV length.'
|
||||
},
|
||||
{
|
||||
name: 'pixelFilter',
|
||||
type: 'number',
|
||||
default: '745.0',
|
||||
description: 'Pixel filter factor determining pixelation.'
|
||||
},
|
||||
{
|
||||
name: 'spinEase',
|
||||
type: 'number',
|
||||
default: '1.0',
|
||||
description: 'Ease factor for spin.'
|
||||
},
|
||||
{
|
||||
name: 'isRotate',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Determines if the shader rotates continuously.'
|
||||
},
|
||||
{
|
||||
name: 'mouseInteraction',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: 'Enables or disables mouse interaction for rotation.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
z-index: 1;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user