taut: mouse-operated slider bar

This commit is contained in:
minjaesong
2026-06-04 11:46:06 +09:00
parent e27a01dca6
commit 729e5246c9
2 changed files with 199 additions and 29 deletions

View File

@@ -4007,6 +4007,163 @@ function drawGroupHeader(y, title) {
print(txt + `\u00FB`.repeat(dashes)) print(txt + `\u00FB`.repeat(dashes))
} }
// ── Inline value sliders (Gen.1 / Gen.2 knob editing) ──────────────────────
// A horizontal slider painted alongside a numeric field. The knob is one 7-px
// cell wide and slides with per-pixel precision via the sym.slider1..7 glyphs
// (slider1 = knob snug in one cell; slider2..7 straddle two cells at a 1..6 px
// offset). The trough is a flat colBLACK bar capped by inverse-video round pads
// (0xAB left, 0xAA right). Two trough widths only: small (10) and wide (20).
//
// Clicking/dragging a trough drives the knob: the label updates live as the knob
// moves, and the instrument byte(s) are written only on mouse release (see
// runSliderDrag). instSliders is rebuilt on every Gen.1/Gen.2 body repaint and
// hit-tested by the panel's slider mouse region.
const SLIDER_TW_SMALL = 25
const SLIDER_TW_WIDE = 36
const SLIDER_LABEL_W = 10
const SLIDER_END_COL = SCRW - 1 // common right edge
const SLIDER_SMALL_SX = SLIDER_END_COL - (SLIDER_TW_SMALL + 1) // small left-pad col
const SLIDER_WIDE_SX = SLIDER_END_COL - (SLIDER_TW_WIDE + 1) // wide left-pad col
const SLIDER_VALUE_W = SLIDER_SMALL_SX - (INST_RIGHT_X + SLIDER_LABEL_W)
const sliderGlyphs = [sym.slider1, sym.slider2, sym.slider3, sym.slider4,
sym.slider5, sym.slider6, sym.slider7]
// Rebuilt by drawInstTabGeneral1/2; each entry is
// { y, sx, tw, troughLeftPx, min, max, render(val), commit(val) }.
let instSliders = []
// Paint the trough + knob for value-fraction `frac` (0..1) at (y, sx).
function drawSlider(y, sx, tw, frac) {
const pmax = (tw - 1) * CELL_PW
const p = Math.round((frac < 0 ? 0 : frac > 1 ? 1 : frac) * pmax)
const cell = (p / CELL_PW) | 0
const sub = p - cell * CELL_PW
const cells = new Array(tw).fill(' ')
if (sub === 0) cells[cell] = sliderGlyphs[0]
else {
const g = sliderGlyphs[sub] // 2-char glyph straddling cell..cell+1
cells[cell] = g[0]
if (cell + 1 < tw) cells[cell + 1] = g[1]
}
con.color_pair(colBLACK, colStatus); con.move(y, sx); con.prnch(0xAB)
con.color_pair(colStatus, colBLACK); con.move(y, sx + 1); print(cells.join(''))
con.color_pair(colBLACK, colStatus); con.move(y, sx + tw + 1); con.prnch(0xAA)
}
// Pixel X (mouse) → quantised slider value, knob centred under the cursor.
function sliderMouseToVal(s, pxX) {
const pmax = (s.tw - 1) * CELL_PW
let knob = Math.round((pxX - s.troughLeftPx) - CELL_PW / 2)
if (knob < 0) knob = 0
if (knob > pmax) knob = pmax
const frac = (pmax === 0) ? 0 : knob / pmax
let v = Math.round(s.min + frac * (s.max - s.min))
if (v < s.min) v = s.min
if (v > s.max) v = s.max
return v
}
// Write byte pairs [[offset, value], ...] into instrument `slot`'s peripheral
// record. The audio adapter decodes these live, so edits take effect at once.
function instWriteBytes(slot, pairs) {
const memBase = audio.getMemAddr()
const base = TAUT_INST_WINDOW_OFF + slot * TAUT_INST_RECORD_SIZE
for (let i = 0; i < pairs.length; i++) {
sys.poke(memBase - (base + pairs[i][0]), pairs[i][1] & 0xFF)
}
}
// Drag interaction: live label updates while held, commit on release, ESC cancels.
function runSliderDrag(s, downEvent) {
let val = sliderMouseToVal(s, downEvent[1])
let committed = false
s.render(val)
let dragging = true
while (dragging) {
input.withEvent(e => {
const t = e[0]
if (t === 'mouse_move') {
const nv = sliderMouseToVal(s, e[1])
if (nv !== val) { val = nv; s.render(val) }
} else if (t === 'mouse_up') {
dragging = false; committed = true
} else if (t === 'key_down' && e[1] === '<ESC>') {
dragging = false
}
// mouse_down echo and other events are ignored during a drag
})
}
if (committed) s.commit(val)
drawInstrumentsContents()
}
// fmt helpers — kept short so the value still fits the SLIDER_VALUE_W field.
function fmtByte255(v) { return '$' + _hex(v, 2) + ' (' + v + ')' }
function fmtSigned(v) { return _signed(v) }
function fmtFilter(v) { return (v === 0xFF) ? 'off' : ('$' + _hex(v, 2) + ' (' + v + ')') }
function fmtFadeout(v) {
if (v <= 0) return '0 none'
if (v >= 1024) return '1024 cut'
return v + ' ~' + (1024 / v).toFixed(1) + 't'
}
// Emit a small-slider row: label, numeric value, then the knob. `encode(val)`
// returns the byte pairs to poke on commit.
function sliderRow(y, e, label, val0, min, max, fmt, encode) {
const sx = SLIDER_SMALL_SX, tw = SLIDER_TW_SMALL
const render = (val) => {
if (val < min) val = min
if (val > max) val = max
con.move(y, INST_RIGHT_X)
con.color_pair(colInstLabel, colBackPtn)
print((label + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W))
con.color_pair(colInstValue, colBackPtn)
print((fmt(val) + ' '.repeat(SLIDER_VALUE_W)).substring(0, SLIDER_VALUE_W))
drawSlider(y, sx, tw, (max === min) ? 0 : (val - min) / (max - min))
}
render(val0)
instSliders.push({
y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render,
val: (val0 < min ? min : val0 > max ? max : val0), // current value, for wheel ±1 deltas
commit: (v) => { instWriteBytes(e.slot, encode(v)); e.decoded = decodeInstFull(readInstRecord(e.slot)) }
})
}
// Emit the wide two-row Detune slider: knob on `y`, numeric labels on `y+1`.
function detuneRow(y, e, val0) {
const sx = SLIDER_WIDE_SX, tw = SLIDER_TW_WIDE
const min = -32768, max = 32767
const render = (val) => {
if (val < min) val = min
if (val > max) val = max
con.move(y, INST_RIGHT_X)
con.color_pair(colInstLabel, colBackPtn)
print((' Detune:' + ' '.repeat(SLIDER_LABEL_W)).substring(0, SLIDER_LABEL_W))
drawSlider(y, sx, tw, (val - min) / (max - min))
con.move(y + 1, INST_RIGHT_X)
con.color_pair(colInstValue, colBackPtn)
const s = ' ' + _signed(val) + ' (' + (val / 0x1000).toFixed(3) + ' octave, 4096-TET)'
print((s + ' '.repeat(INST_RIGHT_W)).substring(0, INST_RIGHT_W))
}
render(val0)
instSliders.push({
y, sx, tw, troughLeftPx: sx * CELL_PW, min, max, render,
val: (val0 < min ? min : val0 > max ? max : val0), // current value, for wheel ±1 deltas
commit: (v) => { instWriteBytes(e.slot, [[184, v & 0xFF], [185, (v >> 8) & 0xFF]]); e.decoded = decodeInstFull(readInstRecord(e.slot)) }
})
}
// Hit-test the live instSliders list for a cell (cy, cx). Gen.1/Gen.2 only.
function sliderAt(cy, cx) {
if (instSubTab !== INST_TAB_GEN1 && instSubTab !== INST_TAB_GEN2) return null
for (let i = 0; i < instSliders.length; i++) {
const s = instSliders[i]
if (cy === s.y && cx >= s.sx && cx <= s.sx + s.tw + 1) return s
}
return null
}
// ── Tab body: General (page 1 + page 2) ─────────────────────────────────── // ── Tab body: General (page 1 + page 2) ───────────────────────────────────
// Page 1 (Gen.1): // Page 1 (Gen.1):
// Sample binding — sample link, length, c4Rate, play/loop positions, loop mode // Sample binding — sample link, length, c4Rate, play/loop positions, loop mode
@@ -4053,25 +4210,18 @@ function drawInstTabGeneral1(e) {
y++ y++
drawGroupHeader(y++, 'Volume') drawGroupHeader(y++, 'Volume')
drawLabelRow(y++, ' Inst. GV:', _hex(d.igv, 2) + ' (' + d.igv + '/255)') sliderRow(y++, e, ' Inst.GV:', d.igv, 0, 255, fmtByte255, (v) => [[171, v]])
drawLabelRow(y++, ' DefNote:', _hex(d.defNoteVol, 2) + ' (' + d.defNoteVol + '/255' + sliderRow(y++, e, ' DefNote:', d.defNoteVol, 0, 255, fmtByte255, (v) => [[196, v]])
(d.defNoteVol === 0 ? ' legacy: row default 63' : '') + ')') sliderRow(y++, e, ' Fadeout:', d.fadeout, 0, 1024, fmtFadeout, (v) => [[172, v & 0xFF], [173, (v >> 8) & 0x0F]])
let fadeStr sliderRow(y++, e, ' Swing:', d.volSwing, 0, 255, fmtByte255, (v) => [[174, v]])
if (d.fadeout === 0) fadeStr = '0 (no fade)'
else if (d.fadeout >= 1024) fadeStr = d.fadeout + ' (1-tick cut)'
else {
const ticks = (1024 / d.fadeout)
fadeStr = d.fadeout + ' (~' + ticks.toFixed(1) + ' ticks)'
}
drawLabelRow(y++, ' Fadeout:', fadeStr)
drawLabelRow(y++, ' Swing:', _hex(d.volSwing, 2))
y++ y++
drawGroupHeader(y++, 'Panning') drawGroupHeader(y++, 'Panning')
drawLabelRow(y++, ' Default:', _hex(d.defPan, 2) + ' Use: ' + sliderRow(y++, e, ' Default:', d.defPan, 0, 255, fmtByte255, (v) => [[177, v]])
sliderRow(y++, e, ' Sep:', d.pitchPanSep, -128, 127, fmtSigned, (v) => [[180, v & 0xFF]])
sliderRow(y++, e, ' Swing:', d.panSwing, 0, 255, fmtByte255, (v) => [[181, v]])
drawLabelRow(y++, ' PPanCnt:', '$' + _hex(d.pitchPanCenter, 4) + ' Use: ' +
(d.panEnv.panUseDef ? sym.ticked + ' on' : sym.unticked + ' off')) (d.panEnv.panUseDef ? sym.ticked + ' on' : sym.unticked + ' off'))
drawLabelRow(y++, ' PPanCtr:', '$' + _hex(d.pitchPanCenter, 4) + ' Sep: ' + _signed(d.pitchPanSep))
drawLabelRow(y++, ' Swing:', _hex(d.panSwing, 2))
} }
function drawInstTabGeneral2(e) { function drawInstTabGeneral2(e) {
@@ -4079,27 +4229,27 @@ function drawInstTabGeneral2(e) {
let y = INST_BODY_Y let y = INST_BODY_Y
drawGroupHeader(y++, 'Filter') drawGroupHeader(y++, 'Filter')
drawLabelRow(y++, ' Cutoff:', (d.defCutoff === 0xFF ? 'off' : ('$' + _hex(d.defCutoff, 2) + sliderRow(y++, e, ' Cutoff:', d.defCutoff, 0, 255, fmtFilter, (v) => [[182, v]])
' (' + d.defCutoff + '/254)'))) sliderRow(y++, e, ' Reso:', d.defReso, 0, 255, fmtFilter, (v) => [[183, v]])
drawLabelRow(y++, ' Reso:', (d.defReso === 0xFF ? 'off' : ('$' + _hex(d.defReso, 2) +
' (' + d.defReso + '/254)')))
y++ y++
drawGroupHeader(y++, 'Vibrato') drawGroupHeader(y++, 'Vibrato')
drawLabelRow(y++, ' Waveform:', VIB_WF_NAMES[d.vibWaveform & 7]) drawLabelRow(y++, ' Wave:', VIB_WF_NAMES[d.vibWaveform & 7], SLIDER_LABEL_W)
drawLabelRow(y++, ' Speed:', _hex(d.vibSpeed, 2) + ' Depth: ' + _hex(d.vibDepth, 2)) sliderRow(y++, e, ' Speed:', d.vibSpeed, 0, 255, fmtByte255, (v) => [[175, v]])
drawLabelRow(y++, ' Sweep:', _hex(d.vibSweep, 2) + ' Rate: ' + _hex(d.vibRate, 2)) sliderRow(y++, e, ' Depth:', d.vibDepth, 0, 255, fmtByte255, (v) => [[187, v]])
sliderRow(y++, e, ' Sweep:', d.vibSweep, 0, 255, fmtByte255, (v) => [[176, v]])
sliderRow(y++, e, ' Rate:', d.vibRate, 0, 255, fmtByte255, (v) => [[188, v]])
y++ y++
drawGroupHeader(y++, 'Note actions') drawGroupHeader(y++, 'Note actions')
drawLabelRow(y++, ' NNA:', NNA_NAMES[d.nna & 3]) drawLabelRow(y++, ' NNA:', NNA_NAMES[d.nna & 3], SLIDER_LABEL_W)
drawLabelRow(y++, ' DCT:', DCT_NAMES[d.dct & 3]) drawLabelRow(y++, ' DCT:', DCT_NAMES[d.dct & 3], SLIDER_LABEL_W)
drawLabelRow(y++, ' DCA:', DCA_NAMES[d.dca & 3]) drawLabelRow(y++, ' DCA:', DCA_NAMES[d.dca & 3], SLIDER_LABEL_W)
y++ y++
drawGroupHeader(y++, 'Tuning') drawGroupHeader(y++, 'Tuning')
const detStr = _signed(d.detune) + ' (' + (d.detune / 0x1000).toFixed(3) + ' octave, 4096-TET)' detuneRow(y, e, d.detune)
drawLabelRow(y++, ' Detune:', detStr) y += 2
} }
// ── Envelope rendering (shared by Volume/Panning/Pitch tabs) ─────────────── // ── Envelope rendering (shared by Volume/Panning/Pitch tabs) ───────────────
@@ -4320,6 +4470,7 @@ function clearInstrumentsPanel() {
function drawInstrumentsContents(wo) { function drawInstrumentsContents(wo) {
if (instrumentsCache === null) refreshInstrumentsCache() if (instrumentsCache === null) refreshInstrumentsCache()
clampInstrumentsCursor() clampInstrumentsCursor()
instSliders.length = 0 // rebuilt by the Gen.1/Gen.2 body drawers below
clearInstrumentsPanel() clearInstrumentsPanel()
drawInstrumentsListColumn() drawInstrumentsListColumn()
drawInstrumentsSeparator() drawInstrumentsSeparator()
@@ -4431,6 +4582,25 @@ function registerInstrumentsMouse() {
requestEditorLaunch('taut_instredit', [fullPathObj.full, VIEW_INSTRMNT, slot]) requestEditorLaunch('taut_instredit', [fullPathObj.full, VIEW_INSTRMNT, slot])
} }
}) })
// Slider body (Gen.1 / Gen.2): one region that hit-tests the live instSliders
// list. Click/drag the matched knob until mouse release; wheel nudges by ±1
// (wheel up = +1) and commits each notch for fine control.
addPanelMouseRegion(INST_RIGHT_X, INST_BODY_Y, INST_RIGHT_W, INST_BODY_H, {
onClick: (cy, cx, btn, ev) => {
if (btn !== 1) return
const s = sliderAt(cy, cx)
if (s) runSliderDrag(s, ev)
},
onWheel: (cy, cx, dy) => {
const s = sliderAt(cy, cx)
if (!s) return
const nv = Math.max(s.min, Math.min(s.max, s.val + (dy < 0 ? 1 : -1)))
if (nv === s.val) return
s.val = nv
s.render(nv)
s.commit(nv)
}
})
} }
///////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -2218,7 +2218,7 @@ from source.
* FastTracker2 has instrumentwise config (0..255) * FastTracker2 has instrumentwise config (0..255)
* The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2 * The spec follows FastTracker2, and conversion must be performed when importing from FastTracker2
176 Uint8 Vibrato sweep 176 Uint8 Vibrato sweep
* FastTracker2 instrument config * FastTracker2 instrument config (0..255)
177 Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag) 177 Uint8 Default pan value (0..255 full range, see offset 17 for the enable flag)
* ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config * ImpulseTracker has samplewise default pan and instrumentwise default pan, and they must be taken into account because Taud has no samplewise config
178 Uint16 Pitch-pan centre (4096-TET note value) 178 Uint16 Pitch-pan centre (4096-TET note value)
@@ -2226,7 +2226,7 @@ from source.
181 Uint8 Pan swing (0..255 full range) 181 Uint8 Pan swing (0..255 full range)
182 Uint8 Default cutoff (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud) 182 Uint8 Default cutoff (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
183 Uint8 Default resonance (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud) 183 Uint8 Default resonance (0..254 full range, 255 to off (-1 on IT). Effect range equals to that of ImpulseTracker -- 127 in IT is equal to 254 in Taud)
184 Uint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly) 184 Sint16 Sample detune (in 4096-TET unit) (FT2 finetune scale need to be rescaled accordingly)
186 Bit8 Instrument Flag 186 Bit8 Instrument Flag
0b 000 www nn 0b 000 www nn
n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT) n: New note action. 00: note off, 01: note cut, 10: continue, 11: note fade (arranged differently to IT)