Skins
QUP Karaoke Engine: Making Skins
For skin makers. To browse and install ready-made skins, see the skins gallery instead.
A QUP skin is a folder of PNG images plus one skin.json that says which image dresses which control. No code and no build step: if you can make a PNG and edit a text file, you can make a skin. This page covers the whole format (skin.json format 2), current as of v2.18.1.
How skinning works
- QUP owns behaviour; the skin owns the look. QUP decides which controls exist and what they do. A skin can't add, remove or rewire a control, or change how it answers the mouse.
- QUP draws text and live data. Labels, song lists, numbers, waveforms, the EQ curve, meter levels and knob value rings are drawn by QUP at runtime, in your skin's colours. Never bake text into your art.
- A broken skin only looks wrong. A bad part falls back to QUP's default look for the controls it dressed. A skin QUP can't read at all is replaced by the default, and QUP tells you why. Nothing ever disappears from the screen.
- Some windows are never skinned: Preferences, the About box and anything they open, and everything the audience sees (the singer display, the projector, the scroller).
Quick start
- Download the reference skin, Brushed Steel. It dresses every control in both modes, so it's the best starting point.
- In QUP, open Preferences → Display → Skin & Layout and click Open My Skins Folder.
- Unzip Brushed Steel there and rename the folder, say
my-skin. - In its
skin.json, change"id"to"my-skin"and"name"to your skin's name. Theidis what QUP knows a skin by, so it must be unique. - Edit the PNGs and tokens, restart QUP, and pick your skin under Bitmap Skin → Skin pack. A new choice takes effect when QUP restarts.
The smallest skin is just colours. A skin can build on another one and change only what it needs. This one is Brushed Steel with an amber accent:
{ "format": 2, "id": "brushed-steel-amber", "name": "Brushed Steel (Amber)",
"inherits": "brushed-steel",
"tokens": { "accent": "#FFB000", "deck.a": "#FFB000" } }
The pack folder
my-skin/
skin.json the manifest
README.md optional: how the art was made
1x/ every image at normal size
2x/ the same file names at exactly double size
preview/ optional
QUP looks for skins in two places: the skins that come with QUP, and your own skins folder (the one Open My Skins Folder opens). When both hold the same id, the built-in one wins, so a downloaded skin can never pretend to be a shipped one.
Image rules
- PNG, 32-bit RGBA, sRGB, straight (not premultiplied) alpha. Strip colour chunks (
gAMA,cHRM,iCCP,sRGB): Java honours them inconsistently. 2x/is exactly twice1x/in both dimensions, and should be a real re-render, not an upscale. A missing @2x image is only a warning (QUP scales the @1x), but a missing @1x makes that layer unusable.- File names: lowercase ASCII letters, digits,
-,_and., ending in.png. No spaces. - Every state of a part is the same size, so nothing jumps on hover or press.
skin.json
| Key | Required | Meaning |
|---|---|---|
format |
yes | 2 |
id |
yes | lowercase letters, digits and -, starting with a letter or digit |
name |
no | shown in QUP; defaults to the id |
version, author, description, license |
no | informational |
sources |
no | names any third-party art you used; it must be CC0 / public domain and free to redistribute |
inherits |
no | another skin's id to build on |
tokens |
no | colours and values |
parts |
no | what dresses each kind of control |
overrides |
no | per-component changes |
Parts
A part dresses one kind of control: a button, a knob, a title bar. Here's Brushed Steel's transport button:
"transport": {
"states": {
"normal": [ { "image": "transport-normal.png", "mode": "nine", "insets": [7,7,7,7] } ],
"hover": [ { "image": "transport-hover.png", "mode": "nine", "insets": [7,7,7,7] } ],
"pressed": [ { "image": "transport-pressed.png", "mode": "nine", "insets": [7,7,7,7] } ],
"active": [ { "image": "transport-active.png", "mode": "nine", "insets": [7,7,7,7] } ],
"disabled": [ { "image": "transport-disabled.png", "mode": "nine", "insets": [7,7,7,7] } ]
}
}
A state can also be written as a single image name, or as { "layers": [ ... ] }. A part may also carry slots (named, stateless layer lists, such as a knob's base and indicator) and properties (such as a knob's sweep).
States
| State | When |
|---|---|
normal |
resting. Required whenever a part has states. |
hover |
pointer over it |
pressed |
mouse down |
active |
latched on, lit, selected, checked, playing |
focused |
keyboard focus (text fields, combos) |
disabled |
not available |
inactive |
a window's title bar while another window has focus |
shaded |
a title bar while its window is rolled up |
A state a part doesn't define paints as normal. A pad's lit state, a selected tab and a checked checkbox are all active.
Layers
Layers paint bottom first, and each is one image or one effect.
| Key | Meaning |
|---|---|
image |
the file name, looked up in 1x/ and 2x/ |
mode |
how it fills the control (below) |
insets |
[top, left, bottom, right] in @1x pixels, for nine |
overflow |
[top, left, bottom, right]: how far art reaches outside the control (shadows, glows). It never enlarges the click area. |
pivot |
[x, y]: the rotation centre for rotate |
opacity |
0 to 1 |
tint |
a colour or token multiplied over the layer |
blend |
normal, add, screen or multiply |
| Mode | Behaviour |
|---|---|
nine |
nine-sliced: corners fixed, edges stretch along one axis, the centre stretches both. Keep grain out of the stretching parts. |
tile |
repeated to fill. For seamless textures. |
stretch |
scaled to the control. Only for art with no texture (gradients, sheens). |
fixed |
drawn at natural size, centred. For knobs, glyphs, caps, LEDs. |
rotate |
fixed, turned about pivot by the control's value. Draw it pointing at 12 o'clock. |
Effect layers need no PNG: gradient (from, to, angle), dropShadow and innerShadow (color, radius, dx, dy), outerGlow (color, radius), roundedClip (radius), tint (color), plus the moving pulse and transition. Moving effects only run at the Full quality tier, so design every part to look complete with images alone.
Tokens: your skin's colours
tokens maps names to colours (#RGB, #RRGGBB or #RRGGBBAA) or to other token names. Setting these recolours QUP even where no part dresses a control:
| Token | What it colours |
|---|---|
accent |
the one "on / selected / playing" colour |
text.plate |
labels on your plates. A light plate needs a dark text.plate. |
text.well, text.well.dim |
text in lists and readouts |
text.active, text.disabled, text.status |
lit-control text, disabled text, the status line |
plate.bg, well.bg, divider |
flat fills where art is absent |
row.alt, row.selected, row.selected.text, row.playing, row.border |
table rows |
waveform.bg, .played, .unplayed, .playhead |
waveforms |
meter.green, .amber, .red, .peak, .bg |
meters |
readout.on, .off, .bg |
VFD-style readouts |
eq.fill, eq.fill.cut |
the EQ curve above and below 0 dB |
knob.arc, knob.arc.track |
the knob value ring |
deck.a, deck.b |
DJ deck colours |
pad.cue, pad.loop, pad.roll, pad.fx |
performance pad glows by mode |
Point colours that should follow your accent at the token ("knob.arc": "accent") rather than repeating the hex. Then a colours-only child skin changes them all at once.
Which part dresses which control
Every skinnable control has a stable id such as karaoke.play, filler.eq.band.1k, dj.a.pad.3 or window.karaokeEq.titlebar. Download the full list of ids and kinds. A control takes the first of:
- a part named with its own id, e.g.
"dj.a.play": { ... }; - the part an override names for it;
- the first of its kind's part names that your skin defines:
| Kind | Part names, most specific first |
|---|---|
| Plate / window body | plate.main, plate |
| Well (lists, readouts) | well |
| Window frame, title bar | window.frame, titlebar |
| Button | button |
| Toggle | button.toggle, button |
| Transport button | transport, button |
| ±5 step buttons | button.small, button |
| Knob | knob.32, knob.52, knob |
| Vertical fader | fader.v |
| Horizontal slider | slider.h |
| Crossfader | crossfader, slider.h |
| Meter | meter.spectrum / meter.vu, then meter |
| Timer digits | readout.digits, well.lcd, well |
| Scrolling title | readout.matrix, well.lcd, well |
| Waveform, EQ curve | waveform / eq.curve, then well |
| Table, tree, text field | table / tree / textfield, then well |
| Tab, combo, checkbox, radio | tab, combo, checkbox, radio |
| Scrollbar, menu, tooltip | scrollbar, menu, tooltip |
| Performance pad, LED button | pad, led |
| Resize grip | resize.grip |
A control with no part keeps QUP's default look, and QUP lists it when the skin loads. A complete skin leaves nothing on the default look in either mode.
Sub-parts and slots
| Part | Its pieces |
|---|---|
transport |
glyph sub-parts transport.glyph.play, .pause, .stop, .next, .restart, .eject, with states normal, disabled, active |
knob.32, knob.52 |
slots base (fixed, never rotates) and indicator (rotate); property sweep (degrees, 270) |
fader.v, slider.h, crossfader |
thumb sub-parts fader.v.thumb, slider.h.thumb, crossfader.thumb; property trackAxis ("y" or "x") keeps a slot's thickness |
titlebar |
titlebar.button (close/minimise bodies) and slots in titlebar.glyphs: close, min, menu |
led |
slots lamp.off, lamp.green, lamp.amber, lamp.red, lamp.accent |
meter |
LED segment slots seg.<h\|v>.<green\|amber\|red>.<on\|off> and seg.<h\|v>.peak |
readout.digits |
sprite strips glyphs.accent, glyphs.dim; properties cell: [w, h], chars |
pad |
slot glow (white; QUP tints it by pad mode) |
table |
table.header (states) and table.sort slots asc, desc |
tree |
slots expand, collapse and optional 16 px icons icon.folder, icon.folder-open, icon.karaoke, icon.filler, icon.playlist, icon.plugin, icon.singer, icon.audio-plugin |
combo, scrollbar |
combo.arrow; scrollbar.thumb, scrollbar.arrow.up / .down / .left / .right |
menu |
menu.item (normal, hover) and slots separator, arrow, check |
Slider thumbs must fit the 17×17 click area: QUP clips thumb art to it. Title-bar buttons reach titlebar.button through an override (below).
Overrides
"overrides": {
"window.*.close": { "part": "titlebar.button" },
"window.*.min": { "part": "titlebar.button" },
"dj.b.*": { "tokens": { "accent": "deck.b", "knob.arc": "deck.b" } }
}
Keys are id patterns (* matches anything, dots included). part dresses the matching controls with a part; tokens recolours them. A later match wins. The dj.b.* line above is how deck B gets its own colour.
Custom meter and knob gradients
Your VU meters, spectrum analysers and knob rings don't have to be green-yellow-red. Give meter.vu, meter.spectrum or meter a fill:
"meter.spectrum": {
"states": { "normal": [ { "image": "meter-bezel.png", "mode": "nine", "insets": [4,4,4,4] } ] },
"fill": { "style": "gradient", "banded": true, "peak": "#FFFFC0",
"stops": ["#4A0402", "#C0200A", "#FF6A10", "#FFC040", "#FFF8B0"] }
}
| Key | Meaning |
|---|---|
style |
gradient (default), solid, or segments (uses the part's seg.* LED art) |
stops |
[position, colour] pairs from 0 to 1, or a bare list of colours spread evenly. A Winamp viscolor.txt table drops straight in. |
banded |
true for flat bands, false to blend |
unlit |
the part of the scale not reached: a number 0..1 dims the fill (a ghost meter), a colour fills it flat, 0 shows the well |
peak |
the peak-mark colour, or "fill" for the fill's colour at that height |
grid |
QUP's dot-matrix overlay on top (default true) |
Colour is by position, never by level: the top of the scale is always the last stop.
Knobs take the same keys as arc on knob.32 / knob.52, plus cells (1 to 64) for an LED-ring look:
"knob.32": { "base": [ ... ], "indicator": [ ... ],
"arc": { "unlit": 0.2, "stops": [[0.0,"#6A0A02"],[0.6,"#FF6A10"],[1.0,"#FFF0A0"]] } }
Sizes
Fixed-size art (knobs, caps, glyphs, LEDs, checkboxes, title-bar buttons) is never scaled, so it must match these @1x sizes. Everything else is nine-sliced and only needs to look right from these sizes up.
| Control | Size | Brushed Steel's art |
|---|---|---|
| Knob, DJ strips and FX | dial 32 | knob.32 32×32, cap 24 (leaves 4 px for the value ring) |
| Knob, Mic Effects | dial 52 | knob.52 52×52 |
| Transport button | 32×32 | nine [7,7,7,7]; glyphs 16×16 |
| ±5 step buttons | 20×18 | button.small nine [4,4,4,4] |
| General buttons | 24 tall | button 24×24 nine [5,5,5,5] |
| LED buttons | 22 tall | led 30×22 nine [5,16,5,5], lamp in the left band |
| Title bar | 14 tall | 32×14 nine [3,4,3,4] |
| Title-bar buttons | 18×12 | nine [3,3,3,3] |
| EQ fader | 27×67, thumb area 17×17 | track 12×24 nine [5,0,5,0]; thumb 17×11 |
| VOL / BAL / CUT, crossfader | 21 tall, thumb area 17×17 | track 24×12 nine [0,5,0,5]; thumb 11×17 (crossfader 17×17) |
| Spectrum / VU | 86×12 / 270×14 | bezel 16×12 nine [4,4,4,4] |
| Timer | 86×28 | well.lcd + 12×22 digit cells |
| Performance pads | about 76×44 | pad 32×44 nine [8,8,8,8] |
| Combo / text field / tab | 26 / 28 / 24 tall | nine [6,6,6,6] |
| Checkbox, radio | 14×14 | fixed |
Inheritance
"inherits": "<id>" builds on another installed skin. The parent's manifest is merged under yours key by key, down to a single state of a single part, so you only write what changes. Images resolve in the pack that declared them, so you don't copy the parent's PNGs. A skin that inherits itself, or names a parent that isn't installed, won't load.
Testing your skin
- The load report. Every time a skin loads, QUP writes one report to its log (
[Skin] <id>: ...). If anything fell back, QUP shows a notice once after startup. - The developer overlay. In Preferences → General → Hotkeys, bind Skin developer overlay to a key. Press it, and over every window QUP outlines each control: green if your skin dresses it, red if it fell back to the default look, grey if drawn from tokens. It also shows each control's id and part, and lists the load report's problems.
- Quality tiers. Preferences → Display → Skin & Layout → Quality picks Basic (images only), Standard (plus static effects), Full (plus moving effects) or Auto (Standard, dropping to Basic if the show needs it). Check your skin at Basic: effects are polish, and the images alone should look complete.
What QUP does with mistakes
| Severity | Examples | Result |
|---|---|---|
| Fatal | invalid JSON; a bad id; an inheritance loop; a missing parent |
the default skin loads instead, and QUP says why |
| Problem | a missing @1x image; nine without usable insets; an unknown mode or effect; a bad colour |
only that layer, part or token is dropped |
| Warning | a missing @2x; @2x not exactly double; states of different sizes; unknown keys | loaded as best it can be |
| Fallback | a control your skin has no part for | that control keeps the default look |
Making it look good
- Light comes from the top-left, everywhere: knobs, bevels, wells. Pressed states invert the bevel; they never shift the art.
- Labels must read. Aim for
text.plateat 7:1 contrast against your plate,text.wellat 10:1 onwell.bg, and selected-row text at 7:1. - Every state must differ from
normalin greyscale, not just by hue, so it reads for everyone. - Plate textures tile seamlessly with no baked-in lighting, and keep the grain subtle (about ±4%) so text stays clean.
- Use your own art. Anything third-party must be CC0 or public domain, and listed in
sources.
Made something you're proud of? Show it off in the Forums.