From http://www.jwz.org/xscreensaver/xscreensaver-5.40.tar.gz
[xscreensaver] / hacks / vfeedback.c
1 /* vfeedback, Copyright (c) 2018 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  * Simulates video feedback: pointing a video camera at an NTSC television.
12  *
13  * Created: 4-Aug-2018.
14  *
15  * TODO:
16  *
17  * - Figure out better UI gestures on mobile to pan, zoom and rotate.
18  *
19  * - When zoomed in really far, grab_rectangle should decompose pixels
20  *   into RGB phosphor dots.
21  *
22  * - Maybe load an image and chroma-key it, letting transparency bleed,
23  *   for that Amiga Genlock, Cabaret Voltaire look.
24  */
25
26 #ifdef HAVE_CONFIG_H
27 # include "config.h"
28 #endif /* HAVE_CONFIG_H */
29
30 #include "screenhack.h"
31 #include "analogtv.h"
32
33 #include <time.h>
34
35 #undef DEBUG
36 #undef DARKEN
37
38 #ifdef DEBUG
39 # include "ximage-loader.h"
40 # include "images/gen/testcard_bbcf_png.h"
41 #endif
42
43 #undef countof
44 #define countof(x) (sizeof((x))/sizeof((*x)))
45
46 #define RANDSIGN() ((random() & 1) ? 1 : -1)
47
48 struct state {
49   Display *dpy;
50   Window window;
51   XWindowAttributes xgwa;
52   int w, h;
53   Pixmap pix;
54   GC gc;
55   double start, last_time;
56   double noise;
57   double zoom, rot;
58   double value, svalue, speed, dx, dy, ds, dth;
59
60   struct { double x, y, w, h, th; } rect, orect;
61   struct { int x, y, s; } specular;
62
63   enum { POWERUP, IDLE, MOVE } state;
64   analogtv *tv;
65   analogtv_reception rec;
66   Bool button_down_p;
67   int mouse_x, mouse_y;
68   double mouse_th;
69   Bool dragmode;
70
71 # ifdef DEBUG
72   XImage *tcimg;
73 # endif
74
75 };
76
77
78 static void
79 twiddle_knobs (struct state *st)
80 {
81   st->rec.ofs = random() % ANALOGTV_SIGNAL_LEN;         /* roll picture once */
82   st->rec.level = 0.8 + frand(1.0);  /* signal strength (low = dark, static) */
83   st->tv->color_control    = frand(1.0) * RANDSIGN();
84   st->tv->contrast_control = 0.4 + frand(1.0);
85   st->tv->tint_control     = frand(360);
86 }
87
88
89 static void
90 twiddle_camera (struct state *st)
91 {
92 # if 0
93   st->rect.x  = 0;
94   st->rect.y  = 0;
95   st->rect.w  = 1;
96   st->rect.h  = 1;
97   st->rect.th = 0;
98 # else
99   st->rect.x = frand(0.1) * RANDSIGN();
100   st->rect.y = frand(0.1) * RANDSIGN();
101   st->rect.w = st->rect.h = 1 + frand(0.4) * RANDSIGN();
102   st->rect.th = 0.2 + frand(1.0) * RANDSIGN();
103 # endif
104 }
105
106
107 static void *
108 vfeedback_init (Display *dpy, Window window)
109 {
110   struct state *st = (struct state *) calloc (1, sizeof(*st));
111   XGCValues gcv;
112
113   st->dpy = dpy;
114   st->window = window;
115   st->tv = analogtv_allocate (st->dpy, st->window);
116   analogtv_set_defaults (st->tv, "");
117   st->tv->need_clear = 1;
118   st->rec.input = analogtv_input_allocate();
119   analogtv_setup_sync (st->rec.input, 1, 0);
120   st->tv->use_color = 1;
121   st->tv->powerup = 0;
122   st->rec.multipath = 0;
123   twiddle_camera (st);
124   twiddle_knobs (st);
125   st->noise = get_float_resource (st->dpy, "noise", "Float");
126   st->speed = get_float_resource (st->dpy, "speed", "Float");
127
128   XGetWindowAttributes (dpy, window, &st->xgwa);
129
130   st->state = POWERUP;
131   st->value = 0;
132
133   st->w = 640;
134   st->h = 480;
135   gcv.foreground = get_pixel_resource (st->dpy, st->xgwa.colormap,
136                                        "foreground", "Foreground");
137   st->gc = XCreateGC (dpy, st->window, GCForeground, &gcv);
138
139   st->orect = st->rect;
140
141 # ifdef DEBUG
142   {
143     int w, h;
144     Pixmap p;
145     p = image_data_to_pixmap (dpy, window,
146                               testcard_bbcf_png, sizeof(testcard_bbcf_png),
147                               &w, &h, 0);
148     st->tcimg = XGetImage (dpy, p, 0, 0, w, h, ~0L, ZPixmap);
149     XFreePixmap (dpy, p);
150   }
151 # endif
152
153 # ifndef HAVE_JWXYZ
154   XSelectInput (dpy, window,
155                 PointerMotionMask | st->xgwa.your_event_mask);
156 # endif
157
158   return st;
159 }
160
161
162 static double
163 ease_fn (double r)
164 {
165   return cos ((r/2 + 1) * M_PI) + 1; /* Smooth curve up, end at slope 1. */
166 }
167
168
169 static double
170 ease_ratio (double r)
171 {
172   double ease = 0.5;
173   if      (r <= 0)     return 0;
174   else if (r >= 1)     return 1;
175   else if (r <= ease)  return     ease * ease_fn (r / ease);
176   else if (r > 1-ease) return 1 - ease * ease_fn ((1 - r) / ease);
177   else                 return r;
178 }
179
180
181 static XImage *
182 grab_rectangle (struct state *st)
183 {
184   XImage *in, *out;
185
186   /* Under XQuartz we can't just do XGetImage on the Window, we have to
187      go through an intermediate Pixmap first.  I don't understand why.
188    */
189   if (! st->pix)
190     st->pix = XCreatePixmap (st->dpy, st->window, 
191                              st->xgwa.width, st->xgwa.height, st->xgwa.depth);
192
193   XCopyArea (st->dpy, st->window, st->pix, st->gc, 0, 0,
194              st->xgwa.width, st->xgwa.height, 0, 0);
195
196   if (st->specular.s)
197     {
198       double p = 0.2;
199       double r = (st->svalue <    p ? st->svalue/p :
200                   st->svalue >= 1-p ? (1-st->svalue)/p :
201                   1);
202       double s = st->specular.s * ease_ratio (r * 2);
203       XFillArc (st->dpy, st->pix, st->gc,
204                 st->specular.x - s/2,
205                 st->specular.y - s/2,
206                 s, s, 0, 360*64);
207     }
208
209 # ifdef DEBUG
210   in = st->tcimg;
211 # else
212   in = XGetImage (st->dpy, st->pix,
213                   0, 0, st->xgwa.width, st->xgwa.height,
214                   ~0L, ZPixmap);
215   /* Could actually use st->tv->image here, except we don't have the
216      subrectangle being used (overall_top, usewidth, etc.) */
217 # endif
218
219   out = XCreateImage (st->dpy, st->xgwa.visual, st->xgwa.depth,
220                       ZPixmap, 0, NULL,
221                       st->w, st->h, 8, 0);
222
223   if (! in) abort();
224   if (! out) abort();
225   out->data = (char *) calloc (out->height, out->bytes_per_line);
226   if (! out->data) abort();
227
228   {
229     double C = cos (st->rect.th);
230     double S = sin (st->rect.th);
231     unsigned long black = BlackPixelOfScreen (st->xgwa.screen);
232     int ox, oy;
233     for (oy = 0; oy < out->height; oy++)
234       {
235         double doy = (double) oy / out->height;
236         double diy = st->rect.h * doy + st->rect.y - 0.5;
237
238         float dix_mul = (float) st->rect.w / out->width;
239         float dix_add = (-0.5 + st->rect.x) * st->rect.w;
240         float ix_add = (-diy * S + 0.5) * in->width;
241         float iy_add = ( diy * C + 0.5) * in->height;
242         float ix_mul = C * in->width;
243         float iy_mul = S * in->height;
244
245         ix_add += dix_add * ix_mul;
246         iy_add += dix_add * iy_mul;
247         ix_mul *= dix_mul;
248         iy_mul *= dix_mul;
249
250         if (in->bits_per_pixel == 32 &&
251             out->bits_per_pixel == 32)
252           {
253             /* Unwrapping XGetPixel and XPutPixel gains us several FPS here */
254             uint32_t *out_line =
255               (uint32_t *) (out->data + out->bytes_per_line * oy);
256             for (ox = 0; ox < out->width; ox++)
257               {
258                 float dix = ox;
259                 int ix = dix * ix_mul + ix_add;
260                 int iy = dix * iy_mul + iy_add;
261                 unsigned long p = (ix >= 0 && ix < in->width &&
262                                    iy >= 0 && iy < in->height
263                                    ? ((uint32_t *)
264                                       (in->data + in->bytes_per_line * iy))[ix]
265                                    : black);
266 # ifdef HAVE_JWXYZ
267                 p |= black;   /* We get 0 instead of BlackPixel... */
268 # endif
269                 out_line[ox] = p;
270               }
271           }
272         else
273           for (ox = 0; ox < out->width; ox++)
274             {
275               float dix = ox;
276               int ix = dix * ix_mul + ix_add;
277               int iy = dix * iy_mul + iy_add;
278               unsigned long p = (ix >= 0 && ix < in->width &&
279                                  iy >= 0 && iy < in->height
280                                  ? XGetPixel (in, ix, iy)
281                                  : black);
282 # ifdef HAVE_JWXYZ
283               p |= black;   /* We get 0 instead of BlackPixel... */
284 # endif
285               XPutPixel (out, ox, oy, p);
286             }
287       }
288   }
289
290 # ifndef DEBUG
291   XDestroyImage (in);
292 # endif
293
294   return out;
295 }
296
297
298 static double
299 double_time (void)
300 {
301   struct timeval now;
302 # ifdef GETTIMEOFDAY_TWO_ARGS
303   struct timezone tzp;
304   gettimeofday(&now, &tzp);
305 # else
306   gettimeofday(&now);
307 # endif
308
309   return (now.tv_sec + ((double) now.tv_usec * 0.000001));
310 }
311
312
313 static unsigned long
314 vfeedback_draw (Display *dpy, Window window, void *closure)
315 {
316   struct state *st = (struct state *) closure;
317   const analogtv_reception *rec = &st->rec;
318   double then = double_time(), now, timedelta;
319   XImage *img = 0;
320
321   switch (st->state) {
322   case POWERUP: case IDLE: break;
323   case MOVE:
324     st->rect.x  = st->orect.x  + st->dx  * ease_ratio (st->value);
325     st->rect.y  = st->orect.y  + st->dy  * ease_ratio (st->value);
326     st->rect.th = st->orect.th + st->dth * ease_ratio (st->value);
327     st->rect.w  = st->orect.w * (1 + (st->ds * ease_ratio (st->value)));
328     st->rect.h  = st->orect.h * (1 + (st->ds * ease_ratio (st->value)));
329     break;
330   default:
331     abort();
332     break;
333   }
334
335   if (! st->button_down_p)
336     {
337       st->value  += 0.03 * st->speed;
338       if (st->value > 1 || st->state == POWERUP)
339         {
340           st->orect = st->rect;
341           st->value = 0;
342           st->dx = st->dy = st->ds = st->dth = 0;
343
344           switch (st->state) {
345           case POWERUP:
346             /* Wait until the monitor has warmed up before turning on
347                the camcorder? */
348             /* if (st->tv->powerup > 4.0) */
349               st->state = IDLE;
350             break;
351           case IDLE:
352             st->state = MOVE;
353             if (! (random() % 5))
354               st->ds = frand(0.2) * RANDSIGN();         /* zoom */
355             if (! (random() % 3))
356               st->dth = frand(0.2) * RANDSIGN();        /* rotate */
357             if (! (random() % 8))
358               st->dx = frand(0.05) * RANDSIGN(),        /* pan */
359               st->dy = frand(0.05) * RANDSIGN();
360             if (! (random() % 2000))
361               {
362                 twiddle_knobs (st);
363                 if (! (random() % 10))
364                   twiddle_camera (st);
365               }
366             break;
367           case MOVE:
368             st->state = IDLE;
369             st->value = 0.3;
370             break;
371           default:
372             abort();
373             break;
374           }
375         }
376
377       /* Draw a specular reflection somewhere on the screen, to mix it up
378          with a little noise from environmental light.
379        */
380       if (st->specular.s)
381         {
382           st->svalue += 0.01 * st->speed;
383           if (st->svalue > 1)
384             {
385               st->svalue = 0;
386               st->specular.s = 0;
387             }
388         }
389       else if (! (random() % 300))
390         {
391 # if 1
392           /* Center on the monitor's screen, depth 1 */
393           int cx = st->xgwa.width / 2;
394           int cy = st->xgwa.height / 2;
395 # else
396           /* Center on the monitor's screen, depth 0 -- but this clips. */
397           int cx = (st->rect.x + st->rect.w / 2) * st->xgwa.width;
398           int cy = (st->rect.y + st->rect.h / 2) * st->xgwa.height;
399 # endif
400           int ww = 4 + (st->rect.h * st->xgwa.height) / 12;
401           st->specular.x = cx + (random() % ww) * RANDSIGN();
402           st->specular.y = cy + (random() % ww) * RANDSIGN();
403           st->specular.s = ww * (0.8 + frand(0.4));
404           st->svalue = 0;
405         }
406     }
407
408   if (st->last_time == 0)
409     st->start = then;
410
411   if (st->state != POWERUP)
412     {
413       img = grab_rectangle (st);
414       analogtv_load_ximage (st->tv, st->rec.input, img, 0, 0, 0, 0, 0);
415     }
416
417   analogtv_reception_update (&st->rec);
418   analogtv_draw (st->tv, st->noise, &rec, 1);
419   if (img)
420     XDestroyImage (img);
421
422   now = double_time();
423   timedelta = (1 / 29.97) - (now - then);
424
425   st->tv->powerup = then - st->start;
426   st->last_time = then;
427
428   return timedelta > 0 ? timedelta * 1000000 : 0;
429 }
430
431
432 static void
433 vfeedback_reshape (Display *dpy, Window window, void *closure, 
434                     unsigned int w, unsigned int h)
435 {
436   struct state *st = (struct state *) closure;
437   analogtv_reconfigure (st->tv);
438   XGetWindowAttributes (dpy, window, &st->xgwa);
439
440   if (st->pix)
441     {
442       XFreePixmap (dpy, st->pix);
443       st->pix = 0;
444     }
445 }
446
447
448 static Bool
449 vfeedback_event (Display *dpy, Window window, void *closure, XEvent *event)
450 {
451   struct state *st = (struct state *) closure;
452   double i = 0.02;
453
454   /* Pan with left button and no modifier keys.
455      Rotate with other buttons, or left-with-modifiers.
456    */
457   if (event->xany.type == ButtonPress &&
458       (event->xbutton.button == Button1 ||
459        event->xbutton.button == Button2 ||
460        event->xbutton.button == Button3))
461     {
462       st->button_down_p = True;
463       st->mouse_x = event->xbutton.x;
464       st->mouse_y = event->xbutton.y;
465       st->mouse_th = st->rect.th;
466       st->dragmode = (event->xbutton.button == Button1 && 
467                       !event->xbutton.state);
468       return True;
469     }
470   else if (event->xany.type == ButtonRelease &&
471            (event->xbutton.button == Button1 ||
472             event->xbutton.button == Button2 ||
473             event->xbutton.button == Button3))
474     {
475       st->button_down_p = False;
476       return True;
477     }
478   else if (event->xany.type == MotionNotify && st->button_down_p)
479     {
480       if (st->dragmode)
481         {
482           double dx = st->mouse_x - event->xmotion.x;
483           double dy = st->mouse_y - event->xmotion.y;
484           st->rect.x += dx / st->xgwa.width  * st->rect.w;
485           st->rect.y += dy / st->xgwa.height * st->rect.h;
486           st->mouse_x = event->xmotion.x;
487           st->mouse_y = event->xmotion.y;
488         }
489       else
490         {
491           /* Angle between center and initial click */
492           double a1 = -atan2 (st->mouse_y - st->xgwa.height / 2,
493                               st->mouse_x - st->xgwa.width  / 2);
494           /* Angle between center and drag position */
495           double a2 = -atan2 (event->xmotion.y - st->xgwa.height / 2,
496                               event->xmotion.x - st->xgwa.width  / 2);
497           /* Cumulatively rotate by difference between them */
498           st->rect.th = a2 - a1 + st->mouse_th;
499         }
500       goto OK;
501     }
502
503   /* Zoom with mouse wheel */
504
505   else if (event->xany.type == ButtonPress &&
506            (event->xbutton.button == Button4 ||
507             event->xbutton.button == Button6))
508     {
509       i = 1-i;
510       goto ZZ;
511     }
512   else if (event->xany.type == ButtonPress &&
513            (event->xbutton.button == Button5 ||
514             event->xbutton.button == Button7))
515     {
516       i = 1+i;
517       goto ZZ;
518     }
519   else if (event->type == KeyPress)
520     {
521       KeySym keysym;
522       char c = 0;
523       XLookupString (&event->xkey, &c, 1, &keysym, 0);
524       switch (keysym) {
525         /* pan with arrow keys */
526       case XK_Up:    st->rect.y += i; goto OK; break;
527       case XK_Down:  st->rect.y -= i; goto OK; break;
528       case XK_Left:  st->rect.x += i; goto OK; break;
529       case XK_Right: st->rect.x -= i; goto OK; break;
530       default: break;
531       }
532       switch (c) {
533
534         /* rotate with <> */
535       case '<': case ',': st->rect.th += i; goto OK; break;
536       case '>': case '.': st->rect.th -= i; goto OK; break;
537
538         /* zoom with += */
539       case '-': case '_':
540         i = 1+i;
541         goto ZZ;
542       case '=': case '+':
543         i = 1-i;
544       ZZ:
545         st->orect = st->rect;
546         st->rect.w *= i;
547         st->rect.h *= i;
548         st->rect.x += (st->orect.w - st->rect.w) / 2;
549         st->rect.y += (st->orect.h - st->rect.h) / 2;
550         goto OK;
551         break;
552
553         /* tv controls with T, C, B, O */
554       case 't': st->tv->tint_control       += 5;    goto OK; break;
555       case 'T': st->tv->tint_control       -= 5;    goto OK; break;
556       case 'c': st->tv->color_control      += 0.1;  goto OK; break;
557       case 'C': st->tv->color_control      -= 0.1;  goto OK; break;
558       case 'b': st->tv->brightness_control += 0.01; goto OK; break;
559       case 'B': st->tv->brightness_control -= 0.01; goto OK; break;
560       case 'o': st->tv->contrast_control   += 0.1;  goto OK; break;
561       case 'O': st->tv->contrast_control   -= 0.1;  goto OK; break;
562       case 'r': st->rec.level              += 0.01; goto OK; break;
563       case 'R': st->rec.level              -= 0.01; goto OK; break;
564       default: break;
565       }
566       goto NOPE;
567     OK:
568 # if 0
569       fprintf (stderr, " %.6f x %.6f @ %.6f, %.6f %.6f\t",
570                st->rect.w, st->rect.h,
571                st->rect.x, st->rect.y, st->rect.th);
572       fprintf (stderr," T=%.2f C=%.2f B=%.2f O=%.2f R=%.3f\n",
573                st->tv->tint_control,
574                st->tv->color_control/* * 100*/,
575                st->tv->brightness_control/* * 100*/,
576                st->tv->contrast_control/* * 100*/,
577                st->rec.level);
578 # endif
579       st->value = 0;
580       st->state = IDLE;
581       st->orect = st->rect;
582       return True;
583     }
584
585  NOPE:
586   if (screenhack_event_helper (dpy, window, event))
587     {
588       /* SPC or RET re-randomize the TV controls. */
589       twiddle_knobs (st);
590       goto OK;
591     }
592
593   return False;
594 }
595
596
597 static void
598 vfeedback_free (Display *dpy, Window window, void *closure)
599 {
600   struct state *st = (struct state *) closure;
601   analogtv_release (st->tv);
602   if (st->pix)
603     XFreePixmap (dpy, st->pix);
604   XFreeGC (dpy, st->gc);
605   free (st);
606 }
607
608
609 static const char *vfeedback_defaults [] = {
610
611   ".foreground:  #CCCC44",
612   ".background:  #000000",
613   "*noise:       0.02",
614   "*speed:       1.0",
615   ANALOGTV_DEFAULTS
616   "*TVBrightness: 1.5",
617   "*TVContrast:   150",
618   0
619 };
620
621 static XrmOptionDescRec vfeedback_options [] = {
622   { "-noise",           ".noise",     XrmoptionSepArg, 0 },
623   { "-speed",           ".speed",     XrmoptionSepArg, 0 },
624   ANALOGTV_OPTIONS
625   { 0, 0, 0, 0 }
626 };
627
628 XSCREENSAVER_MODULE ("VFeedback", vfeedback)