pyglass-qt 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyglass_qt-0.2.0/LICENSE +21 -0
- pyglass_qt-0.2.0/PKG-INFO +199 -0
- pyglass_qt-0.2.0/README.md +169 -0
- pyglass_qt-0.2.0/pyglass/__init__.py +62 -0
- pyglass_qt-0.2.0/pyglass/backdrop.py +291 -0
- pyglass_qt-0.2.0/pyglass/blur.py +52 -0
- pyglass_qt-0.2.0/pyglass/demo.py +125 -0
- pyglass_qt-0.2.0/pyglass/desktop.py +129 -0
- pyglass_qt-0.2.0/pyglass/effect.py +223 -0
- pyglass_qt-0.2.0/pyglass/glass.py +343 -0
- pyglass_qt-0.2.0/pyglass/pane.py +312 -0
- pyglass_qt-0.2.0/pyglass/refract.py +433 -0
- pyglass_qt-0.2.0/pyglass_qt.egg-info/PKG-INFO +199 -0
- pyglass_qt-0.2.0/pyglass_qt.egg-info/SOURCES.txt +17 -0
- pyglass_qt-0.2.0/pyglass_qt.egg-info/dependency_links.txt +1 -0
- pyglass_qt-0.2.0/pyglass_qt.egg-info/requires.txt +2 -0
- pyglass_qt-0.2.0/pyglass_qt.egg-info/top_level.txt +1 -0
- pyglass_qt-0.2.0/pyproject.toml +41 -0
- pyglass_qt-0.2.0/setup.cfg +4 -0
pyglass_qt-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neomosh8
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyglass-qt
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Physically-grounded refractive glass widgets for PyQt6 — real refraction, chromatic dispersion, Fresnel reflection and frost.
|
|
5
|
+
Author-email: neomosh8 <mosh@neocore.tech>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/neomosh8/pyglass
|
|
8
|
+
Project-URL: Repository, https://github.com/neomosh8/pyglass
|
|
9
|
+
Project-URL: Issues, https://github.com/neomosh8/pyglass/issues
|
|
10
|
+
Keywords: pyqt6,qt,glass,glassmorphism,refraction,dispersion,ui,widgets
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
23
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: PyQt6>=6.6
|
|
28
|
+
Requires-Dist: numpy>=1.24
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# PyGlass
|
|
32
|
+
|
|
33
|
+
Physically-grounded refractive **glass** for **PyQt6** — drop it onto any app.
|
|
34
|
+
|
|
35
|
+
PyGlass renders glass the way glass behaves: refraction through a beveled slab,
|
|
36
|
+
chromatic dispersion, Fresnel reflectance, an iridescent rim and an optional
|
|
37
|
+
frosted (rough-surface) blur. It ships as a reusable package with two layers —
|
|
38
|
+
a one-line widget for the common case, and the raw engine for custom widgets.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install pyglass-qt # from PyPI
|
|
44
|
+
# or straight from GitHub:
|
|
45
|
+
pip install "git+https://github.com/neomosh8/pyglass.git"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> The distribution is **`pyglass-qt`** (the name `pyglass` was taken on PyPI), but
|
|
49
|
+
> you still `import pyglass`. Only PyQt6 + numpy are pulled in.
|
|
50
|
+
|
|
51
|
+
To run the demos from a clone instead:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python3 -m venv .venv
|
|
55
|
+
.venv/bin/python -m pip install -r requirements.txt # PyQt6 + numpy
|
|
56
|
+
.venv/bin/python main.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Use it in your app
|
|
60
|
+
|
|
61
|
+
**High level — `GlassPane`.** A frameless glass widget. Give it a parent and it
|
|
62
|
+
becomes an in-app modal/panel that refracts your app; leave it parentless and it
|
|
63
|
+
becomes a top-level window that refracts the live desktop. Draggable, with live
|
|
64
|
+
dials built in.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pyglass import GlassPane, GlassMaterial
|
|
68
|
+
from PyQt6.QtWidgets import QVBoxLayout, QLabel
|
|
69
|
+
|
|
70
|
+
# A glass modal over your existing window — refracts whatever's behind it.
|
|
71
|
+
pane = GlassPane(my_window, material=GlassMaterial(thickness=0.6, frost=0.3))
|
|
72
|
+
QVBoxLayout(pane.content).addWidget(QLabel("Hello from glass"))
|
|
73
|
+
pane.show()
|
|
74
|
+
|
|
75
|
+
# …or a glass window over the live desktop:
|
|
76
|
+
desk = GlassPane(material=GlassMaterial(thickness=0.7, frost=0.15))
|
|
77
|
+
desk.show()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Put your widgets in `pane.content`. The pane captures its parent (with itself
|
|
81
|
+
hidden) for the backdrop, so **no cooperation from the host is needed** — it
|
|
82
|
+
works on any widget. See [`examples/`](examples/).
|
|
83
|
+
|
|
84
|
+
**Low level — compose it yourself.** Build the glass inside your own
|
|
85
|
+
`paintEvent` with the engine pieces: a backdrop provider →
|
|
86
|
+
[`GlassRenderer`](pyglass/effect.py) (backdrop array → refracted pixmap) →
|
|
87
|
+
[`paint_glass`](pyglass/effect.py) (shadow + refraction + tint + rim). See
|
|
88
|
+
[`pyglass/glass.py`](pyglass/glass.py) (`GlassPopup`) for a full worked example
|
|
89
|
+
with a scrim and an open/close animation.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from pyglass import GlassRenderer, paint_glass, WidgetBackdrop, GlassMaterial
|
|
93
|
+
|
|
94
|
+
backdrop = WidgetBackdrop(host) # or ScreenBackdrop(window)
|
|
95
|
+
renderer = GlassRenderer(GlassMaterial(), w, h, radius)
|
|
96
|
+
backdrop.changed.connect(lambda: self.update())
|
|
97
|
+
# in paintEvent:
|
|
98
|
+
pm = renderer.refract(backdrop.array(), origin, backdrop.dpr())
|
|
99
|
+
paint_glass(painter, panel_rect, radius, pm)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## The two dials
|
|
103
|
+
|
|
104
|
+
The entire look is driven by [`GlassMaterial`](pyglass/refract.py) — two
|
|
105
|
+
perceptual dials in `[0, 1]` that re-derive a dozen physical parameters so the
|
|
106
|
+
pane always reads as one coherent piece of glass. The neutral pair
|
|
107
|
+
(`thickness=0.5, frost=0`) reproduces the tuned baseline exactly.
|
|
108
|
+
|
|
109
|
+
| Dial | What it means | What it drives |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| **`thickness`** | perceived slab depth / mass (optical path length) | displacement (`strength`), the curved lens-wrap width (`bevel`), the IOR range / rim bend, chromatic dispersion (`chroma`), the spectral rim-line width, and the capture margin so the wrap never clamps |
|
|
112
|
+
| **`frost`** | surface roughness (ground / milk glass) | a transmission blur (scatter), a milky multiple-scatter haze, and a softened dispersion line — transmission-side only, so `frost=0` is byte-for-byte the sharp look |
|
|
113
|
+
|
|
114
|
+
`thickness` is a single scalar standing in for *T*: a thicker slab bends light
|
|
115
|
+
more, has a bigger rounded edge, disperses colour more (longer optical path) and
|
|
116
|
+
casts a thicker rim — all slaved together. `frost` is microfacet roughness: a
|
|
117
|
+
rough face scatters transmitted light into a cone that projects to a blur, plus
|
|
118
|
+
a faint milky veil.
|
|
119
|
+
|
|
120
|
+
`GlassStyle` separately tunes the non-physical chrome (shadow, tint, sheen, rim).
|
|
121
|
+
|
|
122
|
+
## Run the demos
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
.venv/bin/python main.py # in-app frosted refractive modal
|
|
126
|
+
.venv/bin/python main.py --desktop # glass window over your live desktop
|
|
127
|
+
.venv/bin/python examples/in_app_modal.py
|
|
128
|
+
.venv/bin/python examples/desktop_window.py
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
In any of them: **drag** the panel; **`[`** / **`]`** adjust thickness;
|
|
132
|
+
**`-`** / **`=`** adjust frost; **`R`** refreshes the backdrop; on the desktop
|
|
133
|
+
pane **`L`** toggles live auto-refresh; **`Esc`** closes.
|
|
134
|
+
|
|
135
|
+
## Desktop mode — glass over your real screen
|
|
136
|
+
|
|
137
|
+
A parentless `GlassPane` (or `python main.py --desktop`) floats over your **live**
|
|
138
|
+
desktop and refracts whatever is behind it — all your windows, not just the
|
|
139
|
+
wallpaper.
|
|
140
|
+
|
|
141
|
+
* **macOS:** it shells out to the system **`screencapture`** (which, unlike Qt's
|
|
142
|
+
`grabWindow`, returns the full screen with every window) and excludes *itself*
|
|
143
|
+
from capture via `NSWindowSharingNone`. Because the window is excluded, the
|
|
144
|
+
backdrop **auto-refreshes live** with no hide/flicker, and dragging stays
|
|
145
|
+
smooth (it re-slices the last capture each frame).
|
|
146
|
+
|
|
147
|
+
> Needs Screen Recording permission (System Settings → Privacy & Security →
|
|
148
|
+
> Screen Recording) for the terminal/app running Python. If only the wallpaper
|
|
149
|
+
> shows, grant it and relaunch.
|
|
150
|
+
|
|
151
|
+
* **Windows / Linux:** Qt's `grabWindow` can't be told to exclude the window, so
|
|
152
|
+
a periodic re-grab would flicker. PyGlass therefore captures **once and stays
|
|
153
|
+
paused** (press **`R`** to refresh) — no flicker. The dials still work live
|
|
154
|
+
against the cached frame.
|
|
155
|
+
|
|
156
|
+
## Platform support
|
|
157
|
+
|
|
158
|
+
Cross-platform — **macOS, Windows, Linux**. PyQt6 + numpy only. The in-app glass
|
|
159
|
+
reads the app's *own* rendered scene (no OS screen-capture permission needed);
|
|
160
|
+
fonts fall back gracefully (SF Pro → Segoe UI → Arial) and device-pixel-ratio is
|
|
161
|
+
handled, so it renders correctly on Windows HiDPI and Retina alike.
|
|
162
|
+
|
|
163
|
+
## Render a preview without a display
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
QT_QPA_PLATFORM=offscreen .venv/bin/python scripts/render_preview.py preview.png
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Layout
|
|
170
|
+
|
|
171
|
+
| File | Purpose |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| [`pyglass/refract.py`](pyglass/refract.py) | Engine — `GlassKernel` (refraction + Fresnel over a beveled SDF) and `GlassMaterial` (the two dials) |
|
|
174
|
+
| [`pyglass/effect.py`](pyglass/effect.py) | `GlassRenderer`, `paint_glass`, `GlassStyle` — the reusable rendering core |
|
|
175
|
+
| [`pyglass/backdrop.py`](pyglass/backdrop.py) | `WidgetBackdrop` / `ScreenBackdrop` — *what* the glass refracts |
|
|
176
|
+
| [`pyglass/pane.py`](pyglass/pane.py) | `GlassPane` — the drop-in glass widget (+ `ui_font`) |
|
|
177
|
+
| [`pyglass/glass.py`](pyglass/glass.py) | `GlassPopup` — in-app modal demo built on the low-level core |
|
|
178
|
+
| [`pyglass/desktop.py`](pyglass/desktop.py) | `DesktopGlass` — desktop-window demo, a thin `GlassPane` subclass |
|
|
179
|
+
| [`pyglass/demo.py`](pyglass/demo.py) | `DemoBackground` — colourful host scene + launch button |
|
|
180
|
+
| [`examples/`](examples/) | Standalone third-party usage of `GlassPane` |
|
|
181
|
+
| [`main.py`](main.py) | Entry point (`--desktop` for desktop mode) |
|
|
182
|
+
|
|
183
|
+
## How the refraction works
|
|
184
|
+
|
|
185
|
+
The panel is a **beveled glass slab** over a rounded-rectangle signed distance
|
|
186
|
+
field. The flat centre passes light straight through; the rim is a quarter-circle
|
|
187
|
+
**roundover** whose slope grows toward the edge. The vertical incident ray is
|
|
188
|
+
refracted there with **Snell's law** and projected through the glass thickness,
|
|
189
|
+
so the `1/(-T_z)` term curls the background into a curved lens-*wrap* (not a flat
|
|
190
|
+
shift). Each colour channel uses its own IOR → a **chromatic-dispersion** fringe.
|
|
191
|
+
The Schlick–**Fresnel** term rises from ~`F0` at the centre to ~1 at the grazing
|
|
192
|
+
rim, where the surface reflects a virtual environment (horizon ambient + a warm
|
|
193
|
+
key and cool fill light). A lightened **iridescent** spectral line is added along
|
|
194
|
+
the border. Frost adds a fast separable blur of the transmitted background.
|
|
195
|
+
|
|
196
|
+
All geometry-dependent work (normals, per-channel sample coordinates, Fresnel
|
|
197
|
+
weight, reflected environment) is precomputed once into a `GlassKernel`; each
|
|
198
|
+
frame only runs the bilinear gather (+ the box blur when frosted), so dragging
|
|
199
|
+
stays smooth.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# PyGlass
|
|
2
|
+
|
|
3
|
+
Physically-grounded refractive **glass** for **PyQt6** — drop it onto any app.
|
|
4
|
+
|
|
5
|
+
PyGlass renders glass the way glass behaves: refraction through a beveled slab,
|
|
6
|
+
chromatic dispersion, Fresnel reflectance, an iridescent rim and an optional
|
|
7
|
+
frosted (rough-surface) blur. It ships as a reusable package with two layers —
|
|
8
|
+
a one-line widget for the common case, and the raw engine for custom widgets.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install pyglass-qt # from PyPI
|
|
14
|
+
# or straight from GitHub:
|
|
15
|
+
pip install "git+https://github.com/neomosh8/pyglass.git"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> The distribution is **`pyglass-qt`** (the name `pyglass` was taken on PyPI), but
|
|
19
|
+
> you still `import pyglass`. Only PyQt6 + numpy are pulled in.
|
|
20
|
+
|
|
21
|
+
To run the demos from a clone instead:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 -m venv .venv
|
|
25
|
+
.venv/bin/python -m pip install -r requirements.txt # PyQt6 + numpy
|
|
26
|
+
.venv/bin/python main.py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use it in your app
|
|
30
|
+
|
|
31
|
+
**High level — `GlassPane`.** A frameless glass widget. Give it a parent and it
|
|
32
|
+
becomes an in-app modal/panel that refracts your app; leave it parentless and it
|
|
33
|
+
becomes a top-level window that refracts the live desktop. Draggable, with live
|
|
34
|
+
dials built in.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from pyglass import GlassPane, GlassMaterial
|
|
38
|
+
from PyQt6.QtWidgets import QVBoxLayout, QLabel
|
|
39
|
+
|
|
40
|
+
# A glass modal over your existing window — refracts whatever's behind it.
|
|
41
|
+
pane = GlassPane(my_window, material=GlassMaterial(thickness=0.6, frost=0.3))
|
|
42
|
+
QVBoxLayout(pane.content).addWidget(QLabel("Hello from glass"))
|
|
43
|
+
pane.show()
|
|
44
|
+
|
|
45
|
+
# …or a glass window over the live desktop:
|
|
46
|
+
desk = GlassPane(material=GlassMaterial(thickness=0.7, frost=0.15))
|
|
47
|
+
desk.show()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Put your widgets in `pane.content`. The pane captures its parent (with itself
|
|
51
|
+
hidden) for the backdrop, so **no cooperation from the host is needed** — it
|
|
52
|
+
works on any widget. See [`examples/`](examples/).
|
|
53
|
+
|
|
54
|
+
**Low level — compose it yourself.** Build the glass inside your own
|
|
55
|
+
`paintEvent` with the engine pieces: a backdrop provider →
|
|
56
|
+
[`GlassRenderer`](pyglass/effect.py) (backdrop array → refracted pixmap) →
|
|
57
|
+
[`paint_glass`](pyglass/effect.py) (shadow + refraction + tint + rim). See
|
|
58
|
+
[`pyglass/glass.py`](pyglass/glass.py) (`GlassPopup`) for a full worked example
|
|
59
|
+
with a scrim and an open/close animation.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pyglass import GlassRenderer, paint_glass, WidgetBackdrop, GlassMaterial
|
|
63
|
+
|
|
64
|
+
backdrop = WidgetBackdrop(host) # or ScreenBackdrop(window)
|
|
65
|
+
renderer = GlassRenderer(GlassMaterial(), w, h, radius)
|
|
66
|
+
backdrop.changed.connect(lambda: self.update())
|
|
67
|
+
# in paintEvent:
|
|
68
|
+
pm = renderer.refract(backdrop.array(), origin, backdrop.dpr())
|
|
69
|
+
paint_glass(painter, panel_rect, radius, pm)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## The two dials
|
|
73
|
+
|
|
74
|
+
The entire look is driven by [`GlassMaterial`](pyglass/refract.py) — two
|
|
75
|
+
perceptual dials in `[0, 1]` that re-derive a dozen physical parameters so the
|
|
76
|
+
pane always reads as one coherent piece of glass. The neutral pair
|
|
77
|
+
(`thickness=0.5, frost=0`) reproduces the tuned baseline exactly.
|
|
78
|
+
|
|
79
|
+
| Dial | What it means | What it drives |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| **`thickness`** | perceived slab depth / mass (optical path length) | displacement (`strength`), the curved lens-wrap width (`bevel`), the IOR range / rim bend, chromatic dispersion (`chroma`), the spectral rim-line width, and the capture margin so the wrap never clamps |
|
|
82
|
+
| **`frost`** | surface roughness (ground / milk glass) | a transmission blur (scatter), a milky multiple-scatter haze, and a softened dispersion line — transmission-side only, so `frost=0` is byte-for-byte the sharp look |
|
|
83
|
+
|
|
84
|
+
`thickness` is a single scalar standing in for *T*: a thicker slab bends light
|
|
85
|
+
more, has a bigger rounded edge, disperses colour more (longer optical path) and
|
|
86
|
+
casts a thicker rim — all slaved together. `frost` is microfacet roughness: a
|
|
87
|
+
rough face scatters transmitted light into a cone that projects to a blur, plus
|
|
88
|
+
a faint milky veil.
|
|
89
|
+
|
|
90
|
+
`GlassStyle` separately tunes the non-physical chrome (shadow, tint, sheen, rim).
|
|
91
|
+
|
|
92
|
+
## Run the demos
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
.venv/bin/python main.py # in-app frosted refractive modal
|
|
96
|
+
.venv/bin/python main.py --desktop # glass window over your live desktop
|
|
97
|
+
.venv/bin/python examples/in_app_modal.py
|
|
98
|
+
.venv/bin/python examples/desktop_window.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
In any of them: **drag** the panel; **`[`** / **`]`** adjust thickness;
|
|
102
|
+
**`-`** / **`=`** adjust frost; **`R`** refreshes the backdrop; on the desktop
|
|
103
|
+
pane **`L`** toggles live auto-refresh; **`Esc`** closes.
|
|
104
|
+
|
|
105
|
+
## Desktop mode — glass over your real screen
|
|
106
|
+
|
|
107
|
+
A parentless `GlassPane` (or `python main.py --desktop`) floats over your **live**
|
|
108
|
+
desktop and refracts whatever is behind it — all your windows, not just the
|
|
109
|
+
wallpaper.
|
|
110
|
+
|
|
111
|
+
* **macOS:** it shells out to the system **`screencapture`** (which, unlike Qt's
|
|
112
|
+
`grabWindow`, returns the full screen with every window) and excludes *itself*
|
|
113
|
+
from capture via `NSWindowSharingNone`. Because the window is excluded, the
|
|
114
|
+
backdrop **auto-refreshes live** with no hide/flicker, and dragging stays
|
|
115
|
+
smooth (it re-slices the last capture each frame).
|
|
116
|
+
|
|
117
|
+
> Needs Screen Recording permission (System Settings → Privacy & Security →
|
|
118
|
+
> Screen Recording) for the terminal/app running Python. If only the wallpaper
|
|
119
|
+
> shows, grant it and relaunch.
|
|
120
|
+
|
|
121
|
+
* **Windows / Linux:** Qt's `grabWindow` can't be told to exclude the window, so
|
|
122
|
+
a periodic re-grab would flicker. PyGlass therefore captures **once and stays
|
|
123
|
+
paused** (press **`R`** to refresh) — no flicker. The dials still work live
|
|
124
|
+
against the cached frame.
|
|
125
|
+
|
|
126
|
+
## Platform support
|
|
127
|
+
|
|
128
|
+
Cross-platform — **macOS, Windows, Linux**. PyQt6 + numpy only. The in-app glass
|
|
129
|
+
reads the app's *own* rendered scene (no OS screen-capture permission needed);
|
|
130
|
+
fonts fall back gracefully (SF Pro → Segoe UI → Arial) and device-pixel-ratio is
|
|
131
|
+
handled, so it renders correctly on Windows HiDPI and Retina alike.
|
|
132
|
+
|
|
133
|
+
## Render a preview without a display
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
QT_QPA_PLATFORM=offscreen .venv/bin/python scripts/render_preview.py preview.png
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Layout
|
|
140
|
+
|
|
141
|
+
| File | Purpose |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| [`pyglass/refract.py`](pyglass/refract.py) | Engine — `GlassKernel` (refraction + Fresnel over a beveled SDF) and `GlassMaterial` (the two dials) |
|
|
144
|
+
| [`pyglass/effect.py`](pyglass/effect.py) | `GlassRenderer`, `paint_glass`, `GlassStyle` — the reusable rendering core |
|
|
145
|
+
| [`pyglass/backdrop.py`](pyglass/backdrop.py) | `WidgetBackdrop` / `ScreenBackdrop` — *what* the glass refracts |
|
|
146
|
+
| [`pyglass/pane.py`](pyglass/pane.py) | `GlassPane` — the drop-in glass widget (+ `ui_font`) |
|
|
147
|
+
| [`pyglass/glass.py`](pyglass/glass.py) | `GlassPopup` — in-app modal demo built on the low-level core |
|
|
148
|
+
| [`pyglass/desktop.py`](pyglass/desktop.py) | `DesktopGlass` — desktop-window demo, a thin `GlassPane` subclass |
|
|
149
|
+
| [`pyglass/demo.py`](pyglass/demo.py) | `DemoBackground` — colourful host scene + launch button |
|
|
150
|
+
| [`examples/`](examples/) | Standalone third-party usage of `GlassPane` |
|
|
151
|
+
| [`main.py`](main.py) | Entry point (`--desktop` for desktop mode) |
|
|
152
|
+
|
|
153
|
+
## How the refraction works
|
|
154
|
+
|
|
155
|
+
The panel is a **beveled glass slab** over a rounded-rectangle signed distance
|
|
156
|
+
field. The flat centre passes light straight through; the rim is a quarter-circle
|
|
157
|
+
**roundover** whose slope grows toward the edge. The vertical incident ray is
|
|
158
|
+
refracted there with **Snell's law** and projected through the glass thickness,
|
|
159
|
+
so the `1/(-T_z)` term curls the background into a curved lens-*wrap* (not a flat
|
|
160
|
+
shift). Each colour channel uses its own IOR → a **chromatic-dispersion** fringe.
|
|
161
|
+
The Schlick–**Fresnel** term rises from ~`F0` at the centre to ~1 at the grazing
|
|
162
|
+
rim, where the surface reflects a virtual environment (horizon ambient + a warm
|
|
163
|
+
key and cool fill light). A lightened **iridescent** spectral line is added along
|
|
164
|
+
the border. Frost adds a fast separable blur of the transmitted background.
|
|
165
|
+
|
|
166
|
+
All geometry-dependent work (normals, per-channel sample coordinates, Fresnel
|
|
167
|
+
weight, reflected environment) is precomputed once into a `GlassKernel`; each
|
|
168
|
+
frame only runs the bilinear gather (+ the box blur when frosted), so dragging
|
|
169
|
+
stays smooth.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""PyGlass — physically-grounded refractive glass for PyQt6.
|
|
2
|
+
|
|
3
|
+
Two layers, use whichever fits:
|
|
4
|
+
|
|
5
|
+
* **High-level** — :class:`GlassPane`: a drop-in frameless glass widget. As a
|
|
6
|
+
child it's an in-app modal/panel that refracts your app; parentless it's a
|
|
7
|
+
top-level window that refracts the live desktop. Draggable, with live
|
|
8
|
+
``thickness`` / ``frost`` dials.
|
|
9
|
+
|
|
10
|
+
from pyglass import GlassPane, GlassMaterial
|
|
11
|
+
pane = GlassPane(parent=my_window, material=GlassMaterial(thickness=0.6, frost=0.3))
|
|
12
|
+
pane.show()
|
|
13
|
+
|
|
14
|
+
* **Low-level** — compose the engine yourself inside any ``paintEvent``:
|
|
15
|
+
:class:`GlassRenderer` (backdrop → refracted pixmap), :func:`paint_glass`
|
|
16
|
+
(compositing), and the :mod:`~pyglass.backdrop` providers. See
|
|
17
|
+
:class:`pyglass.glass.GlassPopup` for a worked example.
|
|
18
|
+
|
|
19
|
+
The look is driven by :class:`GlassMaterial`'s two dials — ``thickness`` (slab
|
|
20
|
+
depth/mass) and ``frost`` (surface roughness) — over the physical
|
|
21
|
+
:class:`GlassKernel`. :class:`GlassStyle` tunes the non-physical chrome.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
__version__ = "0.2.0"
|
|
27
|
+
|
|
28
|
+
from .backdrop import Backdrop, ScreenBackdrop, WidgetBackdrop, exclude_from_capture
|
|
29
|
+
from .effect import GlassRenderer, GlassStyle, paint_glass
|
|
30
|
+
from .glass import GlassPopup
|
|
31
|
+
from .pane import GlassPane, ui_font
|
|
32
|
+
from .refract import (
|
|
33
|
+
GlassKernel,
|
|
34
|
+
GlassMaterial,
|
|
35
|
+
array_to_qimage,
|
|
36
|
+
compute_glass,
|
|
37
|
+
qimage_to_array,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
# high-level widget
|
|
43
|
+
"GlassPane",
|
|
44
|
+
"GlassPopup",
|
|
45
|
+
# material / physics
|
|
46
|
+
"GlassMaterial",
|
|
47
|
+
"GlassKernel",
|
|
48
|
+
"compute_glass",
|
|
49
|
+
# rendering core
|
|
50
|
+
"GlassRenderer",
|
|
51
|
+
"GlassStyle",
|
|
52
|
+
"paint_glass",
|
|
53
|
+
# backdrops
|
|
54
|
+
"Backdrop",
|
|
55
|
+
"WidgetBackdrop",
|
|
56
|
+
"ScreenBackdrop",
|
|
57
|
+
"exclude_from_capture",
|
|
58
|
+
# helpers
|
|
59
|
+
"ui_font",
|
|
60
|
+
"qimage_to_array",
|
|
61
|
+
"array_to_qimage",
|
|
62
|
+
]
|