├── .gitignore ├── CHANGES.md ├── DrawingPrimitives.py ├── LICENSE ├── NodeBox-Info.plist ├── NodeBox.xcodeproj └── project.pbxproj ├── NodeBox_Prefix.pch ├── README.md ├── Resources ├── English.lproj │ ├── AskString.xib │ ├── Credits.rtf │ ├── ExportImageAccessory.xib │ ├── ExportMovieAccessory.xib │ ├── InfoPlist.strings │ ├── MainMenu.xib │ ├── NodeBoxDocument.xib │ ├── NodeBoxPreferences.xib │ └── ProgressBarSheet.xib ├── NodeBox.icns ├── NodeBoxFile.icns ├── zoombig.png └── zoomsmall.png ├── art ├── help-start.psd ├── nodeboxfile.psd ├── nodeboxicon.psd ├── nodeboxlogo.pdf ├── nodeboxlogo.py ├── type.txt ├── version.psd └── zoomicons.psd ├── buildext.py ├── class-diagram.graffle ├── examples ├── Advanced │ ├── Clipping.py │ ├── Feedback.py │ ├── Sorting.py │ └── SwissCheese.py ├── Animation │ ├── Hypnoval.py │ ├── Parade.py │ └── WishyWorm.py ├── Content │ ├── AutoText.py │ ├── insults.xml │ ├── kant.xml │ └── thanks.xml ├── Grid │ ├── Balls.py │ ├── ColorGrid.py │ ├── Foliage.py │ ├── FormFunction.py │ └── Nauseating.py ├── Images │ ├── ImageGrid.py │ └── nodeboxicon.png ├── Interactivity │ ├── Avoider.py │ └── Drawing.py ├── Math │ ├── HowCurvesWork.py │ ├── MathSculpture.py │ └── TunnelEffect.py ├── Path │ ├── BitBop.py │ └── Spider.py ├── Primitives │ ├── AppendingTransforms.py │ ├── CoverGen.py │ ├── Ovals.py │ └── StarFun.py └── Text │ ├── Copyrights.py │ ├── GridGrid.py │ ├── NumberTunnel.py │ ├── OrganicBall.py │ ├── RandomFont.py │ ├── RandomText.py │ └── TextFromList.py ├── libs ├── buildlibs.py ├── cGeo │ ├── cGeo.c │ └── setup.py ├── pathmatics │ ├── pathmatics.c │ ├── pathmatics.py │ ├── pathmatics_test.py │ └── setup.py └── polymagic │ ├── gpc.c │ ├── gpc.h │ ├── polymagic.m │ ├── polymagic_test.py │ └── setup.py ├── macboot.py ├── main.m ├── nodebox ├── Testing.txt ├── __init__.py ├── console.py ├── geo │ ├── __init__.py │ └── pathmatics.py ├── graphics │ ├── __init__.py │ ├── bezier.py │ └── cocoa.py ├── gui │ ├── __init__.py │ └── mac │ │ ├── AskString.py │ │ ├── PyDETextView.py │ │ ├── ValueLadder.py │ │ ├── __init__.py │ │ ├── dashboard.py │ │ ├── preferences.py │ │ ├── progressbar.py │ │ └── util.py ├── macboot.py ├── macbuild.sh ├── macsetup.py ├── setup.py ├── tests │ ├── graphics │ │ ├── 001-basic-primitives.png │ │ ├── 001-basic-primitives.py │ │ ├── 001-basic-primitives.result.png │ │ ├── 002-text.png │ │ ├── 002-text.py │ │ ├── 003-color.png │ │ ├── 003-color.py │ │ ├── 004-state.png │ │ ├── 004-state.py │ │ ├── 005-path.png │ │ ├── 005-path.py │ │ ├── 006-text.png │ │ ├── 006-text.py │ │ ├── __init__.py │ │ ├── runtests.py │ │ └── tests.py │ └── vdiff │ │ ├── __init__.py │ │ ├── images │ │ ├── 001-added-square │ │ │ ├── bluesquare.png │ │ │ └── original.png │ │ ├── 002-antialiased-text │ │ │ ├── cocoa.png │ │ │ └── photoshop.png │ │ ├── 003-movement │ │ │ ├── moved.png │ │ │ └── original.png │ │ ├── 004-color │ │ │ ├── darker.png │ │ │ └── original.png │ │ ├── 005-antialiased-text │ │ │ ├── none.png │ │ │ └── smooth.png │ │ ├── 006-totally-different │ │ │ ├── ant.png │ │ │ └── people.png │ │ ├── 007-black-white │ │ │ ├── black.png │ │ │ └── white.png │ │ └── vdiff.css │ │ └── tests.py └── util │ ├── PyFontify.py │ ├── QTSupport.py │ ├── __init__.py │ ├── kgp.py │ ├── ottobot.py │ └── vdiff.py ├── screenshots └── nodebox-191.png └── tests ├── colortest.py ├── compliance.py ├── graphics.py ├── nodeboxblack.py └── numberslider.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | build/ 4 | dist/ 5 | 6 | # Xcode user files 7 | project.xcworkspace 8 | xcuserdata -------------------------------------------------------------------------------- /DrawingPrimitives.py: -------------------------------------------------------------------------------- 1 | # This file is obsolete. 2 | # NodeBox now uses a package structure. 3 | # The drawing primitives are now in the new "nodebox.graphics" package. 4 | # This will also ensure you get the graphics package for the correct platform. 5 | 6 | import nodebox.graphics.cocoa 7 | from nodebox.graphics.cocoa import * 8 | from nodebox.util import * 9 | 10 | __all__ = nodebox.graphics.cocoa.__all__ 11 | 12 | import warnings 13 | warnings.warn('DrawingPrimitives is deprecated. Please use "from nodebox import graphics"', DeprecationWarning, stacklevel=2) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Frederik De Bleser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /NodeBox-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleDocumentTypes 8 | 9 | 10 | CFBundleTypeExtensions 11 | 12 | py 13 | 14 | CFBundleTypeIconFile 15 | NodeBoxFile.icns 16 | CFBundleTypeName 17 | DocumentType 18 | CFBundleTypeOSTypes 19 | 20 | ???? 21 | 22 | CFBundleTypeRole 23 | Editor 24 | NSDocumentClass 25 | NodeBoxDocument 26 | 27 | 28 | CFBundleExecutable 29 | ${EXECUTABLE_NAME} 30 | CFBundleIconFile 31 | NodeBox.icns 32 | CFBundleIdentifier 33 | net.nodebox.NodeBox 34 | CFBundleInfoDictionaryVersion 35 | 6.0 36 | CFBundleName 37 | ${PRODUCT_NAME} 38 | CFBundlePackageType 39 | APPL 40 | CFBundleShortVersionString 41 | 1.9.7rc2 42 | CFBundleSignature 43 | NdBx 44 | CFBundleURLTypes 45 | 46 | CFBundleVersion 47 | 1 48 | LSApplicationCategoryType 49 | public.app-category.graphics-design 50 | LSMinimumSystemVersion 51 | 10.5 52 | NSMainNibFile 53 | MainMenu 54 | NSPrincipalClass 55 | NSApplication 56 | NSServices 57 | 58 | UTExportedTypeDeclarations 59 | 60 | UTImportedTypeDeclarations 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /NodeBox_Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header for all source files of the 'NodeBox' target in the 'NodeBox' project 3 | // 4 | 5 | #ifdef __OBJC__ 6 | #import 7 | #endif 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NodeBox 2 | ======= 3 | NodeBox is an application used in graphic design research. It provides 4 | an interactive Python environment where you can create two-dimensional 5 | graphics. NodeBox scripts can create PDFs or QuickTime movies that can 6 | contain anything from simple geometrical shapes to fully-fledged bitmaps, 7 | vector images and text. 8 | 9 | NodeBox is mostly meant to design and explore generative design and 10 | animation. It features several ways to manipulate parameters inside 11 | of a program: it contains an interface builder and an on-the-fly value 12 | changing gizmo called the throttle. 13 | 14 | http://nodebox.net/ 15 | 16 | Credits 17 | ------- 18 | NodeBox itself is written by Frederik De Bleser. (frederik@burocrazy.com) 19 | The NodeBox manual is written by Tom De Smedt. (tomdesmedt@organisms.be) 20 | 21 | NodeBox is a fork of DrawBot (http://drawbot.com) by Just van Rossum (just@letterror.com), 22 | which is released under a MIT-style license. 23 | 24 | Contributing 25 | ------------ 26 | The NodeBox source is available on GitHub: 27 | 28 | http://github.com/nodebox/nodebox-pyobjc 29 | -------------------------------------------------------------------------------- /Resources/English.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 2 | {\fonttbl\f0\fswiss\fcharset0 ArialMT;} 3 | {\colortbl;\red255\green255\blue255;\red26\green26\blue26;\red255\green255\blue255;\red16\green60\blue192; 4 | \red62\green0\blue63;} 5 | \margl1440\margr1440\vieww9000\viewh9000\viewkind0 6 | \deftab720 7 | \pard\pardeftab720 8 | 9 | \f0\fs26 \cf2 \cb3 NodeBox\'a0is copyright 2004-2012 Experimental Media Research Group, Antwerp, Belgium. \'96 {\field{\*\fldinst{HYPERLINK "http://www.emrg.be/"}}{\fldrslt \cf4 \ul \ulc4 http://www.emrg.be/}}\ 10 | \pard\pardeftab720 11 | \cf5 NodeBox\'a0is maintained by Frederik De Bleser ({\field{\*\fldinst{HYPERLINK "mailto:frederik@emrg.be"}}{\fldrslt \cf4 \ul \ulc4 frederik@emrg.be}}) and Tom De Smedt ({\field{\*\fldinst{HYPERLINK "mailto:tom@emrg.be"}}{\fldrslt \cf4 \ul \ulc4 tom@emrg.be}}).\ 12 | \ 13 | \pard\pardeftab720 14 | 15 | \b \cf2 Authors 16 | \b0 \ 17 | Frederik De Bleser ({\field{\*\fldinst{HYPERLINK "mailto:frederik@emrg.be"}}{\fldrslt \cf4 \ul \ulc4 frederik@emrg.be}}), application, GUI, graphics engine\ 18 | \pard\pardeftab720 19 | \cf5 Tom\'a0De Smedt ({\field{\*\fldinst{HYPERLINK "mailto:tom@emrg.be"}}{\fldrslt \cf4 tom\ul @emrg.be}}), documentation & examples, Bezier math\ 20 | \ 21 | \pard\pardeftab720 22 | 23 | \b \cf2 Contributors (alphabetical) 24 | \b0 \ 25 | Alan Murta\ 26 | Florimond De Smedt\ 27 | Giorgio Olivero\ 28 | Lieven Menschaert\ 29 | Lucas Nijs\ 30 | \pard\pardeftab720 31 | \cf5 \ 32 | \pard\pardeftab720 33 | 34 | \i \cf5 NodeBox\'a0is based on DrawBot v0.9a by Just van Rossum ({\field{\*\fldinst{HYPERLINK "mailto:just@letterror.com"}}{\fldrslt \cf4 \ul \ulc4 just@letterror.com}}) 35 | \i0 \ 36 | 37 | \i \ 38 | \pard\pardeftab720 39 | {\field{\*\fldinst{HYPERLINK "http://nodebox.net/code/"}}{\fldrslt 40 | \i0 \cf4 \ul \ulc4 http://nodebox.net/code/}}} -------------------------------------------------------------------------------- /Resources/English.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Resources/NodeBox.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/Resources/NodeBox.icns -------------------------------------------------------------------------------- /Resources/NodeBoxFile.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/Resources/NodeBoxFile.icns -------------------------------------------------------------------------------- /Resources/zoombig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/Resources/zoombig.png -------------------------------------------------------------------------------- /Resources/zoomsmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/Resources/zoomsmall.png -------------------------------------------------------------------------------- /art/help-start.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/help-start.psd -------------------------------------------------------------------------------- /art/nodeboxfile.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/nodeboxfile.psd -------------------------------------------------------------------------------- /art/nodeboxicon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/nodeboxicon.psd -------------------------------------------------------------------------------- /art/nodeboxlogo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/nodeboxlogo.pdf -------------------------------------------------------------------------------- /art/nodeboxlogo.py: -------------------------------------------------------------------------------- 1 | size(100,100) 2 | 3 | def bar(x, y, w, depth, filled=1.0): 4 | 5 | d1 = depth*filled 6 | 7 | colormode(HSB) 8 | f = fill() 9 | s = stroke() 10 | if f != None and f.brightness != 1: 11 | s = color(f.hue, f.saturation+0.2, f.brightness-0.4) 12 | nostroke() 13 | 14 | #front 15 | if f != None: fill(f) 16 | rect(x, y, w, w) 17 | 18 | #bottom 19 | beginpath(x, y+w) 20 | lineto(x-d1, y+w+d1) 21 | lineto(x-d1+w, y+w+d1) 22 | lineto(x+w, y+w) 23 | endpath() 24 | 25 | #left 26 | beginpath(x, y) 27 | lineto(x-d1, y+d1) 28 | lineto(x-d1, y+w+d1) 29 | lineto(x, y+w) 30 | endpath() 31 | 32 | #top 33 | if f != None: fill(f.hue, f.saturation-0, f.brightness-0.15) 34 | beginpath(x, y) 35 | lineto(x+w, y) 36 | lineto(x+w-d1, y+d1) 37 | lineto(x-d1, y+d1) 38 | endpath() 39 | 40 | #right 41 | if f != None: fill(f.hue, f.saturation-0, f.brightness-0.15) 42 | beginpath(x+w, y) 43 | lineto(x+w-d1, y+d1) 44 | lineto(x+w-d1, y+w+d1) 45 | lineto(x+w, y+w) 46 | endpath() 47 | 48 | if s != None: stroke(s) 49 | 50 | line(x, y, x+w, y) 51 | line(x, y, x-d1, y+d1) 52 | line(x+w, y, x+w, y+w) 53 | line(x+w, y+w, x+w-d1, y+w+d1) 54 | line(x, y+w, x-d1, y+w+d1) 55 | line(x+w, y, x+w-d1, y+d1) 56 | 57 | #front 58 | if f != None: fill(f) 59 | rect(x-d1, y+d1, w, w) 60 | 61 | x += d1 62 | y += d1 63 | d2 = depth*(1-filled) 64 | 65 | if d2 != 0: 66 | 67 | line(x, y, x+d2, y+d2) 68 | line(x+w, y, x+w+d2, y+d2) 69 | line(x+w, y+w, x+w+d2, y+w+d2) 70 | line(x, y+w, x+d2, y+w+d2) 71 | 72 | f = fill() 73 | nofill() 74 | rect(x+d2, y+d2, w, w) 75 | 76 | if f != None: fill(f) 77 | 78 | def cube(x, y, w, filled=1.0): 79 | bar(x, y, w, w*0.5, filled) 80 | 81 | from random import seed 82 | seed(55) 83 | w = 20 84 | n = 3 85 | strokewidth(0.5) 86 | 87 | colormode(RGB) 88 | c = color(0.05,0.65,0.85) 89 | c.brightness += 0.2 90 | 91 | for x in range(n): 92 | 93 | for y in range(n): 94 | 95 | bottom = w * n 96 | 97 | for z in range(n): 98 | stroke(0.1) 99 | strokewidth(1) 100 | 101 | colormode(RGB) 102 | dr = (1-c.r)/(n-1) * (x*0.85+y*0.15+z*0.05) * 1.1 103 | dg = (1-c.g)/(n-1) * (x*0.85+y*0.15+z*0.05) * 1.2 104 | db = (1-c.b)/(n-1) * (x*0.85+y*0.15+z*0.05) * 1.1 105 | fill(1.2-dr, 1.1-dg, 1.2-db) 106 | 107 | if random() > 0.5: 108 | nostroke() 109 | nofill() 110 | 111 | dx = w*x - w/2*z 112 | dy = bottom-w*y + w/2*z 113 | 114 | transform(CORNER) 115 | translate(33,-17) 116 | scale(1.01) 117 | cube(dx, dy, w) 118 | reset() -------------------------------------------------------------------------------- /art/type.txt: -------------------------------------------------------------------------------- 1 | Type is set in Dolly-Roman. 2 | Navigation is 13pt, strong anti-aliasing with an extra layer copy at 70%. 3 | Same for topics. 4 | Components are 12pt, sharp anti-aliasing with an extra layer copy at 70%. 5 | Reference text is 16pt, sharp anti-aliasing with an extra layer copy at 70%. 6 | -------------------------------------------------------------------------------- /art/version.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/version.psd -------------------------------------------------------------------------------- /art/zoomicons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/art/zoomicons.psd -------------------------------------------------------------------------------- /buildext.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import subprocess 4 | from distutils.core import run_setup 5 | from shutil import copyfile 6 | 7 | # Extensions will be copied in the nodebox ext source folder. 8 | dstdir = 'nodebox/ext' 9 | 10 | # Setup creates the needed extensions 11 | run_setup('setup.py', ['build_ext']) 12 | 13 | # These extensions are stored in a folder that looks like build/lib.macosx-10.6-universal-2.6 14 | # Find all extensions from that folder. 15 | extensions = glob.glob("build/lib*/*.so") 16 | 17 | # Copy all found extensions to the nodebox ext source folder. 18 | for src in extensions: 19 | dst = os.path.join(dstdir, os.path.basename(src)) 20 | print src, '-->', dst 21 | copyfile(src, dst) 22 | 23 | -------------------------------------------------------------------------------- /class-diagram.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/class-diagram.graffle -------------------------------------------------------------------------------- /examples/Advanced/Clipping.py: -------------------------------------------------------------------------------- 1 | size(300,300) 2 | # Examples of clipping in NodeBox. 3 | # NodeBox can clip to any path using the beginclip() and endclip() commands. 4 | # The beginclip can take any path, be it one of the drawing primitives or text 5 | # (using the textpath command demonstrated below) 6 | 7 | # ---- PART 1: Clipping Primitives ---- # 8 | 9 | # Transformation commands can have an effect on the clipped path, the things within 10 | # the clipped path, or both. If you only want to rotate the clipped path 11 | # (and not subsequent draws), make sure you surround the command that generates the path 12 | # (in this case, the arrow) with push() and pop(). 13 | push() 14 | # Rotate the arrow to let it point down. 15 | rotate(-90) 16 | # The last parameter instructs the command not to draw the output, but return it as a path. 17 | p = arrow(250, 100, 200, draw=False) 18 | pop() 19 | 20 | # The path is an object that has several useful methods. We use the bounds method to 21 | # get the bounding box of the arrow, used for defining where we should draw the ovals. 22 | (x,y), (w,h) = p.bounds 23 | 24 | # Begin the clipping operation. 25 | # All drawing methods inside of the beginclip() and endclip() are clipped according 26 | # to the given path. 27 | beginclip(p) 28 | # Draw 200 ovals of a random size. 29 | for i in range(200): 30 | fill(random(), 0.7, 0, random()) 31 | s = random(50) 32 | oval(x+random(w), y+random(h), s, s) 33 | endclip() 34 | 35 | # ---- PART 2: Clipping Text ---- # 36 | # There is no fundamental difference in clipping text or primitives: 37 | # all you have to do is to use the textpath command to return the path. 38 | # Note that you CANNOT use text("", x, y, draw=False). 39 | 40 | # Set the font. Note that all state operations work as expected for the textpath 41 | # command, such as font, align, and giving a width. 42 | font('Lucida Grande Bold', 72) 43 | align(CENTER) 44 | # Return a path that can be used for clipping. Textpaths never draw on screen. 45 | p = textpath("Clippy!", 0, 270, width=300) 46 | 47 | # The path is an object that has several useful methods. We use the bounds method to 48 | # get the bounding box of the arrow, used for defining where we should draw the ovals. 49 | (x,y), (w,h) = p.bounds 50 | # Begin the clipping operation. 51 | # All drawing methods inside of the beginclip() and endclip() are clipped according 52 | # to the given path. 53 | beginclip(p) 54 | # Draw 200 rectangles of a random size. 55 | for i in range(105): 56 | fill(random(), random(), 0, random()) 57 | s = random(50) 58 | rotate(random(5, -5)) 59 | rect(x+random(w), y+random(h), s, s) 60 | endclip() 61 | -------------------------------------------------------------------------------- /examples/Advanced/Feedback.py: -------------------------------------------------------------------------------- 1 | # NodeBox allows you to query the current state of the canvas, and even 2 | # the contents of it as an image. This allows for some nice feed-back effects. 3 | size(500, 500) 4 | 5 | # Currently, we use a background, so all content gets replaced. 6 | # Enabling the next line to get a transparent background. 7 | # background(None) 8 | 9 | # Draw some ovals on screen, just to get something to display. 10 | for i in range(1000): 11 | oval(random(WIDTH), random(HEIGHT), 20, 20, fill=(random(), random(), 0, random())) 12 | 13 | # Now do the feedback ten times. 14 | for i in range(10): 15 | # Scale each copy down a bit, and rotate it. This gives the familiar feedback effect. 16 | # Play with these parameters to get other effects. 17 | scale(0.98) 18 | rotate(1) 19 | # This is the trick. We don't use an image from file, but we load image data from the canvas. 20 | # The canvas is a variable set in the local namespace: it has a private _nsImage property 21 | # that renders its current contents as an NSImage, which the image command accepts. 22 | # Try blurring the image: add alpha=0.8 23 | image(None, random(-50,49), random(-50,49), image=canvas._nsImage) 24 | -------------------------------------------------------------------------------- /examples/Advanced/Sorting.py: -------------------------------------------------------------------------------- 1 | # This example shows one of the possibilities of the canvas: 2 | # the ability to access all existing objects on it. 3 | size(550, 300) 4 | 5 | fill(1, 0.8) 6 | strokewidth(1.5) 7 | 8 | # First, generate some rectangles all over the canvas, rotated randomly. 9 | for i in range(3000): 10 | grob = rect(random(WIDTH)-25, random(HEIGHT)-25,50, 50) 11 | grob.rotate(random(360)) 12 | 13 | # Now comes the smart part: 14 | # We want to sort the objects on the canvas using a custom sorting 15 | # method; in this case, their vertical position. 16 | # The bounds property returns the following: ( (x, y), (width, height) ) 17 | # The lambda function thus compares the y positions against eachother. 18 | sorted_grobs = list(canvas) 19 | sorted_grobs.sort(lambda v1, v2: cmp(v1.bounds[0][1], v2.bounds[0][1])) 20 | 21 | # Now that we have all the graphic objects ("grobs") sorted, 22 | # traverse them in order and change their properties. 23 | 24 | # t is a counter going from 0.0 to 1.0 25 | t = 0.0 26 | # d is the delta amount added each step 27 | d = 1.0 / len(sorted_grobs) 28 | 29 | for grob in sorted_grobs: 30 | # Grobs will get bigger 31 | grob.scale(t) 32 | # Grobs's stroke will get darker 33 | grob.stroke = (0.6 - t, 0.50) 34 | t += d 35 | 36 | # This is really a hack, but you can replace the internal 37 | # grob list of the canvas with your own to change the Z-ordering. 38 | # Comment out this line to get a different effect. 39 | canvas._grobs = sorted_grobs -------------------------------------------------------------------------------- /examples/Advanced/SwissCheese.py: -------------------------------------------------------------------------------- 1 | # Swiss Cheese Factory 2 | # This program makes swiss cheese by cutting out circles from another 3 | # circle. It demonstrates the use of boolean operations on paths. 4 | 5 | size(300, 300) 6 | background(0.0, 0.0, 0.15) 7 | 8 | # We define a custom circle function that draws from the middle. 9 | # This is easier to work with in this case than the regular oval 10 | # function. 11 | def circle(x, y, sz, draw=True): 12 | return oval(x-sz, y-sz, sz*2, sz*2, draw) 13 | 14 | # Now create the "cheese" -- the central circle in the middle. 15 | # Don't draw it yet, since we will be creating a new path were 16 | # pieces are cut out off. 17 | cheese = circle(WIDTH/2, HEIGHT/2, 130, draw=False) 18 | # Instead of the circle, you can use any arbitrary path. 19 | # Try out the following lines to use a letter. 20 | #font("Helvetica-Bold", 400) 21 | #cheese = textpath("S", 7, 291) 22 | 23 | # Do ten punches out of the cheese 24 | for i in range(10): 25 | # Make a circle somewhere on the canvas, with a random size. 26 | x = random(WIDTH) 27 | y = random(HEIGHT) 28 | sz = random(10, 60) 29 | c = circle(x, y, sz, draw=False) 30 | # Now comes the central part: create a new path out of the cheese 31 | # that is the difference between the cheese and the newly created 32 | # circle. Save this in cheese (so the old cheese gets overwritten. 33 | cheese = cheese.difference(c, 0.1) 34 | # Another cool effect is to mirror the cutouts. Here, we create a 35 | # mirrored copy of the circle, then cut it out of the cheese 36 | # as well. (You might want to change the number of punchouts.) 37 | #mx = WIDTH - x 38 | #c = circle(mx, y, sz, draw=False) 39 | #cheese = cheese.difference(c, 0.1) 40 | 41 | # Now that the cheese is ready, draw it. 42 | 43 | # Here's a little caveat: you can only draw an object once. 44 | # Here, we create a copy of the cheese to use as a shadow object. 45 | shadow_cheese = BezierPath(cheese) 46 | fill(0.1) 47 | drawpath(shadow_cheese) 48 | 49 | # Once the shadow is drawn, translate the original cheese and 50 | # draw it as well. 51 | translate(-3, -3) 52 | fill(0.3, 0.9, 0.1, 0.8) 53 | drawpath(cheese) 54 | -------------------------------------------------------------------------------- /examples/Animation/Hypnoval.py: -------------------------------------------------------------------------------- 1 | # A simple animation example. 2 | # The hypnotating ovals use a little bit of math to 3 | # make smooth animations, notably sinus and cosinus functions. 4 | # NodeBox knows a script is an animation when it calls the "speed" method. 5 | # Animation scripts always contain: 6 | # - a setup method that is run once, at the start of the animation 7 | # - a draw method that is run for every frame. 8 | # Variables that you want to use in your script should be declared global. 9 | 10 | # Because we use some math functions, we import sinus and cosinus functions 11 | # from the math library. 12 | from math import sin, cos 13 | 14 | size(300,300) 15 | 16 | # This method indicates this script will be an animation. 17 | speed(30) 18 | 19 | # The setup method is called once, at the start of the animation. 20 | # Here, it initializes the counter. 21 | def setup(): 22 | global cnt 23 | cnt = 0.0 24 | 25 | # The draw method is called for every frame. 26 | # Here, it draws the oval grid. 27 | def draw(): 28 | global cnt 29 | # We use an internal counter that modifies each 30 | # oval slightly 31 | s = 0.0 32 | # Move the canvas a bit. 33 | translate(29,40) 34 | # Draw a grid of 5 by 5. 35 | for x, y in grid(5,5,45,42): 36 | # Oscillate the fill color. 37 | fill(0, 0, sin(cnt+s*5.0)/2.0) 38 | # Draw the oval. 39 | oval(x + sin(cnt+s)*10.0, y + cos(cnt+s)*-6.0, 41.0, 36.0) 40 | # Increase the counter so that every oval looks a bit different. 41 | s += 0.05 42 | # Increase the global counter. 43 | cnt += 0.19 44 | -------------------------------------------------------------------------------- /examples/Animation/Parade.py: -------------------------------------------------------------------------------- 1 | # The parade! 2 | # 3 | # This example shows object-oriented design in animation for 4 | # defining a set of "actors" (the balls) that parade on stage. 5 | 6 | speed(30) 7 | size(400,646) 8 | 9 | # Import random and geometry functions 10 | from random import seed 11 | from math import sin,cos 12 | 13 | # Define our own circle method (NodeBox doesn't have one) 14 | # that draws from the center. 15 | def circle(x, y, size): 16 | oval(x-size/2, y-size/2, size, size) 17 | 18 | # The main actor in the animation is a Ball. 19 | # A Ball has a set of state values: its position, size, color and delta-values. 20 | # The delta-values affect the position and size, and are a simple way to give 21 | # each ball "character". Higher delta-values make the ball more hectic. 22 | class Ball: 23 | # Initialize a ball -- set all the values to their defaults. 24 | def __init__(self): 25 | self.x = random(WIDTH) 26 | self.y = random(HEIGHT) 27 | self.size = random(10, 72) 28 | self.dx = self.dy = self.ds = 0.0 29 | self.color = color(random(), 1, random(0,2), random()) 30 | 31 | # Update the internal state values. 32 | def update(self): 33 | self.dx = sin(FRAME/float(random(1,100))) * 20.0 34 | self.dy = cos(FRAME/float(random(1,100))) * 20.0 35 | self.ds = cos(FRAME/float(random(1,123))) * 10.0 36 | 37 | # Draw a ball: set the fill color first and draw a circle. 38 | def draw(self): 39 | fill(self.color) 40 | circle(self.x + self.dx, self.y + self.dy, self.size + self.ds) 41 | 42 | # Initialize the animation by instantiating a list of balls. 43 | def setup(): 44 | global balls 45 | balls = [] 46 | for i in range(30): 47 | balls.append(Ball()) 48 | 49 | # Draw the animation by updating and drawing each individual ball. 50 | def draw(): 51 | global balls 52 | seed(1) 53 | # This translate command makes the ball move up on the screen. 54 | translate(0, HEIGHT-FRAME) 55 | for ball in balls: 56 | ball.update() 57 | ball.draw() 58 | -------------------------------------------------------------------------------- /examples/Animation/WishyWorm.py: -------------------------------------------------------------------------------- 1 | size(400, 400) 2 | speed(100) 3 | # Generates sculptures using a set of mathematical functions. 4 | # Every iteration adds a certain value to the current coordinates. 5 | # Rewriting this program to use transforms is left as an exercise 6 | # for the reader. 7 | 8 | # This program uses some mathematical functions that are not 9 | # standard functions of NodeBox. 10 | # Instead, they are in Python's math library. The next 11 | # line imports those functions. 12 | from math import sin, cos, tan, log10 13 | from random import seed 14 | 15 | def setup(): 16 | global a,b 17 | a = 10.0 18 | b = 0.0 19 | pass 20 | 21 | def draw(): 22 | global a,b 23 | seed(0) 24 | 25 | background(0,0,0.15) 26 | cX = a 27 | cY = b 28 | 29 | x = 180 30 | y = -27 31 | fontsize(54) 32 | c = 0.0 33 | for i in range(48): 34 | x += cos(cY)*5 35 | y += log10(cX)*8.36 + sin(cX) * 2 36 | 37 | fill(sin(a+c), 0.3, 0.0, 0.5) 38 | 39 | s = 22 + cos(cX)*17 40 | oval(x-s/2, y-s/2, s, s) 41 | # Try the next line instead of the previous one to see how 42 | # you can use other primitives. 43 | #star(x-s/2,y-s/2, random(5,10), inner=2+s*0.1,outer=10+s*0.1) 44 | 45 | cX += random(0.25) 46 | cY += random(0.25) 47 | c += 0.1 48 | a += 0.1 49 | b += 0.05 -------------------------------------------------------------------------------- /examples/Content/AutoText.py: -------------------------------------------------------------------------------- 1 | # Automatically generates text based on the Kant Generator Pro. 2 | # The Kant Generator Pro is an example script of the 3 | # "Dive Into Python" manual. 4 | 5 | size(400, 1000) 6 | 7 | # Generate automatic text from an XML file and store it as a string 8 | # in the txt variable. Also try the "thanks.xml", which generates 9 | # "thank you" notes, and "insults.xml", which generates all sorts 10 | # of crazy insults. 11 | txt = autotext("kant.xml") 12 | 13 | font("Times", 12) 14 | lineheight(1.7) 15 | 16 | text(txt, 25, 30, width=320, height=950) -------------------------------------------------------------------------------- /examples/Content/insults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 |

