Migrated Balatro background component.

This commit is contained in:
msavulescu
2025-07-14 21:54:26 +03:00
parent 07ce88d997
commit 6705330900
5 changed files with 463 additions and 1 deletions

View File

@@ -0,0 +1,280 @@
<template>
<div ref="containerRef" :class="className" :style="style" class="balatro-container" />
</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>
<style scoped>
.balatro-container {
width: 100%;
height: 100%;
}
</style>