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