dribbling

11 |

snivelling

12 |

braindead

13 |

tentacled

14 |

three eyed

15 |

one dimensional

16 |

borg loving

17 |

slime sucking

18 |

borg sniffing

19 |

bug eyed

20 |

single celled

21 |

scruffy looking

22 |
23 | 24 |

son of a

25 |

clone of a

26 |

excuse for a

27 |

hologram of a

28 |

apology for a

29 |

pimp for a

30 |
31 | 32 |

mutant

33 |

parasitic

34 |

vat-grown

35 |

Ferengi

36 |

radiation damaged

37 |

deranged

38 |

space sick

39 |

warp sick

40 |

deviant

41 |

clockwork

42 |
43 | 44 |

star goat

45 |

space weevil

46 |

toilet cleaning droid

47 |

bilge spore

48 |

nose worm

49 |

hyper slug

50 |

replicant

51 |

android

52 |

garbage droid

53 |

cyborg

54 |

pleasure droid

55 |

nerf herder

56 |
57 | 58 | 59 |

artless

60 |

bawdy

61 |

beslubbering

62 |

bootless

63 |

churlish

64 |

cockered

65 |

clouted

66 |

craven

67 |

currish

68 |

dankish

69 |

dissembling

70 |

droning

71 |

errant

72 |

fawning

73 |

fobbing

74 |

froward

75 |

frothy

76 |

gleeking

77 |

goatish

78 |

gorbellied

79 |

impertinent

80 |

infectious

81 |

jarring

82 |

loggerheaded

83 |

lumpish

84 |

mammering

85 |

mangled

86 |

mewling

87 |

paunchy

88 |

pribbling

89 |

puking

90 |

puny

91 |

qualling

92 |

rank

93 |

reeky

94 |

roguish

95 |

ruttish

96 |

saucy

97 |

spleeny

98 |

spongy

99 |

surly

100 |

tottering

101 |

unmuzzled

102 |

vain

103 |

venomed

104 |

villainous

105 |

warped

106 |

wayward

107 |

weedy

108 |

yeasty

109 |
110 | 111 |

base-court

112 |

bat-fowling

113 |

beef-witted

114 |

beetle-headed

115 |

boil-brained

116 |

clapper-clawed

117 |

clay-brained

118 |

common-kissing

119 |

crook-pated

120 |

dismal-dreaming

121 |

dizzy-eyed

122 |

doghearted

123 |

dread-bolted

124 |

earth-vexing

125 |

elf-skinned

126 |

fat-kidneyed

127 |

fen-sucked

128 |

flap-mouthed

129 |

fly-bitten

130 |

folly-fallen

131 |

fool-born

132 |

full-gorged

133 |

guts-griping

134 |

half-faced

135 |

hasty-witted

136 |

hedge-born

137 |

hell-hated

138 |

idle-headed

139 |

ill-breeding

140 |

ill-nurtured

141 |

knotty-pated

142 |

milk-livered

143 |

motley-minded

144 |

onion-eyed

145 |

plume-plucked

146 |

pottle-deep

147 |

pox-marked

148 |

reeling-ripe

149 |

rough-hewn

150 |

rude-growing

151 |

rump-fed

152 |

shard-borne

153 |

sheep-biting

154 |

spur-galled

155 |

swag-bellied

156 |

tardy-gaited

157 |

tickle-brained

158 |

toad-spotted

159 |

unchin-snouted

160 |

weather-bitten

161 |
162 | 163 |

apple-john

164 |

baggage

165 |

barnacle

166 |

bladder

167 |

boar-pig

168 |

bugbear

169 |

bum-bailey

170 |

canker-blossom

171 |

clack-dish

172 |

clotpole

173 |

coxcomb

174 |

codpiece

175 |

death-token

176 |

dewberry

177 |

flap-dragon

178 |

flax-wench

179 |

flirt-gill

180 |

foot-licker

181 |

fustilarian

182 |

giglet

183 |

gudgeon

184 |

haggard

185 |

harpy

186 |

hedge-pig

187 |

horn-beast

188 |

hugger-mugger

189 |

joithead

190 |

lewdster

191 |

lout

192 |

maggot-pie

193 |

malt-worm

194 |

mammet

195 |

measle

196 |

minnow

197 |

miscreant

198 |

moldwarp

199 |

mumble-news

200 |

nut-hook

201 |

pigeon-egg

202 |

pignut

203 |

puttock

204 |

pumpion

205 |

ratsbane

206 |

scut

207 |

skainsmate

208 |

strumpet

209 |

varlot

210 |

vassal

211 |

whey-face

212 |

wagtail

213 |
214 | 215 | 216 | 217 |

back stabbing

218 |

bawdy

219 |

beer-bellied

220 |

bone-headed

221 |

conniving

222 |

cross eyed

223 |

disagreeable

224 |

knock-kneed

225 |

lily-livered

226 |

lard eatin

227 |

lop-jawed

228 |

pea-brained

229 |

pot-bellied

230 |

whimpy

231 |
232 | 233 |

aggravatin

234 |

awkward

235 |

back-woods

236 |

high falutin

237 |

impertinent

238 |

illiterate

239 |

low down

240 |

mealy-mouthed

241 |

shiftless

242 |

smart-mouth

243 |

worthless

244 |
245 | 246 |

bean pole

247 |

boob

248 |

brat

249 |

buzzard bait

250 |

cracker

251 |

clod hopper

252 |

dufus

253 |

hick

254 |

jerk

255 |

moron

256 |

mule

257 |

nimrod

258 |

plow boy

259 |

pole cat

260 |

red neck

261 |

rip snorter

262 |

river rat

263 |

sap sucker

264 |

skunk

265 |

slop wallower

266 |

swamp momma

267 |

trailer bum

268 |
269 | 270 | 271 |

ignorant

272 |

selfish

273 |

self-absorbed

274 |

prejudiced

275 |

stupid

276 |

inhuman

277 |

unfeeling

278 |

amoral

279 |

craven

280 |

close-minded

281 |

elitist

282 |

self-rightous

283 |

loud-mouth

284 |

bigoted

285 |
286 | 287 |

blind-to-reality

288 |

money-grubbing

289 |

Gingrich-suckling

290 |

holier-than-thou

291 |

party-line

292 |

thick-skulled

293 |

upper-class

294 |

flapping-mouthed

295 |

flag-worshipping

296 |

school-prayer-supporting

297 |

anti-choice

298 |

cash-worshipping

299 |

obstructing

300 |

bible-thumping

301 |

militaristic

302 |

selfish

303 |
304 | 305 |

moron

306 |

ditto-head

307 |

elephant-lover

308 |

pork-barrel

309 |

automaton

310 |

oppressor

311 |

animal-hater

312 |

industrialist

313 |

bourgoisie

314 |

redneck

315 |

dipshit

316 |

forest-cutter

317 |

Limbaugh-lover

318 |

fascist

319 |

bonehead

320 |

muckraker

321 |

retroeconomist

322 |
323 | 324 | 325 | 326 |

You !

327 |
328 | 329 | 330 |

You !

331 |
332 | 333 | 334 |

You !

335 |
336 | 337 | 338 |

You !

339 |
340 | 341 | 342 |

343 |

344 |

345 |

