Changeset - 512381de45bd
[Not reviewed]
default
0 9 0
Drew Perttula - 11 years ago 2014-06-10 08:48:34
drewp@bigasterisk.com
effectclasss in subserver. multiline code suppport (except for evaulation). add some old effect funcs to the new evaluator
Ignore-this: c9bb7359c851bc7c43fb48c50bf41d79
9 files changed with 180 insertions and 105 deletions:
0 comments (0 inline, 0 general)
bin/effecteval
Show inline comments
 
#!bin/python
 
from __future__ import division
 
from run_local import log
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks, returnValue
 
import cyclone.web, cyclone.websocket, cyclone.httpclient
 
import sys, optparse, logging, subprocess, json, time, traceback
 
import sys, optparse, logging, subprocess, json, time, traceback, itertools
 
from rdflib import URIRef, Literal
 

	
 
sys.path.append(".")
 
from light9 import networking, showconfig, Submaster, dmxclient
 
from light9.curvecalc.curve import CurveResource
 
from light9.effecteval.effect import EffectNode
 
@@ -36,30 +36,45 @@ class SongEffects(PrettyErrorHandler, cy
 
    def post(self):
 
        song = URIRef(self.get_argument('uri'))
 
        dropped = URIRef(self.get_argument('drop'))
 
        ctx = song
 
        graph = self.settings.graph
 
        effect = graph.sequentialUri(song + "/effect-")
 
        curve = graph.sequentialUri(song + "/curve-")
 
        quads = [
 
            (song, L9['effect'], effect, ctx),
 
            (effect, RDF.type, L9['Effect'], ctx),
 
        ]
 

	
 
        with graph.currentState(
 
                tripleFilter=(dropped, RDFS.label, None)) as g:
 
            droppedSubLabel = g.label(dropped)
 
                tripleFilter=(dropped, None, None)) as g:
 
            droppedTypes = g.objects(dropped, RDF.type)
 
            droppedLabel = g.label(dropped)
 
            droppedCodes = g.objects(dropped, L9['code'])
 

	
 
        if L9['EffectClass'] in droppedTypes:
 
            quads.extend([
 
                (effect, RDFS.label, droppedLabel, ctx),
 
                (effect, RDF.type, dropped, ctx),
 
                ] + [(effect, L9['code'], c, ctx) for c in droppedCodes])
 
        elif L9['Curve'] in droppedTypes:
 
            curve = graph.sequentialUri(song + "/curve-")
 
            cr = CurveResource(graph, curve)
 
            cr.newCurve(ctx, label=Literal('sub %s' % droppedLabel))
 
            cr.saveCurve()
 
            quads.extend([
 
                (song, L9['curve'], curve, ctx),                
 
                (effect, L9['code'],
 
                 Literal('out = %s * %s' % (dropped.n3(), curve.n3())),
 
                 ctx),
 
                ])
 
        else:
 
            raise NotImplementedError(
 
                "don't know how to add an effect from %r (types=%r)" %
 
                (dropped, droppedTypes))
 
            
 
        cr = CurveResource(graph, curve)
 
        cr.newCurve(ctx, label=Literal('sub %s' % droppedSubLabel))
 
        cr.saveCurve()
 
        graph.patch(Patch(addQuads=[
 
            (song, L9['curve'], curve, ctx),
 
            (song, L9['effect'], effect, ctx),
 
            (effect, RDF.type, L9['Effect'], ctx),
 
            (effect, L9['code'],
 
             Literal('out = %s * %s' % (dropped.n3(), curve.n3())),
 
             ctx),
 
            ]))
 
        
 
        graph.patch(Patch(addQuads=quads))
 
        
 
class SongEffectsUpdates(cyclone.websocket.WebSocketHandler):
 
    def connectionMade(self, *args, **kwargs):
 
        self.graph = self.settings.graph
 
        self.graph.addHandler(self.updateClient)
 
        
 
