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

  1. Download the reference skin, Brushed Steel. It dresses every control in both modes, so it's the best starting point.
  2. In QUP, open Preferences → Display → Skin & Layout and click Open My Skins Folder.
  3. Unzip Brushed Steel there and rename the folder, say my-skin.
  4. In its skin.json, change "id" to "my-skin" and "name" to your skin's name. The id is what QUP knows a skin by, so it must be unique.
  5. 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 twice 1x/ 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:

  1. a part named with its own id, e.g. "dj.a.play": { ... };
  2. the part an override names for it;
  3. 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.plate at 7:1 contrast against your plate, text.well at 10:1 on well.bg, and selected-row text at 7:1.
  • Every state must differ from normal in 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.