Changelog: @tremolo-ui/dom
0.9.0
Minor Changes
-
#403
1001907Thanks @m1m0zzz! - Report when a change of value starts and ends, whatever makes it, for hosts that record automation and need to know when a control is touched.Knob,SliderandXYPadtakeonChangeStart(value, source)andonChangeEnd(value, source)(change-start/change-endin Vue). The start gets the value before the change, the end the value it ended on, andsourcesays what made it:'pointer','wheel','keyboard'or'doubleClick'.- A drag starts on the press and ends on release
- The wheel and the arrow keys have no end of their own, so the change ends
changeEndDelay(default 500 ms) after the last notch or key press - A double click on
Knobthat resets the value starts and ends around it
These replace
onDragStart/onDragEndonSliderandXYPad(drag-start/drag-endin Vue): a drag is now one of the sources. To keep reacting to drags only, checksource === 'pointer'.The tracking lives in
@tremolo-ui/domascreateChangeGesture, with theChangeSourcetype.createDragValuenow callsonDragStartbefore theonChangeof the press withupdateOnPointerDown, so that a drag is reported as started before its first value. -
#390
ecabb23Thanks @m1m0zzz! - Stop exporting what no wrapper uses:drawingState,isDrawingStateand theDrawingContext/DrawingState/DrawingStateValuetypes,knobArcPoint,isArrowKeyandArrowKey,mapModifier,noteAt,createSelectionBox,selectionBoxCoversand their option and instance types, andmatchesAccept.SelectionBoxRectstays, as the shape of a points editor's selection box.isDrawingStateis removed outright. -
#406
c9df5f4Thanks @m1m0zzz! - Split@tremolo-ui/dominto two entries before 1.0, when everything in the main one comes under semver.The main entry keeps what users of the components and of this package write: the
create*interactions, the values and types that appear in props (the default input options,SHORTCUTS, the mappings,XYand so on), and the geometry a part of your own needs to draw (KNOB_VIEWBOX_SIZE,knobArcPath,knobArcRadius,knobAngles,valuePercent,notePosition).What only the wrappers use moves to
@tremolo-ui/dom/internal, which is not covered by semver: the number input's text and draft handling (parseNumberText,numberSpan,commitNumberInputText,nudgeNumberInput, ...),createStepperDrag, the key and wheel direction helpers,applyDeltaandselectModifier, the piano layout apart fromnotePosition,clampPoint/POINT_AXIS/POINTS_EDITOR_DEFAULT_*,sliderMarks,partitionByAccept,replaceOptions,checkSteps,cssLength,visuallyHiddenStyleandtoXY.The wrappers now depend on
@tremolo-ui/domat the exact version rather than a caret range, since they use the internal entry. -
#397
885853fThanks @m1m0zzz! - ReplaceexternalStyles: { cursor }with adragCursorstring onKnob,Slider,XYPadandPointsEditor. The object only ever held the cursor shown while dragging. The defaults stay as they were:'grabbing'forKnoband the points ofPointsEditor,'pointer'forSliderandXYPad.externalStyles={{ cursor: 'move' }}→dragCursor="move"
NumberInputtakes adragCursortoo, for dragging aStepper, where the cursor was fixed to'ns-resize'.createStepperDragin@tremolo-ui/domtakes the matchingcursoroption.The
PointsEditorcontext carries it asdragCursor, in place ofexternalStylesin React andcursorin Svelte and Vue. -
#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.
-
#405
3126d10Thanks @m1m0zzz! -PointsEditor.Pointreports when a change of it starts and ends, like the other controls, in place ofonDragStart/onDragEnd(drag-start/drag-endin Vue):onChangeStart(value, source)/onChangeEnd(value, source)(change-start/change-end), withchangeEndDelay. A drag, the wheel while the point has the focus, and the arrow keys all count.The change belongs to the point being operated: the points that move along with a selection report through
onChangeonly, as they did before.PointsEditorPointin@tremolo-ui/domtakes an optionalbeforeWheel, whichnudgeFocusedPointcalls just before the wheel moves the point. -
#394
d623e55Thanks @m1m0zzz! -createWheeltakes its handler as a requiredonWheeloption, likecreateLongPresstakesonPress:createWheel(element, { onWheel, requireFocus })instead ofcreateWheel(element, onWheel, { requireFocus }). The handler was given twice before, once as an argument and once as an option forupdate().The Svelte
wheelaction takes those same options, soWheelActionOptionsis gone; the object passed touse:wheeldoes not change.The Vue
useWheelno longer acceptsonWheelamong its options. Passing one replaced the handler given as the second argument on the next update, and taking it out again left no handler at all.
Patch Changes
- Updated dependencies [
62a07a4]:- @tremolo-ui/functions@0.9.0
0.8.0
Minor Changes
-
#336
4a7b743Thanks @m1m0zzz! - AddcreateLongPress, the press-and-hold repeat behinduseLongPressand theNumberInputsteppers: it fires once on the press, then everyintervalafterdelay, until the pointer that started it is released. -
#340
63d750fThanks @m1m0zzz! - AddreplaceOptions, which turns a whole new set of options into theupdate()argument that leaves an instance with exactly those — clearing what the new set leaves out instead of keeping the old value. -
#333
a3bb088Thanks @m1m0zzz! - Add the framework-independent pieces the components were computing on their own, so that every wrapper gives the same answer: which way an arrow key or a wheel notch moves a value (arrowKeyDirection,arrowKeyMove,wheelDirection,wheelMove), where a value sits along a track (valuePercent), the knob geometry (knobAngles,knobArcPath, …), the slider marks (sliderMarks), reading a number input's text (parseNumberText,caretDecimalOffset, …), the step check behind the development warnings (checkSteps), the default input options (DEFAULT_KEYBOARD_OPTIONS,DEFAULT_WHEEL_OPTIONS,DEFAULT_DRAG_SENSITIVITY),cssLength,visuallyHiddenStyleandpartitionByAccept. -
#338
8309125Thanks @m1m0zzz! - Add the pieces of a number input that do not depend on a framework:numberInputRanges,nudgeNumberInput,numberInputBoundsandcommitNumberInputTextfor the value, andcreateStepperDragfor dragging the steppers up and down. -
#360
221b3efThanks @m1m0zzz! - Treat everything in a number input's text that is not the unit as the number.selectOnFocus="number"andkeepCaretOnStepnow cover a signed number (+6.0 dB,−6.0 dB), an exponent (1e+21) and a number after a unit (L 30), where they used to select nothing or stop part way.The default
parsereads the same number, and givesNaN, which leaves the value alone, for one it cannot read whole instead of the digits in front:1,000 Hzand1:30no longer commit 1, and a number with a unit in front is left to aparseof your own. -
#337
6a1710fThanks @m1m0zzz! -createPianoInputnow plays notes from the computer keyboard itself, with the newkeyboardShortcutsandkeyboardShortcutsScopeoptions, so that every wrapper's Piano handles held keys, focus loss and changes to the mapping the same way.SHORTCUTSandKeyboardShortcutsmoved to@tremolo-ui/dom: import them from there instead of@tremolo-ui/react. -
#339
b7dec12Thanks @m1m0zzz! - AddcreatePointsEditor, the selection and the moves behindPointsEditor: which points a press or a selection box selects, and how a selection moves as one and stops together at the edge.clampPointand the point type moved to@tremolo-ui/dom; importclampPointandPointPosition(formerlyPointBaseType) from there.PointsEditorContextValuenow carries the editor instance instead of its individual functions. -
#346
479f29eThanks @m1m0zzz! - AddPianoto@tremolo-ui/svelte, withplayNote/stopNoteon the component andlabelas a snippet.@tremolo-ui/domgainsfitWhiteKeyWidth, the key width aresizablepiano fills its container with.
Patch Changes
- #362
eaeeb6fThanks @m1m0zzz! -FileInputandDropZonereport the rejected files after the accepted ones for the same selection or drop. A list of rejected files cleared inonChange/onDropand filled inonRejectnow shows what a mixed selection left out, where it used to be cleared straight away. - Updated dependencies []:
- @tremolo-ui/functions@0.8.0
0.7.0
Minor Changes
-
#305
63ea333Thanks @m1m0zzz! -DropZoneis new. It takes files dropped onto an area and hands them over asFiles, stopping whereFileInputdoes.<DropZone.Root accept="audio/*" onDrop={(files) => load(files[0])}>Drop an audio file here</DropZone.Root>[data-dragover]while files are over it, and[data-invalid]when it can already tell it will not take them — the browser reports the type of what is being dragged, soaudio/*decides before the drop while.wavcannot- Leaving a child does not end the drag.
dragenteranddragleavefire for descendants too, so the pairs are counted - The drop is cancelled even when it is refused. An unhandled drop makes the browser leave the page and open the file
- A drag that ends elsewhere — dropped on another element, cancelled with
Escape, taken out of the window — sends no
dragleave, and is cleared anyway
useDropZoneis new, and is what the component is built on. It makes any element a drop target without wrapping it:const { refCallback, over, invalid } = useDropZone({ accept: 'audio/*', onDrop })return <canvas ref={refCallback} />createDropZoneis new in@tremolo-ui/dom, where the reading of the drag lives so that a wrapper for another framework uses the same one.The demo theme has a
DropZone.module.cssto go with it. -
#304
490ed19Thanks @m1m0zzz! -FileInputis new. It picks files and hands them over asFiles; reading them is yours.<FileInput.Root accept="audio/*" multiple onChange={(files) => load(files)}><FileInput.Trigger>Choose audio</FileInput.Trigger></FileInput.Root>acceptis checked again on the way in. The browser treats the attribute as a hint to the picker, which a person can switch to "All Files". What does not match arrives atonRejectrather thanonChange- Picking the same file twice works. A file input fires no
changeevent while the selection is unchanged, so the value is cleared as soon as it has been read - The native
<input type="file">is still the control: out of sight, in the tab order, and named byFileInput.Trigger, which is a<label>for it
matchesAcceptis new in@tremolo-ui/dom. It answers whether a file satisfies anacceptattribute, with the same extension, MIME type and type group syntax the attribute has.The demo theme has a
FileInput.module.cssto go with it. -
#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.
Patch Changes
0.6.0
Minor Changes
-
#213
91f2486Thanks @m1m0zzz! - ドラッグ終了時の移動、pointer lock、複数インスタンスのスタイル管理など、Pointer API の境界条件を修正します。Wheel の callback はupdate()で差し替えられるようになります。 -
#150
9c5abb1Thanks @m1m0zzz! - Build out the MIDI input: every channel voice message, the channel they arrived on, and devices that come and go.Three bugs came out of it:
- A device plugged in after permission was granted never worked.
createMIDIMessagetook the input list once, when it was created, so a keyboard connected later got no listener and stayed silent until the component remounted. The device list is now followed throughstatechange. - Pitch bend reported its two bytes the wrong way round.
onPitchBendEvent(msb, lsb)was handed(data[1], data[2]), but pitch bend sends the low 7 bits first — the opposite order from every other message. useMIDIInputanduseMIDIMessageresubscribed on every render. The handlers were effect dependencies, so writing one inline tore the listeners down and built them again each time. They are read fresh on every event now, and the listeners stay put.
Beyond note on/off and pitch bend,
createMIDIInputnow decodes control change, program change, polyphonic aftertouch and channel pressure. Every handler is given the channel last, as 0-15.useMIDIInput(midiAccess, {onNoteOnEvent: (note, velocity, channel) => play(note, velocity / 127),onNoteOffEvent: (note) => stop(note),onControlChangeEvent: (controller, value) => {if (controller === 1) setModulation(value / 127)},})createMIDIAccessgained the rest of what a device UI needs:inputsin its state, kept current as devices come and go;request({ sysex: true })for system exclusive; and errors told apart rather than flattened —SecurityErrorandNotAllowedErrorbecomePERMISSION_DENIED,NotSupportedErrorbecomesNOT_SUPPORTED, and everything else becomes the newUNAVAILABLE. A user who said no can be asked again; a browser without the API cannot.Breaking changes:
useMIDIInput(access, onNoteOn, onNoteOff, onPitchBend)takes a handlers object instead:useMIDIInput(access, { onNoteOnEvent, onNoteOffEvent, ... }). Seven handlers do not fit in positional argumentsonPitchBendEventis(value, channel), wherevalueis the 14-bit bend 0-16383, centred at the newPITCH_BEND_CENTER(8192), rather than the two raw bytesuseMIDIAccess().requesttakes options, soonClick={request}has to becomeonClick={() => request()}— otherwise the click event arrives as the options object
- A device plugged in after permission was granted never worked.
-
#208
ba7be4bThanks @m1m0zzz! - Breaking: Piano keyboard shortcuts now listen from the focused root instead of the whole page by default. The root is a focusablegroup; setkeyboardShortcutsScope="window"to keep page-wide shortcuts. Held shortcut notes are released when mappings change, focus is lost, or the component unmounts, and shortcuts no longer play outside the displayed range or while editing text.Lowering
midiMaxnow releases active notes above the new limit, including every source and pointer holding them. -
#259
afce3a3Thanks @m1m0zzz! - Move the selection box ofPointsEditorinto the core ascreateSelectionBox: which items a rectangle covers, and whether a press adds to the selection or replaces it, no longer live in React.PointsEditorContextValue.selectionBoxnow carries the core'sSelectionBoxRect, and the React-onlySelectionBoxtype is gone. -
#175
eec3e53Thanks @m1m0zzz! - Shift makes aKnobdrag count a tenth as much, matching what it already does on the arrow keys.dragSensitivityrebinds it, or takes a bare number to use no modifier.Pressing or releasing the key mid-drag does not disturb the value: the travel so far is kept and the new sensitivity applies from the next movement. Holding it before the pointer goes down applies it from the first pixel.
relativeMappingof@tremolo-ui/domtakes asensitivitycallback for this, read on every move.selectModifierof@tremolo-ui/functionsresolves any per-modifier setting, not just an input amount. -
#187
0721aa2Thanks @m1m0zzz! -PointsEditorselects points, and a selection moves as one, withselectable.It is off by default. Selection changes what a press and a drag mean, and an editor whose points each mean something different — the four handles of an ADSR envelope, say — has nothing to gain from moving them together.
- a press selects the point it landed on
- ctrl or ⌘ adds to the selection — not shift, which is the fine-adjustment key on every control here and cannot be both
- a drag on empty space draws a rubber band and selects what it covers
- dragging or arrow-keying one selected point moves the whole selection
The editor keeps the selection unless you take it over with
selection/onSelectionChange. Points are named by theiridprop, or by one generated to last as long as the point is mounted. A selected point carriesdata-selected="true", and the rubber band is.tremolo-points-editor-marquee.A selection stops as a whole when any one of its points reaches a limit. Clamping each point on its own would leave that one behind while the rest carried on, pulling the selection out of shape.
A drag now moves a point rather than putting it under the pointer. Grabbing a point at its edge used to shift it under the cursor on the first movement. It keeps the offset it was grabbed at now — which is also what makes moving several at once mean anything.
Deleting and duplicating are not included: the points are yours, and only you know what the array behind them is.
createDragof@tremolo-ui/domtakesshouldStart, which decides whether a pointerdown starts a drag at all. It is checked before the pointer is captured, which is the whole point: a rubber band on a container has to decline a press that landed on one of the objects it would select, and declining any later means the capture has already been taken away from the object that was going to handle it.useDragValue's handlers receive theDragStateas a second argument, the wayuseDrag's do.Two things to know when you turn it on.
Update each point from the previous state. A selection calls
onChangeon several points in the same tick, so a handler that rebuilds its state from a value captured in the render keeps only the last one, and every point but one appears stuck:// goodonChange={(v) => setPoints((prev) => ({ ...prev, [id]: v }))}// throws away every call but the lastonChange={(v) => setPoints({ ...points, [id]: v })}The rubber band puts a drag on
PointsEditor.Container, and a drag setstouch-action: noneon what it holds, so dragging a finger across the editor draws a selection rather than scrolling the page. That was already true over a point; withselectableit is true over the whole surface. Without it the container takes no drag at all. -
#182
30e927aThanks @m1m0zzz! - Shift now makes aSlider,XYPadorPointsEditordrag fine, the way it already did onKnob. All four takedragSensitivity, default{ default: 1, shift: 0.1 }, and a bare number opts out of modifiers.These three are different from
Knob: their value is the position pointed at, so a fine drag cannot stay under the pointer. It moves a tenth as fast and the two drift apart — and they stay apart once the key is released. Snapping the value back under the pointer would move it by however far they had drifted, which is a jump nobody asked for.Pressing or releasing the key partway through does not disturb the value, and a plain click still lands where it was aimed.
elementMappingof@tremolo-ui/domtakes the samesensitivitycallbackrelativeMappingdoes:elementMapping(() => track, {sensitivity: (state) => (state.event.shiftKey ? 0.1 : 1),})It reports
origin + (position - anchor) * sensitivity, where origin and anchor only move when the sensitivity does. With no callback the two never part, so the result is the raw position and nothing changes. -
#215
12d6463Thanks @m1m0zzz! - 競合する Web MIDI access request と終了後の非同期結果を安全に扱い、Piano の命令 API で MIDI note 範囲を検証します。 -
#288
36a1c39Thanks @m1m0zzz! - Rename therelativeSizeoption ofcreateAnimationCanvastoresizable, matchingAnimationCanvasandPianoin@tremolo-ui/react.- createAnimationCanvas(canvas, { draw, relativeSize: true })+ createAnimationCanvas(canvas, { draw, resizable: true }) -
#219
7ec27ccThanks @m1m0zzz! - Keep an animation canvas sharp when the device pixel ratio changes, and preserve its drawing state across every resize. Snapshot pixels are restored with neutral compositing before the caller's styles, line dash, and transform are reinstated. -
#185
5ebb50eThanks @m1m0zzz! -pointerLockhides the cursor for the length of a drag onKnobandNumberInput.Stepper, reading the pointer movement directly instead of following it around the screen.The reason is not tidiness. A relative drag does not care where the pointer is, but it still stops at the edge of the screen: the operating system pins the pointer there and the coordinates stop changing, so the value stops moving however far you keep dragging. A fine drag — shift held, or a low
dragSensitivity— reaches that edge quickly.It is off by default. The browser shows a notice of its own, Esc takes the lock back, and the request can be refused. A refusal is not an error: the drag carries on as an ordinary one, since the coordinates are only read as movement once the lock is actually held. Losing the lock ends the drag, because no pointerup is coming after it.
Movement that happened while the request was in flight is kept, so the value does not jump when the lock takes effect.
Not for
Slider,XYPadorPointsEditor: their value is the position pointed at, andclientX/clientYfreeze under the lock.createDragandcreateDragValueof@tremolo-ui/domtake the option, as do theuseDraganduseDragValuehooks.
Patch Changes
-
#207
5f12a47Thanks @m1m0zzz! - End every active drag when its instance is destroyed or its pointer capture is lost. These paths use the existingonDragEndcallback, as cleanup is required after every drag that started regardless of how the pointer stopped tracking.React controls now balance the page-wide
user-select: nonethey acquire even when they unmount or become readonly during a drag. -
#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. -
Updated dependencies [
fc5b383,a712bc2,d1dc65f,2ff5c1d,041483e,e180798,e31065c,83fa74b,84cbcdb]:- @tremolo-ui/functions@0.6.0
0.5.0
Minor Changes
-
#139
954dc53Thanks @m1m0zzz! - Move the drag-to-value logic into@tremolo-ui/domascreateDragValue.Slider,Knob,XYPadandPointsEditoreach turned pointer movement into a value on their own. They now share one primitive: a mapping decides where the pointer sits on the 0-1 travel of each axis, and the axis options (min/max/step/skew/reverse) turn that into a value, in the same order everywhere — position,reverse,skew, rounding to the step, clamping to the range.Two mappings ship with it.
elementMappingnormalizes the pointer against the bounding rect of an element, so the value is the position pointed at (Slider,XYPad,PointsEditor).relativeMappingmoves the value away from where it stood when the drag started, by the distance dragged (Knob).Breaking:
useDragWithElementis replaced byuseDragValue, which reports values rather than normalized coordinates and covers both mappings.const { refCallback, dragging } = useDragValue({axis: { min: 0, max: 100, step: 1 },baseElementRef: trackRef,updateOnPointerDown: true,onChange: ([x]) => setValue(x),})Breaking: the
XYOrSingletype ofXYPadis nowXYInput, and its pair form is areadonlytuple. A single value is told from a pair withArray.isArray, so where the value could itself be an array the single form is dropped and only the pair is left.createDragandcreateDragValuealso gainedupdate(), so a wrapper can feed fresh settings in without tearing down the listeners. Changingminormaxwhile a drag is in progress no longer aborts it.Fixes a crash when the element a drag normalizes against has collapsed to zero width or height: the position now reads 0 instead of throwing a
RangeError. -
#144
27209dcThanks @m1m0zzz! - Move the AnimationCanvas loop into@tremolo-ui/domascreateAnimationCanvas, and stop restarting it on every renderAnimationCanvaskept its whole setup — the 2D context, theResizeObserver, the device pixel ratio, therequestAnimationFrameloop — inside one effect whose dependencies includeddraw,initandoptions. Those are written inline at almost every call site, so they were new on every render and the effect tore everything down and built it again:initran repeatedly, andcountandelapsedTimewent back to zero. A canvas next to anything that sets state — a meter reading its level, say — never got past frame 0.The loop now lives in
@tremolo-ui/dom:const instance = createAnimationCanvas(canvas, { draw, animate, size })instance.update({ draw: nextDraw }) // swaps the handler, keeps the loop runninginstance.redraw()instance.destroy()The React component is a wrapper that creates the instance once and pushes fresh handlers in with
update(), the same shapeuseDragValuealready used.Fixed along the way:
widthandheighthad no effect once mounted. They were not effect dependencies, so changing them reset the canvas's backing store without re-applying the device pixel ratio transform, leaving the drawing at the wrong scale.- The first size of a
relativeSizecanvas came fromparent.clientWidth, later ones from the observer'scontentRect. Those differ by the parent's padding, so a padded parent drew at one size and then jumped. TheResizeObservernow reports every size, including the first. - Resizing blurred the canvas on a HiDPI screen. The snapshot that carries the drawing across a resize was scaled down by the device pixel ratio on the way out and back up on the way in. The two cancelled out, so it landed in the right place at the right size, but it had been through a downscale and an upscale. It is now copied at the canvas's own device resolution and drawn back at its old CSS size, which is a 1:1 copy of device pixels while the ratio holds — and a single correct rescale from full resolution when the ratio changes.
- The hidden
<canvas>used to carry the drawing across a resize is no longer rendered into the document; the core makes one off-document when it needs it.
With
animateoff,update()draws a frame: the loop is not running, so a resize and that call are the only things that can put a new drawing on the canvas. That is what keeps the documented "reactive canvas" —useStatewithanimate={false}— repainting when state changes.optionsis no longer an effect dependency. It is read once when the context is created, so writing it inline no longer rebuilds the canvas on every render.AnimationCanvaskeeps the same props.@tremolo-ui/domgainscreateAnimationCanvas,drawingStateandisDrawingState, with theAnimationFrame,AnimationCanvasOptionsandAnimationCanvasInstancetypes. -
#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.
Patch Changes
-
#137
68f05cbThanks @m1m0zzz! - Fix a slow drag not registering. Movement belowcreateDrag's threshold was discarded instead of carried over, and pointer coordinates are fractional, so dragging slowly moved less than a pixel per event and never reported anything. It now accumulates until it crosses the threshold.The threshold also now defaults to 0 in
createDrag, which restoresuseDragWithElement(used bySlider,XYPadandPointsEditor) to having no threshold at all, as it did before the move to@tremolo-ui/dom.useDragstill defaults to 1. -
Updated dependencies [
0646236,fe74061,95df589]:- @tremolo-ui/functions@0.5.0
0.4.0
Minor Changes
-
#133
c0bda83Thanks @m1m0zzz! - Move pointer drag and wheel handling into@tremolo-ui/domascreateDragandcreateWheel.createDraguses Pointer Events only and relies on pointer capture, so the drag keeps working once the pointer leaves the element and no window level listeners are needed. It appliestouch-action: noneso that touch dragging does not scroll the page, and cancelsselectstartso that a long press does not start a text selection instead.The drag cursor (
externalStyles.cursor) is now applied to the dragged element rather than todocument.body. Pointer capture keeps it in effect once the pointer leaves the element, so there is no need to restyle the whole document, and long pressing no longer flashes a selection across the page.Fixes a bug where a drag starting at screen coordinate 0 (the top or left edge of the screen) never reported any movement, and a bug where
useDragWithElementpassed stale coordinates toonDragStart.SliderandXYPadnow move to the pointer position on pointer down, instead of waiting for the first movement.Breaking:
useDragnow returns a single ref callback instead of[refCallback, pointerDownHandler], anduseDragWithElementreturns{ refCallback, dragging }instead of{ refHandler, pointerDownHandler, dragging }. A newuseWheelhook is exported.Breaking:
DragObserverandWheelObserverare removed. They had become thin wrappers arounduseDraganduseWheel, which replace them.