changeset 2298:63aad60fb070

big effect rewrite: the effect functions & library
author drewp@bigasterisk.com
date Tue, 30 May 2023 23:48:42 -0700
parents 33d1f00de395
children 8c82f13a3298
files light9/effect/effect_function_library.py light9/effect/effect_function_library_test.py light9/effect/effect_functions.py light9/effect/effecteval.py light9/effect/scale.py light9/namespaces.py light9/newtypes.py
diffstat 7 files changed, 180 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/effect_function_library.py	Tue May 30 23:48:42 2023 -0700
@@ -0,0 +1,67 @@
+"""repo of the EffectFunctions in the graph. Includes URI->realPythonFunction"""
+import logging
+from dataclasses import dataclass, field
+from typing import Callable, Dict, List, Optional, Tuple, cast
+
+from rdfdb.syncedgraph.syncedgraph import SyncedGraph
+from rdflib import RDF, RDFS, Literal, Namespace, URIRef
+
+from light9.namespaces import DEV, FUNC, L9
+from light9.newtypes import (DeviceAttr, DeviceUri, EffectAttr, EffectFunction, EffectUri, HexColor, VTUnion)
+from light9.typedgraph import typedValue
+
+log = logging.getLogger('effectfuncs')
+
+from . import effect_functions
+
+
+@dataclass
+class _EffectFunctionInput:
+    effectAttr: EffectAttr
+    defaultValue: Optional[VTUnion]
+
+
+@dataclass
+class _RdfEffectFunction:
+    uri: EffectFunction
+    label: Optional[Literal]
+    inputs: List[_EffectFunctionInput]
+
+
+@dataclass
+class EffectFunctionLibrary:
+    """parses :EffectFunction structures"""
+    graph: SyncedGraph
+
+    funcs: List[_RdfEffectFunction] = field(default_factory=list)
+
+    def __post_init__(self):
+        self.graph.addHandler(self._compile)
+
+    def _compile(self):
+        self.funcs = []
+        for subj in self.graph.subjects(RDF.type, L9['EffectFunction']):
+            label = typedValue(Literal | None, self.graph, subj, RDFS.label)
+            inputs = []
+            for inp in self.graph.objects(subj, L9['input']):
+                inputs.append(
+                    _EffectFunctionInput(  #
+                        typedValue(EffectAttr, self.graph, inp, L9['effectAttr']),  #
+                        typedValue(VTUnion | None, self.graph, inp, L9['defaultValue'])))
+
+            self.funcs.append(_RdfEffectFunction(cast(EffectFunction, subj), label, inputs))
+
+    def getFunc(self, uri: EffectFunction) -> Callable:
+        return {
+            FUNC['scale']: effect_functions.effect_scale,
+            FUNC['strobe']: effect_functions.effect_strobe,
+        }[uri]
+    
+    def getDefaultValue(self, uri: EffectFunction, attr:EffectAttr) -> VTUnion:
+        for f in self.funcs:
+            if f.uri == uri:
+                for i in f.inputs:
+                    if i.effectAttr==attr:
+                        if i.defaultValue is not None:
+                            return i.defaultValue
+        raise ValueError(f'no default for {uri} {attr}')
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/effect_function_library_test.py	Tue May 30 23:48:42 2023 -0700
@@ -0,0 +1,40 @@
+from light9.effect.effect_function_library import EffectFunctionLibrary
+
+from light9.mock_syncedgraph import MockSyncedGraph
+from light9.namespaces import L9
+
+PREFIXES = '''
+@prefix : <http://light9.bigasterisk.com/> .
+@prefix dev: <http://light9.bigasterisk.com/theater/test/device/> .
+@prefix effect: <http://light9.bigasterisk.com/effect/> .
+@prefix func: <http://light9.bigasterisk.com/effectFunction/> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+'''
+
+GRAPH = PREFIXES + '''
+
+    func:scale
+        a :EffectFunction;
+        rdfs:label "a submaster- scales :deviceSettings";
+        :input 
+            [ :effectAttr :strength; :defaultValue 0.0 ],
+            [ :effectAttr :deviceSettings; ] . # e.g. "par2 at color=red; par3 at color=white"
+
+    func:strobe
+        a :EffectFunction;
+        rdfs:label "blink specified devices";
+        :input 
+            [ :effectAttr :strength; :defaultValue 0.0 ],
+            [ :effectAttr :period; :defaultValue 0.5 ],
+            [ :effectAttr :onTime; :defaultValue 0.1 ],
+            [ :effectAttr :deviceSettings ] .
+
+
+'''
+
+class TestParsesGraph:
+    def test(self):
+        g = MockSyncedGraph(GRAPH)
+        lib = EffectFunctionLibrary(g)
+        assert len(lib.funcs) == 2
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/effect_functions.py	Tue May 30 23:48:42 2023 -0700
@@ -0,0 +1,63 @@
+import logging
+import random
+
+from PIL import Image
+from webcolors import rgb_to_hex
+
+from light9.effect.scale import scale
+from light9.effect.settings import DeviceSettings
+from light9.namespaces import L9
+
+random.seed(0)
+
+log = logging.getLogger('effectfunc')
+
+
+def sample8(img, x, y, repeat=False):
+    if not (0 <= y < img.height):
+        return (0, 0, 0)
+    if 0 <= x < img.width:
+        return img.getpixel((x, y))
+    elif not repeat:
+        return (0, 0, 0)
+    else:
+        return img.getpixel((x % img.width, y))
+
+
+def effect_scale(strength: float, devs: DeviceSettings) -> DeviceSettings:
+    out = []
+    if strength != 0:
+        for d, da, v in devs.asList():
+            out.append((d, da, scale(v, strength)))
+    return DeviceSettings(devs.graph, out)
+
+
+def effect_strobe(
+        songTime: float,  #
+        strength: float,
+        period: float,
+        onTime: float,
+        devs: DeviceSettings) -> DeviceSettings:
+    if period == 0:
+        scl=0
+    else:
+        scl = strength  if (songTime % period) < onTime else 0
+    return effect_scale(scl, devs)
+
+
+def effect_image(
+    songTime: float,  #
+    strength: float,
+    period: float,
+    image: Image.Image,
+    devs: DeviceSettings,
+) -> DeviceSettings:
+    x = int((songTime / period) * image.width)
+    out = []
+    for y, (d, da, v) in enumerate(devs.asOrderedList()):
+        if da != L9['color']:
+            continue
+        color8 = sample8(image, x, y, repeat=True)
+        color = rgb_to_hex(tuple(color8))
+        out.append((d, da, scale(color, strength * v)))
+    return DeviceSettings(devs.graph, out)
\ No newline at end of file
--- a/light9/effect/effecteval.py	Tue May 30 23:47:08 2023 -0700
+++ b/light9/effect/effecteval.py	Tue May 30 23:48:42 2023 -0700
@@ -114,11 +114,6 @@
         return self.function(inputs)
 
 
