1. NodeBox 1
    1. Homepage
    2. NodeBox 3Node-based app for generative design and data visualization
    3. NodeBox OpenGLHardware-accelerated cross-platform graphics library
    4. NodeBox 1Generate 2D visuals using Python code (Mac OS X only)
  2. Gallery
  3. Documentation
  4. Forum
  5. Blog

PyObjC

NodeBox is built on PyObjC, a bridge between the Python and Objective-C programming languages. The primary usage of PyObjC is writing Cocoa applications for Mac OS X in pure python. Cocoa is the programming environment on Mac OS X that allows developers to create graphical applications with a typical Apple-look-and-feel.

Cocoa's two main libraries (Foundation Kit and Application Kit) are bundled with NodeBox. If you know a little PyObjC it's easy to start playing around with all the magic in Mac OS X, right from your NodeBox script.

This tutorial contains advanced material.

 


AppKit manual

The first thing you'll notice about AppKit is that it has a lot of long and odd objects all beginning with NS: NSBezierPath, NSRect, NSImage, and so on (the NS stems from the fact that AppKit is a direct descendant of NeXTSTEP, Steve Jobs' technology that revived Apple).

Commands like layoutManager.drawGlyphsForGlyphRange_atPoint_(glyphRange, (x-dx,y-dy-self.font.defaultLineHeightForFont())) are common. So the first thing to do is get a manual. I recommend using AppKiDo. It requires that you've installed Xcode, the Apple developer tools. Usually you'll find these on the installation CD that came with your Mac.

 

pyobjc-appkido

 


AppKit rules of thumb

AppKiDo lists all of the objects in their Objective-C form. To use them in Python, a good rule of thumb is to replace the colon with a dash (e.g. setShadowBlurRadius:(float)val becomes setShadowBlurRadius_(floatval) ).

Another good thing to know is that you create AppKit objects with object.alloc() and then initialise them with object.init() or object.initWithARangeOfParamaters().

Also note that some things that are in Foundation like NSSize or NSRect can usually be substituted by simple Python tuples.

To start using AppKit in NodeBox simply import the library:

from Appkit import *

I'll show you some examples of what you can do with AppKit.

 


Playing sounds in NodeBox

The NSSound object in AppKit provides a very easy way to play AIFF and WAV sound files in Mac applications. The class below is a NodeBox wrapper for NSSound.

from AppKit import NSSound
 
class sound:
    def __init__(self, file):
        self._sound = NSSound.alloc()
        self._sound.initWithContentsOfFile_byReference_(file, True)
    def play(self): self._sound.play()
    def stop(self): self._sound.stop()
    def is_playing(self): return self._sound.isPlaying()

As you can see our sound() command takes a file parameter (that is the location of your sound file) and returns an object with a number of properties:

  • sound.play(): start playing the sound
  • sound.stop(): stop playing
  • sound.is_playing(): returns True when playing
woof = sound("dog.aiff")
woof.play()

The following class defines a simple sound mixer/timeline. It has a number of channels that play sounds at a defined time.

from time import time
 
class mixer:   
    def __init__(self, channels=4):
        self.channels = [[] for i in range(channels)]
        self.start = time()
        self.playing = []
    def queue(self, channel, time, file):
        self.channels[channel].append( (time, sound(file)) )
        self.channels[channel].sort()
    def play(self):
        now = time() - self.start
        for ch in self.channels:
            if len(ch) > 0 and ch[0][0] < now:
                self.playing.append(ch[0][1])
                ch[0][1].play()
                del ch[0]
    def stop(self):
        for sound in self.playing:
            sound.stop()
        self.playing = []
        self.channels = [[] for ch in self.channels]

Queueing multiple sounds is now very easy:

m = mixer(2)
m.queue(0, 0.0, "woof.aiff")
m.queue(0, 0.4, "woof.aiff")
m.queue(0, 0.8, "woof.aiff")
m.queue(0, 1.2, "woof.aiff")
m.queue(1, 0.4, "meow.aiff")
m.queue(1, 1.2, "meow.aiff")

 


