diff --git a/control b/control index e26b72e..55c9e74 100644 --- a/control +++ b/control @@ -7,3 +7,4 @@ Installed-size: 27000 Depends: bash, python, python-imaging, imagemagick, libzzip-0-13, sox, python-numpy, mplayer Maintainer: skycooler@gmail.com Description: Lightningbeam is an open-source animated content creation tool. + diff --git a/kt.py b/kt.py index 6036c9c..664d545 100644 --- a/kt.py +++ b/kt.py @@ -9,16 +9,18 @@ from kivy.graphics import Color, Ellipse, Line Builder.load_file("lightningbeam.kv") -class Lightningbeam(TabbedPanel): +class LightningbeamPanel(TabbedPanel): pass +class KivyCanvas(Widget): + def on_touch_down(self, touch): + print touch.button -class MyPaintApp(App): +class LightningbeamApp(App): def build(self): - return Lightningbeam() - + return LightningbeamPanel() if __name__ == '__main__': - MyPaintApp().run() \ No newline at end of file + LightningbeamApp().run() \ No newline at end of file diff --git a/lightningbeam.kv b/lightningbeam.kv index d786086..6322c4d 100644 --- a/lightningbeam.kv +++ b/lightningbeam.kv @@ -1,12 +1,16 @@ #:kivy 1.0 #:import ActionScriptLexer pygments.lexers.ActionScriptLexer -: +: + + +: do_default_tab: False TabbedPanelItem: text: 'Drawing' BoxLayout: orientation: "vertical" + KivyCanvas: TabbedPanelItem: text: 'Tools' @@ -25,7 +29,7 @@ Button: text: "Ellipse" Button: - text: "Painbrush" + text: "Paintbrush" TabbedPanelItem: text: 'ActionScript' CodeInput: diff --git a/lightningbeam.py b/lightningbeam.py index 98ad6e1..cc6a339 100755 --- a/lightningbeam.py +++ b/lightningbeam.py @@ -111,6 +111,7 @@ def onMouseDownGroup(self, x, y,button=1,clicks=1): if svlgui.MODE in [" ", "s"]: if self.hitTest(x, y): self.clicked = True + elif svlgui.MODE in ["r", "e", "p"]: if svlgui.MODE=="r": # 'c' stands for 'current' @@ -118,7 +119,26 @@ def onMouseDownGroup(self, x, y,button=1,clicks=1): elif svlgui.MODE=="e": self.cshape = ellipse(x, y, 0, 0) elif svlgui.MODE=="p": + for i in self.lines: + if abs(x-i.endpoint1.x)<10 and abs(y-i.endpoint1.y)<10: + x, y = i.endpoint1.x, i.endpoint1.y + break + elif abs(x-i.endpoint2.x)<10 and abs(i.endpoint2.y)<10: + x, y = i.endpoint2.x, i.endpoint2.y + break self.cshape = shape(x, y) + '''self.cshape = svlgui.Line(svlgui.Point(x, y),svlgui.Point(x,y)) + for i in self.lines: + if abs(self.cshape.endpoint1.x-i.endpoint1.x)<10 and abs(self.cshape.endpoint1.y-i.endpoint1.y)<10: + self.cshape.connection1 = i.endpoint1 + self.cshape.connection1.lines.add(self.cshape) + break + elif abs(self.cshape.endpoint1.x-i.endpoint2.x)<10 and abs(self.cshape.endpoint1.y-i.endpoint2.y)<10: + self.cshape.connection1 = i.endpoint2 + self.cshape.connection1.lines.add(self.cshape) + break + self.lines.append(self.cshape) + return''' #self.cshape.rotation = 5 self.cshape.initx,self.cshape.inity = x, y self.add(self.cshape) @@ -185,9 +205,50 @@ def onMouseUpGroup(self, x, y,button=1,clicks=1): undo_stack[-1] = undo_stack[-1].complete({"obj":cobj, "frame":self.activelayer.currentframe, "layer":self.activelayer}) clear(redo_stack) elif svlgui.MODE=="p": - print len(self.cshape.shapedata) + prelen = len(self.cshape.shapedata) self.cshape.shapedata = misc_funcs.simplify_shape(self.cshape.shapedata, svlgui.PMODE.split()[-1],1) - print len(self.cshape.shapedata) + postlen = len(self.cshape.shapedata) + print str((prelen-postlen)*100/prelen)+"% reduction: started at "+str(prelen)+" vertices, ended at "+str(postlen)+" vertices" + if svlgui.PMODE.split()[-1]=="straight": + lastline = None + x, y = self.cshape.x, self.cshape.y + for a, b in misc_funcs.pairwise(self.cshape.shapedata): + l = svlgui.Line(svlgui.Point(a[1]+x,a[2]+y),svlgui.Point(b[1]+x,b[2]+y)) + if lastline: + l.connection1 = lastline.endpoint2 + l.connection1.lines.add(l) + lastline = l + self.lines.append(l) + self.delete(self.activelayer.frames[self.currentframe].objs[-1]) + for line in self.lines: + for otherline in self.lines: + if not otherline is line: + if line.connection1 and otherline in line.connection1.lines: continue + if line.connection2 and otherline in line.connection2.lines: continue + inter = line.intersects(otherline) + if inter: + print "INTERSECTION" + inter = svlgui.Point(*inter) + l1 = svlgui.Line(line.endpoint1,inter,line.connection1,inter) + l2 = svlgui.Line(line.endpoint2,inter,line.connection2,inter) + l3 = svlgui.Line(otherline.endpoint1,inter,otherline.connection1,inter) + l4 = svlgui.Line(otherline.endpoint2,inter,otherline.connection2,inter) + inter.lines.add(l1) + inter.lines.add(l2) + inter.lines.add(l3) + inter.lines.add(l4) + self.lines[self.lines.index(line):self.lines.index(line)+1]=[l1,l2] + self.lines[self.lines.index(otherline):self.lines.index(otherline)+1]=[l3,l4] + break + '''for i in self.lines: + if abs(self.cshape.endpoint2.x-i.endpoint1.x)<10 and abs(self.cshape.endpoint2.y-i.endpoint1.y)<10: + self.cshape.connection2 = i.endpoint1 + self.cshape.connection2.lines.add(self.cshape) + break + elif abs(self.cshape.endpoint2.x-i.endpoint2.x)<10 and abs(self.cshape.endpoint2.y-i.endpoint2.y)<10: + self.cshape.connection2 = i.endpoint2 + self.cshape.connection2.lines.add(self.cshape) + break''' self.cshape = None MainWindow.stage.draw() def onMouseUpObj(self, x, y,button=1,clicks=1): @@ -247,6 +308,8 @@ def onMouseDragGroup(self, x, y,button=1,clicks=1): self.cshape.shapedata = [["M",x/2,0],["C",4*x/5,0,x,y/5,x,y/2],["C",x,4*y/5,4*x/5,y,x/2,y],["C",x/5,y,0,4*y/5,0,y/2],["C",0,y/5,x/5,0,x/2,0]] elif svlgui.MODE == "p": self.cshape.shapedata.append(["L",x-self.cshape.initx,y-self.cshape.inity]) + # self.cshape.endpoint2.x = x + # self.cshape.endpoint2.y = y def onMouseDragObj(self, x, y,button=1,clicks=1): if svlgui.MODE==" ": self.x = x-self.initx @@ -546,6 +609,8 @@ if svlgui.SYSTEM == "gtk": MainWindow = lightningbeam_windows.MainWindow() elif svlgui.SYSTEM=="osx": MainWindow = lightningbeam_windows.MainWindowOSX() +elif svlgui.SYSTEM=="kivy": + MainWindow = lightningbeam_windows.MainWindowKivy() elif svlgui.SYSTEM=="html": MainWindow = lightningbeam_windows.MainWindowHTML() elif svlgui.SYSTEM=="pyglet": diff --git a/lightningbeam_windows.py b/lightningbeam_windows.py index 63d79ae..a7f2a3a 100755 --- a/lightningbeam_windows.py +++ b/lightningbeam_windows.py @@ -9,6 +9,7 @@ import misc_funcs from misc_funcs import * class MainWindow: + ''' GTK UI. Not currently used. ''' def __init__(self): self.window = svlgui.Window("Lightningbeam") self.window.maximize() @@ -134,6 +135,7 @@ class MainWindow: class MainWindowAndroid: + ''' Android UI. Not currently used. Will be replaced with Kivy. ''' def __init__(self): class stagewrapper: def add(self, obj, x, y): @@ -231,7 +233,8 @@ class MainWindowOSX: # self.toolbox.buttons[1][0]._int().enabled = False self.toolbox.buttons[3][0]._int().enabled = False self.toolbox.buttons[4][0]._int().enabled = False - self.scriptwindow = svlgui.TextView(code=True) + # self.scriptwindow = svlgui.TextView(code=True) + self.scriptwindow = svlgui.TextView(code=False) self.paintgroup = svlgui.RadioGroup("Draw straight", "Draw smooth", "Draw as inked") def setmode(self): svlgui.PMODE = self.value @@ -435,7 +438,10 @@ class MainWindowHTML: [self.stage,self.toolbox._int(),self.scriptwindow._int(),self.timelinebox._int()+2,0,"nsew", "hv"] ) self.window.add(self.frame) - +class MainWindowKivy: + def __init__(self): + from kivy.lang import Builder + Builder.load_file("lightningbeam.kv") if __name__=="__main__": a = MainWindow() diff --git a/misc_funcs.py b/misc_funcs.py index 336f251..5cf511f 100644 --- a/misc_funcs.py +++ b/misc_funcs.py @@ -5,10 +5,12 @@ import svlgui from threading import Event, Thread +from itertools import tee, izip import math import subprocess import re import os +import sys def select_any(self): svlgui.MODE = " " @@ -96,8 +98,30 @@ def lastval(arr,index): return i +def angle_to_point(point1, point2): + deltaX = point2.x-point1.x + deltaY = point2.y-point1.y + angleInDegrees = math.atan2(-deltaY, deltaX) * 180 / math.pi + if angleInDegrees<0: angleInDegrees = 360+angleInDegrees + return angleInDegrees +def sqr(x) : + return x * x +def dist2(v, w): + return sqr(v.x - w.x) + sqr(v.y - w.y) +def distToSegmentSquared(p, v, w): + l2 = dist2(v, w) + if l2 == 0: + return dist2(p, v) + t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2 + if t < 0: + return dist2(p, v) + if t > 1: + return dist2(p, w) + return dist2(p, svlgui.Point(x=(v.x+t*(w.x-v.x)), y=(v.y+t*(w.y-v.y)))) +def distToSegment(p, v, w): + return math.sqrt(distToSegmentSquared(p, v, w)) def catmullRom2bezier( points ) : #crp = points.split(/[,\s]/); @@ -184,7 +208,6 @@ def simplify_shape(shape,mode,iterations): del shape[j] if mode=="smooth": shape = catmullRom2bezier([shape[0]]*2+shape+[shape[-1]]) - print shape return shape#+nshape @@ -237,3 +260,34 @@ class RepeatTimer(Thread): def cancel(self): self.finished.set() + + +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = tee(iterable) + next(b, None) + return izip(a, b) + +def hittest(linelist,x,y): + hits = False + def IsOnLeft(a, b, c): + return Area2(a, b, c) > 0 + def IsOnRight(a, b, c): + return Area2(a, b, c) < 0 + def IsCollinear(a, b, c): + return Area2(a, b, c) == 0 + # calculates the triangle's size (formed by the "anchor" segment and additional point) + def Area2(a, b, c): + return (b[0]-a[0])*(c[1]-a[1])-(c[0]-a[0])*(b[1]-a[1]) + def intersects(a,b,c,d): + return not (IsOnLeft(a,b,c) != IsOnRight(a,b,d)) + def ccw(a,b,c): + return (c[1]-a[1])*(b[0]-a[0]) > (b[1]-a[1])*(c[0]-a[0]) + def intersect(a,b,c,d): + return ccw(a,c,d) != ccw(b,c,d) and ccw(a,b,c) != ccw(a,b,d) + for i in xrange(len(linelist)): + hits = hits != intersect([linelist[i-1].endpoint1.x,linelist[i-1].endpoint1.y], + [linelist[i].endpoint1.x,linelist[i].endpoint1.y],[x,y],[x,sys.maxint]) + print hits, x, y + return hits + diff --git a/svlgui.py b/svlgui.py index 699786a..3364bc4 100644 --- a/svlgui.py +++ b/svlgui.py @@ -136,6 +136,8 @@ class Color (object): retval = "var "+self.val.split('/')[-1].replace(' ','_').replace('.','_')+" = new Image();\n" retval = retval+self.val.split('/')[-1].replace(' ','_').replace('.','_')+".src = \""+self.val.split("/")[-1]+"\";\n" return retval + def print_json(self): + return {'type':'Color','arguments':{'val':self.val}} def rgb2hex(r, g, b, a=1): r=hex(int(r*255)).split("x")[1].zfill(2) g=hex(int(g*255)).split("x")[1].zfill(2) @@ -176,6 +178,8 @@ LINECOLOR = Color("#990099") FILLCOLOR = Color("#00FF00") TEXTCOLOR = Color("#000000") +LINEWIDTH = 2 + #Magic. Detect platform and select appropriate toolkit. To be used throughout code. if sys.platform=="linux2": id = platform.machine() @@ -201,6 +205,7 @@ if sys.platform=="linux2": import pickle import tarfile import tempfile + import GUI # Using PyGUI. Experimental. from GUI import Window as OSXWindow, Button as OSXButton, Image as OSXImage from GUI import Frame as OSXFrame, Color as OSXColor, Grid as OSXGrid, CheckBox as OSXCheckBox @@ -218,7 +223,17 @@ if sys.platform=="linux2": from PIL import Image as PILImage except ImportError: import Image as PILImage + SYSTEM="osx" from GUI.Geometry import offset_rect, rect_sized + ''' + + from kivy.app import App # Using Kivy. Very experimental. + from kivy.uix.widget import Widget + from kivy.uix.codeinput import CodeInput + from kivy.uix.tabbedpanel import TabbedPanel + from kivy.uix.button import Button + from kivy.graphics import Color, Ellipse, Line + SYSTEM="kivy"''' #If we can import this, we are in the install directory. Mangle media paths accordingly. try: @@ -226,7 +241,6 @@ if sys.platform=="linux2": except: media_path = "" #app = GUI.application() - SYSTEM="osx" TEMPDIR = "/tmp" FONT = u'Times New Roman' ''' @@ -377,6 +391,31 @@ if SYSTEM=="osx": app = Lightningbeam() +elif SYSTEM=="kivy": + class Lightningbeam(App): + def build(self): + return LightningbeamPanel() + class LightningbeamPanel(TabbedPanel): + pass + class KivyCanvas(Widget): + def draw(self): + with self.canvas: + for i in self.objs: + try: + i.draw(None) + except: + traceback.print_exc() + def on_touch_down(self, touch): + x, y = touch.x, touch.y + try: + try: + for i in self.objs: + i._onMouseDown(x,y,button=touch.button, clicks=(3 if touch.is_triple_click else (2 if touch.is_double_click else 1))) + except ObjectDeletedError: + return + except: + traceback.print_exc() + self.draw() elif SYSTEM=="html": app = "" @@ -1151,6 +1190,9 @@ class Canvas(Widget): pass self.canvas = OSXCanvas(extent = (width, height), scrolling = 'hv') self.canvas.objs = self.objs + elif SYSTEM=="kivy": + + self.canvas = KivyCanvas() elif SYSTEM=="html": global ids while True: @@ -1183,6 +1225,8 @@ class Canvas(Widget): def draw(self): if SYSTEM=="gtk": self.expose_event(self.canvas, "draw_event", self.objs) + elif SYSTEM=="kivy": + self.canvas.draw() elif SYSTEM in ["osx", "android"]: self.canvas.invalidate_rect((0,0,self.canvas.extent[0],self.canvas.extent[1])) elif SYSTEM=="html": @@ -1190,6 +1234,8 @@ class Canvas(Widget): def is_focused(self): if SYSTEM=="osx": return self.canvas.is_target() + else: + return false def add(self, obj, x, y): obj.x = x obj.y = y @@ -1491,6 +1537,8 @@ class Image(object): pass def print_sc(self): return ".png "+self.name+" \""+self.path+"\"\n" + def print_json(self): + return {'type':'Image','arguments':{'image':self.image,'x':self.x,'y':self.y,'animated':self.animated,'canvas':None,'htiles':self.htiles,'vtiles':self.vtiles,'skipl':false}} class Shape (object): def __init__(self,x=0,y=0,rotation=0,fillcolor=None,linecolor=None): @@ -1636,6 +1684,10 @@ class Shape (object): else: cr.stroke() cr.grestore() + elif SYSTEM=="kivy": + Color(1, 1, 0) + d = 30. + Ellipse(pos=(self.x - d / 2, self.y - d / 2), size=(d, d)) elif SYSTEM=="html": tb = "" tb+="cr.save()\n" @@ -1766,6 +1818,13 @@ class Shape (object): retval += self.name+".fill = \""+self.fillcolor.rgb+"\";\n"+self.name+".line = \""+self.linecolor.rgb+"\";\n" retval += self.name+".filled = "+str(self.filled).lower()+";\n" return retval + def print_json(self): + return {'type':'Shape','arguments':{'x':self.x, + 'y':self.y, + 'rotation':self.rotation, + 'linecolor':self.linecolor.print_json(), + 'fillcolor':self.fillcolor.print_json()}, + 'properties':{'shapedata':self.shapedata}} class Text (object): def __getstate__(self): @@ -2359,6 +2418,7 @@ class Layer: print "#>>",i for j in self.frames[self.currentframe].objs: if j == i: + print "Deleting",j del self.currentFrame()[self.currentFrame().index(j)] def add_frame(self,populate): if self.activeframe>len(self.frames): @@ -2553,6 +2613,9 @@ class Group (object): self.tempgroup = None self.is_mc = False self.name = "g"+str(int(random.random()*10000))+str(SITER) + self.lines = [] + self.fills = [] + self.activepoint = None if "onload" in kwargs: kwargs["onload"](self) def draw(self,cr=None,transform=None,rect=None): @@ -2573,8 +2636,14 @@ class Group (object): cr.pencolor = Color([0,0,1]).pygui cr.stroke_rect([sorted([self.startx,self.cx])[0], sorted([self.starty,self.cy])[0], \ sorted([self.startx,self.cx])[1], sorted([self.starty,self.cy])[1]]) + for i in self.fills: + i.draw(cr, rect=rect) + for i in self.lines: + i.draw(cr, rect=rect) def add(self, *args): self.activelayer.add(*args) + def delete(self, *args): + self.activelayer.delete(*args) def add_frame(self, populate): self.activelayer.add_frame(populate) def add_layer(self, index): @@ -2644,6 +2713,147 @@ class Group (object): self.tempgroup = None self.activelayer.currentselect = None self.startx, self.starty = x, y + if MODE in " s": + for i in self.lines: + if abs(x-i.endpoint1.x)<10 and abs(y-i.endpoint1.y)<10: + i.endpoint1.x = x + i.endpoint1.y = y + self.activepoint = i.endpoint1 + return + elif abs(x-i.endpoint2.x)<10 and abs(y-i.endpoint2.y)<10: + i.endpoint2.x = x + i.endpoint2.y = y + self.activepoint = i.endpoint2 + return + elif MODE=="b": + nlines = [i for i in self.lines] + # First, remove all line segments that have at least one free endpoit, not coincident with any other segment. + # Do that repeatedly until no such segment remains. + for i in reversed(nlines): + if not (i.endpoint1 in [j.endpoint1 for j in nlines if not j==i]+[j.endpoint2 for j in nlines if not j==i]): + nlines.remove(i) + elif not (i.endpoint2 in [j.endpoint1 for j in nlines if not j==i]+[j.endpoint2 for j in nlines if not j==i]): + nlines.remove(i) + + # Find the closest segment to the point. + if nlines: + mindist = sys.maxint + point = Point(x, y) + closestsegment = None + for i in nlines: + d = misc_funcs.distToSegment(point,i.endpoint1,i.endpoint2) + if d4 or abs(self.starty-y)>4: + # this should make a temporary group containing all the objects within the area we dragged objs = [] for i in reversed(self.currentFrame()): if self.startx" + + +class Line(object): + """Use Lines to build Shapes while allowing paintbucketing.""" + def __init__(self, endpoint1, endpoint2, connection1 = None, connection2 = None): + super(Line, self).__init__() + self.endpoint1 = endpoint1 + self.endpoint2 = endpoint2 + self.endpoint1.lines.add(self) + self.endpoint2.lines.add(self) + self.connection1 = connection1 + self.connection2 = connection2 + if self.connection1: self.connection1.lines.add(self) + if self.connection2: self.connection2.lines.add(self) + self.linecolor = LINECOLOR + self.linewidth = LINEWIDTH + def __repr__(self): + return "" + def assign(self, point, which): + if which==1: + self.connection1 = point + self.connection1.lines.add(self) + elif which==2: + self.connection2 = point + self.connection2.lines.add(self) + def angle(self, other): + if self.endpoint1==other.endpoint1: + x1 = self.endpoint2.x-self.endpoint1.x + y1 = self.endpoint2.y-self.endpoint1.y + x2 = other.endpoint2.x-other.endpoint1.x + y2 = other.endpoint2.y-other.endpoint1.y + elif self.endpoint2==other.endpoint1: + x1 = self.endpoint1.x-self.endpoint2.x + y1 = self.endpoint1.y-self.endpoint2.y + x2 = other.endpoint2.x-other.endpoint1.x + y2 = other.endpoint2.y-other.endpoint1.y + elif self.endpoint1==other.endpoint2: + x1 = self.endpoint2.x-self.endpoint1.x + y1 = self.endpoint2.y-self.endpoint1.y + x2 = other.endpoint1.x-other.endpoint2.x + y2 = other.endpoint1.y-other.endpoint2.y + elif self.endpoint2==other.endpoint2: + x1 = self.endpoint1.x-self.endpoint2.x + y1 = self.endpoint1.y-self.endpoint2.y + x2 = other.endpoint1.x-other.endpoint2.x + y2 = other.endpoint1.y-other.endpoint2.y + dot = x1*x2+y1*y2 + mag1 = math.sqrt(x1**2+y1**2) + mag2 = math.sqrt(x2**2+y2**2) + angle = math.acos(dot/(mag1*mag2))/math.pi*180 + cz = x1*y2-y1*x2 + if cz>0: angle=360-angle + return angle + def intersects(self,other): + '''def IsOnLeft(a,b,c): + return Area2(a,b,c) > 0 + def IsOnRight(a,b,c): + return Area2(a,b,c) < 0 + def IsCollinear(a,b,c): + return Area2(a,b,c) == 0 + def Area2 (a,b,c): + return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y) + if (IsOnLeft(self.endpoint1,self.endpoint2,other.endpoint1) and IsOnRight(self.endpoint1,self.endpoint2,other.endpoint2)) + or (IsOnLeft(self.endpoint1,self.endpoint2,other.endpoint2) and IsOnRight(self.endpoint1,self.endpoint2,other.endpoint1): + if (IsOnLeft(other.endpoint1,other.endpoint2,self.endpoint1) and IsOnRight(other.endpoint1,other.endpoint2,self.endpoint2)) + or (IsOnLeft(other.endpoint1,other.endpoint2,self.endpoint2) and IsOnRight(other.endpoint1,other.endpoint2,self.endpoint1): + return True''' + # Formula for line is y = mx + b + try: + sm = (self.endpoint1.y-self.endpoint2.y)/(self.endpoint1.x-self.endpoint1.y) + om = (other.endpoint1.y-other.endpoint2.y)/(other.endpoint1.x-other.endpoint1.y) + sb = self.endpoint1.y-sm*self.endpoint1.x + ob = other.endpoint1.y-sm*other.endpoint1.x + if sm == om: return False + x = (ob-sb)/(sm-om) + y = sm*x + sb + if min(self.endpoint1.x,self.endpoint2.x)