-def effect_scale(strength: float, devs: DeviceSettings) -> DeviceSettings:
-    out = []
-    for d, da, v in devs.asList():
-        out.append((d, da, scale(v, strength)))
-    return DeviceSettings(devs.graph, out)
 
 
 @dataclass
--- a/light9/effect/scale.py	Tue May 30 23:47:08 2023 -0700
+++ b/light9/effect/scale.py	Tue May 30 23:48:42 2023 -0700
@@ -1,26 +1,23 @@
 from decimal import Decimal
 
-from rdflib import Literal
+from light9.newtypes import VTUnion
 from webcolors import hex_to_rgb, rgb_to_hex
 
 
-def scale(value, strength):
-    if isinstance(value, Literal):
-        value = value.toPython()
-
+def scale(value:VTUnion, strength:float):
     if isinstance(value, Decimal):
-        value = float(value)
+        raise TypeError()
 
     if isinstance(value, str):
         if value[0] == '#':
             if strength == '#ffffff':
                 return value
             r, g, b = hex_to_rgb(value)
-            if isinstance(strength, Literal):
-                strength = strength.toPython()
-            if isinstance(strength, str):
-                sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)]
-            else:
+            # if isinstance(strength, Literal):
+            #     strength = strength.toPython()
+            # if isinstance(strength, str):
+            #     sr, sg, sb = [v / 255 for v in hex_to_rgb(strength)]
+            if True:
                 sr = sg = sb = strength
             return rgb_to_hex((int(r * sr), int(g * sg), int(b * sb)))
     elif isinstance(value, (int, float)):
--- a/light9/namespaces.py	Tue May 30 23:47:08 2023 -0700
+++ b/light9/namespaces.py	Tue May 30 23:48:42 2023 -0700
@@ -18,6 +18,7 @@
 
 
 L9 = FastNs("http://light9.bigasterisk.com/")
+FUNC = FastNs("http://light9.bigasterisk.com/effectFunction/")
 MUS = Namespace("http://light9.bigasterisk.com/music/")
 XSD = Namespace("http://www.w3.org/2001/XMLSchema#")
 DCTERMS = Namespace("http://purl.org/dc/terms/")
--- a/light9/newtypes.py	Tue May 30 23:47:08 2023 -0700
+++ b/light9/newtypes.py	Tue May 30 23:48:42 2023 -0700
@@ -11,7 +11,7 @@
 DmxIndex = NewType('DmxIndex', int)  # 1..512
 DmxMessageIndex = NewType('DmxMessageIndex', int)  # 0..511
 DeviceAttr = NewType('DeviceAttr', URIRef)  # e.g. :rx
-EffectClass = NewType('EffectClass', URIRef)  # e.g. effect:chase
+EffectFunction = NewType('EffectFunction', URIRef)  # e.g. func:strobe
 EffectUri = NewType('EffectUri', URIRef)  # unclear when to use this vs EffectClass
 EffectAttr = NewType('EffectAttr', URIRef)  # e.g. :chaseSpeed
 NoteUri = NewType('NoteUri', URIRef)