346 |
347 |
-------------------------------------------------------------------------------- /examples/Grid/Balls.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | # Use a grid to generate a bubble-like composition. 3 | # This example shows that a grid doesn't have to be rigid at all. 4 | # It's very easy to breake loose from the coordinates NodeBox 5 | # passes you, as is shown here. The trick is to add or subtract 6 | # something from the x and y values NodeBox passes on. Here, 7 | # we also use random sizes. 8 | 9 | # We use a little bit of math to define the fill colors. 10 | # Sinus and cosinus are not standard functions of NodeBox. 11 | # Instead, they are in Python's math library. The next 12 | # line imports those functions. 13 | from math import sin, cos 14 | 15 | gridSize = 40 16 | # Translate a bit to the right and a bit to the bottom to 17 | # create a margin. 18 | translate(100,100) 19 | 20 | startval = random() 21 | c = random() 22 | for x, y in grid(10,10, gridSize, gridSize): 23 | fill(sin(startval + y*x/100.0), cos(c), cos(c),random()) 24 | s = random()*gridSize 25 | oval(x, y,s, s) 26 | fill(cos(startval + y*x/100.0), cos(c), cos(c),random()) 27 | deltaX = (random()-0.5)*10 28 | deltaY = (random()-0.5)*10 29 | deltaS = (random()-0.5)*200 30 | oval(x+deltaX, y+deltaY,deltaS, deltaS) 31 | c += 0.01 -------------------------------------------------------------------------------- /examples/Grid/ColorGrid.py: -------------------------------------------------------------------------------- 1 | size(625, 625) 2 | # Create a color Grid. 3 | # This example also shows of the HSB color mode that allows 4 | # you to select colors more naturally, by specifying a hue, 5 | # saturation and brightness. 6 | 7 | colormode(HSB) 8 | 9 | # Set some initial values. You can and should play around with these. 10 | h = 0 11 | s = 0.5 12 | b = 0.9 13 | a = .5 14 | 15 | # Size is the size of one grid square. 16 | size = 50 17 | 18 | # Using the translate command, we can give the grid some margin. 19 | translate(50,50) 20 | 21 | # Create a grid with 10 rows and 10 columns. The width of the columns 22 | # and the height of the rows is defined in the 'size' variable. 23 | for x, y in grid(10, 10, size, size): 24 | # Increase the hue while choosing a random saturation. 25 | # Try experimenting here, like decreasing the brightness while 26 | # changing the alpha value etc. 27 | h+=.01 28 | s=random() 29 | 30 | # Set this to be the current fill color. 31 | fill(h, s, b, a) 32 | 33 | # Draw a rectangle that is one and a half times larger than the 34 | # grid size to get an overlap. 35 | rect(x, y, size*1.5, size*1.5) 36 | -------------------------------------------------------------------------------- /examples/Grid/Foliage.py: -------------------------------------------------------------------------------- 1 | size(700, 700) 2 | # A foliage generator! 3 | # The foliage are actually green stars with random 4 | # inner and outer radii and a random number of points. 5 | # They are skewed to make it look more random. 6 | 7 | translate(50,50) 8 | # By using HSB colormode, we can change the saturation and brightness 9 | # of the leaves to get more natural color variations. 10 | colormode(HSB) 11 | 12 | # Generate a 50 x 50 grid. Each row and column is 12 points wide. 13 | for x, y in grid(50,50,12,12): 14 | push() 15 | fill(0.3,random(),random(0.2,0.6),0.8) 16 | skew(random(-50,50)) 17 | star(x+random(-5,5),y+random(-5,5),random(10),random(1,40),15) 18 | pop() -------------------------------------------------------------------------------- /examples/Grid/FormFunction.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | for x, y in grid(30,30,20,20): 3 | if random() > 0.6: 4 | # Here, we choose between two functions: oval and rect. 5 | # The chosen function is stored in the 'form' variable, which 6 | # is then called on the next line. Note that both functions 7 | # should have the same parameters, and in the same order. 8 | form = choice((oval, rect)) 9 | form(x, y, 18,18) -------------------------------------------------------------------------------- /examples/Grid/Nauseating.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | # Nauseating grid of circles. 3 | # Each circle is randomly enlarged or shrinked a little bit, 4 | # so it looks like a really blown up raster image. 5 | # "What do you see?" 6 | 7 | # Create a grid of 20 by 20. Each row and column is 30 points. 8 | for x, y in grid(20,20,30,30): 9 | push() 10 | # Scale every element from 20% to 120% of its original size. 11 | scale(random(0.2,1.2)) 12 | # Draw an oval that is a little bit smaller than the row and 13 | # column width. 14 | oval(x,y,30,30) 15 | pop() 16 | -------------------------------------------------------------------------------- /examples/Images/ImageGrid.py: -------------------------------------------------------------------------------- 1 | size(400, 400) 2 | # Demonstrates some uses of the image command. 3 | 4 | # There are several ways to scale an image, 5 | # but by far the easiest is to provide a width 6 | # parameter, such as here. 7 | for x, y in grid(8, 8, 50, 50): 8 | image("nodeboxicon.png", x, y, width=50) 9 | 10 | # Overlay the grid of images with an alpha-transparent 11 | # rectangle. 12 | fill(0.1, 0.1, 0.75, 0.8) 13 | rect(0, 0, WIDTH, HEIGHT) 14 | 15 | # Show the main image. 16 | image("nodeboxicon.png", 100, 100) 17 | 18 | # Show the text below the image 19 | font("Georgia Bold", 39) 20 | fill(1, 1, 1) 21 | # We use center-alignment so we can change the 22 | # text without impacting its placement. 23 | # Note that for this to work, we need to give 24 | # a width as parameter for the text command. 25 | align(CENTER) 26 | text("NodeBox", 0, 345, width=400) -------------------------------------------------------------------------------- /examples/Images/nodeboxicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/examples/Images/nodeboxicon.png -------------------------------------------------------------------------------- /examples/Interactivity/Avoider.py: -------------------------------------------------------------------------------- 1 | # Avoider: the first NodeBox game! 2 | # The purpose of the game is pretty simple: 3 | # try to avoid the red blobs for as long as possible. 4 | 5 | # This example is a bit longer than the others, and features some geometry, 6 | # object-oriented programming, and interactivity. 7 | 8 | # You can change the size of the canvas to get a bigger or smaller playing field. 9 | size(300, 324) 10 | speed(30) 11 | 12 | # The height of the bar at the bottom displaying the current time. 13 | STATUS_BAR_HEIGHT = 12 14 | 15 | # The import statement imports some import functions needed for geometry calculations 16 | from math import pi, sqrt, sin, cos, asin, atan2 17 | from nodebox.geo import angle, coordinates, distance 18 | # The time import is used for calculating game time 19 | import time 20 | 21 | class Hero: 22 | """The hero of the game.""" 23 | 24 | def __init__(self, x, y): 25 | self.x, self.y = x, y 26 | self.speed = 5.0 27 | self.size = 5.0 28 | 29 | def draw(self): 30 | fill(0, 0, 0) 31 | oval(self.x-self.size, self.y-self.size, self.size*2, self.size*2) 32 | 33 | class Blob: 34 | """"The bad guys in the game. Avoid them! 35 | 36 | These are non-player characters, meaning they aren't controlled by the player directly. 37 | The update method contains their "brain".""" 38 | 39 | def __init__(self, x, y): 40 | self.x, self.y = x, y 41 | self.size = 5.0 42 | self.speed = random(0.5,0.8) 43 | self.seed = random() 44 | self.angle = 0.0 45 | 46 | def draw(self): 47 | # This drawing code draws the circle body and the "eye". 48 | # To do this, we use a translation to move the blob's position, 49 | # then draw using relative coordinates. 50 | push() 51 | sz = self.size # We use size a lot in this method -- store it 52 | # Move to the center of the blob 53 | translate(self.x+sz, self.y+sz) 54 | scale(sz) 55 | # Rotate the blob. You won't see this when drawing the first oval, 56 | # since it's round, but it affects x and y coordinates, so the 57 | # eye will point in the right direction 58 | rotate(-self.angle) 59 | # Draw the body 60 | fill(1,0,0) 61 | oval(-1.0, -1.0, 2.0, 2.0) 62 | # Draw the eye 63 | fill(0, 0 ,0) 64 | oval(0.2, -0.5, 1.0, 1.0) 65 | pop() 66 | 67 | def update(self, hero, blobs): 68 | # Increase and decrease the size based on the speed of the blob 69 | self.size = abs(sin(self.seed+FRAME/(5.0-self.speed*2.0)) * 2.0+self.seed) + 4.0 70 | # This code implements the chase behaviour of the blobs. 71 | # First, calculate the angle between ourselves and the hero 72 | self.angle = angle(self.x, self.y, hero.x, hero.y) 73 | # Then, move in that direction using the moving speed 74 | self.x, self.y = coordinates(self.x, self.y, self.speed, self.angle) 75 | # Calculate if I'm not bumping into another blob. If I am, calculate a new 76 | # jump to an empty spot on the board. 77 | for blob in blobs: 78 | if blob is not self and abs(distance(self.x, self.y, blob.x, blob.y)) < blob.size*2: 79 | self.x, self.y = random_spot_away_from_hero(hero) 80 | 81 | 82 | def random_spot_away_from_hero(hero, mindist = 20.0): 83 | """Calculate a random spot that is at least mindist away from the hero.""" 84 | dist = 0.0 85 | # We use a brute-force: while we have not found a good point, choose a random 86 | # point and calculate its distance. Rinse and repeat until a good point is found. 87 | while dist < mindist: 88 | x, y = random(WIDTH), random(HEIGHT) 89 | dist = distance(x, y, hero.x, hero.y) 90 | return x, y 91 | 92 | # The setup of the game. This initializes the positions of the hero and the blobs, 93 | # sets the begintime and various other constants. 94 | def setup(): 95 | global hero, blobs, gameover, starttime, endtime 96 | hero = Hero(100, 100) 97 | blobs = [] 98 | gameover = False 99 | endtime = None 100 | starttime = time.time() 101 | for i in range(10): 102 | x, y = random_spot_away_from_hero(hero) 103 | blobs.append(Blob(x, y)) 104 | 105 | # The main game loop 106 | def draw(): 107 | global hero, blobs, gameover, starttime, endtime 108 | 109 | # To make things a little more interesting, we rotate and scale the canvas while 110 | # the game is running. For this to work, we need corner-mode transformations. 111 | transform(CORNER) 112 | # Move to the middle of the screen to set the rotation. This makes sure the rotation 113 | # isn't applied from a corner, but from the middle of the screen. 114 | translate(WIDTH/2, HEIGHT/2) 115 | # The rotation amount and speed is linked to the current FRAME. The farther in the game, 116 | # the faster and bigger the rotation gets 117 | rotate(sin(FRAME/70.0)*FRAME/10.0) 118 | # The speed of the saling is also linked to the current FRAME. 119 | scale(0.6 + abs(sin(FRAME/100.0)*0.4)) 120 | # Move the canvas back. The rotation is now applied. 121 | translate(-WIDTH/2, -HEIGHT/2) 122 | 123 | # Draw a rectangle, defining the playing field 124 | stroke(0) 125 | nofill() 126 | rect(0,0,WIDTH,HEIGHT-STATUS_BAR_HEIGHT) 127 | nostroke() 128 | 129 | # The following functions apply when the game is not over, 130 | # in other words when we are still playing. 131 | if not gameover: 132 | # Check the keys and move the hero accordingly. 133 | # The min and max lines keep the hero within the bounds 134 | # of the playing field 135 | if keydown: 136 | if keycode == KEY_UP: 137 | hero.y -= hero.speed 138 | hero.y = max(hero.size, hero.y) 139 | if keycode == KEY_DOWN: 140 | hero.y += hero.speed 141 | hero.y = min(WIDTH-hero.size, hero.y) 142 | if keycode == KEY_LEFT: 143 | hero.x -= hero.speed 144 | hero.x = max(hero.size, hero.x) 145 | if keycode == KEY_RIGHT: 146 | hero.x += hero.speed 147 | hero.x = min(WIDTH-hero.size, hero.x) 148 | 149 | # Update the blobs. This part is the actual "intelligence" of the game. 150 | # This routine also calculates if one of the blobs hits your hero, in 151 | # which case the game is over. 152 | for blob in blobs: 153 | blob.update(hero, blobs) 154 | if abs(distance(hero.x, hero.y, blob.x, blob.y)) < blob.size + hero.size: 155 | gameover = True 156 | # The endtime stores how long we survived. 157 | endtime = time.time() 158 | 159 | # Draw everything. This is done even when the game is over. 160 | hero.draw() 161 | for blob in blobs: 162 | blob.draw() 163 | 164 | # The status indicators are drawn on-screen without all the funky rotations 165 | # and scaling. Reset the canvas. 166 | reset() 167 | 168 | # The time to display is either the endtime (on gameover), or the current time. 169 | if endtime is not None: 170 | t = endtime - starttime 171 | else: 172 | t = time.time()-starttime 173 | # Draw the time 174 | fontsize(12) 175 | fill(0,0.6) 176 | rect(0,HEIGHT-STATUS_BAR_HEIGHT, WIDTH, STATUS_BAR_HEIGHT) 177 | fill(1) 178 | text("%.2f seconds" % t, 5, HEIGHT-2) 179 | 180 | # If the game is over, scale up the hero to get a black screen 181 | # and draw the "GAME OVER" message 182 | if gameover: 183 | if hero.size < 500: 184 | hero.size += 30.0 185 | fill(1) 186 | text("GAME OVER", (WIDTH/2.0)-textwidth("game over")/2.0, HEIGHT/2) 187 | -------------------------------------------------------------------------------- /examples/Interactivity/Drawing.py: -------------------------------------------------------------------------------- 1 | # This example allows you to draw lines on screen. 2 | # It also transforms the line while you're drawing it. 3 | 4 | # Each time you move the mouse, a new point is stored in a list of points. 5 | # The draw method first transforms this list (to get a mutated line), 6 | # then draws the points in this list. We use findpath to find a path 7 | # "through" the list of points. 8 | 9 | size(500, 500) 10 | speed(30) 11 | 12 | def setup(): 13 | # px/py are the previous mouse coordinates. 14 | global px, py 15 | px, py = 0, 0 16 | 17 | # pointlist is the list of points we created by moving the mouse. 18 | global pointlist 19 | pointlist = [] 20 | 21 | def draw(): 22 | global px, py 23 | global pointlist 24 | 25 | # For each frame, set the background color to a darkish blue. 26 | background(0.0, 0.0, 0.2) 27 | 28 | # Only draw a new point if the mouse has moved, which means the current mouse 29 | # position is different from the previous one. 30 | # You can append "and mousedown" to the if statement to only draw points when 31 | # you hold down the mouse button. If you do, try clicking to get lines. 32 | if MOUSEX != px and MOUSEY != py: 33 | pointlist.append(Point(MOUSEX, MOUSEY)) 34 | px, py = MOUSEX, MOUSEY 35 | 36 | # Set the correct color, a light blue. 37 | nofill() 38 | stroke(0.9, 0.9, 1.0) 39 | strokewidth(2) 40 | 41 | # This method actually transforms the points. We replace our current list of points 42 | # by the transformed version. 43 | # If you comment this line out, points won't be transformed, and you get a regular 44 | # line drawing program, but where's the fun in that? 45 | pointlist = transform_list(pointlist) 46 | 47 | # If there are points in the list... 48 | if len(pointlist) > 0: 49 | # ...draw them. We use the new findpath function to find a path 50 | # that goes through the list of points. The curvature defines 51 | # whether the path is rounded (1.0) or straight (0.0). 52 | drawpath(findpath(pointlist, curvature=1.0)) 53 | 54 | 55 | def transform_point(pt, index, total_length): 56 | """This is the transformation function that gets applied to all points in the path. 57 | It returns either a new point, or None if the point needs to be deleted. 58 | Currently, it applies some sinus/cosinus functions to the point to make them curl 59 | and move offscreen.""" 60 | from math import sin, cos 61 | # Add something to the x and y coordinates. 62 | # The formula (total_length - index), makes the influence on "older" 63 | # points (in the beginning of the list, with a low index) greater. 64 | pt.x += sin(index/50.0) * (total_length-index) / 100.0 65 | pt.y -= cos(index/100.0) * (total_length-index) / 100.0 66 | # If the point is offscreen, return None to indicate that we want the point deleted. 67 | if pt.x < 0 or pt.x > WIDTH or pt.y < 0 or pt.y > HEIGHT: 68 | return None 69 | return pt 70 | 71 | def transform_list(pointlist, fn=transform_point): 72 | """This method transforms a list of points, and returns a new list. 73 | For advanced users, you can specify the function that will be used to transform 74 | each point. You can copy/paste the transform_point function and try to make a 75 | new one yourself.""" 76 | total_length = len(pointlist) 77 | # We make a new list because we are going to be deleting elements from the old list. 78 | # This messes up enumeration. 79 | newlist = [] 80 | # Enumerate not only returns each point, but also its index, which the transform_point 81 | # method uses. 82 | for i, pt in enumerate(pointlist): 83 | # For each point, "apply" the method to get a new point. Note that we can specify 84 | # the method as a parameter (fn), which is a function. 85 | newpoint = fn(pt, i, total_length) 86 | # If the transformation method returns None, it means that the point can be 87 | # deleted. We don't actually delete the point, we just don't include it in 88 | # the new list. 89 | if newpoint is not None: 90 | newlist.append(newpoint) 91 | return newlist 92 | -------------------------------------------------------------------------------- /examples/Math/HowCurvesWork.py: -------------------------------------------------------------------------------- 1 | size(200,200) 2 | # This boring example demonstrate how curves work, and goes a bit 3 | # into the different parameters for drawing curves on screen. 4 | import math 5 | 6 | # Setup colors: no fill is needed, and stroke the curves with black. 7 | nofill() 8 | stroke(0) 9 | 10 | # Set the initial position 11 | x,y = 50, 50 12 | width = 50 13 | 14 | # The dx and dy parameters are the relative control points. 15 | # When using math.pi/2, you actually define the lower half 16 | # of a circle. 17 | dy = width/(math.pi / 2) 18 | 19 | # Begin drawing the path. The starting position is on the 20 | # given x and y coordinates. 21 | beginpath(x, y) 22 | # Calculate the control points. 23 | cp1 = (x, y + dy) 24 | cp2 = (x + width, y + dy) 25 | # Draw the curve. The first four parameters are the coordinates 26 | # of the two control curves; the last two parameters are 27 | # the coordinates of the destination point. 28 | curveto(cp1[0], cp1[1], cp2[0], cp2[1], x + width, y) 29 | # End the path; ending the path automatically draws it. 30 | endpath() 31 | 32 | # To demonstrate where the control points actually are, 33 | # we draw them using lines. 34 | # The first control point starts at the x,y position. 35 | line(x, y, cp1[0], cp1[1]) 36 | # The second control point is the ending point. 37 | line(x + width, y, cp2[0], cp2[1]) 38 | 39 | # To liven things up just a little bit, little ovals are 40 | # drawn in red on the position of the control points. 41 | nostroke() 42 | fill(1,0,0) 43 | oval(cp1[0] - 2, cp1[1] - 2, 4, 4) 44 | oval(cp2[0] - 2, cp2[1] - 2, 4, 4) 45 | -------------------------------------------------------------------------------- /examples/Math/MathSculpture.py: -------------------------------------------------------------------------------- 1 | size(400, 800) 2 | # Generates sculptures using a set of mathematical functions. 3 | # Every iteration adds a certain value to the current coordinates. 4 | # Rewriting this program to use transforms is left as an exercise 5 | # for the reader. 6 | 7 | # This program uses some mathematical functions that are not 8 | # standard functions of NodeBox. 9 | # Instead, they are in Python's math library. The next 10 | # line imports those functions. 11 | from math import sin, cos, tan, log10 12 | 13 | background(0) 14 | 15 | cX = random(1,10) 16 | cY = random(1,10) 17 | 18 | x = 200 19 | y = 54 20 | fontsize(10) 21 | for i in range(278): 22 | x += cos(cY)*10 23 | y += log10(cX)*1.85 + sin(cX) * 5 24 | 25 | fill(random()-0.4, 0.8, 0.8, random()) 26 | 27 | s = 10 + cos(cX)*15 28 | oval(x-s/2, y-s/2, s, s) 29 | # Try the next line instead of the previous one to see how 30 | # you can use other primitives. 31 | #star(x-s/2,y-s/2, random(5,10), inner=2+s*0.1,outer=10+s*0.1) 32 | 33 | cX += random(0.25) 34 | cY += random(0.25) -------------------------------------------------------------------------------- /examples/Math/TunnelEffect.py: -------------------------------------------------------------------------------- 1 | size(1000, 1000) 2 | # Play around with elementary math functions. 3 | # Here, we are creating some sort of tunnel effect by 4 | # using sinus and cosinus functions. 5 | 6 | # Sinus and cosinus are not standard functions of NodeBox. 7 | # Instead, they are in Python's math library. The next 8 | # line imports those functions. 9 | from math import sin, cos 10 | 11 | startX = startY = 100 12 | startval = random() 13 | stroke(0.2) 14 | c = random() 15 | for i in range(300): 16 | delta = (random()-0.5) * 0.1 17 | x = 400 + sin(c+delta) * (i+random(-10,10)) 18 | y = 400 + cos(c+delta) * (i+random(-10,10)) 19 | s = random(c*2) 20 | 21 | fill(random()-0.4, 0.2, 0.2, random()) 22 | 23 | # The next line two lines look straightforward, 24 | # but actually show off a really powerful Python feature. 25 | # We choose here between two functions, the oval 26 | # and rect function. After we put the desired function 27 | # in the primitive variable, we execute that function with 28 | # the given parameters. Note that the parameters of 29 | # the two functions should match for this to work. 30 | primitive = choice((oval,rect)) 31 | primitive(x-s/2, y-s/2, s, s) 32 | 33 | c += random()*0.25 -------------------------------------------------------------------------------- /examples/Path/BitBop.py: -------------------------------------------------------------------------------- 1 | size(535, 140) 2 | # BitBop -- a fun demonstration of path.contains. 3 | # 4 | # The textpath command returns a BezierPath of the text that can 5 | # be manipulated or, as demonstrated here, queried using path.contains. 6 | # A grid is generated and everywhere a point in the path is encountered, 7 | # a random square is drawn. 8 | 9 | background(0.8, 0.7, 0) 10 | fill(0.1, 0.1, 0.2) 11 | 12 | # Set the font and create the text path. 13 | font("Verdana", 100) 14 | align(CENTER) 15 | tp = textpath("NodeBox", 0, 100, width=WIDTH) 16 | #tp.draw() # Draws the underlying path 17 | 18 | # Here are the variables that influence the composition: 19 | resx = 100 # The horizontal resolution 20 | resy = 100 # The vertical resolution 21 | rx = 5.0 # The horizontal randomness each point has 22 | ry = 5.0 # The vertical randomness each point has 23 | dotsize = 6.0 # The maximum size of one dot. 24 | dx = WIDTH / float(resx) # The width each dot covers 25 | dy = HEIGHT / float(resy) # The height each dot covers 26 | 27 | # We create a grid of the specified resolution. 28 | # Each x,y coordinate is a measuring point where 29 | # we check if it falls within the path. 30 | for x, y in grid(resx, resy): 31 | sz = random(dotsize) 32 | # Create the point that will be checked 33 | px = x*dx-sz 34 | py = y*dy-sz 35 | # Only do something if the point falls within the path bounds. 36 | # You could add an "else" statement, that draws something in the 37 | # empty positions. 38 | if tp.contains(px, py): 39 | # Change the color for each point -- try it out! 40 | # fill(0, 0, random(), random()) 41 | oval(px+random(-rx, rx), 42 | py+random(-ry, ry), 43 | sz, sz) 44 | 45 | -------------------------------------------------------------------------------- /examples/Path/Spider.py: -------------------------------------------------------------------------------- 1 | # This example uses the points method to connect letters together. 2 | # It actually draws lines between points of each letter contour 3 | # that are a certain distance from eachother. 4 | 5 | size(600, 207) 6 | background(0.3,0,0.2) 7 | 8 | # This utility method calculates the length between points. 9 | # It's just a standard Pythagoras algorithm. 10 | def calc_length(x1, y1, x2, y2): 11 | from math import sqrt, pow 12 | return sqrt(pow(x2-x1, 2) + pow(y2-y1, 2)) 13 | 14 | 15 | # First, create a textpath that we will use further on. 16 | fontsize(150) 17 | path = textpath("SPIDER",20, 150) 18 | 19 | # Select a color for the lines. 20 | nofill() 21 | stroke(1) 22 | strokewidth(0.3) 23 | 24 | # The mutation adds a little extra randomness to each calculated point. 25 | # Increase it to make the lines deviate more from the template path. 26 | mutation = 2.0 27 | 28 | # The maximum distance between two points. Increase this to get a more 29 | # "spidery" effect. 30 | maxdist = 40.0 31 | 32 | # Amount of lines for each contour. 33 | lines_per_contour = 300 34 | 35 | # A path has a contours property that returns each seperate contours. 36 | # Note that "holes" in a letter (such as a P or D) are contours as well. 37 | for contour in path.contours: 38 | # Get a list of 100 points on each contour, properly divided amongst 39 | # the path. This is different from the elements of the path, because 40 | # the points are evenly distributed along the path. 41 | path_points = list(contour.points(100)) 42 | 43 | # We want a certain amount of lines. 44 | for i in range(lines_per_contour): 45 | # Choose a point on the path 46 | pt1 = choice(path_points) 47 | # To find the second point, we use a "brute-force" approach. 48 | # We randomly select a point on the path, and see if its distance 49 | # from the first point is smaller than the maximum allowed distance. 50 | # If it is, the point is selected; otherwise, we try another point. 51 | # Note that this might loop infinitely for very short (or negative) distances. 52 | # Use Command-Period to break out of the loop. 53 | 54 | # Initialize the current length to "infinity", which means it won't get selected. 55 | length = float("inf") 56 | while length > maxdist: 57 | pt2 = choice(path_points) 58 | length = calc_length(pt1.x, pt1.y, pt2.x, pt2.y) 59 | 60 | # Once we've found a second point, draw it. Use the mutation parameter to add a bit 61 | # of randomness to the position of the line. 62 | line(pt1.x + random(-mutation, mutation), pt1.y + random(-mutation, mutation), \ 63 | pt2.x + random(-mutation, mutation), pt2.y + random(-mutation, mutation)) 64 | -------------------------------------------------------------------------------- /examples/Primitives/AppendingTransforms.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | # Appending transformations for very large and very small compositions. 3 | 4 | translate(WIDTH/2, HEIGHT/2) 5 | scale(5) 6 | # Because we don't use push and pop, all transformations are appended 7 | # to eachother. This means that, depending on the random value, a 8 | # composition might grow to be very big or very small. 9 | for i in range(100): 10 | rotate(random(-10, 10)) 11 | # Use the next line instead of the previous ones to see 12 | # what russian constructivism looks like. 13 | # rotate(random(1,4)*45) 14 | scale(random(0.5,1.5)) 15 | fill(random()-0.4, 0.2, 0.2, random()) 16 | translate(random(-5,5),random(-5,5)) 17 | # After all transformations are done, draw the shape. 18 | rect(-10, -10, 10,10) 19 | # The following line is commented out. Remove the 20 | # comment sign to see how this looks with stars. 21 | # star(0,0,points=random(5,50),inner=1,outer=14) 22 | 23 | -------------------------------------------------------------------------------- /examples/Primitives/CoverGen.py: -------------------------------------------------------------------------------- 1 | # Cover generator 2 | 3 | size(400, 300) 4 | 5 | # The HSB color mode is the big trick here. We use it to generate 6 | # a random hue with a fixed brightness and saturation. That way, 7 | # we can use all kinds of different colors that automatically fit 8 | # together. 9 | colormode(HSB) 10 | 11 | # Set a random background color 12 | fill(random(),0.5,0.5) 13 | rect(0,0,WIDTH,HEIGHT) 14 | 15 | # Draw 10 rectangles on top of each other, each with a different color, 16 | # and a random starting position and height. 17 | for i in range(10): 18 | fill(random(),0.5,0.5) 19 | rect(0,random(-200,HEIGHT+200),WIDTH,random(-200,HEIGHT+200)) 20 | 21 | # Draw the text. 22 | fill(1,0,1) 23 | scale(8) 24 | text("*",WIDTH-50,50) 25 | reset() 26 | text("NODEBOX",10,HEIGHT-30) 27 | fontsize(13.5) 28 | fill(1,0,1,0.4) 29 | text("AUTUMN | WINTER 2007",10,HEIGHT-18) -------------------------------------------------------------------------------- /examples/Primitives/Ovals.py: -------------------------------------------------------------------------------- 1 | # This is a really simple example that draws some stroked circles. 2 | # Note that we generate a random radius beforehand, so both width 3 | # and height have the same value. We also use a simple formulas 4 | # to center the circles on screen. 5 | 6 | size(500, 500) 7 | 8 | # Set the background color to a specific hue of red 9 | background(0.16, 0, 0) 10 | 11 | # Set filling off, so ovals will be stroked only 12 | nofill() 13 | 14 | for i in range(50): 15 | 16 | # Set the stroke color to a random tint of orange 17 | stroke(random(), random(0.31), 0) 18 | 19 | # Set the stroke width 20 | strokewidth(random(0.2, 4.0)) 21 | 22 | # Define the radius of the oval. This needs to be done 23 | # beforehand: 24 | # If we were to do "oval(20, 20, random(50), random(50))", 25 | # width and height would each get a different random value. 26 | radius = random(50) 27 | 28 | # Draw the oval onscreen. The width/height are both set to the 29 | # radius * 2 to get the diameter. The radius is subtracted from the 30 | # x and y coordinates to place the oval in the middle. 31 | oval(random(WIDTH)-radius, random(HEIGHT)-radius, radius*2, radius*2) -------------------------------------------------------------------------------- /examples/Primitives/StarFun.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | # Fun with stars! 3 | 4 | # Use the HSB color model to generate matching random colors. 5 | colormode(HSB) 6 | 7 | # This loop has no push and pop, meaning that every transformation 8 | # is appended to the previous ones. 9 | for y in range(100): 10 | fill(random(0.8,1),random(),random(0.2,0.6),random()) 11 | rotate(random(-3,3)) 12 | translate(random(-100,100), random(-100,100)) 13 | star(300,300,random(1,100), random(1,5), random(1,500)) 14 | -------------------------------------------------------------------------------- /examples/Text/Copyrights.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | 3 | # Small example demonstrating how to display unicode text. 4 | 5 | white = color(1,1,1,0.9) 6 | red = color(1,0,0,0.9) 7 | black = color(0,0,0,0.9) 8 | 9 | for i in range(20): 10 | # Choose a color from the list. This list is created on-the-fly, 11 | # and is actually a tuple (a non-editable list). Therefore, 12 | # the list itself is wrapped in brackets. The second brackets 13 | # surrounding it are for the choice command. 14 | fill(choice((white,red,black))) 15 | font("Arial Bold") 16 | fontsize(random(600)) 17 | 18 | # The TradeMark, Registered and Copyright signs are 19 | # Unicode characters. You should prefix the string with 20 | # a 'u', for Unicode. 21 | text(u"™", random(500),random(400)) 22 | text(u"®", random(500),random(400)) 23 | text(u"©", random(500),random(400)) -------------------------------------------------------------------------------- /examples/Text/GridGrid.py: -------------------------------------------------------------------------------- 1 | size(500, 500) 2 | # Draw a grid of grids. 3 | 4 | # Use corner transformations to rotate objects from the top-left corner, 5 | # instead of from the center of an object (which is the default). 6 | transform(CORNER) 7 | 8 | font('Gill Sans', 72) 9 | 10 | for i in range(600): 11 | # At the beginning of the loop, push the current transformation. 12 | # This means that each loop begins with a "clean slate". 13 | push() 14 | # Fills aren't remembered using push/pop, only transformations. 15 | fill(random(),0,0,0.5) 16 | # Use this way of translation to put objects on a grid. 17 | # NodeBox also has a grid function: see the examples in Examples/Grid 18 | translate(random(1,10)*50, random(1,10)*50) 19 | rotate(random(360)) 20 | scale(random(1.8)) 21 | # Change this text for other interesting results. 22 | text('#', 0, 0) 23 | pop() -------------------------------------------------------------------------------- /examples/Text/NumberTunnel.py: -------------------------------------------------------------------------------- 1 | size(600, 600) 2 | # Create interesting effects by using 3 | # appending transformations. 4 | 5 | # All transformations in NodeBox are remembered. 6 | # Every transformation you do is appended to the 7 | # previous ones. Here, the scale and rotation are 8 | # only changed a little bit, but by doing this multiple 9 | # times, you will get full transformations. 10 | # Also note that the actual position of the text is never 11 | # changed: instead, it is modified by the transformations. 12 | 13 | # The first thing we do is move to the center of the screen. 14 | translate(300,300) 15 | for i in range(60): 16 | # The fill color is changed. Try using alpha transparency. 17 | fill((75-i)*0.01) 18 | # The imaged is scaled up. A random range from 1.0 19 | # (no scale) to 1.2 (20% larger) is selected 20 | scale(random(1.0,1.2)) 21 | # Rotate by a random value, from 0 to 10. 22 | rotate(random(10)) 23 | # Use the loop counter as a text string. The loop will 24 | # go from 0 to 99. Try changing this in a fixed text 25 | # to see some interesting effects. 26 | text(str(i),0,0) 27 | -------------------------------------------------------------------------------- /examples/Text/OrganicBall.py: -------------------------------------------------------------------------------- 1 | # Create organic balls using text. 2 | 3 | size(600, 600) 4 | 5 | font('Zapfino') 6 | fontsize(72) 7 | 8 | # Draw a black background. Setting the background to None 9 | # gives an empty background. 10 | background(0) 11 | 12 | # Move to the center of the composition. Note that, because 13 | # we use Zapfino, the ball will end up off-center. 14 | translate(WIDTH/2,HEIGHT/2) 15 | for i in range(100): 16 | # The trick is skewing, rotating and scaling without 17 | # moving so all elements share the same centerpoint. 18 | push() 19 | # Select a value between (0,0,0) (black) and (1,0,0) (red). 20 | fill(random(),0,0) 21 | rotate(random(0,800)) 22 | scale(random()*2) 23 | skew(random(200)) 24 | text('(',0,0) 25 | pop() -------------------------------------------------------------------------------- /examples/Text/RandomFont.py: -------------------------------------------------------------------------------- 1 | size(400, 400) 2 | var("textsize", NUMBER, 50.0, 0.0, 100.0) 3 | # Demonstrate how to randomly select a font from a list. 4 | # In addition, it also demonstrates how to use variables 5 | # to set the textsize. 6 | 7 | names = ['Helvetica', 'Arial', 'Times', 'Impact', 'Verdana'] 8 | 9 | fill(1,0,0) 10 | 11 | # Select a font randomly from the list of names. 12 | font(choice(names)) 13 | 14 | # textsize is a variable defined under Python > Variables. 15 | fontsize(textsize) 16 | 17 | # Display the text. Because the coordinates start from 18 | # the baseline, you have to add the size of the font to 19 | # the y coordinate so it doesn't fall of of the page. 20 | text('Hi there!', 12, textsize) -------------------------------------------------------------------------------- /examples/Text/RandomText.py: -------------------------------------------------------------------------------- 1 | size(800, 600) 2 | # Generate compositions using random text. 3 | 4 | font('Arial Black') 5 | 6 | def rndText(): 7 | """Returns a random string of up to 9 characters.""" 8 | t = u"" 9 | for i in range(random(10)): 10 | t += chr(random(10,120)) 11 | return t 12 | 13 | 14 | # Define some colors. 15 | colormode(HSB) 16 | white = color(1,1,1,0.8) 17 | black = color(0,0,0,0.8) 18 | red = color(random(),0,0.2,0.8) 19 | 20 | translate(0,-200) 21 | for i in range(100): 22 | # This translation is not reset every time, so it is 23 | # appended to previous translations. This gives 24 | # interesting effects. 25 | translate(random(-100,100),random(-100,100)) 26 | # Save the current transformation. It's a good idea 27 | # to do this in the beginning of a loop. End the 28 | # loop with a pop. 29 | push() 30 | # Rotate in increments of 45 degrees. 31 | rotate(random(5)*45) 32 | fontsize(random(800)) 33 | fill(choice((white,black,red))) 34 | someText = rndText() 35 | text(someText, 0,0) 36 | pop() -------------------------------------------------------------------------------- /examples/Text/TextFromList.py: -------------------------------------------------------------------------------- 1 | size(800, 600) 2 | # Generate compositions using predefined words. 3 | 4 | txt = [ 5 | 'DIE', 6 | 'BUY', 7 | 'WHY', 8 | 'NOW', 9 | '!' 10 | ] 11 | 12 | font('Arial Black') 13 | 14 | # Define some colors. 15 | white = color(1,1,1) 16 | black = color(0,0,0) 17 | red = color(1,0,0) 18 | 19 | translate(0,-200) 20 | for i in range(100): 21 | # The next line isn't inside of the push-pops and therefore 22 | # the translate is appended every time. This might mean that 23 | # the composition goes off-screen. This also means that 24 | # it creates more interesting compositions. 25 | translate(random(-100,100),random(-100,100)) 26 | # Save the current transformation. It's a good idea 27 | # to do this in the beginning of a loop. End the 28 | # loop with a pop. 29 | push() 30 | # Rotate in increments of 45 degrees. 31 | rotate(random(5)*45) 32 | fontsize(random(800)) 33 | fill(choice((white,black,red))) 34 | someText = choice(txt) 35 | # One in two times, change the text to lowercase. 36 | if random(2) == 1: 37 | someText = someText.lower() 38 | text(someText, 0,0) 39 | pop() -------------------------------------------------------------------------------- /libs/buildlibs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import errno 4 | 5 | def mkdirs(newdir, mode=0777): 6 | try: os.makedirs(newdir, mode) 7 | except OSError, err: 8 | # Reraise the error unless it's about an already existing directory 9 | if err.errno != errno.EEXIST or not os.path.isdir(newdir): 10 | raise 11 | 12 | def build_libraries(): 13 | print "Building all required NodeBox libraries..." 14 | 15 | # Store the current working directory for later. 16 | lib_root = os.getcwd() 17 | # Find all setup.py files in the current folder 18 | setup_scripts = glob.glob('*/setup.py') 19 | for setup_script in setup_scripts: 20 | lib_name = os.path.dirname(setup_script) 21 | print "Building", lib_name 22 | # run_setup gave some wonky errors, so we're defaulting to a simple os.system call. 23 | os.chdir(os.path.dirname(setup_script)) 24 | result = os.system('python setup.py build') 25 | if result > 0: 26 | raise OSError("Could not build %s" % lib_name) 27 | os.chdir(lib_root) 28 | 29 | # Make sure the destination folder exists. 30 | mkdirs('../build/libs') 31 | 32 | # Copy all build results to the ../build/libs folder. 33 | build_dirs = glob.glob("*/build/lib*") 34 | for build_dir in build_dirs: 35 | lib_name = os.path.dirname(os.path.dirname(build_dir)) 36 | print "Copying", lib_name 37 | cmd = 'cp -R -p %s/* ../build/libs' % build_dir 38 | print cmd 39 | result = os.system(cmd) 40 | if result > 0: 41 | raise OSError("Could not copy %s" % lib_name) 42 | 43 | def clean_build_files(): 44 | print "Cleaning all library build files..." 45 | 46 | build_dirs = glob.glob('*/build') 47 | for build_dir in build_dirs: 48 | lib_name = os.path.dirname(build_dir) 49 | print "Cleaning", lib_name 50 | os.system('rm -r %s' % build_dir) 51 | 52 | if __name__=='__main__': 53 | import sys 54 | if len(sys.argv) > 1 and sys.argv[1] == 'clean': 55 | clean_build_files() 56 | else: 57 | build_libraries() 58 | -------------------------------------------------------------------------------- /libs/cGeo/cGeo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // FAST INVERSE SQRT 5 | // Chris Lomont, http://www.math.purdue.edu/~clomont/Math/Papers/2003/InvSqrt.pdf 6 | float _fast_inverse_sqrt(float x) { 7 | float xhalf = 0.5f*x; 8 | int i = *(int*)&x; 9 | i = 0x5f3759df - (i>>1); 10 | x = *(float*)&i; 11 | x = x*(1.5f-xhalf*x*x); 12 | return x; 13 | } 14 | static PyObject * 15 | fast_inverse_sqrt(PyObject *self, PyObject *args) { 16 | double x; 17 | if (!PyArg_ParseTuple(args, "d", &x)) 18 | return NULL; 19 | x = _fast_inverse_sqrt(x); 20 | return Py_BuildValue("d", x); 21 | } 22 | 23 | // ANGLE 24 | void _angle(double x0, double y0, double x1, double y1, double *a) { 25 | *a = atan2(y1-y0, x1-x0) / M_PI * 180; 26 | } 27 | static PyObject * 28 | angle(PyObject *self, PyObject *args) { 29 | double x0, y0, x1, y1, a; 30 | if (!PyArg_ParseTuple(args, "dddd", &x0, &y0, &x1, &y1)) 31 | return NULL; 32 | _angle(x0, y0, x1, y1, &a); 33 | return Py_BuildValue("d", a); 34 | } 35 | 36 | // DISTANCE 37 | void _distance(double x0, double y0, double x1, double y1, double *d) { 38 | *d = 1.0 / _fast_inverse_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); 39 | } 40 | static PyObject * 41 | distance(PyObject *self, PyObject *args) { 42 | double x0, y0, x1, y1, d; 43 | if (!PyArg_ParseTuple(args, "dddd", &x0, &y0, &x1, &y1)) 44 | return NULL; 45 | _distance(x0, y0, x1, y1, &d); 46 | return Py_BuildValue("d", d); 47 | } 48 | 49 | // COORDINATES 50 | void _coordinates(double x0, double y0, double d, double a, double *x1, double *y1) { 51 | *x1 = x0 + cos(a/180*M_PI) * d; 52 | *y1 = y0 + sin(a/180*M_PI) * d; 53 | } 54 | static PyObject * 55 | coordinates(PyObject *self, PyObject *args) { 56 | double x0, y0, d, a, x1, y1; 57 | if (!PyArg_ParseTuple(args, "dddd", &x0, &y0, &d, &a)) 58 | return NULL; 59 | _coordinates(x0, y0, d, a, &x1, &y1); 60 | return Py_BuildValue("dd", x1, y1); 61 | } 62 | 63 | static PyMethodDef geometry_methods[]={ 64 | { "fast_inverse_sqrt", fast_inverse_sqrt, METH_VARARGS }, 65 | { "angle", angle, METH_VARARGS }, 66 | { "distance", distance, METH_VARARGS }, 67 | { "coordinates", coordinates, METH_VARARGS }, 68 | { NULL, NULL } 69 | }; 70 | 71 | PyMODINIT_FUNC initcGeo(void){ 72 | PyObject *m; 73 | m = Py_InitModule("cGeo", geometry_methods); 74 | } 75 | 76 | int main(int argc, char *argv[]) 77 | { 78 | Py_SetProgramName(argv[0]); 79 | Py_Initialize(); 80 | initcGeo(); 81 | return 0; 82 | } -------------------------------------------------------------------------------- /libs/cGeo/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | cGeo = Extension("cGeo", sources = ["cGeo.c"]) 4 | 5 | setup (name = "cGeo", 6 | version = "0.1", 7 | author = "Tom De Smedt", 8 | description = "Fast geometric functionality.", 9 | ext_modules = [cGeo]) -------------------------------------------------------------------------------- /libs/pathmatics/pathmatics.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | void _linepoint(double t, double x0, double y0, double x1, double y1, 5 | double *out_x, double *out_y 6 | ) 7 | { 8 | *out_x = x0 + t * (x1-x0); 9 | *out_y = y0 + t * (y1-y0); 10 | } 11 | 12 | 13 | void _linelength(double x0, double y0, double x1, double y1, 14 | double *out_length 15 | ) 16 | { 17 | double a, b; 18 | a = pow(fabs(x0 - x1), 2); 19 | b = pow(fabs(y0 - y1), 2); 20 | *out_length = sqrt(a + b); 21 | } 22 | 23 | void _curvepoint(double t, double x0, double y0, double x1, double y1, 24 | double x2, double y2, double x3, double y3, 25 | double *out_x, double *out_y, 26 | double *out_c1x, double *out_c1y, double *out_c2x, double *out_c2y 27 | ) 28 | { 29 | double mint, x01, y01, x12, y12, x23, y23; 30 | 31 | mint = 1 - t; 32 | x01 = x0 * mint + x1 * t; 33 | y01 = y0 * mint + y1 * t; 34 | x12 = x1 * mint + x2 * t; 35 | y12 = y1 * mint + y2 * t; 36 | x23 = x2 * mint + x3 * t; 37 | y23 = y2 * mint + y3 * t; 38 | 39 | *out_c1x = x01 * mint + x12 * t; 40 | *out_c1y = y01 * mint + y12 * t; 41 | *out_c2x = x12 * mint + x23 * t; 42 | *out_c2y = y12 * mint + y23 * t; 43 | *out_x = *out_c1x * mint + *out_c2x * t; 44 | *out_y = *out_c1y * mint + *out_c2y * t; 45 | } 46 | 47 | void _curvepoint_handles(double t, double x0, double y0, double x1, double y1, 48 | double x2, double y2, double x3, double y3, 49 | double *out_x, double *out_y, 50 | double *out_c1x, double *out_c1y, double *out_c2x, double *out_c2y, 51 | double *out_h1x, double *out_h1y, double *out_h2x, double *out_h2y 52 | ) 53 | { 54 | double mint, x01, y01, x12, y12, x23, y23; 55 | 56 | mint = 1 - t; 57 | x01 = x0 * mint + x1 * t; 58 | y01 = y0 * mint + y1 * t; 59 | x12 = x1 * mint + x2 * t; 60 | y12 = y1 * mint + y2 * t; 61 | x23 = x2 * mint + x3 * t; 62 | y23 = y2 * mint + y3 * t; 63 | 64 | *out_c1x = x01 * mint + x12 * t; 65 | *out_c1y = y01 * mint + y12 * t; 66 | *out_c2x = x12 * mint + x23 * t; 67 | *out_c2y = y12 * mint + y23 * t; 68 | *out_x = *out_c1x * mint + *out_c2x * t; 69 | *out_y = *out_c1y * mint + *out_c2y * t; 70 | *out_h1x = x01; 71 | *out_h1y = y01; 72 | *out_h2x = x23; 73 | *out_h2y = y23; 74 | } 75 | 76 | void _curvelength(double x0, double y0, double x1, double y1, 77 | double x2, double y2, double x3, double y3, int n, 78 | double *out_length 79 | ) 80 | { 81 | double xi, yi, t, c; 82 | double pt_x, pt_y, pt_c1x, pt_c1y, pt_c2x, pt_c2y; 83 | int i; 84 | double length = 0; 85 | 86 | xi = x0; 87 | yi = y0; 88 | 89 | for (i=0; i 37 | 38 | 39 | /* 40 | =========================================================================== 41 | Constants 42 | =========================================================================== 43 | */ 44 | 45 | /* Increase GPC_EPSILON to encourage merging of near coincident edges */ 46 | 47 | #define GPC_EPSILON (DBL_EPSILON) 48 | 49 | #define GPC_VERSION "2.32" 50 | 51 | 52 | /* 53 | =========================================================================== 54 | Public Data Types 55 | =========================================================================== 56 | */ 57 | 58 | typedef enum /* Set operation type */ 59 | { 60 | GPC_DIFF, /* Difference */ 61 | GPC_INT, /* Intersection */ 62 | GPC_XOR, /* Exclusive or */ 63 | GPC_UNION /* Union */ 64 | } gpc_op; 65 | 66 | typedef struct /* Polygon vertex structure */ 67 | { 68 | double x; /* Vertex x component */ 69 | double y; /* vertex y component */ 70 | } gpc_vertex; 71 | 72 | typedef struct /* Vertex list structure */ 73 | { 74 | int num_vertices; /* Number of vertices in list */ 75 | gpc_vertex *vertex; /* Vertex array pointer */ 76 | } gpc_vertex_list; 77 | 78 | typedef struct /* Polygon set structure */ 79 | { 80 | int num_contours; /* Number of contours in polygon */ 81 | int *hole; /* Hole / external contour flags */ 82 | gpc_vertex_list *contour; /* Contour array pointer */ 83 | } gpc_polygon; 84 | 85 | typedef struct /* Tristrip set structure */ 86 | { 87 | int num_strips; /* Number of tristrips */ 88 | gpc_vertex_list *strip; /* Tristrip array pointer */ 89 | } gpc_tristrip; 90 | 91 | 92 | /* 93 | =========================================================================== 94 | Public Function Prototypes 95 | =========================================================================== 96 | */ 97 | 98 | void gpc_read_polygon (FILE *infile_ptr, 99 | int read_hole_flags, 100 | gpc_polygon *polygon); 101 | 102 | void gpc_write_polygon (FILE *outfile_ptr, 103 | int write_hole_flags, 104 | gpc_polygon *polygon); 105 | 106 | void gpc_add_contour (gpc_polygon *polygon, 107 | gpc_vertex_list *contour, 108 | int hole); 109 | 110 | void gpc_polygon_clip (gpc_op set_operation, 111 | gpc_polygon *subject_polygon, 112 | gpc_polygon *clip_polygon, 113 | gpc_polygon *result_polygon); 114 | 115 | void gpc_tristrip_clip (gpc_op set_operation, 116 | gpc_polygon *subject_polygon, 117 | gpc_polygon *clip_polygon, 118 | gpc_tristrip *result_tristrip); 119 | 120 | void gpc_polygon_to_tristrip (gpc_polygon *polygon, 121 | gpc_tristrip *tristrip); 122 | 123 | void gpc_free_polygon (gpc_polygon *polygon); 124 | 125 | void gpc_free_tristrip (gpc_tristrip *tristrip); 126 | 127 | #endif 128 | 129 | /* 130 | =========================================================================== 131 | End of file: gpc.h 132 | =========================================================================== 133 | */ 134 | -------------------------------------------------------------------------------- /libs/polymagic/polymagic_test.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSBezierPath, NSFont, NSApplication 2 | 3 | # Force NSApp initialisation. 4 | NSApplication.sharedApplication().activateIgnoringOtherApps_(0) 5 | 6 | p1 = NSBezierPath.bezierPathWithOvalInRect_(((0, 0), (100, 100))) 7 | p2 = NSBezierPath.bezierPathWithOvalInRect_(((40, 40), (100, 100))) 8 | 9 | # Outside of bounding box 10 | p3 = NSBezierPath.bezierPathWithOvalInRect_(((110, 0), (100, 100))) 11 | 12 | # In bounding box, doesn't intersect 13 | p4 = NSBezierPath.bezierPathWithOvalInRect_(((72, 72), (100, 100))) 14 | 15 | 16 | 17 | from cPolymagic import * 18 | 19 | print intersects(p1, p4) 20 | 21 | f = NSFont.fontWithName_size_("Helvetica", 72) 22 | fp1 = NSBezierPath.bezierPath() 23 | fp1.moveToPoint_((100, 100)) 24 | fp1.appendBezierPathWithGlyph_inFont_(68, f) 25 | 26 | fp2 = NSBezierPath.bezierPath() 27 | fp2.moveToPoint_((110, 100)) 28 | fp2.appendBezierPathWithGlyph_inFont_(68, f) 29 | 30 | print intersects(fp1, fp2) 31 | 32 | # Some other thing inside of the function perhaps? 33 | 34 | print intersects(fp2, fp2) 35 | 36 | p = union(fp1, fp2) 37 | print p.elementCount() 38 | 39 | p = intersect(fp1, fp2) 40 | print p.elementCount() 41 | 42 | p = difference(fp1, fp2) 43 | print p.elementCount() 44 | 45 | p = xor(fp1, fp2) 46 | print p.elementCount() 47 | 48 | -------------------------------------------------------------------------------- /libs/polymagic/setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | CFLAGS=[] 4 | 5 | cPolymagic = Extension("cPolymagic", sources = ["gpc.c", "polymagic.m"], extra_compile_args=CFLAGS) 6 | 7 | setup (name = "polymagic", 8 | version = "0.1", 9 | author = "Frederik De Bleser", 10 | description = "Additional utility functions for NSBezierPath using GPC.", 11 | ext_modules = [cPolymagic]) -------------------------------------------------------------------------------- /macboot.py: -------------------------------------------------------------------------------- 1 | # Startup file for the NodeBox OS X application 2 | # PyObjC requires the startup file to be in the root folder. 3 | # This just imports everything from the nodebox.gui.mac module 4 | # and works from there 5 | 6 | import objc 7 | import Foundation 8 | import AppKit 9 | 10 | from PyObjCTools import AppHelper 11 | 12 | import nodebox.gui.mac 13 | 14 | AppHelper.runEventLoop() 15 | -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 7 | 8 | NSBundle *mainBundle = [NSBundle mainBundle]; 9 | NSString *resourcePath = [mainBundle resourcePath]; 10 | NSArray *pythonPathArray = [NSArray arrayWithObjects: resourcePath, [resourcePath stringByAppendingPathComponent:@"python"], nil]; 11 | 12 | setenv("PYTHONPATH", [[pythonPathArray componentsJoinedByString:@":"] UTF8String], 1); 13 | 14 | NSArray *possibleMainExtensions = [NSArray arrayWithObjects: @"py", @"pyc", @"pyo", nil]; 15 | NSString *mainFilePath = nil; 16 | 17 | for (NSString *possibleMainExtension in possibleMainExtensions) { 18 | mainFilePath = [mainBundle pathForResource: @"macboot" ofType: possibleMainExtension]; 19 | if ( mainFilePath != nil ) break; 20 | } 21 | 22 | if ( !mainFilePath ) { 23 | [NSException raise: NSInternalInconsistencyException format: @"%s:%d main() Failed to find the macboot.{py,pyc,pyo} file in the application wrapper's Resources directory.", __FILE__, __LINE__]; 24 | } 25 | 26 | Py_SetProgramName("/usr/bin/python"); 27 | Py_Initialize(); 28 | PySys_SetArgv(argc, (char **)argv); 29 | 30 | const char *mainFilePathPtr = [mainFilePath UTF8String]; 31 | FILE *mainFile = fopen(mainFilePathPtr, "r"); 32 | int result = PyRun_SimpleFile(mainFile, (char *)[[mainFilePath lastPathComponent] UTF8String]); 33 | 34 | if ( result != 0 ) 35 | [NSException raise: NSInternalInconsistencyException 36 | format: @"%s:%d main() PyRun_SimpleFile failed with file '%@'. See console for errors.", __FILE__, __LINE__, mainFilePath]; 37 | 38 | [pool drain]; 39 | 40 | return result; 41 | } 42 | -------------------------------------------------------------------------------- /nodebox/Testing.txt: -------------------------------------------------------------------------------- 1 | Inferface testing 2 | 3 | Open an animation script. 4 | Export without running first to an image. 5 | Export without running first to a movie. 6 | 7 | Open a static script. 8 | Export without running first to an image. 9 | Export without running first to a movie. 10 | 11 | Open an animation script. 12 | Zoom in. 13 | Export to an image. 14 | Export to a movie. 15 | 16 | Make a NodeBox animation without a draw() command. 17 | Export one image frame. (should give an error) 18 | Make the draw command 19 | Export one image frame. (Should export the one frame) 20 | 21 | Stop while rendering an animation. The animation should have all frames up till that point. -------------------------------------------------------------------------------- /nodebox/__init__.py: -------------------------------------------------------------------------------- 1 | __version__='1.9.7rc2' 2 | 3 | def get_version(): 4 | return __version__ -------------------------------------------------------------------------------- /nodebox/console.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSApplication 2 | 3 | try: 4 | import nodebox 5 | except ImportError: 6 | import sys, os 7 | nodebox_dir = os.path.dirname(os.path.abspath(__file__)) 8 | sys.path.append(os.path.dirname(nodebox_dir)) 9 | 10 | from nodebox import graphics 11 | from nodebox import util 12 | 13 | class NodeBoxRunner(object): 14 | 15 | def __init__(self): 16 | # Force NSApp initialisation. 17 | NSApplication.sharedApplication().activateIgnoringOtherApps_(0) 18 | self.namespace = {} 19 | self.canvas = graphics.Canvas() 20 | self.context = graphics.Context(self.canvas, self.namespace) 21 | self.__doc__ = {} 22 | self._pageNumber = 1 23 | self.frame = 1 24 | 25 | def _check_animation(self): 26 | """Returns False if this is not an animation, True otherwise. 27 | Throws an expection if the animation is not correct (missing a draw method).""" 28 | if self.canvas.speed is not None: 29 | if not self.namespace.has_key('draw'): 30 | raise graphics.NodeBoxError('Not a correct animation: No draw() method.') 31 | return True 32 | return False 33 | 34 | def run(self, source_or_code): 35 | self._initNamespace() 36 | if isinstance(source_or_code, basestring): 37 | source_or_code = compile(source_or_code + "\n\n", "", "exec") 38 | exec source_or_code in self.namespace 39 | if self._check_animation(): 40 | if self.namespace.has_key('setup'): 41 | self.namespace['setup']() 42 | self.namespace['draw']() 43 | 44 | def run_multiple(self, source_or_code, frames): 45 | if isinstance(source_or_code, basestring): 46 | source_or_code = compile(source_or_code + "\n\n", "", "exec") 47 | 48 | # First frame is special: 49 | self.run(source_or_code) 50 | yield 1 51 | animation = self._check_animation() 52 | 53 | for i in range(frames-1): 54 | self.canvas.clear() 55 | self.frame = i + 2 56 | self.namespace["PAGENUM"] = self.namespace["FRAME"] = self.frame 57 | if animation: 58 | self.namespace['draw']() 59 | else: 60 | exec source_or_code in self.namespace 61 | yield self.frame 62 | 63 | def _initNamespace(self, frame=1): 64 | self.canvas.clear() 65 | self.namespace.clear() 66 | # Add everything from the namespace 67 | for name in graphics.__all__: 68 | self.namespace[name] = getattr(graphics, name) 69 | for name in util.__all__: 70 | self.namespace[name] = getattr(util, name) 71 | # Add everything from the context object 72 | self.namespace["_ctx"] = self.context 73 | for attrName in dir(self.context): 74 | self.namespace[attrName] = getattr(self.context, attrName) 75 | # Add the document global 76 | self.namespace["__doc__"] = self.__doc__ 77 | # Add the frame 78 | self.frame = frame 79 | self.namespace["PAGENUM"] = self.namespace["FRAME"] = self.frame 80 | 81 | def make_image(source_or_code, outputfile): 82 | 83 | """Given a source string or code object, executes the scripts and saves the result as an image. 84 | Supported image extensions: pdf, tiff, png, jpg, gif""" 85 | 86 | runner = NodeBoxRunner() 87 | runner.run(source_or_code) 88 | runner.canvas.save(outputfile) 89 | 90 | def make_movie(source_or_code, outputfile, frames, fps=30): 91 | 92 | """Given a source string or code object, executes the scripts and saves the result as a movie. 93 | You also have to specify the number of frames to render. 94 | Supported movie extension: mov""" 95 | 96 | from nodebox.util import QTSupport 97 | runner = NodeBoxRunner() 98 | movie = QTSupport.Movie(outputfile, fps) 99 | for frame in runner.run_multiple(source_or_code, frames): 100 | movie.add(runner.canvas) 101 | movie.save() 102 | 103 | def usage(err=""): 104 | if len(err) > 0: 105 | err = '\n\nError: ' + str(err) 106 | print """NodeBox console runner 107 | Usage: console.py sourcefile imagefile 108 | or: console.py sourcefile moviefile number_of_frames [fps] 109 | Supported image extensions: pdf, tiff, png, jpg, gif 110 | Supported movie extension: mov""" + err 111 | 112 | def main(): 113 | import sys, os 114 | if len(sys.argv) < 2: 115 | usage() 116 | elif len(sys.argv) == 3: # Should be an image 117 | basename, ext = os.path.splitext(sys.argv[2]) 118 | if ext not in ('.pdf', '.gif', '.jpg', '.jpeg', '.png', '.tiff'): 119 | return usage('This is not a supported image format.') 120 | make_image(open(sys.argv[1]).read(), sys.argv[2]) 121 | elif len(sys.argv) == 4 or len(sys.argv) == 5: # Should be a movie 122 | basename, ext = os.path.splitext(sys.argv[2]) 123 | if ext != '.mov': 124 | return usage('This is not a supported movie format.') 125 | if len(sys.argv) == 5: 126 | try: 127 | fps = int(sys.argv[4]) 128 | except ValueError: 129 | return usage() 130 | else: 131 | fps = 30 132 | make_movie(open(sys.argv[1]).read(), sys.argv[2], int(sys.argv[3]), fps) 133 | 134 | def test(): 135 | # Creating the NodeBoxRunner class directly: 136 | runner = NodeBoxRunner() 137 | runner.run('size(500,500)\nfor i in range(400):\n oval(random(WIDTH),random(HEIGHT),50,50, fill=(random(), 0,0,random()))') 138 | runner.canvas.save('console-test.pdf') 139 | runner.canvas.save('console-test.png') 140 | 141 | # Using the runner for animations: 142 | runner = NodeBoxRunner() 143 | for frame in runner.run_multiple('size(300, 300)\ntext(FRAME, 100, 100)', 10): 144 | runner.canvas.save('console-test-frame%02i.png' % frame) 145 | 146 | # Using the shortcut functions: 147 | make_image('size(200,200)\ntext(FRAME, 100, 100)', 'console-test.gif') 148 | make_movie('size(200,200)\ntext(FRAME, 100, 100)', 'console-test.mov', 10) 149 | 150 | if __name__=='__main__': 151 | main() -------------------------------------------------------------------------------- /nodebox/geo/__init__.py: -------------------------------------------------------------------------------- 1 | # Geometric functionality 2 | 3 | from math import degrees, atan2 4 | from math import sqrt, pow 5 | from math import radians, sin, cos 6 | 7 | try: 8 | # Faster C versions. 9 | import cGeo 10 | isqrt = inverse_sqrt = cGeo.fast_inverse_sqrt 11 | angle = cGeo.angle 12 | distance = cGeo.distance 13 | coordinates = cGeo.coordinates 14 | except ImportError: 15 | def inverse_sqrt(x): 16 | return 1.0 / sqrt(x) 17 | 18 | isqrt = inverse_sqrt 19 | 20 | def angle(x0, y0, x1, y1): 21 | a = degrees( atan2(y1-y0, x1-x0) ) 22 | return a 23 | 24 | def distance(x0, y0, x1, y1): 25 | return sqrt(pow(x1-x0, 2) + pow(y1-y0, 2)) 26 | 27 | def coordinates(x0, y0, distance, angle): 28 | x1 = x0 + cos(radians(angle)) * distance 29 | y1 = y0 + sin(radians(angle)) * distance 30 | return x1, y1 31 | 32 | def reflect(x0, y0, x1, y1, d=1.0, a=180): 33 | d *= distance(x0, y0, x1, y1) 34 | a += angle(x0, y0, x1, y1) 35 | x, y = coordinates(x0, y0, d, a) 36 | return x, y 37 | 38 | -------------------------------------------------------------------------------- /nodebox/geo/pathmatics.py: -------------------------------------------------------------------------------- 1 | from math import sqrt, pow 2 | 3 | def linepoint(t, x0, y0, x1, y1): 4 | 5 | """Returns coordinates for point at t on the line. 6 | 7 | Calculates the coordinates of x and y for a point 8 | at t on a straight line. 9 | 10 | The t parameter is a number between 0.0 and 1.0, 11 | x0 and y0 define the starting point of the line, 12 | x1 and y1 the ending point of the line, 13 | 14 | """ 15 | 16 | out_x = x0 + t * (x1-x0) 17 | out_y = y0 + t * (y1-y0) 18 | return (out_x, out_y) 19 | 20 | def linelength(x0, y0, x1, y1): 21 | 22 | """Returns the length of the line.""" 23 | 24 | a = pow(abs(x0 - x1), 2) 25 | b = pow(abs(y0 - y1), 2) 26 | return sqrt(a+b) 27 | 28 | def curvepoint(t, x0, y0, x1, y1, x2, y2, x3, y3, handles=False): 29 | 30 | """Returns coordinates for point at t on the spline. 31 | 32 | Calculates the coordinates of x and y for a point 33 | at t on the cubic bezier spline, and its control points, 34 | based on the de Casteljau interpolation algorithm. 35 | 36 | The t parameter is a number between 0.0 and 1.0, 37 | x0 and y0 define the starting point of the spline, 38 | x1 and y1 its control point, 39 | x3 and y3 the ending point of the spline, 40 | x2 and y2 its control point. 41 | 42 | If the handles parameter is set, 43 | returns not only the point at t, 44 | but the modified control points of p0 and p3 45 | should this point split the path as well. 46 | """ 47 | 48 | mint = 1 - t 49 | 50 | x01 = x0 * mint + x1 * t 51 | y01 = y0 * mint + y1 * t 52 | x12 = x1 * mint + x2 * t 53 | y12 = y1 * mint + y2 * t 54 | x23 = x2 * mint + x3 * t 55 | y23 = y2 * mint + y3 * t 56 | 57 | out_c1x = x01 * mint + x12 * t 58 | out_c1y = y01 * mint + y12 * t 59 | out_c2x = x12 * mint + x23 * t 60 | out_c2y = y12 * mint + y23 * t 61 | out_x = out_c1x * mint + out_c2x * t 62 | out_y = out_c1y * mint + out_c2y * t 63 | 64 | if not handles: 65 | return (out_x, out_y, out_c1x, out_c1y, out_c2x, out_c2y) 66 | else: 67 | return (out_x, out_y, out_c1x, out_c1y, out_c2x, out_c2y, x01, y01, x23, y23) 68 | 69 | def curvelength(x0, y0, x1, y1, x2, y2, x3, y3, n=20): 70 | 71 | """Returns the length of the spline. 72 | 73 | Integrates the estimated length of the cubic bezier spline 74 | defined by x0, y0, ... x3, y3, by adding the lengths of 75 | lineair lines between points at t. 76 | 77 | The number of points is defined by n 78 | (n=10 would add the lengths of lines between 0.0 and 0.1, 79 | between 0.1 and 0.2, and so on). 80 | 81 | The default n=20 is fine for most cases, usually 82 | resulting in a deviation of less than 0.01. 83 | """ 84 | 85 | length = 0 86 | xi = x0 87 | yi = y0 88 | 89 | for i in range(n): 90 | t = 1.0 * (i+1) / n 91 | pt_x, pt_y, pt_c1x, pt_c1y, pt_c2x, pt_c2y = \ 92 | curvepoint(t, x0, y0, x1, y1, x2, y2, x3, y3) 93 | c = sqrt(pow(abs(xi-pt_x),2) + pow(abs(yi-pt_y),2)) 94 | length += c 95 | xi = pt_x 96 | yi = pt_y 97 | 98 | return length 99 | -------------------------------------------------------------------------------- /nodebox/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/gui/__init__.py -------------------------------------------------------------------------------- /nodebox/gui/mac/AskString.py: -------------------------------------------------------------------------------- 1 | __all__ = ["AskString"] 2 | 3 | import objc 4 | from Foundation import * 5 | from AppKit import * 6 | 7 | # class defined in AskString.xib 8 | class AskStringWindowController(NSWindowController): 9 | questionLabel = objc.IBOutlet() 10 | textField = objc.IBOutlet() 11 | 12 | def __new__(cls, question, resultCallback, default="", parentWindow=None): 13 | self = cls.alloc().initWithWindowNibName_("AskString") 14 | self.question = question 15 | self.resultCallback = resultCallback 16 | self.default = default 17 | self.parentWindow = parentWindow 18 | if self.parentWindow is None: 19 | self.window().setFrameUsingName_("AskStringPanel") 20 | self.setWindowFrameAutosaveName_("AskStringPanel") 21 | self.showWindow_(self) 22 | else: 23 | NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_( 24 | self.window(), self.parentWindow, None, None, 0) 25 | self.retain() 26 | return self 27 | 28 | def windowWillClose_(self, notification): 29 | self.autorelease() 30 | 31 | def awakeFromNib(self): 32 | self.questionLabel.setStringValue_(self.question) 33 | self.textField.setStringValue_(self.default) 34 | 35 | def done(self): 36 | if self.parentWindow is None: 37 | self.close() 38 | else: 39 | sheet = self.window() 40 | NSApp().endSheet_(sheet) 41 | sheet.orderOut_(self) 42 | 43 | def ok_(self, sender): 44 | value = self.textField.stringValue() 45 | self.done() 46 | self.resultCallback(value) 47 | 48 | def cancel_(self, sender): 49 | self.done() 50 | self.resultCallback(None) 51 | 52 | 53 | def AskString(question, resultCallback, default="", parentWindow=None): 54 | AskStringWindowController(question, resultCallback, default, parentWindow) 55 | -------------------------------------------------------------------------------- /nodebox/gui/mac/ValueLadder.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | 4 | MAGICVAR = "__magic_var__" 5 | 6 | class ValueLadder: 7 | 8 | view = None 9 | visible = False 10 | value = None 11 | origValue = None 12 | dirty = False 13 | type = None 14 | negative = False 15 | unary = False 16 | add = False 17 | 18 | def __init__(self, textView, value, clickPos, screenPoint, viewPoint): 19 | self.textView = textView 20 | self.value = value 21 | self.origValue = value 22 | self.type = type(value) 23 | self.clickPos = clickPos 24 | self.origX, self.origY = screenPoint 25 | self.x, self.y = screenPoint 26 | self.viewPoint = viewPoint 27 | (x,y),(self.width,self.height) = self.textView.bounds() 28 | self.originalString = self.textView.string() 29 | self.backgroundColor = NSColor.colorWithCalibratedRed_green_blue_alpha_(0.4,0.4,0.4,1.0) 30 | self.strokeColor = NSColor.colorWithCalibratedRed_green_blue_alpha_(0.1,0.1,0.1, 1.0) 31 | self.textColor = NSColor.colorWithCalibratedRed_green_blue_alpha_(1.,1.,1.,1.) 32 | paraStyle = NSMutableParagraphStyle.alloc().init() 33 | paraStyle.setAlignment_(NSCenterTextAlignment) 34 | font = NSFont.fontWithName_size_("Monaco", 10) 35 | self.textAttributes = {NSForegroundColorAttributeName:self.textColor,NSParagraphStyleAttributeName:paraStyle,NSFontAttributeName:font} 36 | 37 | # To speed things up, the code is compiled only once. 38 | # The number is replaced with a magic variable, that is set in the 39 | # namespace when executing the code. 40 | begin,end = self.clickPos 41 | self.patchedSource = self.originalString[:begin] + MAGICVAR + self.originalString[end:] 42 | 43 | 44 | #ast = parse(self.patchedSource + "\n\n") 45 | #self._checkSigns(ast) 46 | success, output = self.textView.document._boxedRun(self._parseAndCompile) 47 | if success: 48 | self.show() 49 | else: 50 | self.textView.document._flushOutput(output) 51 | 52 | def _parseAndCompile(self): 53 | from compiler import parse 54 | ast = parse(self.patchedSource.encode('ascii', 'replace') + "\n\n") 55 | self._checkSigns(ast) 56 | self.textView.document._compileScript(self.patchedSource) 57 | 58 | def _checkSigns(self, node): 59 | """Recursively check for special sign cases. 60 | 61 | The following cases are special: 62 | - Substraction. When you select the last part of a substraction 63 | (e.g. the 5 of "10-5"), it might happen that you drag the number to 64 | a positive value. In that case, the result should be "10+5". 65 | - Unary substraction. Values like "-5" should have their sign removed 66 | when you drag them to a positive value. 67 | - Addition. When you select the last part of an addition 68 | (e.g. the 5 of "10+5"), and drag the number to a negative value, 69 | the result should be "10-5". 70 | 71 | This algorithm checks for these cases. It tries to find the magic var, 72 | and then checks the parent node to see if it is one of these cases, 73 | then sets the appropriate state variables in the object. 74 | 75 | This algorithm is recursive. Because we have to differ between a 76 | "direct hit" (meaning the current child was the right one) and a 77 | "problem resolved" (meaning the algorithm found the node, did its 78 | work and now needs to bail out), we have three return codes: 79 | - -1: nothing was found in this node and its child nodes. 80 | - 1: direct hit. The child you just searched contains the magicvar. 81 | check the current node to see if it is one of the special cases. 82 | - 0: bail out. Somewhere, a child contained the magicvar, and we 83 | acted upon it. Now leave this algorithm as soon as possible. 84 | """ 85 | 86 | from compiler.ast import Sub, UnarySub, Add 87 | 88 | # Check whether I am the correct node 89 | try: 90 | if node.name == MAGICVAR: 91 | return 1 # If i am, return the "direct hit" code. 92 | except AttributeError: 93 | pass 94 | 95 | # We keep an index to see what child we are checking. This 96 | # is important for binary operations, were we are only interested 97 | # in the second part. ("a-10" has to change to "a+10", 98 | # but "10-a" shouldn't change to "+10-a") 99 | index = 0 100 | # Recursively check my children 101 | for child in node.getChildNodes(): 102 | retVal = self._checkSigns(child) 103 | # Direct hit. The child I just searched contains the magicvar. 104 | # Check whether this node is one of the special cases. 105 | if retVal == 1: 106 | # Unary substitution. 107 | if isinstance(node, UnarySub): 108 | self.negative = True 109 | self.unary = True 110 | # Binary substitution. Only the second child is of importance. 111 | elif isinstance(node, Sub) and index == 1: 112 | self.negative = True 113 | # Binary addition. Only the second child is of importance. 114 | elif isinstance(node, Add) and index == 1: 115 | self.add = True 116 | # Return the "bail out" code, whether we found some 117 | # special case or not. There can only be one magicvar in the 118 | # code, so once that is found we can stop looking. 119 | return 0 120 | # If the child returns a bail out code, we leave this routine 121 | # without checking the other children, passing along the 122 | # bail out code. 123 | elif retVal == 0: 124 | return 0 # Nothing more needs to be done. 125 | 126 | # Next child. 127 | index += 1 128 | 129 | # We searched all children, but couldn't find any magicvars. 130 | return -1 131 | 132 | def show(self): 133 | self.visible = True 134 | self.textView.setNeedsDisplay_(True) 135 | NSCursor.hide() 136 | 137 | def hide(self): 138 | """Hide the ValueLadder and update the code. 139 | 140 | Updating the code means we have to replace the current value with 141 | the new value, and account for any special cases.""" 142 | 143 | self.visible = False 144 | begin,end = self.clickPos 145 | 146 | # Potentionally change the sign on the number. 147 | # The following cases are valid: 148 | # - A subtraction where the value turned positive "random(5-8)" --> "random(5+8)" 149 | # - A unary subtraction where the value turned positive "random(-5)" --> "random(5)" 150 | # Note that the sign dissapears here. 151 | # - An addition where the second part turns negative "random(5+8)" --> "random(5-8)" 152 | # Note that the code replaces the sign on the place where it was, leaving the code intact. 153 | 154 | # Case 1: Negative numbers where the new value is negative as well. 155 | # This means the numbers turn positive. 156 | if self.negative and self.value < 0: 157 | # Find the minus sign. 158 | i = begin - 1 159 | notFound = True 160 | while True: 161 | if self.originalString[i] == '-': 162 | if self.unary: # Unary subtractions will have the sign removed. 163 | # Re-create the string: the spaces between the value and the '-' + the value 164 | value = self.originalString[i+1:begin] + str(abs(self.value)) 165 | else: # Binary subtractions get a '+' 166 | value = '+' + self.originalString[i+1:begin] + str(abs(self.value)) 167 | range = (i,end-i) 168 | break 169 | i -= 1 170 | # Case 2: Additions (only additions where we are the second part 171 | # interests us, this is checked already on startup) 172 | elif self.add and self.value < 0: 173 | # Find the plus sign. 174 | i = begin - 1 175 | notFound = True 176 | while True: 177 | if self.originalString[i] == '+': 178 | # Re-create the string: 179 | # - a '+' (instead of the minus) 180 | # - the spaces between the '-' and the constant 181 | # - the constant itself 182 | value = '-' + self.originalString[i+1:begin] + str(abs(self.value)) 183 | range = (i,end-i) 184 | break 185 | i -= 1 186 | # Otherwise, it's a normal case. Note that here also, positive numbers 187 | # can turn negative, but no existing signs have to be changed. 188 | else: 189 | value = str(self.value) 190 | range = (begin, end-begin) 191 | 192 | # The following textView methods make sure that an undo operation 193 | # is registered, so users can undo their drag. 194 | self.textView.shouldChangeTextInRange_replacementString_(range, value) 195 | self.textView.textStorage().replaceCharactersInRange_withString_(range, value) 196 | self.textView.didChangeText() 197 | self.textView.setNeedsDisplay_(True) 198 | self.textView.document.currentView.direct = False 199 | NSCursor.unhide() 200 | 201 | def draw(self): 202 | mx,my=self.viewPoint 203 | 204 | x = mx-20 205 | w = 80 206 | h = 20 207 | h2 = h*2 208 | 209 | context = NSGraphicsContext.currentContext() 210 | aa = context.shouldAntialias() 211 | context.setShouldAntialias_(False) 212 | r = ((mx-w/2,my+12),(w,h)) 213 | NSBezierPath.setDefaultLineWidth_(0) 214 | self.backgroundColor.set() 215 | NSBezierPath.fillRect_(r) 216 | self.strokeColor.set() 217 | NSBezierPath.strokeRect_(r) 218 | 219 | # A standard value just displays the value that you have been dragging. 220 | if not self.negative: 221 | v = str(self.value) 222 | # When the value is negative, we don't display a double negative, 223 | # but a positive. 224 | elif self.value < 0: 225 | v = str(abs(self.value)) 226 | # When the value is positive, we have to add a minus sign. 227 | else: 228 | v = "-" + str(self.value) 229 | 230 | NSString.drawInRect_withAttributes_(v, ((mx-w/2,my+14),(w,h2)), self.textAttributes) 231 | context.setShouldAntialias_(aa) 232 | 233 | def mouseDragged_(self, event): 234 | mod = event.modifierFlags() 235 | newX, newY = NSEvent.mouseLocation() 236 | deltaX = newX-self.x 237 | delta = deltaX 238 | if self.negative: 239 | delta = -delta 240 | if mod & NSAlternateKeyMask: 241 | delta /= 100.0 242 | elif mod & NSShiftKeyMask: 243 | delta *= 10.0 244 | self.value = self.type(self.value + delta) 245 | self.x, self.y = newX, newY 246 | self.dirty = True 247 | self.textView.setNeedsDisplay_(True) 248 | self.textView.document.magicvar = self.value 249 | self.textView.document.currentView.direct = True 250 | self.textView.document.runScriptFast() 251 | -------------------------------------------------------------------------------- /nodebox/gui/mac/dashboard.py: -------------------------------------------------------------------------------- 1 | #from PyObjCTools import NibClassBuilder, AppHelper 2 | from PyObjCTools import AppHelper 3 | from AppKit import * 4 | import objc 5 | 6 | from nodebox import graphics 7 | 8 | SMALL_FONT = NSFont.systemFontOfSize_(NSFont.smallSystemFontSize()) 9 | MINI_FONT = NSFont.systemFontOfSize_(NSFont.systemFontSizeForControlSize_(NSMiniControlSize)) 10 | 11 | # class defined in NodeBoxDocument.xib 12 | class DashboardController(NSObject): 13 | document = objc.IBOutlet() 14 | documentWindow = objc.IBOutlet() 15 | panel = objc.IBOutlet() 16 | 17 | def clearInterface(self): 18 | for s in list(self.panel.contentView().subviews()): 19 | s.removeFromSuperview() 20 | 21 | def numberChanged_(self, sender): 22 | var = self.document.vars[sender.tag()] 23 | var.value = sender.floatValue() 24 | self.document.runScript(compile=False, newSeed=False) 25 | 26 | def textChanged_(self, sender): 27 | var = self.document.vars[sender.tag()] 28 | var.value = sender.stringValue() 29 | self.document.runScript(compile=False, newSeed=False) 30 | 31 | def booleanChanged_(self, sender): 32 | var = self.document.vars[sender.tag()] 33 | if sender.state() == NSOnState: 34 | var.value = True 35 | else: 36 | var.value = False 37 | self.document.runScript(compile=False, newSeed=False) 38 | 39 | def buttonClicked_(self, sender): 40 | var = self.document.vars[sender.tag()] 41 | self.document.fastRun(self.document.namespace[var.name], newSeed=True) 42 | #self.document.runFunction_(var.name) 43 | 44 | def buildInterface(self, vars): 45 | self.vars = vars 46 | self.clearInterface() 47 | if len(self.vars) > 0: 48 | self.panel.orderFront_(None) 49 | else: 50 | self.panel.orderOut_(None) 51 | return 52 | 53 | # Set the title of the parameter panel to the title of the window 54 | self.panel.setTitle_(self.documentWindow.title()) 55 | 56 | (px,py),(pw,ph) = self.panel.frame() 57 | # Height of the window. Each element has a height of 21. 58 | # The extra "fluff" is 38 pixels. 59 | ph = len(self.vars) * 21 + 54 60 | # Start of first element 61 | # First element is the height minus the fluff. 62 | y = ph - 49 63 | cnt = 0 64 | for v in self.vars: 65 | if v.type == graphics.NUMBER: 66 | self._addLabel(v, y, cnt) 67 | self._addSlider(v, y, cnt) 68 | elif v.type == graphics.TEXT: 69 | self._addLabel(v, y, cnt) 70 | self._addTextField(v, y, cnt) 71 | elif v.type == graphics.BOOLEAN: 72 | self._addSwitch(v, y, cnt) 73 | elif v.type == graphics.BUTTON: 74 | self._addButton(v, y, cnt) 75 | y -= 21 76 | cnt += 1 77 | self.panel.setFrame_display_animate_( ((px,py),(pw,ph)), True, True ) 78 | 79 | def _addLabel(self, v, y, cnt): 80 | control = NSTextField.alloc().init() 81 | control.setFrame_(((0,y),(100,13))) 82 | control.setStringValue_(v.name + ":") 83 | control.setAlignment_(NSRightTextAlignment) 84 | control.setEditable_(False) 85 | control.setBordered_(False) 86 | control.setDrawsBackground_(False) 87 | control.setFont_(SMALL_FONT) 88 | self.panel.contentView().addSubview_(control) 89 | 90 | def _addSlider(self, v, y, cnt): 91 | control = NSSlider.alloc().init() 92 | control.setMaxValue_(v.max) 93 | control.setMinValue_(v.min) 94 | control.setFloatValue_(v.value) 95 | control.setFrame_(((108,y-1),(172,13))) 96 | control.cell().setControlSize_(NSMiniControlSize) 97 | control.cell().setControlTint_(NSGraphiteControlTint) 98 | control.setContinuous_(True) 99 | control.setTarget_(self) 100 | control.setTag_(cnt) 101 | control.setAction_(objc.selector(self.numberChanged_, signature="v@:@@")) 102 | self.panel.contentView().addSubview_(control) 103 | 104 | def _addTextField(self, v, y, cnt): 105 | control = NSTextField.alloc().init() 106 | control.setStringValue_(v.value) 107 | control.setFrame_(((108,y-2),(172,15))) 108 | control.cell().setControlSize_(NSMiniControlSize) 109 | control.cell().setControlTint_(NSGraphiteControlTint) 110 | control.setFont_(MINI_FONT) 111 | control.setTarget_(self) 112 | control.setTag_(cnt) 113 | control.setAction_(objc.selector(self.textChanged_, signature="v@:@@")) 114 | self.panel.contentView().addSubview_(control) 115 | 116 | def _addSwitch(self, v, y, cnt): 117 | control = NSButton.alloc().init() 118 | control.setButtonType_(NSSwitchButton) 119 | if v.value: 120 | control.setState_(NSOnState) 121 | else: 122 | control.setState_(NSOffState) 123 | control.setFrame_(((108,y-2),(172,16))) 124 | control.setTitle_(v.name) 125 | control.setFont_(SMALL_FONT) 126 | control.cell().setControlSize_(NSSmallControlSize) 127 | control.cell().setControlTint_(NSGraphiteControlTint) 128 | control.setTarget_(self) 129 | control.setTag_(cnt) 130 | control.setAction_(objc.selector(self.booleanChanged_, signature="v@:@@")) 131 | self.panel.contentView().addSubview_(control) 132 | 133 | def _addButton(self, v, y, cnt): 134 | control = NSButton.alloc().init() 135 | control.setFrame_(((108, y-2),(172,16))) 136 | control.setTitle_(v.name) 137 | control.setBezelStyle_(1) 138 | control.setFont_(SMALL_FONT) 139 | control.cell().setControlSize_(NSMiniControlSize) 140 | control.cell().setControlTint_(NSGraphiteControlTint) 141 | control.setTarget_(self) 142 | control.setTag_(cnt) 143 | control.setAction_(objc.selector(self.buttonClicked_, signature="v@:@@")) 144 | self.panel.contentView().addSubview_(control) 145 | -------------------------------------------------------------------------------- /nodebox/gui/mac/preferences.py: -------------------------------------------------------------------------------- 1 | from AppKit import * 2 | from Foundation import * 3 | from PyDETextView import getBasicTextAttributes, getSyntaxTextAttributes 4 | from PyDETextView import setTextFont, setBasicTextAttributes, setSyntaxTextAttributes 5 | 6 | # class defined in NodeBoxPreferences.xib 7 | class NodeBoxPreferencesController(NSWindowController): 8 | commentsColorWell = objc.IBOutlet() 9 | fontPreview = objc.IBOutlet() 10 | funcClassColorWell = objc.IBOutlet() 11 | keywordsColorWell = objc.IBOutlet() 12 | stringsColorWell = objc.IBOutlet() 13 | 14 | def init(self): 15 | self = self.initWithWindowNibName_("NodeBoxPreferences") 16 | self.setWindowFrameAutosaveName_("NodeBoxPreferencesPanel") 17 | self.timer = None 18 | return self 19 | 20 | def awakeFromNib(self): 21 | self.textFontChanged_(None) 22 | syntaxAttrs = syntaxAttrs = getSyntaxTextAttributes() 23 | self.stringsColorWell.setColor_(syntaxAttrs["string"][NSForegroundColorAttributeName]) 24 | self.keywordsColorWell.setColor_(syntaxAttrs["keyword"][NSForegroundColorAttributeName]) 25 | self.funcClassColorWell.setColor_(syntaxAttrs["identifier"][NSForegroundColorAttributeName]) 26 | self.commentsColorWell.setColor_(syntaxAttrs["comment"][NSForegroundColorAttributeName]) 27 | 28 | nc = NSNotificationCenter.defaultCenter() 29 | nc.addObserver_selector_name_object_(self, "textFontChanged:", "PyDETextFontChanged", None) 30 | 31 | def windowWillClose_(self, notification): 32 | fm = NSFontManager.sharedFontManager() 33 | fp = fm.fontPanel_(False) 34 | if fp is not None: 35 | fp.setDelegate_(None) 36 | fp.close() 37 | 38 | @objc.IBAction 39 | def updateColors_(self, sender): 40 | if self.timer is not None: 41 | self.timer.invalidate() 42 | self.timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 43 | 1.0, self, "timeToUpdateTheColors:", None, False) 44 | 45 | def timeToUpdateTheColors_(self, sender): 46 | syntaxAttrs = getSyntaxTextAttributes() 47 | syntaxAttrs["string"][NSForegroundColorAttributeName] = self.stringsColorWell.color() 48 | syntaxAttrs["keyword"][NSForegroundColorAttributeName] = self.keywordsColorWell.color() 49 | syntaxAttrs["identifier"][NSForegroundColorAttributeName] = self.funcClassColorWell.color() 50 | syntaxAttrs["comment"][NSForegroundColorAttributeName] = self.commentsColorWell.color() 51 | setSyntaxTextAttributes(syntaxAttrs) 52 | 53 | @objc.IBAction 54 | def chooseFont_(self, sender): 55 | fm = NSFontManager.sharedFontManager() 56 | basicAttrs = getBasicTextAttributes() 57 | fm.setSelectedFont_isMultiple_(basicAttrs[NSFontAttributeName], False) 58 | fm.orderFrontFontPanel_(sender) 59 | fp = fm.fontPanel_(False) 60 | fp.setDelegate_(self) 61 | 62 | @objc.IBAction 63 | def changeFont_(self, sender): 64 | oldFont = getBasicTextAttributes()[NSFontAttributeName] 65 | newFont = sender.convertFont_(oldFont) 66 | if oldFont != newFont: 67 | setTextFont(newFont) 68 | 69 | def textFontChanged_(self, notification): 70 | basicAttrs = getBasicTextAttributes() 71 | font = basicAttrs[NSFontAttributeName] 72 | self.fontPreview.setFont_(font) 73 | size = font.pointSize() 74 | if size == int(size): 75 | size = int(size) 76 | s = u"%s %s" % (font.displayName(), size) 77 | self.fontPreview.setStringValue_(s) 78 | -------------------------------------------------------------------------------- /nodebox/gui/mac/progressbar.py: -------------------------------------------------------------------------------- 1 | import objc 2 | from Foundation import * 3 | from AppKit import * 4 | 5 | class ProgressBarController(NSWindowController): 6 | messageField = objc.IBOutlet() 7 | progressBar = objc.IBOutlet() 8 | 9 | def init(self): 10 | NSBundle.loadNibNamed_owner_("ProgressBarSheet", self) 11 | return self 12 | 13 | def begin(self, message, maxval): 14 | self.value = 0 15 | self.message = message 16 | self.maxval = maxval 17 | self.progressBar.setMaxValue_(self.maxval) 18 | self.messageField.cell().setTitle_(self.message) 19 | parentWindow = NSApp().keyWindow() 20 | NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(self.window(), parentWindow, self, None, 0) 21 | 22 | def inc(self): 23 | self.value += 1 24 | self.progressBar.setDoubleValue_(self.value) 25 | date = NSDate.dateWithTimeIntervalSinceNow_(0.01) 26 | NSRunLoop.currentRunLoop().acceptInputForMode_beforeDate_(NSDefaultRunLoopMode, date) 27 | 28 | def end(self): 29 | NSApp().endSheet_(self.window()) 30 | self.window().orderOut_(self) 31 | -------------------------------------------------------------------------------- /nodebox/gui/mac/util.py: -------------------------------------------------------------------------------- 1 | from Foundation import * 2 | from AppKit import * 3 | 4 | def errorAlert(msgText, infoText): 5 | # Force NSApp initialisation. 6 | NSApplication.sharedApplication().activateIgnoringOtherApps_(0) 7 | alert = NSAlert.alloc().init() 8 | alert.setMessageText_(msgText) 9 | alert.setInformativeText_(infoText) 10 | alert.setAlertStyle_(NSCriticalAlertStyle) 11 | btn = alert.addButtonWithTitle_("OK") 12 | return alert.runModal() 13 | -------------------------------------------------------------------------------- /nodebox/macboot.py: -------------------------------------------------------------------------------- 1 | # Startup file for the NodeBox OS X application 2 | # PyObjC requires the startup file to be in the root folder. 3 | # This just imports everything from the nodebox.gui.mac module 4 | # and works from there 5 | 6 | from nodebox.gui.mac import AppHelper 7 | AppHelper.runEventLoop 8 | -------------------------------------------------------------------------------- /nodebox/macbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Builds the NodeBox Mac OS X distribution folder. 3 | 4 | # Remove dist and build 5 | rm -rf dist build 6 | 7 | # First, make the help files. 8 | cd ../helpsrc 9 | python makehelp.py 10 | 11 | # Now build the application. 12 | cd ../src 13 | python macsetup.py py2app 14 | 15 | # Now make a distro directory that will contain all the files. 16 | mkdir -p dist/NodeBox 17 | cd dist/NodeBox 18 | mv ../NodeBox.app . 19 | cp ../../Changes.txt . 20 | cp ../../ReadMe.txt . 21 | svn export http://dev.nodebox.net/svn/nodebox/trunk/examples/ Examples 22 | 23 | # Zip the distro. 24 | cd ../ 25 | zip -r NodeBox-latest.zip NodeBox 26 | 27 | # All done. 28 | cd ../ -------------------------------------------------------------------------------- /nodebox/macsetup.py: -------------------------------------------------------------------------------- 1 | # Setup file for the NodeBox OS X application. 2 | # 3 | # You can create the application by running: 4 | # 5 | # python macsetup.py py2app -A 6 | # 7 | # This creates a linked distribution, so changes to the python files only require you to restart the application. 8 | # This is the preferred way to develop. 9 | # To create a full distribution, use the macbuild.sh shell script to pull in all the right files. 10 | 11 | #from distutils.core import setup, Extension 12 | 13 | import py2app 14 | from distutils.core import setup, Extension 15 | 16 | from setup import VERSION, NAME, packages, ext_modules 17 | packages.extend(['nodebox.gui', 'nodebox.gui.mac']) 18 | 19 | # Useful site packages 20 | includes = ['Image', 'numpy', 'psyco'] 21 | 22 | plist = dict( 23 | CFBundleDocumentTypes = [ 24 | dict( 25 | CFBundleTypeExtensions = ["py"], 26 | CFBundleTypeName = "Python Source File", 27 | CFBundleTypeRole = "Editor", 28 | NSDocumentClass = "NodeBoxDocument", 29 | CFBundleTypeIconFile = "NodeBoxFile.icns", 30 | ), 31 | ], 32 | CFBundleIconFile = NAME + '.icns', 33 | CFBundleName = NAME, 34 | CFBundleShortVersionString = VERSION, 35 | CFBundleVersion='', 36 | CFBundleGetInfoString= VERSION, 37 | CFBundleExecutable = NAME, 38 | LSMinimumSystemVersion = "10.5.0", 39 | CFBundleIdentifier = 'net.nodebox.NodeBox', 40 | CFBundleSignature = 'NdBx', 41 | CFBundleInfoDictionaryVersion=VERSION, 42 | NSHumanReadableCopyright= u'Copyright © 2003-2009 NodeBox. All Rights Reserved.', 43 | CFBundleHelpBookFolder='NodeBox Help', 44 | CFBundleHelpBookName='NodeBox Help', 45 | ) 46 | py2app_options = { 47 | 'optimize': 2, 48 | 'iconfile': 'Resources/NodeBox.icns', 49 | 'site_packages': True, 50 | 'includes': includes, 51 | } 52 | 53 | if __name__=='__main__': 54 | 55 | setup(name = NAME, 56 | version = VERSION, 57 | data_files = [ 58 | 'Resources/English.lproj', 59 | 'Resources/Credits.rtf', 60 | 'Resources/NodeBox.icns', 61 | 'Resources/NodeBoxFile.icns', 62 | 'Resources/zoombig.png', 63 | 'Resources/zoomsmall.png', 64 | ], 65 | app = [dict(script='macboot.py', plist=plist)], 66 | packages = packages, 67 | ext_modules = ext_modules, 68 | options = {'py2app':py2app_options}, 69 | ), 70 | -------------------------------------------------------------------------------- /nodebox/setup.py: -------------------------------------------------------------------------------- 1 | # This is a setup file for a command-line version of NodeBox. 2 | # If you want to work on the Mac OS X version, go look in macsetup.py. 3 | 4 | # This is your standard setup.py, so to install the package, use: 5 | # python setup.py install 6 | 7 | # We require some dependencies: 8 | # - PyObjC 9 | # - psyco 10 | # - py2app 11 | # - cPathMatics (included in the "libs" folder) 12 | # - polymagic (included in the "libs" folder) 13 | # - Numeric (included in the "libs" folder) 14 | # - Numpy (installable using "easy_install numpy") 15 | 16 | from distutils.core import setup, Extension 17 | import nodebox 18 | 19 | NAME = 'NodeBox' 20 | VERSION = nodebox.__version__ 21 | 22 | AUTHOR = "Frederik De Bleser", 23 | AUTHOR_EMAIL = "frederik@pandora.be", 24 | URL = "http://nodebox.net/", 25 | CLASSIFIERS = ( 26 | "Development Status :: 5 - Production/Stable", 27 | "Environment :: MacOS X :: Cocoa", 28 | "Intended Audience :: Developers", 29 | "Intended Audience :: Education", 30 | "Intended Audience :: End Users/Desktop", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: MacOS :: MacOS X", 33 | "Programming Language :: Python", 34 | "Topic :: Artistic Software", 35 | "Topic :: Multimedia :: Graphics", 36 | "Topic :: Multimedia :: Graphics :: Editors :: Vector-Based", 37 | "Topic :: Multimedia :: Video", 38 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 39 | "Topic :: Software Development :: User Interfaces", 40 | "Topic :: Text Editors :: Integrated Development Environments (IDE)", 41 | ) 42 | 43 | DESCRIPTION = "Simple application for creating 2-dimensional graphics and animation using Python code" 44 | LONG_DESCRIPTION = """NodeBox is a Mac OS X application that allows you to create visual output 45 | with programming code. The application targets an audience of designers, with an easy set of state 46 | commands that is both intuitive and creative. It is essentially a learning environment and an automation tool. 47 | 48 | The current version features: 49 | 50 | * State-based graphics context 51 | * Extensive reference documentation and tutorials 52 | * PDF export for graphics 53 | * QuickTime export for animations 54 | * Manipulate every numeric variable in a script by command-dragging it, even during animation 55 | * Creating simple user interfaces using text fields, sliders, and buttons 56 | * Stop a running script by typing command-period 57 | * Universal Binary 58 | * Integrated bezier mathematics and boolean operations 59 | * Command-line interface 60 | * Zooming 61 | """ 62 | 63 | ext_modules = [ 64 | Extension('cGeo', ['libs/cGeo/cGeo.c']), 65 | Extension('cPathmatics', ['libs/pathmatics/pathmatics.c']), 66 | Extension('cPolymagic', ['libs/polymagic/gpc.c', 'libs/polymagic/polymagic.m'], extra_link_args=['-framework', 'AppKit', '-framework', 'Foundation']) 67 | ] 68 | 69 | packages = ['nodebox', 'nodebox.graphics', 'nodebox.util', 'nodebox.geo'] 70 | 71 | if __name__=='__main__': 72 | 73 | setup(name = NAME, 74 | version = VERSION, 75 | description = DESCRIPTION, 76 | long_description = LONG_DESCRIPTION, 77 | author = AUTHOR, 78 | author_email = AUTHOR_EMAIL, 79 | url = URL, 80 | classifiers = CLASSIFIERS, 81 | ext_modules = ext_modules, 82 | packages = packages 83 | ) 84 | 85 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/001-basic-primitives.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/001-basic-primitives.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/001-basic-primitives.py: -------------------------------------------------------------------------------- 1 | size(300, 300) 2 | 3 | _total_w = 0 4 | def flow(w, h): 5 | global _total_w 6 | if _total_w + w*2 >= WIDTH: 7 | translate(-_total_w, h) 8 | _total_w = 0 9 | else: 10 | translate(w, 0) 11 | _total_w += w 12 | 13 | x, y = 10, 10 14 | rect(x, y, 50, 50) 15 | flow(60, 60) 16 | rect(x, y, 50, 50, 0.6) 17 | flow(60, 60) 18 | oval(x, y, 50, 50) 19 | flow(60, 60) 20 | star(x+25, y+25, 20, outer=25, inner=15) 21 | flow(60, 60) 22 | arrow(x+50, y+25, 50) 23 | flow(60, 60) 24 | arrow(x+50, y, 50, type=FORTYFIVE) 25 | flow(60, 60) 26 | oval(x, y, 50, 50) 27 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/001-basic-primitives.result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/001-basic-primitives.result.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/002-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/002-text.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/002-text.py: -------------------------------------------------------------------------------- 1 | size(300, 300) 2 | x, y = 10, 30 3 | fontsize(12) 4 | 5 | text("Hello", x, y) 6 | drawpath(textpath("Hello", x + 100, y)) 7 | rect(x, y-12,50,20, fill=None, stroke=0) 8 | rect(x + 100, y-12,50,20, fill=None, stroke=0) 9 | 10 | y += 30 11 | align(LEFT) 12 | text("Hello", x, y, width=50) 13 | drawpath(textpath("Hello", x + 100, y, width=50)) 14 | rect(x, y-12,50,20, fill=None, stroke=0) 15 | rect(x + 100, y-12,50,20, fill=None, stroke=0) 16 | 17 | y += 30 18 | align(CENTER) 19 | text("Hello", x, y, width=50) 20 | drawpath(textpath("Hello", x + 100, y, width=50)) 21 | rect(x, y-12,50,20, fill=None, stroke=0) 22 | rect(x + 100, y-12,50,20, fill=None, stroke=0) 23 | 24 | y += 30 25 | align(RIGHT) 26 | text("Hello", x, y, width=50) 27 | drawpath(textpath("Hello", x + 100, y, width=50)) 28 | rect(x, y-12,50,20, fill=None, stroke=0) 29 | rect(x + 100, y-12,50,20, fill=None, stroke=0) 30 | 31 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/003-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/003-color.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/003-color.py: -------------------------------------------------------------------------------- 1 | size(300, 300) 2 | 3 | def draw_rect(*args, **kwargs): 4 | r = rect(0, 0, 12, 42) # Draw default square 5 | r = rect(20, 0, 42, 42, *args, **kwargs) 6 | return r 7 | 8 | _counter = 1 9 | def move(): 10 | global _counter 11 | text(_counter, 80, 25, fontsize=12, fill=0) 12 | _counter += 1 13 | translate(0, 50) 14 | 15 | # 1: Give the color using a tuple 16 | draw_rect(fill=(1, 0, 0)) 17 | move() 18 | 19 | # 2: Give the color using a Color object 20 | draw_rect(fill=Color(1, 0, 0)) 21 | move() 22 | 23 | # 3: Use HSB Colors 24 | colormode(HSB) 25 | draw_rect(fill=Color(0, 1, 1)) 26 | move() 27 | 28 | # 4: Use CMYK Colors 29 | colormode(CMYK) 30 | draw_rect(fill=Color(0, 0, 0, 1)) 31 | move() 32 | 33 | # 5: Color range 34 | colormode(RGB) 35 | colorrange(255) 36 | draw_rect(fill=(128, 0, 0)) # One red component 37 | move() 38 | 39 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/004-state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/004-state.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/004-state.py: -------------------------------------------------------------------------------- 1 | # State semantics 2 | 3 | size(300, 300) 4 | 5 | _total_w = 0 6 | _counter = 1 7 | def flow(w=50, h=70): 8 | global _total_w 9 | global _counter 10 | text(_counter, 17, 60, fontsize=12, fill=0) 11 | _counter += 1 12 | if _total_w + w*2 >= WIDTH: 13 | translate(-_total_w, h) 14 | _total_w = 0 15 | else: 16 | translate(w, 0) 17 | _total_w += w 18 | 19 | def draw_rect(*args, **kwargs): 20 | r = rect(0, 0, 42, 42, *args, **kwargs) 21 | return r 22 | 23 | # 1: The current state specifies the color of an object. 24 | fill(1, 0, 0) 25 | draw_rect() 26 | flow() 27 | 28 | # 2: The current state can be overridden by direct access. 29 | draw_rect(fill=(0, 0, 1)) 30 | flow() 31 | 32 | # 3: You can also change things "after the facts" 33 | r = draw_rect() 34 | r.fill = 0, 0, 1 35 | flow() 36 | 37 | # 4: Re-inheriting from the context 38 | r = draw_rect() # Would be red 39 | fill(0, 1, 0) # The current fill is now green 40 | r.inheritFromContext() # Copy the fill 41 | flow() 42 | 43 | # 5: By default, objects don't inherit from the context 44 | pt = BezierPath() 45 | pt.rect(200, 0, 42, 42) # Note that I set the position directly 46 | pt.draw() 47 | flow() 48 | 49 | # 6: You have to let them inherit 50 | pt = BezierPath() 51 | pt.rect(0, 0, 42, 42) 52 | pt.inheritFromContext() 53 | pt.draw() 54 | flow() 55 | 56 | # 7: Drawpath does a inheritFromContext 57 | pt = BezierPath() 58 | pt.rect(0, 0, 42, 42) 59 | pt.fill = 0, 0, 1 # This gets overridden by inheritFromContext 60 | drawpath(pt) 61 | flow() 62 | 63 | # 8: Drawpath can also override attributes 64 | pt = BezierPath() 65 | pt.rect(0, 0, 42, 42) 66 | drawpath(pt, fill=(0, 0, 1)) 67 | flow() 68 | 69 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/005-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/005-path.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/005-path.py: -------------------------------------------------------------------------------- 1 | # Path semantics 2 | 3 | size(300, 300) 4 | 5 | _total_w = 0 6 | _counter = 1 7 | def flow(w=50, h=70): 8 | global _total_w 9 | global _counter 10 | text(_counter, 17, 60, fontsize=12, fill=0) 11 | _counter += 1 12 | if _total_w + w*2 >= WIDTH: 13 | translate(-_total_w, h) 14 | _total_w = 0 15 | else: 16 | translate(w, 0) 17 | _total_w += w 18 | 19 | def draw_rect(*args, **kwargs): 20 | r = rect(0, 0, 42, 42, *args, **kwargs) 21 | return r 22 | 23 | # 1: A basic bezier path filled with a color 24 | pt = BezierPath(fill=(1,0,0)) 25 | pt.rect(0, 0, 42, 42) 26 | pt.draw() 27 | flow() 28 | 29 | # 2: Using the transform bezier path method should copy color 30 | trans = Transform() 31 | trans.translate(50, 0) 32 | pt2 = trans.transformBezierPath(pt) 33 | pt2.draw() 34 | flow() 35 | -------------------------------------------------------------------------------- /nodebox/tests/graphics/006-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/006-text.png -------------------------------------------------------------------------------- /nodebox/tests/graphics/006-text.py: -------------------------------------------------------------------------------- 1 | size(300, 300) 2 | fontsize(40) 3 | fill(0, 0.5) 4 | stroke(0) 5 | line(0,150, WIDTH, 150) 6 | line(10, 0, 10, HEIGHT) 7 | text("hamburgevons", 10, 150) 8 | nofill() 9 | p = text("hamburgevons", 10, 150, outline=True) 10 | (x, y), (w,h) = p.bounds 11 | rect(x, y, w, h) -------------------------------------------------------------------------------- /nodebox/tests/graphics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/__init__.py -------------------------------------------------------------------------------- /nodebox/tests/graphics/runtests.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import os 3 | import re 4 | from glob import glob 5 | 6 | import sys 7 | sys.path.insert(0, '../..') 8 | 9 | from nodebox.console import make_image 10 | from nodebox.util import vdiff 11 | 12 | test_re = re.compile("^[0-9]{3}.*.py$") 13 | 14 | class CompareError(Exception): 15 | def __init__(self, scriptname, stats): 16 | self.scriptname = scriptname 17 | self.stats = stats 18 | def __str__(self): 19 | return "%s: Images don't match: %s" % (self.scriptname, self.stats) 20 | 21 | all_python_files = glob("*.py") 22 | test_files = [file for file in all_python_files if test_re.match(file)] 23 | 24 | # Remove and re-create the results directory 25 | try: 26 | shutil.rmtree("_results") 27 | except OSError, e: 28 | if e.errno != 2: # The directory doesn't exist yet 29 | raise e 30 | RESULTS_DIR = "_results" 31 | os.mkdir("_results") 32 | 33 | stats_list = [] 34 | differences = False 35 | # Test all the scripts one by one. 36 | for test_file in test_files: 37 | basename = os.path.splitext(test_file)[0] 38 | ref_image = "%s.png" % basename 39 | result_image = os.path.join(RESULTS_DIR, "%s.result.png" % basename) 40 | test_script = open(test_file).read() 41 | make_image(test_script, result_image) 42 | 43 | stats = vdiff.Statistics(ref_image, result_image, name=basename) 44 | stats.save_comparison_image(os.path.join(RESULTS_DIR, "%s.compare.png" % basename)) 45 | stats.fname1 = "../" + stats.fname1 # This will link to the original file 46 | stats.fname2 = os.path.split(stats.fname2)[1] # This will link to a file in the same folder 47 | stats.comparison_image_fname = os.path.split(stats.comparison_image_fname)[1] # This will link to a file in the same folder 48 | stats_list.append(stats) 49 | 50 | if stats.number_of_differences > 0: 51 | differences = True 52 | print "E", 53 | else: 54 | print ".", 55 | print 56 | 57 | html = vdiff.format_stats_list(stats_list) 58 | open(os.path.join(RESULTS_DIR, '_results.html'), 'w').write(html) 59 | 60 | if differences: 61 | for stats in stats_list: 62 | if stats.number_of_differences > 0: 63 | print "%s: Images don't match." % stats.name -------------------------------------------------------------------------------- /nodebox/tests/graphics/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/graphics/tests.py -------------------------------------------------------------------------------- /nodebox/tests/vdiff/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/__init__.py -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/001-added-square/bluesquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/001-added-square/bluesquare.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/001-added-square/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/001-added-square/original.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/002-antialiased-text/cocoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/002-antialiased-text/cocoa.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/002-antialiased-text/photoshop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/002-antialiased-text/photoshop.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/003-movement/moved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/003-movement/moved.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/003-movement/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/003-movement/original.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/004-color/darker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/004-color/darker.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/004-color/original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/004-color/original.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/005-antialiased-text/none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/005-antialiased-text/none.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/005-antialiased-text/smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/005-antialiased-text/smooth.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/006-totally-different/ant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/006-totally-different/ant.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/006-totally-different/people.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/006-totally-different/people.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/007-black-white/black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/007-black-white/black.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/007-black-white/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/nodebox/tests/vdiff/images/007-black-white/white.png -------------------------------------------------------------------------------- /nodebox/tests/vdiff/images/vdiff.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px 0 20px 150px; 3 | } 4 | 5 | body, td, th { 6 | font: 11px/1.5em Verdana, sans-serif; 7 | } 8 | 9 | 10 | h1 { 11 | font-size: 160%; 12 | padding: 0; 13 | margin: 0em 0 -2em 0; 14 | } 15 | 16 | h2 { 17 | font-size: 130%; 18 | padding: 0; 19 | margin: 4em 0 0.2em 0; 20 | clear:both; 21 | } 22 | 23 | img { 24 | float: left; 25 | border: 1px solid #000; 26 | margin: 2px; 27 | } 28 | 29 | table.statistics { 30 | margin:2px; 31 | width:16em; 32 | border:1px solid #666; 33 | } 34 | 35 | table.statistics td { 36 | font-weight: bold; 37 | text-align: right; 38 | padding: 2px 5px; 39 | } 40 | 41 | table.statistics td + td{ 42 | font-weight: normal; 43 | text-align: left; 44 | } 45 | 46 | tr.even { 47 | background: #eee; 48 | } 49 | 50 | tr.odd { 51 | background: #ddd; 52 | } -------------------------------------------------------------------------------- /nodebox/tests/vdiff/tests.py: -------------------------------------------------------------------------------- 1 | import os, glob 2 | from nodebox.util import vdiff 3 | 4 | TEST_PATH = 'images' 5 | create_compare_image = True 6 | 7 | def format_stats(name, stats, compare_image=None): 8 | html = """

