ref: 02ac617541ca1a7bf82b1615fb5a58235469b5d3
dir: /appl/wm/bounce.b/
implement Bounce;
# bouncing balls demo. it uses tk and multiple processes to animate a
# number of balls bouncing around the screen. each ball has its own
# process; CPU time is doled out fairly to each process by using
# a central monitor loop.
include "sys.m";
sys: Sys;
include "draw.m";
draw: Draw;
Point, Rect: import draw;
include "tk.m";
tk: Tk;
include "tkclient.m";
tkclient: Tkclient;
include "math.m";
math: Math;
include "rand.m";
Bounce: module {
init: fn(ctxt: ref Draw->Context, argv: list of string);
};
BALLSIZE: con 4;
ZERO: con 1e-6;
π: con Math->Pi;
Line: adt {
p1, p2: Point;
};
Realpoint: adt {
x, y: real;
};
gamecmds := array[] of {
"canvas .c",
"bind .c <ButtonRelease-1> {send cmd 0 %x %y}",
"bind .c <ButtonRelease-2> {send cmd 0 %x %y}",
"bind .c <Button-1> {send cmd 1 %x %y}",
"bind .c <Button-2> {send cmd 2 %x %y}",
"frame .f",
"button .f.left -bitmap small_color_left.bit -bd 0 -command {send cmd k -1}",
"button .f.right -bitmap small_color_right.bit -bd 0 -command {send cmd k 1}",
"label .f.l -text {8 balls}",
"pack .f.left .f.right -side left",
"pack .f.l -side left",
"pack .f -fill x",
"pack .c -fill both -expand 1",
};
randch: chan of int;
lines: list of (int, Line);
lineid := 0;
lineversion := 0;
addline(win: ref Tk->Toplevel, v: Line)
{
lines = (++lineid, v) :: lines;
cmd(win, ".c create line " + pt2s(v.p1) + " " + pt2s(v.p2) + " -width 3 -fill black" +
" -tags l" + string lineid);
lineversion++;
}
nomod(s: string)
{
sys->fprint(sys->fildes(2), "bounce: cannot load %s: %r\n", s);
raise "fail:bad module";
}
init(ctxt: ref Draw->Context, argv: list of string)
{
sys = load Sys Sys->PATH;
draw = load Draw Draw->PATH;
math = load Math Math->PATH;
tk = load Tk Tk->PATH;
tkclient = load Tkclient Tkclient->PATH;
if (tkclient == nil)
nomod(Tkclient->PATH);
tkclient->init();
nballs := 8;
if (argv != nil && tl argv != nil)
nballs = int hd tl argv;
if (nballs < 0) {
sys->fprint(sys->fildes(2), "usage: bounce [nballs]\n");
raise "fail:usage";
}
sys->pctl(Sys->NEWPGRP, nil);
if(ctxt == nil)
ctxt = tkclient->makedrawcontext();
(win, wmctl) := tkclient->toplevel(ctxt, nil, "Bounce", 0);
cmdch := chan of string;
tk->namechan(win, cmdch, "cmd");
for (i := 0; i < len gamecmds; i++)
cmd(win, gamecmds[i]);
cmd(win, ".c configure -width 400 -height 400");
cmd(win, "pack propagate . 0");
cmd(win, ".f.l configure -text '" + string nballs + " balls");
tkclient->onscreen(win, nil);
tkclient->startinput(win, "kbd"::"ptr"::nil);
mch := chan of (int, Point);
randch = chan of int;
spawn randgenproc(randch);
csz := Point(int cmd(win, ".c cget -actwidth"), int cmd(win, ".c cget -actheight"));
# add edges of window
addline(win, ((-1, -1), (csz.x, -1)));
addline(win, ((csz.x, -1), csz));
addline(win, (csz, (-1, csz.y)));
addline(win, ((-1, csz.y), (-1, -1)));
spawn makelinesproc(win, mch);
mkball := chan of (int, Realpoint, Realpoint);
spawn monitor(win, mkball);
for (i = 0; i < nballs; i++)
mkball <-= (1, randpoint(csz), makeunit(randpoint(csz)));
for (;;) alt {
s := <-win.ctxt.kbd =>
tk->keyboard(win, s);
s := <-win.ctxt.ptr =>
tk->pointer(win, *s);
s := <-win.ctxt.ctl or
s = <-win.wreq or
s = <-wmctl =>
tkclient->wmctl(win, s);
c := <-cmdch =>
(nil, toks) := sys->tokenize(c, " ");
if (hd toks != "k") {
mch <-= (int hd toks, Point(int hd tl toks, int hd tl tl toks));
continue;
}
n := nballs + int hd tl toks;
if (n < 0)
n = 0;
dn := 1;
if (n < nballs)
dn = -1;
for (; nballs != n; nballs += dn)
mkball <-= (dn, randpoint(csz), makeunit(randpoint(csz)));
cmd(win, ".f.l configure -text '" + string nballs + " balls");
cmd(win, "update");
}
}
randpoint(size: Point): Realpoint
{
return (randreal(size.x), randreal(size.y));
}
# return randomish real number between 1 and x-1
randreal(x: int): real
{
return real (<-randch % ((x - 1) * 100)) / 100.0 + 1.0;
}
# make sure cpu time is handed to all ball processes fairly
# by passing a "token" around to each process in turn.
# each process does its work when it *hasn't* got its
# token but it can't go through two iterations without
# waiting its turn.
#
# new processes can be created and destroyed by
# sending on mkball. processes are arranged in a stack-like
# order: new processes are added to the top of the stack, and
# processes are destroyed from the top of the stack downwards.
monitor(win: ref Tk->Toplevel, mkball: chan of (int, Realpoint, Realpoint))
{
procl := proc := chan of int :: nil;
spawn nullproc(hd proc); # always there to avoid deadlock when no balls.
hd proc <-= 1; # hand token to dummy proc
for (;;) {
procc := hd proc;
alt {
(n, p, v) := <-mkball =>
if (n > 0) { # start new ball proc going.
procl = chan of int :: procl;
spawn animproc(hd procl, win, p, v);
} else if (tl procl != nil) { # stop a ball proc.
<-hd proc; # get token.
hd procl <-= 0; # stop proc.
proc = procl = tl procl; # remove proc.
hd proc <-= 1; # hand out token.
}
<-procc => # got token.
if ((proc = tl proc) == nil)
proc = procl;
hd proc <-= 1; # hand token to next process.
}
}
}
nullproc(c: chan of int)
{
for (;;)
c <-= <-c;
}
# animate one ball. initial position and unit-velocity are
# given by p and v.
animproc(c: chan of int, win: ref Tk->Toplevel, p, v: Realpoint)
{
speed := 0.1 + real (<-randch % 40) / 100.0;
ballid := cmd(win, sys->sprint(".c create oval 0 0 1 1 -fill #%.6x", <-randch & 16rffffff));
hitlineid := -1;
smallcount := 0;
version := lineversion;
loop: for (;;) {
hitline: Line;
hitp: Realpoint;
dist := 1000000.0;
oldid := hitlineid;
for (l := lines; l != nil; l = tl l) {
(id, line) := hd l;
(ok, hp, hdist) := intersect(p, v, line);
if (ok && hdist < dist && id != oldid && (smallcount < 10 || hdist > 1.5)) {
(hitp, hitline, hitlineid, dist) = (hp, line, id, hdist);
}
}
if (dist > 10000.0) {
sys->print("no intersection!\n");
# sys->print("p: [%f, %f], v: [%f, %f]\n", p.x, p.y, v.x, v.y);
# for (l := lines; l != nil; l = tl l) {
# (id, line) := hd l;
# (ok, hp, hdist) := intersect(p, v, line);
# sys->print("line: [%d %d]->[%d %d] -> %d, [%f, %f], %f\n", line.p1.x, line.p1.y, line.p2.x, line.p2.y,
# ok, hp.x, hp.y, hdist);
# }
cmd(win, ".c delete " + ballid + ";update");
while (c <-= <-c)
;
exit;
}
if (dist < 0.0001)
smallcount++;
else
smallcount = 0;
bouncev := boing(v, hitline);
t0 := sys->millisec();
dt := int (dist / speed);
t := 0;
do {
s := real t * speed;
currp := Realpoint(p.x + s * v.x, p.y + s * v.y);
bp := Point(int currp.x, int currp.y);
cmd(win, ".c coords " + ballid + " " +
string (bp.x-BALLSIZE)+" "+string (bp.y-BALLSIZE)+" "+
string (bp.x+BALLSIZE)+" "+string (bp.y+BALLSIZE));
cmd(win, "update");
if (lineversion > version) {
(p, hitlineid, version) = (currp, oldid, lineversion);
continue loop;
}
# pass the token back to the monitor.
if (<-c == 0) {
cmd(win, ".c delete " + ballid + ";update");
exit;
}
c <-= 1;
t = sys->millisec() - t0;
} while (t < dt);
p = hitp;
v = bouncev;
}
}
# thread-safe access to the Rand module
randgenproc(ch: chan of int)
{
rand := load Rand Rand->PATH;
for (;;)
ch <-= rand->rand(16r7fffffff);
}
makelinesproc(win: ref Tk->Toplevel, mch: chan of (int, Point))
{
for (;;) {
(down, p1) := <-mch;
addline(win, (p1, p1));
(id, nil) := hd lines;
p2 := p1;
do {
(down, p2) = <-mch;
cmd(win, ".c coords l" + string id + " " + pt2s(p1) + " " + pt2s(p2));
cmd(win, "update");
lines = (id, (p1, p2)) :: tl lines;
lineversion++;
if (down > 1) {
dp := p2.sub(p1);
if (dp.x*dp.x + dp.y*dp.y > 5) {
p1 = p2;
addline(win, (p2, p2));
(id, nil) = hd lines;
}
}
} while (down);
}
}
# make a vector of unit-length, parallel to v.
makeunit(v: Realpoint): Realpoint
{
mag := math->sqrt(v.x * v.x + v.y * v.y);
return (v.x / mag, v.y / mag);
}
# bounce ball travelling in direction av off line b.
# return the new unit vector.
boing(av: Realpoint, b: Line): Realpoint
{
f := b.p2.sub(b.p1);
d := math->atan2(real f.y, real f.x) * 2.0 - math->atan2(av.y, av.x);
return (math->cos(d), math->sin(d));
}
# compute the intersection of lines a and b.
# b is assumed to be fixed, and a is indefinitely long
# but doesn't extend backwards from its starting point.
# a is defined by the starting point p and the unit vector v.
intersect(p, v: Realpoint, b: Line): (int, Realpoint, real)
{
w := Realpoint(real (b.p2.x - b.p1.x), real (b.p2.y - b.p1.y));
det := w.x * v.y - v.x * w.y;
if (det > -ZERO && det < ZERO)
return (0, (0.0, 0.0), 0.0);
y21 := real b.p1.y - p.y;
x21 := real b.p1.x - p.x;
s := (w.x * y21 - w.y * x21) / det;
if (s < 0.0)
return (0, (0.0, 0.0), 0.0);
hp := Realpoint(p.x+v.x*s, p.y+v.y*s);
if (b.p1.x > b.p2.x)
(b.p1.x, b.p2.x) = (b.p2.x, b.p1.x);
if (b.p1.y > b.p2.y)
(b.p1.y, b.p2.y) = (b.p2.y, b.p1.y);
return (int hp.x >= b.p1.x && int hp.x <= b.p2.x
&& int hp.y >= b.p1.y && int hp.y <= int b.p2.y, hp, s);
}
cmd(top: ref Tk->Toplevel, s: string): string
{
e := tk->cmd(top, s);
if (e != nil && e[0] == '!')
sys->print("tk error %s on '%s'\n", e, s);
return e;
}
pt2s(p: Point): string
{
return string p.x + " " + string p.y;
}