#!/usr/bin/env python import array from array import * import math import random import sys import time import cairo import gobject import gtk from gtk import gdk class ScreenSaverItem: def __init__(self): pass def update_state(self, time): pass def paint(self, context): pass def find_position_on_path(self, path, time): # We want to do a cubic bezier path, so we first need to # start with a generic cubic equation: # # b(t) = A * t^3 + B * t^2 + C * t + b0 # # Obviously, the values of the constants determine # the shape of the curve. We don't know about the constants # though. With a bezier curve we only know about the # start point, end point and two control points. # So, we want to compute values for the contants in terms of # those points. Since there are 3 unknowns, we're going to need # three other equations to solve the constants. # # We know that a bezier curve is defined by 4 points, # the start point b(t0), the end point b(t1), and two # control points which aren't on the curve b(t). There # are some other interesting relationships we already know: # # - the start point can be connected to the end point # with a straight line that intersects the curve at # the start point and end point. # # - the first control point can be connected to the # start point by a straight line that is tangent to and # intersects the curve at the start point # # - the second control point can be connected to the # end point by a straight line that is tangent to and # intersects the curve at the end point # # Let's keep things simple by defining the start point to be at # time 0.0 and the end point to be at time 1.0. We won't # lose generality by doing this, its just determines the # scale of the resulting graph. # # Note that one general equation of a straight line is: # # l(t) = D * t + l0 # # We want to a find a line that both b(0) and b(1) are on. # b(0) and b(1) intersect the line where b = l, so let's set # the two equations equal to each other. # # A * t^3 + B * t^2 + C * t + b0 = D * t + l0 # # Let's start with the start point (t = 0). We get # # b0 = l0 # # if we cancel out all the t factors (since t = 0) # # So let's put that back in to the equation: # # A * t^3 + B * t^2 + C * t + b0 = D * t + b0 # # Now let's look at the end point (t = 1). We get # # A + B + C + b0 = D + b0 # # Cancel the b0's and you see that D = A + B + C. # # go back to the line equation and you'll see that # # l(1) = A + B + C + l0 => b(1) = A + B + C + b0 # # So, b(1) = A + B + C + b0 is one equation we can use to solve # the cubic. Two more to go... # # To find the slope of the tangent line of the curve we just need to take # its derivative: # # b'(t) = 3 * A * t^2 + 2 * B * t + C # # We're only interested in the tangent lines at the start point # and end point. Given a slope and a point we can form a line # using our general equation of a line from earlier: # # l(u) = D * u + l0 # # D is the slope of the line, l0 is a point. So our line is going to # be something like: # # l(t, u) = (3 * A * t^2 + 2 * B * t + C) * u + l0 # # We care about the start point, b(0), and the first control point, c1. # # Note the only point on the line that is also on the curve is point b(0), # and also note that the slope of the line is constant for all points on # the line. That means that t must be 0. If we simplify the equation with # this knowledge and define the line in terms of of the control point then # we get: # # l(u) = C * u + c1 # # Then l(0) = c1 and l(1) = b(0): # # C + c1 = b0, or c1 = b0 - C, so that's equation 2. one more to go... # # j(t, u) = (3 * A * t^2 + 2 * B * t + C) * u + l0 # # We care about the end point, b(1), and the second control point, c2. # # t must be 1 for the entirety of the line, so: # # j(u) = (3 * A + 2 * B + C) * u + c2 # # Note, j(1) = b(1), so: # # (3 * A + 2 * B + C) + c2 = C + b0 # # If we simplify then we get: # # c2 = C + b0 - (3 * A + 2 * B + C) => # c2 = C + b0 - 3 * A - 2 * B - C => # c2 = b0 - 3 * A - 2 * B # # and that's our third equation, so in summary, we have: # # b(t) = A * t^3 + B * t^2 + C * t + b0 # # b1 = A + B + C + b0 # c1 = b0 - C # c2 = b0 - 3 * A - 2 * B # # So now we have three equations, 4 knowns (the points), and # three unknowns (the coefficients from the original cubic # equation). We just need to solve those unknowns... # # Let's start with the middle one because it's easy: # # C = b0 - c1 # # and now the top one: # # b1 = A + B + (b0 - c1) + b0 => # b1 = A + B + 2 * b0 - c1 => # b0 = .5 * (c1 - A - B) + b1 => # # b0 = -.5 * A + -.5 * B + .5 * c1 + b1 # # not solved, yet but if we look at the last of our three equations, # we'll find: # # c2 = b0 - 3 * A - 2 * B => # b0 = 3 * A + 2 * B + c2 # # and now we can stack, multiply one equation by a constant to # eliminate a term and then add... # # # (b0 = 3 * A + 2 * B + c2) # 6 * b0 = - 3 * A + - 3 * B + 3 * c1 + 6 * b1 # ---------------------------------------------------- # 7 * b0 = -B + 3 * c1 + 6 * b1 + c2 # # and we can solve for B... # # B = 3 * c1 + 6 * b1 + c2 - 7 * b0 # # Now we know B and we know C, so finding A shouldn't be too hard. # We just go back to one of our 3 solved equations and substitute: # # b1 = A + B + C + b0 => # b1 = A + (3 * c1 + 6 * b1 + c2 - 7 * b0) + (b0 - c1) + b0 => # A = b1 - (3 * c1 + 6 * b1 + c2 - 7 * b0) - (b0 - c1) - b0 => # A = b1 - 3 * c1 - 6 * b1 - c2 + 7 * b0 - b0 + c1 - b0 => # A = (b1 - 6 * b1) + (c1 - 3 * c1) + (7 * b0 - b0 - b0) - c2 => # A = - 5 * b1 - 2 * c1 + 5 * b0 - c2 # # and that's it. We just have to substite A, B, and C back into the # cubic equation to find out bezier curve... # # Note most sites say the three equations from earlier are: # p1 = p0 + C / 3 # p2 = p1 + (C + B) / 3 # p3 = p0 + C + B + A # I don't know how they derived p1 and p2. (x0, y0), (x1, y1), (x2, y2), (x3, y3) = path at some point it must have been p1 = p0 + (C * A) / (3 * A) cx = 3 * (x1 - x0) bx = 3 * (x2 - x1) - cx ax = x3 - x0 - cx - bx cy = 3 * (y1 - y0) by = 3 * (y2 - y1) - cy ay = y3 - y0 - cy - by x = ax * (time ** 3) + bx * (time ** 2) + cx * time + x0 y = ay * (time ** 3) + by * (time ** 2) + cy * time + y0 # b0 c1 c2 b1 #(x0, y0), (x1, y1), (x2, y2), (x3, y3) = path #Ax = -5 * x3 - 2 * x1 + 5 * x0 - x2 #Bx = 3 * x1 + 6 * x3 + x2 - 7 * x0 #Cx = x0 - x1 #Ay = -5 * y3 - 2 * y1 + 5 * y0 - y2 #By = 3 * y1 + 6 * y3 + y2 - 7 * y0 #Cy = y0 - y1 #x = Ax * (time ** 3) + Bx * (time ** 2) + Cx * time + x0 #y = Ay * (time ** 3) + By * (time ** 2) + Cy * time + y0 print "(%d, %d)\n" % (x, y) return [x, y] class Floater(ScreenSaverItem): _SIZE = 128 def __init__(self, position=[.5, .5], speed=[10.0, 10.0], acceleration=[0.0, 0.0], scale=1.0): ScreenSaverItem.__init__(self) self.cached_pixbufs = {} self.start_position = position[:] self.position = self.start_position[:] self.scale = scale self.opacity = 1.0 self.speed = speed[:] self.acceleration = acceleration[:] self.start_time = time.time() def update_state(self, time): ScreenSaverItem.update_state(self, time) #self.position[0] = .5 * self.acceleration[1] * time * time + self.speed[0] * time + self.start_position[0] #self.position[1] = .5 * self.acceleration[1] * time * time + self.speed[1] * time + self.start_position[1] self.position = self.find_position_on_path([(self.start_position[0], self.start_position[1]), (350, 0), (650, 0), (self.start_position[0], self.start_position[1])], time) self.opacity = self.scale def paint(self, context): ScreenSaverItem.paint(self, context) size = int(self._SIZE * self.scale) if self.cached_pixbufs.has_key(size): pixbuf = self.cached_pixbufs[size] else: pixbuf = gdk.pixbuf_new_from_file_at_size(sys.argv[1], size, -1) self.cached_pixbufs[size] = pixbuf context.save() context.translate(*self.position) context.set_source_pixbuf(pixbuf, 0.0, 0.0) context.rectangle (0.0, 0.0, pixbuf.get_width(), pixbuf.get_height()) context.clip() context.paint_with_alpha(self.opacity) context.restore() class ScreenSaverCanvas(gtk.DrawingArea): __gsignals__ = { 'expose-event' : 'override', 'size-allocate': 'override'} # speed is in pixels per second _MAX_FLOATER_SPEED = 128.0 def __init__(self): gtk.DrawingArea.__init__(self) self.updates_per_second = 30 self.first_update_time = None self.last_update_time = None self.current_update_time = None self.first_frame_time = None self.last_frame_time = None self.current_frame_time = None self.timeout_id = None self.items = [] for state in range(len(self.style.bg)): self.modify_bg(state, self.style.dark[state]) def create_initial_floaters(self): self.items = [] for i in range(10): position = [random.random() * self.allocation.width + self.allocation.width / 2, random.random() * self.allocation.height + self.allocation.width / 2] speed = [random.random() * self._MAX_FLOATER_SPEED - self._MAX_FLOATER_SPEED / 2, random.random() * self._MAX_FLOATER_SPEED - self._MAX_FLOATER_SPEED / 2] scale = random.random() * 1.0 item = Floater(position=position, speed=speed, scale=scale) self.items.append(item) def start(self): def print_stats(*args): print "updates per second: %f, frames per second: %f" % (self.get_updates_per_second(), self.get_frames_per_second()) return True self.timeout_id = gobject.timeout_add(1000 / self.updates_per_second, self.do_update_state) gobject.timeout_add(2000, print_stats) def stop(self): gobject.source_remove(self.timeout_id) self.timeout_id = None self.items = [] def get_updates_per_second(self): if self.last_update_time != None and \ self.current_update_time != None: return 1 / (self.current_update_time - self.last_update_time) return 0.0 def get_frames_per_second(self): if self.last_frame_time != None and \ self.current_frame_time != None: return 1 / (self.current_frame_time - self.last_frame_time) return 0.0 def do_update_state(self): self.last_update_time = self.current_update_time if self.first_update_time == None: self.first_update_time = time.time() self.current_update_time = self.first_update_time else: self.current_update_time = time.time() for item in self.items: item.update_state(self.current_update_time - self.first_update_time) if self.window != None: self.window.invalidate_rect((0, 0, self.allocation.width, self.allocation.height), True) return True def do_size_allocate(self, allocation): self.chain(allocation) self.create_initial_floaters() def do_expose_event(self, event): self.chain(event) self.last_frame_time = self.current_frame_time if self.first_frame_time == None: self.first_frame_time = time.time() self.current_frame_time = self.first_frame_time else: self.current_frame_time = time.time() context = self.window.cairo_create() context.rectangle (0.0, 0.0, self.allocation.width, self.allocation.height) context.clip() for item in self.items: try: item.paint(context) except KeyboardInterrupt: gtk.main_quit() window = gtk.Window() window.set_title('Floaters') window.connect('delete-event', gtk.main_quit) window.fullscreen() window.realize() window.set_default_size(window.get_screen().get_width(), window.get_screen().get_height()) canvas = ScreenSaverCanvas() canvas.start() canvas.show() window.add(canvas) window.show() gtk.main()