BabbleBox: speech synthesis in NodeBox

The example below wraps the NSSpeechSynthesizer in two NodeBox commands. The voices() command returns a list of all available voices. The say() command makes NodeBox speak out a sentence. The optional voice parameter sets the voice you want to use.

from AppKit import NSSpeechSynthesizer
 
def voices():
    voices = NSSpeechSynthesizer.availableVoices()
    voices = [x.split(".")[-1] for x in voices]
    return voices
    
def say(txt, voice=None):
    if voice in voices():
        voice = "com.apple.speech.synthesis.voice."+voice
    else:
        voice = NSSpeechSynthesizer.defaultVoice()
    speech = NSSpeechSynthesizer.alloc().initWithVoice_(voice)
    speech.startSpeakingString_(txt)

Now say hello in a random voice:

say("hello", voice=choice(voices()))


All the fonts on your system

The command below wraps the NSFontManager object. It returns a list with the PostScript name of each font installed on your system. Thanks to Mark for this one.

from AppKit import NSFontManager
 
def fonts():
    return NSFontManager.sharedFontManager().availableFonts()

Now you can do lots of fun typography:

background(0.15, 0.1, 0.1)
 
x, y, h = 0, 0, 0
lineheight(1)
fontsize(14)
for f in fonts()[:250]:
 
    font(f)
 
    # Random pink, blue or white color 
    fill(random(), random(0.5), 0.75)
    if random() > 0.8: fill(1)
    
    # Wrap text to the next line 
    if x + textwidth(f) > WIDTH:
        x = 0
        y += h
        h = 0
 
    text(f, x, y)
 
    # Line height is equal to biggest font 
    h = max(h, textheight(f))
    x += textwidth(f)

pyobjc-fonts

 


Dashed lines

Obviously all of the commands in NodeBox are wrappers to AppKit calls. In most cases you can still manipulate them directly with PyObjC. For example, a BezierPath object has a path property containing the actual NSBezierPath. This we can manipulate to create some custom effects.

def linedash(path, segment=10, gap=5):
    path.path.setLineDash_count_phase_([segment, gap], 2, 0)
    return path
p = line(0, 0, 200, 200, draw=False)
linedash(p, segment=5)
stroke(0)
drawpath(p)

pyobjc-linedash

 


Dropshadows in NodeBox

Here I create a wrapper for NSShadow. The result are two commands, shadow() and noshadow() that work like fill() and nofill(): every drawing command defined after shadow() will have a dropshadow until you call noshadow().

Note that the shadow class inherits from something called Grob. This is short for Graphic Object. A Grob executes its _draw() method the moment it is drawn. You need to initialise it with the current context (canvas) to draw in, that's why we pass the global _ctx to Grob.__init__(). The _ctx contains the current context.

This is a typical example of what NodeBox drawing commands look like (also see the source).

from AppKit import NSShadow, NSColor
from nodebox.graphics import Grob
 
class shadow(Grob):
    def __init__(self, x=10, y=10, alpha=0.25, blur=4.0):
        Grob.__init__(self, _ctx)
        self._shadow = NSShadow.alloc().init()
        self._shadow.setShadowOffset_((x, -y))
        self._shadow.setShadowColor_(color(0, 0, 0, alpha)._rgb)
        self._shadow.setShadowBlurRadius_(blur)
        self.draw()
    def _draw(self):
        self._shadow.set()
        
def noshadow():
    shadow(alpha=0)

Shadows work very well to create a feeling of depth:

shadow(blur=10.0)
 
for i in range(30):
    fill(random(0.75), 0, random(0.25,0.75))
    r = random(50)
    oval(random(WIDTH), random(HEIGHT), r, r)

 

pybobjc-shadow

 

pyobjc-shadow2