Commit 8b0be44e authored by Sam Rushing's avatar Sam Rushing

scrolling field demo + iOS touch events + quadtree

parent 216926d0
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title></title>
</head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<body>
<canvas id="canvas" width="1024" height="1024"></canvas>
<div id="msgs"></div>
<script src="field.js"></script>
</body>
</html>
var connection;
var msgs_div = document.getElementById ('msgs')
function message (msg) {
msgs_div.innerHTML = msg;
}
if (window["WebSocket"]) {
connection = new WebSocket("ws://127.0.0.1:9001/field")
connection.onopen = function () {
message ('connected')
};
connection.onclose = function (event) {
message ('disconnected')
}
connection.onmessage = function (event) {
//message ('websocket event: ' + event.data)
switch (event.data[0]) {
case 'F':
// draw field
var elems = event.data.split ('|')
context.clearRect (0, 0, 1024, 1024);
context.fillStyle = 'rgb(0,0,0,128)';
for (i=0; i < elems.length; i++) {
var elem = elems[i];
var p = elem.split (',')
switch (p[0]) {
case 'B':
context.fillStyle = p[1];
context.fillRect (parseInt (p[2]), parseInt (p[3]), parseInt (p[4]), parseInt (p[5]));
break;
}
}
break;
case 'C':
context.clearRect (0, 0, 1024, 1024);
break;
case 'M':
message (event.data);
break;
}
}
}
var canvas = document.getElementById ('canvas')
var context = canvas.getContext ('2d')
document.addEventListener ('mousedown', on_mouse_down, false)
document.addEventListener ('mouseup', on_mouse_up, false)
document.addEventListener ('mousemove', on_mouse_move, false)
document.addEventListener ('keydown', on_key_down, false)
document.addEventListener ('keyup', on_key_up, false)
document.addEventListener ('touchstart', on_touch_start, false)
document.addEventListener ('touchmove', on_touch_move, false)
document.addEventListener ('touchend', on_touch_end, false)
function on_mouse_down (event) {
connection.send ('MD,' + event.clientX + ',' + event.clientY)
}
function on_mouse_up (event) {
connection.send ('MU,' + event.clientX + ',' + event.clientY)
}
function on_mouse_move (event) {
connection.send ('MM,' + event.clientX + ',' + event.clientY)
}
function make_touch_list (tl) {
var result = new Array (tl.length);
for (i=0; i < tl.length; i++) {
result[i] = tl[i].pageX + '.' + tl[i].pageY;
}
return result.join (',');
}
function on_touch_start (event) {
event.preventDefault();
connection.send ('TS,' + make_touch_list (event.touches));
}
function on_touch_end (event) {
// no touch list on this one...
connection.send ('TE');
}
function on_touch_move (event) {
connection.send ('TM,' + make_touch_list (event.touches));
}
function on_key_down (event) {
connection.send ('KD,' + event.keyCode)
}
function on_key_up (event) {
connection.send ('KU,' + event.keyCode)
}
# -*- Mode: Python -*-
from coro.http.websocket import handler, websocket
import pickle
import random
import re
import quadtree
import coro
W = coro.write_stderr
colors = ['red', 'green', 'blue', 'magenta', 'purple', 'plum', 'orange']
# sample 'box' object.
class box:
def __init__ (self, color, rect):
self.color = color
self.rect = rect
def get_rect (self):
return self.rect
def __repr__ (self):
return '<box (%d,%d,%d,%d)>' % self.rect
class field:
def __init__ (self, w=1024*20, h=1024*20):
self.w = w
self.h = h
self.clients = set()
self.Q = quadtree.quadtree()
self.generate_random_field()
def generate_random_field (self):
for i in range (5000):
x = random.randint (0, self.w - 100)
y = random.randint (0, self.h - 100)
w = random.randint (50, 300)
h = random.randint (50, 300)
c = random.choice (colors)
b = box (c, (x, y, x+w, y+h))
self.Q.insert (b)
def new_conn (self, *args, **kwargs):
c = field_conn (self, *args, **kwargs)
self.clients.add (c)
class field_conn (websocket):
def __init__ (self, field, *args, **kwargs):
websocket.__init__ (self, *args, **kwargs)
self.send_mutex = coro.mutex()
self.field = field
rx = random.randint (0, self.field.w-1024)
ry = random.randint (0, self.field.h-1024)
self.rect = (rx, ry, rx+1024, ry+1024)
self.draw_window()
self.mouse_down = None, None
def move_window (self, mx, my):
x0, y0, x1, y1 = self.rect
x0 = x0 + mx; y0 = y0 + my
x1 = x1 + mx; y1 = y1 + my
if x0 < 0:
x0 = 0; x1 = 1024
if y0 < 0:
y0 = 0; y1 = 1024
if x1 > self.field.w-1024:
x1 = self.field.w-1024; x0 = x1 - 1024
if y1 > self.field.h-1024:
y1 = self.field.h-1024; y0 = y1 - 1024
self.rect = (x0, y0, x1, y1)
self.draw_window()
self.send_text ('M pos=%d,%d' % self.rect[:2])
def draw_window (self):
r = ['F']
px, py = self.rect[:2]
for ob in self.field.Q.search_gen (self.rect):
c = ob.color
x0, y0, x1, y1 = ob.get_rect()
r.append ('B,%s,%d,%d,%d,%d' % (c, x0-px, y0-py, x1-x0, y1-y0))
self.send_text ('|'.join (r))
def send_text (self, payload):
with self.send_mutex:
websocket.send_text (self, payload)
def handle_packet (self, p):
data = p.unpack()
event = p.unpack().split (',')
#W ('packet = %r event=%r\n' % (p, event))
if event[0] == 'KD':
ascii = int (event[1])
if ascii == 87: # W
self.move_window (0, -10)
elif ascii == 65: # A
self.move_window (-10, 0)
elif ascii == 83: # S
self.move_window (0, 10)
elif ascii == 68: # D
self.move_window (10, 0)
elif ascii == 82: # R
x0, y0 = self.rect[:2]
self.move_window (-x0, -y0)
elif event[0] == 'MD':
self.on_mouse_down (int (event[1]), int (event[2]))
elif event[0] == 'MU':
self.on_mouse_up (int (event[1]), int (event[2]))
elif event[0] == 'MM':
self.on_mouse_move (int (event[1]), int (event[2]))
elif event[0] == 'TS':
tl = self.unpack_touch_list (event[1:])
self.on_mouse_down (tl[0][0], tl[0][1])
elif event[0] == 'TM':
tl = self.unpack_touch_list (event[1:])
self.last_touch_move = tl[0][0], tl[0][1]
self.on_mouse_move (tl[0][0], tl[0][1])
elif event[0] == 'TE':
# emulate mouse up by with saved last touch_move
x0, y0 = self.last_touch_move
self.on_mouse_up (x0, y0)
else:
W ('unknown event: %r\n' % (event,))
return False
def unpack_touch_list (self, tl):
W ('touch_list=%r\n' % (tl,))
return [ [int(y) for y in x.split('.')] for x in tl ]
def on_mouse_down (self, x, y):
self.mouse_down = x, y
def on_mouse_up (self, x1, y1):
x0, y0 = self.mouse_down
self.mouse_down = None, None
# 1) draw a box in the region chosen
#if x0 > x1:
# x0, x1 = x1, x0
#if y0 > y1:
# y0, y1 = y1, y0
#px, py = self.rect[:2]
#b = box (random.choice (colors), (x0+px, y0+py, x1+px, y1+py))
#self.field.Q.insert (b)
# 2) or move the window
self.move_window (x0-x1, y0-y1)
self.draw_window()
def on_mouse_move (self, x1, y1):
x0, y0 = self.mouse_down
if x0:
if abs(x1-x0) > 10 or abs(y1-y0) > 10:
# moved enough to redraw
self.mouse_down = x1, y1
self.move_window (x0-x1, y0-y1)
if __name__ == '__main__':
import coro.http
import coro.backdoor
import os
cwd = os.getcwd()
f = field()
ih = coro.http.handlers.favicon_handler()
sh = coro.http.handlers.coro_status_handler()
th = handler ('/field', f.new_conn)
fh = coro.http.handlers.file_handler (cwd)
# so you can browse the source
import mimetypes
mimetypes.init()
mimetypes.add_type ('text/plain', '.py')
handlers = [th, ih, sh, fh]
#server = coro.http.server (('0.0.0.0', 9001))
server = coro.http.server()
for h in handlers:
server.push_handler (h)
#coro.spawn (server.start)
coro.spawn (server.start, ('0.0.0.0', 9001))
coro.spawn (coro.backdoor.serve, unix_path='/tmp/ws.bd')
coro.event_loop (30.0)
# -*- Mode: Python; tab-width: 4 -*-
# SMR 2012: originally from dynwin, circa 1995-96.
# modernized, replace some funs with generators
#
# Quad-Tree. A 2D spatial data structure.
#
# Used to quickly locate 'objects' within a particular region. Each
# node in the tree represents a rectangular region, while its children
# represent that region split into four quadrants. An 'object' is
# stored at a particular level if it is contained within that region,
# but will not fit into any of the individual quadrants inside it.
#
# If an object is inserted into a quadtree that 'overflows' the current
# boundaries, the tree is re-rooted in a larger space.
import region
contains = region.region_contains_region_p
intersects = region.region_intersect_p
# split a rect into four quadrants
def split (rect):
l,t,r,b = rect
w2 = ((r-l)/2)+l
h2 = ((b-t)/2)+t
return (
(l,t,w2,h2),
(w2,t,r,h2),
(l,h2,w2,b),
(w2,h2,r,b)
)
# insert an object into the tree. The object must have a
# 'get_rect()' method in order to support searching.
def insert (tree, tree_rect, ob, ob_rect):
quads = split(tree_rect)
# If tree_rect is in quads, then we've shrunk down to a
# degenerate rectangle, and we will store the object at
# this level without splitting further.
if tree_rect not in quads:
for i in range(4):
if contains (quads[i], ob_rect):
if not tree[i]:
tree[i] = [None,None,None,None,[]]
insert (tree[i], quads[i], ob, ob_rect)
return
tree[4].append (ob)
# generate all the objects intersecting with <search_rect>
def search_gen (tree, tree_rect, search_rect):
quads = split (tree_rect)
for ob in tree[4]:
if intersects (ob.get_rect(), search_rect):
yield ob
for i in range(4):
if tree[i] and intersects (quads[i], search_rect):
for ob in search_gen (tree[i], quads[i], search_rect):
yield ob
# delete a particular object from the tree.
def delete (tree, tree_rect, ob, ob_rect):
if tree[4]:
try:
tree[4].remove (ob)
# is this branch now empty?
return (tree[4] == [])
except ValueError:
# object not stored here
pass
quads = split (tree_rect)
for i in range(4):
if tree[i] and intersects (quads[i], ob_rect):
if not delete (tree[i], quads[i], ob, ob_rect):
tree[i] == None
# equivalent to "tree != [None,None,None,None,[]]"
return not filter (None, tree)
return tree[4]
def gen_all (tree):
if tree[4]:
for ob in tree[4]:
yield ob
for quad in tree[:4]:
if quad:
for x in gen_all (quad):
yield x
# wrapper for a quadtree, maintains bounds, keeps track of the
# number of objects, etc...
class quadtree:
def __init__ (self, rect=(0,0,16,16)):
self.rect = rect
self.tree = [None,None,None,None,[]]
self.num_obs = 0
self.bounds = (0,0,0,0)
def __repr__ (self):
return '<quad tree (objects:%d) bounds:%s >' % (
self.num_obs,
repr(self.bounds)
)
def check_bounds (self, rect):
l,t,r,b = self.bounds
L,T,R,B = rect
if L < l:
l = L
if T < t:
t = T
if R > r:
r = R
if B > b:
b = B
self.bounds = l,t,r,b
def get_bounds (self):
return self.bounds
def insert (self, ob):
rect = ob.get_rect()
while not contains (self.rect, rect):
l,t,r,b = self.rect
w, h = r-l, b-t
# favor growing right and down
if (rect[2] > r) or (rect[3] > b):
# resize, placing original in the upper left
self.rect = l, t, (r+w), (b+h)
self.tree = [self.tree, None, None, None, []]
elif (rect[0] < l) or (rect[1] < t):
# resize, placing original in lower right
self.rect = (l-w,t-h,r,b)
self.tree = [None, None, None, self.tree, []]
# we know the target rect fits in our space
insert (self.tree, self.rect, ob, rect)
self.check_bounds (rect)
self.num_obs = self.num_obs + 1
def gen_all (self):
for ob in gen_all (self.tree):
yield ob
def search_gen (self, rect):
for ob in search_gen (self.tree, self.rect, rect):
yield ob
def delete (self, ob):
# we ignore the return, because we can't 'forget'
# the root node.
delete (self.tree, self.rect, ob, ob.get_rect())
# sample 'box' object.
class box:
def __init__ (self, rect):
self.rect = rect
def get_rect (self):
return self.rect
def __repr__ (self):
return '<box (%d,%d,%d,%d)>' % self.rect
# -*- Mode: Python; tab-width: 4 -*-
# SMR 2012: originally from dynwin, circa 1995
###########################################################################
# regions
###########################################################################
#
# do two rectangular regions intersect?
#
# +--->
# | lb,tb-------+
# | | |
# | la,ta--+-----+ |
# | | | | |
# | | +-----+-rb,bb
# V | |
# +--------ra,ba
# a rect is (left,top,right,bottom)
# top is < bottom, left is < right
# proof: imagine all possible cases in 1 dimension,
# (there are six) and then generalize. simplify the
# expression and you get this. (trust me 8^)
def region_intersect_p (a,b):
return (a[2] >= b[0]) and \
(b[2] >= a[0]) and \
(a[3] >= b[1]) and \
(b[3] >= a[1])
def point_in_region_p (x,y,r):
return (r[0] <= x <= r[2]) and (r[1] <= y <= r[3])
# does region <a> fully contain region <b>?
def region_contains_region_p (a,b):
return (a[0] <= b[0]) and \
(a[2] >= b[2]) and \
(a[1] <= b[1]) and \
(a[3] >= b[3])
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment