feat: add animated visual component to homepage

Introduce new top page visual component and ensure translation files are complete.

Co-authored-by: Abkdarrk <122768967+Abdullakala@users.noreply.github.com>
This commit is contained in:
v0 2026-04-17 14:38:00 +00:00
parent af7d8980ba
commit 96c42e7a1f
2 changed files with 197 additions and 1 deletions

View file

@ -0,0 +1,175 @@
<script>
import { onMount } from "svelte";
let container: HTMLDivElement;
let animationFrame: number;
onMount(() => {
const canvas = container.querySelector("canvas") as HTMLCanvasElement;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = container.offsetWidth * dpr;
canvas.height = 280 * dpr;
ctx.scale(dpr, dpr);
const width = container.offsetWidth;
const height = 280;
interface Node {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
color: string;
}
const nodes: Node[] = [];
const nodeCount = 12;
for (let i = 0; i < nodeCount; i++) {
nodes.push({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
radius: 3 + Math.random() * 3,
color: `hsla(${30 + Math.random() * 10}, 100%, 60%, ${0.4 + Math.random() * 0.3})`,
});
}
let time = 0;
function drawNode(node: Node) {
ctx!.fillStyle = node.color;
ctx!.beginPath();
ctx!.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
ctx!.fill();
// Glow effect
const gradient = ctx!.createRadialGradient(
node.x,
node.y,
0,
node.x,
node.y,
node.radius * 2
);
gradient.addColorStop(0, `hsla(${30 + Math.random() * 10}, 100%, 60%, 0.1)`);
gradient.addColorStop(1, `hsla(${30 + Math.random() * 10}, 100%, 60%, 0)`);
ctx!.fillStyle = gradient;
ctx!.fillRect(
node.x - node.radius * 2,
node.y - node.radius * 2,
node.radius * 4,
node.radius * 4
);
}
function drawConnection(node1: Node, node2: Node, distance: number, maxDistance: number) {
const alpha = (1 - distance / maxDistance) * 0.3;
ctx!.strokeStyle = `rgba(249, 115, 22, ${alpha})`;
ctx!.lineWidth = 1;
ctx!.beginPath();
ctx!.moveTo(node1.x, node1.y);
ctx!.lineTo(node2.x, node2.y);
ctx!.stroke();
}
function animate() {
// Clear with fade effect
ctx!.fillStyle = "rgba(30, 30, 30, 0.05)";
ctx!.fillRect(0, 0, width, height);
time += 0.005;
// Update nodes
for (const node of nodes) {
node.x += node.vx;
node.y += node.vy;
// Bounce off walls
if (node.x - node.radius < 0 || node.x + node.radius > width) {
node.vx *= -1;
node.x = Math.max(node.radius, Math.min(width - node.radius, node.x));
}
if (node.y - node.radius < 0 || node.y + node.radius > height) {
node.vy *= -1;
node.y = Math.max(node.radius, Math.min(height - node.radius, node.y));
}
// Apply slight gravity toward center
const centerX = width / 2;
const centerY = height / 2;
const dx = centerX - node.x;
const dy = centerY - node.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
node.vx += (dx / distance) * 0.0002;
node.vy += (dy / distance) * 0.0002;
}
// Apply friction
node.vx *= 0.9995;
node.vy *= 0.9995;
}
// Draw connections
const connectionDistance = 150;
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < connectionDistance) {
drawConnection(nodes[i], nodes[j], distance, connectionDistance);
}
}
}
// Draw nodes
for (const node of nodes) {
drawNode(node);
}
animationFrame = requestAnimationFrame(animate);
}
animate();
return () => cancelAnimationFrame(animationFrame);
});
</script>
<div bind:this={container} class="visualization-container">
<canvas />
</div>
<style>
.visualization-container {
width: 100%;
height: 280px;
border-radius: 24px;
overflow: hidden;
background: linear-gradient(135deg, rgba(249, 115, 22, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(249, 115, 22, 0.2);
box-shadow:
inset 0 0 40px rgba(249, 115, 22, 0.1),
0 8px 32px rgba(0, 0, 0, 0.2);
margin-bottom: 40px;
position: relative;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>

View file

@ -2,6 +2,7 @@
import { t } from "$lib/i18n/translations";
import Omnibox from "$components/save/Omnibox.svelte";
import AnimatedVisualization from "$components/visualization/AnimatedVisualization.svelte";
</script>
<svelte:head>
@ -21,6 +22,10 @@
<p id="brand-sub">{$t("general.embed.description")}</p>
</div>
<div id="visualization-wrapper">
<AnimatedVisualization />
</div>
<div id="input-wrapper">
<Omnibox />
</div>
@ -49,10 +54,26 @@
align-items: center;
justify-content: center;
width: 100%;
max-width: 680px;
max-width: 800px;
gap: 28px;
}
/* visualization wrapper */
#visualization-wrapper {
width: 100%;
display: flex;
justify-content: flex-end;
padding-right: 20px;
margin-top: 10px;
}
@media screen and (max-width: 768px) {
#visualization-wrapper {
padding-right: 0;
justify-content: center;
}
}
/* brand block */
#brand {
display: flex;