lfocomp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "lfocomp",
3
+ "version": "0.1.0",
4
+ "description": "Ableton-style LFO modulation component — wire LFOs to any HTML input by drag-and-drop",
5
+ "type": "module",
6
+ "main": "./lfo-comp.js",
7
+ "module": "./lfo-comp.js",
8
+ "types": "./types/lfo-comp.d.ts",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./types/lfo-comp.d.ts",
13
+ "default": "./lfo-comp.js"
14
+ },
15
+ "./engine": {
16
+ "types": "./types/lfo-engine.d.ts",
17
+ "default": "./lfo-engine.js"
18
+ },
19
+ "./ui": {
20
+ "types": "./types/lfo-ui.d.ts",
21
+ "default": "./lfo-ui.js"
22
+ },
23
+ "./bundle": {
24
+ "types": "./types/lfo.d.ts",
25
+ "default": "./lfo.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "lfo-comp.js",
30
+ "lfo-engine.js",
31
+ "lfo-ui.js",
32
+ "lfo.js",
33
+ "types/",
34
+ "CHANGELOG.md"
35
+ ],
36
+ "scripts": {
37
+ "build:types": "tsc -p tsconfig.json && tsc -p tsconfig.bundle.json",
38
+ "build:bundle": "rollup lfo-comp.js --file lfo.js --format es",
39
+ "build": "npm run build:bundle && npm run build:types",
40
+ "prepublishOnly": "npm run build",
41
+ "test": "vitest run",
42
+ "test:watch": "vitest",
43
+ "test:coverage": "vitest run --coverage",
44
+ "serve": "python3 -m http.server 8080"
45
+ },
46
+ "keywords": [
47
+ "lfo",
48
+ "modulation",
49
+ "synthesizer",
50
+ "audio",
51
+ "ui",
52
+ "knob",
53
+ "ableton",
54
+ "bitwig",
55
+ "serum"
56
+ ],
57
+ "homepage": "https://kush42.github.io/lfo.html",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/KUSH42/lfocomp.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/KUSH42/lfocomp/issues"
64
+ },
65
+ "license": "MIT",
66
+ "devDependencies": {
67
+ "@vitest/coverage-v8": "^3.2.4",
68
+ "jsdom": "^29.0.1",
69
+ "playwright": "^1.58.2",
70
+ "rollup": "^4.60.0",
71
+ "typescript": "^6.0.2",
72
+ "vitest": "^3.0.0"
73
+ }
74
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Create a fully wired LFO widget.
3
+ *
4
+ * @param {HTMLElement|object} containerOrOpts
5
+ * If an HTMLElement, the widget is appended to it and opts is the second arg.
6
+ * If an object, treated as opts — caller is responsible for appending widget.element.
7
+ * @param {object} [opts]
8
+ * @param {string} [opts.shape='sine'] Initial waveform shape.
9
+ * @param {number} [opts.rate=1] Initial rate in Hz.
10
+ * @param {number} [opts.depth=1] Initial depth 0–1.
11
+ * @param {number} [opts.phase=0] Initial phase offset 0–1.
12
+ * @param {number} [opts.offset=0] DC offset -1–1.
13
+ * @param {string} [opts.color] Accent color. Auto-assigned if omitted.
14
+ * @param {string} [opts.label] Widget label. Auto-assigned if omitted.
15
+ * @param {function} [opts.onConnect] Called when a connection is made.
16
+ * @param {function} [opts.onDisconnect] Called when a connection is removed.
17
+ *
18
+ * @returns {{ lfoId: string, widget: LFOWidget }}
19
+ */
20
+ export function createLFO(containerOrOpts?: HTMLElement | object, opts?: {
21
+ shape?: string;
22
+ rate?: number;
23
+ depth?: number;
24
+ phase?: number;
25
+ offset?: number;
26
+ color?: string;
27
+ label?: string;
28
+ onConnect?: Function;
29
+ onDisconnect?: Function;
30
+ }): {
31
+ lfoId: string;
32
+ widget: LFOWidget;
33
+ };
34
+ /**
35
+ * Programmatically connect an LFO to an HTML element.
36
+ * Equivalent to calling widget.connect(element, opts).
37
+ *
38
+ * @param {LFOWidget} widget
39
+ * @param {HTMLElement} element
40
+ * @param {object} [opts]
41
+ * @param {number} [opts.depth=0.5]
42
+ * @returns {string|null} routeId, or null if connection was rejected.
43
+ */
44
+ export function connect(widget: LFOWidget, element: HTMLElement, opts?: {
45
+ depth?: number;
46
+ }): string | null;
47
+ /**
48
+ * Remove a modulation route and its UI indicator.
49
+ * Prefer calling widget.disconnectRoute(routeId) directly when you have the widget.
50
+ * This function handles programmatic routes created via connect() or drag-to-assign.
51
+ *
52
+ * @param {string} routeId
53
+ */
54
+ export function disconnect(routeId: string): void;
55
+ /**
56
+ * Get all currently active modulation routes.
57
+ * @returns {object[]}
58
+ */
59
+ export function getRoutes(): object[];
60
+ import { LFOWidget } from './lfo-ui.js';
61
+ import { engine } from './lfo-engine.js';
62
+ import { SHAPES } from './lfo-engine.js';
63
+ import { seededRand } from './lfo-engine.js';
64
+ import { smoothRand } from './lfo-engine.js';
65
+ import { ModIndicator } from './lfo-ui.js';
66
+ import { injectStyles } from './lfo-ui.js';
67
+ import { LFO_COLORS } from './lfo-ui.js';
68
+ export { engine, SHAPES, seededRand, smoothRand, LFOWidget, ModIndicator, injectStyles, LFO_COLORS };
@@ -0,0 +1,150 @@
1
+ /** Deterministic pseudo-random in [-1, 1] for a given integer seed. */
2
+ export function seededRand(seed: any): number;
3
+ /** Smoothly interpolated random (cubic Hermite) for non-integer phase. */
4
+ export function smoothRand(seed: any, phase: any): number;
5
+ /**
6
+ * Piecewise-linear phase skew — repositions the midpoint of a 0–1 fractional phase.
7
+ * skew = 0.5 → symmetric (no change).
8
+ * skew < 0.5 → ascending flank compressed (peak shifts left).
9
+ * skew > 0.5 → descending flank compressed (peak shifts right).
10
+ * @param {number} frac Fractional phase in [0, 1).
11
+ * @param {number} skew Skew amount in [0, 1].
12
+ * @returns {number} Skewed fractional phase in [0, 1).
13
+ */
14
+ export function applySkew(frac: number, skew: number): number;
15
+ /**
16
+ * lfo-engine.js — Core LFO math engine, no DOM dependencies.
17
+ *
18
+ * Manages LFO instances, a global RAF tick loop, and a modulation routing
19
+ * graph. Routes can target HTML elements (range/number inputs) or other
20
+ * LFO parameters (for chaining).
21
+ *
22
+ * Usage:
23
+ * import { engine, SHAPES } from './lfo-engine.js';
24
+ * const id = engine.createLFO({ shape: 'sine', rate: 2 });
25
+ * const routeId = engine.addRoute(id, 'element', sliderEl, null, { depth: 0.5 });
26
+ */
27
+ export const SHAPES: string[];
28
+ export namespace SHAPE_FN {
29
+ function sine(p: any): number;
30
+ function triangle(p: any): number;
31
+ function saw(p: any): number;
32
+ function rsaw(p: any): number;
33
+ function square(p: any): 1 | -1;
34
+ }
35
+ export class LFOEngine {
36
+ /** @type {Map<string, LFOState>} */
37
+ _lfos: Map<string, LFOState>;
38
+ /** @type {Map<string, RouteState>} */
39
+ _routes: Map<string, RouteState>;
40
+ /** @type {Map<HTMLElement, ElementState>} */
41
+ _elementStates: Map<HTMLElement, ElementState>;
42
+ /** @type {Set<function>} */
43
+ _subscribers: Set<Function>;
44
+ _lastTs: any;
45
+ _rafId: number;
46
+ _running: boolean;
47
+ _tick(ts: any): void;
48
+ start(): void;
49
+ stop(): void;
50
+ /**
51
+ * Create a new LFO instance.
52
+ * @param {object} opts
53
+ * @param {string} [opts.shape='sine'] Waveform shape. One of SHAPES.
54
+ * @param {number} [opts.rate=1] Frequency in Hz.
55
+ * @param {number} [opts.depth=1] Modulation depth multiplier 0–1.
56
+ * @param {number} [opts.phase=0] Initial phase offset 0–1.
57
+ * @param {number} [opts.offset=0] DC offset added to output, -1–1.
58
+ * @param {boolean} [opts.bipolar=true] If false, output is remapped 0–1.
59
+ * @returns {string} LFO id.
60
+ */
61
+ createLFO(opts?: {
62
+ shape?: string;
63
+ rate?: number;
64
+ depth?: number;
65
+ phase?: number;
66
+ offset?: number;
67
+ bipolar?: boolean;
68
+ }): string;
69
+ /**
70
+ * Remove an LFO and all its routes.
71
+ * @param {string} id
72
+ */
73
+ destroyLFO(id: string): void;
74
+ /**
75
+ * Add a modulation route.
76
+ *
77
+ * For element routes: LFO modulates an HTML input element.
78
+ * engine.addRoute(lfoId, 'element', inputEl, null, { depth: 0.5 });
79
+ *
80
+ * For LFO-chain routes: LFO modulates another LFO's rate or depth.
81
+ * engine.addRoute(lfoId, 'lfo', targetLfoId, 'rate', { depth: 0.3 });
82
+ *
83
+ * @param {string} sourceId
84
+ * @param {'element'|'lfo'} targetType
85
+ * @param {HTMLElement|string} target Element or target LFO id.
86
+ * @param {string|null} targetParam 'rate'|'depth' for lfo, null for element.
87
+ * @param {object} opts
88
+ * @param {number} [opts.depth=0.5] Modulation depth 0–1.
89
+ * @param {number} [opts.min] Override element min.
90
+ * @param {number} [opts.max] Override element max.
91
+ * @param {number} [opts.step] Override element step.
92
+ * @returns {string|null} Route id, or null if the route was rejected (e.g. self-modulation).
93
+ */
94
+ addRoute(sourceId: string, targetType: "element" | "lfo", target: HTMLElement | string, targetParam: string | null, opts?: {
95
+ depth?: number;
96
+ min?: number;
97
+ max?: number;
98
+ step?: number;
99
+ }): string | null;
100
+ /**
101
+ * Remove a modulation route.
102
+ * @param {string} routeId
103
+ */
104
+ removeRoute(routeId: string): void;
105
+ /**
106
+ * Set the depth of an existing route.
107
+ * @param {string} routeId
108
+ * @param {number} depth 0–1
109
+ */
110
+ setRouteDepth(routeId: string, depth: number): void;
111
+ /**
112
+ * Enable or disable a route without removing it.
113
+ * @param {string} routeId
114
+ * @param {boolean} enabled
115
+ */
116
+ setRouteEnabled(routeId: string, enabled: boolean): void;
117
+ /**
118
+ * Set a parameter on an LFO.
119
+ * @param {string} lfoId
120
+ * @param {string} param 'shape'|'rate'|'depth'|'phase'|'offset'|'bipolar'|'baseRate'|'baseDepth'|'jitter'|'skew'
121
+ * @param {*} value
122
+ */
123
+ setParam(lfoId: string, param: string, value: any): void;
124
+ /**
125
+ * Get a parameter from an LFO.
126
+ * @param {string} lfoId
127
+ * @param {string} param
128
+ * @returns {*}
129
+ */
130
+ getParam(lfoId: string, param: string): any;
131
+ /** Get the current output value of an LFO. @param {string} lfoId @returns {number} */
132
+ getValue(lfoId: string): number;
133
+ /**
134
+ * Subscribe to tick notifications. Called each frame for every LFO.
135
+ * @param {function(lfoId: string, value: number): void} fn
136
+ * @returns {function} Unsubscribe callback.
137
+ */
138
+ subscribe(fn: any): Function;
139
+ getLFOs(): LFOState[];
140
+ getLFO(id: any): LFOState;
141
+ getAllRoutes(): RouteState[];
142
+ getRoute(id: any): RouteState;
143
+ getRoutesByElement(element: any): RouteState[];
144
+ getElementMeta(element: any): ElementState;
145
+ _initElementState(element: any, opts: any): void;
146
+ _cleanupElementState(element: any): void;
147
+ _computeValue(lfo: any, dt: any): any;
148
+ }
149
+ /** Global engine singleton — import and use directly. */
150
+ export const engine: LFOEngine;
@@ -0,0 +1,147 @@
1
+ export function injectStyles(): void;
2
+ export const LFO_COLORS: string[];
3
+ /**
4
+ * Floating badge anchored to a connected input element.
5
+ * Shows depth, allows drag-to-adjust, and a remove button.
6
+ */
7
+ export class ModIndicator {
8
+ /**
9
+ * @param {HTMLElement} element Connected input.
10
+ * @param {string} routeId
11
+ * @param {string} lfoId
12
+ * @param {string} color
13
+ * @param {string} lfoLabel
14
+ * @param {function} onRemove Called when the user removes this connection.
15
+ */
16
+ constructor(element: HTMLElement, routeId: string, lfoId: string, color: string, lfoLabel: string, onRemove: Function);
17
+ _element: HTMLElement;
18
+ _routeId: string;
19
+ _lfoId: string;
20
+ _color: string;
21
+ _onRemove: Function;
22
+ _badge: HTMLDivElement;
23
+ _arcCanvas: HTMLCanvasElement;
24
+ _rafId: number;
25
+ _buildBadge(label: any): void;
26
+ _depthLabel: HTMLSpanElement;
27
+ _buildArcCanvas(): void;
28
+ _arcCtx: CanvasRenderingContext2D;
29
+ _updateDepthLabel(): void;
30
+ _startPositioning(): void;
31
+ _reposition(): void;
32
+ _badgeSize: {
33
+ bw: number;
34
+ bh: number;
35
+ };
36
+ _updateArc(): void;
37
+ destroy(): void;
38
+ get routeId(): string;
39
+ }
40
+ /**
41
+ * Canvas-based LFO panel with waveform display, shape selector, parameter
42
+ * sliders, and a drag handle for wiring to any modulation target.
43
+ */
44
+ export class LFOWidget {
45
+ /**
46
+ * @param {HTMLElement} container Where the widget is appended.
47
+ * @param {string} lfoId Engine LFO id.
48
+ * @param {object} [opts]
49
+ * @param {string} [opts.color] Accent color hex.
50
+ * @param {string} [opts.label] Display label.
51
+ * @param {function} [opts.onConnect] (lfoId, element, routeId) => void
52
+ * @param {function} [opts.onDisconnect] (routeId) => void
53
+ */
54
+ constructor(container: HTMLElement, lfoId: string, opts?: {
55
+ color?: string;
56
+ label?: string;
57
+ onConnect?: Function;
58
+ onDisconnect?: Function;
59
+ });
60
+ _lfoId: string;
61
+ _color: string;
62
+ _label: string;
63
+ _onConnect: Function;
64
+ _onDisconnect: Function;
65
+ /** @type {Map<string, ModIndicator>} routeId → indicator */
66
+ _indicators: Map<string, ModIndicator>;
67
+ _latestValue: number;
68
+ _unsub: Function;
69
+ _rafHandle: number;
70
+ _destroyed: boolean;
71
+ _build(container: any): void;
72
+ _root: HTMLDivElement;
73
+ _led: HTMLDivElement;
74
+ _canvas: HTMLCanvasElement;
75
+ _ctx: CanvasRenderingContext2D;
76
+ _shapesEl: HTMLDivElement;
77
+ _bipolarBtn: HTMLButtonElement;
78
+ _rateInput: HTMLInputElement;
79
+ _depthInput: HTMLInputElement;
80
+ _phaseInput: HTMLInputElement;
81
+ _offsetInput: HTMLInputElement;
82
+ _jitterInput: HTMLInputElement;
83
+ _skewInput: HTMLInputElement;
84
+ _handle: HTMLDivElement;
85
+ _addParam(container: any, labelText: any, min: any, max: any, defaultVal: any, step: any, param: any, fmt: any, scale?: string): HTMLInputElement;
86
+ _refreshShapeButtons(): void;
87
+ _refreshBipolarBtn(): void;
88
+ /**
89
+ * Reflect effective (post-chain-modulation) rate and depth onto the param
90
+ * sliders so they visually track the modulated position. Only touches the
91
+ * DOM when the effective value actually differs from the displayed value to
92
+ * avoid unnecessary repaints.
93
+ */
94
+ _syncChainedSliders(): void;
95
+ /**
96
+ * Remove any indicators whose routes have been deleted externally — e.g. when
97
+ * the target LFO is destroyed and engine.destroyLFO cleans up incoming routes.
98
+ * Called each tick so stale badges are cleared within one frame.
99
+ */
100
+ _pruneDeadRoutes(): void;
101
+ _recordSample(currentValue: any): void;
102
+ _wfBuf: HTMLCanvasElement;
103
+ _wfBufCtx: CanvasRenderingContext2D;
104
+ _wfHistory: Float32Array<ArrayBuffer>;
105
+ _wfSubpx: any;
106
+ _wfLastTime: number;
107
+ _wfPrevValue: any;
108
+ _wfNeedsFullRedraw: boolean;
109
+ /** Composite _wfBuf → visible canvas + cursor dot. Called from RAF. */
110
+ _rafDraw(): void;
111
+ /** Full repaint of _wfBuf from _wfHistory. */
112
+ _wfPaintFull(bCtx: any, W: any, H: any): void;
113
+ /**
114
+ * Scroll _wfBuf left by intShift, fill background+grid in new strip, then
115
+ * draw only the new right-edge stroke from _wfHistory.
116
+ */
117
+ _wfPaintIncremental(bCtx: any, W: any, H: any, intShift: any): void;
118
+ _wfDrawGrid(bCtx: any, W: any, H: any, x0: any, x1: any): void;
119
+ /** Stroke _wfHistory[xFrom..xTo] onto bCtx. */
120
+ _wfDrawHistoryStroke(bCtx: any, W: any, H: any, xFrom: any, xTo: any): void;
121
+ _updateLed(value: any): void;
122
+ _attachDragHandlers(handle: any): void;
123
+ _connectTo(element: any): void;
124
+ /**
125
+ * Programmatically connect this LFO to an element.
126
+ * @param {HTMLElement} element
127
+ * @param {object} [opts]
128
+ * @param {number} [opts.depth=0.5]
129
+ * @returns {string|null} routeId, or null if rejected (duplicate, self-mod).
130
+ */
131
+ connect(element: HTMLElement, opts?: {
132
+ depth?: number;
133
+ }): string | null;
134
+ /**
135
+ * Remove a specific modulation route.
136
+ * @param {string} routeId
137
+ */
138
+ disconnectRoute(routeId: string): void;
139
+ /** Disconnect all routes from this widget. */
140
+ disconnectAll(): void;
141
+ /** Tear down the widget and all its connections. */
142
+ destroy(): void;
143
+ get element(): HTMLDivElement;
144
+ get lfoId(): string;
145
+ get color(): string;
146
+ get label(): string;
147
+ }