%s

\n""" % name 9 | html += """\n""" % stats.fname1 10 | html += """\n""" % stats.fname2 11 | if compare_image is not None: 12 | html += """\n""" % compare_image 13 | html += """\n""" 14 | html += """\n""" % len(stats.differences) 15 | html += """\n""" % stats.total_delta 16 | html += """\n""" % stats.max_delta 17 | html += """\n""" % stats.mean 18 | html += """\n""" % stats.stdev 19 | html += """
Differences:%i
Total delta:%i
Max delta:%i
Mean:%.4f
Stdev:%.4f
\n""" 20 | return html 21 | 22 | html = """ 23 | 24 | 25 | vdiff tests 26 | 27 | 28 | 29 |

vdiff tests

30 | """ 31 | 32 | for testPath in glob.glob('%s/*' % TEST_PATH): 33 | if not os.path.isdir(testPath): continue 34 | name = os.path.basename(testPath) 35 | print name 36 | testFiles = glob.glob('%s/*.png' % testPath) 37 | try: 38 | testFiles.remove('%s/_compare.png' % testPath) 39 | except ValueError: pass 40 | if len(testFiles) == 2: 41 | fname1, fname2 = testFiles 42 | stats = vdiff.Statistics(fname1, fname2) 43 | if create_compare_image: 44 | compare_image = vdiff.make_comparison_image(stats.size, stats.differences) 45 | compare_image_fname = '%s/_compare.png' % testPath 46 | compare_image.save(compare_image_fname) 47 | html += format_stats(name, stats, compare_image_fname) 48 | else: 49 | html += format_stats(name, stats) 50 | else: 51 | print "path %s has more than two PNG images: %s" % (testPath, testFiles) 52 | 53 | html += """\n\n""" 54 | 55 | fp = open('test-results.html', 'w') 56 | fp.write(html) 57 | fp.close() 58 | 59 | print "Generated test-results.html" -------------------------------------------------------------------------------- /nodebox/util/PyFontify.py: -------------------------------------------------------------------------------- 1 | """Module to analyze Python source code; for syntax coloring tools. 2 | 3 | Interface: 4 | for tag, start, end, sublist in fontify(pytext, searchfrom, searchto): 5 | ... 6 | 7 | The 'pytext' argument is a string containing Python source code. 8 | The (optional) arguments 'searchfrom' and 'searchto' may contain a slice in pytext. 9 | The returned value is a list of tuples, formatted like this: 10 | [('keyword', 0, 6, None), ('keyword', 11, 17, None), ('comment', 23, 53, None), etc. ] 11 | The tuple contents are always like this: 12 | (tag, startindex, endindex, sublist) 13 | tag is one of 'keyword', 'string', 'comment' or 'identifier' 14 | sublist is not used, hence always None. 15 | """ 16 | 17 | # Based on FontText.py by Mitchell S. Chapman, 18 | # which was modified by Zachary Roadhouse, 19 | # then un-Tk'd by Just van Rossum. 20 | # Many thanks for regular expression debugging & authoring are due to: 21 | # Tim (the-incredib-ly y'rs) Peters and Cristian Tismer 22 | # So, who owns the copyright? ;-) How about this: 23 | # Copyright 1996-2003: 24 | # Mitchell S. Chapman, 25 | # Zachary Roadhouse, 26 | # Tim Peters, 27 | # Just van Rossum 28 | 29 | from __future__ import generators 30 | 31 | __version__ = "0.5" 32 | 33 | import re 34 | from nodebox import graphics 35 | from nodebox import util 36 | from keyword import kwlist as keywordsList 37 | keywordsList = keywordsList[:] 38 | keywordsList += ["None", "True", "False"] 39 | keywordsList += graphics.__all__ 40 | keywordsList += util.__all__ 41 | keywordsList += dir(graphics.Context) 42 | 43 | # Build up a regular expression which will match anything 44 | # interesting, including multi-line triple-quoted strings. 45 | commentPat = r"#[^\n]*" 46 | 47 | pat = r"[uU]?[rR]?q[^\\q\n]*(\\[\000-\377][^\\q\n]*)*q?" 48 | quotePat = pat.replace("q", "'") + "|" + pat.replace('q', '"') 49 | 50 | # Way to go, Tim! 51 | pat = r""" 52 | [uU]?[rR]? 53 | qqq 54 | [^\\q]* 55 | ( 56 | ( \\[\000-\377] 57 | | q 58 | ( \\[\000-\377] 59 | | [^\q] 60 | | q 61 | ( \\[\000-\377] 62 | | [^\\q] 63 | ) 64 | ) 65 | ) 66 | [^\\q]* 67 | )* 68 | (qqq)? 69 | """ 70 | pat = "".join(pat.split()) # get rid of whitespace 71 | tripleQuotePat = pat.replace("q", "'") + "|" + pat.replace('q', '"') 72 | 73 | # Build up a regular expression which matches all and only 74 | # Python keywords. This will let us skip the uninteresting 75 | # identifier references. 76 | keyPat = r"\b(" + "|".join(keywordsList) + r")\b" 77 | 78 | matchPat = commentPat + "|" + keyPat + "|(" + tripleQuotePat + "|" + quotePat + ")" 79 | matchRE = re.compile(matchPat) 80 | 81 | idKeyPat = "[ \t]*([A-Za-z_][A-Za-z_0-9.]*)" # Ident w. leading whitespace. 82 | idRE = re.compile(idKeyPat) 83 | asRE = re.compile(r".*?\b(as)\b") 84 | 85 | def fontify(pytext, searchfrom=0, searchto=None): 86 | if searchto is None: 87 | searchto = len(pytext) 88 | # Cache a few attributes for quicker reference. 89 | search = matchRE.search 90 | idMatch = idRE.match 91 | asMatch = asRE.match 92 | 93 | commentTag = 'comment' 94 | stringTag = 'string' 95 | keywordTag = 'keyword' 96 | identifierTag = 'identifier' 97 | 98 | start = 0 99 | end = searchfrom 100 | while 1: 101 | m = search(pytext, end) 102 | if m is None: 103 | break # EXIT LOOP 104 | if start >= searchto: 105 | break # EXIT LOOP 106 | keyword = m.group(1) 107 | if keyword is not None: 108 | # matched a keyword 109 | start, end = m.span(1) 110 | yield keywordTag, start, end, None 111 | if keyword in ["def", "class"]: 112 | # If this was a defining keyword, color the 113 | # following identifier. 114 | m = idMatch(pytext, end) 115 | if m is not None: 116 | start, end = m.span(1) 117 | yield identifierTag, start, end, None 118 | elif keyword == "import": 119 | # color all the "as" words on same line; 120 | # cheap approximation to the truth 121 | while 1: 122 | m = asMatch(pytext, end) 123 | if not m: 124 | break 125 | start, end = m.span(1) 126 | yield keywordTag, start, end, None 127 | elif m.group(0)[0] == "#": 128 | start, end = m.span() 129 | yield commentTag, start, end, None 130 | else: 131 | start, end = m.span() 132 | yield stringTag, start, end, None 133 | 134 | 135 | def test(path): 136 | f = open(path) 137 | text = f.read() 138 | f.close() 139 | for tag, start, end, sublist in fontify(text): 140 | print tag, repr(text[start:end]) 141 | 142 | 143 | if __name__ == "__main__": 144 | import sys 145 | test(sys.argv[1]) 146 | -------------------------------------------------------------------------------- /nodebox/util/QTSupport.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tempfile import mkstemp 3 | from Foundation import NSNumber 4 | from AppKit import NSImage, NSApplication, NSColor, NSData, NSBitmapImageRep, NSJPEGFileType 5 | from QTKit import QTMovie, QTDataReference, QTMovieFileNameAttribute, QTMakeTimeRange, QTMakeTime, QTMovieEditableAttribute, QTAddImageCodecType, QTMovieFlatten 6 | 7 | class Movie(object): 8 | 9 | def __init__(self, fname, fps=30): 10 | if os.path.exists(fname): 11 | os.remove(fname) 12 | self.frame = 1 13 | self.fname = fname 14 | self.tmpfname = None 15 | self.firstFrame = True 16 | self.movie = None 17 | self.fps = fps 18 | self._time = QTMakeTime(int(600/self.fps), 600) 19 | 20 | def add(self, canvas_or_context): 21 | if self.movie is None: 22 | # The first frame will be written to a temporary png file, 23 | # then opened as a movie file, then saved again as a movie. 24 | handle, self.tmpfname = mkstemp('.tiff') 25 | canvas_or_context.save(self.tmpfname) 26 | try: 27 | movie, err = QTMovie.movieWithFile_error_(self.tmpfname, None) 28 | movie.setAttribute_forKey_(NSNumber.numberWithBool_(True), QTMovieEditableAttribute) 29 | range = QTMakeTimeRange(QTMakeTime(0,600), movie.duration()) 30 | movie.scaleSegment_newDuration_(range, self._time) 31 | if err is not None: 32 | raise str(err) 33 | movie.writeToFile_withAttributes_(self.fname, {QTMovieFlatten:True}) 34 | self.movie, err = QTMovie.movieWithFile_error_(self.fname, None) 35 | self.movie.setAttribute_forKey_(NSNumber.numberWithBool_(True), QTMovieEditableAttribute) 36 | if err is not None: raise str(err) 37 | self.imageTrack = self.movie.tracks()[0] 38 | finally: 39 | os.remove(self.tmpfname) 40 | else: 41 | try: 42 | canvas_or_context.save(self.tmpfname) 43 | img = NSImage.alloc().initByReferencingFile_(self.tmpfname) 44 | self.imageTrack.addImage_forDuration_withAttributes_(img, self._time, {QTAddImageCodecType:'tiff'}) 45 | finally: 46 | try: 47 | os.remove(self.tmpfname) 48 | except OSError: pass 49 | self.frame += 1 50 | 51 | def save(self): 52 | self.movie.updateMovieFile() 53 | 54 | def test(): 55 | import sys 56 | sys.path.insert(0, '../..') 57 | from nodebox.graphics import Canvas, Context 58 | from math import sin 59 | 60 | NSApplication.sharedApplication().activateIgnoringOtherApps_(0) 61 | w, h = 500, 300 62 | m = Movie("xx3.mov") 63 | for i in range(200): 64 | print "Frame", i 65 | ctx = Context() 66 | ctx.size(w, h) 67 | ctx.rect(100.0+sin(i/10.0)*100.0,i/2.0,100,100) 68 | ctx.text(i, 0, 200) 69 | m.add(ctx) 70 | m.save() 71 | 72 | if __name__=='__main__': 73 | test() 74 | -------------------------------------------------------------------------------- /nodebox/util/__init__.py: -------------------------------------------------------------------------------- 1 | from random import choice 2 | 3 | __all__ = ('grid', 'random', 'choice', 'files', 'autotext', '_copy_attr', '_copy_attrs') 4 | 5 | ### Utilities ### 6 | 7 | def grid(cols, rows, colSize=1, rowSize=1, shuffled = False): 8 | """Returns an iterator that contains coordinate tuples. 9 | 10 | The grid can be used to quickly create grid-like structures. A common way to use them is: 11 | for x, y in grid(10,10,12,12): 12 | rect(x,y, 10,10) 13 | """ 14 | # Prefer using generators. 15 | rowRange = xrange(int(rows)) 16 | colRange = xrange(int(cols)) 17 | # Shuffled needs a real list, though. 18 | if (shuffled): 19 | rowRange = list(rowRange) 20 | colRange = list(colRange) 21 | shuffle(rowRange) 22 | shuffle(colRange) 23 | for y in rowRange: 24 | for x in colRange: 25 | yield (x*colSize,y*rowSize) 26 | 27 | def random(v1=None, v2=None): 28 | """Returns a random value. 29 | 30 | This function does a lot of things depending on the parameters: 31 | - If one or more floats is given, the random value will be a float. 32 | - If all values are ints, the random value will be an integer. 33 | 34 | - If one value is given, random returns a value from 0 to the given value. 35 | This value is not inclusive. 36 | - If two values are given, random returns a value between the two; if two 37 | integers are given, the two boundaries are inclusive. 38 | """ 39 | import random 40 | if v1 != None and v2 == None: # One value means 0 -> v1 41 | if isinstance(v1, float): 42 | return random.random() * v1 43 | else: 44 | return int(random.random() * v1) 45 | elif v1 != None and v2 != None: # v1 -> v2 46 | if isinstance(v1, float) or isinstance(v2, float): 47 | start = min(v1, v2) 48 | end = max(v1, v2) 49 | return start + random.random() * (end-start) 50 | else: 51 | start = min(v1, v2) 52 | end = max(v1, v2) + 1 53 | return int(start + random.random() * (end-start)) 54 | else: # No values means 0.0 -> 1.0 55 | return random.random() 56 | 57 | def files(path="*"): 58 | """Returns a list of files. 59 | 60 | You can use wildcards to specify which files to pick, e.g. 61 | f = files('*.gif') 62 | """ 63 | from glob import glob 64 | return glob(path) 65 | 66 | def autotext(sourceFile): 67 | from nodebox.util.kgp import KantGenerator 68 | k = KantGenerator(sourceFile) 69 | return k.output() 70 | 71 | def _copy_attr(v): 72 | if v is None: 73 | return None 74 | elif hasattr(v, "copy"): 75 | return v.copy() 76 | elif isinstance(v, list): 77 | return list(v) 78 | elif isinstance(v, tuple): 79 | return tuple(v) 80 | elif isinstance(v, (int, str, unicode, float, bool, long)): 81 | return v 82 | else: 83 | raise NodeBoxError, "Don't know how to copy '%s'." % v 84 | 85 | def _copy_attrs(source, target, attrs): 86 | for attr in attrs: 87 | setattr(target, attr, _copy_attr(getattr(source, attr))) 88 | -------------------------------------------------------------------------------- /nodebox/util/kgp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | """Kant Generator for Python 3 | 4 | Generates mock philosophy based on a context-free grammar 5 | 6 | Usage: python kgp.py [options] [source] 7 | 8 | Options: 9 | -g ..., --grammar=... use specified grammar file or URL 10 | -h, --help show this help 11 | -d show debugging information while parsing 12 | 13 | Examples: 14 | kgp.py generates several paragraphs of Kantian philosophy 15 | kgp.py -g husserl.xml generates several paragraphs of Husserl 16 | kpg.py "" generates a paragraph of Kant 17 | kgp.py template.xml reads from template.xml to decide what to generate 18 | 19 | This program is part of "Dive Into Python", a free Python book for 20 | experienced programmers. Visit http://diveintopython.org/ for the 21 | latest version. 22 | """ 23 | 24 | __author__ = "Mark Pilgrim (f8dy@diveintopython.org)" 25 | __version__ = "$Revision: 1.3 $" 26 | __date__ = "$Date: 2002/05/28 17:05:23 $" 27 | __copyright__ = "Copyright (c) 2001 Mark Pilgrim" 28 | __license__ = "Python" 29 | 30 | from xml.dom import minidom 31 | import random 32 | import sys 33 | import getopt 34 | 35 | _debug = 0 36 | 37 | def openAnything(source): 38 | """URI, filename, or string --> stream 39 | 40 | This function lets you define parsers that take any input source 41 | (URL, pathname to local or network file, or actual data as a string) 42 | and deal with it in a uniform manner. Returned object is guaranteed 43 | to have all the basic stdio read methods (read, readline, readlines). 44 | Just .close() the object when you're done with it. 45 | 46 | Examples: 47 | >>> from xml.dom import minidom 48 | >>> sock = openAnything("http://localhost/kant.xml") 49 | >>> doc = minidom.parse(sock) 50 | >>> sock.close() 51 | >>> sock = openAnything("c:\\inetpub\\wwwroot\\kant.xml") 52 | >>> doc = minidom.parse(sock) 53 | >>> sock.close() 54 | >>> sock = openAnything("andor") 55 | >>> doc = minidom.parse(sock) 56 | >>> sock.close() 57 | """ 58 | 59 | if hasattr(source, "read"): 60 | return source 61 | 62 | if source == "-": 63 | import sys 64 | return sys.stdin 65 | 66 | # try to open with urllib (if source is http, ftp, or file URL) 67 | import urllib 68 | try: 69 | return urllib.urlopen(source) 70 | except (IOError, OSError): 71 | pass 72 | 73 | # try to open with native open function (if source is pathname) 74 | try: 75 | return open(source) 76 | except (IOError, OSError): 77 | pass 78 | 79 | # treat source as string 80 | import StringIO 81 | return StringIO.StringIO(str(source)) 82 | 83 | class NoSourceError(Exception): pass 84 | 85 | class KantGenerator: 86 | """generates mock philosophy based on a context-free grammar""" 87 | 88 | def __init__(self, grammar, source=None): 89 | self.loadGrammar(grammar) 90 | self.loadSource(source and source or self.getDefaultSource()) 91 | self.refresh() 92 | 93 | def _load(self, source): 94 | """load XML input source, return parsed XML document 95 | 96 | - a URL of a remote XML file ("http://diveintopython.org/kant.xml") 97 | - a filename of a local XML file ("~/diveintopython/common/py/kant.xml") 98 | - standard input ("-") 99 | - the actual XML document, as a string 100 | """ 101 | sock = openAnything(source) 102 | xmldoc = minidom.parse(sock).documentElement 103 | sock.close() 104 | return xmldoc 105 | 106 | def loadGrammar(self, grammar): 107 | """load context-free grammar""" 108 | self.grammar = self._load(grammar) 109 | self.refs = {} 110 | for ref in self.grammar.getElementsByTagName("ref"): 111 | self.refs[ref.attributes["id"].value] = ref 112 | 113 | def loadSource(self, source): 114 | """load source""" 115 | self.source = self._load(source) 116 | 117 | def getDefaultSource(self): 118 | """guess default source of the current grammar 119 | 120 | The default source will be one of the s that is not 121 | cross-referenced. This sounds complicated but it's not. 122 | Example: The default source for kant.xml is 123 | "", because 'section' is the one 124 | that is not 'd anywhere in the grammar. 125 | In most grammars, the default source will produce the 126 | longest (and most interesting) output. 127 | """ 128 | xrefs = {} 129 | for xref in self.grammar.getElementsByTagName("xref"): 130 | xrefs[xref.attributes["id"].value] = 1 131 | xrefs = xrefs.keys() 132 | standaloneXrefs = [e for e in self.refs.keys() if e not in xrefs] 133 | if not standaloneXrefs: 134 | raise NoSourceError, "can't guess source, and no source specified" 135 | return '' % random.choice(standaloneXrefs) 136 | 137 | def reset(self): 138 | """reset parser""" 139 | self.pieces = [] 140 | self.capitalizeNextWord = 0 141 | 142 | def refresh(self): 143 | """reset output buffer, re-parse entire source file, and return output 144 | 145 | Since parsing involves a good deal of randomness, this is an 146 | easy way to get new output without having to reload a grammar file 147 | each time. 148 | """ 149 | self.reset() 150 | self.parse(self.source) 151 | return self.output() 152 | 153 | def output(self): 154 | """output generated text""" 155 | return "".join(self.pieces) 156 | 157 | def randomChildElement(self, node): 158 | """choose a random child element of a node 159 | 160 | This is a utility method used by do_xref and do_choice. 161 | """ 162 | choices = [e for e in node.childNodes 163 | if e.nodeType == e.ELEMENT_NODE] 164 | chosen = random.choice(choices) 165 | if _debug: 166 | sys.stderr.write('%s available choices: %s\n' % \ 167 | (len(choices), [e.toxml() for e in choices])) 168 | sys.stderr.write('Chosen: %s\n' % chosen.toxml()) 169 | return chosen 170 | 171 | def parse(self, node): 172 | """parse a single XML node 173 | 174 | A parsed XML document (from minidom.parse) is a tree of nodes 175 | of various types. Each node is represented by an instance of the 176 | corresponding Python class (Element for a tag, Text for 177 | text data, Document for the top-level document). The following 178 | statement constructs the name of a class method based on the type 179 | of node we're parsing ("parse_Element" for an Element node, 180 | "parse_Text" for a Text node, etc.) and then calls the method. 181 | """ 182 | parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) 183 | parseMethod(node) 184 | 185 | def parse_Document(self, node): 186 | """parse the document node 187 | 188 | The document node by itself isn't interesting (to us), but 189 | its only child, node.documentElement, is: it's the root node 190 | of the grammar. 191 | """ 192 | self.parse(node.documentElement) 193 | 194 | def parse_Text(self, node): 195 | """parse a text node 196 | 197 | The text of a text node is usually added to the output buffer 198 | verbatim. The one exception is that

