diff --git a/bin/effecteval b/bin/effecteval --- a/bin/effecteval +++ b/bin/effecteval @@ -4,7 +4,7 @@ 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(".") @@ -39,24 +39,39 @@ class SongEffects(PrettyErrorHandler, cy 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): @@ -89,7 +104,8 @@ class EffectUpdates(cyclone.websocket.We 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") @@ -98,18 +114,41 @@ class EffectUpdates(cyclone.websocket.We 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) diff --git a/light9/Effects.py b/light9/Effects.py --- a/light9/Effects.py +++ b/light9/Effects.py @@ -1,5 +1,6 @@ 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 @@ -9,12 +10,18 @@ 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) @@ -31,6 +38,7 @@ def chase(t, ontime=0.5, offset=0.2, onv 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 = {} @@ -41,7 +49,8 @@ def hsv(h, s, v, light='all', centerScal 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 @@ -65,6 +74,10 @@ def stack(t, names=None, fade=0): return Submaster.Submaster(name="stack", levels=lev) +@register +def smoove(x): + return -2 * (x ** 3) + 3 * (x ** 2) + def configExprGlobals(): graph = showconfig.getGraph() ret = {} @@ -75,7 +88,35 @@ def configExprGlobals(): 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 diff --git a/light9/curvecalc/subterm.py b/light9/curvecalc/subterm.py --- a/light9/curvecalc/subterm.py +++ b/light9/curvecalc/subterm.py @@ -13,7 +13,6 @@ class Expr(object): 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""" @@ -23,50 +22,27 @@ class Expr(object): # 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 diff --git a/light9/effecteval/effect.coffee b/light9/effecteval/effect.coffee --- a/light9/effecteval/effect.coffee +++ b/light9/effecteval/effect.coffee @@ -1,19 +1,20 @@ 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 diff --git a/light9/effecteval/effect.html b/light9/effecteval/effect.html --- a/light9/effecteval/effect.html +++ b/light9/effecteval/effect.html @@ -7,9 +7,14 @@ - Effects / + Effects / -
code:
+
+
+ code: + +
+
diff --git a/light9/effecteval/effect.py b/light9/effecteval/effect.py --- a/light9/effecteval/effect.py +++ b/light9/effecteval/effect.py @@ -2,7 +2,7 @@ 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): @@ -23,6 +23,7 @@ class CodeLine(object): self.graph, self.code = graph, code self.outName, self.expr, self.resources = self._asPython() + self.pyResources = self._resourcesAsPython(self.resources) def _asPython(self): """ @@ -55,32 +56,14 @@ class CodeLine(object): # 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) @@ -92,21 +75,39 @@ class EffectNode(object): 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 diff --git a/light9/effecteval/effectloop.py b/light9/effecteval/effectloop.py --- a/light9/effecteval/effectloop.py +++ b/light9/effecteval/effectloop.py @@ -79,7 +79,8 @@ class EffectLoop(object): 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) diff --git a/light9/subserver/effects.coffee b/light9/subserver/effects.coffee --- a/light9/subserver/effects.coffee +++ b/light9/subserver/effects.coffee @@ -1,5 +1,7 @@ class Model constructor: -> + @classes = ko.observable([]) + @chases = ko.observable([]) @moreExprs = [ {label: "rainbow", expr: "hsv(t*1,1,1)"}, @@ -22,12 +24,11 @@ class Model ] - 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 from following the # submaster hyperlink, but it may make certain clicks act wrong diff --git a/light9/subserver/effects.jade b/light9/subserver/effects.jade --- a/light9/subserver/effects.jade +++ b/light9/subserver/effects.jade @@ -19,6 +19,16 @@ html 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")