From kragen at pobox.com Sat Dec 1 03:37:01 2007 From: kragen at pobox.com (Kragen Javier Sitaker) Date: Sat Dec 1 03:37:02 2007 Subject: simple assembly exercise: print ASCII codes of a string Message-ID: <20071113072633.00CC0183CBD@panacea.canonical.org> # For i386 Linux. You'll have to run this through cpp and gas; # easiest way is to put it in a file whose name ends in .S and say # gcc -m32 -nostdlib -static -o printstring printstring.S #include .section .rodata mystr: .asciz "ABCDE is a string" .data byte: .byte 0 .text .globl _start _start: mov $mystr, %eax call printchars mov $'\n, %eax call printchar mov $__NR_exit, %eax mov $0, %ebx int $0x80 printchars: # args: %eax points to string xor %ebx, %ebx loop: mov (%eax), %bl inc %eax push %eax push %ebx movzbl %bl, %eax call printnumspc pop %ebx pop %eax and %ebx, %ebx # nul-terminated string jnz loop ret printnumspc: # args: %eax is number to print call printnum # number must be nonnegative mov $32, %eax # space char jmp printchar # tail-call printnum: # args: %eax is number to print mov $10, %ebx mov $'0, %ecx and %eax, %eax jnz printnonzero mov $'0, %eax jmp printchar # tail-call printnonzero: # args: %eax is number to print # number must be nonnegative xor %edx, %edx # %ebx is base (nonclobbered) idiv %ebx # %ecx is zero digit # (nonclobbered) and %eax, %eax jz done push %edx call printnonzero pop %edx done: add %ecx, %edx push %eax push %ebx push %ecx mov %edx, %eax call printchar pop %ecx pop %ebx pop %eax ret printchar: # args: %al is char to print mov %al, byte mov $__NR_write, %eax mov $1, %ebx # stdout mov $byte, %ecx mov $1, %edx # length int $0x80 ret From kragen at pobox.com Sun Dec 9 21:24:41 2007 From: kragen at pobox.com (Kragen Javier Sitaker) Date: Sun Dec 9 21:24:42 2007 Subject: simple music sequencer in PyGame Message-ID: <20071210022441.GA1974@canonical.org> Here's a simple sequencer that has some interesting features in PyGame. See http://pobox.com/~kragen/sw/pygmusic/ for more details. #!/usr/bin/python # -*- encoding: utf-8 -*- """Pygmusic: A simple music sequencer in PyGame. By Kragen Sitaker, 2007-12-07, 08, and 09. Drag things around with the mouse; the right mouse button exits. I, the copyright holder of this work, hereby release it into the public domain. This applies worldwide. In case this is not legally possible, I grant any entity the right to use this work for any purpose, without any conditions, unless such conditions are required by law. """ import pygame, time, os, sys, math ### basic utility functions def gray(x): "Return a grayscale color tuple given an argument in [0,256)" return (x, x, x) black = gray(0) white = gray(255) def padd((x1, y1), (x2, y2)): "Add an offset to a 2D point." return (x2 + x1, y2 + y1) def diff((x1, y1), (x2, y2)): "Take the offset between two 2D points." return (x2 - x1, y2 - y1) ### basic drawable objects class Visible: """Things that get drawn on the screen and maybe handle mouse clicks or make sounds. These objects can be added to the world with world.add and are expected to be able to do the following: - obj.contains((x, y)): tell whether a mouse position is in the object - obj.handle_click(world, event): handle a mouse button going down - obj.play(world): make any appropriate sounds - obj.center(): return (x, y) center to decide when to be played - obj.draw(self, world, screen): display itself; called every frame. - obj.is_drop_target_for(self, object, world): handle object being dropped on us Additionally, if an object is draggable, it needs to support these: - obj.move(delta): move by delta x and delta y - obj.handle_drop(world): finish being dragged In order to use the default implementations of contains and center, the object needs to have a rect property that acts like a pygame.Rect. """ def contains(self, (x, y)): return self.rect.collidepoint(x, y) def center(self): return self.rect.center def is_drop_target_for(self, obj, world): "Default is to do nothing." def handle_click(self, world, ev): "Default is to do nothing." def play(self, world): "Default is to do nothing." class Timer(Visible): "A horizontal strip on the screen that plays things in it when triggered." def __init__(self, rect, cycletime, color, active=True, divisions=8): "cycletime is the time to go through the whole cycle." self.rect = rect self.cycletime = cycletime self.color = color self.active = active self.start = time.time() self.lastoffset = 0 self.divisions = divisions def drawvbar(self, screen, offset, color): "Draws a timing mark at a given offset." screen.fill(color, ((self.rect.left + offset, self.rect.top), (1, self.rect.h))) def drawvbars(self, screen, n, color): "Draw timing marks for odd intervals of every nth of a cycle." for ii in range(1, n): self.drawvbar(screen, ii * self.rect.w / float(n), color) def draw(self, world, screen): screen.fill(gray(self.color), self.rect) self.drawvbars(screen, self.divisions*2, gray(136)) self.drawvbars(screen, self.divisions, gray(144)) self.drawvbars(screen, 2, gray(192)) if self.active: self.draw_cursor(world, screen) def time(self): "The time since the beginning of the current play cycle." age = time.time() - self.start if age > self.cycletime: return self.cycletime return age def offset(self): "The current position of the cursor for this timer." return int(self.time() / self.cycletime * self.rect.w + 0.5) def handle_click(self, world, ev): self.trigger() def trigger(self): "Start playing the sounds within." self.active = True self.lastoffset = 0 self.start = time.time() def cursor_rect(self, start, end): "Returns a Rect from pixel offset 'start' to 'end' for a cursor." return pygame.Rect((self.rect.left + start, self.rect.top), (end - start, self.rect.h)) def cursor_rects(self, offset): "Returns the Rects showing the currently playing period on the screen." assert offset >= self.lastoffset return [self.cursor_rect(self.lastoffset, offset)] def draw_cursor(self, world, screen): """Draws the white box that represents the currently playing period, and also plays the sounds found within.""" offset = self.offset() for rect in self.cursor_rects(offset): screen.fill(white, rect) # If we .play() things immediately, there could be # surprising effects (e.g. if we're playing ourselves). # So we enqueue the playing for later. for obj in world.objects_in(rect): world.defer(lambda obj=obj: obj.play(world)) self.lastoffset = offset class RepeatingTimer(Timer): "A horizontal strip on the screen that plays things in it repeatedly." def time(self): return (time.time() - self.start) % self.cycletime def handle_click(self, world, ev): "Turns the timer on and off when clicked." if not self.active: self.lastoffset = self.offset() self.active = not self.active def cursor_rects(self, offset): if offset >= self.lastoffset: return [self.cursor_rect(self.lastoffset, offset)] else: return [self.cursor_rect(self.lastoffset, self.rect.w), self.cursor_rect(0, offset)] class ImageDisplay(Visible): "A visible object that displays by merely blitting an image." def __init__(self, pos, image): self.image = image self.rect = pygame.Rect(pos, image.get_size()) # to satisfy Visible def draw(self, world, surface): "ImageDisplay.draw just blits the object's image." surface.blit(self.image, self.rect.topleft) class UglyHalo(ImageDisplay): """A white square that fades to nothing over 0.6 seconds to show that something has happened.""" def __init__(self, rect): "rect is the area to draw the halo around." halosize = rect.h * 2 # I wanted to do a circle, but it turns out that in SDL you can't # have both a per-pixel alpha and a per-surface alpha, and I # figured that fading the per-pixel alpha in a Python nested loop # would be too slow, so for now, it's a white square. See the # NumericHalo class for an alternative. image = pygame.surface.Surface((halosize, halosize)) image.fill(white) ImageDisplay.__init__(self, diff(image.get_rect().center, rect.center), image) self.start = time.time() def draw(self, world, surface): "Blits the halo image with the current opacity; possibly suicides." age = time.time() - self.start opacity = int(255 * (0.6 - age)) if opacity <= 0: world.delete(self) else: self.image.set_alpha(opacity) ImageDisplay.draw(self, world, surface) ### rendering haloes with Numeric def scale(mask, component): "Encode a floating-point color component in [0, 1] with a bitmask." mask = mask % (2**32) # make unsigned rv = int(mask * component) & mask # scale mask by weight if rv >= 2**31: rv -= 2**32 # convert back to signed rv = int(rv) # convert back to non-long return rv def pixel(masks, components): "Encode an (r, g, b, alpha) tuple according to given masks." return sum(map(scale, masks, components)) class NumericHaloMovie: "Renders frames of a halo on demand, then caches them." def __init__(self, args): framelength, size, max_age, fuzz = args self.framelength = framelength self.size = size # size of the object halo is around self.max_age = max_age self.fuzzsq = fuzz**2 self.frames = {} self.shape = (size*2, size*2) cx = cy = size # x and y at center xs, ys = Numeric.indices(self.shape) # x and y coords of each pixel (dx, dy) = (xs - cx, ys - cy) # distances from center for each pixel self.rsq = dx*dx + dy*dy # squared distance from center def render_frame(self, ii): age = ii * self.framelength # age in seconds when to show this frame # create a surface that has an alpha channel, to render to and return mysurf = pygame.Surface(self.shape).convert_alpha() masks = mysurf.get_masks() # get bitmasks for r, g, b, alpha global_alpha = (1.2 * self.max_age - age)/self.max_age fsq = self.fuzzsq # palette is the colors in delta-rsquared, from densest to # most rarefied. Most rarefied is transparent, 0. Densest is # nearly white. palette = Numeric.array( [pixel(masks, (1, 1, # r and g are always 100% 0.8 * float(fsq-ii)/fsq, # b is 0-80% 0.9 * float(fsq-ii)/fsq * global_alpha)) for ii in range(fsq)] + [0]) # This is the r-squared where that maximum density is found. # The formula is just voodoo --- I whacked on it until the # effect looked OK. It doesn't scale properly with max_age. max_level = self.size**2/2 * (1 - (1 - age*2)**2) # Take absolute difference of r-squared from the point of the # current maximum, clamp it between 0 and fuzz**2, convert to # integer so we can use it to index the palette. density = Numeric.clip(Numeric.absolute(self.rsq - max_level), 0, fsq).astype(Numeric.Int) colors = Numeric.take(palette, density) pygame.surfarray.blit_array(mysurf, colors) return mysurf def __getitem__(self, framenum): "Get a frame." try: return self.frames[framenum] except KeyError: frame = self.render_frame(framenum) self.frames[framenum] = frame return frame halo_movies = {} def get_halo_movie(*args): "Find a requested halo movie." # This way multiple haloes of the same size (and other attributes) # can share the same rendered frames. if halo_movies.has_key(args): return halo_movies[args] halo_movies[args] = NumericHaloMovie(args) return halo_movies[args] class NumericHalo(Visible): "Draws a fading halo computed with Numerical Python." def __init__(self, rect): "rect is the area to draw the halo around." fuzz = 10 self.framelength = 1/120.0 # of a second. self.max_age = 0.4 # of a second self.frames = get_halo_movie(self.framelength, rect.h, self.max_age, fuzz) pos = diff(self.frames[0].get_rect().center, rect.center) self.rect = pygame.Rect(pos, self.frames[0].get_size()) self.start = time.time() def draw(self, world, surface): "Blits the best frame for the halo's current age; possibly suicides." age = time.time() - self.start if age > self.max_age: world.delete(self) else: surface.blit(self.frames[int(age / self.framelength + 0.5)], self.rect.topleft) try: # test to see if Numeric and surfarray are available import Numeric pygame.surfarray.blit_array except: make_halo = UglyHalo else: make_halo = NumericHalo ### Sound-making objects class DragSource(ImageDisplay): "Here is a source you can drag new Visibles from." def __init__(self, pos, image, instance): "instance is a callable to call with (x, y) to make the new thing." ImageDisplay.__init__(self, pos, image) self.instance = instance def handle_click(self, world, ev): "Start a drag with a new instance of, say, a Sound." # The (15, 15) offset has two purposes: # - it makes it clear that the new thing is a new thing, and # not a change of color of the old thing; # - it happens to put that thing by default into one of the # timers, so that if you don't drag it anywhere, it will still # get played. new = self.instance(padd(self.rect.topleft, (15, 15))) world.add(new) new.start_drag(world, ev) new.play(world) class Sound(ImageDisplay): "A draggable sound that you can put in the tracks." def __init__(self, pos, image, sound): ImageDisplay.__init__(self, pos, image) self.sound = sound def move(self, delta): self.rect = self.rect.move(delta) def start_drag(self, world, ev): world.raise_to_top(self) world.grab(self, ev.pos) def handle_drop(self, world): "Handle being dropped; called by the world." # we cheat and look a pixel up and left droptarget = world.object_at(padd(self.rect.topleft, (-1, -1))) if droptarget: droptarget.is_drop_target_for(self, world) def handle_click(self, world, ev): self.start_drag(world, ev) self.play(world) def play(self, world): "Plays the object's sound and kicks off a halo; called by timer." self.sound.play() world.add_nonclickable(make_halo(self.rect)) class Trigger(Sound): """A draggable object that you can put in a timer to trigger some other thing, such as another timer.""" # XXX currently inherits from Sound so as to be draggable. def __init__(self, pos, image, gun): ImageDisplay.__init__(self, pos, image) self.gun = gun def play(self, world): world.add_nonclickable(make_halo(self.rect)) self.gun.trigger() ### miscellaneous including the world class Trash(ImageDisplay): """The trashcan deletes things dropped on it.""" def is_drop_target_for(self, object, world): "Delete the dropped thing and make a halo." world.delete(object) world.add_nonclickable(make_halo(self.rect)) class Profiler: def __init__(self): self.times = {} def __str__(self): return str(self.times) def start(self): self.last_time = time.time() def note(self, what): now = time.time() dur = now - self.last_time self.last_time = now if not self.times.has_key(what): self.times[what] = 0 self.times[what] += dur class World: """Manages the set of stuff you see on the screen and routes events. I think this is basically the Smalltalk MVC Controller.""" def __init__(self, screen): "screen is an SDL/PyGame surface to draw on." self.screen = screen self.objects = [] # clickable visible objects self.nonclickable_objects = [] # halos and such self.grab(None, None) # initialize drag state self.redraw_profiler = Profiler() self.deferreds = [] def redraw(self): "This gets called whenever there's idle time, i.e. each frame." self.screen.fill(black) self.redraw_profiler.start() for obj in self.objects + self.nonclickable_objects: obj.draw(self, self.screen) self.redraw_profiler.note(obj.__class__.__name__) for task in self.deferreds: task() self.deferreds = [] def defer(self, task): "Enqueue a task to be done as soon as possible after redraw." self.deferreds.append(task) def add(self, obj): "Call to put a new visible, clickable object on the screen." self.objects.append(obj) def add_nonclickable(self, obj): """Like add, but for nonclickable objects above everything. I added this for halos because they were accidentally getting drawn underneath other screen objects, and they aren't supposed to impede the clickability of the objects they're haloing around. """ self.nonclickable_objects.append(obj) def object_at(self, pos): "Return the topmost object at pos, or None." # iterate backwards so objects drawn "on top" get first choice for ii in range(len(self.objects)-1, -1, -1): if self.objects[ii].contains(pos): return self.objects[ii] def handle_click(self, ev): "Route a click event to the relevant object." obj = self.object_at(ev.pos) if obj: obj.handle_click(self, ev) else: self.ungrab() def handle_release(self, ev): "Handle a button release event." self.ungrab() def handle_motion(self, ev): "Handle mouse motion, by doing a mouse drag if needed." if self.dragobj is None: return self.dragobj.move(diff(self.dragpos, ev.pos)) self.dragpos = ev.pos def ungrab(self): "Terminate any drag." if self.dragobj is not None: self.dragobj.handle_drop(self) self.grab(None, None) def grab(self, obj, pos): "Start dragging some object." (self.dragobj, self.dragpos) = (obj, pos) def delete(self, obj): "Remove a visible object (clickable or not) from the world." if obj in self.nonclickable_objects: self.nonclickable_objects.remove(obj) else: self.objects.remove(obj) def raise_to_top(self, obj): "Move an object to the top of the drawing stack." self.delete(obj) self.add(obj) def objects_in(self, arect): """Find all the objects whose centers are in a rectangle. This is used by the timer to figure out what to play. """ return [obj for obj in self.objects if arect.contains(pygame.Rect(obj.center(), (1, 1,)))] def main(argv): "Main program." mydir = os.path.split(argv[0])[0] pygame.init() screen = pygame.display.set_mode((640, 480), pygame.FULLSCREEN) world = World(screen) timerwidth = 440 def timer(y, cycletime, color, active=True): world.add(RepeatingTimer(rect=pygame.Rect((100, y), (timerwidth,30)), cycletime=cycletime*0.16667, color=color, active=active, divisions=cycletime)) font = pygame.font.Font(None, 24) # use default font, 24 pixels high def getsound(soundname): path = os.path.join(mydir, soundname + '.wav') return pygame.mixer.Sound(path) def renderletter(letter, color): return font.render(letter, 1, color) def addsource(xpos, letter, soundname): image = renderletter(letter, white) sound = getsound(soundname) make = lambda pos: Sound(pos=pos, image=image, sound=sound) source = DragSource(pos=(xpos, 80), image=renderletter(letter, (255, 128, 128)), instance=make) world.add(source) return lambda pos: world.add(source.instance(pos)) yy = addsource(100, 'Y', 'score') zz = addsource(120, 'Z', 'extraball') aa = addsource(140, 'A', 'reflect_paddle') bb = addsource(160, 'B', 'reflect_brick') addsource(180, 'C', 'menu_click') addsource(200, 'D', 'shrink') timer(100, 3, 128) bb((95, 100)) timer(130, 5, 120, active=False) zz((95, 130)) timer(160, 7, 128, active=False) yy((95, 160)) timer(190, 8, 120) aa((95, 190)) aa((95 + timerwidth/2, 190)) mytimer = Timer(rect=pygame.Rect((100, 250), (timerwidth,30)), cycletime=1, color=128, active=True, divisions=6) world.add(mytimer) world.add(DragSource((75, 250), image=renderletter("E", (255, 128, 128)), instance = lambda pos: Trigger(pos, renderletter("E", white), mytimer))) trashf = os.path.join(mydir, 'trashcan_empty.png') world.add(Trash((100, 300), pygame.image.load(trashf).convert())) # Some basic instructions. world.add(ImageDisplay((200, 300), font.render("Drag things around with the mouse.", 1, white))) world.add(ImageDisplay((200, 320), font.render("The right mouse button exits.", 1, white))) frames = 0 start = time.time() while 1: ev = pygame.event.poll() if ev.type == pygame.NOEVENT: world.redraw() frames += 1 pygame.display.flip() elif ev.type == pygame.MOUSEMOTION: world.handle_motion(ev) elif ev.type == pygame.MOUSEBUTTONDOWN: if ev.button == 3: break world.handle_click(ev) elif ev.type == pygame.MOUSEBUTTONUP: world.handle_release(ev) elif ev.type == pygame.QUIT: break end = time.time() print "%.2f seconds, %.2f fps" % ((end - start), frames / (end - start)) print 'redraw times', world.redraw_profiler if __name__ == '__main__': main(sys.argv) From kragen at pobox.com Sat Dec 15 03:37:02 2007 From: kragen at pobox.com (Kragen Javier Sitaker) Date: Sat Dec 15 03:37:03 2007 Subject: fun moire patterns with pygame Message-ID: <20071210022552.GA2077@canonical.org> This program was a sketch for the little bursts in the music sequencer, but the background makes for an interesting effect. #!/usr/bin/python # Draw a cool burst on the screen. # Has the following interesting features: # - The area covered by any individual color remains constant as the # burst expands, once it's out far enough that there's an empty part # in the middle. I think. # - Nicely alpha-blends with background. # - Starts expanding rapidly and then slows down a bit. # - Renders at about 60fps on my laptop in 320x240, or 30fps in 640x480 # Currently has the following problems: # - the burst surface is bigger than needed # - I don't understand how to adjust the clock to do the right thing. # - I'd like the palette to make a nice gradual transition from white, # down to red, down to yellow, and then down to transparent. # - Too symmetrical. # - The code is too wordy. import pygame, Numeric, time, math burstsize = 40 fuzz = 20 def scale(mask, component): "Encode a floating-point color component in [0, 1] with a bitmask." mask = mask % (2**32) # make unsigned rv = int(mask * component) & mask # scale mask by weight if rv >= 2**31: rv -= 2**32 # convert back to signed rv = int(rv) # convert back to non-long return rv def pixel(masks, components): "Encode an (r, g, b, alpha) tuple according to given masks." return sum(map(scale, masks, components)) class World: def __init__(self, screen): self.screen = screen # make a simple, pretty background. bxs, bys = Numeric.indices(self.screen.get_size()) self.background = bxs * bys * bys / 128 surfsize = (burstsize+fuzz)*2 self.burstsurface = pygame.Surface((surfsize,surfsize)).convert_alpha() xs, ys = Numeric.indices(self.burstsurface.get_size()) (dx, dy) = (xs - surfsize/2, ys - surfsize/2) # distances from center self.rsq = dx*dx + dy*dy # square of distances from center # Compute where to blit the surface cx, cy = self.screen.get_rect().center self.blitdest = (cx - surfsize/2, cy - surfsize/2) # Red and green stay at 100%; blue fades from 80% down to 0, # and alpha fades from 90% down to 0. Then the last palette # value is entirely transparent. masks = self.burstsurface.get_masks() self.palette = Numeric.array([pixel(masks, (1, 1, 0.8 * float(fuzz**2-ii)/fuzz**2, 0.9 * float(fuzz**2-ii)/fuzz**2)) for ii in range(fuzz**2)] + [0]) self.palette = self.palette.astype(Numeric.Int32) # just to make sure def redraw(self): pygame.surfarray.blit_array(self.screen, self.background) # The threshold value of r-squared with maximum brightness. I # fiddled with this until it was kind of OK, but I don't # really like it. clock = burstsize**2 - ((-time.time() / 3 % 2 - 1) * burstsize)**2 # the per-pixel distance from that threshold burst = Numeric.absolute(self.rsq - clock) # mapped to fit the palette burst_clamped = Numeric.clip(burst, 0, fuzz**2).astype(Numeric.Int) burst_image = Numeric.take(self.palette, burst_clamped) pygame.surfarray.blit_array(self.burstsurface, burst_image) self.screen.blit(self.burstsurface, self.blitdest) def main(): pygame.init() # It gets really trippy if you specify certain bit depths in place # of 0. Like 22. I assume this is some kind of SDL bug, but it's # really cool. screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN, 0) world = World(screen) frames = 0 start = time.time() while 1: ev = pygame.event.poll() if ev.type == pygame.NOEVENT: world.redraw() pygame.display.flip() frames += 1 elif ev.type == pygame.QUIT: break elif ev.type == pygame.MOUSEBUTTONDOWN: break dur = time.time() - start print "%d frames; %.2f/sec" % (frames, frames/dur) if __name__ == '__main__': main() From kragen at pobox.com Sat Dec 22 03:37:01 2007 From: kragen at pobox.com (Kragen Javier Sitaker) Date: Sat Dec 22 03:37:03 2007 Subject: wave mechanics with PyGame and Numeric Message-ID: <20071210022713.GA2111@canonical.org> This gets 20fps at 320x240 with one wave on my 700MHz laptop. #!/usr/bin/python # Simple wave mechanics in PyGame, by Kragen Javier Sitaker, 2007-12-07. # Needs Python, SDL, PyGame, and Numeric Python installed. # Notes on speed: # Sadly at first I was only able to render <17fps with a single wave, # which means it's doing under 1.3 million pixels per second on my # 700MHz PIII-Coppermine, and 11fps with two waves. I thought it was # absurd that it takes more than 500 clock cycles per pixel, # especially given that it's not doing any significant amount of # Python (it's doing about 80 Python bytecodes in the redraw function, # which adds up to 960 pixels per Python bytecode) but I wrote a C # version (just doing all the math inside the loop, instead of in big # arrays) and it was only 50% faster. After some experimentation, I # switched the C version to avoid floating-point math in the inner # loop, to approximate the square root with linear interpolation, and # to replace the sine function and scaling to palette index operations # with a table lookup, and quadrupled its speed, making it about six # times as fast as this Python version at the time. I tried the same # optimizations on this program, and it got slower. # I finally got to 20fps (with one wave) (19% of the C version's # speed) by switching to single-precision float math. The tricky # parts were that single-precision isn't precise enough to express the # current time (so you have to take it mod 2*pi) and that you have to # manually convert each scalar to a single-precision float. import pygame, sys, Numeric, time, math twopi = 2 * math.pi def grayscale_for_masks(masks, level): "Compute a grayscale pixel from bit masks and a floating-point level [0,1)" return sum([int(mask * level) & mask for mask in masks]) class World: "The stuff that gets drawn on the screen." def __init__(self, screen): self.screen = screen width, height = self.screen.get_size() # This is a bit hard to explain, but this makes arrays 'xs' # and 'ys' that contain the x and y coordinates of each pixel. # So every row of the 'xs' array is [0, 1, 2, 3...], and row 0 # of the 'ys' array is [0, 0, 0, 0...], while row 1 is [1, 1, # 1, 1...]. This is somewhat confused by the default Python # display of these guys being transposed if you print them out. (self.xs, self.ys) = (xs, ys) = Numeric.indices((width, height)) # Now we want an array of radii (hi Andy). So we from_center_x = xs - width / 2 from_center_y = ys - height / 2 self.r = (Numeric.sqrt(from_center_x ** 2 + from_center_y ** 2) / (width/64)).astype(Numeric.Float32) self.tmp = self.r.copy() # temp space for later (to reduce per-frame allocation) masks = self.screen.get_masks()[0:3] # Lookup table for grayscale levels. self.palette = Numeric.array([grayscale_for_masks(masks, level/256.0) for level in range(256)]) def add_second_wave(self, to_what): pass def peak(self): return 1.01 # was getting occasional overflow errors on y1 def redraw(self): # This function is written in a fairly assembly-language style # in order to cut down on the number of intermediate result # spaces that must be allocated. tmp = self.tmp # to make code briefer N = Numeric f32 = lambda x: N.array(x, N.Float32) # tmp gets -time.time() + self.r N.add(f32(-time.time() % twopi), self.r, tmp) # tmp gets sin(tmp), i.e. sin(r - time) N.sin(tmp, tmp) self.add_second_wave(tmp) # add a second wave in the subclass # tmp gets tmp + peak, i.e. peak + sin(r - time) N.add(tmp, f32(self.peak()), tmp) # tmp gets tmp * (256/ (2*peak)), i.e. (1 + sin(r-time))/2 * 256 N.multiply(tmp, f32(256 / (self.peak()*2)), tmp) # round floats to Int8 so we can look things up in palette ints = tmp.astype(N.Int8) # Look up the pixel value for each grayscale level in the palette grayscale = N.take(self.palette, ints) # I tried using surfarray.pixels2d and blitting from there, # but that made things like 10% slower. So here we blit_array # onto the screen. pygame.surfarray.blit_array(self.screen, grayscale) class World2Waves(World): def __init__(self, screen): World.__init__(self, screen) # Center our second set of waves at the upper left-hand corner # of the screen instead of the middle, and give it twice as # long a wavelength self.r2 = (Numeric.sqrt(self.xs ** 2 + self.ys ** 2) / (screen.get_width()/32)).astype(Numeric.Float32) self.tmp2 = self.r.copy() def add_second_wave(self, to_what): # our second wave travels slower by a factor of e Numeric.add(Numeric.array(-time.time() / Numeric.e % twopi, Numeric.Float32), self.r2, self.tmp2) Numeric.sin(self.tmp2, self.tmp2) Numeric.add(self.tmp2, to_what, to_what) def peak(self): return 2 def main(argv): pygame.init() screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN) world = World2Waves(screen) # alternatively just World(screen) frames = 0 start = time.time() while 1: ev = pygame.event.poll() if ev.type == pygame.NOEVENT: frames += 1 world.redraw() pygame.display.flip() elif ev.type == pygame.MOUSEBUTTONDOWN: break elif ev.type == pygame.QUIT: break end = time.time() print "%.2f seconds, %.2f fps" % ((end - start), frames / (end - start)) if __name__ == '__main__': main(sys.argv) From kragen at pobox.com Sat Dec 29 03:37:01 2007 From: kragen at pobox.com (Kragen Javier Sitaker) Date: Sat Dec 29 03:37:02 2007 Subject: fractal barcodes for archival mass digital data storage Message-ID: <20071229045125.65A781835EF@panacea.canonical.org> Storing high-volume digital data so that it will last for decades or centuries without any intervention or maintenance --- the way the Archimedes Palimpsest partly survived being ignored for centuries in a variety of untoward places --- is an important problem. One problem is that the very clever ways we have of encoding data with error-correction coding, data compression, interleaving bits from different codewords so that localized damage only takes out a few from each code word, and so on --- make the data formats very difficult to reverse-engineer. Here's a two-dimensional image data format that's designed to be easy to reverse-engineer, even though it contains a certain amount of redundancy. In theory, it should allow you to store about three megabytes per side on a letter-size sheet of paper printed in a 1200dpi laser printer, and if you use archival-quality paper, the laser priner toner on the paper should be able to survive many centuries of benign neglect. It's also cool to look at. I haven't written a decoder, although I have decoded some of the data by hand to verify that it's working properly. #!/usr/bin/python # -*- encoding: utf-8 -*- # Fractal barcodes, by Kragen Sitaker, 2007. 8-of-9 variant. # This program converts a stream of data into a self-similar # two-dimensional matrix barcode, with the earliest bits of data being # encoded as the largest features. Each level of detail is is encoded # in a 3x3 square, which encodes 4 bits of data, and 8 of the 9 # squares are then used for the next level of detail. # The reason this is interesting is the following: # - It looks really cool. # - It should be really easy to get a fix on the positioning of the # resulting image and compensate for any distortions. # - It's an "archival-friendly" encoding mechanism in the sense that # there are visual clues to the encoding mechanism. # - If the image is scanned with insufficient resolution to capture # all of its details, whatever scale gets captured will convey some # prefix of the data. # So: # with 9 pixels (8 live) you get 4 bits (4 new) # with 81 pixels (64 live) you get 36 bits (32 new) # with 729 pixels (512 live) you get 292 bits (256 new) # with 6561 pixels (4096 live) you get 2340 bits (2048 new) # with 59049 pixels (32768 live) you get 18724 bits (16384 new) # with 531441 pixels (262144 live) you get 149796 bits (131072 new) # with 4782969 pixels (2097152 live) you get 1198372 bits (1048576 new) # (that's the limit at 300 dpi) # with 43046721 pixels (16777216 live) you get 9586980 bits (8388608 new) # (Actually you could get one more bit at the top level, but I don't.) # The bits b0-b3 and their complements are laid out like this: # !b2 !b1 !b0 # b3 K !b3 # b0 b1 b2 # K is the bit from the next higher size of block. b0 is the MSB, # i.e. the 4's bit of the nibble. White means 1; black means 0. # The symmetry of the pattern should simplify reverse-engineering its # interpretation. # (There are actually 70 8-bit patterns that are half ones and half # zeroes, not just 16, so you could encode 6 bits or a little more per # 3x3 region, which would boost your coding efficiency by 50%. This # has some disadvantages for an archival format, though; it destroys # byte-alignment of the bits being encoded, and the mapping between # the balanced 8-bit patterns and the "cleartext" 6-bit patterns is # necessarily a bit arbitrary and thus difficult to discover.) # This is a little trickier than the 2x2-based one I did earlier, # because laser printer resolutions tend to increase by factors of 2, # not 3: 150dpi, 300dpi, 600dpi, 1200dpi. So you get some benefit # from not being resolution-agnostic. But you could produce: # * at 300dpi, a 7.29" x 7.29" 2187x2187 matrix containing 4782969 # pixels and 1.2 megabits (25% coding efficiency) according to the # above. (That's the size this program prints.) # * at 600dpi, four 3.645" square matrices (unfortunately you may not be # able to fit six on a sheet) for a total of 4.8 megabits or 600kB. # * at 1200dpi, you could have a single 6561x6561 matrix at 5.47" # square, encoding about 9.6 megabits (22% coding efficiency); you # might be able to fit two of them. Or you could have a bunch of # 1.8" square 2187x2187 matrices --- say, 20 of them, in a 4x5 # arrangement, encoding just under 24 megabits or three megabytes. # Three megabytes of ASCII text is somewhere between 400 and 1000 # printed pages. If you just make 80x66 page-images out of 5x8 pixels, # you get up to 25x22 = 550 pages out of a 1200dpi letter-size page. # So this method has comparable overhead, just distributed # differently. # I think this makes for a reasonable-cost inactive long-term archival # format for digitally-encoded data; although it's still a little more # expensive than archival-quality CD-Rs, it should last many centuries # longer on 3.5-cent-per-page archival paper. # So our basic scale is 2187 pixels, divided by 300 pixels per inch. import sys bytes = lambda infile: iter(lambda: infile.read(1), '') # read(1) until '' def nibbles(infile): for char in bytes(infile): yield ord(char) >> 4 yield ord(char) & 15 def zoomed(array): return [[v for v in row for _ in range(3)] for row in array for _ in range(3)] class Symbol: def __init__(self): self.full = [[False]] # no-go zones self.colors = [[1]] # an optimization to avoid redundant painting def zoom_in(self): self.full, self.colors = (zoomed(self.full), zoomed(self.colors)) print "zoom_in" def put_square(self, xx, yy, color): if self.colors[xx][yy] != color: print " %s %s %s sq" % (xx, yy, color) self.colors[xx][yy] = color def blocks(self): return ((xx, yy) for yy in range(0, len(self.full), 3) for xx in range(0, len(self.full), 3) if not self.full[xx][yy]) def fill(self, xx, yy): self.full[xx][yy] = True print """%! /rl { rlineto } bind def % abbreviation to shorten 'sq' (draw a square) /sq { setgray moveto 1 0 rl 0 1 rl -1 0 rl closepath fill } bind def /inch { 72 mul } def /center { sub 2 div } def /sz 2187 300 div 72 mul def % 2187 pixels (3^7) divided by 300 dpi 8.5 inch sz center 11 inch sz center translate % close enough on A4 sz dup scale % now the square is (0, 0) to (1, 1) /zoom_in { 1 3 div dup scale } def gsave """ symbol, input_nibbles = Symbol(), nibbles(sys.stdin) try: while 1: symbol.zoom_in() for (xx, yy) in symbol.blocks(): symbol.fill(xx+1, yy+1) # center of block nibble = input_nibbles.next() for ((dx, dy), mask) in [((0, 0), 8), ((1, 0), 4), ((2, 0), 2), ((0, 1), 1)]: if nibble & mask: color = 1 # white else: color = 0 # black symbol.put_square(xx+dx, yy+dy, color) symbol.put_square(xx+(2-dx), yy+(2-dy), 1-color) except StopIteration: pass print "grestore showpage"