1 /* vfeedback, Copyright (c) 2018 Jamie Zawinski <jwz@jwz.org>
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
11 * Simulates video feedback: pointing a video camera at an NTSC television.
13 * Created: 4-Aug-2018.
17 * - Figure out better UI gestures on mobile to pan, zoom and rotate.
19 * - When zoomed in really far, grab_rectangle should decompose pixels
20 * into RGB phosphor dots.
22 * - Maybe load an image and chroma-key it, letting transparency bleed,
23 * for that Amiga Genlock, Cabaret Voltaire look.
28 #endif /* HAVE_CONFIG_H */
30 #include "screenhack.h"
39 # include "ximage-loader.h"
40 # include "images/gen/testcard_bbcf_png.h"
44 #define countof(x) (sizeof((x))/sizeof((*x)))
46 #define RANDSIGN() ((random() & 1) ? 1 : -1)
51 XWindowAttributes xgwa;
55 double start, last_time;
58 double value, svalue, speed, dx, dy, ds, dth;
60 struct { double x, y, w, h, th; } rect, orect;
61 struct { int x, y, s; } specular;
63 enum { POWERUP, IDLE, MOVE } state;
65 analogtv_reception rec;
79 twiddle_knobs (struct state *st)
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);
90 twiddle_camera (struct state *st)
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();
108 vfeedback_init (Display *dpy, Window window)
110 struct state *st = (struct state *) calloc (1, sizeof(*st));
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;
122 st->rec.multipath = 0;
125 st->noise = get_float_resource (st->dpy, "noise", "Float");
126 st->speed = get_float_resource (st->dpy, "speed", "Float");
128 XGetWindowAttributes (dpy, window, &st->xgwa);
135 gcv.foreground = get_pixel_resource (st->dpy, st->xgwa.colormap,
136 "foreground", "Foreground");
137 st->gc = XCreateGC (dpy, st->window, GCForeground, &gcv);
139 st->orect = st->rect;
145 p = image_data_to_pixmap (dpy, window,
146 testcard_bbcf_png, sizeof(testcard_bbcf_png),
148 st->tcimg = XGetImage (dpy, p, 0, 0, w, h, ~0L, ZPixmap);
149 XFreePixmap (dpy, p);
154 XSelectInput (dpy, window,
155 PointerMotionMask | st->xgwa.your_event_mask);
165 return cos ((r/2 + 1) * M_PI) + 1; /* Smooth curve up, end at slope 1. */
170 ease_ratio (double r)
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);
182 grab_rectangle (struct state *st)
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.
190 st->pix = XCreatePixmap (st->dpy, st->window,
191 st->xgwa.width, st->xgwa.height, st->xgwa.depth);
193 XCopyArea (st->dpy, st->window, st->pix, st->gc, 0, 0,
194 st->xgwa.width, st->xgwa.height, 0, 0);
199 double r = (st->svalue < p ? st->svalue/p :
200 st->svalue >= 1-p ? (1-st->svalue)/p :
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,
212 in = XGetImage (st->dpy, st->pix,
213 0, 0, st->xgwa.width, st->xgwa.height,
215 /* Could actually use st->tv->image here, except we don't have the
216 subrectangle being used (overall_top, usewidth, etc.) */
219 out = XCreateImage (st->dpy, st->xgwa.visual, st->xgwa.depth,
225 out->data = (char *) calloc (out->height, out->bytes_per_line);
226 if (! out->data) abort();
229 double C = cos (st->rect.th);
230 double S = sin (st->rect.th);
231 unsigned long black = BlackPixelOfScreen (st->xgwa.screen);
233 for (oy = 0; oy < out->height; oy++)
235 double doy = (double) oy / out->height;
236 double diy = st->rect.h * doy + st->rect.y - 0.5;
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;
245 ix_add += dix_add * ix_mul;
246 iy_add += dix_add * iy_mul;
250 if (in->bits_per_pixel == 32 &&
251 out->bits_per_pixel == 32)
253 /* Unwrapping XGetPixel and XPutPixel gains us several FPS here */
255 (uint32_t *) (out->data + out->bytes_per_line * oy);
256 for (ox = 0; ox < out->width; 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
264 (in->data + in->bytes_per_line * iy))[ix]
267 p |= black; /* We get 0 instead of BlackPixel... */
273 for (ox = 0; ox < out->width; 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)
283 p |= black; /* We get 0 instead of BlackPixel... */
285 XPutPixel (out, ox, oy, p);
302 # ifdef GETTIMEOFDAY_TWO_ARGS
304 gettimeofday(&now, &tzp);
309 return (now.tv_sec + ((double) now.tv_usec * 0.000001));
314 vfeedback_draw (Display *dpy, Window window, void *closure)
316 struct state *st = (struct state *) closure;
317 const analogtv_reception *rec = &st->rec;
318 double then = double_time(), now, timedelta;
322 case POWERUP: case IDLE: break;
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)));
335 if (! st->button_down_p)
337 st->value += 0.03 * st->speed;
338 if (st->value > 1 || st->state == POWERUP)
340 st->orect = st->rect;
342 st->dx = st->dy = st->ds = st->dth = 0;
346 /* Wait until the monitor has warmed up before turning on
348 /* if (st->tv->powerup > 4.0) */
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))
363 if (! (random() % 10))
377 /* Draw a specular reflection somewhere on the screen, to mix it up
378 with a little noise from environmental light.
382 st->svalue += 0.01 * st->speed;
389 else if (! (random() % 300))
392 /* Center on the monitor's screen, depth 1 */
393 int cx = st->xgwa.width / 2;
394 int cy = st->xgwa.height / 2;
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;
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));
408 if (st->last_time == 0)
411 if (st->state != POWERUP)
413 img = grab_rectangle (st);
414 analogtv_load_ximage (st->tv, st->rec.input, img, 0, 0, 0, 0, 0);
417 analogtv_reception_update (&st->rec);
418 analogtv_draw (st->tv, st->noise, &rec, 1);
423 timedelta = (1 / 29.97) - (now - then);
425 st->tv->powerup = then - st->start;
426 st->last_time = then;
428 return timedelta > 0 ? timedelta * 1000000 : 0;
433 vfeedback_reshape (Display *dpy, Window window, void *closure,
434 unsigned int w, unsigned int h)
436 struct state *st = (struct state *) closure;
437 analogtv_reconfigure (st->tv);
438 XGetWindowAttributes (dpy, window, &st->xgwa);
442 XFreePixmap (dpy, st->pix);
449 vfeedback_event (Display *dpy, Window window, void *closure, XEvent *event)
451 struct state *st = (struct state *) closure;
454 /* Pan with left button and no modifier keys.
455 Rotate with other buttons, or left-with-modifiers.
457 if (event->xany.type == ButtonPress &&
458 (event->xbutton.button == Button1 ||
459 event->xbutton.button == Button2 ||
460 event->xbutton.button == Button3))
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);
470 else if (event->xany.type == ButtonRelease &&
471 (event->xbutton.button == Button1 ||
472 event->xbutton.button == Button2 ||
473 event->xbutton.button == Button3))
475 st->button_down_p = False;
478 else if (event->xany.type == MotionNotify && st->button_down_p)
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;
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;
503 /* Zoom with mouse wheel */
505 else if (event->xany.type == ButtonPress &&
506 (event->xbutton.button == Button4 ||
507 event->xbutton.button == Button6))
512 else if (event->xany.type == ButtonPress &&
513 (event->xbutton.button == Button5 ||
514 event->xbutton.button == Button7))
519 else if (event->type == KeyPress)
523 XLookupString (&event->xkey, &c, 1, &keysym, 0);
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;
535 case '<': case ',': st->rect.th += i; goto OK; break;
536 case '>': case '.': st->rect.th -= i; goto OK; break;
545 st->orect = st->rect;
548 st->rect.x += (st->orect.w - st->rect.w) / 2;
549 st->rect.y += (st->orect.h - st->rect.h) / 2;
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;
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*/,
581 st->orect = st->rect;
586 if (screenhack_event_helper (dpy, window, event))
588 /* SPC or RET re-randomize the TV controls. */
598 vfeedback_free (Display *dpy, Window window, void *closure)
600 struct state *st = (struct state *) closure;
601 analogtv_release (st->tv);
603 XFreePixmap (dpy, st->pix);
604 XFreeGC (dpy, st->gc);
609 static const char *vfeedback_defaults [] = {
611 ".foreground: #CCCC44",
612 ".background: #000000",
616 "*TVBrightness: 1.5",
621 static XrmOptionDescRec vfeedback_options [] = {
622 { "-noise", ".noise", XrmoptionSepArg, 0 },
623 { "-speed", ".speed", XrmoptionSepArg, 0 },
628 XSCREENSAVER_MODULE ("VFeedback", vfeedback)