Metadata-Version: 2.4
Name: spore-engine
Version: 0.1.0
Summary: A library-grade ASCII (text-mode) graphics engine
Author: Spore Engine Authors
License-Expression: MIT
Project-URL: Homepage, https://github.com/spore-engine/spore-engine
Project-URL: Changelog, https://github.com/spore-engine/spore-engine/blob/main/CHANGELOG.md
Keywords: ascii,terminal,text-mode,graphics,engine
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Environment :: Console
Classifier: Topic :: Multimedia :: Graphics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.21
Provides-Extra: media
Requires-Dist: pillow>=9.0.0; extra == "media"
Provides-Extra: fx
Requires-Dist: scipy>=1.7; extra == "fx"
Provides-Extra: speed
Requires-Dist: numba>=0.55; extra == "speed"
Provides-Extra: all
Requires-Dist: pillow>=9.0.0; extra == "all"
Requires-Dist: scipy>=1.7; extra == "all"
Requires-Dist: numba>=0.55; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: pillow>=9.0.0; extra == "dev"
Requires-Dist: scipy>=1.7; extra == "dev"
Requires-Dist: numba>=0.55; extra == "dev"
Dynamic: license-file

# Spore Engine

A library-grade ASCII (text-mode) graphics engine with truecolor ANSI support, 3D rendering, physics, fluid simulation, procedural generation, cellular automata, ray tracing, and 142 demo scenes — pure Python plus `numpy`, with Pillow, SciPy, and Numba optional. Also includes image/video/GIF to ASCII conversion via ffmpeg or PIL, plus a **glyph art** module that converts media to copyable plain-text ASCII.

## Demo

![Animated capture of Spore Engine demo scenes: particle fountains, ray-traced spheres, reaction-diffusion patterns and fluid flow, rendered as truecolour ANSI text.](main.gif)

Terminal capture of the interactive scene demos, straight from `demo.py`.

<details>
<summary>Full-resolution video capture (<code>demo.mp4</code>)</summary>

<video src="demo.mp4" controls width="100%"></video>

</details>

## Feature Highlights

