Initial commit

This commit is contained in:
Michael Smith 2023-05-04 11:24:05 +02:00
commit f7b57817ff
9 changed files with 307971 additions and 0 deletions

160
.gitignore vendored Normal file
View File

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

26
bitmap.py Normal file
View File

@ -0,0 +1,26 @@
import json
import pygame
from palette import Palette
class Bitmap:
def __init__(self, filepath):
with open(filepath,) as jsonfile:
image = json.load(jsonfile)
self.filename = image['filename']
self.width = image['width']
self.height = image['height']
self.pixels = image['pixels']
self.palette = Palette(image['colors'], image['cycles'])
self.surface = pygame.Surface((self.width, self.height), 0, 8)
self.surface.set_palette(self.palette.colors)
# Load image onto surface
with pygame.PixelArray(self.surface) as pixels:
for x in range(self.width):
for y in range(self.height):
offset = x + (y * self.width)
pixels[x, y] = self.pixels[offset]

7
cycle.py Normal file
View File

@ -0,0 +1,7 @@
class Cycle:
def __init__(self, rate, reverse, low, high):
self.count = 0
self.rate = rate
self.reverse = reverse
self.low = low
self.high = high

67
flake.lock generated Normal file
View File

@ -0,0 +1,67 @@
{
"nodes": {
"devshell": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1676293499,
"narHash": "sha256-uIOTlTxvrXxpKeTvwBI1JGDGtCxMXE3BI0LFwoQMhiQ=",
"owner": "numtide",
"repo": "devshell",
"rev": "71e3022e3ab20bbf1342640547ef5bc14fb43bf4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1676283394,
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1676177817,
"narHash": "sha256-OQnBnuKkpwkfNY31xQyfU5hNpLs1ilWt+hVY6ztEEOM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1b82144edfcd0c86486d2e07c7298f85510e7fb8",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-22.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

48
flake.nix Normal file
View File

@ -0,0 +1,48 @@
{
description = "Paletteshifter development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
flake-utils.url = "github:numtide/flake-utils";
devshell = {
url = "github:numtide/devshell";
inputs = {
flake-utils.follows = "flake-utils";
nixpkgs.follows = "nixpkgs";
};
};
};
outputs = { self, nixpkgs, flake-utils, devshell }:
flake-utils.lib.eachDefaultSystem (system: {
devShell =
let
pkgs = import nixpkgs {
inherit system;
overlays = [ devshell.overlay ];
};
in
pkgs.devshell.mkShell {
name = "Paletteshifter development environment";
packages = with pkgs; [
(pkgs.python3.withPackages
(ps: with ps; [ pygame psutil ])
)
nixpkgs-fmt
isort
];
env = [ ];
commands = [
{
name = "paletteshifter:lint";
category = "Maintenance";
help = "Lint all the files in project";
command = ''
nixpkgs-fmt *.nix
'';
}
];
};
});
}

113
main.py Executable file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env python
import os
from collections import deque
import pygame
from pygame.locals import USEREVENT
import psutil
from bitmap import Bitmap
FPS = 30
FULLSCREEN = False
WIDTH = 1280
HEIGHT = 800
SCALE_WIDTH = 1064
SCALE_HEIGHT = 798
pygame.init()
if FULLSCREEN:
pygame.mouse.set_visible(False)
screen = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
else:
screen = pygame.display.set_mode((WIDTH, HEIGHT))
screen_rect = screen.get_rect()
pygame.display.set_caption('Mark J. Ferrari tribute')
clock = pygame.time.Clock()
font = pygame.font.SysFont('bitstreamverasansmono', 14)
UPDATE_STATS = USEREVENT + 1
pygame.time.set_timer(UPDATE_STATS, 1000)
# Load scenes
filenames = os.listdir('scenes/')
filenames.sort()
scenes = deque([f'scenes/{filename}' for filename in filenames
if filename != 'TESTRAMP.json'])
bitmap = Bitmap(scenes[0])
bitmap_rect = bitmap.surface.get_rect()
bitmap_rect.center = screen_rect.center
pygame.display.set_caption(f'Mark J. Ferrari tribute - {bitmap.filename}')
showpalette = False
cycling = True
running = True
cpu_percent = psutil.cpu_percent()
mem_percent = psutil.virtual_memory().percent
while running:
# Events
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == UPDATE_STATS:
if showpalette:
cpu_percent = psutil.cpu_percent()
mem_percent = psutil.virtual_memory().percent
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
cycling = not cycling
elif event.key == pygame.K_LEFT:
scenes.rotate(1)
bitmap = Bitmap(scenes[0])
pygame.display.set_caption(
f'Living Worlds - {bitmap.filename}')
elif event.key == pygame.K_RIGHT:
scenes.rotate(-1)
bitmap = Bitmap(scenes[0])
pygame.display.set_caption(
f'Living Worlds - {bitmap.filename}')
elif event.key == pygame.K_p:
showpalette = not showpalette
else:
running = False
# Update
if cycling:
bitmap.palette.cycle()
bitmap.surface.set_palette(bitmap.palette.colors)
# Render
screen.fill(pygame.Color('black'))
if FULLSCREEN:
# Scale preserving aspect ratio and center
screen.blit(
pygame.transform.scale(bitmap.surface,
(SCALE_WIDTH, SCALE_HEIGHT)),
((WIDTH - SCALE_WIDTH) / 2, 0)
)
else:
screen.blit(bitmap.surface, bitmap_rect)
if showpalette:
# Palette
bitmap.palette.surface.set_palette(bitmap.palette.colors)
screen.blit(bitmap.palette.surface, (690, 35))
# System stats
fps = int(clock.get_fps())
stats_str = (f"fps: {fps} "
f"cpu: {cpu_percent}% mem: {mem_percent}%")
stats = font.render(stats_str, True, pygame.Color('white'),
pygame.Color('black'))
stats_rect = stats.get_rect()
screen.blit(stats, (screen_rect.width - stats_rect.width - 50, 5))
pygame.display.flip()
# Cap framerate
clock.tick(FPS)
pygame.quit()

60
palette.py Normal file
View File

@ -0,0 +1,60 @@
from collections import deque
import pygame
from cycle import Cycle
pcolumns = 13
prows = 20
psize = 16
pmargin = 5
pwidth = ((psize + pmargin) * pcolumns) + pmargin
pheight = ((psize + pmargin) * prows) + pmargin
# NOTE(m): This magic number comes from the original Deluxe Paint source code
# file CCYCLE.C line number 19
# The division by two comes from our FPS of 30, which is half of 60 fps,
# the modern LCD equivalent of 60 Hz on an old school CRT.
# This seems to most accurately recreate the speed as measured in Deluxe Paint
# in DOSBox.
ONE_PER_TICK = 16384 / 2
class Palette:
def __init__(self, colors, cycles):
self.colors = [tuple(color) for color in colors]
self.basecolors = [color for color in self.colors]
self.surface = pygame.Surface((pwidth, pheight), 0, 8)
self.surface.set_palette(self.colors)
# Process cycles
self.cycles = [Cycle(cycle['rate'],
cycle['reverse'],
cycle['low'],
cycle['high'])
for cycle in cycles
if cycle['rate'] > 0]
for index in range(256):
rownumber = int(index / pcolumns)
colnumber = index % pcolumns
destrect = pygame.Rect(0, 0, psize, psize)
pygame.draw.rect(
self.surface,
index,
destrect.move(pmargin + (colnumber * (psize + pmargin)),
(pmargin + rownumber * (psize + pmargin)))
)
def cycle(self):
for cycle in self.cycles:
cycle.count += cycle.rate
if cycle.count >= ONE_PER_TICK:
cycle.count -= ONE_PER_TICK
cyclecolors = deque(self.colors[cycle.low:cycle.high + 1])
if cycle.reverse == 2:
cyclecolors.rotate(-1)
else:
cyclecolors.rotate(1)
self.colors[cycle.low:cycle.high + 1] = cyclecolors

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
certifi==2020.11.8
chardet==3.0.4
idna==2.10
psutil==5.7.3
pygame==2.0.0
requests==2.25.0
urllib3==1.26.2

307483
scenes/testramp.json Normal file

File diff suppressed because it is too large Load Diff