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