Py.Cafe

AIPHeX/

bascul-dmx-viz

viz

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import solara
import solara.lab        # ensure lab extensions (including Card) are available
from solara import component, reactive, SliderFloat, Columns, Markdown, FigurePlotly
import numpy as np
import plotly.graph_objects as go

# — Static geometry (precomputed once) —
img_N = 512
domain = 1.5
xg = np.linspace(-domain, domain, img_N)
yg = np.linspace(-domain, domain, img_N)

theta = np.linspace(0, 2 * np.pi, 7)[:6]
subs = [-0.8, 0, 0.8]
theta[0:2] = subs[0] + np.pi * 0.015 * np.array([-1, 1])
theta[2:4] = subs[1] + np.pi * 0.015 * np.array([-1, 1])
theta[4:6] = subs[2] + np.pi * 0.015 * np.array([-1, 1])
fix_x = np.sin(theta)
fix_y = np.cos(theta)
fix_labels = ['', 'FIX1,2', 'FIX3,4', '', 'FIX5,6', '']

background_traces = []
# fixture markers
background_traces.append(go.Scatter(
    x=fix_x, y=fix_y,
    mode="markers",
    marker=dict(color="white", size=8, line=dict(color="black", width=2)),
    showlegend=False,
))
# labels
for i, lbl in enumerate(fix_labels):
    if lbl:
        background_traces.append(go.Scatter(
            x=[1.1*fix_x[i]], y=[1.1*fix_y[i]],
            mode="text",
            text=[lbl],
            textfont=dict(color="white", size=12),
            showlegend=False,
        ))
# outer ring
background_traces.append(go.Scatter(
    x=[0], y=[0],
    mode="markers",
    marker=dict(size=76000,
                color="rgba(0,0,0,0)",
                line=dict(color="darkgrey", width=100)),
    showlegend=False,
))

def make_alpha_mask(x0, y0, radius, xg, yg, alpha_max=0.5,
                    direction_angle=0.0, direction_sigma=np.pi/8):
    X, Y = np.meshgrid(xg, yg)
    dx, dy = X - x0, Y - y0
    rsq = dx*dx + dy*dy
    base_theta = np.arctan2(-y0, -x0)
    angle_to_pixel = np.arctan2(dy, dx)
    offset = (angle_to_pixel - base_theta - direction_angle + np.pi) % (2*np.pi) - np.pi
    dir_mod = np.exp(-0.5*(offset/direction_sigma)**2)
    return np.clip(1 - rsq/radius, 0, 1) * dir_mod * alpha_max

def make_blob(alpha_mask, R, G, B, W, dim):
    img = np.zeros(alpha_mask.shape + (4,))
    img[...,0] = dim*(R*alpha_mask + W*alpha_mask)
    img[...,1] = dim*(G*alpha_mask + W*alpha_mask)
    img[...,2] = dim*(B*alpha_mask + W*alpha_mask)
    img[...,3] = dim*alpha_mask
    return img

def sum_blobs(blobs):
    out = np.zeros(blobs[0].shape)
    for b in blobs:
        out[..., :3] += b[..., :3]
        out[..., 3]  += b[..., 3]
    out[..., :3] = np.clip(out[..., :3], 0, 1)
    out[..., 3]  = np.clip(out[..., 3],   0, 1)
    return out

# — Module‐level reactive state —
fixtures = []
for _ in range(6):
    fixtures.append({
        "R": reactive(1.0),
        "G": reactive(0.0),
        "B": reactive(0.0),
        "W": reactive(0.0),
        "radius": reactive(1.0),
        "angle": reactive(0.0),
        "sigma": reactive(np.pi/8),
    })
master_dimmer = reactive(1.0)

# — The card component, exactly 4-tabs style —
@component
def FixtureBlendCard():
    def build_figure():
        fig = go.Figure()
        # static background
        for t in background_traces:
            fig.add_trace(t)
        # dynamic blobs
        blobs = []
        dim = master_dimmer.value
        for i, fx in enumerate(fixtures):
            mask = make_alpha_mask(
                fix_x[i], fix_y[i],
                fx["radius"].value, xg, yg,
                alpha_max=0.5,
                direction_angle=fx["angle"].value,
                direction_sigma=fx["sigma"].value,
            )
            blobs.append(make_blob(
                mask,
                fx["R"].value, fx["G"].value, fx["B"].value, fx["W"].value,
                dim,
            ))
        img = sum_blobs(blobs)
        fig.add_trace(go.Image(z=img))
        fig.update_layout(
            showlegend=False,
            margin=dict(l=0, r=0, t=0, b=0),
            plot_bgcolor="black", paper_bgcolor="black",
        )
        fig.update_xaxes(visible=False, range=[-domain, domain], constrain="domain")
        fig.update_yaxes(visible=False, scaleanchor="x", scaleratio=1, range=[-domain, domain])
        return fig

    # list all reactives so FigurePlotly only reruns on change
    deps = [master_dimmer]
    for fx in fixtures:
        deps += [fx[k] for k in ("R", "G", "B", "W", "radius", "angle", "sigma")]

    controls = [SliderFloat("Master Dimmer", value=master_dimmer, min=0.0, max=1.0, step=0.01)]
    for i, fx in enumerate(fixtures, start=1):
        controls += [
            Markdown(f"**Fixture {i}**"),
            SliderFloat("R", value=fx["R"], min=0.0, max=1.0, step=0.01),
            SliderFloat("G", value=fx["G"], min=0.0, max=1.0, step=0.01),
            SliderFloat("B", value=fx["B"], min=0.0, max=1.0, step=0.01),
            SliderFloat("W", value=fx["W"], min=0.0, max=1.0, step=0.01),
            SliderFloat("blob_radius",    value=fx["radius"], min=0.2, max=2.5, step=0.01),
            SliderFloat("direction_angle",value=fx["angle"],  min=-np.pi, max=np.pi, step=0.01),
            SliderFloat("direction_sigma",value=fx["sigma"],  min=0.01, max=1.0, step=0.01),
        ]

    # use positional args: first the title, then the single child Columns(...)
    return solara.Card(
        "Fixture Blend",
        Columns([2, 1], [
            FigurePlotly(build_figure, dependencies=deps),
            controls
        ])
    )

# Then simply add to your tabs:
# tabs = [
#   ...,
#   ("Fixture Blend", FixtureBlendCard),
# ]
@component
def Page():
    return FixtureBlendCard()