diff --git a/packages/core/draw.mjs b/packages/core/draw.mjs index 81700f6c..bc6d3cd8 100644 --- a/packages/core/draw.mjs +++ b/packages/core/draw.mjs @@ -29,3 +29,98 @@ Pattern.prototype.onPaint = function (onPaint) { this.context = { onPaint }; return this; }; + +// const round = (x) => Math.round(x * 1000) / 1000; + +// encapsulates starting and stopping animation frames +export class Framer { + constructor(onFrame, onError) { + this.onFrame = onFrame; + this.onError = onError; + } + start() { + const self = this; + let frame = requestAnimationFrame(function updateHighlights(time) { + try { + self.onFrame(time); + } catch (err) { + self.onError(err); + } + frame = requestAnimationFrame(updateHighlights); + }); + self.cancel = () => { + cancelAnimationFrame(frame); + }; + } + stop() { + if (this.cancel) { + this.cancel(); + } + } +} + +// syncs animation frames to a cyclist scheduler +// see vite-vanilla-repl-cm6 for an example +export class Drawer { + constructor(onDraw, drawTime) { + let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] + lookbehind = Math.abs(lookbehind); + this.visibleHaps = []; + this.lastFrame = null; + this.drawTime = drawTime; + this.framer = new Framer( + () => { + if (!this.scheduler) { + console.warn('Drawer: no scheduler'); + return; + } + // calculate current frame time (think right side of screen for pianoroll) + const phase = this.scheduler.now() + lookahead; + // first frame just captures the phase + if (this.lastFrame === null) { + this.lastFrame = phase; + return; + } + // query haps from last frame till now. take last 100ms max + const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); + this.lastFrame = phase; + this.visibleHaps = (this.visibleHaps || []) + // filter out haps that are too far in the past (think left edge of screen for pianoroll) + .filter((h) => h.whole.end >= phase - lookbehind - lookahead) + // add new haps with onset (think right edge bars scrolling in) + .concat(haps.filter((h) => h.hasOnset())); + const time = phase - lookahead; + onDraw(this.visibleHaps, time, this); + }, + (err) => { + console.warn('draw error', err); + }, + ); + } + check() { + if (!this.scheduler) { + throw new Error('no scheduler set..'); + } + } + invalidate() { + this.check(); + const t = this.scheduler.now(); + let [_, lookahead] = this.drawTime; + // remove all future haps + this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); + // query future haps + const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. + // append future haps + this.visibleHaps = this.visibleHaps.concat(futureHaps); + } + start(scheduler) { + this.scheduler = scheduler; + this.invalidate(); + this.framer.start(); + } + stop() { + if (this.framer) { + this.framer.stop(); + } + } +} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js b/packages/core/examples/vite-vanilla-repl-cm6/drawer.js deleted file mode 100644 index 3bd6fbc9..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/drawer.js +++ /dev/null @@ -1,91 +0,0 @@ -const round = (x) => Math.round(x * 1000) / 1000; - -export class Framer { - constructor(onFrame, onError) { - this.onFrame = onFrame; - this.onError = onError; - } - start() { - const self = this; - let frame = requestAnimationFrame(function updateHighlights(time) { - try { - self.onFrame(time); - } catch (err) { - self.onError(err); - } - frame = requestAnimationFrame(updateHighlights); - }); - self.cancel = () => { - cancelAnimationFrame(frame); - }; - } - stop() { - if (this.cancel) { - this.cancel(); - } - } -} - -export class Drawer { - constructor(onDraw, drawTime) { - let [lookbehind, lookahead] = drawTime; // e.g. [-2, 2] - lookbehind = Math.abs(lookbehind); - this.visibleHaps = []; - this.lastFrame = null; - this.drawTime = drawTime; - this.framer = new Framer( - () => { - if (!this.scheduler) { - console.warn('Drawer: no scheduler'); - return; - } - // calculate current frame time (think right side of screen for pianoroll) - const phase = this.scheduler.now() + lookahead; - // first frame just captures the phase - if (this.lastFrame === null) { - this.lastFrame = phase; - return; - } - // query haps from last frame till now. take last 100ms max - const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase); - this.lastFrame = phase; - this.visibleHaps = (this.visibleHaps || []) - // filter out haps that are too far in the past (think left edge of screen for pianoroll) - .filter((h) => h.whole.end >= phase - lookbehind - lookahead) - // add new haps with onset (think right edge bars scrolling in) - .concat(haps.filter((h) => h.hasOnset())); - const time = phase - lookahead; - onDraw(this.visibleHaps, time, this); - }, - (err) => { - console.warn('draw error', err); - }, - ); - } - check() { - if (!this.scheduler) { - throw new Error('no scheduler set..'); - } - } - invalidate() { - this.check(); - const t = this.scheduler.now(); - let [_, lookahead] = this.drawTime; - // remove all future haps - this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t); - // query future haps - const futureHaps = this.scheduler.pattern.queryArc(Math.max(t, 0), t + lookahead + 0.1); // +0.1 = workaround for weird holes in query.. - // append future haps - this.visibleHaps = this.visibleHaps.concat(futureHaps); - } - start(scheduler) { - this.scheduler = scheduler; - this.invalidate(); - this.framer.start(); - } - stop() { - if (this.framer) { - this.framer.stop(); - } - } -} diff --git a/packages/core/examples/vite-vanilla-repl-cm6/main.js b/packages/core/examples/vite-vanilla-repl-cm6/main.js index 33f529ff..bede9161 100644 --- a/packages/core/examples/vite-vanilla-repl-cm6/main.js +++ b/packages/core/examples/vite-vanilla-repl-cm6/main.js @@ -2,9 +2,8 @@ import { StrudelMirror } from '@strudel/codemirror'; import { initStrudel } from './strudel'; -import { Drawer } from './drawer'; import { funk42 } from './tunes'; -import { pianoroll, getDrawOptions } from '@strudel.cycles/core'; +import { pianoroll, getDrawOptions, Drawer } from '@strudel.cycles/core'; import './style.css'; const repl = initStrudel(); diff --git a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js b/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js deleted file mode 100644 index cce83699..00000000 --- a/packages/core/examples/vite-vanilla-repl-cm6/one-dark.js +++ /dev/null @@ -1,139 +0,0 @@ -import { EditorView } from '@codemirror/view'; -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { tags as t } from '@lezer/highlight'; - -// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors - -const chalky = '#e5c07b', - coral = '#e06c75', - cyan = '#56b6c2', - invalid = '#ffffff', - ivory = '#abb2bf', - stone = '#7d8799', // Brightened compared to original to increase contrast - malibu = '#61afef', - sage = '#98c379', - whiskey = '#d19a66', - violet = '#c678dd', - darkBackground = '#21252b', - highlightBackground = '#2c313a', - background = '#282c34', - tooltipBackground = '#353a42', - selection = '#3E4451', - cursor = '#528bff'; - -/// The colors used in the theme, as CSS color strings. -export const color = { - chalky, - coral, - cyan, - invalid, - ivory, - stone, - malibu, - sage, - whiskey, - violet, - darkBackground, - highlightBackground, - background, - tooltipBackground, - selection, - cursor, -}; - -/// The editor theme styles for One Dark. -export const oneDarkTheme = EditorView.theme( - { - '&': { - color: ivory, - backgroundColor: background, - }, - - '.cm-content': { - caretColor: cursor, - }, - - '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor }, - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': - { backgroundColor: selection }, - - '.cm-panels': { backgroundColor: darkBackground, color: ivory }, - '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, - '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, - - '.cm-searchMatch': { - backgroundColor: '#72a1ff59', - outline: '1px solid #457dff', - }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: '#6199ff2f', - }, - - '.cm-activeLine': { backgroundColor: '#6699ff0b' }, - '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, - - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - backgroundColor: '#bad0f847', - }, - - '.cm-gutters': { - backgroundColor: background, - color: stone, - border: 'none', - }, - - '.cm-activeLineGutter': { - backgroundColor: highlightBackground, - }, - - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: '#ddd', - }, - - '.cm-tooltip': { - border: 'none', - backgroundColor: tooltipBackground, - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent', - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: tooltipBackground, - borderBottomColor: tooltipBackground, - }, - '.cm-tooltip-autocomplete': { - '& > ul > li[aria-selected]': { - backgroundColor: highlightBackground, - color: ivory, - }, - }, - }, - { dark: true }, -); - -/// The highlighting style for code in the One Dark theme. -export const oneDarkHighlightStyle = HighlightStyle.define([ - { tag: t.keyword, color: violet }, - { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral }, - { tag: [t.function(t.variableName), t.labelName], color: malibu }, - { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey }, - { tag: [t.definition(t.name), t.separator], color: ivory }, - { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky }, - { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan }, - { tag: [t.meta, t.comment], color: stone }, - { tag: t.strong, fontWeight: 'bold' }, - { tag: t.emphasis, fontStyle: 'italic' }, - { tag: t.strikethrough, textDecoration: 'line-through' }, - { tag: t.link, color: stone, textDecoration: 'underline' }, - { tag: t.heading, fontWeight: 'bold', color: coral }, - { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey }, - { tag: [t.processingInstruction, t.string, t.inserted], color: sage }, - { tag: t.invalid, color: invalid }, -]); - -/// Extension to enable the One Dark theme (both the editor theme and -/// the highlight style). -export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];