sets 199 | a flag to capitalize the first letter of the next word. If 200 | that flag is set, we capitalize the text and reset the flag. 201 | """ 202 | text = node.data 203 | if self.capitalizeNextWord: 204 | self.pieces.append(text[0].upper()) 205 | self.pieces.append(text[1:]) 206 | self.capitalizeNextWord = 0 207 | else: 208 | self.pieces.append(text) 209 | 210 | def parse_Element(self, node): 211 | """parse an element 212 | 213 | An XML element corresponds to an actual tag in the source: 214 | ,

, , etc. 215 | Each element type is handled in its own method. Like we did in 216 | parse(), we construct a method name based on the name of the 217 | element ("do_xref" for an tag, etc.) and 218 | call the method. 219 | """ 220 | handlerMethod = getattr(self, "do_%s" % node.tagName) 221 | handlerMethod(node) 222 | 223 | def parse_Comment(self, node): 224 | """parse a comment 225 | 226 | The grammar can contain XML comments, but we ignore them 227 | """ 228 | pass 229 | 230 | def do_xref(self, node): 231 | """handle tag 232 | 233 | An tag is a cross-reference to a 234 | tag. evaluates to a randomly chosen child of 235 | . 236 | """ 237 | id = node.attributes["id"].value 238 | self.parse(self.randomChildElement(self.refs[id])) 239 | 240 | def do_p(self, node): 241 | """handle

