1 /* filmleader, Copyright (c) 2018-2019 Jamie Zawinski <jwz@jwz.org>
2  *
3  * Permission to use, copy, modify, distribute, and sell this software and its
4  * documentation for any purpose is hereby granted without fee, provided that
5  * the above copyright notice appear in all copies and that both that
6  * copyright notice and this permission notice appear in supporting
7  * documentation.  No representations are made about the suitability of this
8  * software for any purpose.  It is provided "as is" without express or
9  * implied warranty.
10  *
11  * Simulate an SMPTE Universal Film Leader playing on an analog television.
12  */
13 
14 #ifdef HAVE_CONFIG_H
15 # include "config.h"
16 #endif /* HAVE_CONFIG_H */
17 
18 #include "xft.h" /* before screenhack.h */
19 
20 #include "screenhack.h"
21 #include "analogtv.h"
22 
23 #include <time.h>
24 
25 #undef countof
26 #define countof(x) (sizeof((x))/sizeof((*x)))
27 
28 struct state {
29   Display *dpy;
30   Window window;
31   XWindowAttributes xgwa;
32   int w, h;
33   unsigned long bg, text_color, ring_color, trace_color;
34   XftColor xft_text_color_1, xft_text_color_2;
35 
36   XftFont *font, *font2, *font3;
37   XftDraw *xftdraw;
38   Pixmap pix;
39   GC gc;
40   double start, last_time;
41   double value;
42   int stop;
43   double noise;
44   analogtv *tv;
45   analogtv_input *inp;
46   analogtv_reception rec;
47   Bool button_down_p;
48 };
49 
50 
51 static void *
filmleader_init(Display * dpy,Window window)52 filmleader_init (Display *dpy, Window window)
53 {
54   struct state *st = (struct state *) calloc (1, sizeof(*st));
55   XGCValues gcv;
56   char *s;
57 
58   st->dpy = dpy;
59   st->window = window;
60   st->tv = analogtv_allocate (st->dpy, st->window);
61   analogtv_set_defaults (st->tv, "");
62   st->tv->need_clear = 1;
63   st->inp = analogtv_input_allocate();
64   analogtv_setup_sync (st->inp, 1, 0);
65   st->rec.input = st->inp;
66   st->rec.level = 2.0;
67   st->tv->use_color = 1;
68   st->rec.level = pow(frand(1.0), 3.0) * 2.0 + 0.05;
69   st->rec.ofs = random() % ANALOGTV_SIGNAL_LEN;
70   st->tv->powerup = 0;
71 
72   st->rec.multipath = 0.0;
73   st->tv->color_control += frand(0.3);
74   st->noise = get_float_resource (st->dpy, "noise", "Float");
75   st->value = 18;  /* Leave time for powerup */
76   st->stop = 2 + (random() % 5);
77   XGetWindowAttributes (dpy, window, &st->xgwa);
78 
79   /* Let's render it into a 16:9 pixmap, since that's what most screens are
80      these days.  That means the circle will be squashed on 4:3 screens. */
81   {
82     double r = 16/9.0;
83 
84 # ifdef HAVE_MOBILE
85     /* analogtv.c always fills whole screen on mobile, so use screen aspect. */
86     r = st->xgwa.width / (double) st->xgwa.height;
87     if (r < 1) r = 1/r;
88 # endif
89 
90     st->w = 712;
91     st->h = st->w / r;
92   }
93 
94   if (st->xgwa.width < st->xgwa.height)
95     {
96       int swap = st->w;
97       st->w = st->h;
98       st->h = swap;
99     }
100 
101   st->pix = XCreatePixmap (dpy, window,
102                            st->w > st->h ? st->w : st->h,
103                            st->w > st->h ? st->w : st->h,
104                            st->xgwa.depth);
105   st->gc = XCreateGC (dpy, st->pix, 0, &gcv);
106 
107   st->xftdraw = XftDrawCreate (dpy, st->pix, st->xgwa.visual,
108                                st->xgwa.colormap);
109   s = get_string_resource (dpy, "numberFont", "Font");
110   st->font = load_xft_font_retry (dpy, screen_number (st->xgwa.screen), s);
111   if (s) free (s);
112   s = get_string_resource (dpy, "numberFont2", "Font");
113   st->font2 = load_xft_font_retry (dpy, screen_number (st->xgwa.screen), s);
114   if (s) free (s);
115   s = get_string_resource (dpy, "numberFont3", "Font");
116   st->font3 = load_xft_font_retry (dpy, screen_number (st->xgwa.screen), s);
117   if (s) free (s);
118 
119   st->bg = get_pixel_resource (dpy, st->xgwa.colormap,
120                                "textBackground", "Background");
121   st->text_color = get_pixel_resource (dpy, st->xgwa.colormap,
122                                        "textColor", "Foreground");
123   st->ring_color = get_pixel_resource (dpy, st->xgwa.colormap,
124                                        "ringColor", "Foreground");
125   st->trace_color = get_pixel_resource (dpy, st->xgwa.colormap,
126                                         "traceColor", "Foreground");
127 
128   s = get_string_resource (dpy, "textColor", "Foreground");
129   XftColorAllocName (dpy, st->xgwa.visual, st->xgwa.colormap, s,
130                      &st->xft_text_color_1);
131   if (s) free (s);
132 
133   s = get_string_resource (dpy, "textBackground", "Background");
134   XftColorAllocName (dpy, st->xgwa.visual, st->xgwa.colormap, s,
135                      &st->xft_text_color_2);
136   if (s) free (s);
137 
138   return st;
139 }
140 
141 
142 static double
double_time(void)143 double_time (void)
144 {
145   struct timeval now;
146 # ifdef GETTIMEOFDAY_TWO_ARGS
147   struct timezone tzp;
148   gettimeofday(&now, &tzp);
149 # else
150   gettimeofday(&now);
151 # endif
152 
153   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
154 }
155 
156 
157 static unsigned long
filmleader_draw(Display * dpy,Window window,void * closure)158 filmleader_draw (Display *dpy, Window window, void *closure)
159 {
160   struct state *st = (struct state *) closure;
161   const analogtv_reception *rec = &st->rec;
162   double then = double_time(), now, timedelta;
163   XImage *img;
164   int i, x, y, w2, h2;
165   XGlyphInfo extents;
166   int lbearing, rbearing, ascent, descent;
167   char s[20];
168   double r = 1 - (st->value - (int) st->value);
169   int ivalue = st->value;
170   XftFont *xftfont;
171   XftColor *xftcolor;
172 
173   /* You may ask, why use Xft for this instead of the much simpler XDrawString?
174      Well, for some reason, XLoadQueryFont is giving me horribly-scaled bitmap
175      fonts, but Xft works properly. So perhaps in This Modern World, if one
176      expects large fonts to work, one must use Xft instead of Xlib?
177 
178      Everything is terrible.
179    */
180 
181   const struct { double t; int k, f; const char * const s[4]; } blurbs[] = {
182     {  9.1, 3, 1, { "PICTURE", "  START", 0, 0 }},
183     { 10.0, 2, 1, { "    16", "SOUND", "START", 0 }},
184     { 10.5, 2, 1, { "    32", "SOUND", "START", 0 }},
185     { 11.6, 2, 0, { "PICTURE", "COMPANY", "SERIES", 0 }},
186     { 11.7, 2, 0, { "XSCRNSAVER", 0, 0, 0 }},
187     { 11.9, 2, 0, { "REEL No.", "PROD No.", "PLAY DATE", 0 }},
188     { 12.2, 0, 0, { "    SMPTE     ", "UNIVERSAL", "   LEADER", 0 }},
189     { 12.3, 0, 1, { "X           ", "X", "X", "X" }},
190     { 12.4, 0, 0, { "    SMPTE     ", "UNIVERSAL", "   LEADER", 0 }},
191     { 12.5, 3, 1, { "PICTURE", 0, 0, 0 }},
192     { 12.7, 3, 1, { "HEAD", 0, 0, 0 }},
193     { 12.8, 2, 1, { "OOOO", 0, "ASPECT", "TYPE OF" }},
194     { 12.9, 2, 0, { "SOUND", 0, "RATIO", 0 }},
195     { 13.2, 1, 1, { "                  ", "PICTURE", 0, 0 }},
196     { 13.3, 1, 0, { "REEL No.      ", "COLOR", 0, 0 }},
197     { 13.4, 1, 0, { "LENGTH        ", 0, 0, "ROLL" }},
198     { 13.5, 1, 0, { "SUBJECT", 0, 0, 0 }},
199     { 13.9, 1, 1, { "     \342\206\221", "SPLICE", " HERE", 0 }},
200   };
201 
202   for (i = 0; i < countof(blurbs); i++)
203     {
204       if (st->value >= blurbs[i].t && st->value <= blurbs[i].t + 1/15.0)
205         {
206           int line_height;
207           int j;
208           xftfont = (blurbs[i].f == 1 ? st->font2 :
209                      blurbs[i].f == 2 ? st->font : st->font3);
210 
211           XSetForeground (dpy, st->gc,
212                           blurbs[i].k == 3 ? st->bg : st->text_color);
213           XFillRectangle (dpy, st->pix, st->gc, 0, 0, st->w, st->h);
214           XSetForeground (dpy, st->gc,
215                           blurbs[i].k == 3 ? st->text_color : st->bg);
216           xftcolor = (blurbs[i].k == 3 ?
217                       &st->xft_text_color_1 : &st->xft_text_color_2);
218 
219           /* The height of a string of spaces is 0... */
220           XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *) "My", 2, &extents);
221           line_height = extents.height;
222 
223           XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *)
224                               blurbs[i].s[0], strlen(blurbs[i].s[0]),
225                               &extents);
226           /* lbearing = -extents.x; */
227           rbearing = extents.width - extents.x;
228           ascent   = extents.y;
229           /* descent  = extents.height - extents.y; */
230 
231           x = (st->w - rbearing) / 2;
232           y = st->h * 0.1 + ascent;
233 
234           for (j = 0; j < countof(blurbs[i].s); j++)
235             {
236               if (blurbs[i].s[j])
237                 XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y,
238                                    (FcChar8 *) blurbs[i].s[j],
239                                    strlen(blurbs[i].s[j]));
240 
241               y += line_height * 1.5;
242 
243               if (blurbs[i].s[j])
244                 {
245                   XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *)
246                                       blurbs[i].s[0], strlen(blurbs[i].s[j]),
247                                       &extents);
248                   /* lbearing = -extents.x; */
249                   /* rbearing = extents.width - extents.x; */
250                   /* ascent   = extents.y; */
251                   /* descent  = extents.height - extents.y; */
252                 }
253             }
254 
255           if (blurbs[i].k == 2)  /* Rotate clockwise and flip */
256             {
257               int wh = st->w < st->h ? st->w : st->h;
258               XImage *img1 = XGetImage (dpy, st->pix,
259                                         (st->w - wh) / 2,
260                                         (st->h - wh) / 2,
261                                         wh, wh, ~0L, ZPixmap);
262               XImage *img2 = XCreateImage (dpy, st->xgwa.visual,
263                                            st->xgwa.depth, ZPixmap, 0, 0,
264                                            wh, wh, 32, 0);
265               img2->data = malloc (img2->bytes_per_line * img2->height);
266               for (y = 0; y < wh; y++)
267                 for (x = 0; x < wh; x++)
268                   XPutPixel (img2, y, x, XGetPixel (img1, x, y));
269               XSetForeground (dpy, st->gc,
270                               blurbs[i].k == 3 ? st->bg : st->text_color);
271               XFillRectangle (dpy, st->pix, st->gc, 0, 0, st->w, st->h);
272               XPutImage (dpy, st->pix, st->gc, img2,
273                          0, 0,
274                          (st->w - wh) / 2,
275                          (st->h - wh) / 2,
276                          wh, wh);
277               XDestroyImage (img1);
278               XDestroyImage (img2);
279             }
280           else if (blurbs[i].k == 1)  /* Flip vertically */
281             {
282               XImage *img1 = XGetImage (dpy, st->pix, 0, 0,
283                                         st->w, st->h, ~0L, ZPixmap);
284               XImage *img2 = XCreateImage (dpy, st->xgwa.visual,
285                                            st->xgwa.depth, ZPixmap, 0, 0,
286                                            st->w, st->h, 32, 0);
287               img2->data = malloc (img2->bytes_per_line * img2->height);
288               for (y = 0; y < img2->height; y++)
289                 for (x = 0; x < img2->width; x++)
290                   XPutPixel (img2, x, img2->height-y-1,
291                              XGetPixel (img1, x, y));
292               XPutImage (dpy, st->pix, st->gc, img2, 0, 0, 0, 0, st->w, st->h);
293               XDestroyImage (img1);
294               XDestroyImage (img2);
295             }
296 
297           goto DONE;
298         }
299     }
300 
301   if (st->value < 2.0 || st->value >= 9.0)	/* Black screen */
302     {
303       XSetForeground (dpy, st->gc, st->text_color);
304       XFillRectangle (dpy, st->pix, st->gc, 0, 0, st->w, st->h);
305       goto DONE;
306     }
307 
308   XSetForeground (dpy, st->gc, st->bg);
309   XFillRectangle (dpy, st->pix, st->gc, 0, 0, st->w, st->h);
310 
311   if (r > 1/30.0)				/* Sweep line and background */
312     {
313       x = st->w/2 + st->w * cos (M_PI * 2 * r - M_PI/2);
314       y = st->h/2 + st->h * sin (M_PI * 2 * r - M_PI/2);
315 
316       XSetForeground (dpy, st->gc, st->trace_color);
317       XFillArc (dpy, st->pix, st->gc,
318                 -st->w, -st->h, st->w*3, st->h*3,
319                 90*64,
320                 90*64 - ((r + 0.25) * 360*64));
321 
322       XSetForeground (dpy, st->gc, st->text_color);
323       XSetLineAttributes (dpy, st->gc, 1, LineSolid, CapRound, JoinRound);
324       XDrawLine (dpy, st->pix, st->gc, st->w/2, st->h/2, x, y);
325 
326       XSetForeground (dpy, st->gc, st->text_color);
327       XSetLineAttributes (dpy, st->gc, 2, LineSolid, CapRound, JoinRound);
328       XDrawLine (dpy, st->pix, st->gc, st->w/2, 0, st->w/2, st->h);
329       XDrawLine (dpy, st->pix, st->gc, 0, st->h/2, st->w, st->h/2);
330     }
331 
332   /* Big number */
333 
334   s[0] = (char) (ivalue + '0');
335   xftfont = st->font;
336   xftcolor = &st->xft_text_color_1;
337   XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *) s, 1, &extents);
338   lbearing = -extents.x;
339   rbearing = extents.width - extents.x;
340   ascent   = extents.y;
341   descent  = extents.height - extents.y;
342 
343   x = (st->w - (rbearing + lbearing)) / 2;
344   y = (st->h + (ascent - descent)) / 2;
345   XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y, (FcChar8 *) s, 1);
346 
347   /* Annotations on 7 and 4 */
348 
349   if ((st->value >= 7.75 && st->value <= 7.85) ||
350       (st->value >= 4.00 && st->value <= 4.25))
351     {
352       XSetForeground (dpy, st->gc, st->ring_color);
353       xftcolor = &st->xft_text_color_2;
354       xftfont = st->font2;
355 
356       s[0] = (ivalue == 4 ? 'C' : 'M');
357       s[1] = 0;
358 
359       XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *) s, strlen(s), &extents);
360       lbearing = -extents.x;
361       rbearing = extents.width - extents.x;
362       ascent   = extents.y;
363       /* descent  = extents.height - extents.y; */
364 
365       x = st->w * 0.1;
366       y = st->h * 0.1 + ascent;
367       XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y,
368                          (FcChar8 *) s, strlen(s));
369       x = st->w * 0.9 - (rbearing + lbearing);
370       XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y,
371                          (FcChar8 *) s, strlen(s));
372 
373       s[0] = (ivalue == 4 ? 'F' : '3');
374       s[1] = (ivalue == 4 ? 0   : '5');
375       s[2] = 0;
376 
377       XftTextExtentsUtf8 (dpy, xftfont, (FcChar8 *) s, strlen(s), &extents);
378       lbearing = -extents.x;
379       rbearing = extents.width - extents.x;
380       /* ascent   = extents.y; */
381       /* descent  = extents.height - extents.y; */
382 
383       x = st->w * 0.1;
384       y = st->h * 0.95;
385       XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y,
386                          (FcChar8 *) s, strlen(s));
387       x = st->w * 0.9 - (rbearing + lbearing);
388       XftDrawStringUtf8 (st->xftdraw, xftcolor, xftfont, x, y,
389                          (FcChar8 *) s, strlen(s));
390     }
391 
392   if (r > 1/30.0)				/* Two rings around number */
393     {
394       double r2 = st->w / (double) st->h;
395       double ss = 1;
396 
397       if (st->xgwa.width < st->xgwa.height)
398         ss = 0.5;
399 
400       XSetForeground (dpy, st->gc, st->ring_color);
401       XSetLineAttributes (dpy, st->gc, st->w * 0.025,
402                           LineSolid, CapRound, JoinRound);
403 
404       w2 = st->w * 0.8 * ss / r2;
405       h2 = st->h * 0.8 * ss;
406       x = (st->w - w2) / 2;
407       y = (st->h - h2) / 2;
408       XDrawArc (dpy, st->pix, st->gc, x, y, w2, h2, 0, 360*64);
409 
410       w2 = w2 * 0.8;
411       h2 = h2 * 0.8;
412       x = (st->w - w2) / 2;
413       y = (st->h - h2) / 2;
414       XDrawArc (dpy, st->pix, st->gc, x, y, w2, h2, 0, 360*64);
415     }
416 
417  DONE:
418 
419   img = XGetImage (dpy, st->pix, 0, 0, st->w, st->h, ~0L, ZPixmap);
420 
421   analogtv_load_ximage (st->tv, st->rec.input, img, 0, 0, 0, 0, 0);
422   analogtv_reception_update (&st->rec);
423   analogtv_draw (st->tv, st->noise, &rec, 1);
424 
425   XDestroyImage (img);
426 
427   now = double_time();
428   timedelta = (1 / 29.97) - (now - then);
429 
430   if (! st->button_down_p)
431     {
432       if (st->last_time == 0)
433         st->start = then;
434       else
435         st->value -= then - st->last_time;
436 
437       if (st->value <= 0 ||
438           (r > 0.9 && st->value <= st->stop))
439         {
440           st->value = (random() % 20) ? 8.9 : 15;
441           st->stop = ((random() % 50) ? 2 : 1) + (random() % 5);
442 
443           if (st->value > 9)	/* Spin the knobs again */
444             {
445               st->rec.level = pow(frand(1.0), 3.0) * 2.0 + 0.05;
446               st->rec.ofs = random() % ANALOGTV_SIGNAL_LEN;
447               st->tv->color_control += frand(0.3) - 0.15;
448             }
449         }
450     }
451 
452   st->tv->powerup = then - st->start;
453   st->last_time = then;
454 
455   return timedelta > 0 ? timedelta * 1000000 : 0;
456 }
457 
458 
459 static void
filmleader_reshape(Display * dpy,Window window,void * closure,unsigned int w,unsigned int h)460 filmleader_reshape (Display *dpy, Window window, void *closure,
461                     unsigned int w, unsigned int h)
462 {
463   struct state *st = (struct state *) closure;
464   analogtv_reconfigure (st->tv);
465   XGetWindowAttributes (dpy, window, &st->xgwa);
466 
467   if ((st->w > st->h) != (st->xgwa.width > st->xgwa.height))
468     {
469       int swap = st->w;
470       st->w = st->h;
471       st->h = swap;
472     }
473 }
474 
475 
476 static Bool
filmleader_event(Display * dpy,Window window,void * closure,XEvent * event)477 filmleader_event (Display *dpy, Window window, void *closure, XEvent *event)
478 {
479   struct state *st = (struct state *) closure;
480   if (event->xany.type == ButtonPress)
481     {
482       st->button_down_p = True;
483       return True;
484     }
485   else if (event->xany.type == ButtonRelease)
486     {
487       st->button_down_p = False;
488       return True;
489     }
490   else if (screenhack_event_helper (dpy, window, event))
491     {
492       st->value = 15;
493       st->rec.level = pow(frand(1.0), 3.0) * 2.0 + 0.05;
494       st->rec.ofs = random() % ANALOGTV_SIGNAL_LEN;
495       st->tv->color_control += frand(0.3) - 0.15;
496       return True;
497     }
498   else if (event->xany.type == KeyPress)
499     {
500       KeySym keysym;
501       char c = 0;
502       XLookupString (&event->xkey, &c, 1, &keysym, 0);
503       if (c >= '2' && c <= '8')
504         {
505           st->value = (c - '0') + (st->value - (int) st->value);
506           return True;
507         }
508     }
509 
510   return False;
511 }
512 
513 
514 static void
filmleader_free(Display * dpy,Window window,void * closure)515 filmleader_free (Display *dpy, Window window, void *closure)
516 {
517   struct state *st = (struct state *) closure;
518   analogtv_release (st->tv);
519   free (st->inp);
520   XftDrawDestroy (st->xftdraw);
521   XftColorFree(dpy, st->xgwa.visual, st->xgwa.colormap, &st->xft_text_color_1);
522   XftColorFree(dpy, st->xgwa.visual, st->xgwa.colormap, &st->xft_text_color_2);
523   XFreePixmap (dpy, st->pix);
524   XFreeGC (dpy, st->gc);
525   free (st);
526 }
527 
528 
529 static const char *filmleader_defaults [] = {
530 
531   ".background:  #000000",
532 
533 # ifdef HAVE_MOBILE
534 
535   "*textBackground: #444488",  /* Need much higher contrast for some reason */
536   "*textColor:      #000033",
537   "*ringColor:      #DDDDFF",
538   "*traceColor:     #222244",
539 
540 # else /* X11 or Cocoa */
541 
542   "*textBackground: #9999DD",
543   "*textColor:      #000015",
544   "*ringColor:      #DDDDFF",
545   "*traceColor:     #555577",
546 
547 # endif
548 
549   /* Note: these font sizes aren't relative to screen pixels, but to the
550      712 x Y or X x 712 canvas that we draw in, which is then scaled to
551      the size of the screen by analogtv. */
552 
553 # ifdef USE_IPHONE
554   "*numberFont:  Helvetica Bold 120",
555   "*numberFont2: Helvetica 36",
556   "*numberFont3: Helvetica 28",
557 
558 # elif defined(HAVE_COCOA)
559   /* Need to double these because ANALOGTV_DEFAULTS sets lowrez: true */
560   "*numberFont:  Helvetica Bold 240",
561   "*numberFont2: Helvetica 72",
562   "*numberFont3: Helvetica 56",
563 
564 # else /* X11 or Android */
565 
566   "*numberFont:  -*-helvetica-bold-r-*-*-*-1700-*-*-*-*-*-*",
567   "*numberFont2: -*-helvetica-medium-r-*-*-*-500-*-*-*-*-*-*",
568   "*numberFont3: -*-helvetica-medium-r-*-*-*-360-*-*-*-*-*-*",
569 
570 # endif
571 
572 
573   "*noise:       0.04",
574   ANALOGTV_DEFAULTS
575   "*geometry: 1280x720",
576   0
577 };
578 
579 static XrmOptionDescRec filmleader_options [] = {
580   { "-noise",           ".noise",     XrmoptionSepArg, 0 },
581   ANALOGTV_OPTIONS
582   { 0, 0, 0, 0 }
583 };
584 
585 XSCREENSAVER_MODULE ("FilmLeader", filmleader)
586