diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb9..d26d50d9e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -46,12 +46,21 @@ export function repl({ onUpdateState?.(state); }; + const TIMELINES = { + currentOffsets: {}, + previousOffsets: {}, + }; const schedulerOptions = { onTrigger: getTrigger({ defaultOutput, getTime }), getTime, onToggle: (started) => { updateState({ started }); onToggle?.(started); + if (!started) { + // Reset timeline state + TIMELINES.currentOffsets = {}; + TIMELINES.previousOffsets = {}; + } }, setInterval, clearInterval, @@ -145,6 +154,69 @@ export function repl({ return silence; }; + /** + * Aligns the pattern with the specified `timeline` by ID. + * + * More specifically, if a timeline ID is encountered for the first time, that moment + * in time is marked as the "start" of that pattern. Thereafter, any other patterns aligned + * with that same id will begin at that same moment in time. + * + * If a negative ID is supplied, the timeline will reset at the start of the next cycle. + * + * @param {number | Pattern} id Timeline id. Must be a number. Set to negative to reset the timeline. + * @returns Pattern + */ + const timeline = register('timeline', (id, pat) => { + const behavior = 2; + if (typeof id !== 'number') { + logger(`[query] ${id} is not a valid timeline id. Please ensure it is a number. Defaulting to timeline 1.`); + id = 1; + } + const { currentOffsets: state, previousOffsets: prev } = TIMELINES; + const key = Math.abs(id); + // For negative id we just let the pattern run without resetting, hence tracking `prev` + let t = id < 0 ? prev[key] : state[key]; + // We default here instead of updating `state` to prevent the `draw` functions from + // interfering with `state` when querying + t ??= Math.ceil(getTime()); + return ( + pat + .late(t) + .onTrigger((hap) => { + const T = Number(hap.part.begin); + // Set state on the first trigger + state[key] ??= T; + prev[key] ??= T; + if (id < 0) { + // Restart on next cycle + const nextCycle = Math.floor(T) + 1; + switch (behavior) { + case 0: { + // Restart at start of next cycle + state[key] = nextCycle; + break; + } + case 1: { + // Restart immediately + state[key] = T; + break; + } + case 2: { + // Restart at next hap or next cycle, whichever comes first + const haps = pat.queryArc(T, nextCycle).filter((h) => Number(h.part.begin) !== T); + state[key] = haps.length ? Number(haps[0].part.begin) : nextCycle; + break; + } + } + } else { + prev[key] = state[key]; + } + }, false) + // Add labels + .withValue((v) => ({ ...v, timeline: id, offset: t })) + ); + }); + // set pattern methods that use this repl via closure const injectPatternMethods = () => { Pattern.prototype.p = function (id) { @@ -194,6 +266,7 @@ export function repl({ setcps: setCps, setCpm, setcpm: setCpm, + timeline, }); }; diff --git a/packages/core/test/pattern.test.mjs b/packages/core/test/pattern.test.mjs index 1df5c8776..625f40756 100644 --- a/packages/core/test/pattern.test.mjs +++ b/packages/core/test/pattern.test.mjs @@ -796,7 +796,7 @@ describe('Pattern', () => { }); }); describe('apply', () => { - it('Can apply a function', () => { + (it('Can apply a function', () => { expect(sequence('a', 'b').apply(fast(2)).firstCycle()).toStrictEqual(sequence('a', 'b').fast(2).firstCycle()); }), it('Can apply a pattern of functions', () => { @@ -804,7 +804,7 @@ describe('Pattern', () => { expect(sequence('a', 'b').apply(fast(2), fast(3)).firstCycle()).toStrictEqual( sequence('a', 'b').fast(2, 3).firstCycle(), ); - }); + })); }); describe('layer', () => { it('Can layer up multiple functions', () => { diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index 456ef9f6b..b14146cbd 100644 --- a/website/src/components/Header/Search.css +++ b/website/src/components/Header/Search.css @@ -18,8 +18,8 @@ --docsearch-modal-background: var(--background); --docsearch-muted-color: color-mix(in srgb, var(--foreground), #fff 30%); --docsearch-key-gradient: var(--foreground); - --docsearch-key-shadow: inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), - 0 1px 2px 1px var(--gutterBackground); + --docsearch-key-shadow: + inset 0 -2px 0 0 var(--gutterForeground), inset 0 0 1px 1px var(--foreground), 0 1px 2px 1px var(--gutterBackground); } .dark { --docsearch-muted-color: color-mix(in srgb, var(--foreground), #000 30%); diff --git a/website/src/pages/learn/samples.mdx b/website/src/pages/learn/samples.mdx index eb79cccf7..201d57dfa 100644 --- a/website/src/pages/learn/samples.mdx +++ b/website/src/pages/learn/samples.mdx @@ -381,7 +381,7 @@ Sampler effects are functions that can be used to change the behaviour of sample ### scrub -{' '} + ### speed