| Subsystem | Key Components |
|---|---|
| **Simple API** | `App`, `GameSprite`, `Anim` — sprites from ASCII art, tweened animations, keyboard/mouse input, fluent chaining |
| **Assets & ECS** | Unified `load_sprite`/`load_palette`/`load_model`/`load_text` loaders behind a memoized `Assets` store; `World`/`System`/`Component`/`EcsEntity` game-logic ECS that draws through `SpriteRenderSystem` into Scene/Layer |
| **Core Rendering** | `Canvas`, `HiResCanvas` (2x vertical via half-blocks), `Color` (HSV, hex, blending, gradients), `Sprite`, `Vec2`/`Vec3`/`Mat4`, incremental per-frame flush (only changed cells re-emitted) |
| **3D Rendering** | `Mesh3D` with wireframe & solid shading, OBJ/PLY loader, backface culling, depth sort, light direction |
| **Isometric** | `IsoTile`, `IsoMap`, `IsoCamera` — tile grid with screen projection |
| **Voxel** | `VoxelScene` — heightmap-based 3D landscape with directional shading & fog |
| **Ray Tracing** | `RayScene`, `Sphere`, `Plane`, Phong shading, reflections, refraction, multiple light sources |
| **SDF Ray Marching** | `SDFScene`, `sd_sphere`/`sd_box`/`sd_torus`/`sd_cylinder`, CSG ops (union/subtract/intersect/smooth/repeat) |
| **Physics 2D** | `PhysicsWorld`, `Body`, `Spring`, `AABB`, `RectBody`, `RayCast`, `ForceField`, `DistanceJoint`, SAT collision (`PolyBody`, `CompoundBody`, `sat_collide`) |
| **Physics 3D** | `Body3D`, `BoxBody3D`, `Spring3D`, `PhysicsWorld3D` with gravity, rotation, bounds |
| **Soft Body** | `SoftBody` — pressure-based volume preservation, spring networks, circle/polygon shapes, cloth |
| **Steering Behaviors** | `SteerAgent`, `SteerWorld` — seek, flee, arrive, pursue, evade, wander, flock (separation/alignment/cohesion), path follow, obstacle avoidance |
| **Fluid Simulation** | `FluidSim` (Navier-Stokes with vorticity confinement), `WaveSim` (2D wave equation) |
| **Particle System** | `Particle`, `Burst`, `Emitter`, `FountainEmitter`, `StreamEmitter`, `FireEmitter` — particle life, trails, burst_explosion/burst_ring/burst_directional |
| **Vector Fields** | `FieldSource` (vortex/sink/source/swirl), `VectorField`, `FieldParticle`, `FieldSystem` |
| **2D Lighting** | `Ray2D`, `Light`, `LightManager`, `cast_ray`/`cast_ray_dda`, `visibility_polygon`, `render_shadows` |
| **Visual Effects** | `plasma`, `fire`, `starfield`, `matrix_rain` |
| **Text Effects** | 12 animations: `glitch_text`, `typewriter_text`, `sine_text`, `rainbow_text`, `gradient_text`, `scroll_text`, `star_wars_crawl`, `wave_text`, `fire_text`, `matrix_code_rain`, `bounce_text`, `zoom_text` |
| **Screen Effects** | `shake`, `fade_overlay`, `flash`, `crossfade`, `color_overlay` |
| **Post-Processing** | `box_blur`, `glow`, `edge_detect`, `dither`, `scanlines`, `vignette`, `chromatic_aberration`, `pixelate`, `palette_remap` |
| **Shader Pipeline** | `Shader`, `ShaderPipeline` — 16 modular shaders: `WaveDistort`, `SwirlDistort`, `KuwaharaFilter`, `Posterize`, `Solarize`, `CelShade`, `HeatHaze`, `Emboss`, `PixelSort`, `Crystallize`, `ASCIIRemap`, `ChannelShift`, `Kaleidoscope`, `Warp`, `VHSGlitch`, `Ripple` |
| **Scene Transitions** | `Fade`, `Wipe` (4 dirs), `Slide`, `Checkerboard`, `PixelDissolve` |
| **Volumetric FX** | `VolumetricFog`, `LightCone`, `SmokePlume`, `VolumetricRenderer` |
| **Animation** | `Tween`, `Sequence`, `Oscillator`, `Ticker`, 20+ easing functions, `Animator`, `Entity`, `Path`/`PathFollower`, `Keyframe`/`Track`/`Timeline`, IK `Bone`/`Skeleton` (FABRIK solver, arm/leg/tentacle creators) |
| **Procedural Gen** | `Terrain` (Perlin heightmap + island), `Maze` (DFS/Prim's), `LSystem` (stochastic too), `DungeonGen` (BSP rooms+corridors), `ErosionSim`, `WFC` (wave function collapse), fractals (`Mandelbrot`, `BurningShip`, `NewtonFractal`, `BarnsleyFern`) |
| **Noise** | `PerlinNoise` (2D/3D, fbm), `WorleyNoise`, `OpenSimplexNoise`, `ValueNoise` |
| **Cellular Automata** | `GameOfLife`, `Automata1D`, `WireWorld`, `LangtonsAnt`, `ReactionDiffusion` (Gray-Scott), 8 built-in GoL patterns |
| **Powder Simulation** | `PowderSim` — 12 materials (Sand/Water/Stone/Wood/Fire/Smoke/Oil/Lava/Acid/Plant/Salt/Steam), liquid/gas/solid physics, flammability, melting, erosion |
| **Biome Maps** | `BiomeMap` — 12 biomes from elevation/moisture/temperature (ocean, beach, desert, grassland, forest, rainforest, tundra, taiga, mountains, snow, swamp, savanna) |
| **Scenery** | `ParallaxScenery`, `ParallaxLayer`, `Cloud`, `CloudLayer`, `MountainProfile`, `Tree`, `WaterSurface`, `DayNightCycle` (sky/stars/moon) |
| **Curves & Splines** | `quadratic_bezier`, `cubic_bezier`, `catmull_rom` with rendering |
| **Pathfinding** | `AStar` — 4/8-directional, weighted grid, path visualization |
| **Delaunay / Voronoi** | `Delaunay`, `Point`, `Triangle`, circumcircle-based triangulation, Voronoi diagram rendering |
| **Marching Cubes** | 3D isosurface extraction to `Mesh3D` with density grid generation |
| **Terminal UI** | 17 widgets: `Label`, `TextBox`, `ProgressBar`, `Button`, `Menu`, `Frame`, `Checkbox`, `RadioGroup`, `TabBar`, `Slider`, `Table`, `TextField`, `Toggle`, `Divider`, `StatusBar`, `WidgetManager` + `TerminalApp` (event loop, mouse, keyboard), `Form`, `Dialog` |
| **TileMap** | `TileMap`, `TileCamera`, auto-tiling, collision, `generate_platformer`, `generate_cave` |
| **Bitmap Font** | `Font` — 5x7 character glyphs |
| **Media I/O** | Image/video/GIF → ASCII (`ImageConverter`, `image_to_canvas`, `video_to_ascii`), `ScreenRecorder`, `glyphart` (copyable plain-text ASCII), ANSI file I/O (parse/export/save/load), `Video`/`FramePlayer`, canvas scaling & gradient |
| **Demo Scenes** | 142 interactive demo scenes in `demo.py` / `demos/` |
| **Authoring** | `.spore` scene files (`spore new` / `run -w`), live code-shader editor (`spore shader`), `App` subsystem façade (`app.physics`, `app.particles`, `app.ecs`, ...) |

## Installation

```bash

pip install spore-engine

```

## Authoring: write, run, edit

The engine is not only a library you import. It has a live authoring loop.

```bash
spore new stars              # write a commented starter scene
spore run stars.spore -w     # run it, and reload it every time you save
spore shader                 # live shader preview: edit and save, it updates
spore list                   # what scenes are here
spore info stars.spore       # which contract does it define
```

A `.spore` file is ordinary Python. Define `setup(app)`, `update(app, dt)`, or
both, and it is driven by the same `App` the demos use:

```python
# stars.spore
import math
from spore_engine import Color

_state = {'stars': []}


def setup(app):
    _state['stars'] = [app.sprite('*', x=i * 7 % 78, y=2,
                                  fg=Color(159, 216, 255))
                       for i in range(40)]


def update(app, dt):
    for i, s in enumerate(_state['stars']):
        s.x = (i * 7 + app.t * 12) % 78
        s.y = 2 + math.sin(app.t * 2 + i) * 6
```

State shared between `setup` and `update` has to live at module level — a name
assigned inside `setup` is local to `setup`, and `update` would raise
`NameError` on the first frame.

`spore run --watch` re-reads the file on save and reruns `setup`. If the file
has a syntax error, the running scene stays up and the error is reported — a
half-saved file is normal while editing.

The same applies to shaders. A shader is a program with a `draw` or `shade`
function, and `spore shader` previews it while you edit:

```python
# plasma.spore
uniforms = {"scale": 0.35}

def draw(canvas, t, dt):
    canvas.clear(' ')
    for y in range(canvas.height):
        for x in range(canvas.width):
            v = math.sin(x * 0.3 + t) + math.cos(y * 0.4 - t)
            canvas.set_pixel(x, y, SHADES[int(abs(v) * 3) % 10],
                             lerp_color(BLUE, RED, abs(v) / 2))
```

`uniforms` declares values you can tweak live; they are readable inside the
body as `u`. The shader body is exec'd with a ready-made namespace, so `math`,
`Canvas`, `Color`, `Gradient`, `SHADES`, `PALETTES`, the primary colors
(`BLUE`, `RED`, ...), and the helpers `lerp_color`, `lerp`, `clamp`, `ramp`,
`wave`, `osc`, and `ir` are already in scope — no imports needed.

A shader that raises is recorded and the last good frame stays on screen, so a
typo dims the picture instead of blanking the terminal.

`spore shader` (or `python3 shader_sandbox.py`) is the live preview: it renders
at your frame rate, watches the file, and reloads on save. `e` opens
`$EDITOR` — the terminal is handed over for the duration, so vim and nano work
— and the preview resumes on return. See
[`wiki/usage.md`](wiki/usage.md#shader-lab) for the full key table.

## Subsystems on `App`

`App` wires the engine's subsystems in as lazy properties. Touch one and it is
constructed, registered for auto-update, and placed in the right render stage;
touch none and you pay nothing. They are ordinary objects, so the full API
under each one is still available.

```python
from spore_engine.easy import App

app = App()

# Physics — bodies are Prop objects, and props are auto-updated
a = app.physics.ball('o', 10, 5, fg=0xff8844)
b = app.physics.ball('@', 40, 5, fg=0x88ff44)
app.physics.spring(a, b, stiffness=60)

app.particles.burst(5, 5, 20, colors=[0xffffff, 0x88ccff])
app.particles.fountain(50, 14)
app.field.add_vortex(10, 8, 2.0)
app.waves.splash(12, 8, 1.5)
app.fluid.splat(20, 6, 0.8)

# ECS, with chaining
app.ecs.entity().position(3, 4).sprite('*', fg=0xffffff)

# A live code shader. `source` is a property, so assign to it.
app.shaders.source = "def draw(canvas, t, dt):\n    canvas.clear('.')\n"
app.shaders.load('my.spore')          # or compile a file, reloading on save

app.tilemap(20, 8)                     # TileMap system
app.run()
```

| Property | Stage | Backed by |
|---|---|---|
| `app.physics` | background | `PhysicsWorld`, `Body`, `Spring` — `ball()` returns a `Prop` |
| `app.particles` | background | `ParticleSystem` — `burst`, `fountain`, `stream`, `fire` |
| `app.field` | background | `VectorField`, `FieldSource` — `add_vortex`/`add_sink`/`add_source`/`add_swirl` |
| `app.waves` | background | `WaveSim` — `splash` |
| `app.fluid` | background | `FluidSim` — `splat`, `push` |
| `app.ecs` | background | `World`/`Component` — `entity()`, `on_update(fn)`, `remove_entity` |
| `app.tilemap(w, h)` | background | `TileMap`, auto-tiling and collision |
| `app.raytrace` | background | `RayScene` — `objects` list, `render_to(hr)` |
| `app.sdf` | background | `SDFScene` — `add_sphere`/`add_box`/`add_torus`, `render_to` |
| `app.shaders` | overlay | `ShaderPipeline` plus a live `ShaderProgram` |

`app.systems` lists the bridges that have actually been constructed, in
creation order. Each one has `.enabled` and `.stage`, so you can turn one off or
move it between `BACKGROUND`, `FOREGROUND`, and `OVERLAY`.

## Quick Start

```bash
python3 easy_demo.py             # Simple API demo (sprites, animation, input)
python3 demo.py                  # Interactive demo (142 scenes
```

```python
# Easy API — sprites from ASCII art, fluent animation
from spore_engine.easy import App, GameSprite
from spore_engine import Color

app = App()
player = GameSprite("""  @  \n /@\\ \n / \\""", x=5, y=10, fg=Color(255, 200, 100))
player.move_to(70, 10).over(3).ease('bounce_out')
app.run()
```

```python
# Low-level API — full control
import sys
from spore_engine import Canvas, Color

c = Canvas(80, 24)
c.draw_text(10, 10, 'Hello Spore!', Color(255, 128, 0))
c.render_to(sys.stdout)   # render_to wants a stream with .write
```

```python
# Tween animation with chaining
from spore_engine import Tween

t = (Tween(0, 100, 2, 'bounce_out')
     .on_update(lambda v: print(f'Value: {v:.1f}'))
     .on_done(lambda: print('Done!'))
     .start())
```

```python
# Terminal GUI
from spore_engine.ui import TerminalApp, Label, Button

app = TerminalApp()
app.add(Label(5, 3, 'Hello, world!', fg=Color(0, 255, 0)))
app.add(Button(5, 5, 'Quit', on_click=lambda: app.stop()))
app.run()
```

## Documentation

- [`wiki/usage.md`](wiki/usage.md) — Comprehensive usage guide
- [`wiki/math.md`](wiki/math.md) — Vector/matrix math, rendering math, simulation math
- [`wiki/files.md`](wiki/files.md) — How each file contributes to the engine
- [`wiki/architecture.md`](wiki/architecture.md) — How everything works together


## Glyph Art (copyable ASCII)

```bash
GLYPH_IMG=~/photo.jpg python3 demo.py    # scene 86 — coloured ASCII glyph art
```

```python
from spore_engine.media.glyphart import image_to_glyph, image_to_glyph_colored

# Plain text (copyable, no ANSI codes)
text = image_to_glyph('photo.jpg', width=80)
print(text)                               # ← select & copy this

# Coloured glyph data for display
text, colors = image_to_glyph_colored('photo.jpg', width=80)
```

## Requirements

- Python 3.10+ (developed and verified on Linux)
- `numpy` — the only required third-party package
- Optional: `Pillow` (image I/O), `ffmpeg` (video conversion), `SciPy` and
  `Numba` (faster filters / JIT, both with pure-Python fallbacks)
- A POSIX terminal. Raw mode, `SIGWINCH` resize handling, and the mouse
  protocol are POSIX-only; Windows is untested and unsupported.

### Colour support

The engine emits one of three output levels, selected per canvas:

| `color_depth` | Output |
| --- | --- |
| `3` (default when capable) | 24-bit truecolor |
| `2` | 256-colour (`38;5;n`) |
| `0` or `1` | plain text, no colour escapes at all |

There is **no 16-colour mode** — levels `0` and `1` both render monochrome.

Detection is automatic and honours `NO_COLOR`, `TERM=dumb`, and non-tty output
(output to a pipe or file is always plain). Auto-detection assumes truecolor
for terminal types that do not actually advertise it, so **if you are on a
16-colour or monochrome terminal, or a multiplexer such as tmux that is not
configured for truecolor, set the level yourself:**

```python
app.canvas.color_depth = 1        # 0 or 1 = plain, 2 = 256-colour, 3 = truecolor
```

