Merge branch 'main' into add-program-change

This commit is contained in:
Felix Roos 2025-03-08 22:08:03 +01:00
commit 09dd374722
No known key found for this signature in database
53 changed files with 1763 additions and 395 deletions

View file

@ -29,6 +29,7 @@
"@strudel/csound": "workspace:*",
"@strudel/desktopbridge": "workspace:*",
"@strudel/draw": "workspace:*",
"@strudel/gamepad": "workspace:*",
"@strudel/hydra": "workspace:*",
"@strudel/midi": "workspace:*",
"@strudel/mini": "workspace:*",

Binary file not shown.

View file

@ -0,0 +1,7 @@
100% free for personal and commercial use.
However it's limited on basic latin only,
contact riedjal@gmail.com for full glyph (based on ANSI encoding)
and OTF features (alternates).
src: https://www.dafont.com/cute-aurora.font?text=%24%3A+s%28%22bd%285%2C8%29%22%29.superimpose%28x+%3D%3E+x.note%28%22c2%22%29.midi%28device%29%29

View file

@ -36,8 +36,6 @@ export function Showcase() {
}
let _videos = [
{ title: 'Coding Music With Strudel Workhop by Dan Gorelick and Viola He', id: 'oqyAJ4WeKoU' },
{ title: 'Hexe - playing w strudel live coding music', id: '03m3F5xVOMg' },
{ title: 'DJ_Dave - Array [Lil Data Edit]', id: 'KUujFuTcuKc' },
{ title: 'DJ_Dave - Bitrot [v10101a Edit]', id: 'z_cJMdBp67Q' },
{ title: 'you will not steve reich your way out of it', id: 'xpILnXcWyuo' },
@ -58,7 +56,6 @@ let _videos = [
},
{ title: 'letSeaTstrudeL @ solstice stream 2023', id: 'fTiX6dVtdWQ' },
{ title: 'totalgee (Glen F) @ solstice stream 2023', id: 'IvI6uaE3nLU' },
{ title: 'Dan Gorelick @ solstice stream 2023', id: 'qMJEljJyPi0' },
//
/* { // not sure if this is copyrighted ...
title: 'Creative Coding @ Chalmers University of Technology, video by svt.se',
@ -126,6 +123,11 @@ let _videos = [
'A first foray into combining (an early version) strudel and hydra, using flok for collaborative coding.',
},
{ title: 'froos @ Algorave 10th Birthday stream', id: 'IcMSocdKwvw' },
{ title: 'todepasta 1.5', id: 'gCwaVu1Mijg' },
{ title: 'Djenerative Music by Bogdan Vera @ TOPLAP solstice Dec 2024', id: 'LtMX4Lr1nzY' },
{ title: 'La musique by BuboBubo @ TOPLAP solstice Dec 2024', id: 'Oz00Y_f80wU' },
{ title: 'Livecode and vocal breaks by Switch Angel @ TOPLAP solstice Dec 2024', id: '2kzjOIsL6CM' },
{ title: 'Eddyflux algorave set @ rudolf5', id: 'MXz8131Ut0A' },
];
_shuffled = shuffleArray(_videos);

View file

@ -9,11 +9,11 @@ import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'
// }
export default function UdelsEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context;
return (
<div className={'h-full flex w-full flex-col relative'}>
<div className={'h-full flex w-full flex-col relative'} {...editorProps}>
<Loader active={pending} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">

View file

@ -4,7 +4,7 @@ export default function UdelsHeader(Props) {
const { numWindows, setNumWindows } = Props;
return (
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-800">
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
<div className={'mt-[1px] cursor-pointer'}>🌀</div>

View file

@ -84,6 +84,7 @@ export const SIDEBAR: Sidebar = {
{ text: 'Music metadata', link: 'learn/metadata' },
{ text: 'CSound', link: 'learn/csound' },
{ text: 'Hydra', link: 'learn/hydra' },
{ text: 'Input Devices', link: 'learn/input-devices' },
{ text: 'Device Motion', link: 'learn/devicemotion' },
],
'Pattern Functions': [

View file

@ -1,6 +1,10 @@
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
export function getMetadata(raw_code) {
if (raw_code == null) {
console.error('could not extract metadata from missing pattern code');
raw_code = '';
}
const comment_regexp = /\/\*([\s\S]*?)\*\/|\/\/(.*)$/gm;
const comments = [...raw_code.matchAll(comment_regexp)].map((c) => (c[1] || c[2] || '').trim());
const tags = {};

View file

@ -5,6 +5,6 @@ layout: ../../layouts/MainLayout.astro
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import DeviceMotion from '../../../../packages/motion/docs/devicemotion.mdx';
import DeviceMotion from '@strudel/motion/docs/devicemotion.mdx';
<DeviceMotion />

View file

@ -293,8 +293,6 @@ global effects use the same chain for all events of the same orbit:
<JsDoc client:idle name="iresponse" h={0} />
Next, we'll look at strudel's support for [Csound](/learn/csound).
## Phaser
### phaser
@ -312,3 +310,5 @@ Next, we'll look at strudel's support for [Csound](/learn/csound).
### phasersweep
<JsDoc client:idle name="phasersweep" h={0} />
Next, we'll look at input / output via [MIDI, OSC and other methods](/learn/input-output).

View file

@ -0,0 +1,15 @@
---
title: Input Devices
layout: ../../layouts/MainLayout.astro
---
import { MiniRepl } from '../../docs/MiniRepl';
import { JsDoc } from '../../docs/JsDoc';
import Gamepad from '@strudel/gamepad/docs/gamepad.mdx';
# Input Devices
Strudel supports various input devices like Gamepads and MIDI controllers to manipulate patterns in real-time.
<Gamepad />

View file

@ -100,9 +100,9 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
<JsDoc client:idle name="contract" h={0} />
### repeat
### extend
<JsDoc client:idle name="repeat" h={0} />
<JsDoc client:idle name="extend" h={0} />
### take
@ -116,10 +116,6 @@ Earlier versions of many of these functions had `s_` prefixes, and the `pace` fu
<JsDoc client:idle name="polymeter" h={0} />
### polymeterSteps
<JsDoc client:idle name="polymeterSteps" h={0} />
### shrink
<JsDoc client:idle name="shrink" h={0} />

View file

@ -9,10 +9,12 @@ import UdelsEditor from '@components/Udels/UdelsEditor';
import ReplEditor from './components/ReplEditor';
import EmbeddedReplEditor from './components/EmbeddedReplEditor';
import { useReplContext } from './useReplContext';
import { useSettings } from '@src/settings.mjs';
export function Repl({ embedded = false }) {
const isEmbedded = embedded || isIframe();
const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor;
const context = useReplContext();
return <Editor context={context} />;
const { fontFamily } = useSettings();
return <Editor context={context} style={{ fontFamily }} />;
}

View file

@ -9,10 +9,10 @@ import { Header } from './Header';
// }
export default function EmbeddedReplEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context;
return (
<div className="h-full flex flex-col relative">
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />

View file

@ -11,7 +11,7 @@ export function Header({ context, embedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
context;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen, isButtonRowHidden, isCSSAnimationDisabled } = useSettings();
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<header
@ -22,6 +22,7 @@ export function Header({ context, embedded = false }) {
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex',
)}
style={{ fontFamily }}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
<h1
@ -46,7 +47,7 @@ export function Header({ context, embedded = false }) {
}
}}
>
<span className="block rotate-90"></span>
<span className="block text-foreground rotate-90"></span>
</div>
{!isZen && (
<div className="space-x-2">

View file

@ -10,13 +10,13 @@ import { useSettings } from '@src/settings.mjs';
// }
export default function ReplEditor(Props) {
const { context } = Props;
const { context, ...editorProps } = Props;
const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings();
const { panelPosition, isZen } = settings;
return (
<div className="h-full flex flex-col relative">
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} />
<div className="grow flex relative overflow-hidden">

View file

@ -0,0 +1,67 @@
import { Textbox } from '../textbox/Textbox';
import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) {
return (
<button
tabIndex={-1}
className={cx(
'border border-transparent p-1 text-center hover:text-background text-sm transition-all hover:bg-foreground active:bg-lineBackground disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none',
className,
)}
type="button"
{...buttonProps}
>
{children}
</button>
);
}
export function Incrementor({
onChange,
value,
min = -Infinity,
max = Infinity,
className,
incrementLabel = 'next page',
decrementLabel = 'prev page',
...incrementorProps
}) {
value = parseInt(value);
value = isNaN(value) ? '' : value;
return (
<div className={cx('w-fit bg-background relative flex items-center"> rounded-md', className)}>
<Textbox
min={min}
max={max}
onChange={(v) => {
if (v.length && v < min) {
return;
}
onChange(v);
}}
type="number"
placeholder=""
value={value}
className="w-32 mb-0 mt-0 border-none rounded-r-none bg-transparent appearance-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
{...incrementorProps}
/>
<div className="flex gap-1 ">
<IncButton disabled={value <= min} onClick={() => onChange(value - 1)} aria-label={decrementLabel}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</IncButton>
<IncButton
className="rounded-r-md"
disabled={value >= max}
onClick={() => onChange(value + 1)}
aria-label={incrementLabel}
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" className="w-4 h-4">
<path d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" />
</svg>
</IncButton>
</div>
</div>
);
}

View file

@ -0,0 +1,5 @@
import { Incrementor } from '../incrementor/Incrementor';
export function Pagination({ currPage, onPageChange, className, ...incrementorProps }) {
return <Incrementor min={1} value={currPage} onChange={onPageChange} className={className} {...incrementorProps} />;
}

View file

@ -1,53 +1,32 @@
import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger';
export function ConsoleTab() {
const [log, setLog] = useState([]);
const { fontFamily, fontSize } = useSettings();
useLogger(
useCallback((e) => {
const { message, type, data } = e.detail;
setLog((l) => {
const lastLog = l.length ? l[l.length - 1] : undefined;
const id = nanoid(12);
// if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
if (type === 'loaded-sample') {
// const loadIndex = l.length - 1;
const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
l[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
l = l.concat([{ message, type, id, data }]);
}
return l.slice(-20);
});
}, []),
);
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
return (
<div
id="console-tab"
className="break-all px-4 dark:text-white text-stone-900 text-sm py-2 space-y-1"
style={{ fontFamily, fontSize }}
>
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
);
})}
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
return (
<div
key={l.id}
className={cx(
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline',
)}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
{l.count ? ` (${l.count})` : ''}
</div>
);
})}
</div>
</div>
);
}
@ -72,7 +51,3 @@ function linkify(inputText) {
return replacedText;
}
function useLogger(onTrigger) {
useEvent(logger.key, onTrigger);
}

View file

@ -5,6 +5,7 @@ import { FilesTab } from './FilesTab';
import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
@ -115,6 +116,7 @@ function PanelNav({ children, className, settings, ...props }) {
}
function PanelContent({ context, tab }) {
useLogger();
switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;

View file

@ -1,6 +1,8 @@
import {
exportPatterns,
importPatterns,
loadAndSetFeaturedPatterns,
loadAndSetPublicPatterns,
patternFilterName,
useActivePattern,
useViewingPatternData,
@ -12,10 +14,10 @@ import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { settingsMap, useSettings } from '../../../settings.mjs';
function classNames(...classes) {
return classes.filter(Boolean).join(' ');
}
import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@ -25,21 +27,19 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const date = new Date(pattern.created_at);
if (!isNaN(date)) {
title = date.toLocaleDateString();
} else {
title = 'unnamed';
}
}
if (title == null) {
title = pattern.hash;
}
if (title == null) {
title = 'unnamed';
}
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
}
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
return (
<a
className={classNames(
className={cx(
'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1',
showHiglight && 'bg-selection',
@ -56,7 +56,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternID = viewingPatternData.id;
return (
<div className="font-mono text-sm">
<div className="">
{Object.values(patterns)
.reverse()
.map((pattern) => {
@ -84,82 +84,72 @@ function ActionButton({ children, onClick, label, labelIsHidden }) {
);
}
export function PatternsTab({ context }) {
const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const autoResetPatternOnChange = !isUdels();
function UserPatterns({ context }) {
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter } = useSettings();
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const updateCodeWindow = (patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
const viewingPatternID = viewingPatternData?.id;
const autoResetPatternOnChange = !isUdels();
return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user && (
<div>
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow({ ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(data);
}}
/>
</div>
</div>
)}
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div>
<section className="flex overflow-y-auto max-h-full flex-grow flex-col">
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{patternFilter === patternFilterName.user && (
<PatternButtons
onClick={(id) =>
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
updateCodeWindow(
context,
{ ...userPatterns[id], collection: userPattern.collection },
autoResetPatternOnChange,
)
}
patterns={userPatterns}
started={context.started}
@ -167,24 +157,111 @@ export function PatternsTab({ context }) {
viewingPatternID={viewingPatternID}
/>
)}
{patternFilter !== patternFilterName.user &&
Array.from(collections.keys()).map((collection) => {
const patterns = collections.get(collection);
return (
<section key={collection} className="py-2">
<h2 className="text-xl mb-2">{collection}</h2>
<div className="font-mono text-sm">
<PatternButtons
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
</section>
);
})}
</section>
</div>
</div>
);
}
function PatternPageWithPagination({ patterns, patternOnClick, context, paginationOnChange, initialPage }) {
const [page, setPage] = useState(initialPage);
const debouncedPageChange = useDebounce(() => {
paginationOnChange(page);
});
const onPageChange = (pageNum) => {
setPage(pageNum);
debouncedPageChange();
};
const activePattern = useActivePattern();
return (
<div className="flex flex-grow flex-col h-full overflow-hidden justify-between">
<div className="overflow-auto flex flex-col flex-grow bg-background p-2 rounded-md ">
<PatternButtons
onClick={(id) => patternOnClick(id)}
started={context.started}
patterns={patterns}
activePattern={activePattern}
/>
</div>
<div className="flex items-center gap-2 py-2">
<label htmlFor="pattern pagination">Page</label>
<Pagination id="pattern pagination" currPage={page} onPageChange={onPageChange} />
</div>
</div>
);
}
let featuredPageNum = 1;
function FeaturedPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.featured);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={featuredPageNum}
patternOnClick={(id) => {
updateCodeWindow(
context,
{ ...patterns[id], collection: patternFilterName.featured },
autoResetPatternOnChange,
);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetFeaturedPatterns(pageNum - 1);
featuredPageNum = pageNum;
}}
/>
);
}
let latestPageNum = 1;
function LatestPatterns({ context }) {
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
const patterns = collections.get(patternFilterName.public);
return (
<PatternPageWithPagination
patterns={patterns}
context={context}
initialPage={latestPageNum}
patternOnClick={(id) => {
updateCodeWindow(context, { ...patterns[id], collection: patternFilterName.public }, autoResetPatternOnChange);
}}
paginationOnChange={async (pageNum) => {
await loadAndSetPublicPatterns(pageNum - 1);
latestPageNum = pageNum;
}}
/>
);
}
function PublicPatterns({ context }) {
const { patternFilter } = useSettings();
if (patternFilter === patternFilterName.featured) {
return <FeaturedPatterns context={context} />;
}
return <LatestPatterns context={context} />;
}
export function PatternsTab({ context }) {
const { patternFilter } = useSettings();
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user ? (
<UserPatterns context={context} />
) : (
<PublicPatterns context={context} />
)}
</div>
);
}

View file

@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox';
const availableFunctions = jsdocJson.docs
.filter(({ name, description }) => name && !name.startsWith('_') && !!description)
.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
@ -25,21 +26,16 @@ export function Reference() {
}, [search]);
return (
<div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full flex">
<input
className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search"
value={search}
onInput={(event) => setSearch(event.target.value)}
/>
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
</div>
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
{visibleFunctions.map((entry, i) => (
<a
key={i}
className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
className="cursor-pointer text-foreground flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
onClick={() => {
const el = document.getElementById(`doc-${i}`);
const container = document.getElementById('reference-container');
@ -79,7 +75,9 @@ export function Reference() {
))}
</ul>
{entry.examples?.map((example, j) => (
<pre key={j}>{example}</pre>
<pre className="bg-background" key={j}>
{example}
</pre>
))}
</section>
))}

View file

@ -66,6 +66,7 @@ const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
const fontFamilyOptions = {
monospace: 'monospace',
Courier: 'Courier',
CutiePi: 'CutiePi',
JetBrains: 'JetBrains',
Hack: 'Hack',
FiraCode: 'FiraCode',
@ -108,7 +109,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return (
<div className="text-foreground p-4 space-y-4 w-full">
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@ -141,7 +142,7 @@ export function SettingsTab({ started }) {
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans">
<FormItem label="Font Family">
<SelectInput
options={fontFamilyOptions}

View file

@ -5,6 +5,7 @@ import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx';
const getSamples = (samples) =>
Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1;
@ -52,13 +53,8 @@ export function SoundsTab() {
});
return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<input
className="w-full p-1 bg-background rounded-md my-2"
placeholder="Search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup
@ -74,7 +70,7 @@ export function SoundsTab() {
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-2">
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
{soundEntries.map(([name, { data, onTrigger }]) => {
return (
<span

View file

@ -1,11 +1,12 @@
import cx from '@src/cx.mjs';
import { useSettings } from '@src/settings.mjs';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function WelcomeTab({ context }) {
const { fontFamily } = useSettings();
return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}>
<h3> welcome</h3>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
@ -43,7 +44,8 @@ export function WelcomeTab({ context }) {
<a href="https://github.com/tidalcycles/strudel" target="_blank">
github
</a>
. Please consider to{' '}
. You can also find <a href="https://github.com/felixroos/dough-samples/blob/main/README.md">licensing info</a>{' '}
for the default sound banks there. Please consider to{' '}
<a href="https://opencollective.com/tidalcycles" target="_blank">
support this project
</a>{' '}

View file

@ -0,0 +1,11 @@
import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx('p-1 bg-background rounded-md my-2 border-foreground', className)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
);
}

View file

@ -0,0 +1,33 @@
import useEvent from '@src/useEvent.mjs';
import { logger } from '@strudel/core';
import { nanoid } from 'nanoid';
import { atom } from 'nanostores';
export const $strudel_log_history = atom([]);
function useLoggerEvent(onTrigger) {
useEvent(logger.key, onTrigger);
}
function getUpdatedLog(log, event) {
const { message, type, data } = event.detail;
const lastLog = log.length ? log[log.length - 1] : undefined;
const id = nanoid(12);
if (type === 'loaded-sample') {
const loadIndex = log.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
log[loadIndex] = { message, type, id, data };
} else if (lastLog && lastLog.message === message) {
log = log.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
} else {
log = log.concat([{ message, type, id, data }]);
}
return log.slice(-20);
}
export function useLogger() {
useLoggerEvent((event) => {
const log = $strudel_log_history.get();
const newLog = getUpdatedLog(log, event);
$strudel_log_history.set(newLog);
});
}

View file

@ -0,0 +1,30 @@
import { useMemo } from 'react';
import { useEffect } from 'react';
import { useRef } from 'react';
function debounce(fn, wait) {
let timer;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn(...args), wait);
};
}
export function useDebounce(callback) {
const ref = useRef;
useEffect(() => {
ref.current = callback;
}, [callback]);
const debouncedCallback = useMemo(() => {
const func = () => {
ref.current?.();
};
return debounce(func, 1000);
}, []);
return debouncedCallback;
}

View file

@ -1,4 +1,4 @@
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
import { $featuredPatterns, $publicPatterns, patternFilterName } from '../user_pattern_utils.mjs';
import { useStore } from '@nanostores/react';
import { useMemo } from 'react';
import * as tunes from '../repl/tunes.mjs';
@ -12,9 +12,9 @@ export const useExamplePatterns = () => {
const publicPatterns = useStore($publicPatterns);
const collections = useMemo(() => {
const pats = new Map();
pats.set(collectionName.featured, featuredPatterns);
pats.set(collectionName.public, publicPatterns);
// pats.set(collectionName.stock, stockPatterns);
pats.set(patternFilterName.featured, featuredPatterns);
pats.set(patternFilterName.public, publicPatterns);
// pats.set(patternFilterName.stock, stockPatterns);
return pats;
}, [featuredPatterns, publicPatterns]);

View file

@ -81,6 +81,7 @@ export function loadModules() {
import('@strudel/soundfonts'),
import('@strudel/csound'),
import('@strudel/tidal'),
import('@strudel/gamepad'),
import('@strudel/motion'),
import('@strudel/mqtt'),
];

View file

@ -1,6 +1,7 @@
@font-face {
font-family: 'PressStart';
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
size-adjust: 65%;
}
@font-face {
font-family: 'BigBlueTerminal';
@ -14,6 +15,11 @@
font-family: 'galactico';
src: url('/fonts/galactico/Galactico-Basic.otf');
}
@font-face {
font-family: 'CutiePi';
src: url('/fonts/CutiePi/Cute_Aurora_demo.ttf');
size-adjust: 120%;
}
@font-face {
font-family: 'JetBrains';
src: url('/fonts/JetBrains/JetBrainsMono.woff2');
@ -21,6 +27,7 @@
@font-face {
font-family: 'Monocraft';
src: url('/fonts/Monocraft/Monocraft.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'Hack';
@ -41,10 +48,12 @@
@font-face {
font-family: 'teletext';
src: url('/fonts/teletext/EuropeanTeletext.ttf');
size-adjust: 90%;
}
@font-face {
font-family: 'mode7';
src: url('/fonts/mode7/MODE7GX3.TTF');
size-adjust: 82%;
}
.prose > h1:not(:first-child) {

View file

@ -8,16 +8,12 @@ import { confirmDialog, parseJSON, supabase } from './repl/util.mjs';
export let $publicPatterns = atom([]);
export let $featuredPatterns = atom([]);
export const collectionName = {
user: 'user',
public: 'Last Creations',
stock: 'Stock Examples',
featured: 'Featured',
};
const patternQueryLimit = 20;
export const patternFilterName = {
community: 'community',
public: 'latest',
featured: 'featured',
user: 'user',
// stock: 'stock examples',
};
const sessionAtom = (name, initial = undefined) => {
@ -36,7 +32,7 @@ const sessionAtom = (name, initial = undefined) => {
export let $viewingPatternData = sessionAtom('viewingPatternData', {
id: '',
code: '',
collection: collectionName.user,
collection: patternFilterName.user,
created_at: Date.now(),
});
@ -51,25 +47,50 @@ export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data));
};
export function loadPublicPatterns() {
return supabase.from('code_v1').select().eq('public', true).limit(20).order('id', { ascending: false });
function parsePageNum(page) {
return isNaN(page) ? 0 : page;
}
export function loadPublicPatterns(page) {
page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('public', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
}
export function loadFeaturedPatterns() {
return supabase.from('code_v1').select().eq('featured', true).limit(20).order('id', { ascending: false });
export function loadFeaturedPatterns(page = 0) {
page = parsePageNum(page);
const offset = page * patternQueryLimit;
return supabase
.from('code_v1')
.select()
.eq('featured', true)
.range(offset, offset + patternQueryLimit)
.order('id', { ascending: false });
}
export async function loadAndSetPublicPatterns(page) {
const p = await loadPublicPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$publicPatterns.set(pats);
}
export async function loadAndSetFeaturedPatterns(page) {
const p = await loadFeaturedPatterns(page);
const data = p?.data;
const pats = {};
data?.forEach((data, key) => (pats[data.id ?? key] = data));
$featuredPatterns.set(pats);
}
export async function loadDBPatterns() {
try {
const { data: publicPatterns } = await loadPublicPatterns();
const { data: featuredPatterns } = await loadFeaturedPatterns();
const featured = {};
const pub = {};
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
$publicPatterns.set(pub);
$featuredPatterns.set(featured);
await loadAndSetPublicPatterns();
await loadAndSetFeaturedPatterns();
} catch (err) {
console.error('error loading patterns', err);
}
@ -90,9 +111,9 @@ export function useActivePattern() {
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
const defaultCode = '';
export const defaultCode = '';
export const userPattern = {
collection: collectionName.user,
collection: patternFilterName.user,
getAll() {
const patterns = parseJSON(settingsMap.get().userPatterns);
return patterns ?? {};

View file

@ -46,6 +46,29 @@ module.exports = {
'code::after': {
content: 'none',
},
color: 'var(--foreground)',
a: {
color: 'var(--foreground)',
},
h1: {
color: 'var(--foreground)',
},
h2: {
color: 'var(--foreground)',
},
h3: {
color: 'var(--foreground)',
},
h4: {
color: 'var(--foreground)',
},
pre: {
color: 'var(--foreground)',
background: 'var(--background)',
},
code: {
color: 'var(--foreground)',
},
},
},
};