@multitrack/core 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/LICENSE +21 -0
- package/README.md +107 -0
- package/dist/index.cjs +628 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +345 -0
- package/dist/index.d.ts +345 -0
- package/dist/index.js +608 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Easing function: takes a progress value (0-1) and returns an eased value (0-1).
|
|
3
|
+
*/
|
|
4
|
+
type EasingFunction = (t: number) => number;
|
|
5
|
+
/**
|
|
6
|
+
* Built-in easing preset names.
|
|
7
|
+
*/
|
|
8
|
+
type EasingPreset = "snap" | "linear" | "easeIn" | "easeOut" | "easeInOut";
|
|
9
|
+
/**
|
|
10
|
+
* User-provided step configuration. Declarative — consumers define animations
|
|
11
|
+
* as data, not imperative code.
|
|
12
|
+
*
|
|
13
|
+
* Inspired by video editing: each step occupies a duration on a named track.
|
|
14
|
+
*/
|
|
15
|
+
interface StepConfig {
|
|
16
|
+
/** Unique identifier for this step. Use "buffer" for spacers (auto-renamed). */
|
|
17
|
+
name: string;
|
|
18
|
+
/** How many viewport-heights this step lasts. */
|
|
19
|
+
duration: number;
|
|
20
|
+
/** Which timeline track this step belongs to. */
|
|
21
|
+
track: string;
|
|
22
|
+
/** Easing for opacity transitions. Defaults to "snap" (binary 0 or 1). */
|
|
23
|
+
easing?: EasingPreset | EasingFunction;
|
|
24
|
+
/** Optional predicate — if provided, step is only included when this returns true. */
|
|
25
|
+
condition?: () => boolean;
|
|
26
|
+
/** Named breakpoint this step belongs to. Only included when that breakpoint matches. */
|
|
27
|
+
when?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A step with computed absolute positions on the timeline.
|
|
31
|
+
* Produced by `resolveSteps()` from user-provided `StepConfig[]`.
|
|
32
|
+
*/
|
|
33
|
+
interface Step {
|
|
34
|
+
name: string;
|
|
35
|
+
start: number;
|
|
36
|
+
end: number;
|
|
37
|
+
track: string;
|
|
38
|
+
easing: EasingPreset | EasingFunction;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Maps step names to their current opacity (0-1).
|
|
42
|
+
* Generic so TypeScript can infer step names from user config.
|
|
43
|
+
*/
|
|
44
|
+
type Opacities<T extends string = string> = Record<T, number>;
|
|
45
|
+
interface StepEventPayload {
|
|
46
|
+
name: string;
|
|
47
|
+
track: string;
|
|
48
|
+
start: number;
|
|
49
|
+
end: number;
|
|
50
|
+
}
|
|
51
|
+
interface ScrollEventPayload {
|
|
52
|
+
scrollPercentage: number;
|
|
53
|
+
currentStep: number;
|
|
54
|
+
}
|
|
55
|
+
interface ReconfigureEventPayload {
|
|
56
|
+
steps: Step[];
|
|
57
|
+
totalSteps: number;
|
|
58
|
+
}
|
|
59
|
+
interface TimelineEventMap {
|
|
60
|
+
"step:enter": StepEventPayload;
|
|
61
|
+
"step:exit": StepEventPayload;
|
|
62
|
+
scroll: ScrollEventPayload;
|
|
63
|
+
"timeline:reconfigure": ReconfigureEventPayload;
|
|
64
|
+
}
|
|
65
|
+
type TimelineEventType = keyof TimelineEventMap;
|
|
66
|
+
type TimelineEventHandler<T extends TimelineEventType> = (payload: TimelineEventMap[T]) => void;
|
|
67
|
+
interface ScrollDriverOptions {
|
|
68
|
+
/** Element to listen for scroll events on. Defaults to `window`. */
|
|
69
|
+
target?: HTMLElement | Window;
|
|
70
|
+
}
|
|
71
|
+
interface TimelineOptions<T extends string = string> {
|
|
72
|
+
config: StepConfig[];
|
|
73
|
+
/** Enable devtools integration (exposes state on window.__MULTITRACK_DEVTOOLS__) */
|
|
74
|
+
devtools?: boolean;
|
|
75
|
+
/** Named breakpoints mapping to CSS media queries for responsive step inclusion. */
|
|
76
|
+
breakpoints?: Record<string, string>;
|
|
77
|
+
}
|
|
78
|
+
interface DevtoolsState {
|
|
79
|
+
steps: Step[];
|
|
80
|
+
currentStep: number;
|
|
81
|
+
totalSteps: number;
|
|
82
|
+
opacities: Opacities;
|
|
83
|
+
scrollPercentage: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Collects disposer functions (unsubscribes) so they can be
|
|
88
|
+
* cleaned up in a single `dispose()` call.
|
|
89
|
+
*
|
|
90
|
+
* Inspired by GSAP's gsap.context() — moves cleanup from a
|
|
91
|
+
* discipline problem to an API guarantee.
|
|
92
|
+
*/
|
|
93
|
+
declare class Scope {
|
|
94
|
+
private disposers;
|
|
95
|
+
/** Track a disposer function for later cleanup. */
|
|
96
|
+
add(disposer: () => void): void;
|
|
97
|
+
/** Call all tracked disposers and clear the list. Idempotent. */
|
|
98
|
+
dispose(): void;
|
|
99
|
+
/** Number of tracked disposers. */
|
|
100
|
+
get size(): number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Event passed to middleware functions during step transitions.
|
|
105
|
+
*/
|
|
106
|
+
interface MiddlewareEvent {
|
|
107
|
+
type: "step:enter" | "step:exit";
|
|
108
|
+
payload: StepEventPayload;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Middleware function signature.
|
|
112
|
+
* Call `next()` to continue the chain; skip it to swallow the event.
|
|
113
|
+
*/
|
|
114
|
+
type MiddlewareFn = (event: MiddlewareEvent, next: () => void) => void;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Timeline: the main facade that ties everything together.
|
|
118
|
+
*
|
|
119
|
+
* Usage:
|
|
120
|
+
* ```ts
|
|
121
|
+
* const timeline = new Timeline({
|
|
122
|
+
* config: [
|
|
123
|
+
* { name: "intro", duration: 3, track: "main", easing: "linear" },
|
|
124
|
+
* { name: "content", duration: 5, track: "main" },
|
|
125
|
+
* { name: "caption", duration: 2, track: "text" },
|
|
126
|
+
* ],
|
|
127
|
+
* });
|
|
128
|
+
*
|
|
129
|
+
* timeline.start(); // begins listening to scroll
|
|
130
|
+
* timeline.on("scroll", ({ scrollPercentage }) => {
|
|
131
|
+
* const opacities = timeline.getOpacities(scrollPercentage);
|
|
132
|
+
* });
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare class Timeline {
|
|
136
|
+
private _steps;
|
|
137
|
+
private _totalSteps;
|
|
138
|
+
private config;
|
|
139
|
+
private scrollDriver;
|
|
140
|
+
private emitter;
|
|
141
|
+
private activeSteps;
|
|
142
|
+
private devtoolsEnabled;
|
|
143
|
+
private scrollUnsubscribe;
|
|
144
|
+
private _activeScope;
|
|
145
|
+
private middleware;
|
|
146
|
+
private breakpointManager;
|
|
147
|
+
private breakpointUnsub;
|
|
148
|
+
constructor(options: TimelineOptions);
|
|
149
|
+
/** Resolved steps (re-computed on breakpoint changes). */
|
|
150
|
+
get steps(): Step[];
|
|
151
|
+
/** Total steps across all tracks (re-computed on breakpoint changes). */
|
|
152
|
+
get totalSteps(): number;
|
|
153
|
+
/** Start listening to scroll events. */
|
|
154
|
+
start(): void;
|
|
155
|
+
/** Stop listening and clean up all resources. */
|
|
156
|
+
destroy(): void;
|
|
157
|
+
/**
|
|
158
|
+
* Register middleware that intercepts step:enter/step:exit events.
|
|
159
|
+
* Call `next()` to pass through, or skip it to swallow the event.
|
|
160
|
+
*
|
|
161
|
+
* ```ts
|
|
162
|
+
* timeline.use((event, next) => {
|
|
163
|
+
* analytics.track(event.type, event.payload.name);
|
|
164
|
+
* next();
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
use(fn: MiddlewareFn): () => void;
|
|
169
|
+
/** Subscribe to timeline events. Returns an unsubscribe function. */
|
|
170
|
+
on<T extends TimelineEventType>(event: T, handler: TimelineEventHandler<T>): () => void;
|
|
171
|
+
/**
|
|
172
|
+
* Collect all subscriptions created inside `fn` into a Scope.
|
|
173
|
+
* Call `scope.dispose()` to clean them all up at once.
|
|
174
|
+
*
|
|
175
|
+
* ```ts
|
|
176
|
+
* const ctx = timeline.scope(() => {
|
|
177
|
+
* timeline.on('step:enter', handleEnter);
|
|
178
|
+
* timeline.on('scroll', handleScroll);
|
|
179
|
+
* });
|
|
180
|
+
* // later: ctx.dispose() cleans up both listeners
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
scope(fn: () => void): Scope;
|
|
184
|
+
/** Current scroll progress (0 to 1). */
|
|
185
|
+
get scrollPercentage(): number;
|
|
186
|
+
/** Current step position (0 to totalSteps). */
|
|
187
|
+
get currentStep(): number;
|
|
188
|
+
/** Calculate opacities for all steps at a given scroll position. */
|
|
189
|
+
getOpacities<T extends string = string>(scrollPercentage?: number): Opacities<T>;
|
|
190
|
+
/** Get the start/end range for a named step. */
|
|
191
|
+
getStepRange(name: string): {
|
|
192
|
+
start: number;
|
|
193
|
+
end: number;
|
|
194
|
+
};
|
|
195
|
+
/** Get all steps that are active at a given position. */
|
|
196
|
+
getCurrentSteps(position?: number): {
|
|
197
|
+
name: string;
|
|
198
|
+
track: string;
|
|
199
|
+
start: number;
|
|
200
|
+
end: number;
|
|
201
|
+
}[];
|
|
202
|
+
/** Enable devtools integration (exposes state on window.__MULTITRACK_DEVTOOLS__). */
|
|
203
|
+
enableDevtools(): void;
|
|
204
|
+
/**
|
|
205
|
+
* Filter config by active breakpoints and resolve steps.
|
|
206
|
+
* Steps without `when` are always included.
|
|
207
|
+
* Steps with `when` are included only if that breakpoint currently matches.
|
|
208
|
+
*/
|
|
209
|
+
private resolveWithBreakpoints;
|
|
210
|
+
/**
|
|
211
|
+
* Re-resolve steps after a breakpoint change.
|
|
212
|
+
* Clears active steps and emits timeline:reconfigure.
|
|
213
|
+
*/
|
|
214
|
+
private reconfigure;
|
|
215
|
+
private updateDevtools;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ScrollCallback = (scrollPercentage: number) => void;
|
|
219
|
+
/**
|
|
220
|
+
* Manages scroll event listening and converts scroll position to a 0-1 progress value.
|
|
221
|
+
*
|
|
222
|
+
* Extracted and generalized from sinking-china MainMap.tsx scroll handler.
|
|
223
|
+
* The original was tightly coupled to window.scrollY and React state — this
|
|
224
|
+
* version attaches to any scrollable element and uses a subscription API.
|
|
225
|
+
*/
|
|
226
|
+
declare class ScrollDriver {
|
|
227
|
+
private target;
|
|
228
|
+
private callbacks;
|
|
229
|
+
private bound;
|
|
230
|
+
private _scrollPercentage;
|
|
231
|
+
constructor(options?: ScrollDriverOptions);
|
|
232
|
+
/** Current scroll progress (0 to 1). */
|
|
233
|
+
get scrollPercentage(): number;
|
|
234
|
+
/**
|
|
235
|
+
* Subscribe to scroll updates. Returns an unsubscribe function.
|
|
236
|
+
*/
|
|
237
|
+
onScroll(callback: ScrollCallback): () => void;
|
|
238
|
+
/**
|
|
239
|
+
* Start listening for scroll events.
|
|
240
|
+
*/
|
|
241
|
+
start(): void;
|
|
242
|
+
/**
|
|
243
|
+
* Stop listening and clean up.
|
|
244
|
+
*/
|
|
245
|
+
destroy(): void;
|
|
246
|
+
private calculateProgress;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolve declarative step configs into absolute timeline positions.
|
|
251
|
+
*
|
|
252
|
+
* Each track maintains its own independent cursor. Steps are placed
|
|
253
|
+
* sequentially within their track, producing parallel timelines that
|
|
254
|
+
* can overlap across tracks.
|
|
255
|
+
*
|
|
256
|
+
* Extracted and generalized from sinking-china App.tsx:transformStepConfig()
|
|
257
|
+
*/
|
|
258
|
+
declare function resolveSteps(config: StepConfig[]): Step[];
|
|
259
|
+
/**
|
|
260
|
+
* Get the total number of steps (the maximum end position across all tracks).
|
|
261
|
+
*/
|
|
262
|
+
declare function getTotalSteps(steps: Step[]): number;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Calculate the opacity for a single step at a given scroll position.
|
|
266
|
+
*
|
|
267
|
+
* The easing function controls how opacity transitions within the step range:
|
|
268
|
+
* - "snap": binary 0 or 1 (instant appear/disappear)
|
|
269
|
+
* - "linear": smooth 0→1 over the step duration
|
|
270
|
+
* - Custom easing function: any (t: number) => number mapping
|
|
271
|
+
*
|
|
272
|
+
* Extracted and generalized from sinking-china utils.ts:calculateScrollBasedOpacity()
|
|
273
|
+
*/
|
|
274
|
+
declare function calculateStepOpacity(step: Step, totalSteps: number, scrollPercentage: number): number;
|
|
275
|
+
/**
|
|
276
|
+
* Calculate opacities for all steps at a given scroll position.
|
|
277
|
+
*
|
|
278
|
+
* Returns a record mapping step names to their current opacity (0-1).
|
|
279
|
+
*
|
|
280
|
+
* Extracted from sinking-china utils.ts:calculateAllOpacities()
|
|
281
|
+
*/
|
|
282
|
+
declare function calculateAllOpacities<T extends string = string>(scrollPercentage: number, steps: Step[]): Opacities<T>;
|
|
283
|
+
/**
|
|
284
|
+
* Get the start/end range for a named step.
|
|
285
|
+
*
|
|
286
|
+
* Extracted from sinking-china utils.ts:getStepRange()
|
|
287
|
+
*/
|
|
288
|
+
declare function getStepRange(stepName: string, steps: Step[]): {
|
|
289
|
+
start: number;
|
|
290
|
+
end: number;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Get info about all steps that are active at a given position.
|
|
294
|
+
*
|
|
295
|
+
* Extracted from sinking-china utils.ts:getCurrentStepInfo()
|
|
296
|
+
*/
|
|
297
|
+
declare function getCurrentSteps(currentStep: number, steps: Step[]): Array<{
|
|
298
|
+
name: string;
|
|
299
|
+
track: string;
|
|
300
|
+
start: number;
|
|
301
|
+
end: number;
|
|
302
|
+
}>;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Binary snap: opacity is 1 when inside range, 0 outside.
|
|
306
|
+
* This was the default behavior in the original engine (isLinear: false).
|
|
307
|
+
*/
|
|
308
|
+
declare const snap: EasingFunction;
|
|
309
|
+
/**
|
|
310
|
+
* Linear interpolation from 0 to 1 over the step duration.
|
|
311
|
+
* This was `isLinear: true` in the original engine.
|
|
312
|
+
*/
|
|
313
|
+
declare const linear: EasingFunction;
|
|
314
|
+
/** Quadratic ease-in: slow start, fast end. */
|
|
315
|
+
declare const easeIn: EasingFunction;
|
|
316
|
+
/** Quadratic ease-out: fast start, slow end. */
|
|
317
|
+
declare const easeOut: EasingFunction;
|
|
318
|
+
/** Quadratic ease-in-out: slow start and end, fast middle. */
|
|
319
|
+
declare const easeInOut: EasingFunction;
|
|
320
|
+
/** Map of preset names to easing functions. */
|
|
321
|
+
declare const easingPresets: Record<EasingPreset, EasingFunction>;
|
|
322
|
+
/**
|
|
323
|
+
* Resolve an easing value (preset name or custom function) to an EasingFunction.
|
|
324
|
+
*/
|
|
325
|
+
declare function resolveEasing(easing: EasingPreset | EasingFunction | undefined): EasingFunction;
|
|
326
|
+
|
|
327
|
+
declare class MultitrackError extends Error {
|
|
328
|
+
code: string;
|
|
329
|
+
constructor(code: string, message: string);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Clear emitted warnings. Exposed for testing. */
|
|
333
|
+
declare function resetWarnings(): void;
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Minimal typed event emitter for timeline events.
|
|
337
|
+
*/
|
|
338
|
+
declare class Emitter {
|
|
339
|
+
private listeners;
|
|
340
|
+
on<T extends TimelineEventType>(event: T, handler: TimelineEventHandler<T>): () => void;
|
|
341
|
+
emit<T extends TimelineEventType>(event: T, payload: TimelineEventMap[T]): void;
|
|
342
|
+
removeAllListeners(): void;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export { type DevtoolsState, type EasingFunction, type EasingPreset, Emitter, type MiddlewareEvent, type MiddlewareFn, MultitrackError, type Opacities, type ReconfigureEventPayload, Scope, ScrollDriver, type ScrollDriverOptions, type ScrollEventPayload, type Step, type StepConfig, type StepEventPayload, Timeline, type TimelineEventHandler, type TimelineEventMap, type TimelineEventType, type TimelineOptions, calculateAllOpacities, calculateStepOpacity, easeIn, easeInOut, easeOut, easingPresets, getCurrentSteps, getStepRange, getTotalSteps, linear, resetWarnings, resolveEasing, resolveSteps, snap };
|