tag 242 | 243 | The

tag is the core of the grammar. It can contain almost 244 | anything: freeform text, tags, tags, even other 245 |

tags. If a "class='sentence'" attribute is found, a flag 246 | is set and the next word will be capitalized. If a "chance='X'" 247 | attribute is found, there is an X% chance that the tag will be 248 | evaluated (and therefore a (100-X)% chance that it will be 249 | completely ignored) 250 | """ 251 | keys = node.attributes.keys() 252 | if "class" in keys: 253 | if node.attributes["class"].value == "sentence": 254 | self.capitalizeNextWord = 1 255 | if "chance" in keys: 256 | chance = int(node.attributes["chance"].value) 257 | doit = (chance > random.randrange(100)) 258 | else: 259 | doit = 1 260 | if doit: 261 | for child in node.childNodes: self.parse(child) 262 | 263 | def do_choice(self, node): 264 | """handle tag 265 | 266 | A tag contains one or more

tags. One

tag 267 | is chosen at random and evaluated; the rest are ignored. 268 | """ 269 | self.parse(self.randomChildElement(node)) 270 | 271 | def usage(): 272 | print __doc__ 273 | 274 | def main(argv): 275 | grammar = "kant.xml" 276 | try: 277 | opts, args = getopt.getopt(argv, "hg:d", ["help", "grammar="]) 278 | except getopt.GetoptError: 279 | usage() 280 | sys.exit(2) 281 | for opt, arg in opts: 282 | if opt in ("-h", "--help"): 283 | usage() 284 | sys.exit() 285 | elif opt == '-d': 286 | global _debug 287 | _debug = 1 288 | elif opt in ("-g", "--grammar"): 289 | grammar = arg 290 | 291 | source = "".join(args) 292 | k = KantGenerator(grammar, source) 293 | print k.output() 294 | 295 | if __name__ == "__main__": 296 | main(sys.argv[1:]) 297 | -------------------------------------------------------------------------------- /nodebox/util/ottobot.py: -------------------------------------------------------------------------------- 1 | from AppKit import NSFontManager 2 | 3 | from nodebox.util import random, choice 4 | 5 | COMP_WIDTH = 500 6 | COMP_HEIGHT = 500 7 | 8 | XCOORD = 1 9 | YCOORD = 2 10 | XSIZE = 3 11 | YSIZE = 4 12 | ROTATION = 5 13 | SCALE = 6 14 | CONTROLPOINT = 7 15 | COLOR = 8 16 | STROKEWIDTH = 9 17 | LOOP = 10 18 | GRIDDELTA = 12 19 | GRIDCOUNT = 13 20 | GRIDWIDTH = 14 21 | GRIDHEIGHT = 15 22 | SKEW = 16 23 | STARPOINTS = 17 24 | 25 | class Context: 26 | def __init__(self): 27 | self._indent = 0 28 | self._grid = False 29 | 30 | def indent(self): 31 | self._indent += 1 32 | 33 | def dedent(self): 34 | self._indent -= 1 35 | 36 | def spaces(self): 37 | return " " * self._indent 38 | 39 | def inGrid(self): 40 | return self._grid 41 | 42 | def nrReally(ctx, numberclass): 43 | if numberclass == XCOORD: 44 | if ctx.inGrid(): 45 | #return "x" 46 | return "x + %s" % nr(ctx,GRIDDELTA) 47 | else: 48 | return random(-COMP_WIDTH/2,COMP_WIDTH/2) 49 | elif numberclass == YCOORD: 50 | if ctx.inGrid(): 51 | #return "y" 52 | return "y + %s" % nr(ctx,GRIDDELTA) 53 | else: 54 | return random(-COMP_HEIGHT/2,COMP_HEIGHT/2) 55 | elif numberclass == XSIZE: 56 | return random(0,COMP_WIDTH) 57 | elif numberclass == YSIZE: 58 | return random(0,COMP_HEIGHT) 59 | elif numberclass == ROTATION: 60 | return random(0,360) 61 | elif numberclass == SCALE: 62 | return random(0.5,1.5) 63 | elif numberclass == CONTROLPOINT: 64 | return random(-100,100) 65 | elif numberclass == COLOR: 66 | return random() 67 | elif numberclass == STROKEWIDTH: 68 | return random(1,20) 69 | elif numberclass == LOOP: 70 | return random(2, 20) 71 | elif numberclass == GRIDDELTA: 72 | return random(-100,100) 73 | elif numberclass == GRIDCOUNT: 74 | return random(2, 10) 75 | elif numberclass == GRIDWIDTH: 76 | return 20 77 | return random(1,100) 78 | elif numberclass == GRIDHEIGHT: 79 | return 20 80 | return random(1, 100) 81 | elif numberclass == SKEW: 82 | return random(1,80) 83 | elif numberclass == STARPOINTS: 84 | return random(2,100) 85 | 86 | def nr(ctx, numberclass): 87 | if not ctx.inGrid() and random() > 0.5: 88 | return "random(%s)" % nrReally(ctx, numberclass) 89 | else: 90 | return "%s" % nrReally(ctx, numberclass) 91 | 92 | ### DRAWING COMMANDS ### 93 | 94 | def genDraw(ctx): 95 | fn = choice((genRect,genOval,genArrow,genStar,genPath)) 96 | return fn(ctx) 97 | 98 | def genRect(ctx): 99 | return ctx.spaces() + """rect(%s,%s,%s,%s)\n""" % ( 100 | nr(ctx,XCOORD),nr(ctx,YCOORD),nr(ctx,XSIZE),nr(ctx,YSIZE)) 101 | 102 | def genOval(ctx): 103 | return ctx.spaces() + """oval(%s,%s,%s,%s)\n""" % ( 104 | nr(ctx,XCOORD),nr(ctx,YCOORD),nr(ctx,XSIZE),nr(ctx,YSIZE)) 105 | 106 | def genArrow(ctx): 107 | return ctx.spaces() + """arrow(%s,%s,%s)\n""" % ( 108 | nr(ctx,XCOORD),nr(ctx,YCOORD),nr(ctx,XSIZE)) 109 | 110 | def genStar(ctx): 111 | return ctx.spaces() + """star(%s,%s,%s,%s,%s)\n""" % ( 112 | nr(ctx,XCOORD),nr(ctx,YCOORD),nr(ctx,STARPOINTS),nr(ctx,XSIZE),nr(ctx,XSIZE)) 113 | 114 | def genPath(ctx): 115 | s = ctx.spaces() + """beginpath(%s,%s)\n""" % ( 116 | nr(ctx,XCOORD),nr(ctx,YCOORD)) 117 | for i in range(random(1,10)): 118 | s += genPathDraw(ctx) 119 | s += ctx.spaces() + """endpath()\n""" 120 | return s 121 | 122 | def genPathDraw(ctx): 123 | fn = choice((genLineto, genCurveto)) 124 | return fn(ctx) 125 | 126 | def genLineto(ctx): 127 | return ctx.spaces() + """lineto(%s,%s)\n""" % (nr(ctx,XCOORD),nr(ctx,YCOORD)) 128 | 129 | def genCurveto(ctx): 130 | return ctx.spaces() + """curveto(%s,%s,%s,%s,%s,%s)\n""" % ( 131 | nr(ctx,XCOORD),nr(ctx,YCOORD),nr(ctx,CONTROLPOINT),nr(ctx,CONTROLPOINT),nr(ctx,CONTROLPOINT),nr(ctx,CONTROLPOINT)) 132 | 133 | ### TRANSFORM ### 134 | 135 | def genTransform(ctx): 136 | fn = choice((genRotate, genTranslate, genScale, genSkew, genReset)) 137 | return fn(ctx) 138 | 139 | def genRotate(ctx): 140 | return ctx.spaces() + """rotate(%s)\n""" % nr(ctx,ROTATION) 141 | 142 | def genTranslate(ctx): 143 | return ctx.spaces() + """translate(%s,%s)\n""" % (nr(ctx,XCOORD), nr(ctx,YCOORD)) 144 | 145 | def genScale(ctx): 146 | return ctx.spaces() + """scale(%s)\n""" % (nr(ctx,SCALE)) 147 | 148 | def genSkew(ctx): 149 | return ctx.spaces() + """skew(%s)\n""" % (nr(ctx,SKEW)) 150 | 151 | def genReset(ctx): 152 | return ctx.spaces() + """reset()\n""" 153 | 154 | ### COLOR ### 155 | 156 | def genColor(ctx): 157 | fn = choice((genFill,genFill,genFill,genFill,genFill,genFill,genStroke,genStroke,genStroke,genNofill,genNostroke,genStrokewidth)) 158 | return fn(ctx) 159 | 160 | def genFill(ctx): 161 | return ctx.spaces() + """fill(%s,%s,%s,%s)\n""" % (nr(ctx,COLOR),nr(ctx,COLOR), nr(ctx,COLOR), nr(ctx,COLOR)) 162 | 163 | def genStroke(ctx): 164 | return ctx.spaces() + """stroke(%s,%s,%s,%s)\n""" % (nr(ctx,COLOR), nr(ctx,COLOR), nr(ctx,COLOR), nr(ctx,COLOR)) 165 | 166 | def genNofill(ctx): 167 | return ctx.spaces() + """nofill()\n""" 168 | 169 | def genNostroke(ctx): 170 | return ctx.spaces() + """nostroke()\n""" 171 | 172 | def genStrokewidth(ctx): 173 | return ctx.spaces() + """strokewidth(%s)\n""" % nr(ctx,STROKEWIDTH) 174 | 175 | ### LOOP ### 176 | def genLoop(ctx): 177 | fn = choice((genFor, genGrid)) 178 | return fn(ctx) 179 | 180 | def genFor(ctx): 181 | if ctx._indent >= 2: return "" 182 | s = ctx.spaces() + """for i in range(%s):\n""" % nr(ctx,LOOP) 183 | ctx.indent() 184 | for i in range(random(5)): 185 | s += genStatement(ctx) 186 | s += genVisual(ctx) 187 | ctx.dedent() 188 | return s 189 | 190 | def genGrid(ctx): 191 | if ctx.inGrid(): return "" 192 | s = ctx.spaces() + """for x, y in grid(%s,%s,%s,%s):\n""" % (nr(ctx,GRIDCOUNT), nr(ctx,GRIDCOUNT), nr(ctx,GRIDWIDTH), nr(ctx,GRIDHEIGHT)) 193 | ctx.indent() 194 | ctx._grid = True 195 | for i in range(random(5)): 196 | s += genStatement(ctx) 197 | s += genVisual(ctx) 198 | ctx.dedent() 199 | ctx._grid = False 200 | return s 201 | 202 | ### MAIN ### 203 | 204 | def genVisual(ctx): 205 | fn = choice((genDraw,)) 206 | return fn(ctx) 207 | 208 | def genStatement(ctx): 209 | fn = choice((genVisual,genLoop,genColor,genTransform)) 210 | return fn(ctx) 211 | 212 | def genProgram(): 213 | s = """# This code is generated with OTTOBOT, 214 | # the automatic NodeBox code generator. 215 | size(%s, %s) 216 | translate(%s, %s) 217 | colormode(HSB) 218 | """ % (COMP_WIDTH, COMP_HEIGHT, COMP_WIDTH/2, COMP_HEIGHT/2) 219 | ctx = Context() 220 | for i in range(random(10,20)): 221 | s += genStatement(ctx) 222 | return s 223 | 224 | if __name__ == '__main__': 225 | print genProgram() -------------------------------------------------------------------------------- /nodebox/util/vdiff.py: -------------------------------------------------------------------------------- 1 | import os 2 | import Image 3 | 4 | HTML_HEADER = r''' 5 | 6 | 7 | 8 | Vdiff Test Results 9 | 22 | 23 | 24 |

