def pack(circles, x, y, padding=2, exclude=[]): """ Circle-packing algorithm. Groups the given list of Circle objects around (x,y) in an organic way. """ # Ported from Sean McCullough's Processing code: # http://www.cricketschirping.com/processing/CirclePacking1/ # See also: http://en.wiki.mcneel.com/default.aspx/McNeel/2DCirclePacking # Repulsive force: move away from intersecting circles. for i, circle1 in enumerate(circles): for circle2 in circles[i + 1:]: d = distance(circle1.x, circle1.y, circle2.x, circle2.y) r = circle1.radius + circle2.radius + padding if d < r - 0.01: dx = circle2.x - circle1.x dy = circle2.y - circle1.y vx = (dx / d) * (r - d) * 0.5 vy = (dy / d) * (r - d) * 0.5 if circle1 not in exclude: circle1.x -= vx circle1.y -= vy if circle2 not in exclude: circle2.x += vx circle2.y += vy # Attractive force: move all circles to center. for circle in circles: circle.goal.x = x circle.goal.y = y if circle not in exclude: damping = circle.radius**3 * 0.000001 # Big ones in the middle. vx = (circle.x - x) * damping vy = (circle.y - y) * damping circle.x -= vx circle.y -= vy
def pack(circles, x, y, padding=2, exclude=[]): """ Circle-packing algorithm. Groups the given list of Circle objects around (x,y) in an organic way. """ # Ported from Sean McCullough's Processing code: # http://www.cricketschirping.com/processing/CirclePacking1/ # See also: http://en.wiki.mcneel.com/default.aspx/McNeel/2DCirclePacking # Repulsive force: move away from intersecting circles. for i, circle1 in enumerate(circles): for circle2 in circles[i+1:]: d = distance(circle1.x, circle1.y, circle2.x, circle2.y) r = circle1.radius + circle2.radius + padding if d < r - 0.01: dx = circle2.x - circle1.x dy = circle2.y - circle1.y vx = (dx / d) * (r-d) * 0.5 vy = (dy / d) * (r-d) * 0.5 if circle1 not in exclude: circle1.x -= vx circle1.y -= vy if circle2 not in exclude: circle2.x += vx circle2.y += vy # Attractive force: move all circles to center. for circle in circles: circle.goal.x = x circle.goal.y = y if circle not in exclude: damping = circle.radius ** 3 * 0.000001 # Big ones in the middle. vx = (circle.x - x) * damping vy = (circle.y - y) * damping circle.x -= vx circle.y -= vy
def spider(string, x=0, y=0, radius=25, **kwargs): """ A path filter that creates web threading along the characters of the given string. Its output can be drawn directly to the canvas or used in a render() function. Adapted from: http://nodebox.net/code/index.php/Path_Filters """ # **kwargs represents any additional optional parameters. # For example: spider("hello", 100, 100, font="Helvetica") => # kwargs = {"font": "Helvetica"} # We pass these on to the textpath() call in the function; # so the spider() function takes the same parameters as textpath: # x, y, font, fontsize, fontweight, ... font( kwargs.get("font", "Droid Sans"), kwargs.get("fontsize", 100)) p = textpath(string, x, y, **kwargs) n = int(p.length) m = 2.0 radius = max(radius, 0.1 * fontsize()) points = list(p.points(n)) for i in range(n): pt1 = choice(points) pt2 = choice(points) while distance(pt1.x, pt1.y, pt2.x, pt2.y) > radius: pt2 = choice(points) line(pt1.x + random(-m, m), pt1.y + random(-m, m), pt2.x + random(-m, m), pt2.y + random(-m, m))
def draw_mesh(self, points): strokewidth(0.1) stroke(1, 0, 0.4, 0.6) fill(1, 0, 0.4, 0.3) for i, (p1, dx1, dy1, a1) in enumerate(points): # Draw feeler. line(p1.x, p1.y, p1.x+dx1, p1.y+dy1) ellipse(p1.x+dx1, p1.y+dy1, 1.5, 1.5) ellipse(p1.x, p1.y, 1, 1) stroke(0.8,0.9,1, 0.1) for i, (p1, dx1, dy1, a1) in enumerate(points): # Draw connection to nearest-neighbor particle. nn, d0 = None, None for p2, dx2, dy2, a2 in points: d = distance(p1.x, p1.y, p2.x, p2.y) if p1 != p2 and (d0 is None or d < d0): nn, d0 = p2, d if nn is not None: line(p1.x, p1.y, nn.x, nn.y) nostroke()
def draw_mesh(self, points): strokewidth(0.1) stroke(1, 0, 0.4, 0.6) fill(1, 0, 0.4, 0.3) for i, (p1, dx1, dy1, a1) in enumerate(points): # Draw feeler. line(p1.x, p1.y, p1.x + dx1, p1.y + dy1) ellipse(p1.x + dx1, p1.y + dy1, 1.5, 1.5) ellipse(p1.x, p1.y, 1, 1) stroke(0.8, 0.9, 1, 0.1) for i, (p1, dx1, dy1, a1) in enumerate(points): # Draw connection to nearest-neighbor particle. nn, d0 = None, None for p2, dx2, dy2, a2 in points: d = distance(p1.x, p1.y, p2.x, p2.y) if p1 != p2 and (d0 is None or d < d0): nn, d0 = p2, d if nn is not None: line(p1.x, p1.y, nn.x, nn.y) nostroke()
def update(self): """ Attractor roams around and sucks in particles """ Particle.update(self) # Attractor wants to be in the center of the canvas. # This urge increases as its gravity (i.e., number of attached particles) increases. vx = self.x - canvas.width/2 vy = self.y - canvas.height/2 f = 0.0015 * self.gravity**2 self.x -= vx * f self.y -= vy * f # Attractive force: move all particles to attractor. for p in self.particles: #p.v.angle = 0#angle(p.x, p.y, self.x, self.y) # Point to attractor. f = p.radius * 0.004 vx = (p.x - self.x) * f vy = (p.y - self.y) * f p.v.x = -vx p.v.y = -vy # Repulsive force: move away from intersecting particles. for i, p1 in enumerate(self.particles): for p2 in self.particles[i+1:] + [self]: d = distance(p1.x, p1.y, p2.x, p2.y) r = p1.radius + p2.radius f = 0.15 if d < r - 0.01: dx = p2.x - p1.x dy = p2.y - p1.y vx = (dx / d) * (r-d) * f vy = (dy / d) * (r-d) * f if p1 != self: p1.v.x -= vx p1.v.y -= vy if p2 != self: p2.v.x += vx p2.v.y += vy
def update(self): """ Attractor roams around and sucks in particles """ Particle.update(self) # Attractor wants to be in the center of the canvas. # This urge increases as its gravity (i.e., number of attached particles) increases. vx = self.x - canvas.width / 2 vy = self.y - canvas.height / 2 f = 0.0015 * self.gravity**2 self.x -= vx * f self.y -= vy * f # Attractive force: move all particles to attractor. for p in self.particles: #p.v.angle = 0#angle(p.x, p.y, self.x, self.y) # Point to attractor. f = p.radius * 0.004 vx = (p.x - self.x) * f vy = (p.y - self.y) * f p.v.x = -vx p.v.y = -vy # Repulsive force: move away from intersecting particles. for i, p1 in enumerate(self.particles): for p2 in self.particles[i + 1:] + [self]: d = distance(p1.x, p1.y, p2.x, p2.y) r = p1.radius + p2.radius f = 0.15 if d < r - 0.01: dx = p2.x - p1.x dy = p2.y - p1.y vx = (dx / d) * (r - d) * f vy = (dy / d) * (r - d) * f if p1 != self: p1.v.x -= vx p1.v.y -= vy if p2 != self: p2.v.x += vx p2.v.y += vy
def contains(self, x, y): return distance(self.x, self.y, x, y) <= self.radius
def draw(canvas): global headset global dimmer global images global samples global particles global attractor global ZOOM, ATTRACT, SPAWN, DIM, delay global MUTE glEnable(GL_DITHER) background(0) #image(abspath("g","bg.png"), 0, 0, width=canvas.width, height=canvas.height) image(abspath("g","bg-light.png"), 0, 0, width=canvas.width, height=canvas.height, alpha=0.9) if canvas.key.code == SPACE: MUTE = not MUTE # Poll the headset. # Is alpha above average? => attraction. # Is valence above average? => spawn feelies. headset.update(buffer=1024) ATTRACT = False ATTRACT = delay > 0 ATTRACT = ATTRACT or SHIFT in canvas.key.modifiers if canvas.key.code == SHIFT: ATTRACT = True if len(headset.alpha[0]) > 0 and headset.alpha[0][-1][0] > headset.alpha[0][-1][1] * 1.0: ATTRACT = True delay = 10 # Delay before repulsing to counter small alpha fluctuation. elif delay > 0: delay -= 1 SPAWN = False SPAWN = CTRL in canvas.key.modifiers if canvas.key.code == CTRL: SPAWN = True if len(headset.valence) > 0 and headset.valence[-1][0] > headset.valence[-1][1]: SPAWN = True # In mute mode, ignore triggering alpha and valence. if MUTE: ATTRACT = SPAWN = False delay = 0 # Dimmer sends a value over UDP that drops to 0 when relaxed. # It can be used to dim ambient lighting using a domotica module. m = 0.0025 if ATTRACT: DIM = clamp(DIM-m, 0.0, 1.0) else: DIM = clamp(DIM+m, 0.0, 1.0) if DIM < 0.8 and dimmer is not None: dimmer.send("%.2f" % (DIM * 100)) # Valence controls the balance between high and low ambient. v = headset.valence.slope # -1.0 => +1.0 v = 0.0 dx = 1.0 - v dy = 1.0 + v # Mouse changes the volume of low and high ambient sound. #dx = canvas.mouse.relative_x #dy = canvas.mouse.relative_y samples["ambient_lo"].play(volume=0.7 * dx) samples["ambient_hi"].play(volume=0.7 * dy) if canvas.key.code == ALT: text("%.2f FPS" % canvas.profiler.framerate, canvas.width-80, 15, align=RIGHT, fill=[1,1,1,0.75]) if canvas.frame / 20 % 2 == 0: fill(1,1,1, 0.75) fontsize(9) if len(headset.alpha[0]) > 0: ellipse(canvas.width-18, 19.5, 7, 7, fill=[1,1,1,1]) if ATTRACT or SPAWN: ellipse(15, 19.5, 7, 7, fill=[1,0,0,1]) if ATTRACT and SPAWN: text(" RELAXATION + AROUSAL", 20, 15) elif ATTRACT: text(" RELAXATION", 20, 15) elif SPAWN: text(" AROUSAL", 20, 15) elif MUTE: text(" READY", 20, 15) # Zoom out as the attractor grows larger. # Integrate the zoom scale to make the transition smoother. d = (1.25 - len(attractor.particles) * 0.05) if ZOOM > -0.15 and ZOOM > d: ZOOM -= 0.0025 if ZOOM < +1.25 and ZOOM < d: ZOOM += 0.0025 dx = 0.5 * ZOOM * canvas.width dy = 0.5 * ZOOM * canvas.height translate(-dx, -dy) scale(1.0 + ZOOM) for p in list(particles): d = distance(p.x, p.y, attractor.x, attractor.y) t = d / canvas.width * 2 p.update() # When valence is low, unattached feelie particles fade away. if SPAWN is False: if p.parent is None and p.type == FEELIE: p.alpha -= 0.04 p.alpha = max(p.alpha, 0) if p.alpha == 0: # Remove hidden feelies, so we have a chance to see new ones. particles.remove(p) # Check if a particle falls within the attraction radius: # If so, attract it when alpha is above average. if ATTRACT is True: if p.parent is None and p.frames >= 0 and p.alpha >= 0.25: if d < min(210, p.radius + attractor.radius * attractor.gravity): attractor.append(p) samples["attract"].play().volume = 0.75 p.draw(blur=t, alpha=(1-t)) # Repulse when alpha drops below average. # Press mouse to repulse attracted particles. if ATTRACT is False: if random() > 0.5: if len(attractor.particles) > 0: attractor.remove(attractor.particles[0]) samples["repulse"].play() # When valence is high, feelie particles appear. if SPAWN is True: if random() > 0.5: if len(particles) < 80: p = Particle(x = choice((-30, canvas.width+30)), y = -30, image = choice([images["flower%i.png"%i] for i in range(2,6+1)], bias=0.25), radius = 15 + random(20), bounds = (-65, -65, canvas.width+65, canvas.height+65), speed = 3.5, type = FEELIE) if p.image._src[0].endswith("flower3.png"): p.radius = 20 + random(20) if p.image._src[0].endswith("flower4.png"): p.radius = 15 + random(10) if p.image._src[0].endswith("flower5.png"): p.radius = 15 if p.image._src[0].endswith("flower6.png"): p.radius = 10 + random(5) particles.append(p) attractor.update() attractor.draw_halo() attractor.draw()
def draw(canvas): global headset global dimmer global images global samples global particles global attractor global ZOOM, ATTRACT, SPAWN, DIM, delay global MUTE glEnable(GL_DITHER) background(0) #image(abspath("g","bg.png"), 0, 0, width=canvas.width, height=canvas.height) image(abspath("g", "bg-light.png"), 0, 0, width=canvas.width, height=canvas.height, alpha=0.9) if canvas.key.code == SPACE: MUTE = not MUTE # Poll the headset. # Is alpha above average? => attraction. # Is valence above average? => spawn feelies. headset.update(buffer=1024) ATTRACT = False ATTRACT = delay > 0 ATTRACT = ATTRACT or SHIFT in canvas.key.modifiers if canvas.key.code == SHIFT: ATTRACT = True if len(headset.alpha[0] ) > 0 and headset.alpha[0][-1][0] > headset.alpha[0][-1][1] * 1.0: ATTRACT = True delay = 10 # Delay before repulsing to counter small alpha fluctuation. elif delay > 0: delay -= 1 SPAWN = False SPAWN = CTRL in canvas.key.modifiers if canvas.key.code == CTRL: SPAWN = True if len(headset.valence ) > 0 and headset.valence[-1][0] > headset.valence[-1][1]: SPAWN = True # In mute mode, ignore triggering alpha and valence. if MUTE: ATTRACT = SPAWN = False delay = 0 # Dimmer sends a value over UDP that drops to 0 when relaxed. # It can be used to dim ambient lighting using a domotica module. m = 0.0025 if ATTRACT: DIM = clamp(DIM - m, 0.0, 1.0) else: DIM = clamp(DIM + m, 0.0, 1.0) if DIM < 0.8 and dimmer is not None: dimmer.send("%.2f" % (DIM * 100)) # Valence controls the balance between high and low ambient. v = headset.valence.slope # -1.0 => +1.0 v = 0.0 dx = 1.0 - v dy = 1.0 + v # Mouse changes the volume of low and high ambient sound. #dx = canvas.mouse.relative_x #dy = canvas.mouse.relative_y samples["ambient_lo"].play(volume=0.7 * dx) samples["ambient_hi"].play(volume=0.7 * dy) if canvas.key.code == ALT: text("%.2f FPS" % canvas.profiler.framerate, canvas.width - 80, 15, align=RIGHT, fill=[1, 1, 1, 0.75]) if canvas.frame / 20 % 2 == 0: fill(1, 1, 1, 0.75) fontsize(9) if len(headset.alpha[0]) > 0: ellipse(canvas.width - 18, 19.5, 7, 7, fill=[1, 1, 1, 1]) if ATTRACT or SPAWN: ellipse(15, 19.5, 7, 7, fill=[1, 0, 0, 1]) if ATTRACT and SPAWN: text(" RELAXATION + AROUSAL", 20, 15) elif ATTRACT: text(" RELAXATION", 20, 15) elif SPAWN: text(" AROUSAL", 20, 15) elif MUTE: text(" READY", 20, 15) # Zoom out as the attractor grows larger. # Integrate the zoom scale to make the transition smoother. d = (1.25 - len(attractor.particles) * 0.05) if ZOOM > -0.15 and ZOOM > d: ZOOM -= 0.0025 if ZOOM < +1.25 and ZOOM < d: ZOOM += 0.0025 dx = 0.5 * ZOOM * canvas.width dy = 0.5 * ZOOM * canvas.height translate(-dx, -dy) scale(1.0 + ZOOM) for p in list(particles): d = distance(p.x, p.y, attractor.x, attractor.y) t = d / canvas.width * 2 p.update() # When valence is low, unattached feelie particles fade away. if SPAWN is False: if p.parent is None and p.type == FEELIE: p.alpha -= 0.04 p.alpha = max(p.alpha, 0) if p.alpha == 0: # Remove hidden feelies, so we have a chance to see new ones. particles.remove(p) # Check if a particle falls within the attraction radius: # If so, attract it when alpha is above average. if ATTRACT is True: if p.parent is None and p.frames >= 0 and p.alpha >= 0.25: if d < min(210, p.radius + attractor.radius * attractor.gravity): attractor.append(p) samples["attract"].play().volume = 0.75 p.draw(blur=t, alpha=(1 - t)) # Repulse when alpha drops below average. # Press mouse to repulse attracted particles. if ATTRACT is False: if random() > 0.5: if len(attractor.particles) > 0: attractor.remove(attractor.particles[0]) samples["repulse"].play() # When valence is high, feelie particles appear. if SPAWN is True: if random() > 0.5: if len(particles) < 80: p = Particle( x=choice((-30, canvas.width + 30)), y=-30, image=choice( [images["flower%i.png" % i] for i in range(2, 6 + 1)], bias=0.25), radius=15 + random(20), bounds=(-65, -65, canvas.width + 65, canvas.height + 65), speed=3.5, type=FEELIE) if p.image._src[0].endswith("flower3.png"): p.radius = 20 + random(20) if p.image._src[0].endswith("flower4.png"): p.radius = 15 + random(10) if p.image._src[0].endswith("flower5.png"): p.radius = 15 if p.image._src[0].endswith("flower6.png"): p.radius = 10 + random(5) particles.append(p) attractor.update() attractor.draw_halo() attractor.draw()