From 32d11d03327aa1197a6892561b2851a5b3ace0be Mon Sep 17 00:00:00 2001 From: Aria Date: Sat, 11 Oct 2025 01:11:05 -0500 Subject: [PATCH] Move timeline back to pattern file --- packages/core/pattern.mjs | 74 +++++++++++++++++++++++ packages/core/repl.mjs | 70 +-------------------- packages/core/test/pattern.test.mjs | 4 +- test/__snapshots__/examples.test.mjs.snap | 2 + website/src/components/Header/Search.css | 4 +- 5 files changed, 81 insertions(+), 73 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 72f6371b3..de073db63 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -8,6 +8,7 @@ import TimeSpan from './timespan.mjs'; import Fraction, { isFraction, lcm } from './fraction.mjs'; import Hap from './hap.mjs'; import State from './state.mjs'; +import { getTime } from './time.mjs'; import { unionWithObj } from './value.mjs'; import { @@ -3533,3 +3534,76 @@ export const morph = (frompat, topat, bypat) => { bypat = reify(bypat); return frompat.innerBind((from) => topat.innerBind((to) => bypat.innerBind((by) => _morph(from, to, by)))); }; + +const TIMELINES = { + currentOffsets: {}, + previousOffsets: {}, +}; + +/** + * 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 + * @example + * s("tri").seg(8).n(irand(12)).scale("G#:minor").lpf(400).room(2) + * .timeline("<1 -1 1 2>") + */ +const timeline = register('timeline', (id, pat) => { + // TODO: This is only included to demonstrate the possible options. We should pick one + // before merging + const behavior = 0; + 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 })) + ); +}); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index d26d50d9e..a9b972ab2 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -4,7 +4,7 @@ import { evaluate as _evaluate } from './evaluate.mjs'; import { errorLogger, logger } from './logger.mjs'; import { setTime } from './time.mjs'; import { evalScope } from './evaluate.mjs'; -import { register, Pattern, isPattern, silence, stack } from './pattern.mjs'; +import { register, Pattern, isPattern, silence, stack, TIMELINES } from './pattern.mjs'; export function repl({ defaultOutput, @@ -46,10 +46,6 @@ export function repl({ onUpdateState?.(state); }; - const TIMELINES = { - currentOffsets: {}, - previousOffsets: {}, - }; const schedulerOptions = { onTrigger: getTrigger({ defaultOutput, getTime }), getTime, @@ -154,69 +150,6 @@ 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) { @@ -266,7 +199,6 @@ 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 625f40756..1df5c8776 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/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 73278947d..de668c88f 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -10844,6 +10844,8 @@ exports[`runs examples > example "take" example index 2 1`] = ` ] `; +exports[`runs examples > example "timeline" example index 0 1`] = `[]`; + exports[`runs examples > example "tour" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:e s:folkharp ]", diff --git a/website/src/components/Header/Search.css b/website/src/components/Header/Search.css index b14146cbd..456ef9f6b 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%);