ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
digital_rain.c
Go to the documentation of this file.
1
7#include <ascii-chat/video/digital_rain.h>
8#include <ascii-chat/video/color_filter.h>
9#include <ascii-chat/debug/memory.h>
10#include <ascii-chat/log/logging.h>
11#include <ascii-chat/util/utf8.h>
12#include <math.h>
13#include <stdlib.h>
14#include <string.h>
15#include <stdio.h>
16
17/* ============================================================================
18 * Math Helpers
19 * ============================================================================ */
20
21#ifndef M_PI
22#define M_PI 3.14159265358979323846
23#endif
24
25#define SQRT_2 1.4142135623730951
26#define SQRT_5 2.23606797749979
27
32static float random_float(float x, float y) {
33 float dt = x * 12.9898f + y * 78.233f;
34 float sn = fmodf(dt, (float)M_PI);
35 return fmodf(sinf(sn) * 43758.5453f, 1.0f);
36}
37
42static float wobble(float x) {
43 return x + 0.3f * sinf((float)SQRT_2 * x) + 0.2f * sinf((float)SQRT_5 * x);
44}
45
49static inline float fract(float x) {
50 return x - floorf(x);
51}
52
53/* ============================================================================
54 * Core Algorithm
55 * ============================================================================ */
56
69static float get_rain_brightness(digital_rain_t *rain, int col, int row, float sim_time) {
70 if (col < 0 || col >= rain->num_columns) {
71 return 0.0f;
72 }
73
74 digital_rain_column_t *column = &rain->columns[col];
75
76 // Calculate time for this column (with random offset and speed variation)
77 float column_time = column->time_offset + sim_time * rain->fall_speed * column->speed_multiplier;
78
79 // Calculate rain time for this cell
80 // Subtract row from column_time so the wave moves DOWN (to higher row numbers)
81 // As column_time increases, the pattern shifts to higher rows (downward)
82 float rain_time = (column_time - (float)row) / rain->raindrop_length;
83
84 // Apply wobble for organic variation
85 rain_time = wobble(rain_time);
86
87 // Create sawtooth wave: fract() wraps time to 0-1, creating repeating drops
88 // Subtract from 1.0 so brightness increases toward bottom of drop
89 return 1.0f - fract(rain_time);
90}
91
92/* ============================================================================
93 * Initialization and Cleanup
94 * ============================================================================ */
95
96digital_rain_t *digital_rain_init(int num_columns, int num_rows) {
97 if (num_columns <= 0 || num_rows <= 0) {
98 log_error("digital_rain_init: invalid dimensions %dx%d", num_columns, num_rows);
99 return NULL;
100 }
101
102 digital_rain_t *rain = SAFE_CALLOC(1, sizeof(digital_rain_t), digital_rain_t *);
103 if (!rain) {
104 log_error("digital_rain_init: failed to allocate context");
105 return NULL;
106 }
107
108 rain->num_columns = num_columns;
109 rain->num_rows = num_rows;
110
111 // Allocate column state array
112 rain->columns = SAFE_CALLOC((size_t)num_columns, sizeof(digital_rain_column_t), digital_rain_column_t *);
113 if (!rain->columns) {
114 log_error("digital_rain_init: failed to allocate column array");
115 SAFE_FREE(rain);
116 return NULL;
117 }
118
119 // Allocate previous brightness array
120 size_t grid_size = (size_t)num_columns * (size_t)num_rows;
121 rain->previous_brightness = SAFE_CALLOC(grid_size, sizeof(float), float *);
122 if (!rain->previous_brightness) {
123 log_error("digital_rain_init: failed to allocate brightness array");
124 SAFE_FREE(rain->columns);
125 SAFE_FREE(rain);
126 return NULL;
127 }
128
129 // Initialize per-column random offsets
130 for (int col = 0; col < num_columns; col++) {
131 digital_rain_column_t *column = &rain->columns[col];
132 column->time_offset = random_float((float)col, 0.0f) * 1000.0f;
133 column->speed_multiplier = random_float((float)col + 0.1f, 0.0f) * 0.5f + 0.5f;
134 column->phase_offset = random_float((float)col + 0.2f, 0.0f) * (float)M_PI * 2.0f;
135 }
136
137 // Set default parameters
138 rain->fall_speed = DIGITAL_RAIN_DEFAULT_FALL_SPEED;
139 rain->raindrop_length = DIGITAL_RAIN_DEFAULT_RAINDROP_LENGTH;
140 rain->brightness_decay = DIGITAL_RAIN_DEFAULT_BRIGHTNESS_DECAY;
141 rain->animation_speed = DIGITAL_RAIN_DEFAULT_ANIMATION_SPEED;
142 rain->color_r = DIGITAL_RAIN_DEFAULT_COLOR_R;
143 rain->color_g = DIGITAL_RAIN_DEFAULT_COLOR_G;
144 rain->color_b = DIGITAL_RAIN_DEFAULT_COLOR_B;
145 rain->cursor_brightness = DIGITAL_RAIN_DEFAULT_CURSOR_BRIGHTNESS;
146 rain->rainbow_mode = false;
147 rain->first_frame = true;
148 rain->time = 0.0f;
149
150 log_info("Digital rain initialized: %dx%d grid", num_columns, num_rows);
151 return rain;
152}
153
154void digital_rain_destroy(digital_rain_t *rain) {
155 if (!rain) {
156 return;
157 }
158
159 SAFE_FREE(rain->columns);
160 SAFE_FREE(rain->previous_brightness);
161 SAFE_FREE(rain);
162}
163
164void digital_rain_reset(digital_rain_t *rain) {
165 if (!rain) {
166 return;
167 }
168
169 rain->time = 0.0f;
170 rain->first_frame = true;
171
172 // Clear previous brightness
173 size_t grid_size = (size_t)rain->num_columns * (size_t)rain->num_rows;
174 memset(rain->previous_brightness, 0, grid_size * sizeof(float));
175}
176
177/* ============================================================================
178 * Parameter Adjustment
179 * ============================================================================ */
180
181void digital_rain_set_fall_speed(digital_rain_t *rain, float speed) {
182 if (rain) {
183 rain->fall_speed = speed;
184 }
185}
186
187void digital_rain_set_raindrop_length(digital_rain_t *rain, float length) {
188 if (rain) {
189 rain->raindrop_length = length;
190 }
191}
192
193void digital_rain_set_color(digital_rain_t *rain, uint8_t r, uint8_t g, uint8_t b) {
194 if (rain) {
195 rain->color_r = r;
196 rain->color_g = g;
197 rain->color_b = b;
198 }
199}
200
201void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter) {
202 if (!rain) {
203 return;
204 }
205
206 // If no filter is set, use default Matrix green
207 if (filter == COLOR_FILTER_NONE) {
208 rain->rainbow_mode = false;
209 digital_rain_set_color(rain, DIGITAL_RAIN_DEFAULT_COLOR_R, DIGITAL_RAIN_DEFAULT_COLOR_G,
210 DIGITAL_RAIN_DEFAULT_COLOR_B);
211 return;
212 }
213
214 // Rainbow mode: enable dynamic color cycling
215 if (filter == COLOR_FILTER_RAINBOW) {
216 rain->rainbow_mode = true;
217 // Set initial color to red (will be overridden during rendering)
218 digital_rain_set_color(rain, 255, 0, 0);
219 return;
220 }
221
222 // Get color from filter metadata
223 rain->rainbow_mode = false;
224 const color_filter_def_t *filter_def = color_filter_get_metadata(filter);
225 if (filter_def) {
226 digital_rain_set_color(rain, filter_def->r, filter_def->g, filter_def->b);
227 }
228}
229
230/* ============================================================================
231 * Frame Processing
232 * ============================================================================ */
233
239static const char *parse_ansi_color(const char *str, int *r, int *g, int *b, bool *is_foreground) {
240 if (*str != '\033') {
241 return NULL;
242 }
243
244 const char *p = str + 1;
245 if (*p != '[') {
246 return NULL;
247 }
248 p++; // Skip [
249
250 // Check for foreground (38) or background (48) truecolor
251 if (*p == '3' && *(p + 1) == '8') {
252 *is_foreground = true;
253 p += 2;
254 } else if (*p == '4' && *(p + 1) == '8') {
255 *is_foreground = false;
256 p += 2;
257 } else {
258 return NULL;
259 }
260
261 // Expect ";2;" for RGB mode
262 if (*p != ';' || *(p + 1) != '2' || *(p + 2) != ';') {
263 return NULL;
264 }
265 p += 3;
266
267 // Parse R
268 *r = 0;
269 while (*p >= '0' && *p <= '9') {
270 *r = *r * 10 + (*p - '0');
271 p++;
272 }
273 if (*p != ';')
274 return NULL;
275 p++;
276
277 // Parse G
278 *g = 0;
279 while (*p >= '0' && *p <= '9') {
280 *g = *g * 10 + (*p - '0');
281 p++;
282 }
283 if (*p != ';')
284 return NULL;
285 p++;
286
287 // Parse B
288 *b = 0;
289 while (*p >= '0' && *p <= '9') {
290 *b = *b * 10 + (*p - '0');
291 p++;
292 }
293 if (*p != 'm')
294 return NULL;
295 p++;
296
297 return p;
298}
299
304static const char *skip_ansi_sequence(const char *str) {
305 if (*str != '\033') {
306 return str;
307 }
308
309 str++; // Skip ESC
310
311 if (*str == '[') {
312 str++; // Skip [
313 // Skip until we find a terminator (@ through ~)
314 while (*str && !(*str >= '@' && *str <= '~')) {
315 str++;
316 }
317 if (*str) {
318 str++; // Skip terminator
319 }
320 }
321
322 return str;
323}
324
329static int generate_modulated_color(char *buf, size_t buf_size, int r, int g, int b, float brightness,
330 bool is_foreground, bool is_cursor) {
331 // Apply cursor brightness boost
332 if (is_cursor) {
333 brightness *= 2.0f;
334 }
335
336 // Clamp brightness
337 if (brightness < 0.0f)
338 brightness = 0.0f;
339 if (brightness > 1.0f)
340 brightness = 1.0f;
341
342 // Modulate RGB by brightness
343 int new_r = (int)((float)r * brightness);
344 int new_g = (int)((float)g * brightness);
345 int new_b = (int)((float)b * brightness);
346
347 // Clamp to valid range
348 if (new_r < 0)
349 new_r = 0;
350 if (new_r > 255)
351 new_r = 255;
352 if (new_g < 0)
353 new_g = 0;
354 if (new_g > 255)
355 new_g = 255;
356 if (new_b < 0)
357 new_b = 0;
358 if (new_b > 255)
359 new_b = 255;
360
361 // Generate ANSI code
362 if (is_foreground) {
363 return snprintf(buf, buf_size, "\033[38;2;%d;%d;%dm", new_r, new_g, new_b);
364 } else {
365 return snprintf(buf, buf_size, "\033[48;2;%d;%d;%dm", new_r, new_g, new_b);
366 }
367}
368
369char *digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time) {
370 if (!rain || !frame) {
371 log_error("digital_rain_apply: NULL parameter");
372 return NULL;
373 }
374
375 // Update time
376 rain->time += delta_time * rain->animation_speed;
377 float sim_time = rain->time;
378
379 // Update rainbow color if rainbow mode is enabled
380 if (rain->rainbow_mode) {
381 color_filter_calculate_rainbow(sim_time, &rain->color_r, &rain->color_g, &rain->color_b);
382 }
383
384 // Allocate output buffer (input size + overhead for ANSI codes)
385 // Estimate: each character might need up to 20 bytes for ANSI codes
386 size_t input_len = strlen(frame);
387 size_t output_capacity = input_len * 20 + 1024;
388 char *output = SAFE_MALLOC(output_capacity, char *);
389 if (!output) {
390 log_error("digital_rain_apply: failed to allocate output buffer");
391 return NULL;
392 }
393
394 const char *src = frame;
395 char *dst = output;
396 size_t remaining = output_capacity;
397
398 int col = 0;
399 int row = 0;
400
401 while (*src && remaining > 100) {
402 // Try to parse and modify ANSI color sequences
403 if (*src == '\033') {
404 int r, g, b;
405 bool is_foreground;
406 const char *after = parse_ansi_color(src, &r, &g, &b, &is_foreground);
407
408 if (after) {
409 // Found a color sequence - calculate brightness for this position
410 float brightness = get_rain_brightness(rain, col, row, sim_time);
411 float brightness_below = get_rain_brightness(rain, col, row + 1, sim_time);
412 bool is_cursor = brightness > brightness_below;
413
414 // Blend with previous brightness for smooth transitions
415 if (!rain->first_frame && row < rain->num_rows && col < rain->num_columns) {
416 size_t idx = (size_t)row * (size_t)rain->num_columns + (size_t)col;
417 float prev_brightness = rain->previous_brightness[idx];
418 brightness = prev_brightness + (brightness - prev_brightness) * rain->brightness_decay;
419 rain->previous_brightness[idx] = brightness;
420 } else if (row < rain->num_rows && col < rain->num_columns) {
421 size_t idx = (size_t)row * (size_t)rain->num_columns + (size_t)col;
422 rain->previous_brightness[idx] = brightness;
423 }
424
425 // Generate modulated color
426 char ansi_buf[32];
427 int ansi_len =
428 generate_modulated_color(ansi_buf, sizeof(ansi_buf), r, g, b, brightness, is_foreground, is_cursor);
429
430 if ((size_t)ansi_len < remaining) {
431 memcpy(dst, ansi_buf, (size_t)ansi_len);
432 dst += ansi_len;
433 remaining -= (size_t)ansi_len;
434 }
435
436 src = after;
437 continue;
438 } else {
439 // Not a color sequence - copy as-is
440 const char *after_skip = skip_ansi_sequence(src);
441 size_t ansi_len = (size_t)(after_skip - src);
442 if (ansi_len < remaining) {
443 memcpy(dst, src, ansi_len);
444 dst += ansi_len;
445 remaining -= ansi_len;
446 src = after_skip;
447 } else {
448 break;
449 }
450 continue;
451 }
452 }
453
454 // Handle newline
455 if (*src == '\n') {
456 *dst++ = *src++;
457 remaining--;
458 row++;
459 col = 0;
460 continue;
461 }
462
463 // Copy regular characters - inject color for non-colored characters
464 if (remaining > 0) {
465 // Calculate brightness for this position
466 float brightness = get_rain_brightness(rain, col, row, sim_time);
467 float brightness_below = get_rain_brightness(rain, col, row + 1, sim_time);
468 bool is_cursor = brightness > brightness_below;
469
470 // Blend with previous brightness
471 if (!rain->first_frame && row < rain->num_rows && col < rain->num_columns) {
472 size_t idx = (size_t)row * (size_t)rain->num_columns + (size_t)col;
473 float prev_brightness = rain->previous_brightness[idx];
474 brightness = prev_brightness + (brightness - prev_brightness) * rain->brightness_decay;
475 rain->previous_brightness[idx] = brightness;
476 } else if (row < rain->num_rows && col < rain->num_columns) {
477 size_t idx = (size_t)row * (size_t)rain->num_columns + (size_t)col;
478 rain->previous_brightness[idx] = brightness;
479 }
480
481 // For characters without explicit color codes, use the rain's default color
482 char ansi_buf[32];
483 int ansi_len = generate_modulated_color(ansi_buf, sizeof(ansi_buf), rain->color_r, rain->color_g, rain->color_b,
484 brightness, true, is_cursor);
485
486 if ((size_t)ansi_len < remaining) {
487 memcpy(dst, ansi_buf, (size_t)ansi_len);
488 dst += ansi_len;
489 remaining -= (size_t)ansi_len;
490 }
491
492 // Decode UTF-8 character to get byte length
493 uint32_t codepoint;
494 int utf8_len = utf8_decode((const uint8_t *)src, &codepoint);
495 if (utf8_len < 0) {
496 // Invalid UTF-8, treat as single byte
497 utf8_len = 1;
498 }
499
500 // Copy all bytes of the UTF-8 character
501 for (int i = 0; i < utf8_len && *src && remaining > 0; i++) {
502 *dst++ = *src++;
503 remaining--;
504 }
505
506 // Only increment column once per character (not per byte)
507 col++;
508 } else {
509 break;
510 }
511 }
512
513 // Null-terminate
514 if (remaining > 0) {
515 *dst = '\0';
516 } else {
517 output[output_capacity - 1] = '\0';
518 }
519
520 rain->first_frame = false;
521 return output;
522}
const color_filter_def_t * color_filter_get_metadata(color_filter_t filter)
void color_filter_calculate_rainbow(float time, uint8_t *r, uint8_t *g, uint8_t *b)
void digital_rain_set_raindrop_length(digital_rain_t *rain, float length)
void digital_rain_reset(digital_rain_t *rain)
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
void digital_rain_set_fall_speed(digital_rain_t *rain, float speed)
void digital_rain_set_color(digital_rain_t *rain, uint8_t r, uint8_t g, uint8_t b)
#define SQRT_5
#define SQRT_2
void digital_rain_destroy(digital_rain_t *rain)
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
#define M_PI
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
int utf8_decode(const uint8_t *s, uint32_t *codepoint)
Definition utf8.c:18