524 lines
19 KiB
Text
524 lines
19 KiB
Text
== Creating Assets
|
|
|
|
JoeyLib does not load PNGs, WAVs, or MODs at run time. Every asset is converted
|
|
on the build host into a compact binary the target can use directly, which is
|
|
what keeps loading fast and the runtime small. This chapter covers each asset
|
|
type: what you author, which tool bakes it, and which API call loads it.
|
|
|
|
=== The Pipeline at a Glance
|
|
|
|
[cols="2,2,2,3", options="header"]
|
|
|===
|
|
|You author |Tool |Baked file |Loaded by
|
|
|
|
|Indexed PNG tile sheet
|
|
|`assetbake.py --type tile`
|
|
|`.tbk`
|
|
|`jlTileBankLoad`, `jlTileBankLoadToSurface`
|
|
|
|
|A Tiled map (TMX or TMJ) with indexed tilesets
|
|
|`tiled2joey.py`, then `assetbake.py --type tile` for its sheet
|
|
|a `.map` per layer, and a `.tbk`
|
|
|`jlTileMapPaste` (the map read with `jlDataOpen`)
|
|
|
|
|Indexed PNG sprite sheet
|
|
|`assetbake.py --type sprite`
|
|
|`.spr`
|
|
|`jlSpriteBankLoad`
|
|
|
|
|A `.spr`
|
|
|`spritebake`
|
|
|`.spc`
|
|
|`jlSpriteBankLoadPrecompiled`
|
|
|
|
|Text note notation
|
|
|`songbake.py`
|
|
|`.jym` or a C header
|
|
|`jlMusicPlay`
|
|
|
|
|A tracker song (Furnace) for the SN76489 or AY-3-8910, exported as VGM
|
|
|`vgm2song.py`, then `songbake.py`
|
|
|`.jym` or a C header
|
|
|`jlMusicPlay`
|
|
|
|
|Protracker `.MOD`
|
|
|`joeymod`
|
|
|`.mod`, `.amod`, `.ntp`
|
|
|`jlAudioPlayMod`
|
|
|
|
|Raw signed 8-bit PCM
|
|
|(none -- copied)
|
|
|`.sfx`
|
|
|`jlAudioPlaySfx`
|
|
|===
|
|
|
|
Baked assets are staged into `DATA/` beside the executable, and the program
|
|
opens them with bare names.
|
|
|
|
=== Content Creation Tools
|
|
|
|
These are the tools we recommend for making each kind of asset. All of them
|
|
run on Linux, and all but OpenMPT are native there.
|
|
|
|
[cols="2,4,3", options="header"]
|
|
|===
|
|
|You make |With |Notes
|
|
|
|
|Tile and sprite sheets
|
|
|*GrafX2* (GPLv2), a Deluxe Paint-style editor built around an indexed
|
|
palette. *LibreSprite* (GPLv2), for sprite sheets drawn as layers and
|
|
animation frames. *GIMP* (GPLv3), to bring existing art down to an indexed
|
|
PNG.
|
|
|In GrafX2, set the palette's RGB scale to 16 (right-click the palette
|
|
button for the secondary palette menu): every colour you pick is then one a
|
|
JoeyLib palette holds exactly, 4 bits a gun. The sheet rules are in
|
|
<<_authoring_the_png>>.
|
|
|
|
|Tile maps
|
|
|*Tiled* (GPLv2; what you make with it is yours), a map editor that places
|
|
tiles from tilesets in layers.
|
|
|Orthogonal maps of 8x8 tiles from indexed tilesets, then `tiled2joey.py`
|
|
(<<_maps_from_tiled>>).
|
|
|
|
|Program icons
|
|
|Any of the above, saving an RGBA PNG with transparency.
|
|
|One picture for every system (<<_icons>>).
|
|
|
|
|Chip-tracker music
|
|
|*Furnace* (GPLv2 or later), a chiptune tracker that emulates the chips it
|
|
writes for. `./install.sh install furnace` puts it in
|
|
`toolchains/opt/furnace`.
|
|
|Write for the TI SN76489 or the AY-3-8910, export VGM, and convert it
|
|
(<<_writing_songs_in_furnace>>).
|
|
|
|
|`.MOD` music
|
|
|*MilkyTracker* (GPLv3), the *ProTracker 2 clone* (open source), or
|
|
*OpenMPT* (BSD; Windows, runs under Wine).
|
|
|Four channels, then `joeymod` (<<_music_protracker_modules>>).
|
|
|
|
|Sound effects
|
|
|*sfxr* (MIT) or *jfxr* (open source, in a browser) for generated game
|
|
sounds; *Audacity* (GPLv3) to edit recordings; *SoX* to convert.
|
|
|Save a WAV, then convert it to raw signed 8-bit PCM
|
|
(<<_sound_effects>>).
|
|
|===
|
|
|
|
None of these is needed to build a program: the baked files are what the
|
|
build uses. Only Furnace has an installer component, because
|
|
`scripts/check-vgm.sh` uses it to test the converter on real songs.
|
|
|
|
=== Graphics: Tiles and Sprites
|
|
|
|
==== Authoring the PNG
|
|
|
|
Both tile and sprite sheets start as an *indexed* PNG with strict rules:
|
|
|
|
* Mode `P` (indexed / palette), *not* RGB or RGBA.
|
|
* At most *16 colours*.
|
|
* Width and height both *multiples of 8*.
|
|
* Palette entry 0 is the transparent / background colour. Remember the library
|
|
forces it to black on screen.
|
|
|
|
Any decent pixel editor can produce this. In GIMP: *Image > Mode > Indexed*,
|
|
"Generate optimum palette" with maximum 16 colours, and no dithering --
|
|
dithering invents colours and will push you over the limit.
|
|
|
|
Lay the artwork out on a grid. A tile sheet is read as 8x8 cells row by row; a
|
|
sprite sheet is read as cells of whatever size you declare with `--cell`, also
|
|
row by row.
|
|
|
|
==== Baking Tiles
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/assetbake/assetbake.py --type tile --target atarist \
|
|
font.png build/atarist/bin/DATA/font.tbk
|
|
----
|
|
|
|
Tile banks are *per-target*, because the 8x8 block layout is the platform's
|
|
native pixel format -- plane-major on Amiga and X68000, row-major-with-planes-
|
|
per-row on ST, and chunky 4bpp on DOS and IIgs. You must bake one `.tbk` per platform, and the
|
|
loader refuses a file baked for a different target rather than drawing garbage.
|
|
|
|
`--target` is required for tiles, and accepts `amiga`, `atarist`, `dos`,
|
|
`iigs`, or `x68000`.
|
|
|
|
==== Maps from Tiled
|
|
|
|
`tools/tiled2joey.py` turns a map made in Tiled into what a JoeyLib program
|
|
pastes: a tile sheet, which `assetbake.py` bakes into a tile bank for each
|
|
port, and a map for each tile layer, which `jlTileMapPaste` takes as it is:
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/tiled2joey.py --header build/level1.h level1.tmx \
|
|
build/level1.png build/level1
|
|
tiled2joey: 60x25 map, 87 tiles, 14 colours, layers: ground.map, detail.map
|
|
$ python3 tools/assetbake/assetbake.py --type tile --target iigs \
|
|
build/level1.png build/iigs/bin/DATA/level1.tbk
|
|
----
|
|
|
|
* *The map* is Tiled's own TMX or its JSON (TMJ), orthogonal and finite, with
|
|
8x8 tiles. Its tilesets, in the map or in files of their own (TSX or TSJ),
|
|
are single images: indexed PNGs whose colours make one palette of at most
|
|
16 between them. The first tileset's colours keep their indices, so its
|
|
colour 0 stays the background; another's are matched by colour or take a
|
|
slot the first does not use.
|
|
* *The sheet* holds the tiles the map's layers use, in the order they first
|
|
appear, so the bank holds nothing the map does not place. A tile drawn
|
|
flipped or rotated is baked as a tile of its own (JoeyLib draws tiles only
|
|
as they are), so a map can use up to 255 distinct tiles and orientations
|
|
between its layers.
|
|
* *Each layer* becomes `NAME.map` in the folder given: width x height bytes,
|
|
row after row, each a tile's index in the bank or `JL_TILE_MAP_SKIP` where
|
|
the layer is empty, so pasting a layer over another leaves the one below
|
|
showing through. The files are the same on every port.
|
|
* `--header` writes the map's size and tile count as C constants, and lists
|
|
each layer's file. Object layers, image layers, tile animations, and tile
|
|
properties are not carried over.
|
|
|
|
A map no bigger than the screen pastes in one call. A bigger one -- a level
|
|
the program shows a screen of at a time -- pastes a row of tiles a call,
|
|
starting each row at its place in the whole map:
|
|
|
|
[source,c]
|
|
----
|
|
for (row = 0; row < 25u; row++) {
|
|
jlTileMapPaste(jlStageGet(), 0, (uint8_t)row, 40, 1, tiles, &level[(top + row) * LEVEL1_WIDTH + left]);
|
|
}
|
|
----
|
|
|
|
A level that scrolls is a tile layer over the whole map (see
|
|
<<_scrolling_tile_layers>>). A layer is opaque and draws `JL_TILE_MAP_SKIP` as
|
|
colour 0, so it scrolls one map layer; flatten the scenery into one layer in
|
|
Tiled.
|
|
|
|
[source,c]
|
|
----
|
|
layer = jlTileLayerCreate(tiles, level, LEVEL1_WIDTH, LEVEL1_HEIGHT, 0, 25);
|
|
----
|
|
|
|
==== Baking Sprites
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/assetbake/assetbake.py --type sprite --cell 2x3 \
|
|
player.png build/atarist/bin/DATA/player.spr
|
|
----
|
|
|
|
`--cell` gives the cel size *in tiles, not pixels*, so `2x3` means 16x24
|
|
pixels. The sheet is cut into cels row by row.
|
|
|
|
Sprite `.spr` banks are *cross-target*: the same file works on every platform,
|
|
because it holds chunky 4bpp tiles and each port converts a sprite to its own
|
|
form when the sprite is created. Bake once, ship everywhere.
|
|
|
|
Both formats can embed the 16-entry `$0RGB` palette, and both loaders will hand
|
|
it back through their `outPalette` parameter so artwork carries its own colours
|
|
rather than depending on whatever the stage happens to have set.
|
|
|
|
==== Pre-Compiling Sprites
|
|
|
|
A `.spr` still needs compiling into native blitter code at run time. For a
|
|
large bank that is a visible startup pause. `spritebake` does it offline:
|
|
|
|
[source,console]
|
|
----
|
|
$ build/tools/spritebake-atarist player.spr player.spc
|
|
----
|
|
|
|
The result is a *per-platform* `.spc` holding pre-emitted native routines.
|
|
`jlSpriteBankLoadPrecompiled` copies them straight into the codegen arena and
|
|
skips compilation entirely. A cel too big for the arena silently loads
|
|
uncompiled rather than failing, so this is a safe drop-in replacement.
|
|
|
|
The `spritebake` binaries are built per target by `make/tools.mk`, because each
|
|
one links the emitter for its own CPU while running on the build host.
|
|
|
|
There is a third option if you would rather ship only the small cross-platform
|
|
`.spr`: compile once on first run and cache the result with
|
|
`jlSpriteBankSavePrecompiled`, then load the cache on later runs. That trades a
|
|
one-time startup cost for a much smaller distribution.
|
|
|
|
=== Music: the Portable Chip Tracker
|
|
|
|
This is the recommended music path. One authored song plays on every port
|
|
at register-write cost, with no CPU mixing -- which matters enormously on the
|
|
ST, where `.MOD` playback is only marginally viable.
|
|
|
|
Songs are plain text, one row per line:
|
|
|
|
[source]
|
|
----
|
|
; Four bars of Am - F - C - G.
|
|
tickms 110
|
|
loop
|
|
; voice0 voice1 voice2 noise
|
|
A-2:2 A-4:5 --- N16:5
|
|
--- C-5:5 E-5:3 off
|
|
A-3:3 E-4:5 off N2:7
|
|
--- A-4:5 C-5:3 off
|
|
----
|
|
|
|
The columns are three tone voices plus an optional fourth noise column.
|
|
|
|
[cols="1,4", options="header"]
|
|
|===
|
|
|Cell |Meaning
|
|
|
|
|`C-4:3`
|
|
|Note on: note name, octave, then `:` and an attenuation 0..14 where 0 is
|
|
loudest. Sharps use `#`, as in `C#4:3`. Range `C-0` to `B-7`. The `:atten`
|
|
part may be omitted and defaults to 0.
|
|
|
|
|`@440:3`
|
|
|Note on at an exact frequency in Hz, for chip-accurate transcription where
|
|
equal-tempered note names would round the pitch.
|
|
|
|
|`---`
|
|
|Hold: this voice is unchanged for this row.
|
|
|
|
|`off`
|
|
|Silence this voice.
|
|
|
|
|`N4:3`
|
|
|Noise on (fourth column only): pitch 0..31, where 0 is the brightest hiss
|
|
and 31 the lowest rumble, plus attenuation.
|
|
|===
|
|
|
|
Directives: `tickms N` sets milliseconds per row and is required before the
|
|
first row (later occurrences change tempo mid-song); `loop` marks the next row
|
|
as the loop-back target; `repeat N` ... `endrepeat` repeats a block, and may be
|
|
nested.
|
|
|
|
Bake it either as a binary blob or directly as a C array:
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 tools/songbake/songbake.py song.song song.jym
|
|
$ python3 tools/songbake/songbake.py -c gDemoSong song.song song.h
|
|
----
|
|
|
|
The C header form is usually what you want for a small song: it compiles into
|
|
the program and needs no file I/O at all.
|
|
|
|
[WARNING]
|
|
====
|
|
On the Atari ST the noise generator shares channel C's volume register with
|
|
tone voice 2. Author noise hits and voice-2 notes so they do not sound at the
|
|
same time; `songbake` warns when they overlap.
|
|
====
|
|
|
|
==== Writing Songs in Furnace
|
|
|
|
A tracker is easier than a text file for anything longer than a jingle. Write
|
|
the song in Furnace, export it as VGM (a log of the chip's register writes as
|
|
the song plays), and `tools/vgm2song.py` turns that into a `.song` file, which
|
|
is still plain text you can read and touch up:
|
|
|
|
. Start the song on a system with a *TI SN76489* (the Sega Master System, BBC
|
|
Micro, or TI-99/4A) or an *AY-3-8910 / YM2149* (the ZX Spectrum, MSX, or
|
|
Atari ST's chip). The SN76489 is the closer match: three square tone voices
|
|
and a noise channel with its own volume, attenuated in 2 dB steps exactly as
|
|
JoeyLib's are. Use that one chip only; another chip's part (a Master System
|
|
FM unit, say) is left out.
|
|
. Export: *File > Export > VGM*, or `furnace -vgmout song.vgm song.fur` from a
|
|
shell.
|
|
. Convert and bake:
|
|
+
|
|
[source,console]
|
|
----
|
|
$ python3 tools/vgm2song.py song.vgm song.song
|
|
$ python3 tools/songbake/songbake.py -c gSong song.song song.h
|
|
----
|
|
|
|
What comes across:
|
|
|
|
* *Tones* at their exact frequencies (`@Hz` cells) and volumes; an AY volume
|
|
`v` becomes attenuation `15 - v`, exact on the ST.
|
|
* *Noise* at the nearest JoeyLib noise pitch. The SN76489's periodic noise (a
|
|
pitched buzz, the Master System's bass trick) plays as a square at its
|
|
pitch on a tone voice that is free, and as noise when all three are busy.
|
|
* *The AY's volume envelope*, followed row by row. An envelope repeating
|
|
faster than two rows is a timbre (the "buzzer"), not a volume: it plays at
|
|
full volume, and a channel whose tone is off sounds at the envelope's
|
|
frequency.
|
|
* *Timing.* One `.song` row a tick, by default the song's update interval
|
|
rounded down (16 ms at 60 Hz, 20 ms at 50 Hz), placed by the VGM's own clock,
|
|
so the length and the loop point are right to within a tick.
|
|
* *The loop point*, if the song has one.
|
|
|
|
A baked song must stay under 64 KB. A long, busy song that would not fit gets
|
|
a longer tick, a whole multiple of the default, which merges changes faster
|
|
than it (vibrato, arpeggios); `--tick MS` sets the tick yourself. The
|
|
converter warns about everything it approximates.
|
|
|
|
=== Music: Protracker Modules
|
|
|
|
For richer music, author a four-channel `.MOD` in any tracker (ProTracker,
|
|
MilkyTracker, OpenMPT) -- one whose signature is `M.K.`, `M!K!`, `FLT4`, or
|
|
`4CHN`; the other module formats and the 15-sample Soundtracker files are not
|
|
played. Every port plays its effects as Protracker 2.3 does. Then convert
|
|
per target:
|
|
|
|
[source,console]
|
|
----
|
|
$ tools/joeymod/joeymod song.mod song.mod # every port but the IIgs
|
|
$ tools/joeymod/joeymod song.mod song.amod # Amiga, with an end marker
|
|
$ tools/joeymod/joeymod song.mod song.ntp # Apple IIgs
|
|
----
|
|
|
|
The output extension selects the conversion:
|
|
|
|
* *`.mod`* -- a passthrough copy with the signature validated, played as it
|
|
is on every port but the IIgs. The ports that mix modules in software
|
|
(DOS, the ST, the X68000, and the hosted ports) find the song's end
|
|
themselves, and the Amiga adds an end marker as it copies the module to
|
|
chip RAM, so `+jlAudioPlayMod(..., loop=false)+` plays the song once.
|
|
* *`.amod`* -- the same, plus an `E8F` stop marker added at song end ahead of
|
|
time. The Amiga replayer, PTPlayer, has no song-end signal of its own and
|
|
needs the marker, but the Amiga adds it itself when a song is played with
|
|
`loop=false`, so a plain `.mod` works there too. A module with all 128 song
|
|
positions or all 128 patterns in use has no room for the marker either way,
|
|
and loops on the Amiga. The other ports' module player stops a song that
|
|
does not loop at the marker too, and the IIgs's replayer ignores `E8F`, so
|
|
an `.amod` plays the same anywhere (looping, with the marker's pattern of
|
|
silence at its end).
|
|
* *`.ntp`* -- NinjaTrackerPlus format for the IIgs, produced by the
|
|
NinjaTrackerPlus converter the installer's `ntp` component provides (part of
|
|
`toolchain:iigs`; it needs the PHP command-line interpreter, which the
|
|
installer's host packages include).
|
|
|
|
Keep modules modest. A four-channel mix is real CPU work on DOS, the ST, and
|
|
the X68000, and the sample data has to fit in memory alongside your game.
|
|
|
|
=== Sound Effects
|
|
|
|
Sound effects are *raw signed 8-bit PCM* with no header at all -- just sample
|
|
bytes. Convert from a WAV with any audio editor, or on the command line:
|
|
|
|
[source,console]
|
|
----
|
|
$ sox input.wav -r 11025 -c 1 -b 8 -e signed-integer output.sfx
|
|
----
|
|
|
|
Then play it, telling the library what rate it was recorded at:
|
|
|
|
[source,console]
|
|
----
|
|
jlAudioPlaySfx(4, sfxBytes, sfxLen, 11025);
|
|
----
|
|
|
|
Choose a rate that suits the machines: 8 to 11 kHz is plenty for game effects
|
|
and keeps the data small, which matters when the whole thing must fit on a
|
|
floppy.
|
|
|
|
Two things to know. Prefer *slot 4* if music is playing, because the IIgs maps
|
|
tracker tone voices onto slots 0 to 2 and the noise voice onto slot 3. And the
|
|
IIgs plays a sample value of -128 as -127, because its DOC reads the
|
|
corresponding byte as a wave-end marker -- one level out of 256, inaudible in
|
|
practice, but it explains a tiny waveform difference if you ever measure one.
|
|
|
|
For long sounds such as speech, use `jlAudioPlaySfxStream` with a fill callback
|
|
instead of holding the whole sample in memory.
|
|
|
|
=== Screens and Backgrounds
|
|
|
|
A whole 320x200 screen can be saved and reloaded in target-native form:
|
|
|
|
[source,c]
|
|
----
|
|
jlSurfaceSaveFile(surface, "title.sur");
|
|
jlSurfaceLoadFile(surface, "title.sur");
|
|
----
|
|
|
|
The file is 32000 bytes of pixels, 200 SCB bytes, and 512 bytes of palette. It
|
|
is *not* portable between platforms -- no format conversion happens, so the
|
|
machine that wrote it must be the machine that reads it. It is best used as a
|
|
runtime cache (a generated title screen, say), not as a shipped asset. For
|
|
shipped artwork, bake a tile bank and compose the screen with
|
|
`jlTileBankLoadToSurface`.
|
|
|
|
=== Staging Assets into the Build
|
|
|
|
Baked files must end up in `DATA/` next to the executable. A program's
|
|
Makefile lists them in its `ID_DATA` and makes them with rules of its own,
|
|
written once for every target (<<_data_files>>). The simple copy case, from
|
|
JoeyLib's `make/examples.mk`:
|
|
|
|
[source,makefile]
|
|
----
|
|
AUDIO_DATA := $(addprefix $(call programDataDir,AUDIO)/,test.mod test.sfx)
|
|
|
|
$(call programDataDir,AUDIO)/%: $(REPO_DIR)/assets/%
|
|
@mkdir -p $(dir $@)
|
|
cp $< $@
|
|
----
|
|
|
|
And a bake rule looks like:
|
|
|
|
[source,makefile]
|
|
----
|
|
$(call programDataDir,MYGAME)/font.tbk: assets/font.png
|
|
@mkdir -p $(dir $@)
|
|
python3 $(REPO_DIR)/tools/assetbake/assetbake.py --type tile --target $(ASSET_PLATFORM) $< $@
|
|
----
|
|
|
|
Tile banks and `.spc` files differ per platform, while `.spr`, `.jym`, and
|
|
`.sfx` files are shared, as are the `.map` files `tiled2joey.py` writes:
|
|
`ASSET_PLATFORM` names the target a bake is for (DVX and every hosted port --
|
|
SDL3, the web, macOS, Win32, Win16, the PSP, Haiku, and BeOS -- take the DOS
|
|
bakes; DVX and Win16 run the x86 code of a `.spc`, the others use only its
|
|
cels; the STe build takes the ST's) and `SPRITEBAKE` the matching host
|
|
`spritebake`.
|
|
|
|
`examples/tilebank` is a working reference: one rule in `make/examples.mk`
|
|
bakes the same PNG for each target into its `DATA/tilebank` (`TILEBANK_DATA`),
|
|
and the program loads that bank both ways and shows the loader refusing the
|
|
other targets' bakes. Its `scripts/verify-<port>-tilebank.sh` gates run it on
|
|
every port. Space Taxi, a game built on JoeyLib in a repository of its own,
|
|
has the full-sized one in its `program.mk`: levels extracted from a C64 image,
|
|
a cel bank generated by a host tool of its own and pre-compiled per target,
|
|
per-level banks marked optional, and speech baked with per-target flags.
|
|
|
|
=== Loading Assets at Run Time
|
|
|
|
[source,c]
|
|
----
|
|
// Tiles into a caller-owned array.
|
|
jlTileT tiles[256];
|
|
bool valid[256];
|
|
uint16_t palette[JL_SURFACE_COLORS_PER_PALETTE];
|
|
uint16_t n = jlTileBankLoad("font.tbk", tiles, 256, valid, palette);
|
|
|
|
// Or stream a big bank straight onto a surface, with no caller buffer.
|
|
uint16_t m = jlTileBankLoadToSurface("font.tbk", fontSurface, palette);
|
|
|
|
// Sprites, pre-compiled.
|
|
jlSpriteT *cels[32];
|
|
uint16_t count = jlSpriteBankLoadPrecompiled("player.spc", cels, 32, palette);
|
|
|
|
// Apply the palette the artwork shipped with.
|
|
jlPaletteSet(stage, 0, palette);
|
|
----
|
|
|
|
Prefer `jlTileBankLoadToSurface` for anything large. A 1000-tile font sheet is
|
|
32000 bytes, which no IIgs program can hold in a local array or in bank-0 BSS,
|
|
and the streaming loader needs only a small internal scratch buffer.
|
|
|
|
=== The Logo
|
|
|
|
The JoeyLib logo lives in `assets/` as both the original 320x200 PNG and a
|
|
traced SVG. If the source art ever changes, regenerate the vector with:
|
|
|
|
[source,console]
|
|
----
|
|
$ python3 -m venv /tmp/tracevenv
|
|
$ /tmp/tracevenv/bin/pip install potracer pillow numpy
|
|
$ /tmp/tracevenv/bin/python tools/traceLogo.py \
|
|
assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5
|
|
----
|
|
|
|
Five colour clusters measured as the closest match to the original artwork;
|
|
more clusters start carving the antialiased edges into separate layers, which
|
|
renders as noise rather than detail.
|