Let it snow()

Hey Claude, make it HD etc.

# Setup temporary environment and install dependencies
using Pkg
Pkg.activate(temp=true)
Pkg.add(["Sixel", "ColorTypes", "FixedPointNumbers", "Random"])

using Sixel, ColorTypes, FixedPointNumbers, Random

"""
    HD Realistic Snow Animation using Sixel Graphics

    Renders a realistic winter scene with:
    - Night sky gradient with twinkling stars
    - Realistic pine trees with ornaments and snow
    - Physically-simulated falling snowflakes with depth blur
    - Accumulating snow on the ground
    - Vignette and atmospheric effects
"""

mutable struct Snowflake
    x::Float64
    y::Float64
    size::Float64
    speed::Float64
    drift::Float64
    depth::Float64
end

mutable struct Deer
    x::Float64
    y::Float64
    scale::Float64
    speed::Float64
    leg_phase::Float64
    facing_right::Bool
end

mutable struct Skier
    x::Float64
    speed::Float64
    pole_phase::Float64
    facing_right::Bool
end

function snow_hd(; width=1024, height=768, fps=24)
    # Initialize snowflakes
    n_flakes = 400
    flakes = [Snowflake(
        rand() * width,
        rand() * height,
        0.5 + rand() * 2.5,
        0.8 + rand() * 2.5,
        randn() * 0.3,
        rand()
    ) for _ in 1:n_flakes]

    # Ground snow accumulation
    ground_snow = fill(Float64(height - 25), width)

    # Tree definitions: (center_x, base_y, height, width)
    # Smaller trees positioned higher for proper perspective
    trees = [
        (x=width÷2, y=height-30, h=220, w=140),       # Main center tree (big, closest)
        (x=width÷4, y=height-55, h=170, w=110),       # Left tree (mid-distance)
        (x=3width÷4, y=height-55, h=170, w=110),      # Right tree (mid-distance)
        (x=width÷8, y=height-85, h=120, w=75),        # Far left (more distant)
        (x=7width÷8, y=height-85, h=120, w=75),       # Far right (more distant)
    ]

    # Light colors for tree decorations
    light_colors = [
        RGB{Float64}(1.0, 0.95, 0.4),   # Warm yellow
        RGB{Float64}(1.0, 0.25, 0.2),   # Red
        RGB{Float64}(0.3, 0.5, 1.0),    # Blue
        RGB{Float64}(1.0, 1.0, 1.0),    # White
        RGB{Float64}(0.2, 1.0, 0.4),    # Green
    ]

    # Pre-generate star positions (fixed, no flicker)
    n_stars = round(Int, width * height * 0.6 * 0.0004)  # Same density as before
    star_y_max = round(Int, height * 0.6)
    stars = [(rand(1:width), rand(1:star_y_max), rand()) for _ in 1:n_stars]

    # Pre-generate distant tree positions on hills (x, height_scale, y_offset up the slope)
    # Distant trees (nearer foothills, medium) - scattered up to 35px down the slope
    distant_trees = [(rand(1:width), 0.6 + rand() * 0.4, rand() * 35) for _ in 1:round(Int, width * 0.06)]

    # Initialize pair of deer walking on the snowy foothills
    deer = [
        Deer(width * 0.3, 0.0, 0.8, 0.3, 0.0, true),   # First deer
        Deer(width * 0.22, 0.0, 0.75, 0.3, π, true),   # Second deer, slightly behind
    ]

    # Initialize skier on the snowy hill
    skier = Skier(width * 0.9, 1.2, 0.0, false)  # Skiing left across the hill

    # Pre-sort trees by y position (back to front)
    sorted_tree_indices = sort(collect(enumerate(trees)), by=t->t[2].y)

    # Pre-compute hill heights for each x coordinate
    hill2_heights = Vector{Float64}(undef, width)
    hill3_heights = Vector{Float64}(undef, width)
    for x in 1:width
        cx = (x - width/2) / (width/2)
        valley = abs(cx)^1.5 * sign(cx)^2
        peak2_left = exp(-((x - width*0.25)^2) / (width*60)) * height * 0.10
        peak2_right = exp(-((x - width*0.75)^2) / (width*60)) * height * 0.10
        peak2_mid = exp(-((x - width*0.5)^2) / (width*120)) * height * 0.05
        ridge2 = height * 0.48 - valley * height * 0.15 - peak2_left - peak2_right + peak2_mid
        hill2_heights[x] = ridge2 + 20 * sin(x * 0.012 + 2) + 12 * sin(x * 0.028) + 6 * sin(x * 0.055 + 1)
        ridge3 = height * 0.60 - valley * height * 0.08
        hill3_heights[x] = ridge3 + 15 * sin(x * 0.015 + 0.5) + 8 * sin(x * 0.04 + 3) + 4 * sin(x * 0.08)
    end

    # Pre-render static background (sky gradient, moon, hills)
    bg_img = zeros(RGB{Float64}, height, width)
    sky_color_top = RGB{Float64}(0.02, 0.03, 0.10)

    # Sky gradient
    for y in 1:height
        t = y / height
        t_smooth = t * t * (3 - 2 * t)
        r, g, b = 0.02 + 0.09 * t_smooth, 0.03 + 0.12 * t_smooth, 0.10 + 0.20 * t_smooth
        @inbounds for x in 1:width
            bg_img[y, x] = RGB{Float64}(r, g, b)
        end
    end

    # Moon (static)
    moon_x, moon_y = width * 0.82, height * 0.12
    moon_radius = 35.0
    moon_y_min = max(1, round(Int, moon_y - moon_radius - 60))
    moon_y_max = min(height, round(Int, moon_y + moon_radius + 60))
    moon_x_min = max(1, round(Int, moon_x - moon_radius - 60))
    moon_x_max = min(width, round(Int, moon_x + moon_radius + 60))

    for y in moon_y_min:moon_y_max, x in moon_x_min:moon_x_max
        dx, dy = x - moon_x, y - moon_y
        dist = sqrt(dx^2 + dy^2)
        if dist < moon_radius
            shade = clamp(0.85 + 0.15 * (-dx - dy) / (moon_radius * 1.4), 0.6, 1.0)
            crater1 = exp(-((dx + 8)^2 + (dy + 5)^2) / 80) * 0.15
            crater2 = exp(-((dx - 12)^2 + (dy - 3)^2) / 60) * 0.12
            crater3 = exp(-((dx + 3)^2 + (dy - 10)^2) / 40) * 0.10
            edge = clamp(1 - (dist - moon_radius + 2) / 2, 0, 1)
            brightness = shade - crater1 - crater2 - crater3
            c = bg_img[y, x]
            bg_img[y, x] = RGB{Float64}(
                brightness * 0.98 * edge + red(c) * (1-edge),
                brightness * 0.96 * edge + green(c) * (1-edge),
                brightness * 0.88 * edge + blue(c) * (1-edge)
            )
        elseif dist < moon_radius + 60
            glow = max(0, 1 - (dist - moon_radius) / 60)^2.5 * 0.4
            c = bg_img[y, x]
            bg_img[y, x] = RGB{Float64}(
                min(1, red(c) + glow * 0.95),
                min(1, green(c) + glow * 0.92),
                min(1, blue(c) + glow * 0.85)
            )
        end
    end

    # Hills (static)
    for x in 1:width
        hill2_y = hill2_heights[x]
        for y in max(1, round(Int, hill2_y)):height
            @inbounds if bg_img[y, x] == bg_img[1, 1]
                fade = clamp((y - hill2_y) / 45, 0, 1)
                bg_img[y, x] = RGB{Float64}(0.10 + 0.07 * fade, 0.13 + 0.09 * fade, 0.22 + 0.08 * fade)
            end
        end
        hill3_y = hill3_heights[x]
        for y in max(1, round(Int, hill3_y)):height
            @inbounds c = bg_img[y, x]
            if blue(c) < 0.45
                fade = clamp((y - hill3_y) / 35, 0, 1)
                bg_img[y, x] = RGB{Float64}(0.45 + 0.35 * fade, 0.50 + 0.32 * fade, 0.58 + 0.26 * fade)
            end
        end
    end

    # Distant trees on background (static)
    for (tx, scale, y_off) in distant_trees
        cx_val = (tx - width/2) / (width/2)
        valley = abs(cx_val)^1.5
        ridge3 = height * 0.60 - valley * height * 0.08
        base_y = ridge3 + 15 * sin(tx * 0.015 + 0.5) + 8 * sin(tx * 0.04 + 3) + 4 * sin(tx * 0.08) + y_off
        tree_h = (18 + 14 * scale) * (1 - y_off * 0.005)
        tree_color = RGB{Float64}(0.12 + y_off * 0.002, 0.18 + y_off * 0.002, 0.14 + y_off * 0.002)
        tree_w = tree_h * 0.4
        for dy in 0:round(Int, tree_h)
            y = round(Int, base_y - dy)
            1 <= y <= height || continue
            progress = dy / tree_h
            half_w = round(Int, tree_w * (1 - progress * 0.85))
            for ddx in -half_w:half_w
                xx = tx + ddx
                if 1 <= xx <= width
                    shade = 0.85 + 0.15 * rand()
                    bg_img[y, xx] = RGB{Float64}(red(tree_color) * shade, green(tree_color) * shade, blue(tree_color) * shade)
                end
            end
        end
    end

    # Pre-generate tree light positions for each tree
    tree_lights = Dict{Int, Vector{Tuple{Int,Int,RGB{Float64},Float64}}}()
    tree_snow = Dict{Int, Vector{Tuple{Int,Int}}}()
    for (i, tree) in enumerate(trees)
        lights = Tuple{Int,Int,RGB{Float64},Float64}[]
        snow_positions = Tuple{Int,Int}[]
        cx, base_y, tree_h, tree_w = tree.x, tree.y, tree.h, tree.w
        n_lights = round(Int, tree_h * tree_w * 0.003)  # Light density
        for _ in 1:n_lights
            # Random position within tree bounds
            rel_y = rand()  # 0 = bottom, 1 = top
            layer_width = tree_w * (1 - rel_y * 0.7)
            lx = cx + round(Int, (rand() - 0.5) * layer_width)
            ly = round(Int, base_y - rel_y * tree_h * 0.9)
            push!(lights, (lx, ly, rand(light_colors), rand() * 2π))
        end
        tree_lights[i] = lights

        # Pre-generate snow cap positions for each layer
        # Snow accumulates on the TOP EDGE of each layer (where branches spread out)
        n_layers = 8
        for layer in 1:n_layers
            # layer_base is where this layer starts (the widest point / shelf)
            layer_base = base_y - (layer - 1) * tree_h ÷ (n_layers + 2)
            layer_width = tree_w * (n_layers - layer + 3) / (n_layers + 2)
            # Snow sits on the shelf at the base of each layer
            snow_y = layer_base
            # Only add snow near the edges where branches stick out
            half_w = round(Int, layer_width / 1.3)
            for dx in -half_w:half_w
                # More likely to have snow near edges of branches
                edge_prob = 0.3 + 0.5 * (abs(dx) / (half_w + 1))
                if rand() < edge_prob
                    push!(snow_positions, (cx + dx, snow_y))
                end
            end
        end
        tree_snow[i] = snow_positions
    end

    function draw_tree!(img, tree, tree_idx, frame)
        cx, base_y, tree_h, tree_w = tree.x, tree.y, tree.h, tree.w
        h, w = size(img)

        # Trunk with bark texture
        trunk_w = max(4, tree_w ÷ 7)
        trunk_h = tree_h ÷ 4
        for dy in 0:trunk_h, dx in -trunk_w÷2:trunk_w÷2
            ty, tx = base_y + dy, cx + dx
            if 1 <= ty <= h && 1 <= tx <= w
                bark = 0.28 + 0.08 * rand()
                img[ty, tx] = RGB{Float64}(bark + 0.08, bark - 0.05, bark - 0.12)
            end
        end

        # Layered foliage - more layers for denser trees
        n_layers = 8
        for layer in 1:n_layers
            layer_base = base_y - (layer - 1) * tree_h ÷ (n_layers + 2)
            layer_top = base_y - layer * tree_h ÷ n_layers
            layer_width = tree_w * (n_layers - layer + 3) / (n_layers + 2)

            for y in max(1, layer_top):min(h, layer_base)
                progress = (layer_base - y) / max(1, layer_base - layer_top)
                # Sharper taper toward top (use power function for pointed top)
                taper = (1 - progress * 0.55) ^ (1 + layer * 0.15)
                half_w = max(0, Int(round(layer_width * taper)))

                for dx in -half_w:half_w
                    tx = cx + dx
                    if 1 <= tx <= w
                        # Dense pine needle texture with more variation
                        edge_fade = 1 - 0.7 * (abs(dx) / (half_w + 1))^0.8
                        shade = 0.06 + 0.14 * rand() * edge_fade
                        green_val = 0.30 + 0.28 * rand() * edge_fade
                        img[y, tx] = RGB{Float64}(shade, green_val, shade * 0.35)
                    end
                end
            end
        end

        # Draw pre-generated snow caps (fixed positions, no flicker)
        for (sx, sy) in tree_snow[tree_idx]
            if 1 <= sy <= h && 1 <= sx <= w
                for sdy in -1:1
                    py = sy + sdy
                    if 1 <= py <= h
                        brightness = 0.92 + 0.04 * sdy  # Slightly vary by row
                        img[py, sx] = RGB{Float64}(brightness, brightness, brightness + 0.02)
                    end
                end
            end
        end

        # Draw pre-generated lights (bigger, fixed position)
        for (lx, ly, color, phase) in tree_lights[tree_idx]
            if 1 <= ly <= h && 1 <= lx <= w
                twinkle = 0.5 + 0.5 * sin(frame * 0.25 + phase)
                # Draw bigger lights (3x3 with glow)
                for dy in -2:2, dx in -2:2
                    px, py = lx + dx, ly + dy
                    if 1 <= py <= h && 1 <= px <= w
                        dist = sqrt(dx^2 + dy^2)
                        if dist <= 2.5
                            glow = (1 - dist / 3)^0.7 * twinkle
                            old = img[py, px]
                            img[py, px] = RGB{Float64}(
                                min(1.0, red(old) + red(color) * glow),
                                min(1.0, green(old) + green(color) * glow),
                                min(1.0, blue(old) + blue(color) * glow)
                            )
                        end
                    end
                end
            end
        end
    end

    function draw_flake!(img, flake, h, w)
        x, y = round(Int, flake.x), round(Int, flake.y)
        r = flake.size * (0.4 + 0.6 * flake.depth)
        alpha = 0.25 + 0.75 * flake.depth
        ri = ceil(Int, r)
        r_inv = 1 / r
        @inbounds for dy in -ri:ri, dx in -ri:ri
            dist_sq = dx*dx + dy*dy
            if dist_sq <= r*r
                px, py = x + dx, y + dy
                if 1 <= py <= h && 1 <= px <= w
                    dist = sqrt(dist_sq)
                    brightness = alpha * max(0, 1 - dist * r_inv)^0.6
                    old = img[py, px]
                    img[py, px] = RGB{Float64}(
                        min(1.0, red(old) + brightness * 0.97),
                        min(1.0, green(old) + brightness * 0.98),
                        min(1.0, blue(old) + brightness)
                    )
                end
            end
        end
    end

    function draw_deer!(img, d::Deer, h, w, hill3_heights)
        sc = d.scale
        dx = d.facing_right ? 1 : -1
        x_int = clamp(round(Int, d.x), 1, w)
        base_y = hill3_heights[x_int] + 20

        body_color = RGB{Float64}(0.45, 0.32, 0.22)
        belly_color = RGB{Float64}(0.55, 0.45, 0.38)
        leg_swing = sin(d.leg_phase) * 3 * sc

        body_len = round(Int, 28 * sc)
        body_h = round(Int, 14 * sc)
        leg_len = round(Int, 16 * sc)
        neck_len = round(Int, 12 * sc)
        head_size = round(Int, 8 * sc)

        bx, by = round(Int, d.x), round(Int, base_y)

        # Legs
        for (lx, swing) in [(bx + round(Int, 8*sc*dx), leg_swing), (bx + round(Int, 5*sc*dx), -leg_swing),
                            (bx - round(Int, 8*sc*dx), -leg_swing), (bx - round(Int, 11*sc*dx), leg_swing)]
            for ly in 0:leg_len
                px = lx + round(Int, swing * ly / leg_len)
                py = by + ly
                if 1 <= py <= h && 1 <= px <= w
                    @inbounds img[py, px] = body_color
                    px+1 <= w && (@inbounds img[py, px+1] = body_color)
                end
            end
        end

        # Body
        for bdy in -body_h÷2:body_h÷2
            row_width = round(Int, body_len * sqrt(max(0, 1 - (2*bdy/body_h)^2)) / 2)
            col = bdy > 0 ? belly_color : body_color
            for bdx in -row_width:row_width
                px, py = bx + bdx * dx, by - body_h÷2 + bdy
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = col)
            end
        end

        # Neck
        neck_base_x = bx + round(Int, 10*sc*dx)
        for ny in 0:neck_len
            nx, py = neck_base_x + round(Int, ny * 0.3 * dx), by - body_h÷2 - ny
            if 1 <= py <= h
                for nw in -1:1
                    1 <= nx+nw <= w && (@inbounds img[py, nx+nw] = body_color)
                end
            end
        end

        # Head
        head_x = neck_base_x + round(Int, (neck_len * 0.3 + head_size÷2) * dx)
        head_y = by - body_h÷2 - neck_len - head_size÷2
        hs2 = head_size÷2
        for hdy in -hs2:hs2, hdx in -hs2:hs2
            if hdx*hdx + hdy*hdy <= hs2*hs2
                px, py = head_x + hdx, head_y + hdy
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = body_color)
            end
        end

        # Ears
        for ey in 0:round(Int, 5*sc), eoff in [-3, 3]
            px, py = head_x + round(Int, eoff * sc), head_y - hs2 - ey
            1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = body_color)
        end

        # Tail
        tail_x, tail_y = bx - round(Int, 12*sc*dx), by - body_h÷3
        for ty in 0:round(Int, 4*sc)
            px, py = tail_x + round(Int, ty * 0.5 * dx), tail_y - ty
            1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = belly_color)
        end
    end

    function draw_skier!(img, s::Skier, h, w, hill3_heights)
        dx = s.facing_right ? 1 : -1
        x_int = clamp(round(Int, s.x), 1, w)
        base_y = hill3_heights[x_int] + 25

        # Colors
        jacket_color = RGB{Float64}(0.8, 0.15, 0.1)   # Red jacket
        pants_color = RGB{Float64}(0.1, 0.1, 0.15)    # Dark pants
        skin_color = RGB{Float64}(0.9, 0.75, 0.65)    # Skin
        ski_color = RGB{Float64}(0.2, 0.4, 0.8)       # Blue skis
        pole_color = RGB{Float64}(0.3, 0.3, 0.35)     # Gray poles

        bx, by = round(Int, s.x), round(Int, base_y)

        # Skis (long and thin)
        ski_len = 25
        for ski_off in [-3, 3]
            for sx in -ski_len÷2:ski_len÷2
                px = bx + sx * dx + ski_off
                py = by + 2
                if 1 <= py <= h && 1 <= px <= w
                    @inbounds img[py, px] = ski_color
                    py+1 <= h && (@inbounds img[py+1, px] = ski_color)
                end
            end
            # Ski tips curved up
            tip_x = bx + (ski_len÷2 + 2) * dx + ski_off
            for tip_y in 0:3
                px = tip_x + tip_y * dx
                py = by + 1 - tip_y
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = ski_color)
            end
        end

        # Legs (bent skiing position)
        leg_h = 12
        for leg_off in [-2, 2]
            for ly in 0:leg_h
                px = bx + leg_off + round(Int, ly * 0.15 * dx)
                py = by - ly
                if 1 <= py <= h && 1 <= px <= w
                    @inbounds img[py, px] = pants_color
                    px+1 <= w && (@inbounds img[py, px+1] = pants_color)
                end
            end
        end

        # Body/torso (leaning forward)
        torso_h = 14
        for ty in 0:torso_h
            torso_width = ty < 4 ? 4 : (ty < 10 ? 5 : 4)
            lean = round(Int, ty * 0.25 * dx)
            for tw in -torso_width÷2:torso_width÷2
                px = bx + tw + lean
                py = by - leg_h - ty
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = jacket_color)
            end
        end

        # Arms with poles
        arm_y = by - leg_h - 10
        pole_swing = sin(s.pole_phase) * 4
        for (arm_dir, swing) in [(-1, pole_swing), (1, -pole_swing)]
            arm_x = bx + arm_dir * 5
            # Upper arm
            for ay in 0:6
                px = arm_x + round(Int, (ay * 0.4 + swing * 0.3) * dx)
                py = arm_y + ay
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = jacket_color)
            end
            # Pole
            pole_top_x = arm_x + round(Int, (2 + swing * 0.3) * dx)
            pole_top_y = arm_y + 6
            for py_off in 0:20
                px = pole_top_x + round(Int, py_off * 0.15 * dx)
                py = pole_top_y + py_off
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = pole_color)
            end
        end

        # Head
        head_y = by - leg_h - torso_h - 5
        head_x = bx + round(Int, torso_h * 0.25 * dx)
        for hdy in -4:4, hdx in -3:3
            if hdx*hdx + hdy*hdy <= 16
                px, py = head_x + hdx, head_y + hdy
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = skin_color)
            end
        end

        # Helmet/hat
        for hdy in -5:-2, hdx in -4:4
            if hdx*hdx + (hdy+3)*(hdy+3) <= 20
                px, py = head_x + hdx, head_y + hdy
                1 <= py <= h && 1 <= px <= w && (@inbounds img[py, px] = jacket_color)
            end
        end
    end

    # Check sixel support
    sixel_ok = Sixel.is_sixel_supported()
    if !sixel_ok
        @warn "Terminal doesn't support Sixel graphics. Try iTerm2, WezTerm, or mlterm."
    end

    frame = 0
    print("\e[2J\e[H")  # Clear screen
    print("\e[?25l")    # Hide cursor

    # Pre-allocate frame buffer
    img = zeros(RGB{Float64}, height, width)
    img_out = zeros(RGB{N0f8}, height, width)

    timer = Timer(1/fps)
    try
        while true
            frame += 1

            # Copy pre-rendered background
            copyto!(img, bg_img)

            # Draw twinkling stars (only need to update star pixels)
            @inbounds for (sx, sy, phase) in stars
                twinkle = 0.4 + 0.6 * abs(sin(frame * 0.15 + phase * 10))
                img[sy, sx] = RGB{Float64}(twinkle, twinkle * 0.98, twinkle * 0.9)
            end

            # Update and draw deer
            for d in deer
                d.x += d.speed * (d.facing_right ? 1 : -1)
                d.leg_phase += 0.08
                if d.x > width + 50
                    d.x = -40
                elseif d.x < -50
                    d.x = width + 40
                end
                draw_deer!(img, d, height, width, hill3_heights)
            end

            # Update and draw skier
            skier.x += skier.speed * (skier.facing_right ? 1 : -1)
            skier.pole_phase += 0.15
            if skier.x > width + 50
                skier.x = -40
                skier.facing_right = false
            elseif skier.x < -50
                skier.x = width + 40
                skier.facing_right = true
            end
            draw_skier!(img, skier, height, width, hill3_heights)

            # Ground snow
            @inbounds for x in 1:width
                ground_base = height - 25 + 8 * sin(x * 0.025) + 4 * sin(x * 0.06 + 1)
                snow_top = clamp(round(Int, min(ground_snow[x], ground_base)), 1, height)
                inv_depth = 1 / max(1, height - snow_top)
                for y in snow_top:height
                    depth = (y - snow_top) * inv_depth
                    noise = 0.02 * sin(x * 0.15 + y * 0.1)
                    img[y, x] = RGB{Float64}(0.88 - 0.10 * depth + noise, 0.90 - 0.08 * depth + noise, 0.96 - 0.04 * depth + noise * 0.5)
                end
            end

            # Trees
            for (i, tree) in sorted_tree_indices
                draw_tree!(img, tree, i, frame)
            end

            # Update snowflakes in-place
            @inbounds for i in eachindex(flakes)
                flake = flakes[i]
                wind = 0.4 * sin(frame * 0.04 + flake.y * 0.015)
                new_x = mod(flake.x + flake.drift + wind - 1, width) + 1
                new_y = flake.y + flake.speed * (0.7 + 0.3 * flake.depth)
                gx = clamp(round(Int, new_x), 1, width)
                if new_y >= ground_snow[gx] - 1
                    if rand() < 0.05
                        ground_snow[gx] = max(height * 0.55, ground_snow[gx] - 0.15)
                        for ddx in -3:3
                            nx = clamp(gx + ddx, 1, width)
                            ground_snow[nx] = 0.7 * ground_snow[nx] + 0.3 * ground_snow[gx]
                        end
                    end
                    flakes[i] = Snowflake(rand()*width, -rand()*30, 0.5+rand()*2.5, 0.8+rand()*2.5, randn()*0.3, rand())
                else
                    flakes[i] = Snowflake(new_x, new_y, flake.size, flake.speed, flake.drift, flake.depth)
                end
            end

            # Sort and draw snowflakes
            sort!(flakes, by=f->f.depth)
            @inbounds for flake in flakes
                1 <= flake.y <= height && draw_flake!(img, flake, height, width)
            end

            # Vignette
            @inbounds for y in 1:height, x in 1:width
                dx_v, dy_v = (x - width/2) / (width/2), (y - height/2) / (height/2)
                dist_sq = dx_v*dx_v + dy_v*dy_v
                v = clamp(1 - 0.30 * dist_sq * (1 + 0.3 * dist_sq) + ((x & 3) + 4 * (y & 3)) / 16.0 * 0.015 - 0.0075, 0.0, 1.0)
                c = img[y, x]
                img[y, x] = RGB{Float64}(red(c)*v, green(c)*v, blue(c)*v)
            end

            # Convert to output format
            @inbounds for I in CartesianIndices(img)
                y, x = Tuple(I)
                c = img[I]
                d = ((x & 3) + 4 * (y & 3)) / 16.0 / 255 - 0.5/255
                img_out[I] = RGB{N0f8}(clamp(red(c)+d,0,1), clamp(green(c)+d,0,1), clamp(blue(c)+d,0,1))
            end

            print("\e[H")
            sixel_encode(img_out; transpose=false)
            println("\n Frame $frame | Ctrl+C to exit")
            try wait(timer) catch; end
        end
    catch e
        isa(e, InterruptException) || rethrow(e)
    finally
        close(timer)
        print("\e[?25h")  # Show cursor
    end
end

# Run it!
snow_hd()
26 Likes