@@ -86,33 +101,57 @@ class EffectUpdates(cyclone.websocket.We
 
        self.graph = self.settings.graph
 
        self.graph.addHandler(self.updateClient)
 

	
 
    def updateClient(self):
 
        # todo: if client has dropped, abort and don't get any more
 
        # graph updates
 
        self.sendMessage({'code': self.graph.value(self.uri, L9['code'])})
 
        self.sendMessage({'codeLines':
 
                          list(self.graph.objects(self.uri, L9['code']))})
 
        
 
    def connectionLost(self, reason):
 
        log.info("websocket closed")
 

	
 
    def messageReceived(self, message):
 
        log.info("got message %s" % message)
 
        # write a patch back to the graph
 

	
 
def replaceObjects(graph, c, s, p, newObjs):
 
    patch = graph.getObjectPatch(
 
        context=c,
 
        subject=s,
 
        predicate=p,
 
        newObject=newObjs[0])
 

	
 
    moreAdds = []
 
    for line in newObjs[1:]:
 
        moreAdds.append((s, p, line, c))
 
    fullPatch = Patch(delQuads=patch.delQuads,
 
                      addQuads=patch.addQuads + moreAdds)
 
    graph.patch(fullPatch)
 

	
 
        
 
class Code(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    def put(self):
 
        effect = URIRef(self.get_argument('uri'))
 
        code = Literal(self.get_argument('code'))
 
        codeLines = []
 
        for i in itertools.count(0):
 
            k = 'codeLines[%s][text]' % i
 
            v = self.get_argument(k, None)
 
            if v is not None:
 
                codeLines.append(Literal(v))
 
            else:
 
                break
 
        if not codeLines:
 
            log.info("no codelines recevied on PUT /code")
 
            return
 
        with self.settings.graph.currentState(
 
                tripleFilter=(None, L9['effect'], effect)) as g:
 
            song = g.subjects(L9['effect'], effect).next()
 
        self.settings.graph.patchObject(
 
            context=song,
 
            subject=effect,
 
            predicate=L9['code'],
 
            newObject=code)
 
            
 
        replaceObjects(self.settings.graph, song, effect, L9['code'], codeLines)
 
            
 
        # right here we could tell if the code has a python error and return it
 
        self.send_error(202)
 
        
 
class EffectEval(PrettyErrorHandler, cyclone.web.RequestHandler):
 
    @inlineCallbacks
 
    def get(self):
light9/Effects.py
Show inline comments
 
from __future__ import division
 
from random import Random
 
import random as random_mod
 
import math
 
import logging, colorsys
 
import light9.Submaster as Submaster
 
from chase import chase as chase_logic
 
import showconfig
 
from rdflib import RDF
 
from light9 import Patch
 
from light9.namespaces import L9
 
log = logging.getLogger()
 

	
 
registered = []
 
def register(f):
 
    registered.append(f)
 
    return f
 

	
 
@register
 
def chase(t, ontime=0.5, offset=0.2, onval=1.0, 
 
          offval=0.0, names=None, combiner=max, random=False):
 
    """names is list of URIs. returns a submaster that chases through
 
    the inputs"""
 
    if random:
 
        r = Random(random)
 
        r = random_mod.Random(random)
 
        names = names[:]
 
        r.shuffle(names)
 

	
 
    chase_vals = chase_logic(t, ontime, offset, onval, offval, names, combiner)
 
    lev = {}
 
    for uri, value in chase_vals.items():
 
@@ -28,23 +35,25 @@ def chase(t, ontime=0.5, offset=0.2, onv
 
                   uri))
 
            continue
 
        lev[dmx] = value
 

	
 
    return Submaster.Submaster(name="chase" ,levels=lev)
 

	
 
@register
 
def hsv(h, s, v, light='all', centerScale=.5):
 
    r,g,b = colorsys.hsv_to_rgb(h % 1.0, s, v)
 
    lev = {}
 
    if light in ['left', 'all']:
 
        lev[73], lev[74], lev[75] = r,g,b
 
    if light in ['right', 'all']:
 
        lev[80], lev[81], lev[82] = r,g,b
 
    if light in ['center', 'all']:
 
        lev[88], lev[89], lev[90] = r*centerScale,g*centerScale,b*centerScale
 
    return Submaster.Submaster(name='hsv', levels=lev)
 
    
 

	
 
@register
 
def stack(t, names=None, fade=0):
 
    """names is list of URIs. returns a submaster that stacks the the inputs
 

	
 
    fade=0 makes steps, fade=1 means each one gets its full fraction
 
    of the time to fade in. Fades never...
 
    """
 
@@ -62,20 +71,52 @@ def stack(t, names=None, fade=0):
 
            lev[dmx] = 1
 
        else:
 
            break
 
    
 
    return Submaster.Submaster(name="stack", levels=lev)
 

	
 
@register
 
def smoove(x):
 
    return -2 * (x ** 3) + 3 * (x ** 2)
 
    
 
def configExprGlobals():
 
    graph = showconfig.getGraph()
 
    ret = {}
 

	
 
    for chaseUri in graph.subjects(RDF.type, L9['Chase']):
 
        shortName = chaseUri.rsplit('/')[-1]
 
        chans = graph.value(chaseUri, L9['channels'])
 
        ret[shortName] = list(graph.items(chans))
 
        print "%r is a chase" % shortName
 

	
 
    ret['chase'] = chase
 
    ret['stack'] = stack
 
    ret['hsv'] = hsv
 
    for f in registered:
 
        ret[f.__name__] = f
 

	
 
    ret['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
 
    ret['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
 

	
 
    _smooth_random_items = [random_mod.random() for x in range(100)]
 

	
 
    # suffix '2' to keep backcompat with the versions that magically knew time
 
    def smooth_random2(t, speed=1):
 
        """1 = new stuff each second, <1 is slower, fade-ier"""
 
        x = (t * speed) % len(_smooth_random_items)
 
        x1 = int(x)
 
        x2 = (int(x) + 1) % len(_smooth_random_items)
 
        y1 = _smooth_random_items[x1]
 
        y2 = _smooth_random_items[x2]
 
        return y1 + (y2 - y1) * ((x - x1))
 

	
 
    def notch_random2(t, speed=1):
 
        """1 = new stuff each second, <1 is slower, notch-ier"""
 
        x = (t * speed) % len(_smooth_random_items)
 
        x1 = int(x)
 
        y1 = _smooth_random_items[x1]
 
        return y1
 

	
 
    ret['noise2'] = smooth_random2
 
    ret['notch2'] = notch_random2
 

	
 

	
 

	
 
    
 
    return ret
light9/curvecalc/subterm.py
Show inline comments
 
@@ -10,66 +10,42 @@ log = logging.getLogger()
 

	
 
class Expr(object):
 
    """singleton, provides functions for use in subterm expressions,
 
    e.g. chases"""
 
    def __init__(self):
 
        self.effectGlobals = light9.Effects.configExprGlobals()
 
        self._smooth_random_items = [random.random() for x in range(100)]
 
    
 
    def exprGlobals(self, startDict, t):
 
        """globals dict for use by expressions"""
 

	
 
        glo = startDict.copy()
 
        
 
        # add in functions from Effects
 
        glo.update(self.effectGlobals)
 

	
 
        glo['nsin'] = lambda x: (math.sin(x * (2 * math.pi)) + 1) / 2
 
        glo['ncos'] = lambda x: (math.cos(x * (2 * math.pi)) + 1) / 2
 
        def chan(name):
 
            return Submaster.Submaster(
 
                name=name,
 
                levels={get_dmx_channel(name) : 1.0})
 
        glo['chan'] = chan
 
        glo['within'] = lambda a, b: a < t < b
 
        glo['bef'] = lambda x: t < x
 

	
 

	
 
        def smoove(x):
 
            return -2 * (x ** 3) + 3 * (x ** 2)
 
        glo['smoove'] = smoove
 

	
 
        def aft(t, x, smooth=0):
 
            left = x - smooth / 2
 
            right = x + smooth / 2
 
            if left < t < right:
 
                return smoove((t - left) / (right - left))
 
                return light9.Effects.smoove((t - left) / (right - left))
 
            return t > x
 
        glo['aft'] = lambda x, smooth=0: aft(t, x, smooth)
 

	
 
        def chan(name):
 
            return Submaster.Submaster(
 
                name=name,
 
                levels={get_dmx_channel(name) : 1.0})
 
        glo['chan'] = chan
 

	
 
        def smooth_random(speed=1):
 
            """1 = new stuff each second, <1 is slower, fade-ier"""
 
            x = (t * speed) % len(self._smooth_random_items)
 
            x1 = int(x)
 
            x2 = (int(x) + 1) % len(self._smooth_random_items)
 
            y1 = self._smooth_random_items[x1]
 
            y2 = self._smooth_random_items[x2]
 
            return y1 + (y2 - y1) * ((x - x1))
 

	
 
        def notch_random(speed=1):
 
            """1 = new stuff each second, <1 is slower, notch-ier"""
 
            x = (t * speed) % len(self._smooth_random_items)
 
            x1 = int(x)
 
            y1 = self._smooth_random_items[x1]
 
            return y1
 
            
 
        glo['noise'] = smooth_random
 
        glo['notch'] = notch_random
 

	
 
        glo['smooth_random'] = lambda speed=1: glo['smooth_random2'](t, speed)
 
        glo['notch_random'] = lambda speed=1: glo['notch_random2'](t, speed)
 
        
 
        glo['noise'] = glo['smooth_random']
 
        glo['notch'] = glo['notch_random']
 

	
 
        return glo
 

	
 
exprglo = Expr()
 
        
 
class Subterm(object):
light9/effecteval/effect.coffee
Show inline comments
 
qs = new QueryString()
 
model =
 
  uri: ko.observable(qs.value('uri'))
 
  code: ko.observable()
 
  toSave: 
 
    uri: ko.observable(qs.value('uri'))
 
    codeLines: ko.observableArray([])
 
  
 
socket = reconnectingWebSocket "ws://localhost:8070/effectUpdates" + window.location.search, (msg) ->
 
  console.log('effectData ' + JSON.stringify(msg))
 
  # there's a shorter unpack thing
 
    
 
  model.code(msg.code)
 
  
 
writeBack = ko.computed ->
 
  model.toSave.codeLines(msg.codeLines.map((x) -> {text: ko.observable(x)})) if msg.codeLines?
 

	
 
model.saveCode = ->
 
  $.ajax
 
    type: 'PUT'
 
    url: 'code'
 
    data: {uri: model.uri(), code: model.code()}
 
    data: ko.toJS(model.toSave)
 

	
 
writeBack = ko.computed(model.saveCode)
 
  
 
ko.applyBindings(model)
 
  
 
\ No newline at end of file
light9/effecteval/effect.html
Show inline comments
 
@@ -4,15 +4,20 @@
 
    <title>effect</title>
 
    <meta charset="utf-8" />
 
    <link rel="stylesheet" href="static/style.css">
 

	
 
  </head>
 
  <body>
 
    <a href="./">Effects</a> / <a class="effect" data-bind="attr: {href: uri}, text: uri"></a>
 
    <a href="./">Effects</a> / <a class="effect" data-bind="attr: {href: toSave.uri}, text: toSave.uri"></a>
 

	
 
    <div>code: <input type="text" size="160" data-bind="value: code"></input></div>
 
    <div data-bind="foreach: toSave.codeLines">
 
      <div>
 
        code:
 
        <input type="text" size="160" data-bind="value: text"></input>
 
      </div>
 
    </div>
 
    
 
    <script src="static/jquery-2.1.1.min.js"></script>
 
    <script src="static/knockout-3.1.0.js"></script>
 
    <script src="static/websocket.js"></script>
 
    <script src="static/QueryString.js"></script>
 
    <script src="effect.js"></script>
light9/effecteval/effect.py
Show inline comments
 
import re, logging
 
from rdflib import URIRef
 
from light9.namespaces import L9, RDF
 
from light9.curvecalc.curve import CurveResource
 
from light9 import Submaster
 
from light9 import Submaster, Effects
 
log = logging.getLogger('effect')
 

	
 
def uriFromCode(s):
 
    # i thought this was something a graph could do with its namespace manager
 
    if s.startswith('sub:'):
 
        return URIRef('http://light9.bigasterisk.com/show/dance2014/sub/' + s[4:])
 
@@ -20,12 +20,13 @@ def uriFromCode(s):
 
class CodeLine(object):
 
    """code string is immutable"""
 
    def __init__(self, graph, code):
 
        self.graph, self.code = graph, code
 

	
 
        self.outName, self.expr, self.resources = self._asPython()
 
        self.pyResources = self._resourcesAsPython(self.resources)
 

	
 
    def _asPython(self):
 
        """
 
        out = sub(<uri1>, intensity=<curveuri2>)
 
        becomes
 
          'out',
 
@@ -52,61 +53,61 @@ class CodeLine(object):
 
        return lname, outExpr, resources
 

	
 
    def _uriIsCurve(self, uri):
 
        # this result could vary with graph changes (rare)
 
        return self.graph.contains((uri, RDF.type, L9['Curve']))
 
        
 
class EffectNode(object):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    def prepare(self):
 
        log.info("prepare effect %s", self.uri)
 
        # maybe there can be multiple lines of code as multiple
 
        # objects here, and we sort them by dependencies
 
        codeStr = self.graph.value(self.uri, L9['code'])
 
        if codeStr is None:
 
            raise ValueError("effect %s has no code" % self.uri)
 

	
 
        self.code = CodeLine(self.graph, codeStr)
 

	
 
        self.resourceMap = self.resourcesAsPython()
 
        
 
    def resourcesAsPython(self):
 
    def _resourcesAsPython(self, resources):
 
        """
 
        mapping of the local names for uris in the code to high-level
 
        objects (Submaster, Curve)
 
        """
 
        out = {}
 
        subs = Submaster.get_global_submasters(self.graph)
 
        for localVar, uri in self.code.resources.items():
 
        for localVar, uri in resources.items():
 
            for rdfClass in self.graph.objects(uri, RDF.type):
 
                if rdfClass == L9['Curve']:
 
                    cr = CurveResource(self.graph, uri)
 
                    cr.loadCurve()
 
                    out[localVar] = cr.curve
 
                elif rdfClass == L9['Submaster']:
 
                    out[localVar] = subs.get_sub_by_uri(uri)
 
                else:
 
                    out[localVar] = uri
 

	
 
        return out
 
        
 

	
 
class EffectNode(object):
 
    def __init__(self, graph, uri):
 
        self.graph, self.uri = graph, uri
 
        # this is not expiring at the right time, when an effect goes away
 
        self.graph.addHandler(self.prepare)
 

	
 
    def prepare(self):
 
        log.info("prepare effect %s", self.uri)
 
        # maybe there can be multiple lines of code as multiple
 
        # objects here, and we sort them by dependencies
 
        codeStrs = list(self.graph.objects(self.uri, L9['code']))
 
        if not codeStrs:
 
            raise ValueError("effect %s has no code" % self.uri)
 

	
 
        self.codes = [CodeLine(self.graph, s) for s in codeStrs]
 

	
 
        self.otherFuncs = Effects.configExprGlobals()
 
                
 
    def eval(self, songTime):
 
        ns = {'t': songTime}
 
        ns.update(self.otherFuncs)
 

	
 
        ns.update(dict(
 
            curve=lambda c, t: c.eval(t),
 
            ))
 
        # loop over lines in order, merging in outputs 
 
        # merge in named outputs from previous lines
 
        
 
        ns.update(self.resourceMap)
 
        return eval(self.code.expr, ns)
 

	
 

	
 
class GraphAwareFunction(object):
 
    def __init__(self, graph):
 
        self.graph = graph
 
        for c in self.codes:
 
            codeNs = ns.copy()
 
            codeNs.update(c.pyResources)
 
            if c.outName == 'out':
 
                out = eval(c.expr, codeNs)
 
        return out
 

	
light9/effecteval/effectloop.py
Show inline comments
 
@@ -76,13 +76,14 @@ class EffectLoop(object):
 
                        outSubs.append(e.eval(songTime))
 
                    except Exception as exc:
 
                        now = time.time()
 
                        if now > self.lastErrorLog + 5:
 
                            log.error("effect %s: %s" % (e.uri, exc))
 
                            log.error("  expr: %s", e.code.expr)
 
                            log.error("  resources: %r", e.resourcesAsPython())
 
                            log.error("  resources: %r",
 
                                      getattr(e, 'resourceMap', '?'))
 
                            self.lastErrorLog = now
 
                out = Submaster.sub_maxes(*outSubs)
 

	
 
                self.logLevels(t1, out)
 
                dmx = out.get_dmx_list()
 
                with self.stats.writeDmx.time():
light9/subserver/effects.coffee
Show inline comments
 
class Model
 
  constructor: ->
 
    @classes = ko.observable([])
 
    
 
    @chases = ko.observable([])
 
    @moreExprs = [
 
      {label: "rainbow", expr: "hsv(t*1,1,1)"},
 
      {label: "pulserainbow", expr: "hsv(t*1,1,1)*nsin(t*2)"},
 
      {label: "stacrainbow", expr: "hsv(t*1,1,1)*(nsin(t*2)>.7)"},
 
      {label: "alternatergb", expr: "hsv(t*1,1,1, light='left')*nsin(t*3) + hsv(t*1,1,1,light='right')*(1-nsin(t*3))"},
 
@@ -19,18 +21,17 @@ class Model
 
  subtermExprs: (chase) =>
 
    [
 
      'chase(t, names=LABEL, ontime=0.5, offset=0.2)'.replace(/LABEL/g, chase.label)
 
    ]
 
  
 

	
 

	
 
model = new Model()
 

	
 

	
 
reconnectingWebSocket "ws://localhost:8052/effectsUpdates", (msg) ->
 
  model.chases(msg.chases) if msg.chases?
 
  model.classes(msg.classes) if msg.classes?
 

	
 
# this sort of works to stop clicks in <input> from following the
 
# submaster hyperlink, but it may make certain clicks act wrong
 
$(document).on('click', 'a', (ev) ->
 
  return false if ev.target.tagName == 'INPUT'
 
)
light9/subserver/effects.jade
Show inline comments
 
@@ -16,12 +16,22 @@ html
 
      div.resource.chase
 
        | Chase
 
        a(data-bind="attr: {href: uri}, text: label")
 
        ul(data-bind="foreach: $parent.subtermExprs($data)")
 
          li: a.resource(data-bind="attr: {href: $root.subtermLink($parent.label, $data)}, text: $data")
 

	
 

	
 
    div(data-bind="foreach: classes")
 
      div.resource.effectClass
 
        h2
 
          | Effect class
 
          | 
 
          a(data-bind="attr: {href: uri}, text: label")
 
        div
 
          code(data-bind="text: code")
 
        
 
    #status
 
      
 
    script(src="static/jquery-2.1.1.min.js")
 
    script(src="static/knockout-3.1.0.js")
 
    script(src="static/websocket.js")
 
    script(src="effects.js")
 
\ No newline at end of file
0 comments (0 inline, 0 general)