ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
mirror.c
Go to the documentation of this file.
1
6#include <emscripten.h>
7#include <stdlib.h>
8#include <string.h>
9
10// Logging macros for debug
11#define WASM_LOG(msg) EM_ASM({ console.log('[C] ' + UTF8ToString($0)); }, msg)
12#define WASM_LOG_INT(msg, val) EM_ASM({ console.log('[C] ' + UTF8ToString($0) + ': ' + $1); }, msg, val)
13#define WASM_ERROR(msg) EM_ASM({ console.error('[C] ' + UTF8ToString($0)); }, msg)
14#include <ascii-chat/options/options.h>
15#include <ascii-chat/options/rcu.h>
16#include <ascii-chat/platform/init.h>
17#include <ascii-chat/platform/terminal.h>
18#include <ascii-chat/asciichat_errno.h>
19#include <ascii-chat/log/logging.h>
20#include <ascii-chat/video/ascii.h>
21#include <ascii-chat/video/color_filter.h>
22#include <ascii-chat/video/image.h>
23#include <ascii-chat/video/palette.h>
24#include <ascii-chat/video/ansi_fast.h>
25#include <ascii-chat/video/digital_rain.h>
26#include <ascii-chat/common.h>
27
28// Global digital rain effect context
29static digital_rain_t *g_digital_rain = NULL;
30static char *g_last_rain_output = NULL;
31static double g_last_rain_update_time = 0.0;
32#define RAIN_UPDATE_INTERVAL_MS 100.0 // Update every 100ms for smooth animation
33
34// ============================================================================
35// Initialization
36// ============================================================================
37
43EMSCRIPTEN_KEEPALIVE
44int mirror_init_with_args(const char *args_json) {
45 WASM_LOG("mirror_init_with_args: START");
46 WASM_LOG("mirror_init_with_args: After START log");
47
48 // Initialize platform layer
49 WASM_LOG("Calling platform_init...");
50 WASM_LOG("mirror_init_with_args: About to call platform_init");
51 asciichat_error_t err = platform_init();
52 WASM_LOG("mirror_init_with_args: platform_init returned");
53 if (err != ASCIICHAT_OK) {
54 WASM_ERROR("platform_init FAILED");
55 return -1;
56 }
57 WASM_LOG("platform_init OK");
58
59 // Initialize logging to stderr (console.error in browser)
60 WASM_LOG("Calling log_init...");
61 log_init(NULL, LOG_DEBUG, true, false);
62 WASM_LOG("log_init OK");
63
64 // Parse JSON array into argc/argv
65 // For simplicity, we'll accept a space-separated string instead
66 // JS can pass: "mirror --width 80 --height 40 --color-filter grayscale"
67 WASM_LOG("Parsing arguments...");
68 WASM_LOG("mirror_init_with_args: About to strdup");
69 char *args_copy = strdup(args_json);
70 WASM_LOG("mirror_init_with_args: strdup completed");
71 if (!args_copy) {
72 WASM_ERROR("strdup FAILED");
73 return -1;
74 }
75
76 WASM_LOG("mirror_init_with_args: Starting tokenization");
77 // Count arguments
78 int argc = 0;
79 char *argv[64] = {NULL}; // Max 64 arguments
80 char *token = strtok(args_copy, " ");
81 while (token != NULL && argc < 63) {
82 argv[argc++] = token;
83 token = strtok(NULL, " ");
84 }
85 argv[argc] = NULL;
86 WASM_LOG_INT("Parsed arguments, argc", argc);
87
88 // Initialize options (sets up RCU, defaults, etc.)
89 WASM_LOG("Calling options_init...");
90 WASM_LOG("mirror_init_with_args: About to call options_init");
91 err = options_init(argc, argv);
92 WASM_LOG("mirror_init_with_args: options_init returned");
93 free(args_copy);
94
95 if (err != ASCIICHAT_OK) {
96 WASM_LOG_INT("options_init FAILED", err);
97 return -1;
98 }
99 WASM_LOG("options_init OK");
100
101 // Initialize ANSI color code generation (dec3 cache for RGB values)
102 WASM_LOG("Calling ansi_fast_init...");
104 WASM_LOG("ansi_fast_init OK");
105
106 WASM_LOG("mirror_init_with_args: COMPLETE");
107 return 0;
108}
109
110EMSCRIPTEN_KEEPALIVE
111void mirror_cleanup(void) {
112 if (g_digital_rain) {
113 digital_rain_destroy(g_digital_rain);
114 g_digital_rain = NULL;
115 }
116 if (g_last_rain_output) {
117 SAFE_FREE(g_last_rain_output);
118 g_last_rain_output = NULL;
119 }
122}
123
124// ============================================================================
125// Frame Conversion API
126// ============================================================================
127
128EMSCRIPTEN_KEEPALIVE
129char *mirror_convert_frame(uint8_t *rgba_data, int src_width, int src_height) {
130 if (!rgba_data || src_width <= 0 || src_height <= 0) {
131 return NULL;
132 }
133
134 // Get current settings from options
135 int dst_width = GET_OPTION(width);
136 int dst_height = GET_OPTION(height);
137 color_filter_t filter = (color_filter_t)GET_OPTION(color_filter);
138 terminal_color_mode_t color_mode = (terminal_color_mode_t)GET_OPTION(color_mode);
139 palette_type_t palette_type = (palette_type_t)GET_OPTION(palette_type);
140 bool aspect_ratio = true; // Preserve webcam aspect ratio
141 bool stretch = false; // Don't stretch - maintain proportions
142
143 // Build terminal capabilities structure with user's color mode
144 terminal_capabilities_t caps = {0}; // Zero-initialize all fields first
145 caps.color_level = color_mode;
146 caps.capabilities = 0;
147 caps.color_count = (color_mode == TERM_COLOR_NONE) ? 0
148 : (color_mode == TERM_COLOR_16) ? 16
149 : (color_mode == TERM_COLOR_256) ? 256
150 : 16777216;
151 caps.utf8_support = true;
152 caps.detection_reliable = true;
153 caps.render_mode = (render_mode_t)GET_OPTION(render_mode);
154 caps.wants_background = false;
155 caps.palette_type = palette_type;
156 caps.desired_fps = 60;
157 caps.color_filter = filter;
158
159 // Convert RGBA to RGB (strip alpha channel) and optionally flip horizontally
160 rgb_pixel_t *rgb_pixels = SAFE_MALLOC(src_width * src_height * sizeof(rgb_pixel_t), rgb_pixel_t *);
161 if (!rgb_pixels) {
162 return NULL;
163 }
164
165 bool flip = GET_OPTION(flip_x);
166 for (int y = 0; y < src_height; y++) {
167 for (int x = 0; x < src_width; x++) {
168 int src_x = flip ? (src_width - 1 - x) : x;
169 int src_idx = (y * src_width + src_x) * 4;
170 int dst_idx = y * src_width + x;
171
172 rgb_pixels[dst_idx].r = rgba_data[src_idx + 0];
173 rgb_pixels[dst_idx].g = rgba_data[src_idx + 1];
174 rgb_pixels[dst_idx].b = rgba_data[src_idx + 2];
175 // Alpha (rgba_data[src_idx + 3]) is discarded
176 }
177 }
178
179 // Apply color filter to pixels if needed (except rainbow - handled in ANSI stage)
180 if (filter != COLOR_FILTER_NONE && filter != COLOR_FILTER_RAINBOW) {
181 // color_filter operates on packed RGB24 format
182 uint8_t *rgb24 = (uint8_t *)rgb_pixels;
183 int stride = src_width * 3;
184 float time_seconds = (float)(emscripten_get_now() / 1000.0); // Convert ms to seconds
185 apply_color_filter(rgb24, src_width, src_height, stride, filter, time_seconds);
186 }
187
188 // Create image structure
189 image_t img = {.w = src_width, .h = src_height, .pixels = rgb_pixels, .alloc_method = IMAGE_ALLOC_SIMD};
190
191 // Get palette characters based on selected palette type
192 const char *palette_chars;
193 switch (palette_type) {
194 case PALETTE_BLOCKS:
195 palette_chars = PALETTE_CHARS_BLOCKS;
196 break;
197 case PALETTE_DIGITAL:
198 palette_chars = PALETTE_CHARS_DIGITAL;
199 break;
200 case PALETTE_MINIMAL:
201 palette_chars = PALETTE_CHARS_MINIMAL;
202 break;
203 case PALETTE_COOL:
204 palette_chars = PALETTE_CHARS_COOL;
205 break;
206 case PALETTE_CUSTOM:
207 // Custom palette uses user-provided characters from options
208 palette_chars = GET_OPTION(palette_custom);
209 if (!palette_chars || palette_chars[0] == '\0') {
210 palette_chars = PALETTE_CHARS_STANDARD; // Fallback
211 }
212 break;
213 case PALETTE_STANDARD:
214 default:
215 palette_chars = PALETTE_CHARS_STANDARD;
216 break;
217 }
218
219 // Convert to ASCII using capability-aware function
220 char *ascii_output =
221 ascii_convert_with_capabilities(&img, dst_width, dst_height, &caps, aspect_ratio, stretch, palette_chars);
222
223 // Clean up
224 SAFE_FREE(rgb_pixels);
225
226 if (!ascii_output) {
227 log_error("ascii_convert_with_capabilities returned NULL");
228 return NULL;
229 }
230
231 // Apply rainbow color filter to ANSI output by replacing RGB values
232 // This preserves character selection while applying rainbow colors
233 if (filter == COLOR_FILTER_RAINBOW) {
234 float time_seconds = (float)(emscripten_get_now() / 1000.0); // Convert ms to seconds
235 char *rainbow_output = rainbow_replace_ansi_colors(ascii_output, time_seconds);
236 if (rainbow_output) {
237 SAFE_FREE(ascii_output);
238 ascii_output = rainbow_output;
239 }
240 }
241
242 // Apply digital rain effect if enabled
243 bool matrix_rain = GET_OPTION(matrix_rain);
244 if (matrix_rain) {
245 // Initialize digital rain context if needed or dimensions changed
246 if (!g_digital_rain || g_digital_rain->num_columns != dst_width || g_digital_rain->num_rows != dst_height) {
247 if (g_digital_rain) {
248 digital_rain_destroy(g_digital_rain);
249 }
250 if (g_last_rain_output) {
251 SAFE_FREE(g_last_rain_output);
252 g_last_rain_output = NULL;
253 }
254 g_digital_rain = digital_rain_init(dst_width, dst_height);
255 if (!g_digital_rain) {
256 log_error("Failed to initialize digital rain effect");
257 return ascii_output; // Return without effect on error
258 }
259 g_last_rain_update_time = emscripten_get_now();
260 }
261
262 // Update color from active filter (allows filter changes after initialization)
263 digital_rain_set_color_from_filter(g_digital_rain, filter);
264
265 // Time-based updates: only update effect every RAIN_UPDATE_INTERVAL_MS
266 double current_time = emscripten_get_now();
267 double elapsed_ms = current_time - g_last_rain_update_time;
268
269 if (elapsed_ms >= RAIN_UPDATE_INTERVAL_MS) {
270 // Calculate actual delta time in seconds
271 float delta_time = (float)(elapsed_ms / 1000.0);
272 g_last_rain_update_time = current_time;
273
274 // Apply effect and cache a copy
275 char *rain_output = digital_rain_apply(g_digital_rain, ascii_output, delta_time);
276 if (rain_output) {
277 // Free old cached output
278 if (g_last_rain_output) {
279 SAFE_FREE(g_last_rain_output);
280 }
281 // Cache a copy (we'll return the original rain_output)
282 g_last_rain_output = strdup(rain_output);
283 // Return the rain output (caller will free it)
284 SAFE_FREE(ascii_output);
285 ascii_output = rain_output;
286 }
287 } else if (g_last_rain_output) {
288 // Use cached output
289 SAFE_FREE(ascii_output);
290 ascii_output = strdup(g_last_rain_output);
291 }
292 } else {
293 // Clean up digital rain context if it exists but effect is disabled
294 if (g_digital_rain) {
295 digital_rain_destroy(g_digital_rain);
296 g_digital_rain = NULL;
297 }
298 if (g_last_rain_output) {
299 SAFE_FREE(g_last_rain_output);
300 g_last_rain_output = NULL;
301 }
302 g_last_rain_update_time = 0.0;
303 }
304
305 return ascii_output;
306}
307
308EMSCRIPTEN_KEEPALIVE
309void mirror_free_string(char *ptr) {
310 SAFE_FREE(ptr);
311}
312
313// ============================================================================
314// Help Text API
315// ============================================================================
316
325EMSCRIPTEN_KEEPALIVE
326const char *get_help_text(int mode, const char *option_name) {
327 if (!option_name || !option_name[0])
328 return NULL;
329 asciichat_mode_t mode_enum = (asciichat_mode_t)mode;
330 return options_get_help_text(mode_enum, option_name);
331}
void ansi_fast_init(void)
Definition ansi_fast.c:198
char * ascii_convert_with_capabilities(image_t *original, const ssize_t width, const ssize_t height, const terminal_capabilities_t *caps, const bool use_aspect_ratio, const bool stretch, const char *palette_chars)
Definition ascii.c:190
void aspect_ratio(const ssize_t img_w, const ssize_t img_h, const ssize_t width, const ssize_t height, const bool stretch, ssize_t *out_width, ssize_t *out_height)
char * rainbow_replace_ansi_colors(const char *ansi_string, float time_seconds)
int apply_color_filter(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t stride, color_filter_t filter, float time)
void digital_rain_set_color_from_filter(digital_rain_t *rain, color_filter_t filter)
void digital_rain_destroy(digital_rain_t *rain)
char * digital_rain_apply(digital_rain_t *rain, const char *frame, float delta_time)
digital_rain_t * digital_rain_init(int num_columns, int num_rows)
const char * options_get_help_text(asciichat_mode_t mode, const char *option_name)
Get help text for an option in a specific mode.
Definition help_api.c:27
void platform_destroy(void)
Definition init.c:15
asciichat_error_t platform_init(void)
Definition init.c:10
asciichat_error_t options_init(int argc, char **argv)
void log_init(const char *filename, log_level_t level, bool force_stderr, bool use_mmap)
#define WASM_LOG(msg)
Definition mirror.c:11
#define WASM_LOG_INT(msg, val)
Definition mirror.c:12
#define WASM_ERROR(msg)
Definition mirror.c:13
EMSCRIPTEN_KEEPALIVE void mirror_cleanup(void)
Definition mirror.c:111
EMSCRIPTEN_KEEPALIVE char * mirror_convert_frame(uint8_t *rgba_data, int src_width, int src_height)
Definition mirror.c:129
EMSCRIPTEN_KEEPALIVE void mirror_free_string(char *ptr)
Definition mirror.c:309
#define RAIN_UPDATE_INTERVAL_MS
Definition mirror.c:32
EMSCRIPTEN_KEEPALIVE int mirror_init_with_args(const char *args_json)
Definition mirror.c:44
EMSCRIPTEN_KEEPALIVE const char * get_help_text(int mode, const char *option_name)
Definition mirror.c:326
void options_state_destroy(void)
Definition rcu.c:309