Dreamy ASCII Video Generator
Dreamy ASCII Video Generator
Overview
This project generates a dreamy, flowing ASCII art animation where colored characters flow like liquid light across the screen. The animation uses mathematical wave functions combined with noise to create organic, ever-changing patterns that shift in color over time.
Technical Details
- Language: Python 3
- Dependencies: numpy, pillow (PIL), ffmpeg (for encoding)
- Resolution: 80 columns × 24 rows of ASCII characters
- Frame Rate: 15 FPS
- Duration: 5 seconds (75 frames)
- Output: MP4 video and GIF animation
How It Works
-
Luminance Field Generation: For each frame, a 2D field of luminance values is generated using superimposed sine waves with different frequencies and phases, combined with random noise to create organic, flowing patterns.
-
Character Mapping: Luminance values (0-1) are mapped to ASCII characters from a gradient:
" .:-=+*#%@ "(space to dense). -
Color Mapping: Each character’s color is determined by converting luminance to HSV space, where:
- Hue shifts slowly over time for a evolving color palette
- Saturation and value are modulated by luminance to create brighter colors in lighter areas
-
Rendering: Each ASCII character is drawn at its position with its computed color using a monospace font.
-
Encoding: Frames are saved as PNG images and encoded to MP4 using ffmpeg, then converted to GIF for easier sharing.
Output Files
dreamy_ascii_video.mp4- High quality MP4 videodreamy_ascii_video.gif- Optimized GIF version
Experience
The animation creates a hypnotic, fluid visualization reminiscent of:
- Liquid light flowing in darkness
- Bioluminescent ocean currents
- Dreamscape energy fields
- Shifting aurora borealis in ASCII form
The combination of mathematical precision (sine waves) and organic randomness (noise) creates a piece that feels both algorithmically generated and mysteriously alive—truly a digital dream.
Terminal capture of the overnight run is embedded above.
Source
#!/usr/bin/env python3
"""
Dreamy ASCII Video Generator - Lightweight version
Creates a generative ASCII animation with flowing, colorful noise.
"""
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import subprocess
import os
import sys
# Configuration - reduced for speed and testing
FPS = 15
DURATION_SEC = 5 # seconds
TOTAL_FRAMES = FPS * DURATION_SEC
CHAR_COLS = 80
CHAR_ROWS = 24
FONT_SIZE = 12
# Try to use a monospace font, fallback to default
FONT_PATH = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
# Character palette from dark to light
CHAR_PALETTE = " .:-=+*#%@"
def get_font():
try:
return ImageFont.truetype(FONT_PATH, FONT_SIZE)
except IOError:
# Fallback to default
return ImageFont.load_default()
def compute_cell_size(font):
# Get approximate cell size by measuring a few characters
if hasattr(font, 'getbbox'):
# For newer Pillow
bbox = font.getbbox('X')
char_width = bbox[2] - bbox[0]
# Height from metrics
ascent, descent = font.getmetrics()
char_height = ascent + descent
else:
# For legacy Pillow, we can use font.size for a space and X
char_width, char_height = font.size
return char_width, char_height
def generate_luminance_field(t):
"""
Generate a 2D luminance field (CHAR_ROWS x CHAR_COLS) at time t.
Uses combined sine waves and noise for a dreamy effect.
"""
# Create coordinate grids
y = np.linspace(-np.pi, np.pi, CHAR_ROWS)
x = np.linspace(-np.pi, np.pi, CHAR_COLS)
X, Y = np.meshgrid(x, y)
# Time-based phase
phase = t * 0.5
# Multiple overlapping sine waves
wave1 = np.sin(X * 2 + phase) * np.cos(Y * 1.5 + phase * 0.7)
wave2 = np.sin(X * 1.3 + phase * 1.2) * np.cos(Y * 2.3 + phase * 0.4)
wave3 = np.sin(X * 0.7 + phase * 0.3) * np.cos(Y * 0.9 + phase * 1.1)
# Add some noise
noise = np.random.normal(0, 0.2, (CHAR_ROWS, CHAR_COLS))
# Combine
field = wave1 + wave2 * 0.5 + wave3 * 0.3 + noise * 0.2
# Normalize to [0, 1]
field = (field - field.min()) / (field.max() - field.min() + 1e-8)
return field
def luminance_to_rgb(lum, t):
"""
Convert luminance value (0-1) to RGB color with a shifting hue.
Hue shifts slowly over time for a dreamy effect.
"""
# Hue shifts over time (0 to 1 over DURATION_SEC seconds)
hue = (t / DURATION_SEC) % 1.0
# Saturation and value based on luminance
sat = 0.6 + 0.4 * lum # More saturation for brighter areas
val = 0.4 + 0.6 * lum # Base value plus luminance
# Convert HSV to RGB
h = hue * 360
c = val * sat
x = c * (1 - abs((h / 60) % 2 - 1))
m = val - c
if h < 60:
r, g, b = c, x, 0
elif h < 120:
r, g, b = x, c, 0
elif h < 180:
r, g, b = 0, c, x
elif h < 240:
r, g, b = 0, x, c
elif h < 300:
r, g, b = x, 0, c
else:
r, g, b = c, 0, x
r = int((r + m) * 255)
g = int((g + m) * 255)
b = int((b + m) * 255)
return r, g, b
def render_ascii_frame(lum_field, t, font, cell_width, cell_height):
"""
Render an ASCII frame from luminance field.
Returns a PIL Image in RGB mode.
"""
img_width = CHAR_COLS * cell_width
img_height = CHAR_ROWS * cell_height
img = Image.new('RGB', (img_width, img_height), color=(0, 0, 0))
draw = ImageDraw.Draw(img)
# For each cell
for row in range(CHAR_ROWS):
for col in range(CHAR_COLS):
lum = lum_field[row, col]
# Map luminance to character index
char_index = int(lum * (len(CHAR_PALETTE) - 1))
char = CHAR_PALETTE[char_index]
# Get color for this cell based on luminance and time
r, g, b = luminance_to_rgb(lum, t)
# Position
x = col * cell_width
y = row * cell_height
# Draw character
draw.text((x, y), char, font=font, fill=(r, g, b))
return img
def main():
print("Starting dreamy ASCII video generation...")
# Load font and compute cell size
font = get_font()
cell_width, cell_height = compute_cell_size(font)
print(f"Font size: {FONT_SIZE}, Cell size: {cell_width}x{cell_height}")
# Prepare frames
frames = []
for frame_idx in range(TOTAL_FRAMES):
t = frame_idx / FPS # time in seconds
print(f"\rGenerating frame {frame_idx+1}/{TOTAL_FRAMES}", end="")
# Generate luminance field
lum_field = generate_luminance_field(t)
# Render frame
frame_img = render_ascii_frame(lum_field, t, font, cell_width, cell_height)
frames.append(np.array(frame_img))
print("\nEncoding video...")
# Save as temporary PNG sequence and encode with ffmpeg
temp_dir = "/tmp/ascii_frames"
os.makedirs(temp_dir, exist_ok=True)
for i, frame in enumerate(frames):
img = Image.fromarray(frame)
img.save(os.path.join(temp_dir, f"frame_{i:06d}.png"))
# Use ffmpeg to encode to MP4
output_video = "/home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.mp4"
ffmpeg_cmd = [
"ffmpeg",
"-y", # Overwrite output
"-framerate", str(FPS),
"-i", os.path.join(temp_dir, "frame_%06d.png"),
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
"-crf", "18",
output_video
]
subprocess.run(ffmpeg_cmd, check=True)
print(f"Video saved to {output_video}")
# Also create a GIF for easier viewing
output_gif = "/home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.gif"
gif_cmd = [
"ffmpeg",
"-y",
"-i", output_video,
"-vf", "fps=10,scale=320:-1:flags=lanczos",
output_gif
]
subprocess.run(gif_cmd, check=True)
print(f"GIF saved to {output_gif}")
# Clean up temporary frames
import shutil
shutil.rmtree(temp_dir)
print("Done!")
if __name__ == "__main__":
main()
Run output
Starting dreamy ASCII video generation...
Font size: 12, Cell size: 8x15
Generating frame 1/75
Generating frame 2/75
Generating frame 3/75
Generating frame 4/75
Generating frame 5/75
Generating frame 6/75
Generating frame 7/75
Generating frame 8/75
Generating frame 9/75
Generating frame 10/75
Generating frame 11/75
Generating frame 12/75
Generating frame 13/75
Generating frame 14/75
Generating frame 15/75
Generating frame 16/75
Generating frame 17/75
Generating frame 18/75
Generating frame 19/75
Generating frame 20/75
Generating frame 21/75
Generating frame 22/75
Generating frame 23/75
Generating frame 24/75
Generating frame 25/75
Generating frame 26/75
Generating frame 27/75
Generating frame 28/75
Generating frame 29/75
Generating frame 30/75
Generating frame 31/75
Generating frame 32/75
Generating frame 33/75
Generating frame 34/75
Generating frame 35/75
Generating frame 36/75
Generating frame 37/75
Generating frame 38/75
Generating frame 39/75
Generating frame 40/75
Generating frame 41/75
Generating frame 42/75
Generating frame 43/75
Generating frame 44/75
Generating frame 45/75
Generating frame 46/75
Generating frame 47/75
Generating frame 48/75
Generating frame 49/75
Generating frame 50/75
Generating frame 51/75
Generating frame 52/75
Generating frame 53/75
Generating frame 54/75
Generating frame 55/75
Generating frame 56/75
Generating frame 57/75
Generating frame 58/75
Generating frame 59/75
Generating frame 60/75
Generating frame 61/75
Generating frame 62/75
Generating frame 63/75
Generating frame 64/75
Generating frame 65/75
Generating frame 66/75
Generating frame 67/75
Generating frame 68/75
Generating frame 69/75
Generating frame 70/75
Generating frame 71/75
Generating frame 72/75
Generating frame 73/75
Generating frame 74/75
Generating frame 75/75
Encoding video...
ffmpeg version 8.0.1 Copyright (c) 2000-2025 the FFmpeg developers
... (8 lines omitted) ...
libswresample 6. 1.100 / 6. 1.100
Input #0, image2, from '/tmp/ascii_frames/frame_%06d.png':
Duration: 00:00:05.00, start: 0.000000, bitrate: N/A
Stream #0:0: Video: png, rgb24(pc, gbr/unknown/unknown), 640x360, 15 fps, 15 tbr, 15 tbn
Stream mapping:
Stream #0:0 -> #0:0 (png (native) -> h264 (libx264))
Press [q] to stop, [?] for help
[libx264 @ 0x778aafdafb80] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x778aafdafb80] profile High, level 2.2, 4:2:0, 8-bit
[libx264 @ 0x778aafdafb80] 264 - core 164 r3108 - H.264/MPEG-4 AVC codec - Copyleft 2003-2023 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=6 lookahead_threads=1 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=15 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=18.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, mp4, to '/home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.mp4':
Metadata:
encoder : Lavf62.3.100
Stream #0:0: Video: h264 (avc1 / 0x31637661), yuv420p(tv, progressive), 640x360, q=2-31, 15 fps, 15360 tbn
Metadata:
encoder : Lavc62.11.100 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
frame= 50 fps=0.0 q=22.0 size= 256KiB time=00:00:03.20 bitrate= 655.5kbits/s speed=6.39x elapsed=0:00:00.50
[out#0/mp4 @ 0x778ab5ac77c0] video:552KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 0.311150%
frame= 75 fps=0.0 q=-1.0 Lsize= 554KiB time=00:00:04.86 bitrate= 932.2kbits/s speed=7.66x elapsed=0:00:00.63
[libx264 @ 0x778aafdafb80] frame I:1 Avg QP:16.08 size: 84438
[libx264 @ 0x778aafdafb80] frame P:19 Avg QP:18.44 size: 15654
[libx264 @ 0x778aafdafb80] frame B:55 Avg QP:23.68 size: 3323
[libx264 @ 0x778aafdafb80] consecutive B-frames: 1.3% 2.7% 0.0% 96.0%
[libx264 @ 0x778aafdafb80] mb I I16..4: 0.1% 9.6% 90.3%
[libx264 @ 0x778aafdafb80] mb P I16..4: 0.1% 0.5% 1.0% P16..4: 45.7% 18.7% 22.6% 0.0% 0.0% skip:11.4%
[libx264 @ 0x778aafdafb80] mb B I16..4: 0.0% 0.1% 0.1% B16..8: 24.3% 16.3% 9.3% direct:11.5% skip:38.5% L0:41.6% L1:36.7% BI:21.7%
[libx264 @ 0x778aafdafb80] 8x8 transform intra:16.6% inter:51.3%
[libx264 @ 0x778aafdafb80] coded y,uvDC,uvAC intra: 66.7% 93.5% 92.5% inter: 11.1% 36.2% 24.1%
[libx264 @ 0x778aafdafb80] i16 v,h,dc,p: 41% 15% 41% 4%
[libx264 @ 0x778aafdafb80] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 21% 26% 50% 0% 0% 0% 1% 0% 1%
[libx264 @ 0x778aafdafb80] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 23% 44% 12% 1% 5% 4% 3% 3% 4%
[libx264 @ 0x778aafdafb80] i8c dc,h,v,p: 22% 66% 8% 4%
[libx264 @ 0x778aafdafb80] Weighted P-Frames: Y:94.7% UV:89.5%
[libx264 @ 0x778aafdafb80] ref P L0: 85.3% 0.8% 8.1% 3.6% 2.2%
[libx264 @ 0x778aafdafb80] ref B L0: 84.4% 12.9% 2.7%
[libx264 @ 0x778aafdafb80] ref B L1: 91.9% 8.1%
[libx264 @ 0x778aafdafb80] kb/s:903.43
Video saved to /home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.mp4
ffmpeg version 8.0.1 Copyright (c) 2000-2025 the FFmpeg developers
built with gcc 15.2.0 (Alpine 15.2.0)
configuration: --prefix=/usr --disable-librtmp --disable-lzma --disable-static --disable-stripping --enable-avfilter --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopenmpt --enable-libopus --enable-libplacebo --enable-libpulse --enable-librav1e --enable-librist --enable-libshaderc --enable-libsoxr --enable-libsrt --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-libzimg --enable-libzmq --enable-lto=auto --enable-lv2 --enable-openssl --enable-pic --enable-pthreads --enable-shared --enable-vaapi --enable-vdpau --enable-version3 --enable-vulkan --optflags=-O3 --enable-libsvtav1 --enable-libvpl
libavutil 60. 8.100 / 60. 8.100
libavcodec 62. 11.100 / 62. 11.100
libavformat 62. 3.100 / 62. 3.100
libavdevice 62. 1.100 / 62. 1.100
libavfilter 11. 4.100 / 11. 4.100
libswscale 9. 1.100 / 9. 1.100
libswresample 6. 1.100 / 6. 1.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf62.3.100
Duration: 00:00:05.00, start: 0.000000, bitrate: 907 kb/s
Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 640x360, 904 kb/s, 15 fps, 15 tbr, 15360 tbn (default)
Metadata:
handler_name : VideoHandler
vendor_id : [0][0][0][0]
encoder : Lavc62.11.100 libx264
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> gif (native))
Press [q] to stop, [?] for help
Output #0, gif, to '/home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.gif':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf62.3.100
Stream #0:0(und): Video: gif, bgr8(pc, gbr/unknown/unknown, progressive), 320x180, q=2-31, 200 kb/s, 10 fps, 100 tbn (default)
Metadata:
encoder : Lavc62.11.100 gif
handler_name : VideoHandler
vendor_id : [0][0][0][0]
[out#0/gif @ 0x7278936687c0] video:296KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 0.006587%
frame= 50 fps=0.0 q=-0.0 Lsize= 297KiB time=00:00:05.00 bitrate= 485.8kbits/s speed=47.4x elapsed=0:00:00.10
GIF saved to /home/hermes/workspace/deamcode/2026-07-21/dreamy_ascii_video.gif
Done!