/* * glitchyclock.c * * Copyright © 2019-2020 Thomas White * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ /* Get LED display fonts from here: https://www.keshikan.net/fonts-e.html */ #include #include #include struct glitchyclock { GtkWidget *da; PangoFontDescription *fontdesc; int cue; double real_time_last_cue; double real_time_last_reset; double brightness; int base_hours; int base_minutes; int base_seconds; int flag; double clock_x; double clock_y; double clock_w; double clock_h; }; enum glitch_type { BLACKOUT, /* Fade to black, parameter is fade time in seconds */ FADE_IN, /* Fade from black to running clock, parameter is fade time in seconds */ FADE_IN_FROZEN, /* Fade from black to running clock, parameter is fade time in seconds */ BLACKOUT_FROZEN, GLITCH_BACK, /* Glitch and then jump back, parameter is number of minutes */ GLITCH_STOP, /* Glitch and then stop, parameter is number of minutes */ GLITCH, /* Glitch for the number of seconds */ EOL /* End of list */ }; struct glitch_cue { int type; int parameter; int hours; int minutes; int seconds; }; struct glitch_cue cues[] = { { BLACKOUT, 0, 0,0,0}, { FADE_IN, 1, 23, 10, 50 }, /* Act I sc 1 */ { BLACKOUT, 1, 0,0,0}, { FADE_IN, 1, 9, 52, 50 }, /* Act I sc 2 */ { BLACKOUT, 3, 0,0,0 }, { FADE_IN, 1, 22, 27, 50 }, { BLACKOUT, 1, 0,0,0}, { FADE_IN, 1, 1, 13, 50 }, { BLACKOUT, 0, 0,0,0}, { FADE_IN, 1, 22, 45, 50 }, { GLITCH_BACK, 103, 0,0,0}, { GLITCH_BACK, 33, 0,0,0}, { BLACKOUT, 1, 0, 0, 12 }, { FADE_IN, 1, 2, 1, 50 }, { BLACKOUT, 1, 0,0,0 }, { FADE_IN, 1, 3, 21, 50 }, /* Act I sc VII */ { GLITCH, 1, 0,0,0}, { GLITCH, 3, 0,0,0}, { GLITCH_STOP, 2, 0, 0, 12}, //{ BLACKOUT_FROZEN, 3, 0, 0, 12 }, //{ FADE_IN_FROZEN, 3, 0, 0, 12 }, //{ BLACKOUT_FROZEN, 3 , 0, 0, 12}, //{ FADE_IN_FROZEN, 3, 0, 0, 12 }, { BLACKOUT_FROZEN, 0 , 0, 0, 12}, { EOL, 0, 0, 0, 0 } }; const double screen_w_frac = 0.2; /* Fraction of screen width */ const double screen_y_offset = 0.5; /* Fraction of screen height */ const double clock_aspect = 0.305; /* Clock height divided by width */ static double get_monotonic_seconds() { struct timespec tp; clock_gettime(CLOCK_MONOTONIC, &tp); return tp.tv_sec + (double)tp.tv_nsec/1e9; } static void run_cue(struct glitchyclock *gc) { if ( cues[gc->cue].type == EOL ) return; gc->cue++; gc->real_time_last_cue = get_monotonic_seconds(); gc->flag = 0; if ( cues[gc->cue].type == FADE_IN ) { gc->real_time_last_reset = get_monotonic_seconds(); gc->base_hours = cues[gc->cue].hours; gc->base_minutes = cues[gc->cue].minutes; gc->base_seconds = cues[gc->cue].seconds; } } static void back_cue(struct glitchyclock *gc) { if ( gc->cue == 0 ) return; gc->cue--; gc->real_time_last_cue = get_monotonic_seconds() - 10; gc->flag = 1; if ( cues[gc->cue].type == FADE_IN ) { gc->real_time_last_reset = get_monotonic_seconds() - 10; gc->base_hours = cues[gc->cue].hours; gc->base_minutes = cues[gc->cue].minutes; gc->base_seconds = cues[gc->cue].seconds; } } static gboolean draw_sig(GtkWidget *widget, cairo_t *cr, struct glitchyclock *gc) { double lw, lh; int ilw, ilh; PangoLayout *layout; char timestr[64]; double seconds_elapsed, seconds_rounded; double real_seconds_elapsed, real_seconds_rounded; char col; int clock_hours, clock_minutes, clock_seconds; int glitch = 0; double duty; const char *frozenstr = "uK:IW"; /* Overall background */ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0); cairo_paint(cr); cairo_translate(cr, gc->clock_x, gc->clock_y); layout = pango_layout_new(gtk_widget_get_pango_context(widget)); pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER); pango_layout_set_font_description(layout, gc->fontdesc); seconds_elapsed = get_monotonic_seconds() - gc->real_time_last_cue; modf(seconds_elapsed, &seconds_rounded); real_seconds_elapsed = get_monotonic_seconds() - gc->real_time_last_reset; col = (modf(real_seconds_elapsed, &real_seconds_rounded) < 0.5) ? ':' : ' '; clock_hours = gc->base_hours; clock_minutes = gc->base_minutes; clock_seconds = gc->base_seconds + real_seconds_rounded; clock_minutes += clock_seconds / 60; clock_seconds = clock_seconds % 60; clock_hours += clock_minutes / 60; clock_minutes = clock_minutes % 60; clock_hours = clock_hours % 24; snprintf(timestr, 63, "%02i%c%02i", clock_hours, ':', clock_minutes); if ( cues[gc->cue].type == FADE_IN ) { gc->brightness = seconds_elapsed / cues[gc->cue].parameter; if ( gc->brightness > 1.0 ) gc->brightness = 1.0; } if ( cues[gc->cue].type == FADE_IN_FROZEN ) { gc->brightness = seconds_elapsed / cues[gc->cue].parameter; if ( gc->brightness > 1.0 ) gc->brightness = 1.0; strcpy(timestr, frozenstr); } if ( cues[gc->cue].type == BLACKOUT ) { gc->brightness = 1.0 - (seconds_elapsed / cues[gc->cue].parameter); if ( gc->brightness < 0.0 ) gc->brightness = 0.0; } if ( cues[gc->cue].type == BLACKOUT_FROZEN ) { gc->brightness = 1.0 - (seconds_elapsed / cues[gc->cue].parameter); if ( gc->brightness < 0.0 ) gc->brightness = 0.0; strcpy(timestr, frozenstr); } if ( (cues[gc->cue].type == GLITCH) && (seconds_elapsed < cues[gc->cue].parameter) ) { duty = 0.5; glitch = 1; } if ( cues[gc->cue].type == GLITCH_STOP ) { glitch = 1; duty = 0.5 + seconds_elapsed / cues[gc->cue].parameter; if ( duty > 1.0 ) duty = 1.0; } if ( cues[gc->cue].type == GLITCH_BACK ) { if ( !gc->flag ) { duty = 0.5 + seconds_elapsed / 4; glitch = 1; if ( duty > 1.0 ) { duty = 1.0; gc->real_time_last_reset += cues[gc->cue].parameter * 60; /* The real_time_elapsed will now probably be * negative (unless the clock glitched back by * less than real_time_elapsed). To avoid * negative numbers appearing, go back a whole * day. Yes, I'm a dirty cheat. */ gc->real_time_last_reset -= 24*60*60; gc->flag = 1; } } else { glitch = 0; } } if ( glitch ) { if ( fmod(seconds_elapsed, 0.05) > duty*0.05 ) { strcpy(timestr, "Tz:Yi"); } else { strcpy(timestr, frozenstr); } } pango_layout_set_text(layout, "88:88", -1); pango_cairo_update_layout(cr, layout); pango_layout_get_size(layout, &ilw, &ilh); lw = (double)ilw / PANGO_SCALE; lh = (double)ilh / PANGO_SCALE; cairo_scale(cr, gc->clock_w/lw, gc->clock_h/lh); pango_cairo_update_layout(cr, layout); cairo_set_source_rgb(cr, gc->brightness*0.20, 0.0, 0.0); pango_cairo_show_layout(cr, layout); pango_layout_set_text(layout, timestr, -1); cairo_set_source_rgb(cr, gc->brightness*0.95, gc->brightness*0.0, gc->brightness*0.05); pango_cairo_show_layout(cr, layout); return FALSE; } static gboolean configure_sig(GtkWidget *da, GdkEventConfigure *event, struct glitchyclock *gc) { gc->clock_w = screen_w_frac * event->width; gc->clock_h = clock_aspect * gc->clock_w; gc->clock_x = (event->width - gc->clock_w)/2.0; gc->clock_y = event->height * screen_y_offset; return FALSE; } static gboolean redraw_cb(gpointer data) { struct glitchyclock *gc = data; gtk_widget_queue_draw_area(GTK_WIDGET(gc->da), gc->clock_x, gc->clock_y, gc->clock_w, gc->clock_h); return G_SOURCE_CONTINUE; } static gboolean key_press_sig(GtkWidget *da, GdkEventKey *event, struct glitchyclock *gc) { if ( event->keyval == GDK_KEY_space ) { run_cue(gc); redraw_cb(gc); return TRUE; } if ( event->keyval == GDK_KEY_BackSpace ) { back_cue(gc); redraw_cb(gc); return TRUE; } if ( event->keyval == GDK_KEY_q ) { gtk_main_quit(); } return FALSE; } static gint realise_sig(GtkWidget *da, struct glitchyclock *gc) { GdkCursor *cursor; GdkWindow *win = gtk_widget_get_window(da); /* Keyboard and input method stuff */ gdk_window_set_accept_focus(win, TRUE); g_signal_connect(G_OBJECT(da), "key-press-event", G_CALLBACK(key_press_sig), gc); gc->fontdesc = pango_font_description_from_string("DSEG7 Modern Bold Italic 24"); cursor = gdk_cursor_new_for_display(gtk_widget_get_display(da), GDK_BLANK_CURSOR); gdk_window_set_cursor(gtk_widget_get_window(da), cursor); return FALSE; } int main(int argc, char *argv[]) { struct glitchyclock gc; GtkWidget *mainwindow; gc.cue = 0; gc.brightness = 0.0; gc.real_time_last_cue = get_monotonic_seconds(); gc.real_time_last_reset = get_monotonic_seconds(); gc.base_hours = cues[gc.cue].hours; gc.base_minutes = cues[gc.cue].minutes; gc.base_seconds = cues[gc.cue].seconds; gtk_init(&argc, &argv); mainwindow = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_fullscreen(GTK_WINDOW(mainwindow)); g_signal_connect_swapped(G_OBJECT(mainwindow), "destroy", gtk_main_quit, NULL); gc.da = gtk_drawing_area_new(); gtk_container_add(GTK_CONTAINER(mainwindow), GTK_WIDGET(gc.da)); gtk_widget_set_can_focus(GTK_WIDGET(gc.da), TRUE); gtk_widget_add_events(gc.da, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); g_signal_connect(G_OBJECT(gc.da), "draw", G_CALLBACK(draw_sig), &gc); g_signal_connect(G_OBJECT(gc.da), "realize", G_CALLBACK(realise_sig), &gc); g_signal_connect(G_OBJECT(gc.da), "configure-event", G_CALLBACK(configure_sig), &gc); gtk_widget_grab_focus(GTK_WIDGET(gc.da)); gtk_widget_show_all(mainwindow); g_timeout_add(20, redraw_cb, &gc); gtk_main(); return 0; }