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