diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs index 658a682f1..58c675638 100644 --- a/packages/codemirror/themes/strudel-theme.mjs +++ b/packages/codemirror/themes/strudel-theme.mjs @@ -5,7 +5,7 @@ export const settings = { background: '#222', lineBackground: '#22222299', foreground: '#fff', - muted: '#ffffff50', + muted: '#8a919966', caret: '#ffcc00', selection: 'rgba(128, 203, 196, 0.5)', selectionMatch: '#036dd626', diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index b7a047bfc..df9990982 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -1055,91 +1055,88 @@ function _composeOp(a, b, func) { return func(a, b); } -// Make composers -(function () { - // pattern composers - const composers = { - set: [(a, b) => b], - keep: [(a) => a], - keepif: [(a, b) => (b ? a : undefined)], +// pattern composers +const COMPOSERS = { + set: [(a, b) => b], + keep: [(a) => a], + keepif: [(a, b) => (b ? a : undefined)], - // numerical functions - /** - * - * Assumes a pattern of numbers. Adds the given number to each item in the pattern. - * @name add - * @memberof Pattern - * @tags math - * @example - * // Here, the triad 0, 2, 4 is shifted by different amounts - * n("0 2 4".add("<0 3 4 0>")).scale("C:major") - * // Without add, the equivalent would be: - * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") - * @example - * // You can also use add with notes: - * note("c3 e3 g3".add("<0 5 7 0>")) - * // Behind the scenes, the notes are converted to midi numbers: - * // note("48 52 55".add("<0 5 7 0>")) - */ - add: [numeralArgs((a, b) => a + b)], // support string concatenation - /** - * - * Like add, but the given numbers are subtracted. - * @name sub - * @memberof Pattern - * @tags math - * @example - * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") - * // See add for more information. - */ - sub: [numeralArgs((a, b) => a - b)], - /** - * - * Multiplies each number by the given factor. - * @name mul - * @memberof Pattern - * @tags math - * @example - * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() - */ - mul: [numeralArgs((a, b) => a * b)], - /** - * - * Divides each number by the given factor. - * @name div - * @memberof Pattern - * @tags math - */ - div: [numeralArgs((a, b) => a / b)], - mod: [numeralArgs(_mod)], - pow: [numeralArgs(Math.pow)], - log2: [numeralArgs(Math.log2)], - band: [numeralArgs((a, b) => a & b)], - bor: [numeralArgs((a, b) => a | b)], - bxor: [numeralArgs((a, b) => a ^ b)], - blshift: [numeralArgs((a, b) => a << b)], - brshift: [numeralArgs((a, b) => a >> b)], + // numerical functions + /** + * + * Assumes a pattern of numbers. Adds the given number to each item in the pattern. + * @name add + * @memberof Pattern + * @tags math + * @example + * // Here, the triad 0, 2, 4 is shifted by different amounts + * n("0 2 4".add("<0 3 4 0>")).scale("C:major") + * // Without add, the equivalent would be: + * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") + * @example + * // You can also use add with notes: + * note("c3 e3 g3".add("<0 5 7 0>")) + * // Behind the scenes, the notes are converted to midi numbers: + * // note("48 52 55".add("<0 5 7 0>")) + */ + add: [numeralArgs((a, b) => a + b)], // support string concatenation + /** + * + * Like add, but the given numbers are subtracted. + * @name sub + * @memberof Pattern + * @tags math + * @example + * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") + * // See add for more information. + */ + sub: [numeralArgs((a, b) => a - b)], + /** + * + * Multiplies each number by the given factor. + * @name mul + * @memberof Pattern + * @tags math + * @example + * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() + */ + mul: [numeralArgs((a, b) => a * b)], + /** + * + * Divides each number by the given factor. + * @name div + * @memberof Pattern + * @tags math + */ + div: [numeralArgs((a, b) => a / b)], + mod: [numeralArgs(_mod)], + pow: [numeralArgs(Math.pow)], + log2: [numeralArgs(Math.log2)], + band: [numeralArgs((a, b) => a & b)], + bor: [numeralArgs((a, b) => a | b)], + bxor: [numeralArgs((a, b) => a ^ b)], + blshift: [numeralArgs((a, b) => a << b)], + brshift: [numeralArgs((a, b) => a >> b)], - // TODO - force numerical comparison if both look like numbers? - lt: [(a, b) => a < b], - gt: [(a, b) => a > b], - lte: [(a, b) => a <= b], - gte: [(a, b) => a >= b], - eq: [(a, b) => a == b], - eqt: [(a, b) => a === b], - ne: [(a, b) => a != b], - net: [(a, b) => a !== b], - and: [(a, b) => a && b], - or: [(a, b) => a || b], + // TODO - force numerical comparison if both look like numbers? + lt: [(a, b) => a < b], + gt: [(a, b) => a > b], + lte: [(a, b) => a <= b], + gte: [(a, b) => a >= b], + eq: [(a, b) => a == b], + eqt: [(a, b) => a === b], + ne: [(a, b) => a != b], + net: [(a, b) => a !== b], + and: [(a, b) => a && b], + or: [(a, b) => a || b], - // bitwise ops - func: [(a, b) => b(a)], - }; - - const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; + // bitwise ops + func: [(a, b) => b(a)], +}; +const _setupAlignments = () => { // generate methods to do what and how - for (const [what, [op, preprocess]] of Object.entries(composers)) { + for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) { // make plain version, e.g. pat._add(value) adds that plain value // to all the values in pat Pattern.prototype['_' + what] = function (value) { @@ -1148,16 +1145,18 @@ function _composeOp(a, b, func) { // make patternified monster version Object.defineProperty(Pattern.prototype, what, { + // Set to configurable so we can update if the default alignment changes + configurable: true, // a getter that returns a function, so 'pat' can be // accessed by closures that are methods of that function.. get: function () { const pat = this; // wrap the 'in' function as default behaviour - const wrapper = (...other) => pat[what]['in'](...other); + const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other); // add methods to that function for each behaviour - for (const how of hows) { + for (const how of ALIGNMENTS) { wrapper[how.toLowerCase()] = function (...other) { var howpat = pat; other = sequence(other); @@ -1182,15 +1181,23 @@ function _composeOp(a, b, func) { return wrapper; }, }); - - // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) - for (const how of hows) { - Pattern.prototype[how.toLowerCase()] = function (...args) { - return this.set[how.toLowerCase()](args); - }; - } } +}; +let DEFAULT_ALIGNMENT = 'in'; +const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly']; +const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase()); + +// Make composers +(function () { + _setupAlignments(); + + // Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2) + for (const how of ALIGNMENTS) { + Pattern.prototype[how.toLowerCase()] = function (...args) { + return this.set[how.toLowerCase()](args); + }; + } // binary composers /** * Applies the given structure to the pattern: @@ -1249,6 +1256,27 @@ function _composeOp(a, b, func) { }; })(); +/** + * Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel. + * The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right. + * By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings. + * + * Note that we say the _default_ method, because alignments can also be set explicitly with calls like + * 'add.mix', 'set.squeeze', etc. + * + * @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly' + * @example + * setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc. + * s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3") + */ +export const setDefaultJoin = (alignment) => { + alignment = alignment?.toLowerCase(); + if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) { + DEFAULT_ALIGNMENT = alignment; + _setupAlignments(); + } +}; + // aliases export const polyrhythm = stack; export const pr = stack; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 284f18557..4d2675308 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -10874,6 +10874,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = ` ] `; +exports[`runs examples > example "setDefaultJoin" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]", + "[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]", + "[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]", + "[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]", + "[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]", + "[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]", + "[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]", +] +`; + exports[`runs examples > example "setGainCurve" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd gain:0.5 ]", diff --git a/vitest.setup.mjs b/vitest.setup.mjs index ea965df6d..7b582a458 100644 --- a/vitest.setup.mjs +++ b/vitest.setup.mjs @@ -1,7 +1,9 @@ import { afterEach } from 'vitest'; import { useRNG } from './packages/core/signal.mjs'; +import { setDefaultJoin } from './packages/core/pattern.mjs'; afterEach(() => { // Avoid bleed between tests useRNG('legacy'); + setDefaultJoin('in'); }); diff --git a/website/src/components/Udels/UdelsEditor.jsx b/website/src/components/Udels/UdelsEditor.jsx index f58ac5fc7..b89576351 100644 --- a/website/src/components/Udels/UdelsEditor.jsx +++ b/website/src/components/Udels/UdelsEditor.jsx @@ -1,5 +1,5 @@ import Loader from '@src/repl/components/Loader'; -import { HorizontalPanel } from '@src/repl/components/panel/Panel'; +import { BottomPanel } from '@src/repl/components/panel/Panel'; import { Code } from '@src/repl/components/Code'; import BigPlayButton from '@src/repl/components/BigPlayButton'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; @@ -20,7 +20,7 @@ export default function UdelsEditor(Props) { - + ); } diff --git a/website/src/repl/components/Code.jsx b/website/src/repl/components/Code.jsx index 8481cc272..e8cccb86f 100644 --- a/website/src/repl/components/Code.jsx +++ b/website/src/repl/components/Code.jsx @@ -8,7 +8,7 @@ export function Code(Props) { return (
{ containerRef.current = el; diff --git a/website/src/repl/components/EmbeddedReplEditor.jsx b/website/src/repl/components/EmbeddedReplEditor.jsx index 74db2c655..100d5ad95 100644 --- a/website/src/repl/components/EmbeddedReplEditor.jsx +++ b/website/src/repl/components/EmbeddedReplEditor.jsx @@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader'; import { Code } from '@src/repl/components/Code'; import BigPlayButton from '@src/repl/components/BigPlayButton'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; -import { Header } from './Header'; +import { MainPanel } from './panel/Panel'; // type Props = { // context: replcontext, @@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) { return (
-
+
diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx deleted file mode 100644 index 19e7b8fad..000000000 --- a/website/src/repl/components/Header.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; -import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; -import cx from '@src/cx.mjs'; -import { useSettings, setIsZen } from '../../settings.mjs'; -import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon'; -import '../Repl.css'; - -const { BASE_URL } = import.meta.env; -const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; - -export function Header({ context, isEmbedded = false }) { - const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context; - const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); - - return ( - - ); -} diff --git a/website/src/repl/components/ReplEditor.jsx b/website/src/repl/components/ReplEditor.jsx index 03cf89f14..437475e61 100644 --- a/website/src/repl/components/ReplEditor.jsx +++ b/website/src/repl/components/ReplEditor.jsx @@ -1,8 +1,7 @@ -import Loader from '@src/repl/components/Loader'; -import { HorizontalPanel, VerticalPanel, PanelToggle } from '@src/repl/components/panel/Panel'; import { Code } from '@src/repl/components/Code'; +import Loader from '@src/repl/components/Loader'; +import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; -import { Header } from './Header'; import { useSettings } from '@src/settings.mjs'; // type Props = { @@ -19,15 +18,17 @@ export default function ReplEditor(Props) { return (
-
- -
- - {!isZen && panelPosition === 'right' && } - +
+ {/* */} + +
+ + {!isZen && panelPosition === 'right' && } +
- {!isZen && panelPosition === 'bottom' && } + {!isZen && panelPosition === 'bottom' && } + {/* */}
); } diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index f413a1e97..245cf961e 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -2,7 +2,7 @@ import cx from '@src/cx.mjs'; export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) { return ( - diff --git a/website/src/repl/components/icons/SidebarIcon.jsx b/website/src/repl/components/icons/SidebarIcon.jsx new file mode 100644 index 000000000..07966a2ab --- /dev/null +++ b/website/src/repl/components/icons/SidebarIcon.jsx @@ -0,0 +1,19 @@ +export function SidebarIcon() { + return ( + + + + + ); +} diff --git a/website/src/repl/components/panel/ConsoleTab.jsx b/website/src/repl/components/panel/ConsoleTab.jsx index a80442548..54a8ff549 100644 --- a/website/src/repl/components/panel/ConsoleTab.jsx +++ b/website/src/repl/components/panel/ConsoleTab.jsx @@ -2,13 +2,23 @@ import cx from '@src/cx.mjs'; import { useSettings } from '../../../settings.mjs'; import { useStore } from '@nanostores/react'; import { $strudel_log_history } from '../useLogger'; +import { useEffect, useRef } from 'react'; export function ConsoleTab() { const log = useStore($strudel_log_history); const { fontFamily } = useSettings(); + const scrollRef = useRef(); + // scroll to bottom when log changes + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [log]); return ( -
-
+
+
+ {' '} + {/* bg-background */} {log.map((l, i) => { const message = linkify(l.message); const color = l.data?.hap?.value?.color; @@ -16,12 +26,13 @@ export function ConsoleTab() {
- + {l.count ? ` (${l.count})` : ''}
); diff --git a/website/src/repl/components/panel/ExportTab.jsx b/website/src/repl/components/panel/ExportTab.jsx index 071d8d695..1e953def2 100644 --- a/website/src/repl/components/panel/ExportTab.jsx +++ b/website/src/repl/components/panel/ExportTab.jsx @@ -45,7 +45,7 @@ export default function ExportTab(Props) { return ( <> -
+
{ @@ -56,7 +56,7 @@ export default function ExportTab(Props) { }} disabled={exporting} placeholder="Leave empty to use current date" - className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} + className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')} value={downloadName ?? ''} /> diff --git a/website/src/repl/components/panel/Forms.jsx b/website/src/repl/components/panel/Forms.jsx index 11c893356..8b4e8c0fe 100644 --- a/website/src/repl/components/panel/Forms.jsx +++ b/website/src/repl/components/panel/Forms.jsx @@ -1,15 +1,15 @@ import cx from '@src/cx.mjs'; -export function ButtonGroup({ value, onChange, items }) { +export function ButtonGroup({ value, onChange, items, wrap = false }) { return ( -
+
{Object.entries(items).map(([key, label], i, arr) => ( + + {!isEmbedded && ( + + )} + {!isEmbedded && ( + + learn + + )} +
+ ); +} + function PanelCloseButton() { const { isPanelOpen } = useSettings(); return ( isPanelOpen && (