Let it snow()

snow

26 Likes

It’s night-time where I am

20 Likes

My default background is yellow, but that has unpleasant connotations for a “snow” theme … :snowman::paw_prints:

15 Likes

Yellow is a wild choice for a default background color :yellow_heart:

7 Likes

Did AI really write this with no help?

Just the last tree changes. I did have to tell it interpolation is $() not ${}. Also, I asked it to make the trees green but it had an io issue where the trees used a separate IOBuffer, not the IOContext directly so printstyled didn’t enable color… but the trees with lights were pretty impressive

3 Likes

This works on my Android phone.

8 Likes

Yellow screens keep your eyes healthy

1 Like

fwiw the harms of blue light have been exaggerated out of context (an LED screen is much weaker than sunlight or lamps) and the evidence for the same risks from digital devices is usually very weak (though a blue light-filtering software subscription service would tell you the opposite). The most justified ones are impacts on sleep and eye strain in the dark, though it’s not recommended to use dimmer night modes too late either.
Should You Be Worried About Blue Light? - American Academy of Ophthalmology

9 Likes

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

got error

julia> snow_hd()
ERROR: OutOfMemoryError()
Stacktrace:
  [1] GenericMemory
    @ .\boot.jl:516 [inlined]
  [2] new_as_memoryref
    @ .\boot.jl:535 [inlined]
  [3] Array
    @ .\boot.jl:582 [inlined]
  [4] Array
    @ .\boot.jl:592 [inlined]
  [5] Matrix{RGB{N0f8}}(x::PermutedDimsArray{RGB{N0f8}, 2, (2, 1), (2, 1), Matrix{RGB{N0f8}}})
    @ Base .\array.jl:626
  [6] convert(::Type{Matrix{RGB{N0f8}}}, a::PermutedDimsArray{RGB{N0f8}, 2, (2, 1), (2, 1), Matrix{RGB{N0f8}}})
    @ Base .\array.jl:618
  [7] sixel_encode(io::Base.TTY, img::Matrix{RGB{N0f8}}, enc::Sixel.LibSixel.LibSixelEncoder; transpose::Bool, kwargs::@Kwargs{})
    @ Sixel C:\Users\ty\.julia\packages\Sixel\X78VX\src\encoder.jl:89
  [8] sixel_encode(io::Base.TTY, img::Matrix{RGB{N0f8}}, enc::Sixel.LibSixel.LibSixelEncoder)
    @ Sixel C:\Users\ty\.julia\packages\Sixel\X78VX\src\encoder.jl:77
  [9] sixel_encode(img::Matrix{RGB{N0f8}}, enc::Sixel.LibSixel.LibSixelEncoder; kwargs::@Kwargs{})
    @ Sixel C:\Users\ty\.julia\packages\Sixel\X78VX\src\encoder.jl:59
 [10] sixel_encode
    @ C:\Users\ty\.julia\packages\Sixel\X78VX\src\encoder.jl:59 [inlined]
 [11] sixel_encode(img::Matrix{RGB{N0f8}})
    @ Sixel C:\Users\ty\.julia\packages\Sixel\X78VX\src\encoder.jl:59
 [12] snow_hd(; width::Int64, height::Int64, fps::Int64)
    @ Main .\REPL[47]:595
 [13] snow_hd()
    @ Main .\REPL[47]:1
 [14] top-level scope
    @ REPL[49]:1

Try it after replacing sixel_encode(img_out)sixel_encode(img_out; transpose=false). (I just updated the code above to that). Apparently that avoids an internal copy in Sixel and renders the same for me? Just a Claude guess though..

Let’s see if we can have a white Christmas in 21st century terminals too! (Kitty graphics version. Looks identical to Let it snow() - #30 by ianshmean, so I’m not including a video.)

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

using KittyTerminalImages, ImageShow, ColorTypes, FixedPointNumbers, Random

"""
    HD Realistic Snow Animation using Kitty 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 kitty graphics support
    kitty_ok = !occursin(r"(ghostty|kitty|wezterm)", ENV["TERM"])
    if !kitty_ok
        @warn "Terminal doesn't support Kitty graphics. Try Ghostty, Kitty, or WezTerm."
    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")
            display(img_out)
            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()
1 Like