pixel-space-asset-toolkit 0.1.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.
- pixel_space_asset_toolkit-0.1.0/LICENSE +21 -0
- pixel_space_asset_toolkit-0.1.0/PKG-INFO +86 -0
- pixel_space_asset_toolkit-0.1.0/README.md +63 -0
- pixel_space_asset_toolkit-0.1.0/pyproject.toml +41 -0
- pixel_space_asset_toolkit-0.1.0/setup.cfg +4 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/PKG-INFO +86 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/SOURCES.txt +20 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/dependency_links.txt +1 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/entry_points.txt +2 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/requires.txt +1 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_asset_toolkit.egg-info/top_level.txt +1 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/__init__.py +3 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/__main__.py +5 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/asteroids.py +58 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/cli.py +176 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/preview.py +21 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/starfield.py +31 -0
- pixel_space_asset_toolkit-0.1.0/src/pixel_space_assets/strip_background.py +21 -0
- pixel_space_asset_toolkit-0.1.0/tests/test_asteroids.py +23 -0
- pixel_space_asset_toolkit-0.1.0/tests/test_background_preview.py +33 -0
- pixel_space_asset_toolkit-0.1.0/tests/test_cli.py +81 -0
- pixel_space_asset_toolkit-0.1.0/tests/test_starfield.py +20 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pixel Space Asset Toolkit contributors
|
|
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,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pixel-space-asset-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic pixel-art sci-fi asset utilities for starfields, asteroid tiles, cleanup, and previews.
|
|
5
|
+
Author: Pixel Space Asset Toolkit contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/pixel-space-asset-toolkit
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: pixel-art,procedural,godot,space,assets,gamedev
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Classifier: Topic :: Games/Entertainment
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: Pillow>=10
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Pixel Space Asset Toolkit
|
|
25
|
+
|
|
26
|
+
Deterministic procedural tools for pixel-art sci-fi prototypes: starfields, hex asteroid tiles, transparent-background cleanup, preview sheets, and Godot import guidance.
|
|
27
|
+
|
|
28
|
+
This repo is intentionally more visual than the audit tools, but it still behaves like a production CLI: fixed seeds, manifests, tests, and repeatable outputs.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
python -m pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
When published:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
python -m pip install pixel-space-asset-toolkit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --manifest generated\starfield.json
|
|
46
|
+
pixel-space-assets asteroid-hex --material ferric --count 32 --size 64 --seed 7 --output generated\ferric
|
|
47
|
+
pixel-space-assets strip-background input.png --output cleaned.png --tolerance 4
|
|
48
|
+
pixel-space-assets preview generated\ferric --columns 8 --cell-size 64 --output generated\ferric_preview.png
|
|
49
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --format json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
- `starfield`: deterministic pixel starfield backgrounds.
|
|
55
|
+
- `asteroid-hex`: deterministic transparent hex asteroid tiles.
|
|
56
|
+
- `strip-background`: converts a flat corner-color background to alpha.
|
|
57
|
+
- `preview`: builds contact sheets for review.
|
|
58
|
+
|
|
59
|
+
## Sample Gallery
|
|
60
|
+
|
|
61
|
+
Generated from fixed seeds:
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
Open `examples/gallery/index.html` for a simple static gallery view.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [Starfields](docs/STARFIELDS.md)
|
|
72
|
+
- [Asteroids](docs/ASTEROIDS.md)
|
|
73
|
+
- [Background stripping](docs/BACKGROUND_STRIPPING.md)
|
|
74
|
+
- [Preview sheets](docs/PREVIEWS.md)
|
|
75
|
+
- [Godot import guide](docs/GODOT_IMPORT.md)
|
|
76
|
+
- [CI usage](docs/CI.md)
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
python -m pip install -e .
|
|
82
|
+
python -m unittest discover -s tests -v
|
|
83
|
+
pixel-space-assets starfield --width 64 --height 64 --seed 1 --stars 20 --output generated\starfield.png
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Examples are generic and reproducible. Private project-specific art should stay outside the published repository unless intentionally released.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Pixel Space Asset Toolkit
|
|
2
|
+
|
|
3
|
+
Deterministic procedural tools for pixel-art sci-fi prototypes: starfields, hex asteroid tiles, transparent-background cleanup, preview sheets, and Godot import guidance.
|
|
4
|
+
|
|
5
|
+
This repo is intentionally more visual than the audit tools, but it still behaves like a production CLI: fixed seeds, manifests, tests, and repeatable outputs.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
python -m pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When published:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
python -m pip install pixel-space-asset-toolkit
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --manifest generated\starfield.json
|
|
23
|
+
pixel-space-assets asteroid-hex --material ferric --count 32 --size 64 --seed 7 --output generated\ferric
|
|
24
|
+
pixel-space-assets strip-background input.png --output cleaned.png --tolerance 4
|
|
25
|
+
pixel-space-assets preview generated\ferric --columns 8 --cell-size 64 --output generated\ferric_preview.png
|
|
26
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --format json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
- `starfield`: deterministic pixel starfield backgrounds.
|
|
32
|
+
- `asteroid-hex`: deterministic transparent hex asteroid tiles.
|
|
33
|
+
- `strip-background`: converts a flat corner-color background to alpha.
|
|
34
|
+
- `preview`: builds contact sheets for review.
|
|
35
|
+
|
|
36
|
+
## Sample Gallery
|
|
37
|
+
|
|
38
|
+
Generated from fixed seeds:
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
Open `examples/gallery/index.html` for a simple static gallery view.
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
- [Starfields](docs/STARFIELDS.md)
|
|
49
|
+
- [Asteroids](docs/ASTEROIDS.md)
|
|
50
|
+
- [Background stripping](docs/BACKGROUND_STRIPPING.md)
|
|
51
|
+
- [Preview sheets](docs/PREVIEWS.md)
|
|
52
|
+
- [Godot import guide](docs/GODOT_IMPORT.md)
|
|
53
|
+
- [CI usage](docs/CI.md)
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```powershell
|
|
58
|
+
python -m pip install -e .
|
|
59
|
+
python -m unittest discover -s tests -v
|
|
60
|
+
pixel-space-assets starfield --width 64 --height 64 --seed 1 --stars 20 --output generated\starfield.png
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Examples are generic and reproducible. Private project-specific art should stay outside the published repository unless intentionally released.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pixel-space-asset-toolkit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Deterministic pixel-art sci-fi asset utilities for starfields, asteroid tiles, cleanup, and previews."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Pixel Space Asset Toolkit contributors" }]
|
|
13
|
+
keywords = ["pixel-art", "procedural", "godot", "space", "assets", "gamedev"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Multimedia :: Graphics",
|
|
22
|
+
"Topic :: Games/Entertainment"
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["Pillow>=10"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/NonniGB/godot-production-toolkit/tree/main/pixel-space-asset-toolkit"
|
|
28
|
+
Issues = "https://github.com/NonniGB/godot-production-toolkit/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
pixel-space-assets = "pixel_space_assets.cli:entrypoint"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pixel-space-asset-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic pixel-art sci-fi asset utilities for starfields, asteroid tiles, cleanup, and previews.
|
|
5
|
+
Author: Pixel Space Asset Toolkit contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/pixel-space-asset-toolkit
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: pixel-art,procedural,godot,space,assets,gamedev
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
17
|
+
Classifier: Topic :: Games/Entertainment
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: Pillow>=10
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# Pixel Space Asset Toolkit
|
|
25
|
+
|
|
26
|
+
Deterministic procedural tools for pixel-art sci-fi prototypes: starfields, hex asteroid tiles, transparent-background cleanup, preview sheets, and Godot import guidance.
|
|
27
|
+
|
|
28
|
+
This repo is intentionally more visual than the audit tools, but it still behaves like a production CLI: fixed seeds, manifests, tests, and repeatable outputs.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
python -m pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
When published:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
python -m pip install pixel-space-asset-toolkit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --manifest generated\starfield.json
|
|
46
|
+
pixel-space-assets asteroid-hex --material ferric --count 32 --size 64 --seed 7 --output generated\ferric
|
|
47
|
+
pixel-space-assets strip-background input.png --output cleaned.png --tolerance 4
|
|
48
|
+
pixel-space-assets preview generated\ferric --columns 8 --cell-size 64 --output generated\ferric_preview.png
|
|
49
|
+
pixel-space-assets starfield --width 1080 --height 1920 --seed 42 --stars 900 --output generated\starfield.png --format json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tools
|
|
53
|
+
|
|
54
|
+
- `starfield`: deterministic pixel starfield backgrounds.
|
|
55
|
+
- `asteroid-hex`: deterministic transparent hex asteroid tiles.
|
|
56
|
+
- `strip-background`: converts a flat corner-color background to alpha.
|
|
57
|
+
- `preview`: builds contact sheets for review.
|
|
58
|
+
|
|
59
|
+
## Sample Gallery
|
|
60
|
+
|
|
61
|
+
Generated from fixed seeds:
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
Open `examples/gallery/index.html` for a simple static gallery view.
|
|
68
|
+
|
|
69
|
+
## Documentation
|
|
70
|
+
|
|
71
|
+
- [Starfields](docs/STARFIELDS.md)
|
|
72
|
+
- [Asteroids](docs/ASTEROIDS.md)
|
|
73
|
+
- [Background stripping](docs/BACKGROUND_STRIPPING.md)
|
|
74
|
+
- [Preview sheets](docs/PREVIEWS.md)
|
|
75
|
+
- [Godot import guide](docs/GODOT_IMPORT.md)
|
|
76
|
+
- [CI usage](docs/CI.md)
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```powershell
|
|
81
|
+
python -m pip install -e .
|
|
82
|
+
python -m unittest discover -s tests -v
|
|
83
|
+
pixel-space-assets starfield --width 64 --height 64 --seed 1 --stars 20 --output generated\starfield.png
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Examples are generic and reproducible. Private project-specific art should stay outside the published repository unless intentionally released.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pixel_space_asset_toolkit.egg-info/PKG-INFO
|
|
5
|
+
src/pixel_space_asset_toolkit.egg-info/SOURCES.txt
|
|
6
|
+
src/pixel_space_asset_toolkit.egg-info/dependency_links.txt
|
|
7
|
+
src/pixel_space_asset_toolkit.egg-info/entry_points.txt
|
|
8
|
+
src/pixel_space_asset_toolkit.egg-info/requires.txt
|
|
9
|
+
src/pixel_space_asset_toolkit.egg-info/top_level.txt
|
|
10
|
+
src/pixel_space_assets/__init__.py
|
|
11
|
+
src/pixel_space_assets/__main__.py
|
|
12
|
+
src/pixel_space_assets/asteroids.py
|
|
13
|
+
src/pixel_space_assets/cli.py
|
|
14
|
+
src/pixel_space_assets/preview.py
|
|
15
|
+
src/pixel_space_assets/starfield.py
|
|
16
|
+
src/pixel_space_assets/strip_background.py
|
|
17
|
+
tests/test_asteroids.py
|
|
18
|
+
tests/test_background_preview.py
|
|
19
|
+
tests/test_cli.py
|
|
20
|
+
tests/test_starfield.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=10
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pixel_space_assets
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageDraw
|
|
9
|
+
|
|
10
|
+
PALETTES = {
|
|
11
|
+
"ferric": [(63, 43, 47, 255), (126, 72, 57, 255), (214, 117, 77, 255), (255, 188, 112, 255)],
|
|
12
|
+
"ice": [(37, 58, 82, 255), (72, 118, 145, 255), (145, 214, 226, 255), (232, 255, 255, 255)],
|
|
13
|
+
"carbon": [(20, 24, 32, 255), (48, 55, 69, 255), (92, 102, 118, 255), (170, 186, 202, 255)],
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def generate_asteroid_tiles(
|
|
18
|
+
output: Path,
|
|
19
|
+
*,
|
|
20
|
+
material: str,
|
|
21
|
+
count: int,
|
|
22
|
+
size: int,
|
|
23
|
+
seed: int,
|
|
24
|
+
) -> dict[str, object]:
|
|
25
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
palette = PALETTES.get(material, PALETTES["carbon"])
|
|
27
|
+
tiles = []
|
|
28
|
+
for index in range(count):
|
|
29
|
+
tile_seed = seed + index
|
|
30
|
+
image = _generate_tile(size=size, palette=palette, seed=tile_seed)
|
|
31
|
+
filename = f"{material}_{index:03d}.png"
|
|
32
|
+
image.save(output / filename)
|
|
33
|
+
tiles.append({"file": filename, "seed": tile_seed})
|
|
34
|
+
manifest = {"type": "asteroid-hex", "material": material, "count": count, "size": size, "seed": seed, "tiles": tiles}
|
|
35
|
+
(output / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
|
|
36
|
+
return manifest
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _generate_tile(*, size: int, palette: list[tuple[int, int, int, int]], seed: int) -> Image.Image:
|
|
40
|
+
rng = random.Random(seed)
|
|
41
|
+
image = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
42
|
+
draw = ImageDraw.Draw(image)
|
|
43
|
+
center = (size / 2, size / 2)
|
|
44
|
+
radius = size * rng.uniform(0.34, 0.44)
|
|
45
|
+
points = []
|
|
46
|
+
for side in range(6):
|
|
47
|
+
angle = math.radians(60 * side - 30)
|
|
48
|
+
jitter = rng.uniform(0.82, 1.12)
|
|
49
|
+
points.append((center[0] + math.cos(angle) * radius * jitter, center[1] + math.sin(angle) * radius * jitter))
|
|
50
|
+
draw.polygon(points, fill=palette[1], outline=palette[3])
|
|
51
|
+
for _ in range(max(3, size // 8)):
|
|
52
|
+
x = rng.randrange(size)
|
|
53
|
+
y = rng.randrange(size)
|
|
54
|
+
if image.getpixel((x, y))[3] == 0:
|
|
55
|
+
continue
|
|
56
|
+
color = rng.choice(palette)
|
|
57
|
+
draw.rectangle((x, y, min(size - 1, x + rng.randrange(1, 4)), min(size - 1, y + rng.randrange(1, 4))), fill=color)
|
|
58
|
+
return image
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from .asteroids import generate_asteroid_tiles
|
|
11
|
+
from .preview import build_contact_sheet
|
|
12
|
+
from .starfield import generate_starfield
|
|
13
|
+
from .strip_background import strip_background
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str] | None = None) -> int:
|
|
17
|
+
parser = _build_parser()
|
|
18
|
+
args = parser.parse_args(argv)
|
|
19
|
+
if args.command == "starfield":
|
|
20
|
+
return _starfield(args)
|
|
21
|
+
if args.command == "asteroid-hex":
|
|
22
|
+
manifest = generate_asteroid_tiles(
|
|
23
|
+
Path(args.output),
|
|
24
|
+
material=args.material,
|
|
25
|
+
count=args.count,
|
|
26
|
+
size=args.size,
|
|
27
|
+
seed=args.seed,
|
|
28
|
+
)
|
|
29
|
+
_emit_status(
|
|
30
|
+
args,
|
|
31
|
+
{
|
|
32
|
+
"status": "ok",
|
|
33
|
+
"command": "asteroid-hex",
|
|
34
|
+
"outputs": {
|
|
35
|
+
"directory": str(Path(args.output)),
|
|
36
|
+
"manifest": str(Path(args.output) / "manifest.json"),
|
|
37
|
+
},
|
|
38
|
+
"manifest": manifest,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
return 0
|
|
42
|
+
if args.command == "strip-background":
|
|
43
|
+
return _strip_background(args)
|
|
44
|
+
if args.command == "preview":
|
|
45
|
+
return _preview(args)
|
|
46
|
+
parser.print_help()
|
|
47
|
+
return 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def entrypoint() -> None:
|
|
51
|
+
raise SystemExit(main())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
55
|
+
parser = argparse.ArgumentParser(prog="pixel-space-assets", description="Deterministic pixel-space asset tools.")
|
|
56
|
+
parser.add_argument("--version", action="version", version="pixel-space-assets 0.1.0")
|
|
57
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
58
|
+
|
|
59
|
+
starfield = subparsers.add_parser("starfield")
|
|
60
|
+
starfield.add_argument("--width", type=int, required=True)
|
|
61
|
+
starfield.add_argument("--height", type=int, required=True)
|
|
62
|
+
starfield.add_argument("--seed", type=int, required=True)
|
|
63
|
+
starfield.add_argument("--stars", type=int, default=200)
|
|
64
|
+
starfield.add_argument("--output", required=True)
|
|
65
|
+
starfield.add_argument("--manifest")
|
|
66
|
+
starfield.add_argument("--format", choices=["text", "json"], default="text")
|
|
67
|
+
|
|
68
|
+
asteroid = subparsers.add_parser("asteroid-hex")
|
|
69
|
+
asteroid.add_argument("--material", default="carbon", choices=["carbon", "ferric", "ice"])
|
|
70
|
+
asteroid.add_argument("--count", type=int, default=16)
|
|
71
|
+
asteroid.add_argument("--size", type=int, default=64)
|
|
72
|
+
asteroid.add_argument("--seed", type=int, default=1)
|
|
73
|
+
asteroid.add_argument("--output", required=True)
|
|
74
|
+
asteroid.add_argument("--format", choices=["text", "json"], default="text")
|
|
75
|
+
|
|
76
|
+
strip = subparsers.add_parser("strip-background")
|
|
77
|
+
strip.add_argument("input")
|
|
78
|
+
strip.add_argument("--output", required=True)
|
|
79
|
+
strip.add_argument("--tolerance", type=int, default=0)
|
|
80
|
+
strip.add_argument("--format", choices=["text", "json"], default="text")
|
|
81
|
+
|
|
82
|
+
preview = subparsers.add_parser("preview")
|
|
83
|
+
preview.add_argument("input")
|
|
84
|
+
preview.add_argument("--output", required=True)
|
|
85
|
+
preview.add_argument("--columns", type=int, default=4)
|
|
86
|
+
preview.add_argument("--cell-size", type=int, default=64)
|
|
87
|
+
preview.add_argument("--format", choices=["text", "json"], default="text")
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _starfield(args: argparse.Namespace) -> int:
|
|
92
|
+
output = Path(args.output)
|
|
93
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
image = generate_starfield(width=args.width, height=args.height, seed=args.seed, stars=args.stars)
|
|
95
|
+
image.save(output)
|
|
96
|
+
if args.manifest:
|
|
97
|
+
manifest = {
|
|
98
|
+
"type": "starfield",
|
|
99
|
+
"width": args.width,
|
|
100
|
+
"height": args.height,
|
|
101
|
+
"seed": args.seed,
|
|
102
|
+
"stars": args.stars,
|
|
103
|
+
"output": str(output),
|
|
104
|
+
}
|
|
105
|
+
manifest_path = Path(args.manifest)
|
|
106
|
+
manifest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
|
|
108
|
+
_emit_status(
|
|
109
|
+
args,
|
|
110
|
+
{
|
|
111
|
+
"status": "ok",
|
|
112
|
+
"command": "starfield",
|
|
113
|
+
"outputs": {
|
|
114
|
+
"image": str(output),
|
|
115
|
+
"manifest": str(Path(args.manifest)) if args.manifest else None,
|
|
116
|
+
},
|
|
117
|
+
"parameters": {
|
|
118
|
+
"width": args.width,
|
|
119
|
+
"height": args.height,
|
|
120
|
+
"seed": args.seed,
|
|
121
|
+
"stars": args.stars,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _strip_background(args: argparse.Namespace) -> int:
|
|
129
|
+
with Image.open(args.input) as image:
|
|
130
|
+
stripped = strip_background(image, tolerance=args.tolerance)
|
|
131
|
+
output = Path(args.output)
|
|
132
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
stripped.save(output)
|
|
134
|
+
_emit_status(
|
|
135
|
+
args,
|
|
136
|
+
{
|
|
137
|
+
"status": "ok",
|
|
138
|
+
"command": "strip-background",
|
|
139
|
+
"outputs": {"image": str(output)},
|
|
140
|
+
"parameters": {"input": args.input, "tolerance": args.tolerance},
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _preview(args: argparse.Namespace) -> int:
|
|
147
|
+
root = Path(args.input)
|
|
148
|
+
paths = sorted(root.glob("*.png")) if root.is_dir() else [root]
|
|
149
|
+
sheet = build_contact_sheet(paths, columns=args.columns, cell_size=args.cell_size)
|
|
150
|
+
output = Path(args.output)
|
|
151
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
sheet.save(output)
|
|
153
|
+
_emit_status(
|
|
154
|
+
args,
|
|
155
|
+
{
|
|
156
|
+
"status": "ok",
|
|
157
|
+
"command": "preview",
|
|
158
|
+
"outputs": {"image": str(output)},
|
|
159
|
+
"parameters": {
|
|
160
|
+
"input": args.input,
|
|
161
|
+
"columns": args.columns,
|
|
162
|
+
"cell_size": args.cell_size,
|
|
163
|
+
"source_count": len(paths),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _emit_status(args: argparse.Namespace, payload: dict[str, object]) -> None:
|
|
171
|
+
if getattr(args, "format", "text") == "json":
|
|
172
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
sys.exit(main())
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_contact_sheet(paths: list[Path], *, columns: int, cell_size: int) -> Image.Image:
|
|
10
|
+
if not paths:
|
|
11
|
+
return Image.new("RGBA", (cell_size, cell_size), (0, 0, 0, 0))
|
|
12
|
+
rows = math.ceil(len(paths) / columns)
|
|
13
|
+
sheet = Image.new("RGBA", (columns * cell_size, rows * cell_size), (8, 10, 18, 255))
|
|
14
|
+
for index, path in enumerate(paths):
|
|
15
|
+
with Image.open(path) as image:
|
|
16
|
+
tile = image.convert("RGBA")
|
|
17
|
+
tile.thumbnail((cell_size, cell_size), Image.Resampling.NEAREST)
|
|
18
|
+
x = (index % columns) * cell_size + (cell_size - tile.width) // 2
|
|
19
|
+
y = (index // columns) * cell_size + (cell_size - tile.height) // 2
|
|
20
|
+
sheet.alpha_composite(tile, (x, y))
|
|
21
|
+
return sheet
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
|
|
5
|
+
from PIL import Image, ImageDraw
|
|
6
|
+
|
|
7
|
+
STAR_COLORS = [
|
|
8
|
+
(124, 247, 255, 255),
|
|
9
|
+
(255, 255, 255, 255),
|
|
10
|
+
(255, 218, 121, 255),
|
|
11
|
+
(198, 176, 255, 255),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_starfield(*, width: int, height: int, seed: int, stars: int) -> Image.Image:
|
|
16
|
+
rng = random.Random(seed)
|
|
17
|
+
image = Image.new("RGBA", (width, height), (5, 7, 18, 255))
|
|
18
|
+
draw = ImageDraw.Draw(image)
|
|
19
|
+
for _ in range(stars):
|
|
20
|
+
x = rng.randrange(width)
|
|
21
|
+
y = rng.randrange(height)
|
|
22
|
+
color = rng.choice(STAR_COLORS)
|
|
23
|
+
size = 1 if rng.random() < 0.86 else 2
|
|
24
|
+
draw.rectangle((x, y, min(width - 1, x + size - 1), min(height - 1, y + size - 1)), fill=color)
|
|
25
|
+
if rng.random() < 0.12:
|
|
26
|
+
glow = (color[0], color[1], color[2], 90)
|
|
27
|
+
if x > 0:
|
|
28
|
+
image.putpixel((x - 1, y), glow)
|
|
29
|
+
if x + 1 < width:
|
|
30
|
+
image.putpixel((x + 1, y), glow)
|
|
31
|
+
return image
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from PIL import Image
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def strip_background(image: Image.Image, *, tolerance: int = 0) -> Image.Image:
|
|
7
|
+
source = image.convert("RGBA")
|
|
8
|
+
background = source.getpixel((0, 0))
|
|
9
|
+
output = Image.new("RGBA", source.size, (0, 0, 0, 0))
|
|
10
|
+
for y in range(source.height):
|
|
11
|
+
for x in range(source.width):
|
|
12
|
+
pixel = source.getpixel((x, y))
|
|
13
|
+
if _within_tolerance(pixel, background, tolerance):
|
|
14
|
+
output.putpixel((x, y), (pixel[0], pixel[1], pixel[2], 0))
|
|
15
|
+
else:
|
|
16
|
+
output.putpixel((x, y), pixel)
|
|
17
|
+
return output
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _within_tolerance(pixel: tuple[int, int, int, int], background: tuple[int, int, int, int], tolerance: int) -> bool:
|
|
21
|
+
return max(abs(pixel[index] - background[index]) for index in range(3)) <= tolerance
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pixel_space_assets.asteroids import generate_asteroid_tiles
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsteroidTests(unittest.TestCase):
|
|
10
|
+
def test_generates_tiles_and_manifest(self) -> None:
|
|
11
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
12
|
+
output = Path(tmp)
|
|
13
|
+
|
|
14
|
+
manifest = generate_asteroid_tiles(output, material="ferric", count=3, size=32, seed=7)
|
|
15
|
+
|
|
16
|
+
self.assertEqual(len(list(output.glob("*.png"))), 3)
|
|
17
|
+
self.assertEqual(manifest["material"], "ferric")
|
|
18
|
+
self.assertEqual(len(manifest["tiles"]), 3)
|
|
19
|
+
self.assertEqual(json.loads((output / "manifest.json").read_text(encoding="utf-8"))["seed"], 7)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
unittest.main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from pixel_space_assets.preview import build_contact_sheet
|
|
8
|
+
from pixel_space_assets.strip_background import strip_background
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BackgroundPreviewTests(unittest.TestCase):
|
|
12
|
+
def test_strip_background_makes_corner_color_transparent(self) -> None:
|
|
13
|
+
image = Image.new("RGBA", (2, 2), (255, 0, 255, 255))
|
|
14
|
+
image.putpixel((1, 1), (10, 20, 30, 255))
|
|
15
|
+
|
|
16
|
+
stripped = strip_background(image, tolerance=0)
|
|
17
|
+
|
|
18
|
+
self.assertEqual(stripped.getpixel((0, 0))[3], 0)
|
|
19
|
+
self.assertEqual(stripped.getpixel((1, 1))[3], 255)
|
|
20
|
+
|
|
21
|
+
def test_contact_sheet_dimensions_follow_columns(self) -> None:
|
|
22
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
23
|
+
root = Path(tmp)
|
|
24
|
+
for index in range(3):
|
|
25
|
+
Image.new("RGBA", (8, 8), (index, index, index, 255)).save(root / f"{index}.png")
|
|
26
|
+
|
|
27
|
+
sheet = build_contact_sheet(sorted(root.glob("*.png")), columns=2, cell_size=8)
|
|
28
|
+
|
|
29
|
+
self.assertEqual(sheet.size, (16, 16))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
unittest.main()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from contextlib import redirect_stdout
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from pixel_space_assets.cli import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CliTests(unittest.TestCase):
|
|
12
|
+
def test_cli_prints_version(self) -> None:
|
|
13
|
+
stdout = StringIO()
|
|
14
|
+
|
|
15
|
+
with self.assertRaises(SystemExit) as raised:
|
|
16
|
+
with redirect_stdout(stdout):
|
|
17
|
+
main(["--version"])
|
|
18
|
+
|
|
19
|
+
self.assertEqual(raised.exception.code, 0)
|
|
20
|
+
self.assertIn("pixel-space-assets 0.1.0", stdout.getvalue())
|
|
21
|
+
|
|
22
|
+
def test_starfield_cli_writes_image_and_manifest(self) -> None:
|
|
23
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
24
|
+
output = Path(tmp) / "starfield.png"
|
|
25
|
+
manifest = Path(tmp) / "manifest.json"
|
|
26
|
+
|
|
27
|
+
exit_code = main(
|
|
28
|
+
[
|
|
29
|
+
"starfield",
|
|
30
|
+
"--width",
|
|
31
|
+
"16",
|
|
32
|
+
"--height",
|
|
33
|
+
"16",
|
|
34
|
+
"--seed",
|
|
35
|
+
"5",
|
|
36
|
+
"--stars",
|
|
37
|
+
"5",
|
|
38
|
+
"--output",
|
|
39
|
+
str(output),
|
|
40
|
+
"--manifest",
|
|
41
|
+
str(manifest),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.assertEqual(exit_code, 0)
|
|
46
|
+
self.assertTrue(output.exists())
|
|
47
|
+
self.assertTrue(manifest.exists())
|
|
48
|
+
|
|
49
|
+
def test_starfield_cli_can_print_json_status_for_automation(self) -> None:
|
|
50
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
51
|
+
output = Path(tmp) / "starfield.png"
|
|
52
|
+
stdout = StringIO()
|
|
53
|
+
|
|
54
|
+
with redirect_stdout(stdout):
|
|
55
|
+
exit_code = main(
|
|
56
|
+
[
|
|
57
|
+
"starfield",
|
|
58
|
+
"--width",
|
|
59
|
+
"16",
|
|
60
|
+
"--height",
|
|
61
|
+
"16",
|
|
62
|
+
"--seed",
|
|
63
|
+
"5",
|
|
64
|
+
"--stars",
|
|
65
|
+
"5",
|
|
66
|
+
"--output",
|
|
67
|
+
str(output),
|
|
68
|
+
"--format",
|
|
69
|
+
"json",
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
payload = json.loads(stdout.getvalue())
|
|
74
|
+
self.assertEqual(exit_code, 0)
|
|
75
|
+
self.assertEqual(payload["status"], "ok")
|
|
76
|
+
self.assertEqual(payload["command"], "starfield")
|
|
77
|
+
self.assertTrue(payload["outputs"]["image"].endswith("starfield.png"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
unittest.main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from pixel_space_assets.starfield import generate_starfield
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class StarfieldTests(unittest.TestCase):
|
|
8
|
+
def test_starfield_is_deterministic_for_seed(self) -> None:
|
|
9
|
+
first = generate_starfield(width=32, height=32, seed=42, stars=20)
|
|
10
|
+
second = generate_starfield(width=32, height=32, seed=42, stars=20)
|
|
11
|
+
|
|
12
|
+
self.assertEqual(first.size, (32, 32))
|
|
13
|
+
self.assertEqual(
|
|
14
|
+
hashlib.sha256(first.tobytes()).hexdigest(),
|
|
15
|
+
hashlib.sha256(second.tobytes()).hexdigest(),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
unittest.main()
|