hero: fix 'у' overflow, add tagline integration, remove separate banner
This commit is contained in:
1 parent
02a98b7949
commit
5396801baa
3 files changed
+1005
-11
No files matched your search
@@ -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)
|
||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in new issue
Block a user