PyDiffGame 2.0.1__tar.gz → 2.0.2__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.
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/.github/workflows/python-publish.yml +44 -3
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/CLAUDE.md +15 -5
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/CONTRIBUTING.md +31 -6
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/PKG-INFO +1 -1
- pydiffgame-2.0.2/images/logo.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/pyproject.toml +1 -1
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/__init__.py +1 -1
- pydiffgame-2.0.2/tools/render_logo.py +295 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/uv.lock +1 -1
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/.github/workflows/tests.yml +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/.gitignore +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/.pre-commit-config.yaml +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/CITATIONS.bib +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/CODE_OF_CONDUCT.md +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/LICENSE +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/README.md +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/_config.yml +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/docs/README.md +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/images/Logo_ISTRC_Green_English.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/images/logo_abc.png +0 -0
- /pydiffgame-2.0.1/images/logo.png → /pydiffgame-2.0.2/images/logo_source.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/images/readme/masses_cost.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/images/readme/masses_game_vs_lqr.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/images/readme/masses_schematic.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/requirements.txt +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/_typing.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/base.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/comparison.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/continuous.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/discrete.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/InvertedPendulumComparison.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/MassesWithSpringsComparison.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/PVTOL.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/PVTOLComparison.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/QuadRotorControl.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/LQR_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/LQR_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/two_masses_tikz.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/LQR_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/LQR_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/LQR_large_1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/LQR_large_2.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL10.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL100.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL/PVTOL1000.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL0001.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL001.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL01.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/PVTOL1.png +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/lqr.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/objective.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/plotting.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/conftest.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_discrete.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_examples.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_game.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_lqr.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_objective.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tests/test_simulation.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tools/bump_version.py +0 -0
- {pydiffgame-2.0.1 → pydiffgame-2.0.2}/tools/generate_readme_figures.py +0 -0
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
# Publishes the package to PyPI and creates the matching GitHub Release,
|
|
2
|
-
# auto-incrementing the version
|
|
2
|
+
# auto-incrementing the version when source files change on master.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
4
|
+
# Triggers:
|
|
5
|
+
# - push to master that touches SOURCE files (the package or its packaging
|
|
6
|
+
# metadata) — docs / tests / tooling / CI changes do not cut a release.
|
|
7
|
+
# - workflow_dispatch: manual trigger remains available for on-demand re-runs
|
|
8
|
+
# (use this if you ever need to publish despite no source-file changes).
|
|
9
|
+
#
|
|
10
|
+
# Loop prevention: the workflow itself commits a "chore: bump version" commit
|
|
11
|
+
# back to master. Three independent guards stop that commit from re-triggering
|
|
12
|
+
# the workflow:
|
|
13
|
+
# 1. GitHub Actions natively skips push events whose head commit message
|
|
14
|
+
# contains "[skip ci]" (which the bump commit always does).
|
|
15
|
+
# 2. The bump-version job is guarded with an explicit `if:` so it only runs
|
|
16
|
+
# for human commits / manual dispatch, never for github-actions[bot].
|
|
17
|
+
# 3. The bump commit message itself contains "[skip ci]" so even if (1) and
|
|
18
|
+
# (2) ever break, the message still signals "do not republish".
|
|
19
|
+
#
|
|
20
|
+
# Concurrency: serialize runs so two pushes close together don't race on the
|
|
21
|
+
# bump commit's git push.
|
|
22
|
+
#
|
|
23
|
+
# Flow per run:
|
|
5
24
|
# 1. bump-version : increment the version (carry-at-9 via tools/bump_version.py),
|
|
6
|
-
# commit it back to master.
|
|
25
|
+
# commit it back to master with [skip ci].
|
|
7
26
|
# 2. release-build : build the dists from the bumped master.
|
|
8
27
|
# 3. pypi-publish : upload to PyPI via Trusted Publishing (OIDC, no tokens).
|
|
9
28
|
# 4. github-release: create the v<version> GitHub Release with notes + dists.
|
|
@@ -13,14 +32,36 @@
|
|
|
13
32
|
name: Upload Python Package
|
|
14
33
|
|
|
15
34
|
on:
|
|
35
|
+
push:
|
|
36
|
+
branches: [master]
|
|
37
|
+
# Only publish when source files change. Docs/tests/tooling commits do not
|
|
38
|
+
# cut a release; mixed commits do (the filter matches if ANY changed file
|
|
39
|
+
# matches).
|
|
40
|
+
paths:
|
|
41
|
+
- 'src/PyDiffGame/**'
|
|
42
|
+
- 'pyproject.toml'
|
|
16
43
|
workflow_dispatch:
|
|
17
44
|
|
|
18
45
|
permissions:
|
|
19
46
|
contents: read
|
|
20
47
|
|
|
48
|
+
concurrency:
|
|
49
|
+
group: publish-master
|
|
50
|
+
cancel-in-progress: false
|
|
51
|
+
|
|
21
52
|
jobs:
|
|
22
53
|
bump-version:
|
|
23
54
|
runs-on: ubuntu-latest
|
|
55
|
+
# Defense-in-depth loop guard: only release for human commits or manual
|
|
56
|
+
# dispatch — never for the workflow's own [skip ci] bump commit.
|
|
57
|
+
if: >-
|
|
58
|
+
${{
|
|
59
|
+
github.event_name == 'workflow_dispatch' ||
|
|
60
|
+
(
|
|
61
|
+
github.actor != 'github-actions[bot]' &&
|
|
62
|
+
!contains(github.event.head_commit.message, '[skip ci]')
|
|
63
|
+
)
|
|
64
|
+
}}
|
|
24
65
|
permissions:
|
|
25
66
|
contents: write
|
|
26
67
|
outputs:
|
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
do not bump it in ordinary PRs — the release run does it.
|
|
17
17
|
|
|
18
18
|
## Releasing
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
- **Continuous deployment, gated on source changes.** Every commit to `master`
|
|
20
|
+
that touches `src/PyDiffGame/**` or `pyproject.toml` automatically triggers
|
|
21
|
+
the publish workflow. Docs / tests / tooling / CI changes do **not** trigger
|
|
22
|
+
a release on their own; mixed commits do.
|
|
23
|
+
- The workflow auto-increments the version (`tools/bump_version.py`), commits
|
|
24
|
+
the bump to `master` as `chore: bump version to X.Y.Z [skip ci]`, builds with
|
|
25
|
+
`uv build`, publishes to PyPI via Trusted Publishing (OIDC, no tokens), and
|
|
26
|
+
creates the matching `v<version>` GitHub Release with notes and the built
|
|
27
|
+
dists attached. It is idempotent (`skip-existing`).
|
|
28
|
+
- Manual on-demand publish stays available via
|
|
29
|
+
`Actions -> Upload Python Package -> Run workflow` (`workflow_dispatch`).
|
|
30
|
+
- Three independent guards prevent the bump commit from re-triggering the
|
|
31
|
+
workflow (an infinite loop): `[skip ci]` in the message (which GitHub Actions
|
|
32
|
+
natively honors), an explicit job-level `if:` filtering out
|
|
33
|
+
`github-actions[bot]`, and `paths:` filter scoping to source files.
|
|
24
34
|
|
|
25
35
|
## Docs
|
|
26
36
|
- `README.md` is the single canonical readme and is also the PyPI long-description
|
|
@@ -41,15 +41,25 @@ they pass locally.
|
|
|
41
41
|
|
|
42
42
|
## Releasing
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
The project is on **continuous deployment for source changes**: every commit
|
|
45
|
+
to `master` that touches source files automatically cuts a new release.
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
A commit triggers a release when it changes any of:
|
|
48
|
+
|
|
49
|
+
- `src/PyDiffGame/**` — the package itself
|
|
50
|
+
- `pyproject.toml` — packaging metadata, dependencies, classifiers
|
|
51
|
+
|
|
52
|
+
Docs (`*.md`, `docs/**`), tests (`tests/**`), tooling (`tools/**`, `.github/**`,
|
|
53
|
+
`.pre-commit-config.yaml`), images and the lock file do **not** trigger a
|
|
54
|
+
release on their own. A mixed commit (e.g. `src/foo.py` + `README.md`) does
|
|
55
|
+
trigger one — the path filter matches if any changed file matches.
|
|
56
|
+
|
|
57
|
+
When a release-triggering commit lands on `master`, the publish workflow:
|
|
48
58
|
|
|
49
59
|
1. **Increments the version** with `tools/bump_version.py`, which rolls each
|
|
50
|
-
component over at 9 (`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0`),
|
|
51
|
-
`pyproject.toml` and `src/PyDiffGame/__init__.py`, and commits the bump
|
|
52
|
-
`master`.
|
|
60
|
+
component over at 9 (`2.0.9 -> 2.1.0`, `2.9.9 -> 3.0.0`), updates both
|
|
61
|
+
`pyproject.toml` and `src/PyDiffGame/__init__.py`, and commits the bump back
|
|
62
|
+
to `master` as `chore: bump version to X.Y.Z [skip ci]`.
|
|
53
63
|
2. Builds the distributions with `uv build`.
|
|
54
64
|
3. Uploads them to PyPI via
|
|
55
65
|
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
|
|
@@ -57,11 +67,26 @@ The run automatically:
|
|
|
57
67
|
4. Creates a `v<version>` GitHub Release with auto-generated notes and the built
|
|
58
68
|
wheels/sdist attached.
|
|
59
69
|
|
|
70
|
+
The workflow can also be triggered manually from **Actions -> Upload Python
|
|
71
|
+
Package -> Run workflow** if you ever need to re-run it.
|
|
72
|
+
|
|
60
73
|
You normally never edit the version by hand. To bump it locally (e.g. to test),
|
|
61
74
|
run `uv run python tools/bump_version.py` (`--dry-run` to preview, `--current`
|
|
62
75
|
to print the current version). The PyPI upload is idempotent (`skip-existing`),
|
|
63
76
|
so re-running the workflow is safe.
|
|
64
77
|
|
|
78
|
+
### What stops an infinite loop?
|
|
79
|
+
|
|
80
|
+
The publish workflow itself commits the version bump back to `master`. Three
|
|
81
|
+
independent guards stop that commit from re-triggering the workflow:
|
|
82
|
+
|
|
83
|
+
1. The bump commit message contains `[skip ci]`, which GitHub Actions natively
|
|
84
|
+
honors by **not creating a workflow run at all** for that push.
|
|
85
|
+
2. The `bump-version` job has an explicit `if:` that skips when the actor is
|
|
86
|
+
`github-actions[bot]` or the head-commit message contains `[skip ci]`.
|
|
87
|
+
3. `bump_version.py` is the single source of truth for the version, so a
|
|
88
|
+
tampered bump commit still wouldn't double-bump.
|
|
89
|
+
|
|
65
90
|
Thank you for your contribution!
|
|
66
91
|
|
|
67
92
|
Joshua Shay Kricheli
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyDiffGame
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Nash-equilibrium solutions to linear-quadratic differential games, via a reduction of the Game Hamilton-Jacobi-Bellman equations to coupled algebraic and differential Riccati equations for multi-objective dynamical control systems.
|
|
5
5
|
Project-URL: Homepage, https://krichelj.github.io/PyDiffGame/
|
|
6
6
|
Project-URL: Repository, https://github.com/krichelj/PyDiffGame
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ from PyDiffGame.discrete import DiscretePyDiffGame
|
|
|
34
34
|
from PyDiffGame.lqr import ContinuousLQR, DiscreteLQR
|
|
35
35
|
from PyDiffGame.objective import GameObjective, LQRObjective, Objective
|
|
36
36
|
|
|
37
|
-
__version__ = "2.0.
|
|
37
|
+
__version__ = "2.0.2"
|
|
38
38
|
|
|
39
39
|
__all__ = [
|
|
40
40
|
"PyDiffGame",
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Render a translucent, glossy-glass variant of the PyDiffGame logo.
|
|
2
|
+
|
|
3
|
+
Reads the original ``images/logo.png`` (red glasses-shaped mark + black
|
|
4
|
+
wordmark) and restyles each layer as polished glass: a vertical tonal gradient
|
|
5
|
+
that keeps the original palette (red mark, near-black wordmark), a soft
|
|
6
|
+
matched-colour outer glow, a top-edge inner highlight, a broad top-half gloss
|
|
7
|
+
band ("glaring" specular), and a crisp edge stroke. The transformation is
|
|
8
|
+
deterministic so iterations can be compared side-by-side.
|
|
9
|
+
|
|
10
|
+
Knobs are exposed via CLI flags so iterations can tune the look without
|
|
11
|
+
editing the file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from PIL import Image, ImageChops, ImageEnhance, ImageFilter
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
23
|
+
# Source is the immutable original (red mark + black wordmark) committed
|
|
24
|
+
# alongside this script. The renderer reads it and writes a styled copy
|
|
25
|
+
# (typically over images/logo.png) so the source survives restyles.
|
|
26
|
+
SRC = ROOT / "images" / "logo_source.png"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _split_mark_and_text(src: Image.Image) -> tuple[Image.Image, Image.Image]:
|
|
30
|
+
"""Split the original logo into (mark, wordmark) layers by colour.
|
|
31
|
+
|
|
32
|
+
The original logo has a red mark (the abstract glasses/molecule) and a
|
|
33
|
+
black wordmark "PYDIFFGAME". We separate them so the new style can treat
|
|
34
|
+
them differently (different hues, different translucency).
|
|
35
|
+
"""
|
|
36
|
+
rgba = np.asarray(src.convert("RGBA"), dtype=np.int16)
|
|
37
|
+
r, g, b, a = rgba[..., 0], rgba[..., 1], rgba[..., 2], rgba[..., 3]
|
|
38
|
+
# Red mark: red dominates and pixel is not transparent.
|
|
39
|
+
red_mask = (r > 120) & (r - g > 40) & (r - b > 40) & (a > 32)
|
|
40
|
+
# Black wordmark: low luminance, non-transparent.
|
|
41
|
+
luma = (r + g + b) // 3
|
|
42
|
+
black_mask = (luma < 90) & (a > 32) & ~red_mask
|
|
43
|
+
|
|
44
|
+
mark = np.zeros_like(rgba, dtype=np.uint8)
|
|
45
|
+
mark[red_mask] = [255, 255, 255, 255]
|
|
46
|
+
|
|
47
|
+
text = np.zeros_like(rgba, dtype=np.uint8)
|
|
48
|
+
text[black_mask] = [255, 255, 255, 255]
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
Image.fromarray(mark, "RGBA"),
|
|
52
|
+
Image.fromarray(text, "RGBA"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _tint(mask: Image.Image, color: tuple[int, int, int], alpha: int) -> Image.Image:
|
|
57
|
+
"""Tint a white-on-transparent mask with ``color`` at ``alpha`` opacity."""
|
|
58
|
+
arr = np.asarray(mask, dtype=np.uint8).copy()
|
|
59
|
+
a_in = arr[..., 3].astype(np.float32) / 255.0
|
|
60
|
+
out = np.zeros_like(arr)
|
|
61
|
+
out[..., 0] = color[0]
|
|
62
|
+
out[..., 1] = color[1]
|
|
63
|
+
out[..., 2] = color[2]
|
|
64
|
+
out[..., 3] = (a_in * alpha).astype(np.uint8)
|
|
65
|
+
return Image.fromarray(out, "RGBA")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _gradient_fill(
|
|
69
|
+
mask: Image.Image,
|
|
70
|
+
top_color: tuple[int, int, int],
|
|
71
|
+
bottom_color: tuple[int, int, int],
|
|
72
|
+
alpha: int,
|
|
73
|
+
) -> Image.Image:
|
|
74
|
+
"""Fill ``mask`` with a vertical gradient anchored to the mask's bbox.
|
|
75
|
+
|
|
76
|
+
Pixels above the bbox get ``top_color``, pixels below get ``bottom_color``,
|
|
77
|
+
and within the bbox the colour interpolates linearly top→bottom. The
|
|
78
|
+
output alpha is the mask alpha scaled by ``alpha``.
|
|
79
|
+
"""
|
|
80
|
+
w, h = mask.size
|
|
81
|
+
a = np.asarray(mask, dtype=np.uint8)[..., 3]
|
|
82
|
+
bbox = mask.getbbox()
|
|
83
|
+
if bbox is None:
|
|
84
|
+
return Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
85
|
+
y0, y1 = bbox[1], bbox[3]
|
|
86
|
+
ys = np.arange(h, dtype=np.float32)
|
|
87
|
+
t = np.clip((ys - y0) / max(y1 - y0, 1), 0.0, 1.0)
|
|
88
|
+
top_arr = np.array(top_color, dtype=np.float32)
|
|
89
|
+
bot_arr = np.array(bottom_color, dtype=np.float32)
|
|
90
|
+
line = top_arr * (1 - t)[:, None] + bot_arr * t[:, None]
|
|
91
|
+
rgb = np.broadcast_to(line[:, None, :], (h, w, 3)).astype(np.uint8)
|
|
92
|
+
a_norm = a.astype(np.float32) / 255.0
|
|
93
|
+
out_alpha = (a_norm * (alpha / 255.0) * 255.0).astype(np.uint8)
|
|
94
|
+
out = np.zeros((h, w, 4), dtype=np.uint8)
|
|
95
|
+
out[..., :3] = rgb
|
|
96
|
+
out[..., 3] = out_alpha
|
|
97
|
+
return Image.fromarray(out, "RGBA")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _outer_glow(
|
|
101
|
+
mask: Image.Image,
|
|
102
|
+
color: tuple[int, int, int],
|
|
103
|
+
radius: float,
|
|
104
|
+
alpha: int,
|
|
105
|
+
) -> Image.Image:
|
|
106
|
+
"""Soft outer glow built by blurring a tinted copy of the mask."""
|
|
107
|
+
glow = _tint(mask, color, alpha)
|
|
108
|
+
return glow.filter(ImageFilter.GaussianBlur(radius=radius))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _inner_highlight(mask: Image.Image, offset: int, alpha: int) -> Image.Image:
|
|
112
|
+
"""Top-edge inner highlight to suggest glass curvature."""
|
|
113
|
+
base = np.asarray(mask, dtype=np.uint8)
|
|
114
|
+
a = base[..., 3]
|
|
115
|
+
shifted = np.zeros_like(a)
|
|
116
|
+
shifted[offset:, :] = a[:-offset, :]
|
|
117
|
+
inner = np.minimum(a, 255 - shifted)
|
|
118
|
+
out = np.zeros_like(base)
|
|
119
|
+
out[..., 0] = 255
|
|
120
|
+
out[..., 1] = 255
|
|
121
|
+
out[..., 2] = 255
|
|
122
|
+
out[..., 3] = (inner.astype(np.float32) / 255.0 * alpha).astype(np.uint8)
|
|
123
|
+
img = Image.fromarray(out, "RGBA").filter(ImageFilter.GaussianBlur(radius=0.8))
|
|
124
|
+
return img
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _gloss_band(
|
|
128
|
+
mask: Image.Image,
|
|
129
|
+
height_frac: float,
|
|
130
|
+
alpha: int,
|
|
131
|
+
feather: float,
|
|
132
|
+
color: tuple[int, int, int] = (255, 255, 255),
|
|
133
|
+
) -> Image.Image:
|
|
134
|
+
"""Broad top-half specular gloss clipped to the shape (the "glare").
|
|
135
|
+
|
|
136
|
+
Builds a vertical falloff that is fully opaque at the top of the mask's
|
|
137
|
+
bounding box and fades to zero ``height_frac`` of the way down it, then
|
|
138
|
+
multiplies that falloff by the shape's own alpha so the gloss only
|
|
139
|
+
appears on the shape's surface. A small Gaussian blur softens the edge.
|
|
140
|
+
"""
|
|
141
|
+
a = np.asarray(mask, dtype=np.uint8)[..., 3]
|
|
142
|
+
bbox = mask.getbbox()
|
|
143
|
+
if bbox is None:
|
|
144
|
+
return Image.new("RGBA", mask.size, (0, 0, 0, 0))
|
|
145
|
+
y0, y1 = bbox[1], bbox[3]
|
|
146
|
+
h = max(y1 - y0, 1)
|
|
147
|
+
band_h = max(int(h * height_frac), 1)
|
|
148
|
+
|
|
149
|
+
falloff = np.zeros_like(a, dtype=np.float32)
|
|
150
|
+
ys = np.arange(a.shape[0], dtype=np.float32)
|
|
151
|
+
weight = np.clip(1.0 - (ys - y0) / band_h, 0.0, 1.0)
|
|
152
|
+
falloff[:] = weight[:, None]
|
|
153
|
+
|
|
154
|
+
a_norm = a.astype(np.float32) / 255.0
|
|
155
|
+
out_alpha = (a_norm * falloff * (alpha / 255.0) * 255.0).astype(np.uint8)
|
|
156
|
+
|
|
157
|
+
out = np.zeros((*a.shape, 4), dtype=np.uint8)
|
|
158
|
+
out[..., 0] = color[0]
|
|
159
|
+
out[..., 1] = color[1]
|
|
160
|
+
out[..., 2] = color[2]
|
|
161
|
+
out[..., 3] = out_alpha
|
|
162
|
+
img = Image.fromarray(out, "RGBA")
|
|
163
|
+
if feather > 0:
|
|
164
|
+
img = img.filter(ImageFilter.GaussianBlur(radius=feather))
|
|
165
|
+
return img
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _stroke(mask: Image.Image, color: tuple[int, int, int], alpha: int, width: int = 1) -> Image.Image:
|
|
169
|
+
"""Crisp outline around the shape (used for glass-edge feel)."""
|
|
170
|
+
a = np.asarray(mask, dtype=np.uint8)[..., 3]
|
|
171
|
+
eroded = Image.fromarray(a, "L").filter(ImageFilter.MinFilter(2 * width + 1))
|
|
172
|
+
edge = ImageChops.subtract(Image.fromarray(a, "L"), eroded)
|
|
173
|
+
out = np.zeros((*a.shape, 4), dtype=np.uint8)
|
|
174
|
+
out[..., 0] = color[0]
|
|
175
|
+
out[..., 1] = color[1]
|
|
176
|
+
out[..., 2] = color[2]
|
|
177
|
+
out[..., 3] = (np.asarray(edge, dtype=np.float32) / 255.0 * alpha).astype(np.uint8)
|
|
178
|
+
return Image.fromarray(out, "RGBA")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render(
|
|
182
|
+
out_path: Path,
|
|
183
|
+
*,
|
|
184
|
+
mark_top: tuple[int, int, int] = (255, 95, 105),
|
|
185
|
+
mark_bottom: tuple[int, int, int] = (170, 10, 25),
|
|
186
|
+
text_top: tuple[int, int, int] = (8, 8, 12),
|
|
187
|
+
text_bottom: tuple[int, int, int] = (0, 0, 5),
|
|
188
|
+
mark_fill_alpha: int = 230,
|
|
189
|
+
text_fill_alpha: int = 252,
|
|
190
|
+
mark_glow_alpha: int = 140,
|
|
191
|
+
text_glow_alpha: int = 80,
|
|
192
|
+
mark_glow_radius: float = 5.5,
|
|
193
|
+
text_glow_radius: float = 2.8,
|
|
194
|
+
highlight_alpha: int = 235,
|
|
195
|
+
stroke_alpha: int = 200,
|
|
196
|
+
mark_gloss_alpha: int = 90,
|
|
197
|
+
text_gloss_alpha: int = 70,
|
|
198
|
+
gloss_height: float = 0.5,
|
|
199
|
+
gloss_feather: float = 3.5,
|
|
200
|
+
background: str = "transparent",
|
|
201
|
+
) -> Path:
|
|
202
|
+
"""Render the translucent cool-toned logo and save to ``out_path``."""
|
|
203
|
+
src = Image.open(SRC).convert("RGBA")
|
|
204
|
+
w, h = src.size
|
|
205
|
+
mark_mask, text_mask = _split_mark_and_text(src)
|
|
206
|
+
|
|
207
|
+
# Canvas (transparent or dark backdrop so glow reads in previews).
|
|
208
|
+
if background == "dark":
|
|
209
|
+
canvas = Image.new("RGBA", (w, h), (8, 14, 28, 255))
|
|
210
|
+
elif background == "light":
|
|
211
|
+
canvas = Image.new("RGBA", (w, h), (245, 248, 255, 255))
|
|
212
|
+
else:
|
|
213
|
+
canvas = Image.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
214
|
+
|
|
215
|
+
# --- Mark (glasses) ---
|
|
216
|
+
# Order: outer glow (rim light) -> bbox-anchored gradient fill -> top
|
|
217
|
+
# gloss band (the broad specular glare) -> rim edge stroke -> crisp
|
|
218
|
+
# top-edge inner highlight on top.
|
|
219
|
+
mark_fill = _gradient_fill(mark_mask, mark_top, mark_bottom, mark_fill_alpha)
|
|
220
|
+
mark_glow = _outer_glow(mark_mask, mark_bottom, mark_glow_radius, mark_glow_alpha)
|
|
221
|
+
mark_gloss = _gloss_band(mark_mask, gloss_height, mark_gloss_alpha, gloss_feather)
|
|
222
|
+
mark_stroke = _stroke(mark_mask, mark_bottom, stroke_alpha, width=1)
|
|
223
|
+
mark_highlight = _inner_highlight(mark_mask, offset=3, alpha=highlight_alpha)
|
|
224
|
+
|
|
225
|
+
canvas.alpha_composite(mark_glow)
|
|
226
|
+
canvas.alpha_composite(mark_fill)
|
|
227
|
+
canvas.alpha_composite(mark_gloss)
|
|
228
|
+
canvas.alpha_composite(mark_stroke)
|
|
229
|
+
canvas.alpha_composite(mark_highlight)
|
|
230
|
+
|
|
231
|
+
# --- Wordmark ---
|
|
232
|
+
text_fill = _gradient_fill(text_mask, text_top, text_bottom, text_fill_alpha)
|
|
233
|
+
text_glow = _outer_glow(text_mask, text_bottom, text_glow_radius, text_glow_alpha)
|
|
234
|
+
text_gloss = _gloss_band(text_mask, gloss_height, text_gloss_alpha, gloss_feather)
|
|
235
|
+
text_stroke = _stroke(text_mask, text_bottom, max(stroke_alpha - 40, 0), width=1)
|
|
236
|
+
text_highlight = _inner_highlight(text_mask, offset=2, alpha=int(highlight_alpha * 0.75))
|
|
237
|
+
|
|
238
|
+
canvas.alpha_composite(text_glow)
|
|
239
|
+
canvas.alpha_composite(text_fill)
|
|
240
|
+
canvas.alpha_composite(text_gloss)
|
|
241
|
+
canvas.alpha_composite(text_stroke)
|
|
242
|
+
canvas.alpha_composite(text_highlight)
|
|
243
|
+
|
|
244
|
+
# Light overall sharpen so glass edges stay crisp after blurs.
|
|
245
|
+
sharpened = ImageEnhance.Sharpness(canvas).enhance(1.15)
|
|
246
|
+
sharpened.save(out_path)
|
|
247
|
+
return out_path
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse() -> argparse.Namespace:
|
|
251
|
+
p = argparse.ArgumentParser()
|
|
252
|
+
p.add_argument("--out", type=Path, required=True)
|
|
253
|
+
p.add_argument("--background", choices=["transparent", "dark", "light"], default="transparent")
|
|
254
|
+
p.add_argument("--mark-top", nargs=3, type=int, default=[255, 95, 105])
|
|
255
|
+
p.add_argument("--mark-bottom", nargs=3, type=int, default=[170, 10, 25])
|
|
256
|
+
p.add_argument("--text-top", nargs=3, type=int, default=[8, 8, 12])
|
|
257
|
+
p.add_argument("--text-bottom", nargs=3, type=int, default=[0, 0, 5])
|
|
258
|
+
p.add_argument("--mark-fill-alpha", type=int, default=230)
|
|
259
|
+
p.add_argument("--text-fill-alpha", type=int, default=252)
|
|
260
|
+
p.add_argument("--mark-glow-alpha", type=int, default=140)
|
|
261
|
+
p.add_argument("--text-glow-alpha", type=int, default=80)
|
|
262
|
+
p.add_argument("--mark-glow-radius", type=float, default=5.5)
|
|
263
|
+
p.add_argument("--text-glow-radius", type=float, default=2.8)
|
|
264
|
+
p.add_argument("--highlight-alpha", type=int, default=235)
|
|
265
|
+
p.add_argument("--stroke-alpha", type=int, default=200)
|
|
266
|
+
p.add_argument("--mark-gloss-alpha", type=int, default=90)
|
|
267
|
+
p.add_argument("--text-gloss-alpha", type=int, default=70)
|
|
268
|
+
p.add_argument("--gloss-height", type=float, default=0.5)
|
|
269
|
+
p.add_argument("--gloss-feather", type=float, default=3.5)
|
|
270
|
+
return p.parse_args()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
args = _parse()
|
|
275
|
+
render(
|
|
276
|
+
args.out,
|
|
277
|
+
background=args.background,
|
|
278
|
+
mark_top=tuple(args.mark_top),
|
|
279
|
+
mark_bottom=tuple(args.mark_bottom),
|
|
280
|
+
text_top=tuple(args.text_top),
|
|
281
|
+
text_bottom=tuple(args.text_bottom),
|
|
282
|
+
mark_fill_alpha=args.mark_fill_alpha,
|
|
283
|
+
text_fill_alpha=args.text_fill_alpha,
|
|
284
|
+
mark_glow_alpha=args.mark_glow_alpha,
|
|
285
|
+
text_glow_alpha=args.text_glow_alpha,
|
|
286
|
+
mark_glow_radius=args.mark_glow_radius,
|
|
287
|
+
text_glow_radius=args.text_glow_radius,
|
|
288
|
+
highlight_alpha=args.highlight_alpha,
|
|
289
|
+
stroke_alpha=args.stroke_alpha,
|
|
290
|
+
mark_gloss_alpha=args.mark_gloss_alpha,
|
|
291
|
+
text_gloss_alpha=args.text_gloss_alpha,
|
|
292
|
+
gloss_height=args.gloss_height,
|
|
293
|
+
gloss_feather=args.gloss_feather,
|
|
294
|
+
)
|
|
295
|
+
print(f"wrote {args.out}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/MassesWithSpringsComparison.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_1.png
RENAMED
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/2/2-players_large_2.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_1.png
RENAMED
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/4/4-players_large_2.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_1.png
RENAMED
|
File without changes
|
{pydiffgame-2.0.1 → pydiffgame-2.0.2}/src/PyDiffGame/examples/figures/8/8-players_large_2.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|