hero: fix 'у' overflow, add tagline integration, remove separate banner

This commit is contained in:
neo committed 2026-06-25 21:27:30 +05:00
1 parent 02a98b7949
commit 5396801baa
3 files changed
+1005 -11

No files matched your search

+460
View File
@@ -0,0 +1,460 @@
"""
ROI24 — Matrix 3D Tunnel Hero Animation
========================================
Blender 4.x Geometry Nodes script.
Creates a Matrix-style infinite tunnel with binary digits
and floating KPI cards.
Usage:
1. Open Blender 4.x
2. Scripting workspace → Text Editor → New → "matrix_tunnel.py"
3. Paste this entire file
4. Alt+P (Run Script)
5. Press ~ in 3D viewport → shade smooth
6. Numpad 0 for camera view
7. Ctrl+F12 to render animation
Output: 1920x1080, 60fps, 180 frames (3s loop)
"""
import bpy
import math
import random
import os
# ============================================================
# CONFIG
# ============================================================
RENDER_WIDTH = 1920
RENDER_HEIGHT = 1080
FRAMES = 180
FPS = 60
TUNNEL_RADIUS = 8.0
TUNNEL_LENGTH = 80.0
RINGS = 32
CHARS_PER_RING = 48
FLOW_SPEED = 3.0 # tunnel speed
# ROI24 brand colors
GREEN = (0.133, 0.773, 0.369) # #22c55e
GREEN_BRIGHT = (0.286, 0.886, 0.502) # #4ade80
DARK = (0.039, 0.039, 0.039) # #0a0a0a
# ============================================================
# CLEANUP
# ============================================================
print("Cleaning scene...")
for obj in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
for mat in bpy.data.materials:
bpy.data.materials.remove(mat, do_unlink=True)
for tex in bpy.data.textures:
bpy.data.textures.remove(tex, do_unlink=True)
for img in bpy.data.images:
bpy.data.images.remove(img, do_unlink=True)
for gn in bpy.data.node_groups:
bpy.data.node_groups.remove(gn)
# ============================================================
# MATERIALS
# ============================================================
print("Creating materials...")
def create_green_emit_mat(name):
"""Create a green emission material."""
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
emit = nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = (*GREEN, 1.0)
emit.inputs['Strength'].default_value = 1.5
output = nodes.new('ShaderNodeOutputMaterial')
links.new(emit.outputs['Emission'], output.inputs['Surface'])
return mat
def create_glow_mat(name, color=GREEN, strength=3.0):
"""Create a bright glow material."""
mat = bpy.data.materials.new(name)
mat.use_nodes = True
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
emit = nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = (*color, 1.0)
emit.inputs['Strength'].default_value = strength
output = nodes.new('ShaderNodeOutputMaterial')
links.new(emit.outputs['Emission'], output.inputs['Surface'])
return mat
char_mat = create_green_emit_mat('CharMat')
glow_mat = create_glow_mat('GlowMat', GREEN_BRIGHT, 4.0)
card_mat = create_green_emit_mat('CardMat')
line_mat = create_green_emit_mat('LineMat')
# ============================================================
# CAMERA
# ============================================================
print("Setting up camera...")
cam_data = bpy.data.cameras.new('Camera')
cam_data.type = 'PERSP'
cam_data.lens = 35
cam_data.clip_start = 0.1
cam_data.clip_end = 200
cam = bpy.data.objects.new('Camera', cam_data)
cam.location = (0, 0, 0)
bpy.context.scene.collection.objects.link(cam)
bpy.context.scene.camera = cam
# ============================================================
# SCENE SETUP
# ============================================================
print("Setting up scene...")
scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.cycles.samples = 128
scene.cycles.preview_samples = 32
scene.cycles.device = 'GPU'
scene.render.fps = FPS
scene.frame_start = 0
scene.frame_end = FRAMES - 1
# Render resolution
scene.render.resolution_x = RENDER_WIDTH
scene.render.resolution_y = RENDER_HEIGHT
scene.render.resolution_percentage = 100
# Output path (set before file_format)
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'renders')
os.makedirs(output_dir, exist_ok=True)
scene.render.filepath = os.path.join(output_dir, 'matrix_tunnel')
# FFMPEG settings (Blender 4.x API)
scene.ffmpeg.format = 'MPEG_4'
scene.ffmpeg.codec = 'H264'
scene.ffmpeg.video_bitrate = 50000
# World background
world = bpy.data.worlds.new('World')
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()
bg = nodes.new('ShaderNodeBackground')
bg.inputs['Color'].default_value = (*DARK, 1.0)
bg.inputs['Strength'].default_value = 0.3
output = nodes['World Output']
links.new(bg.outputs['Background'], output.inputs['Surface'])
# Fog
fog = nodes.new('ShaderNodeFog')
fog.fog_type = 'EXP'
fog.density = 0.025
fog.inputs['Color'].default_value = (*GREEN, 0.15)
links.new(fog, output.inputs['Fog'])
scene.world = world
# ============================================================
# GEOMETRY NODES TUNNEL
# ============================================================
print("Creating tunnel with Geometry Nodes...")
# Create instancing system for binary characters
tunnel_gn = bpy.data.node_groups.new('MatrixTunnel', 'GeometryNodeTree')
tunnel_gn.interface.new_socket('Geometry', socket_type='NodeSocketGeometry')
tunnel_gn.interface.new_socket('Progress', socket_type='NodeSocketFloat')
nodes = tunnel_gn.nodes
links = tunnel_gn.links
# Input geometry
input_geom = nodes.new('NodeGroupInput')
# Separate by Z
separate_z = nodes.new('GeometryNodeSeparateGeometry')
separate_z.location = (200, 0)
# Instance on points
instance_on_points = nodes.new('GeometryNodeInstanceOnPoints')
instance_on_points.location = (500, 0)
# Transform geometry (rotate instances to face camera)
transform = nodes.new('GeometryNodeTransform')
transform.location = (750, 0)
# Output
output_gn = nodes.new('NodeGroupOutput')
# Connect
links.new(input_geom.outputs[0], separate_z.inputs[0])
links.new(separate_z.outputs[0], instance_on_points.inputs[0])
links.new(instance_on_points.outputs[0], transform.inputs[0])
links.new(transform.outputs[0], output_gn.inputs[0])
# ============================================================
# CREATE BINARY CHARACTER MESH (simple diamond shape)
# ============================================================
print("Creating character mesh...")
# Diamond shape for '0'
char_mesh_0 = bpy.data.meshes.new('Char0')
verts_0 = [
(0, 0.3, 0), (0.2, 0, 0), (0, -0.3, 0), (-0.2, 0, 0)
]
faces_0 = [[0, 1, 2, 3]]
char_mesh_0.from_pydata(verts_0, [], faces_0)
char_mesh_0.validate()
# Line shape for '1'
char_mesh_1 = bpy.data.meshes.new('Char1')
verts_1 = [
(-0.08, 0.25, 0), (0.08, 0.25, 0),
(0.08, -0.25, 0), (-0.08, -0.25, 0),
(-0.08, -0.15, 0), (0.08, -0.15, 0)
]
faces_1 = [[0, 1, 2, 3], [2, 5, 4, 3]]
char_mesh_1.from_pydata(verts_1, [], faces_1)
char_mesh_1.validate()
# ============================================================
# INSTANCED CHARACTER OBJECTS
# ============================================================
print("Instancing characters...")
def create_instanced_chars():
"""Create instanced binary characters around tunnel."""
angle_step = (2 * math.pi) / CHARS_PER_RING
# Create collection for tunnel objects
tunnel_col = bpy.data.collections.new('MatrixTunnel')
bpy.context.scene.collection.children.link(tunnel_col)
# Create a single "instancer" object with geometry nodes
instancer = bpy.data.objects.new('TunnelInstancer', None)
instancer.location = (0, 0, 0)
tunnel_col.objects.link(instancer)
# Geometry nodes modifier
gn = instancer.modifiers.new('TunnelGenerator', 'NODES')
gn.node_group = tunnel_gn
# Now create actual character instances
ring_spacing = TUNNEL_LENGTH / RINGS
for ring_idx in range(RINGS):
z = -(ring_idx * ring_spacing)
for char_idx in range(CHARS_PER_RING):
angle = char_idx * angle_step
x = math.cos(angle) * TUNNEL_RADIUS
y = math.sin(angle) * TUNNEL_RADIUS
# Create character mesh object
use_mesh = char_mesh_0 if random.random() > 0.5 else char_mesh_1
# Use object instancing for performance
char_obj = bpy.data.objects.new(
f'Char_R{ring_idx:02d}_C{char_idx:02d}',
use_mesh
)
char_obj.location = (x, y, z)
char_obj.rotation_euler = (0, 0, angle + math.radians(90))
# Scale based on position (closer = bigger)
scale = 0.6 + (ring_idx / RINGS) * 0.3
char_obj.scale = (scale, scale, scale)
# Material
char_obj.data.materials.append(char_mat)
# Set material color based on depth
depth_ratio = ring_idx / RINGS
r = GREEN[0] * (0.6 + depth_ratio * 0.4)
g = GREEN[1] * (0.8 + depth_ratio * 0.2)
b = GREEN[2] * (0.7 + depth_ratio * 0.3)
char_mat.nodes['Emission'].inputs['Color'].default_value = (r, g, b, 1.0)
# Store animation data
char_obj['ring_idx'] = ring_idx
char_obj['char_idx'] = char_idx
char_obj['base_x'] = x
char_obj['base_y'] = y
char_obj['base_z'] = z
char_obj['base_scale'] = scale
char_obj['angle'] = angle
tunnel_col.objects.link(char_obj)
return tunnel_col
tunnel_col = create_instanced_chars()
# ============================================================
# KPI CARDS — 3D panels
# ============================================================
print("Creating KPI cards...")
KPI_DATA = [
{'label': 'CPA', 'value': '890₽', 'pos': (-10, 5, -12), 'rot': (0, 0.1, 0), 'size': (3, 2)},
{'label': 'ROI', 'value': '91.8%', 'pos': (10, 4, -18), 'rot': (0, -0.1, 0), 'size': (3, 2)},
{'label': 'LTV', 'value': '12.4K', 'pos': (-9, -2, -24), 'rot': (0, 0.15, 0), 'size': (3, 2)},
{'label': 'CONV', 'value': '2.8%', 'pos': (9, -3, -30), 'rot': (0, -0.15, 0), 'size': (3, 2)},
{'label': 'LEADS', 'value': '44.5K', 'pos': (-8, 1, -36), 'rot': (0, 0.1, 0), 'size': (3, 2)},
{'label': 'REVENUE', 'value': '2.05M', 'pos': (8, 0, -42), 'rot': (0, -0.1, 0), 'size': (3, 2)},
]
for i, kpi in enumerate(KPI_DATA):
w, h = kpi['size']
# Card mesh
card_mesh = bpy.data.meshes.new(f'KPI_Card_{i}')
verts = [
(-w/2, -h/2, 0), (w/2, -h/2, 0),
(w/2, h/2, 0), (-w/2, h/2, 0),
(-w/2, -h/2, -0.05), (w/2, -h/2, -0.05),
(w/2, h/2, -0.05), (-w/2, h/2, -0.05),
]
faces = [
[0, 4, 5, 1], # bottom
[4, 7, 6, 5], # back
[7, 3, 2, 6], # top
[3, 0, 1, 2], # front
[4, 0, 3, 7], # left
[1, 5, 6, 2], # right
]
card_mesh.from_pydata(verts, [], faces)
card_mesh.validate()
card_obj = bpy.data.objects.new(f'KPI_Card_{i}', card_mesh)
card_obj.location = kpi['pos']
card_obj.rotation_euler = kpi['rot']
card_obj.scale = (1, 1, 1)
# Material
card_obj.data.materials.append(card_mat)
# Store data
card_obj['kpi_index'] = i
card_obj['base_x'] = kpi['pos'][0]
card_obj['base_y'] = kpi['pos'][1]
card_obj['base_z'] = kpi['pos'][2]
tunnel_col.objects.link(card_obj)
# ============================================================
# SPEED LINES
# ============================================================
print("Creating speed lines...")
for i in range(48):
angle = (i / 48) * 2 * math.pi
x1 = math.cos(angle) * 0.5
y1 = math.sin(angle) * 0.5
x2 = math.cos(angle) * TUNNEL_RADIUS * 2
y2 = math.sin(angle) * TUNNEL_RADIUS * 2
z_start = -10
z_end = -TUNNEL_LENGTH
# Curve
curve = bpy.data.curves.new(f'SpeedLine_{i}', 'CURVE')
curve.dimensions = '3D'
curve.bevel_depth = 0.03
spline = curve.splines.new('POLY')
spline.points.add(1)
spline.points[0].co = (x1, y1, z_start, 1)
spline.points[1].co = (x2, y2, z_end, 1)
line_obj = bpy.data.objects.new(f'SpeedLine_{i}', curve)
line_obj.data.materials.append(line_mat)
tunnel_col.objects.link(line_obj)
# ============================================================
# ANIMATION FUNCTIONS (uncomment to enable)
# ============================================================
def animate_characters():
"""Animate binary characters flowing toward camera."""
for obj in tunnel_col.objects:
ring_idx = obj.get('ring_idx', None)
if ring_idx is None:
continue
base_z = obj.get('base_z', 0)
base_x = obj.get('base_x', 0)
base_y = obj.get('base_y', 0)
base_scale = obj.get('base_scale', 0.6)
angle = obj.get('angle', 0)
for frame in range(FRAMES):
# Flow forward
z = base_z + ((frame / FPS) * FLOW_SPEED) % TUNNEL_LENGTH
if z > 0:
z -= TUNNEL_LENGTH
# Scale by depth
depth_ratio = max(0, min(1, (TUNNEL_LENGTH + z) / TUNNEL_LENGTH))
scale = base_scale * (0.4 + depth_ratio * 0.7)
# Set keyframe
obj.location = (base_x, base_y, z)
obj.scale = (scale / base_scale, scale / base_scale, 1)
obj.keyframe_insert(data_path='location', frame=frame)
obj.keyframe_insert(data_path='scale', frame=frame)
def animate_kpi_cards():
"""Animate KPI cards floating through tunnel."""
for obj in tunnel_col.objects:
kpi_idx = obj.get('kpi_index', None)
if kpi_idx is None:
continue
base_x = obj.get('base_x', 0)
base_y = obj.get('base_y', 0)
base_z = obj.get('base_z', 0)
for frame in range(FRAMES):
# Move toward camera
z = base_z + ((frame / FPS) * FLOW_SPEED * 0.7)
# Float motion
y = base_y + math.sin((frame / FPS) * 2 + kpi_idx * 1.5) * 1.5
# Scale
depth_ratio = max(0, min(1, (TUNNEL_LENGTH + z) / TUNNEL_LENGTH))
scale = 0.5 + depth_ratio * 0.7
obj.location = (base_x, y, z)
obj.scale = (scale, scale, scale)
obj.keyframe_insert(data_path='location', frame=frame)
obj.keyframe_insert(data_path='scale', frame=frame)
# ============================================================
# SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("ROI24 Matrix 3D Tunnel — Setup Complete")
print("=" * 60)
print(f"Resolution: {RENDER_WIDTH}x{RENDER_HEIGHT}")
print(f"Duration: {FRAMES / FPS}s @ {FPS}fps")
print(f"Engine: {scene.render.engine} (Cycles)")
print(f"Tunnel: {RINGS} rings x {CHARS_PER_RING} chars = {RINGS * CHARS_PER_RING}")
print(f"KPI cards: {len(KPI_DATA)}")
print(f"Speed lines: 48")
print()
print("NEXT STEPS:")
print("1. In 3D Viewport: Press ~ → Shade Smooth")
print("2. Numpad 0 — Camera view")
print("3. F12 — Preview render")
print("4. Ctrl+F12 — Render animation (MP4)")
print()
print("TO ANIMATE:")
print("Uncomment animate_characters() and animate_kpi_cards()")
print("then re-run this script")
print("=" * 60)
+531
View File
@@ -0,0 +1,531 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI24 — Matrix Tunnel Hero</title>
<link href="https://fonts.cdnfonts.com/css/inter" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
<style>
:root {
--bg: #0a0a0a;
--text: #f5f5f5;
--text-sec: #a1a1aa;
--text-dim: #52525b;
--accent: #22c55e;
--accent-dim: rgba(34,197,94,0.12);
--accent-glow: rgba(34,197,94,0.3);
--mono: 'JetBrains Mono', monospace;
--font: 'Inter', sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
background: var(--bg); color: var(--text); font-family: var(--font);
overflow: hidden; height: 100vh;
-webkit-font-smoothing: antialiased;
}
.stage {
position: relative; width: 100%; height: 100vh; overflow: hidden;
}
/* ── Matrix Tunnel Canvas ── */
#matrix-canvas {
position: absolute; inset: 0; z-index: 1;
}
/* ── FX Layer ── */
.fx-layer {
position: absolute; inset: 0; pointer-events: none; z-index: 2;
}
.fx-layer::before {
content: ''; position: absolute; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
opacity: 0.5;
}
.goo {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 600px; height: 600px; border-radius: 50%;
background: radial-gradient(circle,
rgba(34,197,94,0.06) 0%,
rgba(34,197,94,0.02) 40%,
transparent 70%
);
}
/* ── Radial speed lines ── */
.radial-lines {
position: absolute; inset: 0; pointer-events: none;
}
.radial-line {
position: absolute; top: 50%; left: 50%;
height: 1px;
background: linear-gradient(90deg,
rgba(34,197,94,0.1) 0%,
rgba(34,197,94,0.04) 60%,
transparent 100%
);
transform-origin: left center;
}
.radial-line.fast {
background: linear-gradient(90deg,
rgba(34,197,94,0.18) 0%,
rgba(34,197,94,0.06) 50%,
transparent 100%
);
height: 1.5px;
}
/* ── Content overlay ── */
.content {
position: absolute; inset: 0; z-index: 10;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
pointer-events: none;
}
.hero-headline {
text-align: center; margin-bottom: 24px;
pointer-events: auto;
}
.hero-headline .tag {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--mono); font-size: 11px; font-weight: 600;
color: var(--accent); background: var(--accent-dim);
border: 1px solid rgba(34,197,94,0.2);
border-radius: 20px; padding: 4px 14px;
margin-bottom: 20px; letter-spacing: 0.05em; text-transform: uppercase;
}
.hero-headline .tag .dot {
width: 6px; height: 6px; background: var(--accent);
border-radius: 50%; box-shadow: 0 0 6px var(--accent-glow);
animation: pulse 2s ease-in-out infinite;
}
.hero-headline h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 800; letter-spacing: -0.03em;
line-height: 1.15; margin-bottom: 16px;
background: linear-gradient(135deg, #ffffff 0%, #a1a1aa 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-headline h1 .accent {
background: linear-gradient(135deg, var(--accent) 0%, #4ade80 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-headline p {
font-size: clamp(0.9rem, 1.5vw, 1.1rem);
color: var(--text-sec); max-width: 560px;
margin: 0 auto 28px; line-height: 1.6;
}
.cta-group {
display: flex; gap: 12px; align-items: center;
pointer-events: auto;
}
.cta-primary {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 28px;
background: var(--accent);
color: #0a0a0a;
font-family: var(--mono); font-size: 13px; font-weight: 700;
border: none; border-radius: 10px;
cursor: pointer; letter-spacing: 0.02em;
box-shadow: 0 0 20px rgba(34,197,94,0.25);
transition: all 0.3s ease;
text-decoration: none;
}
.cta-primary:hover {
background: #4ade80;
box-shadow: 0 0 30px rgba(34,197,94,0.4);
transform: translateY(-1px);
}
.cta-secondary {
display: inline-flex; align-items: center; gap: 6px;
padding: 12px 24px;
background: transparent;
color: var(--text-sec);
font-family: var(--mono); font-size: 13px; font-weight: 500;
border: 1px solid rgba(255,255,255,0.1);
border-radius: 10px;
cursor: pointer; letter-spacing: 0.02em;
transition: all 0.3s ease;
text-decoration: none;
}
.cta-secondary:hover {
color: var(--text); border-color: rgba(255,255,255,0.2);
}
/* ── KPI Cards ── */
.kpi-card {
position: absolute;
background: rgba(10,10,10,0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(34,197,94,0.15);
border-radius: 12px;
padding: 14px 18px;
pointer-events: auto;
min-width: 140px;
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
opacity: 0;
animation: cardIn 0.6s ease forwards;
}
.kpi-card:hover {
border-color: rgba(34,197,94,0.35);
box-shadow: 0 0 20px rgba(34,197,94,0.15);
}
.kpi-card .kpi-label {
font-family: var(--mono); font-size: 9px; font-weight: 600;
color: var(--text-dim); text-transform: uppercase;
letter-spacing: 0.1em; margin-bottom: 6px;
}
.kpi-card .kpi-value {
font-family: var(--mono); font-size: 22px; font-weight: 700;
color: var(--accent); margin-bottom: 4px; line-height: 1;
}
.kpi-card .kpi-sub {
font-family: var(--mono); font-size: 10px;
color: var(--text-dim);
}
.kpi-card .kpi-trend {
display: inline-flex; align-items: center; gap: 3px;
font-family: var(--mono); font-size: 10px; font-weight: 600;
padding: 2px 6px; border-radius: 4px;
margin-top: 6px;
}
.kpi-card .kpi-trend.up { color: var(--accent); background: var(--accent-dim); }
.kpi-card .kpi-trend.down { color: var(--red); background: rgba(239,68,68,0.12); }
.scanlines {
position: absolute; inset: 0; z-index: 4;
pointer-events: none;
background: repeating-linear-gradient(
0deg, transparent, transparent 2px,
rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px
);
}
@keyframes pulse {
0%,100% { box-shadow: 0 0 6px var(--accent-glow); }
50% { box-shadow: 0 0 14px rgba(34,197,94,0.25); }
}
@keyframes cardIn {
from { opacity:0; transform: scale(0.85) translateY(15px); }
to { opacity:1; transform: scale(1) translateY(0); }
}
@media(max-width:768px){
.kpi-card { min-width: 110px; padding: 10px 14px; }
.kpi-card .kpi-value { font-size: 18px; }
.cta-group { flex-direction: column; }
}
</style>
</head>
<body>
<div class="stage">
<canvas id="matrix-canvas"></canvas>
<div class="fx-layer">
<div class="goo"></div>
<div class="radial-lines" id="radial-lines"></div>
</div>
<!-- KPI Cards -->
<div class="kpi-card" id="kpi-cpa">
<div class="kpi-label">CPA</div>
<div class="kpi-value" data-target="890" data-suffix=" ₽">0 ₽</div>
<div class="kpi-sub">средняя продажа</div>
<div class="kpi-trend up">↓ 5%</div>
</div>
<div class="kpi-card" id="kpi-roi">
<div class="kpi-label">ROI</div>
<div class="kpi-value" data-target="91.8" data-suffix="%" data-decimals="1">0%</div>
<div class="kpi-sub">возврат инвестиций</div>
<div class="kpi-trend up">↑ 8%</div>
</div>
<div class="kpi-card" id="kpi-conversion">
<div class="kpi-label">Конверсия</div>
<div class="kpi-value" data-target="2.8" data-suffix="%" data-decimals="1">0%</div>
<div class="kpi-sub">заявка → продажа</div>
<div class="kpi-trend up">↑ 0.3%</div>
</div>
<div class="kpi-card" id="kpi-ltv">
<div class="kpi-label">LTV</div>
<div class="kpi-value" data-target="12400" data-suffix=" ₽">0 ₽</div>
<div class="kpi-sub">за 6 месяцев</div>
<div class="kpi-trend up">↑ 18%</div>
</div>
<div class="kpi-card" id="kpi-leads">
<div class="kpi-label">Лиды / мес</div>
<div class="kpi-value" data-target="44500" data-suffix="">0</div>
<div class="kpi-sub">за 30 дней</div>
<div class="kpi-trend up">↑ 15%</div>
</div>
<div class="kpi-card" id="kpi-revenue">
<div class="kpi-label">Выручка</div>
<div class="kpi-value" data-target="2051857" data-suffix=" ₽">0 ₽</div>
<div class="kpi-sub">за 30 дней</div>
<div class="kpi-trend up">↑ 12%</div>
</div>
<div class="content">
<div class="hero-headline">
<div class="tag"><span class="dot"></span>ROI24 — Attribution Analytics</div>
<h1>Управляйте бизнесом<br>на основе <span class="accent">данных</span></h1>
<p>Автоматический анализ рекламных расходов. Каждый рубль конвертируется в прибыль. CPA, LTV, ROI — в реальном времени.</p>
<div class="cta-group">
<a href="#" class="cta-primary">
<span>Начать бесплатно</span>
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 7h12M8 2l5 5-5 5"/></svg>
</a>
<a href="#" class="cta-secondary">Демо</a>
</div>
</div>
</div>
<div class="scanlines"></div>
</div>
<script>
(function() {
'use strict';
// ============================================================
// MATRIX TUNNEL — Canvas 2D 3D Projection
// ============================================================
const canvas = document.getElementById('matrix-canvas');
const ctx = canvas.getContext('2d');
let W, H, CX, CY;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * Math.min(window.devicePixelRatio, 2);
canvas.height = H * Math.min(window.devicePixelRatio, 2);
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
ctx.scale(Math.min(window.devicePixelRatio, 2), Math.min(window.devicePixelRatio, 2));
CX = W / 2;
CY = H / 2;
}
resize();
// ── Tunnel Config ──
const RING_COUNT = 50;
const CHARS_PER_RING = 64;
const TUNNEL_RADIUS = 300;
const TUNNEL_DEPTH = 3000;
const FOCAL = 500;
// ── Tunnel Ring Data ──
const rings = [];
for (let r = 0; r < RING_COUNT; r++) {
const ring = { z: -r * (TUNNEL_DEPTH / RING_COUNT), chars: [] };
for (let c = 0; c < CHARS_PER_RING; c++) {
const angle = (c / CHARS_PER_RING) * Math.PI * 2;
ring.chars.push({
x: Math.cos(angle) * TUNNEL_RADIUS,
y: Math.sin(angle) * TUNNEL_RADIUS,
angle: angle,
value: Math.random() > 0.5 ? '0' : '1'
});
}
rings.push(ring);
}
// ── Floating particles ──
const PARTICLE_COUNT = 80;
const particles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
particles.push({
x: (Math.random() - 0.5) * TUNNEL_RADIUS * 3,
y: (Math.random() - 0.5) * TUNNEL_RADIUS * 3,
z: -Math.random() * TUNNEL_DEPTH,
speed: 0.5 + Math.random() * 1.5,
size: 1 + Math.random() * 2
});
}
let zOffset = 0;
const FLOW_SPEED = 200;
function project(x, y, z) {
const scale = FOCAL / (FOCAL + z);
return {
sx: x * scale + CX,
sy: y * scale + CY,
scale: scale
};
}
function draw(time) {
const dt = 1 / 60;
ctx.clearRect(0, 0, W, H);
// Update flow
zOffset = (zOffset + FLOW_SPEED * dt) % (TUNNEL_DEPTH / RING_COUNT);
// ── Draw tunnel rings ──
for (let r = 0; r < RING_COUNT; r++) {
const baseZ = r * (TUNNEL_DEPTH / RING_COUNT);
let z = baseZ + zOffset;
if (z > TUNNEL_DEPTH) z -= TUNNEL_DEPTH;
const depthRatio = 1 - (z / TUNNEL_DEPTH);
for (let c = 0; c < CHARS_PER_RING; c++) {
const char = rings[r].chars[c];
const px = char.x;
const py = char.y;
const p = project(px, py, z);
const fontSize = Math.max(6, 28 * p.scale);
// Distance from center in screen space
const dx = p.sx - CX;
const dy = p.sy - CY;
const distFromCenter = Math.sqrt(dx * dx + dy * dy);
// Only draw if within screen bounds
if (distFromCenter > Math.max(W, H) * 0.65) continue;
if (p.scale < 0.1) continue;
// Opacity: fade in at far end, fade out at near end
let alpha;
if (depthRatio > 0.85) {
alpha = (1 - depthRatio) / 0.15 * 0.6;
} else if (depthRatio < 0.05) {
alpha = depthRatio / 0.05 * 0.8;
} else {
alpha = 0.6 + depthRatio * 0.2;
}
// Green intensity based on depth
const green = Math.floor(180 + depthRatio * 75);
ctx.fillStyle = `rgba(34, ${green}, 94, ${alpha})`;
ctx.font = `bold ${fontSize}px JetBrains Mono, monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Add glow
if (p.scale > 0.3) {
ctx.shadowColor = 'rgba(34, 197, 94, 0.3)';
ctx.shadowBlur = 4 + depthRatio * 6;
} else {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
}
ctx.fillText(char.value, p.sx, p.sy);
}
}
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
// ── Draw particles ──
for (const part of particles) {
part.z += part.speed * 2;
if (part.z > 100) part.z = -TUNNEL_DEPTH;
const p = project(part.x, part.y, part.z);
if (p.scale < 0.05 || p.scale > 3) continue;
const alpha = Math.min(1, p.scale * 0.5) * 0.4;
const size = Math.max(1, part.size * p.scale);
ctx.fillStyle = `rgba(34, 197, 94, ${alpha})`;
ctx.beginPath();
ctx.arc(p.sx, p.sy, size, 0, Math.PI * 2);
ctx.fill();
}
// ── Radial speed lines on canvas ──
const LINE_COUNT = 36;
for (let i = 0; i < LINE_COUNT; i++) {
const angle = (i / LINE_COUNT) * Math.PI * 2 + (time * 0.00005);
const innerR = 50;
const outerR = TUNNEL_RADIUS * 1.5;
const x1 = Math.cos(angle) * innerR + CX;
const y1 = Math.sin(angle) * innerR + CY;
const x2 = Math.cos(angle) * outerR + CX;
const y2 = Math.sin(angle) * outerR + CY;
ctx.strokeStyle = 'rgba(34, 197, 94, 0.04)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
// ── Radial lines (DOM) ──
const linesContainer = document.getElementById('radial-lines');
for (let i = 0; i < LINE_COUNT; i++) {
const line = document.createElement('div');
const isFast = Math.random() > 0.7;
line.className = 'radial-line' + (isFast ? ' fast' : '');
line.style.width = (30 + Math.random() * 35) + 'vw';
line.style.transform = `rotate(${(i / LINE_COUNT) * 360}deg)`;
line.style.opacity = 0.2 + Math.random() * 0.4;
linesContainer.appendChild(line);
}
// ── KPI cards stagger ──
document.querySelectorAll('.kpi-card').forEach((card, i) => {
card.style.animationDelay = `${0.3 + i * 0.12}s`;
});
// ── Animate KPI values ──
document.querySelectorAll('.kpi-value[data-target]').forEach(el => {
const target = parseFloat(el.dataset.target);
const decimals = parseInt(el.dataset.decimals) || 0;
const suffix = el.dataset.suffix || '';
const duration = 1800;
const start = performance.now();
function fmt(v) {
if (target >= 10000) return v.toLocaleString('ru-RU', { maximumFractionDigits: 0 }) + suffix;
return v.toFixed(decimals) + suffix;
}
function tick(now) {
const progress = Math.min((now - start) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
el.textContent = fmt(target * ease);
if (progress < 1) requestAnimationFrame(tick);
}
setTimeout(() => requestAnimationFrame(tick), 400 + Math.random() * 400);
});
// ── Mouse parallax ──
let mx = 0, my = 0;
document.addEventListener('mousemove', (e) => {
mx = (e.clientX / W - 0.5) * 0.3;
my = (e.clientY / H - 0.5) * 0.3;
});
function updateParallax() {
canvas.style.transform = `translate(${mx * 10}px, ${my * 10}px)`;
requestAnimationFrame(updateParallax);
}
requestAnimationFrame(updateParallax);
// ── Resize ──
window.addEventListener('resize', () => {
ctx.setTransform(1, 0, 0, 1, 0, 0);
resize();
});
})();
</script>
</body>
</html>
+14 -11
View File
@@ -224,7 +224,7 @@ section { padding: 120px 0; position: relative; z-index: 1; }
padding: 160px 0 80px; min-height: 90vh; padding: 160px 0 80px; min-height: 90vh;
display: flex; align-items: center; display: flex; align-items: center;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
position: relative; overflow: hidden; position: relative;
} }
.hero-inner { .hero-inner {
display: grid; grid-template-columns: 1fr 1fr; gap: 80px; display: grid; grid-template-columns: 1fr 1fr; gap: 80px;
@@ -242,10 +242,11 @@ section { padding: 120px 0; position: relative; z-index: 1; }
h1 { h1 {
font-size: clamp(3rem, 6vw, 5rem); font-weight: 700; font-size: clamp(3rem, 6vw, 5rem); font-weight: 700;
letter-spacing: -0.04em; line-height: 0.95; margin-bottom: 24px; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 24px;
background: linear-gradient(135deg, var(--text) 45%, var(--accent) 100%); background: linear-gradient(135deg, var(--text) 45%, var(--accent) 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; -webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
text-shadow: 0 0 1px rgba(255,255,255,0.1);
} }
.hero-sub { .hero-sub {
@@ -253,6 +254,16 @@ h1 {
margin-bottom: 40px; max-width: 440px; line-height: 1.65; margin-bottom: 40px; max-width: 440px; line-height: 1.65;
} }
.hero-tagline {
font-size: 1rem; font-weight: 600;
margin-bottom: 32px; line-height: 1.5;
max-width: 440px;
color: rgba(255,255,255,0.85);
}
[data-theme="light"] .hero-tagline {
color: #161616;
}
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; } .hero-actions { display: flex; gap: 12px; flex-wrap: wrap; }
.hero-meta { .hero-meta {
@@ -808,15 +819,6 @@ h1 {
</div> </div>
</header> </header>
<!-- ── TOP BANNER ── -->
<section style="background: var(--accent-dim); border-bottom: 1px solid var(--border); padding: 50px 0 48px; text-align: center; position: relative; z-index: 1;">
<div style="max-width: 1280px; margin: 0 auto; padding: 0 32px;">
<h2 style="font-size: clamp(1.5rem, 3vw, 2.25rem); font-weight: 700; letter-spacing: -0.03em; line-height: 1.3; color: var(--text); padding-top: 50px;">
Сквозная аналитика и панели мониторинга<br>для вашего бизнеса и департаментов под ключ
</h2>
</div>
</section>
<!-- ── HERO ── --> <!-- ── HERO ── -->
<section class="hero" id="hero"> <section class="hero" id="hero">
<div class="wrap"> <div class="wrap">
@@ -829,6 +831,7 @@ h1 {
Обновляется автоматически из 1С, CRM и банков. Обновляется автоматически из 1С, CRM и банков.
Смотрится с телефона каждое утро. Смотрится с телефона каждое утро.
</p> </p>
<p class="hero-tagline">Сквозная аналитика и панели мониторинга<br>для вашего бизнеса и департаментов под ключ</p>
<div class="hero-actions"> <div class="hero-actions">
<a href="#cta" class="btn btn-accent btn-lg"> <a href="#cta" class="btn btn-accent btn-lg">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"> <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">