Changelog: @tremolo-ui/functions
0.9.0
Minor Changes
-
#391
62a07a4Thanks @m1m0zzz! - Drop the MIDI error constantsNOT_SUPPORTED,PERMISSION_DENIEDandUNAVAILABLEfrom@tremolo-ui/dom.MIDIAccessErroris a union of those strings, so compareerrorwith'NOT_SUPPORTED'and the rest directly: the type still catches a misspelling.PITCH_BEND_CENTERmoves from@tremolo-ui/domto@tremolo-ui/functions, next to a newnormalizePitchBend, which turns a 14-bit pitch bend into -1 to- It divides each side of the centre by its own length, so both ends are reached exactly.
0.8.0
No changes in this release.
0.7.0
Minor Changes
-
#299
a2301eeThanks @m1m0zzz! -@tremolo-ui/functionsdrops five exports that were never meant for you. The package is for general-purpose functions — the ones worth having whether or not you use the rest of this library — and these were implementation details that became public by being pure.Removed Instead mod((n % m) + m) % m, three characters longer than the importxora !== b, or!!a !== !!bwhen either side may beundefinedintegerPartString(Math.trunc(x))decimalPartString(x).split('.')[1], though see belowSIGNIFICANT_DIGITSnothing. It was only ever the default of toPrecision, which still applies on its ownintegerPartanddecimalPartreturnedstring | undefinedand both broke on the exponent form:String(1e-7)is'1e-7', which has no decimal point, sodecimalPartreported no digits at all.Slider.Markslabels an interval written in exponent form correctly. It useddecimalPartto decide how far to round each label back, so asteporperof1e-7rounded every label to a whole number. It now counts the digits the exponent stands for. -
#302
67be12fThanks @m1m0zzz! - Reading the modifier keys of an input event moves to@tremolo-ui/dom.applyDelta,selectModifier,mapModifierand the typesInputEventOption,Modifier,ModifierMap,ModifierStateandModifierValueare no longer exported by@tremolo-ui/functions; import them from@tremolo-ui/dominstead.- import { applyDelta, type ModifierValue } from '@tremolo-ui/functions'+ import { applyDelta, type ModifierValue } from '@tremolo-ui/dom'ModifierStateis the shape of the modifier flags on a DOM event, andapplyDeltadecides how far one wheel notch or one key press moves a value — the same jobcreateDragValuealready does for a drag. They now sit together, which is also what a Vue or Svelte wrapper needs in order to implement keyboard control without reading the React components.selectInputEventis gone. It returned whatselectModifierreturns, withvaluerenamed tooption, and nothing else:- const { option, modifier } = selectInputEvent(keyboard, event)+ const { value: option, modifier } = selectModifier(keyboard, event)With this,
@tremolo-ui/functionsis four groups of general-purpose functions: value distributions, numeric conversion, music theory and display formatting. -
#301
6fbc117Thanks @m1m0zzz! - The geometry of a drawn keyboard moves to@tremolo-ui/dom.noteAt,notePosition,pianoWidth,blackKeyWidth,getNoteRangeArray,PianoLayoutandNoteRangeare no longer exported by@tremolo-ui/functions; import them from@tremolo-ui/dominstead. Nothing about them changed.- import { noteAt, type PianoLayout } from '@tremolo-ui/functions'+ import { noteAt, type PianoLayout } from '@tremolo-ui/dom'They are pixel positions and hit testing for a keyboard that has been drawn, which is the layer
@tremolo-ui/domis for.@tremolo-ui/functionskeeps the music theory —noteNumber,noteName,noteToFrequency, the scales — which is useful whether or not anything is on screen.
0.6.0
Minor Changes
-
#214
fc5b383Thanks @m1m0zzz! - 数値・音名・Piano geometry の境界条件を検証し、不正値や極端な有限値にも一貫した結果を返すよう修正します。InputEventOptions型エイリアスを削除し、ModifierValue<InputEventOption>に統一します。 -
#183
a712bc2Thanks @m1m0zzz! - Shift makes aNumberInput.Stepperdrag fine too.dragSensitivityonNumberInput.Rootsays how much the drag counts per modifier key, default{ default: 1, shift: 0.1 }:dragpixels then move a tenth of asteprather than a whole one. As on the arrow keys, a modifier amount is not snapped back onto the grid.The three handlers of
useDragnow receive the wholeDragState.onDraghad only the four numbers, and the modifier keys live on the event:useDrag({ onDrag: (x, y, deltaX, deltaY, state) => state.event.shiftKey })Arguments were added rather than replaced, so existing handlers keep working.
mapModifieris new in@tremolo-ui/functions. It carries a per-modifier setting over to another kind of setting, keeping which modifier each belongs to:mapModifier({ default: 1, shift: 0.1 }, (f) => ['raw', step * f])// { default: ['raw', 1], shift: ['raw', 0.1] }Keeping the map rather than resolving it first is what lets a fine drag move at all — naming a modifier is also what takes
stepout of the pipeline. -
#170
d1dc65fThanks @m1m0zzz! -wheelandkeyboardamounts can now name a modifier key.keyboard={{ default: ['raw', 1], shift: ['raw', 0.1] }}InputEventOptionsaccepts either the old tuple or a map with adefaultand any ofshift/alt/ctrl/meta.selectInputEventresolves one against an event, andapplyDeltatakes the event as a fifth argument.A modifier entry is not snapped to
step. Naming one is a deliberate request to move off the grid, and without the carve-out a finer amount would round straight back to where it started. -
#157
2ff5c1dThanks @m1m0zzz! -NumberInputno longer takesunitsordigit.formatandparseare the only way to control the displayed text, andunitFormatbuilds both:- <NumberInput.Root units={[['Hz', 1], ['kHz', 1000]]} digit={2} ... >+ <NumberInput.Root {...unitFormat('Hz', { digits: 2 })} ... >Units,formatValue,parseValueandselectUnitare removed from@tremolo-ui/functionsalong with them.parsenow reads text with no number in it asNaN, and the input keeps its current value instead of committing a zero the user never typed. -
#216
e31065cThanks @m1m0zzz! - 使用されていなかったisEmptyを公開 API と実装から削除します。 -
#190
83fa74bThanks @m1m0zzz! -styleHelperis removed. It turned a number into a pixel length, with an optional bit of arithmetic —styleHelper(10, '/', 2)giving'5px'.Nothing uses it any more. Its one caller was the border radius of
Slider.Track, which iscalc(var(--thickness) / 2)in CSS now that the track no longer paints itself from JavaScript.Both of its cases are a line of their own:
typeof value === 'number' ? `${value}px` : value`calc(${value} / 2)`It also got the string case wrong —
styleHelper('2rem', '/', 2)returnedcalc(2rempx / 2)— which is one more reason not to keep it. -
#156
84cbcdbThanks @m1m0zzz! - AddunitFormat, which builds theformatandparsepair of a unit.<NumberInput.Root {...unitFormat('Hz', { digits: 2 })} value={v} onChange={setV}>It picks an SI prefix by magnitude (
1234reads1.23kHz), takes abaseoption for values that are already stored in a prefixed unit (milliseconds areunitFormat('s', { base: 'm' })), and can be told to add no prefix at all withprefixes: false, for dB, %, cents and the like.
Patch Changes
-
#203
041483eThanks @m1m0zzz! - FixstepValue()rounding a value to the wrong step.Two cases were wrong. A step small enough that JavaScript prints it in exponential notation — anything under
1e-6— has no decimal part to read a digit count from, and the result was rounded to a whole number instead: a control withstep={1e-7}snapped every value below half a step to0. And a value sitting exactly on a half step went up or down depending on where the error of the division happened to fall, so0.25rounded up to0.3while0.15and0.35rounded down.Both came from deciding the step by comparing the distance to the two neighbouring grid points, each computed with its own rounding. The quotient is now rounded directly, after its artefact is cleared, and a half step always goes up.
-
#179
e180798Thanks @m1m0zzz! - Values no longer collect binary float debris. Holding shift and pressing the arrow key twelve times from 5 used to reach 5.699999999999998, and the input showed exactly that. The fine-adjustment modifier deliberately takesstepout of the pipeline, andstepwas the only thing rounding the artefact back.A value is now rounded to the 15 significant digits a double actually carries, at the two places a value is produced:
applyDelta, which the wheel and the arrow keys go through, andcreateDragValue, which every drag goes through. Rounding happens before the clamp, sominandmaxstill have the last word.This is not rounding in the sense
stepis.stepputs a value on a grid you asked for; this removes digits that were never in the value — the result of a float calculation already carries error that size or larger, so nothing real is lost. There is no way to turn it off, and no reason to want one.toPrecision(x, significantDigits = 15)is exported from@tremolo-ui/functionsalongside the existingtoFixed, withSIGNIFICANT_DIGITSfor the default.
0.5.0
Minor Changes
-
#142
0646236Thanks @m1m0zzz! - Replaceskewwithscale, and add scales that do not break down at the ends of the rangeBreaking. The
skewprop ofSlider.Root,Knob.Root,XYPad.RootandNumberInput.Root, andValueRange.skewin@tremolo-ui/functions, are replaced byscale, which takes aScale.-<Knob.Root min={20} max={22000} skew={skewWithCenterValue(663, 20, 22000)} …>+<Knob.Root min={20} max={22000} scale={exponentialScale} …>@tremolo-ui/functionsgains theScaleinterface and five scales:linearScale— the default. Equal travel, equal change in valueexponentialScale— equal travel, equal ratio: every octave takes the same distance. For frequency, free running rates and delay times. Requiresminandmaxto be non-zero and of the same signcurveScale(curve)— an exponential bend that still passes throughminandmax, so it works on a range that starts at or crosses 0.curve > 0favours the lower end,curve < 0the upper end. Pair it withcurveWithCenterValue()symmetricSkewScale(skew)— the same bend mirrored about the middle, for a bipolar control that needs fine adjustment around its centreskewScale(skew)— the power law of JUCE'sNormalisableRange, for a value that has to agree with a JUCE or iPlug2 parameter.skewWithCenterValue()still applies to it
A
Scaletakesminandmaxas arguments rather than holding them, so it carries no state and can be a module level constant.normalizeValue()andrawValue()lose theirskewparameter and are now the linear mapping alone — every curve lives in aScale.applyDelta()takes the scale through itsValueRange.skewWithCenterValue(),ValueRangeandapplyDelta()keep their behaviour and move next to the scales they belong to; the names exported from the package are unchanged.-normalizeValue(value, min, max, skew)+skewScale(skew).normalize(value, min, max)-rawValue(position, min, max, skew)+skewScale(skew).denormalize(position, min, max)This fixes the value jumping on a knob with a logarithmic scale. The power law is applied to
value - min, so its slope atminis either infinite or zero: a dB knob over-60..6moved 12% of its range on the first pixel of a drag, and a frequency knob over20..22000did not move at all for the first 12 pixels.exponentialScaleandcurveScalehave neither problem.skewScalestill behaves this way, since matching JUCE is the point of it. -
#146
fe74061Thanks @m1m0zzz! - RedesignPiano: the keyboard is no longer built from key components, and several fingers can play at once.Piano.Rootused to draw nothing on its own — a note only sounded becausePiano.WhiteKey/Piano.BlackKeycalledonPlayNotefrom an imperative handle, whichRootreached through an array of refs indexed by the order of its children. Custom children, or children in a different order, silently broke both the sound and the highlighting. The key geometry was kept in two places as well:Roothit-tested with its own white key width while each key drew itself with its ownwidthprop, so<Piano.WhiteKey width={60} />drew a key that responded somewhere else.Rootnow owns the keys and everything that is sounding. Per-key customization is two callbacks rather than a component per key.<Piano.RootnoteRange={{ first: noteNumber('C3'), last: noteNumber('B4') }}label={(note, { index }) => SHORTCUTS.HOME_ROW.keys[index]}keyProps={(note) => ({ 'data-in-scale': inScale(note, root, 'major') })}onPlayNote={(note) => synth.triggerAttack(noteName(note))}onStopNote={(note) => synth.triggerRelease(noteName(note))}/>Each key carries
data-note,data-note-key,data-activeandaria-disabled, so static styling needs no callback at all. The geometry of a key is applied after whateverkeyPropsreturns, so a key can no longer be drawn away from where it responds.Every way of playing a note — pointers, keyboard shortcuts,
playNote()from the ref — now goes through one instance that counts the sources holding each note, so a note stops only once the last of them lets go. Multi-touch and glissando work:createDraggainedmultiPointer, and@tremolo-ui/domgainedcreatePianoInput.Also new in
@tremolo-ui/functions: musical scales (scaleIntervals,inScale,scaleNotes) inmidi, and the piano geometry (PianoLayout,notePosition,noteAt,pianoWidth) inpiano.Breaking changes:
Piano.WhiteKey,Piano.BlackKey,Piano.KeyLabeland theKeyProps/KeyMethods/KeyLabelPropstypes are removed. UselabelandkeyProps, or plain CSS on.tremolo-piano-white-key/.tremolo-piano-black-keylabeltakes(note, state)rather than(note, index); the index isstate.index. A label of'',nullorundefinednow draws nothing rather than an empty boxgetNoteRangeArrayand theNoteRangetype moved to@tremolo-ui/functionswhiteNoteWidthis nowwhiteKeyWidth, and the deadblackNoteWidthprop is replaced byblackKeyWidthRatio/blackKeyHeightRatio/keyGapKeyboardShortcuts.flagsis removed. It was declared but never implemented;SHORTCUTS.HOME_ROW_NATURALcovers whatnaturalOnlywas for, by leaving an empty string where a note has no shortcut
-
#141
95df589Thanks @m1m0zzz! - RebuildNumberInputon the compound pattern the other components already use, and give it one piece of state: the text being typed.The value is a
number, and what the input shows isformat(value)— except while the user is typing, when their own text stands until it is committed. Everything else is derived during render. The<input>is no longer rendered for you; compose it asNumberInput.InputField, the waySlider.Trackis composed.<NumberInput.Root value={value} min={0} max={100} units="Hz" onChange={setValue}><NumberInput.InputField /><NumberInput.Stepper><NumberInput.IncrementStepper /><NumberInput.DecrementStepper /></NumberInput.Stepper></NumberInput.Root>Keeping the typed text rather than reformatting it on every keystroke removes the cursor-position restoring that used to be needed, and lets a value be entered digit by digit: typing is never clamped, and the entry is brought into range when it is committed on blur or Enter.
Breaking changes to
NumberInput:childrenis required, andInputFieldhas to be composed in. There is no default markup to fall back tovalueis anumber; astringis no longer acceptedonChangereports(value: number)rather than(value, text)variant,activeColorandwrapperClassNameare gone.Rootis the wrapper, so itsclassNamestyles it, and the colors are CSS variables. The four variants are shown in the documentation as CSS to copykeepWithinRangeandclampValueOnBlurare oneclampValueprop (defaulttrue)selectWithFocusandblurOnEntermoved toInputField, asselectOnFocusandblurOnEnter. ItsonFocus/onBlurare plain DOM handlersStepperlostdynamic, and the steppers lostsize: both are styling, now the--stepper-icon-sizevariable and the demo CSS- the class names follow the parts:
tremolo-number-inputis the root andtremolo-number-input-fieldthe input, where the root used to betremolo-number-input-wrapper data-errorisdata-out-of-range
New in
NumberInput:skew,format/parsefor text the units cannot express, and dragging theStepperup and down to move the value, onestepeverydragpixels (1 by default). It needs no range to work against, so an unbounded input can be dragged too.The
<input>is now the tab stop and carries the spinbutton role and range, where the wrapper used to take focus and the input was skipped.Wheel control now only acts while the focus is inside the component, across
Slider,Knob,XYPadandNumberInput. Reacting on hover alone took the scroll away from the page, so passing over a control while reading changed its value.createWheelgained arequireFocusoption, andupdate().@tremolo-ui/functionsgainsformatValue,parseValue,selectUnitand theUnitstype, moved out of@tremolo-ui/reactso that they are available to any wrapper.It also gains
applyDelta, which moves a value by one wheel notch or arrow key press.Slider,Knob,XYPadandNumberInputeach had their own copy of this; they now share one, andAxisOptionsof@tremolo-ui/domextends itsValueRange, so a drag and a nudge describe their scaling the same way.Fixes
NumberInputthrowing"min" and "max" are requiredfromwheelorkeyboardinnormalizedmode wheneverminwas0, which a truthiness check rejected.