led_pillar

Documentation: http://frombelow.net/projects/led_pillar/
Clone: git clone https://git.frombelow.net/led_pillar.git
Log | Files | Refs | Submodules | README | LICENSE

commit a8b2ba411221193108d12dd40908322cfedc1879
Author: Gerd Beuster <gerd@frombelow.net>
Date:   Sun, 16 Feb 2020 10:29:15 +0100

Animations, some based on PyIgnition

Diffstat:
A.gitignore | 2++
Anotes.txt | 13+++++++++++++
Apatterns.py | 421+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/PyIgnition.py | 731+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/constants.py | 29+++++++++++++++++++++++++++++
Apyignition/gravity.py | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/interpolate.py | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/keyframes.py | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/obstacles.py | 404+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/particles.py | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apyignition/xml.py | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 2272 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +pyignition/__pycache__/ +**/*.pyc diff --git a/notes.txt b/notes.txt @@ -0,0 +1,13 @@ +sudo sh -c "echo blacklist snd_bcm2835 > /etc/modprobe.d/snd-blacklist.conf" +# In /boot/config.txt auskommentieren: +# dtparam=audio=on +# /boot/cmdline.txt hinzufügen: +spidev.bufsiz=32768 +sudo apt-get install gcc make build-essential python-dev git scons swig +git clone https://github.com/jgarff/rpi_ws281x +cd rpi_ws281x/ +sudo scons +cd python +sudo python setup.py build +sudo python setup.py install + diff --git a/patterns.py b/patterns.py @@ -0,0 +1,421 @@ +#!/usr/bin/env python3 +# Based on NeoPixel library strandtest example +# Author: Tony DiCola (tony@tonydicola.com) +# +# Source: https://github.com/rpi-ws281x/rpi-ws281x-python + +import time +from rpi_ws281x import PixelStrip, Color +import argparse +import random +import sys +sys.path.append('pyignition') +import PyIgnition, pygame, sys, os +import subprocess +import math +import pdb +import multiprocessing + +# LED strip/matrix configuration + +# Matrix +#LED_MATRIX_WIDTH = 16 +#LED_MATRIX_HEIGHT = 16 +# Is row layout of LEDs is left-right, right-left, left-right, ... +# or left-right, left-right, left-right, ...? +#LED_STRIP_SNAKE = True + +# Strip +#LED_MATRIX_WIDTH = 299 +#LED_MATRIX_HEIGHT = 1 +#LED_STRIP_SNAKE = False + +# Pipe with 5 m LED strip, density 60 LEDs / m +#LED_MATRIX_WIDTH = 14.5 +#LED_MATRIX_HEIGHT = 21 +#LED_STRIP_SNAKE = False + +# Pipe with 5 m LED strip, density 144 LEDs / m +LED_MATRIX_WIDTH = 34.7 +LED_MATRIX_HEIGHT = 21 +LED_STRIP_SNAKE = False + +# LED_PIN = 18 # GPIO pin connected to the pixels (18 uses PWM!) - requires root! +LED_PIN = 10 # GPIO pin connected to the pixels (10 uses SPI /dev/spidev0.0) - user must be in group gpio! +LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz) +LED_DMA = 10 # DMA channel to use for generating signal (try 10) +# Set to 0 for darkest and 255 for brightest +# LED_BRIGHTNESS = 255 +LED_BRIGHTNESS = 16 +# LED_BRIGHTNESS = 8 +# LED_BRIGHTNESS = 4 +# LED_BRIGHTNESS = 1 +LED_INVERT = False # True to invert the signal (when using NPN transistor level shift) +LED_CHANNEL = 0 # set to '1' for GPIOs 13, 19, 41, 45 or 53 + + +class Pattern(): + def __init__(self, strip): + self.strip = strip + self.clear_strip() + + def step(self): + pass + + def stop(self): + self.clear_strip() + + def clear_strip(self): + for i in range(math.ceil(LED_MATRIX_WIDTH) * math.ceil(LED_MATRIX_HEIGHT)): + self.strip.setPixelColor(i, Color(0x00, 0x00, 0x00)) + self.strip.show() + + def xy2i(self, x, y): + i = y * LED_MATRIX_WIDTH + if LED_STRIP_SNAKE and (y % 2): + i += LED_MATRIX_WIDTH - x -1 + else: + i += x + return math.floor(i) + + +class TestPattern(Pattern): + + CELL_ON = Color(0x00, 0xFF, 0x00) + CELL_OFF = Color(0x00, 0x00, 0x00) + + def __init__(self, strip): + Pattern.__init__(self, strip=strip) + # Initialize dummy display + os.environ["SDL_VIDEODRIVER"] = "dummy" + subprocess.Popen(["Xvfb", ":1", "-screen", "0", "1024x768x24"], + stderr=subprocess.DEVNULL) + time.sleep(1) + os.environ["DISPLAY"] = ":1" + pygame.display.init() + + self.x = 0 + self.y = 0 + + def step(self): + print("({x}/{y})".format(x=self.x, y=self.y)) + for i in range(math.ceil(LED_MATRIX_WIDTH * LED_MATRIX_HEIGHT)): + self.strip.setPixelColor(i, self.CELL_OFF) + for col in range(math.ceil(LED_MATRIX_WIDTH)): + self.strip.setPixelColor(self.xy2i(col, self.y), self.CELL_ON) + for row in range(math.ceil(LED_MATRIX_HEIGHT)): + self.strip.setPixelColor(self.xy2i(self.x, row), self.CELL_ON) + self.strip.show() + time.sleep(1) + self.x = (self.x + 1) % LED_MATRIX_WIDTH + self.y = (self.y + 1) % LED_MATRIX_HEIGHT + + +class AllOn(Pattern): + def __init__(self, strip): + Pattern.__init__(self, strip=strip) + for i in range(math.ceil(LED_MATRIX_WIDTH*LED_MATRIX_HEIGHT)): + strip.setPixelColor(i, Color(LED_BRIGHTNESS, LED_BRIGHTNESS, LED_BRIGHTNESS)) + strip.show() + + +class Worm(Pattern): + def __init__(self, strip): + Pattern.__init__(self, strip=strip) + # LED_BRIGHTNESS should be at least 15. + self.worm_pattern = list(map(lambda x: Color(x[0], x[1], x[2]), + [(0x00, 0x05, 0x00), + (0x00, 0x20, 0x00), + (0x20, 0x20, 0x00), + (0xFF, 0x00, 0x00), + (0xFF, 0x00, 0x00), + (0xFF, 0x00, 0x00), + (0xFF, 0x00, 0x00), + (0xFF, 0x00, 0x00), + (0x20, 0x20, 0x00), + (0x00, 0x20, 0x00), + (0x00, 0x05, 0x00), + ])) + # Set background color + for i in range(math.ceil(LED_MATRIX_WIDTH*LED_MATRIX_HEIGHT)): + self.strip.setPixelColor(i, Color(0x00, 0x05, 0x00)) + self.strip.show() + self.direction = 1 + self.pos = LED_MATRIX_WIDTH // 2 + + def step(self): + # Larson worm + for i in range(len(self.worm_pattern)): + self.strip.setPixelColor(int(self.pos+i), self.worm_pattern[i]) + if random.random() < .02: + self.direction *= -1 + if self.pos == 0: + self.direction = 1 + if (self.pos + len(self.worm_pattern)) == LED_MATRIX_WIDTH*LED_MATRIX_HEIGHT: + self.direction = -1 + self.pos += self.direction + # Random set blue pixel + if (self.pos % 8) == 0: + self.strip.setPixelColor(random.randint(0, math.ceil(LED_MATRIX_WIDTH*LED_MATRIX_HEIGHT-1)), Color(0x00, 0x00, 0x10)) + # Update Strip + self.strip.show() + time.sleep(.04) + + +class CellularAutomaton(Pattern): + + CELL_ON_COLOR = (0x00, 0xFF, 0x00) + CELL_OFF_COLOR = (0x00, 0x00, 0x00) + + def cell_is_on(self, buf, x, y): + x = (x + LED_MATRIX_WIDTH) % LED_MATRIX_WIDTH + y = (y + LED_MATRIX_HEIGHT) % LED_MATRIX_HEIGHT + return (buf[self.xy2i(x, y)] == self.CELL_ON_COLOR) + + + def show_buf(self, buf): + for i in range(len(buf)): + if(buf[i]): + self.strip.setPixelColor(i, Color(buf[i][0], buf[i][1], buf[i][2])) + else: + self.strip.setPixelColor(i, Color(0x00, 0x00, 0x00)) + self.strip.show() + + + def __init__(self, strip): + Pattern.__init__(self, strip=strip) + self.matrix_buf = [[None] * (math.ceil(LED_MATRIX_WIDTH) * math.ceil(LED_MATRIX_HEIGHT)), + [None] * (math.ceil(LED_MATRIX_WIDTH) * math.ceil(LED_MATRIX_HEIGHT))] + self.number_of_updates = 0 + self.stable_pattern = True + self.current_buf = self.matrix_buf[0] + for index in range(math.ceil(LED_MATRIX_WIDTH) * math.ceil(LED_MATRIX_HEIGHT)): + self.current_buf[index] = self.CELL_OFF_COLOR + + + def step(self): + self.current_buf = self.matrix_buf[self.number_of_updates % 2] + self.next_buf = self.matrix_buf[(self.number_of_updates + 1) % 2] + # Since we get a stable pattern after sometime, we restart with a + # new random cellular automaton after a fixed number of steps or + # when we detect a stable situation. + if self.stable_pattern or (self.number_of_updates == 100): + if True: + # Initialize with random values + for index in range(math.ceil(LED_MATRIX_WIDTH) * math.ceil(LED_MATRIX_HEIGHT)): + if (random.random() < .5): + self.current_buf[index] = self.CELL_ON_COLOR + else: + self.current_buf[index] = self.CELL_OFF_COLOR + else: + # Start with a glider + self.current_buf[self.xy2i(3,6)] = self.CELL_ON_COLOR + self.current_buf[self.xy2i(4,6)] = self.CELL_ON_COLOR + self.current_buf[self.xy2i(5,6)] = self.CELL_ON_COLOR + self.current_buf[self.xy2i(5,7)] = self.CELL_ON_COLOR + self.current_buf[self.xy2i(4,8)] = self.CELL_ON_COLOR + self.number_of_updates = 0 + self.stable_pattern = False + + self.number_of_updates += 1 + # Cycle over all cells + for y in range(math.ceil(LED_MATRIX_HEIGHT)): + for x in range(math.ceil(LED_MATRIX_WIDTH)): + # Count neighbors + neighbors = 0 + for y_neighbor in range(3): + for x_neighbor in range(3): + if (((x_neighbor != 1) or (y_neighbor != 1)) and + self.cell_is_on(self.current_buf, + (x-x_neighbor+1) % LED_MATRIX_WIDTH, + (y-y_neighbor+1) % LED_MATRIX_HEIGHT)): + neighbors += 1 + # Update cell + if (neighbors < 2) or (neighbors > 3): + self.next_buf[self.xy2i(x, y)] = self.CELL_OFF_COLOR + elif neighbors == 3: + self.next_buf[self.xy2i(x, y)] = self.CELL_ON_COLOR + else: + self.next_buf[self.xy2i(x, y)] = self.current_buf[self.xy2i(x, y)] + # Check if pattern has changed since last update. + # (We will exit the loop if the pattern becomes stable) + self.stable_pattern = True + for i in range(math.ceil(LED_MATRIX_HEIGHT) * math.ceil(LED_MATRIX_WIDTH)): + if self.next_buf[i] != self.current_buf[i]: + self.stable_pattern = False + break + self.show_buf(self.current_buf) + time.sleep(.1) + + +class BlueGreenFlames(Pattern): + def __init__(self, strip): + Pattern.__init__(self, strip=strip) + # This mode works great with LED_BRIGHTNESS set to 16. + self.SCALE=40 # Scaling factor for screen + + # Also show in window? + # self.HEADLESS=False + self.HEADLESS=True + + if self.HEADLESS: + # On Raspbian, setting a dummy videodriver + # does not prevent pygame from trying to + # open a X display. Therefore we use a + # virtual framebuffer. + os.environ["SDL_VIDEODRIVER"] = "dummy" + subprocess.Popen(["Xvfb", ":1", "-screen", "0", "1024x768x24"], + stderr=subprocess.DEVNULL) + time.sleep(1) + os.environ["DISPLAY"] = ":1" + + pygame.display.init() + self.screen = pygame.Surface((math.ceil(LED_MATRIX_WIDTH), + math.ceil(LED_MATRIX_HEIGHT))) + self.screen_scaled = pygame.display.set_mode((math.ceil(LED_MATRIX_WIDTH*self.SCALE), + math.ceil(LED_MATRIX_HEIGHT*self.SCALE))) + pygame.display.set_caption("Blue Green Fire") + self.clock = pygame.time.Clock() + + self.fire = PyIgnition.ParticleEffect(self.screen, (0, 0), + (math.ceil(LED_MATRIX_WIDTH), + math.ceil(LED_MATRIX_HEIGHT))) + gravity = self.fire.CreateDirectedGravity(strength=0.1, direction=[0, -1]) + + # Behavior of blue particles + self.blue_flames = [] + blue_flames_scale_factor = 4 + for x_pos in range(0, math.ceil(LED_MATRIX_WIDTH), + math.ceil(LED_MATRIX_WIDTH//5)): + new_flame = self.fire.CreateSource(pos=(x_pos, LED_MATRIX_HEIGHT*1.1), + initspeed = .01, initdirection = 0.0, + initspeedrandrange = .1, + initdirectionrandrange = 0.5, + particlesperframe = 1, + particlelife = 100, + drawtype = PyIgnition.DRAWTYPE_CIRCLE, + colour = (50, 20, 200), + radius = LED_MATRIX_WIDTH/32.0*blue_flames_scale_factor) + new_flame.CreateParticleKeyframe(1, colour = (50, 20, 200), + radius = LED_MATRIX_WIDTH/32.0*blue_flames_scale_factor) + new_flame.CreateParticleKeyframe(3, colour = (0, 0, 150), + radius = LED_MATRIX_WIDTH/32.0*blue_flames_scale_factor) + new_flame.CreateParticleKeyframe(6, colour = (20, 20, 50), + radius = LED_MATRIX_WIDTH/32.0*blue_flames_scale_factor) + new_flame.CreateParticleKeyframe(9, colour = (0, 0, 0), + radius = LED_MATRIX_WIDTH/32.0*blue_flames_scale_factor) + self.blue_flames += [new_flame] + + + # Behavior of green particle + self.green_flame_x = LED_MATRIX_WIDTH / 2 + self.green_flame_y = LED_MATRIX_HEIGHT + 1 + self.green_flame = self.fire.CreateSource(pos=(self.green_flame_x, self.green_flame_y), + initspeed = .3, initdirection = 0.0, + initspeedrandrange = .3, + initdirectionrandrange = 0.5, + particlesperframe = 1, particlelife = 100, + drawtype = PyIgnition.DRAWTYPE_CIRCLE, + colour = (255, 200, 100), + radius = LED_MATRIX_WIDTH/36.0) + self.green_flame.CreateParticleKeyframe(5, colour = (50, 200, 20), + radius = LED_MATRIX_WIDTH/36.0) + self.green_flame.CreateParticleKeyframe(10, colour = (0, 150, 0), + radius = LED_MATRIX_WIDTH/36.0) + self.green_flame.CreateParticleKeyframe(15, colour = (20, 50, 20), + radius = LED_MATRIX_WIDTH/36.0) + self.green_flame.CreateParticleKeyframe(20, colour = (0, 0, 0), + radius = LED_MATRIX_WIDTH/36.0) + + + + + def step(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + # Draw on screen + self.screen.fill((0, 0, 0)) + self.green_flame_x = random.randint(0, math.ceil(LED_MATRIX_WIDTH)-1) + self.green_flame.pos = (self.green_flame_x, self.green_flame_y) + if self.green_flame.curframe % 10 == 0: + self.green_flame.ConsolidateKeyframes() + for b in self.blue_flames: + if b.curframe % 30 == 0: + b.ConsolidateKeyframes() + self.fire.Update() + self.fire.Redraw() + pygame.transform.scale(self.screen, (math.ceil(LED_MATRIX_WIDTH*self.SCALE), + math.ceil(LED_MATRIX_HEIGHT*self.SCALE)), + self.screen_scaled) + pygame.display.update() + # Draw on LED matrix + fb = pygame.PixelArray(self.screen) + for y in range(math.ceil(LED_MATRIX_HEIGHT)): + for x in range(math.ceil(LED_MATRIX_WIDTH)): + r = fb[x][y] >> 16 & 0xFF + g = fb[x][y] >> 8 & 0xFF + b = fb[x][y] & 0xFF + self.strip.setPixelColor(self.xy2i(x, + math.ceil(LED_MATRIX_HEIGHT)-1-y), + Color(r, g, b)) + self.strip.show() + self.clock.tick(5) + +MODE_ALL_ON = 0 +MODE_WORM = 1 +MODE_CELLULAR_AUTOMATON = 2 +MODE_BLUE_GREEN_FLAMES = 3 +MODE_TEST_PATTERN = 4 + +mode = MODE_BLUE_GREEN_FLAMES +# mode = MODE_WORM +# mode = MODE_CELLULAR_AUTOMATON + +def run(cmd_queue): + global mode + # Create NeoPixel object with appropriate configuration. + strip = PixelStrip(math.ceil(LED_MATRIX_WIDTH * LED_MATRIX_HEIGHT), LED_PIN, LED_FREQ_HZ, LED_DMA, LED_INVERT, LED_BRIGHTNESS, LED_CHANNEL) + # Intialize the library (must be called once before other functions). + strip.begin() + # This will be immediately replaced by the actual initial mode. + p = Pattern(strip) + current_mode = -1 + try: + while True: + if not cmd_queue.empty(): + mode = cmd_queue.get_nowait() + print("Got {}".format(mode)) + if current_mode != mode: + # External mode switch + p.stop() + current_mode = mode + p = { + MODE_ALL_ON : AllOn, + MODE_WORM : Worm, + MODE_CELLULAR_AUTOMATON : CellularAutomaton, + MODE_BLUE_GREEN_FLAMES : BlueGreenFlames, + MODE_TEST_PATTERN : TestPattern + }[current_mode](strip) + p.step() + except KeyboardInterrupt: + p.stop() + +def cmd_demo(cmd_queue): + mode = 0 + while True: + mode = (mode + 1) % 5 + print("Putting {}".format(mode)) + cmd_queue.put(mode) + time.sleep(10) + + +# Main program logic follows: +if __name__ == '__main__': + cmd_queue = multiprocessing.Queue() + run = multiprocessing.Process(target=run, args=(cmd_queue,)) + run.start() + if False: + cmd_demo = multiprocessing.Process(target=cmd_demo, args=(cmd_queue,)) + cmd_demo.start() + run.join() diff --git a/pyignition/PyIgnition.py b/pyignition/PyIgnition.py @@ -0,0 +1,731 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Particle effect manager + + +import particles, gravity, obstacles, constants, sys, pygame, xml +from constants import * + + +class ParticleEffect: + def __init__(self, display, pos = (0, 0), size = (0, 0)): + self.display = display + self.pos = pos + self.size = size + + self.left = pos[0] + self.top = pos[1] + self.right = pos[0] + size[0] + self.bottom = pos[1] + size[1] + + self.particles = [] + self.sources = [] + self.gravities = [] + self.obstacles = [] + + def Update(self): + for source in self.sources: + source.Update() + + for gravity in self.gravities: + gravity.Update() + + for obstacle in self.obstacles: + obstacle.Update() + + for particle in self.particles: + radius = 0.0 # Used for making sure radii don't overlap objects... + + if particle.drawtype == DRAWTYPE_CIRCLE or particle.drawtype == DRAWTYPE_BUBBLE: + radius = particle.radius * (1.0 - RADIUS_PERMITTIVITY) # ...But only set if the particle is actually circular + + # First calculate the forces acting on the particle + totalforce = [0.0, 0.0] + + for gravity in self.gravities: + force = gravity.GetForceOnParticle(particle) + totalforce[0] += force[0] + totalforce[1] += force[1] + + for obstacle in self.obstacles: + force = obstacle.GetForce(particle.pos, particle.velocity, radius) + totalforce[0] += force[0] + totalforce[1] += force[1] + + # Apply the forces to the velocity and update the particle + particle.velocity = [particle.velocity[0] + totalforce[0], particle.velocity[1] + totalforce[1]] + + particle.Update() + + # Resolve collisions + for obstacle in self.obstacles: + if (not obstacle.OutOfRange(particle.pos)) and (obstacle.InsideObject(particle.pos, radius)): + particle.pos = obstacle.GetResolved(particle.pos, radius) + + # Delete dead particles + for particle in self.particles: + if not particle.alive: + self.particles.remove(particle) + + def Redraw(self): + for particle in self.particles: + particle.Draw(self.display) + + for obstacle in self.obstacles: + obstacle.Draw(self.display) + + def CreateSource(self, pos = (0, 0), initspeed = 0.0, initdirection = 0.0, initspeedrandrange = 0.0, initdirectionrandrange = 0.0, particlesperframe = 0, particlelife = -1, genspacing = 0, drawtype = 0, colour = (0, 0, 0), radius = 0.0, length = 0.0, imagepath = None): + newsource = particles.ParticleSource(self, pos, initspeed, initdirection, initspeedrandrange, initdirectionrandrange, particlesperframe, particlelife, genspacing, drawtype, colour, radius, length, imagepath) + self.sources.append(newsource) + return newsource # Effectively a reference + + def CreatePointGravity(self, strength = 0.0, strengthrandrange = 0.0, pos = (0, 0)): + newgrav = gravity.PointGravity(strength, strengthrandrange, pos) + self.gravities.append(newgrav) + return newgrav + + def CreateDirectedGravity(self, strength = 0.0, strengthrandrange = 0.0, direction = [0, 1]): + newgrav = gravity.DirectedGravity(strength, strengthrandrange, direction) + self.gravities.append(newgrav) + return newgrav + + def CreateVortexGravity(self, strength = 0.0, strengthrandrange = 0.0, pos = (0, 0)): + newgrav = gravity.VortexGravity(strength, strengthrandrange, pos) + self.gravities.append(newgrav) + return newgrav + + def CreateCircle(self, pos = (0, 0), colour = (0, 0, 0), bounce = 1.0, radius = 0.0): + newcircle = obstacles.Circle(self, pos, colour, bounce, radius) + self.obstacles.append(newcircle) + return newcircle + + def CreateRectangle(self, pos = (0, 0), colour = (0, 0, 0), bounce = 1.0, width = 0.0, height = 0.0): + newrect = obstacles.Rectangle(self, pos, colour, bounce, width, height) + self.obstacles.append(newrect) + return newrect + + def CreateBoundaryLine(self, pos = (0, 0), colour = (0, 0, 0), bounce = 1.0, normal = [0, 1]): + newline = obstacles.BoundaryLine(self, pos, colour, bounce, normal) + self.obstacles.append(newline) + return newline + + def AddParticle(self, particle): + self.particles.append(particle) + + def GetDrawtypeAsString(self, drawtype): + if drawtype == DRAWTYPE_POINT: + return "point" + elif drawtype == DRAWTYPE_CIRCLE: + return "circle" + elif drawtype == DRAWTYPE_LINE: + return "line" + elif drawtype == DRAWTYPE_SCALELINE: + return "scaleline" + elif drawtype == DRAWTYPE_BUBBLE: + return "bubble" + elif drawtype == DRAWTYPE_IMAGE: + return "image" + else: + return "ERROR: Invalid drawtype" + + def GetStringAsDrawtype(self, string): + if string == "point": + return DRAWTYPE_POINT + elif string == "circle": + return DRAWTYPE_CIRCLE + elif string == "line": + return DRAWTYPE_LINE + elif string == "scaleline": + return DRAWTYPE_SCALELINE + elif string == "bubble": + return DRAWTYPE_BUBBLE + elif string == "image": + return DRAWTYPE_IMAGE + else: + return DRAWTYPE_POINT + + def GetInterpolationtypeAsString(self, interpolationtype): + if interpolationtype == INTERPOLATIONTYPE_LINEAR: + return "linear" + elif interpolationtype == INTERPOLATIONTYPE_COSINE: + return "cosine" + + def GetStringAsInterpolationtype(self, string): + if string == "linear": + return INTERPOLATIONTYPE_LINEAR + elif string == "cosine": + return INTERPOLATIONTYPE_COSINE + else: + return INTERPOLATIONTYPE_LINEAR + + def TranslatePos(self, pos): + return (pos[0] - self.pos[0], pos[1] - self.pos[1]) + + def ConvertXMLTuple(self, string): + # 'string' must be of the form "(value, value, value, [...])" + bracketless = string.replace("(", "").replace(")", "") + strings = bracketless.split(", ") + finaltuple = [] + for string in strings: + temp = string.split(".") + if len(temp) > 1: + finaltuple.append(float(string)) + else: + finaltuple.append(int(string)) + + return tuple(finaltuple) + + def SaveToFile(self, outfilename): + outfile = open(outfilename, 'w') + + outfile.write("<?xml version = \"1.0\"?>\n<?pyignition version = \"%f\"?>\n\n" % PYIGNITION_VERSION) + outfile.write("<effect>\n") + + # Write out sources + for source in self.sources: + outfile.write("\t<source>\n") + + # Write out source variables + outfile.write("\t\t<pos>(%i, %i)</pos>\n" % source.pos) + outfile.write("\t\t<initspeed>%f</initspeed>\n" % source.initspeed) + outfile.write("\t\t<initdirection>%f</initdirection>\n" % source.initdirection) + outfile.write("\t\t<initspeedrandrange>%f</initspeedrandrange>\n" % source.initspeedrandrange) + outfile.write("\t\t<initdirectionrandrange>%f</initdirectionrandrange>\n" % source.initdirectionrandrange) + outfile.write("\t\t<particlesperframe>%i</particlesperframe>\n" % source.particlesperframe) + outfile.write("\t\t<particlelife>%i</particlelife>\n" % source.particlelife) + outfile.write("\t\t<genspacing>%i</genspacing>\n" % source.genspacing) + outfile.write("\t\t<drawtype>%s</drawtype>\n" % self.GetDrawtypeAsString(source.drawtype)) + outfile.write("\t\t<colour>(%i, %i, %i)</colour>\n" % source.colour) + outfile.write("\t\t<radius>%f</radius>\n" % source.radius) + outfile.write("\t\t<length>%f</length>\n" % source.length) + outfile.write("\t\t<imagepath>%s</imagepath>\n" % source.imagepath) + + # Write out source keyframes + outfile.write("\t\t<keyframes>\n") + + for keyframe in source.keyframes: + if keyframe.frame == 0: # Don't bother writing out the first keyframe + continue + + outfile.write("\t\t\t<keyframe frame = \"%i\">\n" % keyframe.frame) + + # Write out keyframed variables + for variable in keyframe.variables.keys(): + if variable == "interpolationtype": + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, self.GetInterpolationtypeAsString(keyframe.variables[variable]), variable)) + else: + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, str(keyframe.variables[variable]), variable)) + + outfile.write("\t\t\t</keyframe>\n") + + outfile.write("\t\t</keyframes>\n") + + # Write out source particle keyframes + outfile.write("\t\t<particlekeyframes>\n") + + for keyframe in source.particlekeyframes: + if keyframe.frame == 0: # Don't bother writing out the first keyframe + continue + + outfile.write("\t\t\t<keyframe frame = \"%i\">\n" % keyframe.frame) + + # Write out keyframed variables + for variable in keyframe.variables.keys(): + if variable == "interpolationtype": + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, self.GetInterpolationtypeAsString(keyframe.variables[variable]), variable)) + else: + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, str(keyframe.variables[variable]), variable)) + + outfile.write("\t\t\t</keyframe>\n") + + outfile.write("\t\t</particlekeyframes>\n") + + outfile.write("\t</source>\n\n") + + # Write out gravities + for gravity in self.gravities: + # Identify type + gtype = gravity.type + + outfile.write("\t<%sgravity>\n" % gtype) + + # Write out gravity variables + outfile.write("\t\t<strength>%f</strength>\n" % gravity.initstrength) + outfile.write("\t\t<strengthrandrange>%f</strengthrandrange>\n" % gravity.strengthrandrange) + if gtype == "directed": + outfile.write("\t\t<direction>(%f, %f)</direction>\n" % tuple(gravity.direction)) + elif gtype == "point" or gtype == "vortex": + outfile.write("\t\t<pos>(%i, %i)</pos>\n" % gravity.pos) + + # Write out gravity keyframes + outfile.write("\t\t<keyframes>\n") + + for keyframe in gravity.keyframes: + if keyframe.frame == 0: # Don't bother writing out the first keyframe + continue + + outfile.write("\t\t\t<keyframe frame = \"%i\">\n" % keyframe.frame) + + # Write out keyframed variables + for variable in keyframe.variables.keys(): + if variable == "interpolationtype": + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, self.GetInterpolationtypeAsString(keyframe.variables[variable]), variable)) + else: + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, str(keyframe.variables[variable]), variable)) + + outfile.write("\t\t\t</keyframe>\n") + + outfile.write("\t\t</keyframes>\n") + + outfile.write("\t</%sgravity>\n\n" % gtype) + + # Write out obstacles + for obstacle in self.obstacles: + # Identify type + otype = obstacle.type + + outfile.write("\t<%s>\n" % otype) + + # Write out obstacle variables + outfile.write("\t\t<pos>(%i, %i)</pos>\n" % obstacle.pos) + outfile.write("\t\t<colour>(%i, %i, %i)</colour>\n" % obstacle.colour) + outfile.write("\t\t<bounce>%f</bounce>\n" % obstacle.bounce) + if otype == "circle": + outfile.write("\t\t<radius>%f</radius>\n" % obstacle.radius) + elif otype == "rectangle": + outfile.write("\t\t<width>%i</width>\n" % obstacle.width) + outfile.write("\t\t<height>%i</height>\n" % obstacle.height) + elif otype == "boundaryline": + outfile.write("\t\t<normal>(%f, %f)</normal>\n" % tuple(obstacle.normal)) + + # Write out obstacle keyframes + outfile.write("\t\t<keyframes>\n") + + for keyframe in obstacle.keyframes: + if keyframe.frame == 0: # Don't bother writing out the first keyframe + continue + + outfile.write("\t\t\t<keyframe frame = \"%i\">\n" % keyframe.frame) + + # Write out keyframed variables + for variable in keyframe.variables.keys(): + if variable == "interpolationtype": + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, self.GetInterpolationtypeAsString(keyframe.variables[variable]), variable)) + else: + outfile.write("\t\t\t\t<%s>%s</%s>\n" % (variable, str(keyframe.variables[variable]), variable)) + + outfile.write("\t\t\t</keyframe>\n") + + outfile.write("\t\t</keyframes>\n") + + outfile.write("\t</%s>\n\n" % otype) + + outfile.write("</effect>") + outfile.close() + + def LoadFromFile(self, infilename): + infile = open(infilename, "r") + + data = xml.XMLParser(infile.read()).Parse() + infile.close() + + for child in data.children: + if child.tag == "source": # Source object + pos = (0, 0) + initspeed = 0.0 + initdirection = 0.0 + initspeedrandrange = 0.0 + initdirectionrandrange = 0.0 + particlesperframe = 0 + particlelife = 0 + genspacing = 0 + drawtype = DRAWTYPE_POINT + colour = (0, 0, 0) + radius = 0.0 + length = 0.0 + imagepath = None + + keyframes = None + particlekeyframes = None + + for parameter in child.children: + if parameter.tag == "pos": + pos = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "initspeed": + initspeed = float(parameter.inside) + elif parameter.tag == "initdirection": + initdirection = float(parameter.inside) + elif parameter.tag == "initspeedrandrange": + initspeedrandrange = float(parameter.inside) + elif parameter.tag == "initdirectionrandrange": + initdirectionrandrange = float(parameter.inside) + elif parameter.tag == "particlesperframe": + particlesperframe = int(parameter.inside) + elif parameter.tag == "particlelife": + particlelife = int(parameter.inside) + elif parameter.tag == "genspacing": + genspacing = int(parameter.inside) + elif parameter.tag == "drawtype": + drawtype = self.GetStringAsDrawtype(parameter.inside) + elif parameter.tag == "colour": + colour = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "radius": + radius = float(parameter.inside) + elif parameter.tag == "length": + length = float(parameter.inside) + elif parameter.tag == "image": + imagepath = float(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + elif parameter.tag == "particlekeyframes": + particlekeyframes = parameter.children + + newsource = self.CreateSource(pos, initspeed, initdirection, initspeedrandrange, initdirectionrandrange, particlesperframe, particlelife, genspacing, drawtype, colour, radius, length, imagepath) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "initspeed" and variable.inside != "None": + variables['initspeed'] = float(variable.inside) + elif variable.tag == "initdirection" and variable.inside != "None": + variables['initdirection'] = float(variable.inside) + elif variable.tag == "initspeedrandrange" and variable.inside != "None": + variables['initspeedrandrange'] = float(variable.inside) + elif variable.tag == "initdirectionrandrange" and variable.inside != "None": + variables['initdirectionrandrange'] = float(variable.inside) + elif variable.tag == "particlesperframe" and variable.inside != "None": + variables['particlesperframe'] = int(variable.inside) + elif variable.tag == "genspacing" and variable.inside != "None": + variables['genspacing'] = int(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newsource.CreateKeyframe(frame = frame) + newframe.variables = variables + + for keyframe in particlekeyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "colour_r" and variable.inside != "None": + variables['colour_r'] = int(variable.inside) + elif variable.tag == "colour_g" and variable.inside != "None": + variables['colour_g'] = int(variable.inside) + elif variable.tag == "colour_b" and variable.inside != "None": + variables['colour_b'] = int(variable.inside) + elif variable.tag == "radius" and variable.inside != "None": + variables['radius'] = float(variable.inside) + elif variable.tag == "length" and variable.inside != "None": + variables['length'] = float(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newsource.CreateParticleKeyframe(frame = frame) + newframe.variables = variables + newsource.PreCalculateParticles() + + elif child.tag == "directedgravity": + strength = 0.0 + strengthrandrange = 0.0 + direction = [0, 0] + + keyframes = None + + for parameter in child.children: + if parameter.tag == "strength": + strength = float(parameter.inside) + elif parameter.tag == "strengthrandrange": + strengthrandrange = float(parameter.inside) + elif parameter.tag == "direction": + direction = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newgrav = self.CreateDirectedGravity(strength, strengthrandrange, direction) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "strength" and variable.inside != "None": + variables['strength'] = float(variable.inside) + elif variable.tag == "strengthrandrange" and variable.inside != "None": + variables['strengthrandrange'] = float(variable.inside) + elif variable.tag == "direction_x" and variable.inside != "None": + variables['direction_x'] = float(variable.inside) + elif variable.tag == "direction_y" and variable.inside != "None": + variables['direction_y'] = float(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newgrav.CreateKeyframe(frame = frame) + newframe.variables = variables + + elif child.tag == "pointgravity": + strength = 0.0 + strengthrandrange = 0.0 + pos = (0, 0) + + keyframes = None + + for parameter in child.children: + if parameter.tag == "strength": + strength = float(parameter.inside) + elif parameter.tag == "strengthrandrange": + strengthrandrange = float(parameter.inside) + elif parameter.tag == "pos": + pos = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newgrav = self.CreatePointGravity(strength, strengthrandrange, pos) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "strength" and variable.inside != "None": + variables['strength'] = float(variable.inside) + elif variable.tag == "strengthrandrange" and variable.inside != "None": + variables['strengthrandrange'] = float(variable.inside) + elif variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newgrav.CreateKeyframe(frame = frame) + newframe.variables = variables + + elif child.tag == "vortexgravity": + strength = 0.0 + strengthrandrange = 0.0 + pos = (0, 0) + + keyframes = None + + for parameter in child.children: + if parameter.tag == "strength": + strength = float(parameter.inside) + elif parameter.tag == "strengthrandrange": + strengthrandrange = float(parameter.inside) + elif parameter.tag == "pos": + direction = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newgrav = self.CreateVortexGravity(strength, strengthrandrange, pos) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "strength" and variable.inside != "None": + variables['strength'] = float(variable.inside) + elif variable.tag == "strengthrandrange" and variable.inside != "None": + variables['strengthrandrange'] = float(variable.inside) + elif variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newgrav.CreateKeyframe(frame = frame) + newframe.variables = variables + + elif child.tag == "circle": + pos = (0, 0) + colour = (0, 0, 0) + bounce = 0.0 + radius = 0.0 + + keyframes = None + + for parameter in child.children: + if parameter.tag == "pos": + pos = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "colour": + colour = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "bounce": + bounce = float(parameter.inside) + elif parameter.tag == "radius": + radius = float(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newobstacle = self.CreateCircle(pos, colour, bounce, radius) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "colour_r" and variable.inside != "None": + variables['colour_r'] = int(variable.inside) + elif variable.tag == "colour_g" and variable.inside != "None": + variables['colour_g'] = int(variable.inside) + elif variable.tag == "colour_b" and variable.inside != "None": + variables['colour_b'] = int(variable.inside) + elif variable.tag == "bounce" and variable.inside != "None": + variables['bounce'] = float(variable.inside) + elif variable.tag == "radius" and variable.inside != "None": + variables['radius'] = float(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newobstacle.CreateKeyframe(frame = frame) + newframe.variables = variables + + elif child.tag == "rectangle": + pos = (0, 0) + colour = (0, 0, 0) + bounce = 0.0 + width = 0.0 + height = 0.0 + + keyframes = None + + for parameter in child.children: + if parameter.tag == "pos": + pos = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "colour": + colour = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "bounce": + bounce = float(parameter.inside) + elif parameter.tag == "width": + width = float(parameter.inside) + elif parameter.tag == "height": + height = float(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newobstacle = self.CreateRectangle(pos, colour, bounce, width, height) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "colour_r" and variable.inside != "None": + variables['colour_r'] = int(variable.inside) + elif variable.tag == "colour_g" and variable.inside != "None": + variables['colour_g'] = int(variable.inside) + elif variable.tag == "colour_b" and variable.inside != "None": + variables['colour_b'] = int(variable.inside) + elif variable.tag == "bounce" and variable.inside != "None": + variables['bounce'] = float(variable.inside) + elif variable.tag == "width" and variable.inside != "None": + variables['width'] = float(variable.inside) + elif variable.tag == "height" and variable.inside != "None": + variables['height'] = float(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newobstacle.CreateKeyframe(frame = frame) + newframe.variables = variables + + elif child.tag == "boundaryline": + pos = (0, 0) + colour = (0, 0, 0) + bounce = 0.0 + direction = [0.0, 0.0] + + keyframes = None + + for parameter in child.children: + if parameter.tag == "pos": + pos = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "colour": + colour = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "bounce": + bounce = float(parameter.inside) + elif parameter.tag == "normal": + normal = self.ConvertXMLTuple(parameter.inside) + elif parameter.tag == "keyframes": + keyframes = parameter.children + + newobstacle = self.CreateBoundaryLine(pos, colour, bounce, normal) + + for keyframe in keyframes: + frame = int(keyframe.meta['frame']) + variables = {} + + for variable in keyframe.children: + if variable.tag == "pos_x" and variable.inside != "None": + variables['pos_x'] = int(variable.inside) + elif variable.tag == "pos_y" and variable.inside != "None": + variables['pos_y'] = int(variable.inside) + elif variable.tag == "colour_r" and variable.inside != "None": + variables['colour_r'] = int(variable.inside) + elif variable.tag == "colour_g" and variable.inside != "None": + variables['colour_g'] = int(variable.inside) + elif variable.tag == "colour_b" and variable.inside != "None": + variables['colour_b'] = int(variable.inside) + elif variable.tag == "bounce" and variable.inside != "None": + variables['bounce'] = float(variable.inside) + elif variable.tag == "normal_x" and variable.inside != "None": + variables['normal_x'] = float(variable.inside) + elif variable.tag == "normal_y" and variable.inside != "None": + variables['normal_y'] = float(variable.inside) + elif variable.tag == "interpolationtype" and variable.inside != "None": + variables['interpolationtype'] = self.GetStringAsInterpolationtype(variable.inside) + + newframe = newobstacle.CreateKeyframe(frame = frame) + newframe.variables = variables + + +## Begin testing code +if __name__ == '__main__': + screen = pygame.display.set_mode((800, 600)) + pygame.display.set_caption("PyIgnition demo") + clock = pygame.time.Clock() + test = ParticleEffect(screen, (0, 0), (800, 600)) + testgrav = test.CreatePointGravity(strength = 1.0, pos = (500, 380)) + testgrav.CreateKeyframe(300, strength = 10.0, pos = (0, 0)) + testgrav.CreateKeyframe(450, strength = 10.0, pos = (40, 40)) + testgrav.CreateKeyframe(550, strength = -2.0, pos = (600, 480)) + testgrav.CreateKeyframe(600, strength = -20.0, pos = (600, 0)) + testgrav.CreateKeyframe(650, strength = 1.0, pos = (500, 380)) + anothertestgrav = test.CreateDirectedGravity(strength = 0.04, direction = [1, 0]) + anothertestgrav.CreateKeyframe(300, strength = 1.0, direction = [-0.5, 1]) + anothertestgrav.CreateKeyframe(600, strength = 1.0, direction = [1.0, -0.1]) + anothertestgrav.CreateKeyframe(650, strength = 0.04, direction = [1, 0]) + testsource = test.CreateSource((10, 10), initspeed = 5.0, initdirection = 2.35619449, initspeedrandrange = 2.0, initdirectionrandrange = 1.0, particlesperframe = 5, particlelife = 125, drawtype = DRAWTYPE_SCALELINE, colour = (255, 255, 255), length = 10.0) + testsource.CreateParticleKeyframe(50, colour = (0, 255, 0), length = 10.0) + testsource.CreateParticleKeyframe(75, colour = (255, 255, 0), length = 10.0) + testsource.CreateParticleKeyframe(100, colour = (0, 255, 255), length = 10.0) + testsource.CreateParticleKeyframe(125, colour = (0, 0, 0), length = 10.0) + + test.SaveToFile("PyIgnition test.ppe") + + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + sys.exit() + + screen.fill((0, 0, 0)) + test.Update() + test.Redraw() + pygame.display.update() + clock.tick(20) diff --git a/pyignition/constants.py b/pyignition/constants.py @@ -0,0 +1,28 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Global constants module + + +# Which version is this? +PYIGNITION_VERSION = 1.0 + +# Drawtype constants +DRAWTYPE_POINT = 100 +DRAWTYPE_CIRCLE = 101 +DRAWTYPE_LINE = 102 +DRAWTYPE_SCALELINE = 103 +DRAWTYPE_BUBBLE = 104 +DRAWTYPE_IMAGE = 105 + +# Interpolation type constants +INTERPOLATIONTYPE_LINEAR = 200 +INTERPOLATIONTYPE_COSINE = 201 + +# Gravity constants +UNIVERSAL_CONSTANT_OF_MAKE_GRAVITY_LESS_STUPIDLY_SMALL = 1000.0 # Well, Newton got one to make it less stupidly large. +VORTEX_ACCELERATION = 0.01 # A tiny value added to the centripetal force exerted by vortex gravities to draw in particles +VORTEX_SWALLOWDIST = 20.0 # Particles closer than this will be swallowed up and regurgitated in the bit bucket + +# Fraction of radius which can go inside an object +RADIUS_PERMITTIVITY = 0.3+ \ No newline at end of file diff --git a/pyignition/gravity.py b/pyignition/gravity.py @@ -0,0 +1,197 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Gravity objects + + +from math import sqrt +import keyframes, interpolate, random +from constants import * + + +def RandomiseStrength(base, range): + return base + (float(random.randrange(int(-range * 100), int(range * 100))) / 100.0) + + +class DirectedGravity: + def __init__(self, strength = 0.0, strengthrandrange = 0.0, direction = [0, 1]): + self.type = "directed" + self.initstrength = strength + self.strength = strength + self.strengthrandrange = strengthrandrange + directionmag = sqrt(direction[0]**2 + direction[1]**2) + self.direction = [direction[0] / directionmag, direction[1] / directionmag] + + self.keyframes = [] + self.CreateKeyframe(0, self.strength, self.strengthrandrange, self.direction) + self.curframe = 0 + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'direction_x':self.direction[0], 'direction_y':self.direction[1]}, self.keyframes) + self.initstrength = newvars['strength'] + self.strengthrandrange = newvars['strengthrandrange'] + self.direction = [newvars['direction_x'], newvars['direction_y']] + + if self.strengthrandrange != 0.0: + self.strength = RandomiseStrength(self.initstrength, self.strengthrandrange) + + self.curframe = self.curframe + 1 + + def GetForce(self, pos): + force = [self.strength * self.direction[0], self.strength * self.direction[1]] + + return force + + def GetForceOnParticle(self, particle): + return self.GetForce(particle.pos) + + def CreateKeyframe(self, frame, strength = None, strengthrandrange = None, direction = [None, None], interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'strength':strength, 'strengthrandrange':strengthrandrange, 'direction_x':direction[0], 'direction_y':direction[1], 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'direction_x':self.direction[0], 'direction_y':self.direction[1]}) + + def SetStrength(self, newstrength): + self.CreateKeyframe(self.curframe, strength = newstrength) + + def SetStrengthRandRange(self, newstrengthrandrange): + self.CreateKeyframe(self.curframe, strengthrandrange = newstrengthrandrange) + + def SetDirection(self, newdirection): + self.CreateKeyframe(self.curframe, direction = newdirection) + + +class PointGravity: + def __init__(self, strength = 0.0, strengthrandrange = 0.0, pos = (0, 0)): + self.type = "point" + self.initstrength = strength + self.strength = strength + self.strengthrandrange = strengthrandrange + self.pos = pos + + self.keyframes = [] + self.CreateKeyframe(0, self.strength, self.strengthrandrange, self.pos) + self.curframe = 0 + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'pos_x':self.pos[0], 'pos_y':self.pos[1]}, self.keyframes) + self.initstrength = newvars['strength'] + self.strengthrandrange = newvars['strengthrandrange'] + self.pos = (newvars['pos_x'], newvars['pos_y']) + + if self.strengthrandrange != 0.0: + self.strength = RandomiseStrength(self.initstrength, self.strengthrandrange) + else: + self.strength = self.initstrength + + self.curframe = self.curframe + 1 + + def GetForce(self, pos): + distsquared = (pow(float(pos[0] - self.pos[0]), 2.0) + pow(float(pos[1] - self.pos[1]), 2.0)) + if distsquared == 0.0: + return [0.0, 0.0] + + forcemag = (self.strength * UNIVERSAL_CONSTANT_OF_MAKE_GRAVITY_LESS_STUPIDLY_SMALL) / (distsquared) + + # Calculate normal vector from pos to the gravity point and multiply by force magnitude to find force vector + dist = sqrt(distsquared) + dx = float(self.pos[0] - pos[0]) / dist + dy = float(self.pos[1] - pos[1]) / dist + + force = [forcemag * dx, forcemag * dy] + + return force + + def GetForceOnParticle(self, particle): + return self.GetForce(particle.pos) + + def GetMaxForce(self): + return self.strength * UNIVERSAL_CONSTANT_OF_MAKE_GRAVITY_LESS_STUPIDLY_SMALL + + def CreateKeyframe(self, frame, strength = None, strengthrandrange = None, pos = (None, None), interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'strength':strength, 'strengthrandrange':strengthrandrange, 'pos_x':pos[0], 'pos_y':pos[1], 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'pos_x':self.pos[0], 'pos_y':self.pos[1]}) + + def SetStrength(self, newstrength): + self.CreateKeyframe(self.curframe, strength = newstrength) + + def SetStrengthRandRange(self, newstrengthrandrange): + self.CreateKeyframe(self.curframe, strengthrandrange = newstrengthrandrange) + + def SetPos(self, newpos): + self.CreateKeyframe(self.curframe, pos = newpos) + + +class VortexGravity(PointGravity): + def __init__(self, strength = 0.0, strengthrandrange = 0.0, pos = (0, 0)): + PointGravity.__init__(self, strength, strengthrandrange, pos) + self.type = "vortex" + + self.CreateKeyframe(0, self.strength, self.strengthrandrange, self.pos) + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'pos_x':self.pos[0], 'pos_y':self.pos[1]}, self.keyframes) + self.initstrength = newvars['strength'] + self.strengthrandrange = newvars['strengthrandrange'] + self.pos = (newvars['pos_x'], newvars['pos_y']) + + if self.strengthrandrange != 0.0: + self.strength = RandomiseStrength(self.initstrength, self.strengthrandrange) + else: + self.strength = self.initstrength + + self.curframe = self.curframe + 1 + + def GetForce(self, pos): + try: + self.alreadyshownerror + except: + print("WARNING: VortexGravity relies upon particle velocities as well as positions, and so its \ +force can only be obtained using GetForceOnParticle([PyIgnition particle object]).") + self.alreadyshownerror = True + + return [0.0, 0.0] + + def GetForceOnParticle(self, particle): + # This uses F = m(v^2 / r) (the formula for centripetal force on an object moving in a circle) + # to determine what force should be applied to keep an object circling the gravity. A small extra + # force (self.strength * VORTEX_ACCELERATION) is added in order to accelerate objects inward as + # well, thus creating a spiralling effect. Note that unit mass is assumed throughout. + + distvector = [self.pos[0] - particle.pos[0], self.pos[1] - particle.pos[1]] # Vector from the particle to this gravity + try: + distmag = sqrt(float(distvector[0] ** 2) + float(distvector[1] ** 2)) # Distance from the particle to this gravity + except: + return [0.0, 0.0] # This prevents OverflowErrors + + if distmag == 0.0: + return [0.0, 0.0] + + if distmag <= VORTEX_SWALLOWDIST: + particle.alive = False + + normal = [float(distvector[0]) / distmag, float(distvector[1]) / distmag] # Normal from particle to this gravity + + velocitymagsquared = (particle.velocity[0] ** 2) + (particle.velocity[1] ** 2) # Velocity magnitude squared + forcemag = (velocitymagsquared / distmag) + (self.strength * VORTEX_ACCELERATION) # Force magnitude = (v^2 / r) + radial acceleration + + #velparmag = (particle.velocity[0] * normal[0]) + (particle.velocity[1] * normal[1]) # Magnitude of velocity parallel to normal + #velpar = [normal[0] * velparmag, normal[1] * velparmag] # Vector of velocity parallel to normal + #velperp = [particle.velocity[0] - velpar[0], particle.velocity[1] - velpar[1]] # Vector of velocity perpendicular to normal + # + #fnpar = [-velperp[1], velperp[0]] # Force normal parallel to normal + #fnperp = [velpar[1], -velpar[0]] # Force normal perpendicular to normal + # + #force = [(fnpar[0] + fnperp[0]) * forcemag, (fnpar[1] + fnperp[1]) * forcemag] + + force = [normal[0] * forcemag, normal[1] * forcemag] # Works, but sometimes goes straight to the gravity w/ little spiraling + + return force + + def CreateKeyframe(self, frame, strength = None, strengthrandrange = None, pos = (None, None), interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'strength':strength, 'strengthrandrange':strengthrandrange, 'pos_x':pos[0], 'pos_y':pos[1], 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'strength':self.initstrength, 'strengthrandrange':self.strengthrandrange, 'pos_x':self.pos[0], 'pos_y':self.pos[1]}) diff --git a/pyignition/interpolate.py b/pyignition/interpolate.py @@ -0,0 +1,77 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Utility module for interpolating between keyframed values + + +import math +from constants import * + + +def LinearInterpolate(val1, val2, t): + diff = val2 - val1 + dist = float(diff) * t + + return val1 + dist + +def CosineInterpolate(val1, val2, t): + amplitude = float(val2 - val1) / 2.0 + midpoint = float(val1 + val2) / 2.0 + + return (amplitude * math.cos(math.pi * (1.0 - t))) + midpoint + + +def LinearInterpolateKeyframes(curframe, key1, key2, val1, val2): + if key1 == key2: + return val2 + + factor = float(curframe - key1) / float(key2 - key1) + + return LinearInterpolate(val1, val2, factor) + +def CosineInterpolateKeyframes(curframe, key1, key2, val1, val2): + if key1 == key2: + return val2 + + factor = float(curframe - key1) / float(key2 - key1) + + return CosineInterpolate(val1, val2, factor) + + +def InterpolateKeyframes(curframe, variables, keyframes): + if len(keyframes) == 1: + return keyframes[0].variables + + finalvariables = {} + + if not ('interpolationtype' in variables): + variables['interpolationtype'] = INTERPOLATIONTYPE_LINEAR + + keys = list(variables.keys()) + + for i in range(len(keys)): # Determine current keyframe and next one for this variable + key = keys[i] + curkeyframe = None + nextkeyframe = None + + for i in range(len(keyframes)): + try: + frame = keyframes[i] + if (frame.variables[key] != None): # If the current keyframe has a keyed value for the current variable + if frame.frame <= curframe: # If its frame is below or equal to the current, it is the current keyframe + curkeyframe = i + if (nextkeyframe == None) and (frame.frame > curframe): # If this is the first keyframe with a frame higher than the current, it is the next keyframe + nextkeyframe = i + except KeyError: + pass + + if nextkeyframe == None or key == "interpolationtype": # If there is no next key frame, maintain the value specified by the current one + finalvariables[key] = keyframes[curkeyframe].variables[key] # (Also do this if it is an interpolation type variable; they should only change once their next keyframe has been reached + + else: # Interpolate between the current and next keyframes + if keyframes[nextkeyframe].variables['interpolationtype'] == INTERPOLATIONTYPE_LINEAR: + finalvariables[key] = LinearInterpolateKeyframes(curframe, keyframes[curkeyframe].frame, keyframes[nextkeyframe].frame, keyframes[curkeyframe].variables[key], keyframes[nextkeyframe].variables[key]) + elif keyframes[nextkeyframe].variables['interpolationtype'] == INTERPOLATIONTYPE_COSINE: + finalvariables[key] = CosineInterpolateKeyframes(curframe, keyframes[curkeyframe].frame, keyframes[nextkeyframe].frame, keyframes[curkeyframe].variables[key], keyframes[nextkeyframe].variables[key]) + + return finalvariables diff --git a/pyignition/keyframes.py b/pyignition/keyframes.py @@ -0,0 +1,61 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Keyframe object and generic keyframe creation function + + +from constants import * +import sys + +def CreateKeyframe(parentframes, frame, variables): + newframe = Keyframe(frame, variables) + + # Look for duplicate keyframes and copy other defined variables + try: + if sys.version_info.major == 2: + oldkey = (keyframe for keyframe in parentframes if keyframe.frame == frame).next() + else: + oldkey = (keyframe for keyframe in parentframes if keyframe.frame == frame).__next__() + except StopIteration: + oldkey = None + + if oldkey != None: + for var in oldkey.variables.keys(): # For every variable defined by the old keyframe + if (var not in newframe.variables or newframe.variables[var] == None) and (oldkey.variables[var] != None): # If a variable is undefined, copy its value to the new keyframe + newframe.variables[var] = oldkey.variables[var] + + # Remove the duplicate keyframe, if it existed + for duplicate in (keyframe for keyframe in parentframes if keyframe.frame == frame): + parentframes.remove(duplicate) + break + + # Append the new keyframe then sort them all by frame + parentframes.append(newframe) + sortedframes = sorted(parentframes, key=lambda keyframe: keyframe.frame) + parentframes[:] = sortedframes + + return newframe # Return a reference to the new keyframe, in case it's needed + +def ConsolidateKeyframes(parentframes, frame, variables): + newframe = Keyframe(frame, variables) + parentframes.append(newframe) + + finallist = [] # A truncated list of keyframes + + # Append all the frames which come after the current one to the final list + for keyframe in parentframes: + if keyframe.frame >= frame: + finallist.append(keyframe) + + # Sort the keyframes and give them to the parent object + sortedframes = sorted(finallist, key=lambda keyframe: keyframe.frame) + parentframes[:] = sortedframes + +class Keyframe: + def __init__(self, frame = 0, variables = {}): + self.frame = frame + self.variables = variables + + if not ('interpolationtype' in self.variables): + self.variables['interpolationtype'] = INTERPOLATIONTYPE_LINEAR + diff --git a/pyignition/obstacles.py b/pyignition/obstacles.py @@ -0,0 +1,404 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Obstacle objects + +import pygame +from math import sqrt, pow +from gravity import UNIVERSAL_CONSTANT_OF_MAKE_GRAVITY_LESS_STUPIDLY_SMALL +import keyframes, interpolate +from constants import * + + +MAXDIST = 20.0 + + +def dotproduct2d(v1, v2): + return ((v1[0] * v2[0]) + (v1[1] * v2[1])) + +def magnitude(vec): + try: + return sqrt(vec[0] ** 2 + vec[1] ** 2) + except: + return 1.0 + +def magnitudesquared(vec): + try: + return (vec[0] ** 2 + vec[1] ** 2) + except: + return 1.0 + +def normalise(vec): + mag = magnitude(vec) + return [vec[0] / mag, vec[1] / mag] + + +class Obstacle: + def __init__(self, parenteffect, pos, colour, bounce): + self.parenteffect = parenteffect + self.pos = pos + self.colour = colour + self.bounce = bounce + self.maxdist = MAXDIST # The maximum (square-based, not circle-based) distance away for which forces will still be calculated + self.curframe = 0 + self.keyframes = [] + + def Draw(self, display): + pass + + def Update(self): + self.curframe = self.curframe + 1 + + def OutOfRange(self, pos): + return (abs(pos[0] - self.pos[0]) > self.maxdist) or (abs(pos[1] - self.pos[1]) > self.maxdist) + + def InsideObject(self, pos, pradius): + pass + + def GetResolved(self, pos, pradius): # Get a resolved position for a particle located inside the object + pass + + def GetDist(self, pos): + return magnitude([pos[0] - self.pos[0], pos[1] - self.pos[1]]) + + def GetNormal(self, pos): # Gets the normal relevant to a particle at the supplied potision (for example, that of the appropriate side of a squre) + pass + + def GetForceFactor(self, pos, pradius): # Gets the force as a factor of maximum available force (0.0 - 1.0), determined by an inverse cube distance law + pass + + def GetForce(self, pos, velocity, pradius = 0.0): # Gets the final (vector) force + if self.OutOfRange(pos) or self.bounce == 0.0: + return [0.0, 0.0] + + if (pos[0] == self.pos[0]) and (pos[1] == self.pos[1]): + return [0.0, 0.0] + + normal = self.GetNormal(pos) + scalingfactor = -dotproduct2d(normal, velocity) # An integer between 0.0 and 1.0 used to ensure force is maximised for head-on collisions and minimised for scrapes + + if scalingfactor <= 0.0: # The force should never be attractive, so take any negative value of the scaling factor to be zero + return [0.0, 0.0] # A scaling factor of zero always results in zero force + + forcefactor = (self.GetForceFactor(pos, pradius)) + velmag = magnitude(velocity) # Magnitude of the velocity - multiplied in the force equation + + # Force = bounce factor * velocity * distance force factor (0.0 - 1.0) * angle force factor (0.0 - 1.0), along the direction of the normal pointing away from the obstacle + return [normal[0] * forcefactor * velmag * scalingfactor * self.bounce, normal[1] * forcefactor * velmag * scalingfactor * self.bounce] + + def CreateKeyframe(self): + pass + + def SetPos(self, newpos): + self.CreateKeyframe(self.curframe, pos = newpos) + + def SetColour(self, newcolour): + self.CreateKeyframe(self.curframe, colour = newcolour) + + def SetBounce(self, newbounce): + self.CreateKeyframe(self.curframe, bounce = newbounce) + + +class Circle(Obstacle): + def __init__(self, parenteffect, pos, colour, bounce, radius): + Obstacle.__init__(self, parenteffect, pos, colour, bounce) + self.type = "circle" + self.radius = radius + self.radiussquared = self.radius ** 2 + self.maxdist = MAXDIST + self.radius + self.CreateKeyframe(0, self.pos, self.colour, self.bounce, self.radius) + + def Draw(self, display): + offset = self.parenteffect.pos + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), int(self.radius)) + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'radius':self.radius}, self.keyframes) + self.pos = (newvars['pos_x'], newvars['pos_y']) + self.colour = (newvars['colour_r'], newvars['colour_g'], newvars['colour_b']) + self.bounce = newvars['bounce'] + self.radius = newvars['radius'] + + Obstacle.Update(self) + + def InsideObject(self, pos, pradius = 0.0): + mag = magnitude([pos[0] - self.pos[0], pos[1] - self.pos[1]]) + return (((mag - pradius) ** 2) < self.radiussquared) + + def GetResolved(self, pos, pradius = 0.0): + if pos == self.pos: # If the position is at this obstacle's origin, shift it up a pixel to avoid divide-by-zero errors + return self.GetResolved([pos[0], pos[1] - 1]) + + vec = [pos[0] - self.pos[0], pos[1] - self.pos[1]] + mag = magnitude(vec) + nor = [vec[0] / mag, vec[1] / mag] + + # Split the pradius into appropriate components by multiplying the normal vector by pradius + pradiusvec = [(nor[0] * pradius), (nor[1] * pradius)] + + correctedvec = [nor[0] * (self.radius), nor[1] * (self.radius)] + + return [self.pos[0] + correctedvec[0] + pradiusvec[0], self.pos[1] + correctedvec[1] + pradiusvec[1]] + + def GetNormal(self, pos): + vec = [pos[0] - self.pos[0], pos[1] - self.pos[1]] + mag = magnitude(vec) + + return [vec[0] / mag, vec[1] / mag] + + def GetForceFactor(self, pos, pradius = 0.0): + nvec = self.GetNormal(pos) + tempradius = self.radius + vec = [tempradius * nvec[0], tempradius * nvec[1]] + newpos = [pos[0] - vec[0], pos[1] - vec[1]] + + distcubed = (abs(pow(float(newpos[0] - self.pos[0]), 3.0)) + abs(pow(float(newpos[1] - self.pos[1]), 3.0))) - (pradius * pradius * pradius) + if distcubed <= 1.0: + return 1.0 + + force = (1.0 / distcubed) + + return force + + def CreateKeyframe(self, frame, pos = (None, None), colour = (None, None, None), bounce = None, radius = None, interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'pos_x':pos[0], 'pos_y':pos[1], 'colour_r':colour[0], 'colour_g':colour[1], 'colour_b':colour[2], 'bounce':bounce, 'radius':radius, 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'radius':self.radius}) + + def SetRadius(self, newradius): + self.CreateKeyframe(self.curframe, radius = newradius) + + +class Rectangle(Obstacle): + def __init__(self, parenteffect, pos, colour, bounce, width, height): + Obstacle.__init__(self, parenteffect, pos, colour, bounce) + self.type = "rectangle" + self.width = width + self.halfwidth = self.width / 2.0 + self.height = height + self.halfheight = height / 2.0 + self.maxdist = max(self.halfwidth, self.halfheight) + MAXDIST + self.CreateKeyframe(0, self.pos, self.colour, self.bounce, self.width, self.height) + + def Draw(self, display): + offset = self.parenteffect.pos + pygame.draw.rect(display, self.colour, pygame.Rect(offset[0] + (self.pos[0] - self.halfwidth), offset[1] + (self.pos[1] - self.halfheight), self.width, self.height)) + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'width':self.width, 'height':self.height}, self.keyframes) + self.pos = (newvars['pos_x'], newvars['pos_y']) + self.colour = (newvars['colour_r'], newvars['colour_g'], newvars['colour_b']) + self.bounce = newvars['bounce'] + self.width = newvars['width'] + self.halfwidth = self.width / 2.0 + self.height = newvars['height'] + self.halfheight = self.height / 2.0 + self.maxdist = max(self.halfwidth, self.halfheight) + MAXDIST + + Obstacle.Update(self) + + def InsideObject(self, pos, pradius = 0.0): + return (((pos[0] + pradius) > (self.pos[0] - self.halfwidth)) and ((pos[0] - pradius) < (self.pos[0] + self.halfwidth)) and ((pos[1] + pradius) > (self.pos[1] - self.halfheight)) and ((pos[1] - pradius) < self.pos[1] + self.halfheight)) + + def GetResolved(self, pos, pradius = 0.0): + if pos == self.pos: # If the position is at this obstacle's origin, shift it up a pixel to avoid divide-by-zero errors + return self.GetResolved([pos[0], pos[1] - 1]) + + # Where 'triangles' within the rectangle are referred to, imagine a rectangle with diagonals drawn between its vertices. The four triangles formed by this process are the ones referred to + if pos[0] == self.pos[0]: # If it's directly above the centre of the rectangle + if pos[1] > self.pos[1]: + return [pos[0], self.pos[1] + self.halfheight + pradius] + else: + return [pos[0], self.pos[1] - (self.halfheight + pradius)] + elif pos[1] == self.pos[1]: # If it's directly to one side of the centre of the rectangle + if pos[0] > self.pos[0]: + return [self.pos[0] + self.halfwidth + pradius, pos[1]] + else: + return [self.pos[0] - (self.halfwidth + pradius), pos[1]] + elif abs(float(pos[1] - self.pos[1]) / float(pos[0] - self.pos[0])) > (float(self.height) / float(self.width)): # If it's in the upper or lower triangle of the rectangle + return [pos[0], self.pos[1] + ((self.halfheight + pradius) * ((pos[1] - self.pos[1]) / abs(pos[1] - self.pos[1])))] # Halfheight is multiplied by a normalised version of (pos[1] - self.pos[1]) - thus if (pos[1] - self.pos[1]) is negative, it should be subtracted as the point is in the upper triangle + else: # If it's in the left or right triangle of the rectangle + return [self.pos[0] + ((self.halfwidth + pradius) * ((pos[0] - self.pos[0]) / abs(pos[0] - self.pos[0]))), pos[1]] + + def GetNormal(self, pos): + if pos[1] < (self.pos[1] - self.halfheight): + return [0, -1] + elif pos[1] > (self.pos[1] + self.halfheight): + return [0, 1] + elif pos[0] < (self.pos[0] - self.halfwidth): + return [-1, 0] + elif pos[0] > (self.pos[0] + self.halfwidth): + return [1, 0] + else: + vect = [pos[0] - self.pos[0], pos[1] - self.pos[1]] + mag = magnitude(vect) + return [vect[0] / mag, vect[1] / mag] + + def GetForceFactor(self, pos, pradius = 0.0): + nor = self.GetNormal(pos) + + if nor[0] == 0: + if (pos[0] > (self.pos[0] - self.halfwidth)) and (pos[0] < (self.pos[0] + self.halfwidth)): + r = (abs(pos[1] - self.pos[1]) - self.halfheight) - pradius + else: + return 0.0 + elif nor[1] == 0: + if (pos[1] > (self.pos[1] - self.halfheight)) and (pos[1] < (self.pos[1] + self.halfheight)): + r = (abs(pos[0] - self.pos[0]) - self.halfwidth) - pradius + else: + return 0.0 + else: + return 1.0 + + if r <= 1.0: + return 1.0 + + return (1.0 / pow(float(r), 3.0)) + + def CreateKeyframe(self, frame, pos = (None, None), colour = (None, None, None), bounce = None, width = None, height = None, interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'pos_x':pos[0], 'pos_y':pos[1], 'colour_r':colour[0], 'colour_g':colour[1], 'colour_b':colour[2], 'bounce':bounce, 'width':width, 'height':height, 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'width':self.width, 'height':self.height}) + + def SetWidth(self, newwidth): + self.CreateKeyframe(self.curframe, width = newwidth) + + def SetHeight(self, newheight): + self.CreateKeyframe(self.curframe, height = newheight) + + +class BoundaryLine(Obstacle): + def __init__(self, parenteffect, pos, colour, bounce, normal): + Obstacle.__init__(self, parenteffect, pos, colour, bounce) + self.type = "boundaryline" + self.normal = normalise(normal) + self.edgecontacts = [] + self.hascontacts = False + self.storedw, self.storedh = None, None + self.curframe = 0 + self.CreateKeyframe(0, self.pos, self.colour, self.bounce, self.normal) + + def Draw(self, display): + offset = self.parenteffect.pos + + W = display.get_width() + H = display.get_height() + + if (W != self.storedw) or (H != self.storedh): + self.hascontacts = False + + if not self.hascontacts: + self.storedw, self.storedh = W, H + + edgecontacts = [] # Where the line touches the screen edges + + if self.normal[0] == 0.0: + edgecontacts = [[0, self.pos[1]], [W, self.pos[1]]] + + elif self.normal[1] == 0.0: + edgecontacts = [[self.pos[0], 0], [self.pos[0], H]] + + else: + pdotn = (self.pos[0] * self.normal[0]) + (self.pos[1] * self.normal[1]) + reciprocaln0 = (1.0 / self.normal[0]) + reciprocaln1 = (1.0 / self.normal[1]) + + # Left-hand side of the screen + pointl = [0, 0] + pointl[1] = pdotn * reciprocaln1 + if (pointl[1] >= 0) and (pointl[1] <= H): + edgecontacts.append(pointl) + + # Top of the screen + pointt = [0, 0] + pointt[0] = pdotn * reciprocaln0 + if (pointt[0] >= 0) and (pointt[0] <= W): + edgecontacts.append(pointt) + + # Right-hand side of the screen + pointr = [W, 0] + pointr[1] = (pdotn - (W * self.normal[0])) * reciprocaln1 + if (pointr[1] >= 0) and (pointr[1] <= H): + edgecontacts.append(pointr) + + # Bottom of the screen + pointb = [0, H] + pointb[0] = (pdotn - (H * self.normal[1])) * reciprocaln0 + if (pointb[0] >= 0) and (pointb[0] <= W): + edgecontacts.append(pointb) + + self.edgecontacts = edgecontacts + self.hascontacts = True + + tempedgecontacts = [] + + for contact in self.edgecontacts: + tempedgecontacts.append([offset[0] + contact[0], offset[1] + contact[1]]) + + if len(tempedgecontacts) >= 2: + pygame.draw.aalines(display, self.colour, True, tempedgecontacts) + else: + pass # The line must be completely outwith the boundaries of the display + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'normal_x':self.normal[0], 'normal_y':self.normal[1]}, self.keyframes) + self.pos = (newvars['pos_x'], newvars['pos_y']) + self.colour = (newvars['colour_r'], newvars['colour_g'], newvars['colour_b']) + self.bounce = newvars['bounce'] + oldnormal = self.normal[:] + self.normal = [newvars['normal_x'], newvars['normal_y']] + if self.normal != oldnormal: + self.hascontacts = False + + Obstacle.Update(self) + + def OutOfRange(self, pos): + return (self.GetDist(pos) > MAXDIST) + + def InsideObject(self, pos, pradius = 0.0): + if pradius == 0.0: # If the particle has no radius, just test whether its centre position has crossed the line + return (((float(pos[0] - self.pos[0]) * self.normal[0]) + (float(pos[1] - self.pos[1]) * self.normal[1])) <= 0.0) + + radialnormal = [self.normal[0] * pradius, self.normal[1] * pradius] + leftside = [pos[0] + radialnormal[0], pos[1] + radialnormal[1]] + rightside = [pos[0] - radialnormal[0], pos[1] - radialnormal[1]] + + return ((((float(leftside[0] - self.pos[0]) * self.normal[0]) + (float(leftside[1] - self.pos[1]) * self.normal[1])) <= 0.0) + or (((float(rightside[0] - self.pos[0]) * self.normal[0]) + (float(rightside[1] - self.pos[1]) * self.normal[1])) <= 0.0)) + + def GetResolved(self, pos, pradius = 0.0): + if pos == self.pos: # If the position is at this obstacle's origin, shift it up a pixel to avoid divide-by-zero errors + return self.GetResolved([pos[0], pos[1] - 1]) + + dist = abs(self.GetDist(pos, pradius)) + vec = [dist * self.normal[0], dist * self.normal[1]] + + return [pos[0] + vec[0], pos[1] + vec[1]] + + def GetNormal(self, pos): + return self.normal + + def GetDist(self, pos, pradius = 0.0): + v = [float(pos[0] - self.pos[0]), float(pos[1] - self.pos[1])] + return (v[0] * self.normal[0]) + (v[1] * self.normal[1]) - pradius + + def GetForceFactor(self, pos, pradius = 0.0): + r = self.GetDist(pos) - pradius + + if r <= 1.0: + return 1.0 + + return (1.0 / pow(r, 3.0)) + + def CreateKeyframe(self, frame, pos = (None, None), colour = (None, None, None), bounce = None, normal = [None, None], interpolationtype = INTERPOLATIONTYPE_LINEAR): + if (normal != [None, None]) and (abs(magnitudesquared(normal) - 1.0) >= 0.3): + normal = normalise(normal) + return keyframes.CreateKeyframe(self.keyframes, frame, {'pos_x':pos[0], 'pos_y':pos[1], 'colour_r':colour[0], 'colour_g':colour[1], 'colour_b':colour[2], 'bounce':bounce, 'normal_x':normal[0], 'normal_y':normal[1], 'interpolationtype':interpolationtype}) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'colour_r':self.colour[0], 'colour_g':self.colour[1], 'colour_b':self.colour[2], 'bounce':self.bounce, 'normal_x':self.normal[0], 'normal_y':self.normal[1]}) + + def SetNormal(self, newnormal): + self.CreateKeyframe(self.curframe, normal = newnormal) diff --git a/pyignition/particles.py b/pyignition/particles.py @@ -0,0 +1,200 @@ +### EXESOFT PYIGNITION ### +# Copyright David Barker 2010 +# +# Particle and ParticleSource objects + + +import keyframes, interpolate, random, math, pygame +from constants import * + + +class Particle: + def __init__(self, parent, initpos, velocity, life, drawtype = DRAWTYPE_POINT, colour = (0, 0, 0), radius = 0.0, length = 0.0, image = None, keyframes = []): + self.parent = parent + self.pos = initpos + self.velocity = velocity + self.life = life + self.drawtype = drawtype + self.colour = colour + self.radius = radius + self.length = length + self.image = image + + self.keyframes = [] + self.keyframes.extend(keyframes[:]) + self.curframe = 0 + + self.alive = True + + def Update(self): + self.pos = [self.pos[0] + self.velocity[0], self.pos[1] + self.velocity[1]] + + if self.life != -1 and self.curframe > self.life: + self.alive = False + else: + if self.life == -1 and self.curframe >= len(self.parent.particlecache): # If the particle has infinite life and has passed the last cached frame + self.colour = (self.parent.particlecache[len(self.parent.particlecache) - 1]['colour_r'], self.parent.particlecache[len(self.parent.particlecache) - 1]['colour_g'], self.parent.particlecache[len(self.parent.particlecache) - 1]['colour_b']) + self.radius = self.parent.particlecache[len(self.parent.particlecache) - 1]['radius'] + self.length = self.parent.particlecache[len(self.parent.particlecache) - 1]['length'] + else: # Otherwise, get the values for the current frame + self.colour = (self.parent.particlecache[self.curframe]['colour_r'], self.parent.particlecache[self.curframe]['colour_g'], self.parent.particlecache[self.curframe]['colour_b']) + self.radius = self.parent.particlecache[self.curframe]['radius'] + self.length = self.parent.particlecache[self.curframe]['length'] + + self.curframe = self.curframe + 1 + + def Draw(self, display): + offset = self.parent.parenteffect.pos + + if (self.pos[0] > 10000) or (self.pos[1] > 10000) or (self.pos[0] < -10000) or (self.pos[1] < -10000): + return + + if self.drawtype == DRAWTYPE_POINT: # Point + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), 0) + + elif self.drawtype == DRAWTYPE_CIRCLE: # Circle + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), int(self.radius)) + + elif self.drawtype == DRAWTYPE_LINE: + if self.length == 0.0: + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), 0) + + else: + # Vector = (velocity / mag(velocity)) * length (a line of magnitude 'length' in + # direction of velocity); this is calculated as velocity / (mag(velocity) / length) + # so that parts consistent for both components in the final calculation are only calculated once + velocitymagoverlength = math.sqrt(self.velocity[0]**2 + self.velocity[1]**2) / self.length + + if velocitymagoverlength > 0.0: # Avoid division-by-zero errors by handling lines with zero velocity separately + linevec = [(self.velocity[0] / velocitymagoverlength), (self.velocity[1] / velocitymagoverlength)] + else: + linevec = [self.length, 0.0] # Draw a line pointing to the right + + endpoint = [offset[0] + int(self.pos[0] + linevec[0]), offset[1] + int(self.pos[1] + linevec[1])] + pygame.draw.aaline(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), endpoint) + + elif self.drawtype == DRAWTYPE_SCALELINE: # Scaling line (scales with velocity) + endpoint = [offset[0] + int(self.pos[0] + self.velocity[0]), offset[1] + int(self.pos[1] + self.velocity[1])] + pygame.draw.aaline(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), endpoint) + + elif self.drawtype == DRAWTYPE_BUBBLE: # Bubble + if self.radius >= 1.0: + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), int(self.radius), 1) + else: # Pygame won't draw circles with thickness < radius, so if radius is smaller than one don't bother trying to set thickness + pygame.draw.circle(display, self.colour, (offset[0] + int(self.pos[0]), offset[1] + int(self.pos[1])), int(self.radius)) + + elif self.drawtype == DRAWTYPE_IMAGE: # Image + size = self.image.get_size() + display.blit(self.image, (offset[0] + int(self.pos[0] - size[1]), offset[1] + int(self.pos[1] - size[1]))) + + def CreateKeyframe(self, frame, colour = (None, None, None), radius = None, length = None): + keyframes.CreateKeyframe(self.keyframes, frame, {'colour_r':colour[0], 'colour_g':colour[1], 'colour_b':colour[2], 'radius':radius, 'length':length}) + + +class ParticleSource: + def __init__(self, parenteffect, pos, initspeed, initdirection, initspeedrandrange, initdirectionrandrange, particlesperframe, particlelife, genspacing, drawtype = 0, colour = (0, 0, 0), radius = 0.0, length = 0.0, imagepath = None): + self.parenteffect = parenteffect + self.pos = pos + self.initspeed = initspeed + self.initdirection = initdirection + self.initspeedrandrange = initspeedrandrange + self.initdirectionrandrange = initdirectionrandrange + self.particlesperframe = particlesperframe + self.particlelife = particlelife + self.genspacing = genspacing + self.colour = colour + self.drawtype = drawtype + self.radius = radius + self.length = length + self.imagepath = imagepath + if self.imagepath == None: + self.image = None + else: + self.image = pygame.image.load(self.imagepath).convert_alpha() + self.drawtype = drawtype + + self.keyframes = [] + self.CreateKeyframe(0, self.pos, self.initspeed, self.initdirection, self.initspeedrandrange, self.initdirectionrandrange, self.particlesperframe, self.genspacing) + self.particlekeyframes = [] + self.particlecache = [] + self.CreateParticleKeyframe(0, colour = self.colour, radius = self.radius, length = self.length) + self.curframe = 0 + + def Update(self): + newvars = interpolate.InterpolateKeyframes(self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'initspeed':self.initspeed, 'initdirection':self.initdirection, 'initspeedrandrange':self.initspeedrandrange, 'initdirectionrandrange':self.initdirectionrandrange, 'particlesperframe':self.particlesperframe, 'genspacing':self.genspacing}, self.keyframes) + self.pos = (newvars['pos_x'], newvars['pos_y']) + self.initspeed = newvars['initspeed'] + self.initdirection = newvars['initdirection'] + self.initspeedrandrange = newvars['initspeedrandrange'] + self.initdirectionrandrange = newvars['initdirectionrandrange'] + self.particlesperframe = newvars['particlesperframe'] + self.genspacing = newvars['genspacing'] + + particlesperframe = self.particlesperframe + + if (self.genspacing == 0) or ((self.curframe % self.genspacing) == 0): + for i in range(0, int(particlesperframe)): + self.CreateParticle() + + self.curframe = self.curframe + 1 + + def CreateParticle(self): + if self.initspeedrandrange != 0.0: + speed = self.initspeed + (float(random.randrange(int(-self.initspeedrandrange * 100.0), int(self.initspeedrandrange * 100.0))) / 100.0) + else: + speed = self.initspeed + if self.initdirectionrandrange != 0.0: + direction = self.initdirection + (float(random.randrange(int(-self.initdirectionrandrange * 100.0), int(self.initdirectionrandrange * 100.0))) / 100.0) + else: + direction = self.initdirection + velocity = [speed * math.sin(direction), -speed * math.cos(direction)] + newparticle = Particle(self, initpos = self.pos, velocity = velocity, life = self.particlelife, drawtype = self.drawtype, colour = self.colour, radius = self.radius, length = self.length, image = self.image, keyframes = self.particlekeyframes) + self.parenteffect.AddParticle(newparticle) + + def CreateKeyframe(self, frame, pos = (None, None), initspeed = None, initdirection = None, initspeedrandrange = None, initdirectionrandrange = None, particlesperframe = None, genspacing = None, interpolationtype = INTERPOLATIONTYPE_LINEAR): + return keyframes.CreateKeyframe(self.keyframes, frame, {'pos_x':pos[0], 'pos_y':pos[1], 'initspeed':initspeed, 'initdirection':initdirection, 'initspeedrandrange':initspeedrandrange, 'initdirectionrandrange':initdirectionrandrange, 'particlesperframe':particlesperframe, 'genspacing':genspacing, 'interpolationtype':interpolationtype}) + + def CreateParticleKeyframe(self, frame, colour = (None, None, None), radius = None, length = None, interpolationtype = INTERPOLATIONTYPE_LINEAR): + newframe = keyframes.CreateKeyframe(self.particlekeyframes, frame, {'colour_r':colour[0], 'colour_g':colour[1], 'colour_b':colour[2], 'radius':radius, 'length':length, 'interpolationtype':interpolationtype}) + self.PreCalculateParticles() + return newframe + + def GetKeyframeValue(self, keyframe): + return keyframe.frame + + def PreCalculateParticles(self): + self.particlecache = [] # Clear the cache + + # If the particle has infinite life, interpolate for each frame up until its last keyframe + if self.particlelife == -1: + particlelife = max(self.particlekeyframes, key = self.GetKeyframeValue).frame + else: # Otherwise, interpolate the particle variables for each frame of its life + particlelife = self.particlelife + + for i in range(0, particlelife + 1): + vars = interpolate.InterpolateKeyframes(i, {'colour_r':0, 'colour_g':0, 'colour_b':0, 'radius':0, 'length':0}, self.particlekeyframes) + self.particlecache.append(vars) + + def ConsolidateKeyframes(self): + keyframes.ConsolidateKeyframes(self.keyframes, self.curframe, {'pos_x':self.pos[0], 'pos_y':self.pos[1], 'initspeed':self.initspeed, 'initdirection':self.initdirection, 'initspeedrandrange':self.initspeedrandrange, 'initdirectionrandrange':self.initdirectionrandrange, 'particlesperframe':self.particlesperframe, 'genspacing':self.genspacing}) + + def SetPos(self, newpos): + self.CreateKeyframe(self.curframe, pos = newpos) + + def SetInitSpeed(self, newinitspeed): + self.CreateKeyframe(self.curframe, initspeed = newinitspeed) + + def SetInitDirection(self, newinitdirection): + self.CreateKeyframe(self.curframe, initdirection = newinitdirection) + + def SetInitSpeedRandRange(self, newinitspeedrandrange): + self.CreateKeyframe(self.curframe, initspeedrandrange = newinitspeedrandrange) + + def SetInitDirectionRandRange(self, newinitdirectionrandrange): + self.CreateKeyframe(self.curframe, initdirectionrandrange = newinitdirectionrandrange) + + def SetParticlesPerFrame(self, newparticlesperframe): + self.CreateKeyframe(self.curframe, particlesperframe = newparticlesperframe) + + def SetGenSpacing(self, newgenspacing): + self.CreateKeyframe(self.curframe, genspacing = newgenspacing) diff --git a/pyignition/xml.py b/pyignition/xml.py @@ -0,0 +1,137 @@ +### EXESOFT XML PARSER ### +# Coopyright David Barker 2010 +# +# Python XML parser + + + +class XMLNode: + def __init__(self, parent, tag, meta, data, inside): + self.tag = tag + self.meta = meta + self.data = data + self.inside = inside + self.parent = parent + self.children = [] + self.parsed = False + + +class XMLParser: + def __init__(self, data): + self.data = data + self.meta = {} + self.root = None + + def ReadMeta(self): + while "<?" in self.data: + index = self.data.find("<?") # Start of tag + startindex = index + 2 # Start of tag inside + endindex = self.data.find("?>", index) # Tag end + + # Get the contents of the angular brackets and split into separate meta tags + metaraw = self.data[startindex:endindex].strip() + separated = metaraw.split("\" ") # Split like so ('|' = split off): + # thingy = "value|" |other = "whatever|" |third = "woo!" + + for splitraw in separated: + split = splitraw.split("=") + + # Add it to the dictionary of meta data + self.meta[split[0].strip()] = split[1].strip().strip('\"') + + # Remove this tag from the stored data + before = self.data[:index] + after = self.data[(endindex + 2):] + self.data = "".join([before, after]) + + def GetTagMeta(self, tag): + meta = {} + + metastart = tag.find(" ") + 1 + metaraw = tag[metastart:] + separated = metaraw.split("\" ") # Split like so ('|' = split off): + # thingy = "value|" |other = "whatever|" |third = "woo!" + + for splitraw in separated: + split = splitraw.split("=") + + # Add it to the dictionary of meta data + meta[split[0].strip()] = split[1].strip().strip('\"') + + return meta + + def StripXML(self): + # Remove comments + while "<!--" in self.data: + index = self.data.find("<!--") + endindex = self.data.find("-->", index) + before = self.data[:index] + after = self.data[(endindex + 3):] + self.data = "".join([before, after]) + + # Remove whitespace + self.data = self.data.replace("\n", "").replace("\t", "") + + def GetChildren(self, node): + pass + + def GetRoot(self): + rootstart = self.data.find("<") + rootstartclose = self.data.find(">", rootstart) + roottagraw = self.data[(rootstart + 1):rootstartclose] + + rootmeta = {} + if len(roottagraw.split("=")) > 1: + rootmeta = self.GetTagMeta(roottagraw) + + roottag = roottagraw.strip() + + rootend = self.data.find("</%s" % roottag) + rootendclose = self.data.find(">", rootend) + rootdata = self.data[rootstart:(rootendclose + 1)].strip() + rootinside = self.data[(rootstartclose + 1):rootend] + + self.root = XMLNode(parent = None, tag = roottag, meta = rootmeta, data = rootdata, inside = rootinside) + + def SearchNode(self, node): + node.parsed = True + + tempdata = node.inside + children = [] + + while "<" in tempdata: + start = tempdata.find("<") + startclose = tempdata.find(">", start) + tagraw = tempdata[(start + 1):startclose] + + meta = {} + if "=" in tagraw: + meta = self.GetTagMeta(tagraw) + + tag = tagraw.split(" ")[0] + + end = tempdata.find("</%s" % tag) + endclose = tempdata.find(">", end) + + data = tempdata[start:(endclose + 1)].strip() + inside = tempdata[(startclose + 1):end] + + newnode = XMLNode(node, tag, meta, data, inside) + children.append(newnode) + + before = tempdata[:start] + after = tempdata[(endclose + 1):] + tempdata = "".join([before, after]) + + node.children = children + + for child in node.children: + self.SearchNode(child) + + def Parse(self): + self.ReadMeta() + self.StripXML() + self.GetRoot() + self.SearchNode(self.root) + + return self.root