
It’s night-time where I am
My default background is yellow, but that has unpleasant connotations for a “snow” theme … ![]()
![]()
Yellow is a wild choice for a default background color ![]()
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
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
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()
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()