vdiff tests

25 | ''' 26 | 27 | HTML_FOOTER = r''' 28 | 29 | 30 | ''' 31 | 32 | def format_stats(stats): 33 | if stats.number_of_differences > 0: 34 | clz = " different" 35 | else: 36 | clz = "" 37 | 38 | html = """

%s

\n""" % stats.name 39 | html += """
""" % clz 40 | html += """\n""" % (stats.fname1, stats.fname1) 41 | html += """\n""" % (stats.fname2, stats.fname2) 42 | if stats.comparison_image_fname is not None: 43 | html += """\n""" % (stats.comparison_image_fname, stats.comparison_image_fname) 44 | html += """\n""" 45 | html += """\n""" % len(stats.differences) 46 | html += """\n""" % stats.total_delta 47 | html += """\n""" % stats.max_delta 48 | html += """\n""" % stats.mean 49 | html += """\n""" % stats.stdev 50 | html += """
Differences:%i
Total delta:%i
Max delta:%i
Mean:%.4f
Stdev:%.4f
\n""" 51 | html += """
""" 52 | return html 53 | 54 | def format_stats_list(stats_list): 55 | html = HTML_HEADER 56 | for stats in stats_list: 57 | html += format_stats(stats) 58 | html += HTML_FOOTER 59 | return html 60 | 61 | def compare_pixel(px1, px2): 62 | if px1 == px2: 63 | return 0 64 | r1, g1, b1, a1 = px1 65 | r2, g2, b2, a2 = px2 66 | return abs(r1-r2) + abs(g1-g2) + abs(b1-b2) + abs(a1-a2) 67 | 68 | def visual_diff(img1, img2, threshold=0, stop_on_diff=False): 69 | if isinstance(img1, str) or isinstance(img1, unicode): 70 | img1 = Image.open(img1) 71 | img1 = img1.convert("RGBA") 72 | if isinstance(img2, str) or isinstance(img2, unicode): 73 | img2 = Image.open(img2) 74 | img2 = img2.convert("RGBA") 75 | assert img1.size == img2.size 76 | w, h = img1.size 77 | data1 = img1.getdata() 78 | data2 = img2.getdata() 79 | size = len(data1) 80 | differences = [] 81 | for i in xrange(size): 82 | delta = compare_pixel(data1[i], data2[i]) 83 | if delta > threshold: 84 | x = i % w 85 | y = i / w 86 | differences.append( ( (x, y), data1[i], data2[i], delta ) ) 87 | if stop_on_diff: 88 | # print data1[i], data2[i] 89 | break 90 | return differences 91 | 92 | def make_comparison_image(size, differences): 93 | img = Image.new("L", size, color=255) 94 | for pos, d1, d2, delta in differences: 95 | img.putpixel(pos, 255-delta) 96 | return img 97 | 98 | def isEqual(fname1, fname2, threshold=0): 99 | diff = visual_diff(fname1, fname2, threshold, stop_on_diff=True) 100 | if len(diff) == 0: 101 | return True 102 | return False 103 | 104 | class Statistics(object): 105 | def __init__(self, fname1, fname2, differences=None, name=""): 106 | self.fname1 = fname1 107 | self.fname2 = fname2 108 | if differences is None: 109 | differences = visual_diff(fname1, fname2) 110 | self.differences = differences 111 | self.name = name 112 | 113 | img1 = Image.open(fname1) 114 | self.width, self.height = img1.size 115 | 116 | self._comparison_image = None 117 | self.comparison_image_fname = None 118 | self.calculate() 119 | 120 | def calculate(self): 121 | diff = self.differences 122 | 123 | total_delta = 0 124 | max_delta = 0 125 | for pos, d1, d2, delta in diff: 126 | total_delta += delta 127 | max_delta = max(max_delta, delta) 128 | self.total_delta = total_delta 129 | self.max_delta = max_delta 130 | self.mean = mean = total_delta / float(self.width * self.height) 131 | 132 | stdev = 0 133 | for pos, d1, d2, delta in diff: 134 | stdev += pow(delta-mean, 2) 135 | stdev /= float(self.width * self.height) 136 | self.stdev = stdev 137 | 138 | def _get_size(self): 139 | return self.width, self.height 140 | size = property(_get_size) 141 | 142 | def _get_number_of_differences(self): 143 | return len(self.differences) 144 | number_of_differences = property(_get_number_of_differences) 145 | 146 | def _get_comparison_image(self): 147 | if self._comparison_image is None: 148 | self._comparison_image = make_comparison_image(self.size, self.differences) 149 | return self._comparison_image 150 | comparison_image = property(_get_comparison_image) 151 | 152 | def save_comparison_image(self, fname): 153 | self.comparison_image.save(fname) 154 | self.comparison_image_fname = fname 155 | 156 | def __str__(self): 157 | return "" % ( 158 | len(self.differences), self.total_delta, self.max_delta, self.mean, self.stdev) 159 | 160 | def statistics(fname1, fname2, threshold=0): 161 | diff = visual_diff(fname1, fname2) 162 | stats = Statistics(fname1, fname2, diff) 163 | 164 | print "Differences:", len(stats.differences) 165 | print "Total delta:", stats.total_delta 166 | print "Max delta:", stats.max_delta 167 | print "Mean:", stats.mean 168 | print "Stdev:", stats.stdev 169 | 170 | stats.comparison_image.save('cmp.png') 171 | 172 | def test_vdiff(self): 173 | #fname1 = 'vdiff-tests/001-added-square/original.png' 174 | #fname2 = 'vdiff-tests/001-added-square/bluesquare.png' 175 | 176 | #fname1 = 'vdiff-tests/002-antialiased-text/preview.png' 177 | #fname2 = 'vdiff-tests/002-antialiased-text/photoshop.png' 178 | 179 | #fname1 = 'vdiff-tests/003-movement/original.png' 180 | #fname2 = 'vdiff-tests/003-movement/moved.png' 181 | 182 | #fname1 = 'vdiff-tests/004-color/original.png' 183 | #fname2 = 'vdiff-tests/004-color/darker.png' 184 | 185 | #fname1 = 'vdiff-tests/005-antialiased-text/none.png' 186 | #fname2 = 'vdiff-tests/005-antialiased-text/smooth.png' 187 | 188 | #fname1 = 'vdiff-tests/006-totally-different/ant.png' 189 | #fname2 = 'vdiff-tests/006-totally-different/people.png' 190 | 191 | fname1 = 'vdiff-tests/007-black-white/black.png' 192 | fname2 = 'vdiff-tests/007-black-white/white.png' 193 | 194 | statistics(fname1, fname2) 195 | 196 | def usage(): 197 | print """vdiff -- visually compare images 198 | Usage: vdiff [threshold]""" 199 | 200 | if __name__=='__main__': 201 | import sys 202 | if len(sys.argv) < 3: 203 | usage() 204 | else: 205 | fname1 = sys.argv[1] 206 | fname2 = sys.argv[2] 207 | try: 208 | threshold = int(sys.argv[3]) 209 | except: 210 | threshold = 0 211 | statistics(fname1, fname2, threshold) -------------------------------------------------------------------------------- /screenshots/nodebox-191.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodebox/nodebox-pyobjc/31c7a95ca24fffdc8f4523278d4b68c330adea8e/screenshots/nodebox-191.png -------------------------------------------------------------------------------- /tests/colortest.py: -------------------------------------------------------------------------------- 1 | colormode(RGB) 2 | 3 | fill(0) 4 | rect(0,0,100,100) 5 | 6 | r = 0.5 7 | g = 0.5 8 | b = 0.0 9 | 10 | c = 1 - r 11 | m = 1 - g 12 | y = 1 - b 13 | 14 | k = min(c,m,y) 15 | 16 | c -= k 17 | m -= k 18 | y -= k 19 | 20 | fill(r,g,b) 21 | rect(100.0,0,100,100) 22 | 23 | colormode(CMYK) 24 | fill(c,m,y,k) 25 | rect(100.0,100,100,100) 26 | 27 | 28 | 29 | 30 | colormode(CMYK) 31 | fill(0,0,0,1) 32 | rect(200.0,0,100,100) 33 | -------------------------------------------------------------------------------- /tests/compliance.py: -------------------------------------------------------------------------------- 1 | size(800,2600) 2 | 3 | def header(): 4 | font("Verdana", 12) 5 | text("NodeBox Compliance Tests", 20, 60) 6 | stroke(0) 7 | line(0,60,WIDTH,60) 8 | fontsize(8.5) 9 | nostroke() 10 | text("This functional suite tests all the available NodeBox functions, to see if they comply to their contract." , 20, 80, width=200) 11 | 12 | def primitives(x, y): 13 | nostroke() 14 | rect(x, y, 50, 50) 15 | x += 60 16 | rect(x, y, 50, 50, 0.6) 17 | x += 60 18 | oval(x, y, 50, 50) 19 | x += 60 20 | star(x+25, y+25, 20, outer=25, inner=15) 21 | x += 60 22 | arrow(x+50, y+25, 50) 23 | x += 60 24 | arrow(x+50, y, 50, type=FORTYFIVE) 25 | 26 | def basictext(x, y): 27 | text("Hello", x, y) 28 | 29 | x += 60 30 | align(LEFT) 31 | stroke(0) 32 | nofill() 33 | rect(x, y-12,50,20) 34 | fill(0) 35 | text("Hello", x, y, width=50) 36 | 37 | x += 60 38 | align(CENTER) 39 | stroke(0) 40 | nofill() 41 | rect(x, y-12,50,20) 42 | fill(0) 43 | text("Hello", x, y, width=50) 44 | 45 | x += 60 46 | align(RIGHT) 47 | stroke(0) 48 | nofill() 49 | rect(x, y-12,50,20) 50 | fill(0) 51 | text("Hello", x, y, width=50) 52 | 53 | align(LEFT) 54 | 55 | def textblock(x, y): 56 | stroke(0) 57 | nofill() 58 | rect(x, y-12, 50, 50) 59 | fill(0) 60 | text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", x, y, width=50, height=50) 61 | 62 | x += 60 63 | align(CENTER) 64 | stroke(0) 65 | nofill() 66 | rect(x, y-12, 50, 50) 67 | fill(0) 68 | text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", x, y, width=50, height=50) 69 | 70 | x += 60 71 | align(RIGHT) 72 | stroke(0) 73 | nofill() 74 | rect(x, y-12, 50, 50) 75 | fill(0) 76 | text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", x, y, width=50, height=50) 77 | 78 | x += 60 79 | align(JUSTIFY) 80 | stroke(0) 81 | nofill() 82 | rect(x, y-12, 50, 50) 83 | fill(0) 84 | text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", x, y, width=50, height=50) 85 | 86 | def grays(x, y): 87 | nostroke() 88 | colormode(RGB) 89 | for i in range(11): 90 | fill(i/10.0) 91 | rect(x, y, 50, 50) 92 | fill(0) 93 | text(str(i), x, y+62) 94 | x += 60 95 | 96 | def alphas(x, y): 97 | nostroke() 98 | colormode(RGB) 99 | for i in range(11): 100 | fill(0, i/10.0) 101 | rect(x, y, 50, 50) 102 | fill(0) 103 | text(str(i), x, y+62) 104 | x += 60 105 | 106 | def _clr(x, y, *args): 107 | fill(args) 108 | rect(x, y, 50, 50) 109 | fill(0) 110 | text(str(args), x, y+62) 111 | return x + 60 112 | 113 | def rgbColors(x, y): 114 | nostroke() 115 | colormode(RGB) 116 | x = _clr(x, y, 0,0,0) 117 | x = _clr(x, y, 0,0,1) 118 | x = _clr(x, y, 0,1,0) 119 | x = _clr(x, y, 0,1,1) 120 | x = _clr(x, y, 1,0,0) 121 | x = _clr(x, y, 1,0,1) 122 | x = _clr(x, y, 1,1,0) 123 | x = _clr(x, y, 1,1,1) 124 | 125 | def cmykColors(x, y): 126 | nostroke() 127 | colormode(CMYK) 128 | x = _clr(x, y, 0,0,0,1) 129 | x = _clr(x, y, 0,0,1,0) 130 | x = _clr(x, y, 0,1,0,0) 131 | x = _clr(x, y, 1,0,0,0) 132 | x = _clr(x, y, 1,1,0,0) 133 | x = _clr(x, y, 0,1,1,0) 134 | x = _clr(x, y, 1,1,1,0) 135 | x = _clr(x, y, 0,0,0,0) 136 | 137 | def hsbColors(x, y): 138 | nostroke() 139 | colormode(HSB) 140 | x = _clr(x, y, 0,0,0) 141 | x = _clr(x, y, 0,0,1) 142 | x = _clr(x, y, 0,1,0) 143 | x = _clr(x, y, 0,1,1) 144 | x = _clr(x, y, 1,0,0) 145 | x = _clr(x, y, 1,0,1) 146 | x = _clr(x, y, 1,1,0) 147 | x = _clr(x, y, 1,1,1) 148 | 149 | def marker(y,h=25): 150 | stroke(1,0,0) 151 | line(0, y+h, WIDTH, y+h) 152 | 153 | # Draw the header 154 | header() 155 | 156 | # Draw the primitives at their first position 157 | nostroke() 158 | text("Basic primitives", 20, 165) 159 | primitives(140,140) 160 | marker(140) 161 | 162 | # Simple translation 163 | translate(0, 140) 164 | nostroke() 165 | text("Translated primitives", 20, 165) 166 | primitives(140,140) 167 | marker(140) 168 | 169 | # Translation and rotation 170 | translate(0, 140) 171 | nostroke() 172 | text("Rotated primitives", 20, 165) 173 | push() 174 | rotate(45) 175 | primitives(140,140) 176 | pop() 177 | marker(140) 178 | 179 | # Scaling 180 | translate(0, 140) 181 | nostroke() 182 | text("Scaled primitives", 20, 165) 183 | push() 184 | scale(0.5) 185 | primitives(140,140) 186 | pop() 187 | marker(140) 188 | 189 | # Text 190 | translate(0, 140) 191 | nostroke() 192 | text("Basic text", 20, 165) 193 | basictext(140, 165) 194 | marker(140) 195 | 196 | # Rotated Text 197 | translate(0, 140) 198 | nostroke() 199 | text("Rotated text", 20, 165) 200 | push() 201 | rotate(45) 202 | basictext(140, 165) 203 | pop() 204 | marker(140) 205 | 206 | # Text blocks 207 | translate(0, 140) 208 | nostroke() 209 | text("Text blocks", 20, 165) 210 | textblock(140, 165) 211 | marker(140) 212 | 213 | # Text blocks 214 | translate(0, 140) 215 | nostroke() 216 | text("Rotated text blocks", 20, 165) 217 | push() 218 | rotate(45) 219 | textblock(140, 165) 220 | pop() 221 | marker(140) 222 | 223 | 224 | # Outlined text 225 | translate(0, 140) 226 | nostroke() 227 | text("Outlined text", 20, 165) 228 | fsize = fontsize() 229 | fontsize(48) 230 | fill(0.5, 0.5) 231 | text("hamburgevons", 140, 165) 232 | nofill() 233 | stroke(0.2) 234 | text("hamburgevons", 140, 165, outline=True) 235 | fontsize(fsize) 236 | fill(0) 237 | marker(140) 238 | 239 | # Grays 240 | translate(0, 140) 241 | nostroke() 242 | text("Grays", 20, 165) 243 | grays(140, 140) 244 | marker(140) 245 | 246 | # Grays 247 | translate(0, 140) 248 | nostroke() 249 | text("Alphas", 20, 165) 250 | alphas(140, 140) 251 | marker(140) 252 | 253 | # RGB Colors 254 | translate(0, 140) 255 | nostroke() 256 | text("RGB Colors", 20, 165) 257 | rgbColors(140, 140) 258 | marker(140) 259 | 260 | # HSB Colors 261 | translate(0, 140) 262 | nostroke() 263 | text("HSB Colors", 20, 165) 264 | hsbColors(140, 140) 265 | marker(140) 266 | 267 | # CMYK Colors 268 | translate(0, 140) 269 | nostroke() 270 | text("CMYK Colors", 20, 165) 271 | cmykColors(140, 140) 272 | marker(140) 273 | 274 | # Images 275 | translate(0, 140) 276 | nostroke() 277 | text("Images", 20, 165) 278 | _ctx.noImagesHint = False 279 | #image("icon.tif", 140,140,width=50) 280 | push() 281 | translate(60,0) 282 | rotate(90) 283 | #image("icon.tif", 140,140,width=50) 284 | pop() 285 | push() 286 | translate(140,0) 287 | scale(2.0) 288 | #image("icon.tif", 140,140,width=50) 289 | pop() 290 | marker(140) 291 | 292 | 293 | # Paths 294 | translate(0, 140) 295 | nostroke() 296 | text("Paths", 20, 165) 297 | beginpath(165, 140) 298 | lineto(140, 200) 299 | curveto(160, 250, 160, 200, 190, 200) 300 | p = endpath() 301 | stroke(0) 302 | nofill() 303 | sw = strokewidth() 304 | strokewidth(2) 305 | push() 306 | translate(60,0) 307 | for pt in p: 308 | pt.x += 60 309 | pt.ctrl1.x += 60 310 | pt.ctrl2.x += 60 311 | drawpath(p) 312 | pop() 313 | 314 | strokewidth(sw) 315 | marker(140) 316 | -------------------------------------------------------------------------------- /tests/graphics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import sys 3 | 4 | # To run the test, make sure you have at least built the NodeBox Extensions. 5 | # Run the following command in the Terminal: 6 | # xcodebuild -target "Build Extensions" 7 | sys.path.append('..') 8 | sys.path.append('../build/libs') 9 | 10 | from nodebox.graphics import * 11 | 12 | class GraphicsTestCase(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.ctx = Context() 16 | 17 | def test_arrow_type_error(self): 18 | """Test if passing a wrong arrow type raises an error.""" 19 | self.assertRaises(NodeBoxError, self.ctx.arrow, 0, 0, 100, type=42) 20 | 21 | def test_too_many_pops(self): 22 | """Test if popping too many times raises an error.""" 23 | self.assertRaises(NodeBoxError, self.ctx.pop) 24 | 25 | def test_font_not_found(self): 26 | """Test if setting an unexisting font raises an error.""" 27 | old_font = self.ctx.font() 28 | self.assertRaises(NodeBoxError, self.ctx.font, "THIS_FONT_DOES_NOT_EXIST") 29 | self.assertEquals(self.ctx.font(), old_font, "Current font has not changed.") 30 | 31 | def test_ellipse(self): 32 | """Test if ellipse is an alias for oval.""" 33 | self.assertTrue(hasattr(self.ctx, "ellipse")) 34 | self.assertTrue(self.ctx.ellipse == self.ctx.oval) 35 | p = BezierPath(self.ctx) 36 | self.assertTrue(hasattr(p, "ellipse")) 37 | self.assertTrue(p.ellipse == p.oval) 38 | 39 | class BezierPathTestCase(unittest.TestCase): 40 | 41 | def setUp(self): 42 | self.ctx = Context() 43 | 44 | def test_capstyle_context(self): 45 | self.ctx.capstyle(SQUARE) 46 | p = BezierPath(self.ctx) 47 | self.assertEquals(p.capstyle, BUTT, "Default line cap style is butt.") 48 | p.inheritFromContext() 49 | self.assertEquals(p.capstyle, SQUARE) 50 | 51 | def test_capstyle_constructor(self): 52 | p = BezierPath(self.ctx, capstyle=ROUND) 53 | self.assertEquals(p.capstyle, ROUND) 54 | 55 | def test_capstyle_validation(self): 56 | self.assertRaises(NodeBoxError, self.ctx.capstyle, 42) 57 | self.assertRaises(NodeBoxError, BezierPath, self.ctx, capstyle=42) 58 | p = BezierPath(self.ctx) 59 | self.assertRaises(NodeBoxError, p._set_capstyle, 42) 60 | 61 | def test_joinstyle_context(self): 62 | self.ctx.joinstyle(ROUND) 63 | p = BezierPath(self.ctx) 64 | self.assertEquals(p.joinstyle, MITER, "Default line join style is miter.") 65 | p.inheritFromContext() 66 | self.assertEquals(p.joinstyle, ROUND) 67 | 68 | def test_joinstyle_constructor(self): 69 | p = BezierPath(self.ctx, joinstyle=ROUND) 70 | self.assertEquals(p.joinstyle, ROUND) 71 | 72 | def test_joinstyle_validation(self): 73 | self.assertRaises(NodeBoxError, self.ctx.joinstyle, 42) 74 | self.assertRaises(NodeBoxError, BezierPath, self.ctx, joinstyle=42) 75 | p = BezierPath(self.ctx) 76 | self.assertRaises(NodeBoxError, p._set_joinstyle, 42) 77 | 78 | if __name__=='__main__': 79 | unittest.main() -------------------------------------------------------------------------------- /tests/nodeboxblack.py: -------------------------------------------------------------------------------- 1 | colormode(RGB) 2 | 3 | 4 | fill(0) 5 | rect(0,0,100,100) 6 | 7 | fill(0,0,0) 8 | rect(100.0,0,100,100) 9 | 10 | 11 | colormode(CMYK) 12 | fill(0,0,0,1) 13 | rect(200.0,0,100,100) 14 | 15 | -------------------------------------------------------------------------------- /tests/numberslider.py: -------------------------------------------------------------------------------- 1 | a = 10 2 | b = [1,2,3] 3 | 4 | 5 | 6 | # unary op 7 | rect(100,100,31,20) 8 | rect(100,100, -5,20) 9 | rect(-5,100,20,20) 10 | print -4 11 | print (-5) 12 | a = 10*-5 13 | a = 10%-5 14 | a = 10 # -5 15 | a = 10%-5 16 | a = 10&-5 17 | a = 10|-5 18 | 19 | # binary op 20 | rect(100,100,- 5,20) # Note that the space 21 | rect(100,100,5-5,20) 22 | rect(100,100,5 - 5,20) 23 | rect(-5,100,20,20) 24 | a -5 25 | rect(100,100,random()-5,20) 26 | b[0]-5 27 | random(b[0],5 28 | -5) 29 | 30 | # Rules for unary op 31 | # - no spaces between minus sign and thingie 32 | # - no number before the minus sign 33 | # - no name before minus sign, except "print" 34 | # - no ')' before minus sign 35 | # - no ']' before minus sign 36 | --------------------------------------------------------------------------------