# HG changeset patch
# User drewp@bigasterisk.com
# Date 2023-05-31 06:48:42
# Node ID 63aad60fb070a97d8a7746a80e21ef86444293b9
# Parent 33d1f00de39581bc6d5c2d8bac809f938157aeea
big effect rewrite: the effect functions & library
diff --git a/light9/effect/effect_function_library.py b/light9/effect/effect_function_library.py
new file mode 100644
--- /dev/null
+++ b/light9/effect/effect_function_library.py
@@ -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
diff --git a/light9/effect/effect_function_library_test.py b/light9/effect/effect_function_library_test.py
new file mode 100644
--- /dev/null
+++ b/light9/effect/effect_function_library_test.py
@@ -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 : .
+@prefix dev: .
+@prefix effect: .
+@prefix func: .
+@prefix rdfs: .
+@prefix xsd: .
+'''
+
+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
diff --git a/light9/effect/effect_functions.py b/light9/effect/effect_functions.py
new file mode 100644
--- /dev/null
+++ b/light9/effect/effect_functions.py
@@ -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
diff --git a/light9/effect/effecteval.py b/light9/effect/effecteval.py
--- a/light9/effect/effecteval.py
+++ b/light9/effect/effecteval.py
@@ -114,11 +114,6 @@ class EffectEval2:
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
diff --git a/light9/effect/scale.py b/light9/effect/scale.py
--- a/light9/effect/scale.py
+++ b/light9/effect/scale.py
@@ -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)):
diff --git a/light9/namespaces.py b/light9/namespaces.py
--- a/light9/namespaces.py
+++ b/light9/namespaces.py
@@ -18,6 +18,7 @@ class FastNs:
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/")
diff --git a/light9/newtypes.py b/light9/newtypes.py
--- a/light9/newtypes.py
+++ b/light9/newtypes.py
@@ -11,7 +11,7 @@ DeviceClass = NewType('DeviceClass', URI
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)