diff --git a/packages/core/euclid.mjs b/packages/core/euclid.mjs index 6a5be89b..6aa2283d 100644 --- a/packages/core/euclid.mjs +++ b/packages/core/euclid.mjs @@ -73,7 +73,7 @@ const bjork = function (ons, steps) { */ /** - * Like `iter`, but has an additional parameter for 'rotating' the resulting sequence. + * Like `euclid`, but has an additional parameter for 'rotating' the resulting sequence. * @memberof Pattern * @name euclidRot * @param {number} pulses the number of onsets / beats diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 2af83df9..d3fc7b4d 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -712,7 +712,7 @@ export class Pattern { * @memberof Pattern * @example * s("hh*2").stack( - * n("c2(3,8)") + * note("c2(3,8)") * ) */ stack(...pats) { @@ -729,7 +729,7 @@ export class Pattern { * @memberof Pattern * @example * s("hh*2").seq( - * n("c2(3,8)") + * note("c2(3,8)") * ) */ seq(...pats) { @@ -742,7 +742,7 @@ export class Pattern { * @memberof Pattern * @example * s("hh*2").cat( - * n("c2(3,8)") + * note("c2(3,8)") * ) */ cat(...pats) { @@ -776,8 +776,10 @@ export class Pattern { ); } - log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`) { - return this.onTrigger((...args) => logger(func(...args)), false); + log(func = (_, hap) => `[hap] ${hap.showWhole(true)}`, getData = (_, hap) => ({ hap })) { + return this.onTrigger((...args) => { + logger(func(...args), undefined, getData(...args)); + }, false); } logValues(func = id) { @@ -1045,7 +1047,12 @@ Pattern.prototype.factories = { // Elemental patterns -// Nothing +/** + * Does absolutely nothing.. + * @name silence + * @example + * silence // "~" + */ export const silence = new Pattern(() => []); /** A discrete value that repeats once per cycle. @@ -1215,7 +1222,17 @@ function _sequenceCount(x) { } return [reify(x), 1]; } - +/** + * Aligns one or more given sequences to the given number of steps per cycle. + * + * @name polymeterSteps + * @param {number} steps how many items are placed in one cycle + * @param {any[]} sequences one or more arrays of Patterns / values + * @example + * polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"]) + * .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes + * // note("{c d e f g f e d}%2").stack(s("bd")) + */ export function polymeterSteps(steps, ...args) { const seqs = args.map((a) => _sequenceCount(a)); if (seqs.length == 0) { @@ -1238,6 +1255,14 @@ export function polymeterSteps(steps, ...args) { return stack(...pats); } +/** + * Combines the given lists of patterns with the same pulse. This will create so called polymeters when different sized sequences are used. + * @name polymeter + * @example + * polymeter(["c", "eb", "g"], ["c2", "g2"]).note() + * // "{c eb g, c2 g2}".note() + * + */ export function polymeter(...args) { return polymeterSteps(0, ...args); } @@ -1354,7 +1379,11 @@ export const round = register('round', function (pat) { * Assumes a numerical pattern. Returns a new pattern with all values set to * their mathematical floor. E.g. `3.7` replaced with to `3`, and `-4.2` * replaced with `-5`. + * @name floor + * @memberof Pattern * @returns Pattern + * @example + * "42 42.1 42.5 43".floor().note() */ export const floor = register('floor', function (pat) { return pat.asNumber().fmap((v) => Math.floor(v)); @@ -1364,7 +1393,11 @@ export const floor = register('floor', function (pat) { * Assumes a numerical pattern. Returns a new pattern with all values set to * their mathematical ceiling. E.g. `3.2` replaced with `4`, and `-4.2` * replaced with `-4`. + * @name ceil + * @memberof Pattern * @returns Pattern + * @example + * "42 42.1 42.5 43".ceil().note() */ export const ceil = register('ceil', function (pat) { return pat.asNumber().fmap((v) => Math.ceil(v)); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 6bef319b..c71019ac 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -12,6 +12,7 @@ export function repl({ afterEval, getTime, transpiler, + editPattern, onToggle, }) { const scheduler = new Cyclist({ @@ -41,8 +42,10 @@ export function repl({ } try { beforeEval?.({ code }); - const { pattern } = await _evaluate(code, transpiler); + let { pattern } = await _evaluate(code, transpiler); + logger(`[eval] code updated`); + pattern = editPattern?.(pattern) || pattern; scheduler.setPattern(pattern, autostart); afterEval?.({ code, pattern }); return pattern; diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index bdcd41d6..5e9d79dc 100644 --- a/packages/react/dist/index.cjs.js +++ b/packages/react/dist/index.cjs.js @@ -1 +1 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("react"),Z=require("@uiw/react-codemirror"),w=require("@codemirror/view"),x=require("@codemirror/state"),ee=require("@codemirror/lang-javascript"),c=require("@lezer/highlight"),te=require("@uiw/codemirror-themes"),B=require("@strudel.cycles/core"),K=require("@strudel.cycles/webaudio"),re=require("react-hook-inview"),ae=require("@strudel.cycles/transpiler"),I=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},l=I(t),oe=I(Z),ne=te.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#00000050",gutterBackground:"transparent",gutterForeground:"#8a919966"},styles:[{tag:c.tags.keyword,color:"#c792ea"},{tag:c.tags.operator,color:"#89ddff"},{tag:c.tags.special(c.tags.variableName),color:"#eeffff"},{tag:c.tags.typeName,color:"#c3e88d"},{tag:c.tags.atom,color:"#f78c6c"},{tag:c.tags.number,color:"#c3e88d"},{tag:c.tags.definition(c.tags.variableName),color:"#82aaff"},{tag:c.tags.string,color:"#c3e88d"},{tag:c.tags.special(c.tags.string),color:"#c3e88d"},{tag:c.tags.comment,color:"#7d8799"},{tag:c.tags.variableName,color:"#c792ea"},{tag:c.tags.tagName,color:"#c3e88d"},{tag:c.tags.bracket,color:"#525154"},{tag:c.tags.meta,color:"#ffcb6b"},{tag:c.tags.attributeName,color:"#c792ea"},{tag:c.tags.propertyName,color:"#c792ea"},{tag:c.tags.className,color:"#decb6b"},{tag:c.tags.invalid,color:"#ffffff"}]});const L=x.StateEffect.define(),se=x.StateField.define({create(){return w.Decoration.none},update(e,a){try{for(let r of a.effects)if(r.is(L))if(r.value){const s=w.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=w.Decoration.set([s.range(0,a.newDoc.length)])}else e=w.Decoration.set([]);return e}catch(r){return console.warn("flash error",r),e}},provide:e=>w.EditorView.decorations.from(e)}),U=e=>{e.dispatch({effects:L.of(!0)}),setTimeout(()=>{e.dispatch({effects:L.of(!1)})},200)},A=x.StateEffect.define(),ce=x.StateField.define({create(){return w.Decoration.none},update(e,a){try{for(let r of a.effects)if(r.is(A)){const s=r.value.map(n=>(n.context.locations||[]).map(({start:i,end:u})=>{const f=n.context.color||"#FFCA28";let o=a.newDoc.line(i.line).from+i.column,d=a.newDoc.line(u.line).from+u.column;const g=a.newDoc.length;return o>g||d>g?void 0:w.Decoration.mark({attributes:{style:`outline: 1.5px solid ${f};`}}).range(o,d)})).flat().filter(Boolean)||[];e=w.Decoration.set(s,!0)}return e}catch{return w.Decoration.set([])}},provide:e=>w.EditorView.decorations.from(e)}),ie=[ee.javascript(),ne,ce,se];function W({value:e,onChange:a,onViewChanged:r,onSelectionChange:s,options:n,editorDidMount:i}){const u=t.useCallback(d=>{a?.(d)},[a]),f=t.useCallback(d=>{r?.(d)},[r]),o=t.useCallback(d=>{d.selectionSet&&s&&s?.(d.state.selection)},[s]);return l.default.createElement(l.default.Fragment,null,l.default.createElement(oe.default,{value:e,onChange:u,onCreateEditor:f,onUpdate:o,extensions:ie}))}function z(...e){return e.filter(Boolean).join(" ")}function $({view:e,pattern:a,active:r,getTime:s}){const n=t.useRef([]),i=t.useRef();t.useEffect(()=>{if(e)if(a&&r){let u=requestAnimationFrame(function f(){try{const o=s(),g=[Math.max(i.current||o,o-1/10,0),o+1/60];i.current=g[1],n.current=n.current.filter(v=>v.whole.end>o);const m=a.queryArc(...g).filter(v=>v.hasOnset());n.current=n.current.concat(m),e.dispatch({effects:A.of(n.current)})}catch{e.dispatch({effects:A.of([])})}u=requestAnimationFrame(f)});return()=>{cancelAnimationFrame(u)}}else n.current=[],e.dispatch({effects:A.of([])})},[a,r,e])}function le(e,a=!1){const r=t.useRef(),s=t.useRef(),n=f=>{if(s.current!==void 0){const o=f-s.current;e(f,o)}s.current=f,r.current=requestAnimationFrame(n)},i=()=>{r.current=requestAnimationFrame(n)},u=()=>{r.current&&cancelAnimationFrame(r.current),delete r.current};return t.useEffect(()=>{r.current&&(u(),i())},[e]),t.useEffect(()=>(a&&i(),u),[]),{start:i,stop:u}}function ue({pattern:e,started:a,getTime:r,onDraw:s}){let n=t.useRef([]),i=t.useRef(null);const{start:u,stop:f}=le(t.useCallback(()=>{const o=r();if(i.current===null){i.current=o;return}const d=e.queryArc(Math.max(i.current,o-1/10),o),g=4;i.current=o,n.current=(n.current||[]).filter(m=>m.whole.end>o-g).concat(d.filter(m=>m.hasOnset())),s(o,n.current)},[e]));t.useEffect(()=>{a?u():(n.current=[],f())},[a])}function J(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(a=>window.postMessage(a,"*"),[])}function G({defaultOutput:e,interval:a,getTime:r,evalOnMount:s=!1,initialCode:n="",autolink:i=!1,beforeEval:u,afterEval:f,onEvalError:o,onToggle:d,canvasId:g}){const m=t.useMemo(()=>de(),[]);g=g||`canvas-${m}`;const[v,k]=t.useState(),[C,q]=t.useState(),[E,M]=t.useState(n),[_,T]=t.useState(),[P,D]=t.useState(),[F,H]=t.useState(!1),b=E!==_,{scheduler:h,evaluate:R,start:Q,stop:V,pause:X}=t.useMemo(()=>B.repl({interval:a,defaultOutput:e,onSchedulerError:k,onEvalError:p=>{q(p),o?.(p)},getTime:r,transpiler:ae.transpiler,beforeEval:({code:p})=>{M(p),u?.()},afterEval:({pattern:p,code:N})=>{T(N),D(p),q(),k(),i&&(window.location.hash="#"+encodeURIComponent(btoa(N))),f?.()},onToggle:p=>{H(p),d?.(p)}}),[e,a,r]),Y=J(({data:{from:p,type:N}})=>{N==="start"&&p!==m&&V()}),S=t.useCallback(async(p=!0)=>{await R(E,p),Y({type:"start",from:m})},[R,E]),j=t.useRef();return t.useEffect(()=>{!j.current&&s&&E&&(j.current=!0,S())},[S,s,E]),t.useEffect(()=>()=>{h.stop()},[h]),{id:m,canvasId:g,code:E,setCode:M,error:v||C,schedulerError:v,scheduler:h,evalError:C,evaluate:R,activateCode:S,activeCode:_,isDirty:b,pattern:P,started:F,start:Q,stop:V,pause:X,togglePlay:async()=>{F?h.pause():await S()}}}function de(){return Math.floor((1+Math.random())*65536).toString(16).substring(1)}function O({type:e}){return l.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:l.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:l.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:l.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}const fe="_container_3i85k_1",ge="_header_3i85k_5",me="_buttons_3i85k_9",he="_button_3i85k_9",pe="_buttonDisabled_3i85k_17",be="_error_3i85k_21",ve="_body_3i85k_25",y={container:fe,header:ge,buttons:me,button:he,buttonDisabled:pe,error:be,body:ve},we=()=>K.getAudioContext().currentTime;function Ee({tune:e,hideOutsideView:a=!1,enableKeyboard:r,withCanvas:s=!1,canvasHeight:n=200}){const{code:i,setCode:u,evaluate:f,activateCode:o,error:d,isDirty:g,activeCode:m,pattern:v,started:k,scheduler:C,togglePlay:q,stop:E,canvasId:M}=G({initialCode:e,defaultOutput:K.webaudioOutput,getTime:we});ue({pattern:v,started:s&&k,getTime:()=>C.now(),onDraw:(b,h)=>{const R=document.querySelector("#"+M).getContext("2d");B.pianoroll({ctx:R,time:b,haps:h,autorange:1,fold:1,playhead:1})}});const[_,T]=t.useState(),[P,D]=re.useInView({threshold:.01}),F=t.useRef(),H=t.useMemo(()=>((D||!a)&&(F.current=!0),D||F.current),[D,a]);return $({view:_,pattern:v,active:k&&!m?.includes("strudel disable-highlighting"),getTime:()=>C.getPhase()}),t.useLayoutEffect(()=>{if(r){const b=async h=>{(h.ctrlKey||h.altKey)&&(h.code==="Enter"?(h.preventDefault(),U(_),await o()):h.code==="Period"&&(E(),h.preventDefault()))};return window.addEventListener("keydown",b,!0),()=>window.removeEventListener("keydown",b,!0)}},[r,v,i,f,E,_]),l.default.createElement("div",{className:y.container,ref:P},l.default.createElement("div",{className:y.header},l.default.createElement("div",{className:y.buttons},l.default.createElement("button",{className:z(y.button,k?"sc-animate-pulse":""),onClick:()=>q()},l.default.createElement(O,{type:k?"pause":"play"})),l.default.createElement("button",{className:z(g?y.button:y.buttonDisabled),onClick:()=>o()},l.default.createElement(O,{type:"refresh"}))),d&&l.default.createElement("div",{className:y.error},d.message)),l.default.createElement("div",{className:y.body},H&&l.default.createElement(W,{value:i,onChange:u,onViewChanged:T})),s&&l.default.createElement("canvas",{id:M,className:"w-full pointer-events-none",height:n,ref:b=>{b&&b.width!==b.clientWidth&&(b.width=b.clientWidth)}}))}const ye=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=W;exports.MiniRepl=Ee;exports.cx=z;exports.flash=U;exports.useHighlighting=$;exports.useKeydown=ye;exports.usePostMessage=J;exports.useStrudel=G; +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const t=require("react"),ee=require("@uiw/react-codemirror"),E=require("@codemirror/view"),H=require("@codemirror/state"),te=require("@codemirror/lang-javascript"),i=require("@lezer/highlight"),re=require("@uiw/codemirror-themes"),B=require("@strudel.cycles/core"),W=require("@strudel.cycles/webaudio"),ae=require("react-hook-inview"),oe=require("@strudel.cycles/transpiler"),$=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},u=$(t),ne=$(ee),se=re.createTheme({theme:"dark",settings:{background:"#222",foreground:"#75baff",caret:"#ffcc00",selection:"rgba(128, 203, 196, 0.5)",selectionMatch:"#036dd626",lineHighlight:"#00000050",gutterBackground:"transparent",gutterForeground:"#8a919966"},styles:[{tag:i.tags.keyword,color:"#c792ea"},{tag:i.tags.operator,color:"#89ddff"},{tag:i.tags.special(i.tags.variableName),color:"#eeffff"},{tag:i.tags.typeName,color:"#c3e88d"},{tag:i.tags.atom,color:"#f78c6c"},{tag:i.tags.number,color:"#c3e88d"},{tag:i.tags.definition(i.tags.variableName),color:"#82aaff"},{tag:i.tags.string,color:"#c3e88d"},{tag:i.tags.special(i.tags.string),color:"#c3e88d"},{tag:i.tags.comment,color:"#7d8799"},{tag:i.tags.variableName,color:"#c792ea"},{tag:i.tags.tagName,color:"#c3e88d"},{tag:i.tags.bracket,color:"#525154"},{tag:i.tags.meta,color:"#ffcb6b"},{tag:i.tags.attributeName,color:"#c792ea"},{tag:i.tags.propertyName,color:"#c792ea"},{tag:i.tags.className,color:"#decb6b"},{tag:i.tags.invalid,color:"#ffffff"}]});const j=H.StateEffect.define(),ce=H.StateField.define({create(){return E.Decoration.none},update(e,a){try{for(let r of a.effects)if(r.is(j))if(r.value){const s=E.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=E.Decoration.set([s.range(0,a.newDoc.length)])}else e=E.Decoration.set([]);return e}catch(r){return console.warn("flash error",r),e}},provide:e=>E.EditorView.decorations.from(e)}),J=e=>{e.dispatch({effects:j.of(!0)}),setTimeout(()=>{e.dispatch({effects:j.of(!1)})},200)},P=H.StateEffect.define(),ie=H.StateField.define({create(){return E.Decoration.none},update(e,a){try{for(let r of a.effects)if(r.is(P)){const s=r.value.map(n=>(n.context.locations||[]).map(({start:l,end:d})=>{const m=n.context.color||"#FFCA28";let o=a.newDoc.line(l.line).from+l.column,f=a.newDoc.line(d.line).from+d.column;const b=a.newDoc.length;return o>b||f>b?void 0:E.Decoration.mark({attributes:{style:`outline: 1.5px solid ${m};`}}).range(o,f)})).flat().filter(Boolean)||[];e=E.Decoration.set(s,!0)}return e}catch{return E.Decoration.set([])}},provide:e=>E.EditorView.decorations.from(e)}),le=[te.javascript(),se,ie,ce];function G({value:e,onChange:a,onViewChanged:r,onSelectionChange:s,options:n,editorDidMount:l}){const d=t.useCallback(f=>{a?.(f)},[a]),m=t.useCallback(f=>{r?.(f)},[r]),o=t.useCallback(f=>{f.selectionSet&&s&&s?.(f.state.selection)},[s]);return u.default.createElement(u.default.Fragment,null,u.default.createElement(ne.default,{value:e,onChange:d,onCreateEditor:m,onUpdate:o,extensions:le}))}function O(...e){return e.filter(Boolean).join(" ")}function Q({view:e,pattern:a,active:r,getTime:s}){const n=t.useRef([]),l=t.useRef();t.useEffect(()=>{if(e)if(a&&r){let d=requestAnimationFrame(function m(){try{const o=s(),b=[Math.max(l.current||o,o-1/10,0),o+1/60];l.current=b[1],n.current=n.current.filter(p=>p.whole.end>o);const v=a.queryArc(...b).filter(p=>p.hasOnset());n.current=n.current.concat(v),e.dispatch({effects:P.of(n.current)})}catch{e.dispatch({effects:P.of([])})}d=requestAnimationFrame(m)});return()=>{cancelAnimationFrame(d)}}else n.current=[],e.dispatch({effects:P.of([])})},[a,r,e])}function ue(e,a=!1){const r=t.useRef(),s=t.useRef(),n=m=>{if(s.current!==void 0){const o=m-s.current;e(m,o)}s.current=m,r.current=requestAnimationFrame(n)},l=()=>{r.current=requestAnimationFrame(n)},d=()=>{r.current&&cancelAnimationFrame(r.current),delete r.current};return t.useEffect(()=>{r.current&&(d(),l())},[e]),t.useEffect(()=>(a&&l(),d),[]),{start:l,stop:d}}function de({pattern:e,started:a,getTime:r,onDraw:s}){let n=t.useRef([]),l=t.useRef(null);const{start:d,stop:m}=ue(t.useCallback(()=>{const o=r();if(l.current===null){l.current=o;return}const f=e.queryArc(Math.max(l.current,o-1/10),o),b=4;l.current=o,n.current=(n.current||[]).filter(v=>v.whole.end>o-b).concat(f.filter(v=>v.hasOnset())),s(o,n.current)},[e]));t.useEffect(()=>{a?d():(n.current=[],m())},[a])}function X(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(a=>window.postMessage(a,"*"),[])}function Y({defaultOutput:e,interval:a,getTime:r,evalOnMount:s=!1,initialCode:n="",autolink:l=!1,beforeEval:d,afterEval:m,editPattern:o,onEvalError:f,onToggle:b,canvasId:v}){const p=t.useMemo(()=>fe(),[]);v=v||`canvas-${p}`;const[k,D]=t.useState(),[q,F]=t.useState(),[w,N]=t.useState(n),[C,T]=t.useState(),[z,R]=t.useState(),[S,V]=t.useState(!1),x=w!==C,{scheduler:M,evaluate:c,start:g,stop:_,pause:I}=t.useMemo(()=>B.repl({interval:a,defaultOutput:e,onSchedulerError:D,onEvalError:h=>{F(h),f?.(h)},getTime:r,transpiler:oe.transpiler,beforeEval:({code:h})=>{N(h),d?.()},editPattern:o?h=>o(h,p):void 0,afterEval:({pattern:h,code:L})=>{T(L),R(h),F(),D(),l&&(window.location.hash="#"+encodeURIComponent(btoa(L))),m?.()},onToggle:h=>{V(h),b?.(h)}}),[e,a,r]),Z=X(({data:{from:h,type:L}})=>{L==="start"&&h!==p&&_()}),A=t.useCallback(async(h=!0)=>{await c(w,h),Z({type:"start",from:p})},[c,w]),K=t.useRef();return t.useEffect(()=>{!K.current&&s&&w&&(K.current=!0,A())},[A,s,w]),t.useEffect(()=>()=>{M.stop()},[M]),{id:p,canvasId:v,code:w,setCode:N,error:k||q,schedulerError:k,scheduler:M,evalError:q,evaluate:c,activateCode:A,activeCode:C,isDirty:x,pattern:z,started:S,start:g,stop:_,pause:I,togglePlay:async()=>{S?M.pause():await A()}}}function fe(){return Math.floor((1+Math.random())*65536).toString(16).substring(1)}function U({type:e}){return u.default.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:u.default.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:u.default.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:u.default.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}const ge="_container_3i85k_1",me="_header_3i85k_5",he="_buttons_3i85k_9",pe="_button_3i85k_9",ve="_buttonDisabled_3i85k_17",be="_error_3i85k_21",Ee="_body_3i85k_25",y={container:ge,header:me,buttons:he,button:pe,buttonDisabled:ve,error:be,body:Ee},we=()=>W.getAudioContext().currentTime;function ye({tune:e,hideOutsideView:a=!1,enableKeyboard:r,withCanvas:s=!1,canvasHeight:n=200}){const{code:l,setCode:d,evaluate:m,activateCode:o,error:f,isDirty:b,activeCode:v,pattern:p,started:k,scheduler:D,togglePlay:q,stop:F,canvasId:w,id:N}=Y({initialCode:e,defaultOutput:W.webaudioOutput,getTime:we,editPattern:(c,g)=>c.withContext(_=>({..._,id:g}))});de({pattern:p,started:s&&k,getTime:()=>D.now(),onDraw:(c,g)=>{const _=document.querySelector("#"+w).getContext("2d");B.pianoroll({ctx:_,time:c,haps:g,autorange:1,fold:1,playhead:1})}});const[C,T]=t.useState(),[z,R]=ae.useInView({threshold:.01}),S=t.useRef(),V=t.useMemo(()=>((R||!a)&&(S.current=!0),R||S.current),[R,a]);Q({view:C,pattern:p,active:k&&!v?.includes("strudel disable-highlighting"),getTime:()=>D.getPhase()}),t.useLayoutEffect(()=>{if(r){const c=async g=>{(g.ctrlKey||g.altKey)&&(g.code==="Enter"?(g.preventDefault(),J(C),await o()):g.code==="Period"&&(F(),g.preventDefault()))};return window.addEventListener("keydown",c,!0),()=>window.removeEventListener("keydown",c,!0)}},[r,p,l,m,F,C]);const[x,M]=t.useState([]);return ke(t.useCallback(c=>{const{data:g}=c.detail;g?.hap?.context?.id===N&&M(I=>I.concat([c.detail]).slice(-10))},[])),u.default.createElement("div",{className:y.container,ref:z},u.default.createElement("div",{className:y.header},u.default.createElement("div",{className:y.buttons},u.default.createElement("button",{className:O(y.button,k?"sc-animate-pulse":""),onClick:()=>q()},u.default.createElement(U,{type:k?"pause":"play"})),u.default.createElement("button",{className:O(b?y.button:y.buttonDisabled),onClick:()=>o()},u.default.createElement(U,{type:"refresh"}))),f&&u.default.createElement("div",{className:y.error},f.message)),u.default.createElement("div",{className:y.body},V&&u.default.createElement(G,{value:l,onChange:d,onViewChanged:T})),s&&u.default.createElement("canvas",{id:w,className:"w-full pointer-events-none",height:n,ref:c=>{c&&c.width!==c.clientWidth&&(c.width=c.clientWidth)}}),!!x.length&&u.default.createElement("div",{className:"sc-bg-gray-800 sc-rounded-md sc-p-2"},x.map(({message:c},g)=>u.default.createElement("div",{key:g},c))))}function ke(e){_e(B.logger.key,e)}function _e(e,a,r=!1){t.useEffect(()=>(document.addEventListener(e,a,r),()=>{document.removeEventListener(e,a,r)}),[a])}const Ce=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=G;exports.MiniRepl=ye;exports.cx=O;exports.flash=J;exports.useHighlighting=Q;exports.useKeydown=Ce;exports.usePostMessage=X;exports.useStrudel=Y; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index 6545e0a7..d0027424 100644 --- a/packages/react/dist/index.es.js +++ b/packages/react/dist/index.es.js @@ -1,15 +1,15 @@ -import i, { useCallback as N, useRef as E, useEffect as F, useMemo as V, useState as _, useLayoutEffect as U } from "react"; -import Y from "@uiw/react-codemirror"; -import { Decoration as y, EditorView as W } from "@codemirror/view"; -import { StateEffect as $, StateField as G } from "@codemirror/state"; -import { javascript as Z } from "@codemirror/lang-javascript"; +import l, { useCallback as N, useRef as k, useEffect as _, useMemo as K, useState as w, useLayoutEffect as G } from "react"; +import Z from "@uiw/react-codemirror"; +import { Decoration as y, EditorView as J } from "@codemirror/view"; +import { StateEffect as Q, StateField as X } from "@codemirror/state"; +import { javascript as ee } from "@codemirror/lang-javascript"; import { tags as s } from "@lezer/highlight"; -import { createTheme as ee } from "@uiw/codemirror-themes"; -import { repl as te, pianoroll as re } from "@strudel.cycles/core"; -import { webaudioOutput as oe, getAudioContext as ne } from "@strudel.cycles/webaudio"; -import { useInView as ae } from "react-hook-inview"; -import { transpiler as se } from "@strudel.cycles/transpiler"; -const ce = ee({ +import { createTheme as te } from "@uiw/codemirror-themes"; +import { repl as re, logger as ne, pianoroll as oe } from "@strudel.cycles/core"; +import { webaudioOutput as ae, getAudioContext as ce } from "@strudel.cycles/webaudio"; +import { useInView as se } from "react-hook-inview"; +import { transpiler as ie } from "@strudel.cycles/transpiler"; +const le = te({ theme: "dark", settings: { background: "#222", @@ -42,14 +42,14 @@ const ce = ee({ { tag: s.invalid, color: "#ffffff" } ] }); -const B = $.define(), ie = G.define({ +const j = Q.define(), ue = X.define({ create() { return y.none; }, update(e, r) { try { for (let t of r.effects) - if (t.is(B)) + if (t.is(j)) if (t.value) { const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } }); e = y.set([a.range(0, r.newDoc.length)]); @@ -60,25 +60,25 @@ const B = $.define(), ie = G.define({ return console.warn("flash error", t), e; } }, - provide: (e) => W.decorations.from(e) -}), le = (e) => { - e.dispatch({ effects: B.of(!0) }), setTimeout(() => { - e.dispatch({ effects: B.of(!1) }); + provide: (e) => J.decorations.from(e) +}), de = (e) => { + e.dispatch({ effects: j.of(!0) }), setTimeout(() => { + e.dispatch({ effects: j.of(!1) }); }, 200); -}, H = $.define(), ue = G.define({ +}, z = Q.define(), fe = X.define({ create() { return y.none; }, update(e, r) { try { for (let t of r.effects) - if (t.is(H)) { + if (t.is(z)) { const a = t.value.map( - (n) => (n.context.locations || []).map(({ start: c, end: l }) => { - const d = n.context.color || "#FFCA28"; - let o = r.newDoc.line(c.line).from + c.column, u = r.newDoc.line(l.line).from + l.column; - const f = r.newDoc.length; - return o > f || u > f ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${d};` } }).range(o, u); + (o) => (o.context.locations || []).map(({ start: i, end: u }) => { + const m = o.context.color || "#FFCA28"; + let n = r.newDoc.line(i.line).from + i.column, d = r.newDoc.line(u.line).from + u.column; + const v = r.newDoc.length; + return n > v || d > v ? void 0 : y.mark({ attributes: { style: `outline: 1.5px solid ${m};` } }).range(n, d); }) ).flat().filter(Boolean) || []; e = y.set(a, !0); @@ -88,291 +88,314 @@ const B = $.define(), ie = G.define({ return y.set([]); } }, - provide: (e) => W.decorations.from(e) -}), de = [Z(), ce, ue, ie]; -function fe({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: n, editorDidMount: c }) { - const l = N( - (u) => { - r?.(u); + provide: (e) => J.decorations.from(e) +}), me = [ee(), le, fe, ue]; +function ge({ value: e, onChange: r, onViewChanged: t, onSelectionChange: a, options: o, editorDidMount: i }) { + const u = N( + (d) => { + r?.(d); }, [r] - ), d = N( - (u) => { - t?.(u); + ), m = N( + (d) => { + t?.(d); }, [t] - ), o = N( - (u) => { - u.selectionSet && a && a?.(u.state.selection); + ), n = N( + (d) => { + d.selectionSet && a && a?.(d.state.selection); }, [a] ); - return /* @__PURE__ */ i.createElement(i.Fragment, null, /* @__PURE__ */ i.createElement(Y, { + return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Z, { value: e, - onChange: l, - onCreateEditor: d, - onUpdate: o, - extensions: de + onChange: u, + onCreateEditor: m, + onUpdate: n, + extensions: me })); } -function j(...e) { +function W(...e) { return e.filter(Boolean).join(" "); } -function me({ view: e, pattern: r, active: t, getTime: a }) { - const n = E([]), c = E(); - F(() => { +function pe({ view: e, pattern: r, active: t, getTime: a }) { + const o = k([]), i = k(); + _(() => { if (e) if (r && t) { - let l = requestAnimationFrame(function d() { + let u = requestAnimationFrame(function m() { try { - const o = a(), f = [Math.max(c.current || o, o - 1 / 10, 0), o + 1 / 60]; - c.current = f[1], n.current = n.current.filter((v) => v.whole.end > o); - const m = r.queryArc(...f).filter((v) => v.hasOnset()); - n.current = n.current.concat(m), e.dispatch({ effects: H.of(n.current) }); + const n = a(), v = [Math.max(i.current || n, n - 1 / 10, 0), n + 1 / 60]; + i.current = v[1], o.current = o.current.filter((p) => p.whole.end > n); + const h = r.queryArc(...v).filter((p) => p.hasOnset()); + o.current = o.current.concat(h), e.dispatch({ effects: z.of(o.current) }); } catch { - e.dispatch({ effects: H.of([]) }); + e.dispatch({ effects: z.of([]) }); } - l = requestAnimationFrame(d); + u = requestAnimationFrame(m); }); return () => { - cancelAnimationFrame(l); + cancelAnimationFrame(u); }; } else - n.current = [], e.dispatch({ effects: H.of([]) }); + o.current = [], e.dispatch({ effects: z.of([]) }); }, [r, t, e]); } -function ge(e, r = !1) { - const t = E(), a = E(), n = (d) => { +function he(e, r = !1) { + const t = k(), a = k(), o = (m) => { if (a.current !== void 0) { - const o = d - a.current; - e(d, o); + const n = m - a.current; + e(m, n); } - a.current = d, t.current = requestAnimationFrame(n); - }, c = () => { - t.current = requestAnimationFrame(n); - }, l = () => { + a.current = m, t.current = requestAnimationFrame(o); + }, i = () => { + t.current = requestAnimationFrame(o); + }, u = () => { t.current && cancelAnimationFrame(t.current), delete t.current; }; - return F(() => { - t.current && (l(), c()); - }, [e]), F(() => (r && c(), l), []), { - start: c, - stop: l + return _(() => { + t.current && (u(), i()); + }, [e]), _(() => (r && i(), u), []), { + start: i, + stop: u }; } -function pe({ pattern: e, started: r, getTime: t, onDraw: a }) { - let n = E([]), c = E(null); - const { start: l, stop: d } = ge( +function ve({ pattern: e, started: r, getTime: t, onDraw: a }) { + let o = k([]), i = k(null); + const { start: u, stop: m } = he( N(() => { - const o = t(); - if (c.current === null) { - c.current = o; + const n = t(); + if (i.current === null) { + i.current = n; return; } - const u = e.queryArc(Math.max(c.current, o - 1 / 10), o), f = 4; - c.current = o, n.current = (n.current || []).filter((m) => m.whole.end > o - f).concat(u.filter((m) => m.hasOnset())), a(o, n.current); + const d = e.queryArc(Math.max(i.current, n - 1 / 10), n), v = 4; + i.current = n, o.current = (o.current || []).filter((h) => h.whole.end > n - v).concat(d.filter((h) => h.hasOnset())), a(n, o.current); }, [e]) ); - F(() => { - r ? l() : (n.current = [], d()); + _(() => { + r ? u() : (o.current = [], m()); }, [r]); } -function he(e) { - return F(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []); +function be(e) { + return _(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []); } -function ve({ +function Ee({ defaultOutput: e, interval: r, getTime: t, evalOnMount: a = !1, - initialCode: n = "", - autolink: c = !1, - beforeEval: l, - afterEval: d, - onEvalError: o, - onToggle: u, - canvasId: f + initialCode: o = "", + autolink: i = !1, + beforeEval: u, + afterEval: m, + editPattern: n, + onEvalError: d, + onToggle: v, + canvasId: h }) { - const m = V(() => be(), []); - f = f || `canvas-${m}`; - const [v, k] = _(), [M, T] = _(), [b, A] = _(n), [C, S] = _(), [z, D] = _(), [x, L] = _(!1), h = b !== C, { scheduler: g, evaluate: R, start: J, stop: O, pause: Q } = V( - () => te({ + const p = K(() => we(), []); + h = h || `canvas-${p}`; + const [F, A] = w(), [P, D] = w(), [b, q] = w(o), [x, V] = w(), [I, R] = w(), [L, B] = w(!1), H = b !== x, { scheduler: M, evaluate: c, start: f, stop: C, pause: O } = K( + () => re({ interval: r, defaultOutput: e, - onSchedulerError: k, - onEvalError: (p) => { - T(p), o?.(p); + onSchedulerError: A, + onEvalError: (g) => { + D(g), d?.(g); }, getTime: t, - transpiler: se, - beforeEval: ({ code: p }) => { - A(p), l?.(); + transpiler: ie, + beforeEval: ({ code: g }) => { + q(g), u?.(); }, - afterEval: ({ pattern: p, code: q }) => { - S(q), D(p), T(), k(), c && (window.location.hash = "#" + encodeURIComponent(btoa(q))), d?.(); + editPattern: n ? (g) => n(g, p) : void 0, + afterEval: ({ pattern: g, code: T }) => { + V(T), R(g), D(), A(), i && (window.location.hash = "#" + encodeURIComponent(btoa(T))), m?.(); }, - onToggle: (p) => { - L(p), u?.(p); + onToggle: (g) => { + B(g), v?.(g); } }), [e, r, t] - ), X = he(({ data: { from: p, type: q } }) => { - q === "start" && p !== m && O(); - }), P = N( - async (p = !0) => { - await R(b, p), X({ type: "start", from: m }); + ), Y = be(({ data: { from: g, type: T } }) => { + T === "start" && g !== p && C(); + }), S = N( + async (g = !0) => { + await c(b, g), Y({ type: "start", from: p }); }, - [R, b] - ), K = E(); - return F(() => { - !K.current && a && b && (K.current = !0, P()); - }, [P, a, b]), F(() => () => { - g.stop(); - }, [g]), { - id: m, - canvasId: f, + [c, b] + ), U = k(); + return _(() => { + !U.current && a && b && (U.current = !0, S()); + }, [S, a, b]), _(() => () => { + M.stop(); + }, [M]), { + id: p, + canvasId: h, code: b, - setCode: A, - error: v || M, - schedulerError: v, - scheduler: g, - evalError: M, - evaluate: R, - activateCode: P, - activeCode: C, - isDirty: h, - pattern: z, - started: x, - start: J, - stop: O, - pause: Q, + setCode: q, + error: F || P, + schedulerError: F, + scheduler: M, + evalError: P, + evaluate: c, + activateCode: S, + activeCode: x, + isDirty: H, + pattern: I, + started: L, + start: f, + stop: C, + pause: O, togglePlay: async () => { - x ? g.pause() : await P(); + L ? M.pause() : await S(); } }; } -function be() { +function we() { return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); } -function I({ type: e }) { - return /* @__PURE__ */ i.createElement("svg", { +function $({ type: e }) { + return /* @__PURE__ */ l.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", className: "sc-h-5 sc-w-5", viewBox: "0 0 20 20", fill: "currentColor" }, { - refresh: /* @__PURE__ */ i.createElement("path", { + refresh: /* @__PURE__ */ l.createElement("path", { fillRule: "evenodd", d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", clipRule: "evenodd" }), - play: /* @__PURE__ */ i.createElement("path", { + play: /* @__PURE__ */ l.createElement("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }), - pause: /* @__PURE__ */ i.createElement("path", { + pause: /* @__PURE__ */ l.createElement("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", clipRule: "evenodd" }) }[e]); } -const we = "_container_3i85k_1", ye = "_header_3i85k_5", Ee = "_buttons_3i85k_9", ke = "_button_3i85k_9", _e = "_buttonDisabled_3i85k_17", Fe = "_error_3i85k_21", Ce = "_body_3i85k_25", w = { - container: we, - header: ye, - buttons: Ee, - button: ke, - buttonDisabled: _e, - error: Fe, - body: Ce -}, Ne = () => ne().currentTime; -function Be({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: n = 200 }) { +const ye = "_container_3i85k_1", ke = "_header_3i85k_5", _e = "_buttons_3i85k_9", Fe = "_button_3i85k_9", Ce = "_buttonDisabled_3i85k_17", Ne = "_error_3i85k_21", xe = "_body_3i85k_25", E = { + container: ye, + header: ke, + buttons: _e, + button: Fe, + buttonDisabled: Ce, + error: Ne, + body: xe +}, Me = () => ce().currentTime; +function je({ tune: e, hideOutsideView: r = !1, enableKeyboard: t, withCanvas: a = !1, canvasHeight: o = 200 }) { const { - code: c, - setCode: l, - evaluate: d, - activateCode: o, - error: u, - isDirty: f, - activeCode: m, - pattern: v, - started: k, - scheduler: M, - togglePlay: T, - stop: b, - canvasId: A - } = ve({ + code: i, + setCode: u, + evaluate: m, + activateCode: n, + error: d, + isDirty: v, + activeCode: h, + pattern: p, + started: F, + scheduler: A, + togglePlay: P, + stop: D, + canvasId: b, + id: q + } = Ee({ initialCode: e, - defaultOutput: oe, - getTime: Ne + defaultOutput: ae, + getTime: Me, + editPattern: (c, f) => c.withContext((C) => ({ ...C, id: f })) }); - pe({ - pattern: v, - started: a && k, - getTime: () => M.now(), - onDraw: (h, g) => { - const R = document.querySelector("#" + A).getContext("2d"); - re({ ctx: R, time: h, haps: g, autorange: 1, fold: 1, playhead: 1 }); + ve({ + pattern: p, + started: a && F, + getTime: () => A.now(), + onDraw: (c, f) => { + const C = document.querySelector("#" + b).getContext("2d"); + oe({ ctx: C, time: c, haps: f, autorange: 1, fold: 1, playhead: 1 }); } }); - const [C, S] = _(), [z, D] = ae({ + const [x, V] = w(), [I, R] = se({ threshold: 0.01 - }), x = E(), L = V(() => ((D || !r) && (x.current = !0), D || x.current), [D, r]); - return me({ - view: C, - pattern: v, - active: k && !m?.includes("strudel disable-highlighting"), - getTime: () => M.getPhase() - }), U(() => { + }), L = k(), B = K(() => ((R || !r) && (L.current = !0), R || L.current), [R, r]); + pe({ + view: x, + pattern: p, + active: F && !h?.includes("strudel disable-highlighting"), + getTime: () => A.getPhase() + }), G(() => { if (t) { - const h = async (g) => { - (g.ctrlKey || g.altKey) && (g.code === "Enter" ? (g.preventDefault(), le(C), await o()) : g.code === "Period" && (b(), g.preventDefault())); + const c = async (f) => { + (f.ctrlKey || f.altKey) && (f.code === "Enter" ? (f.preventDefault(), de(x), await n()) : f.code === "Period" && (D(), f.preventDefault())); }; - return window.addEventListener("keydown", h, !0), () => window.removeEventListener("keydown", h, !0); + return window.addEventListener("keydown", c, !0), () => window.removeEventListener("keydown", c, !0); } - }, [t, v, c, d, b, C]), /* @__PURE__ */ i.createElement("div", { - className: w.container, - ref: z - }, /* @__PURE__ */ i.createElement("div", { - className: w.header - }, /* @__PURE__ */ i.createElement("div", { - className: w.buttons - }, /* @__PURE__ */ i.createElement("button", { - className: j(w.button, k ? "sc-animate-pulse" : ""), - onClick: () => T() - }, /* @__PURE__ */ i.createElement(I, { - type: k ? "pause" : "play" - })), /* @__PURE__ */ i.createElement("button", { - className: j(f ? w.button : w.buttonDisabled), - onClick: () => o() - }, /* @__PURE__ */ i.createElement(I, { + }, [t, p, i, m, D, x]); + const [H, M] = w([]); + return Ae( + N((c) => { + const { data: f } = c.detail; + f?.hap?.context?.id === q && M((O) => O.concat([c.detail]).slice(-10)); + }, []) + ), /* @__PURE__ */ l.createElement("div", { + className: E.container, + ref: I + }, /* @__PURE__ */ l.createElement("div", { + className: E.header + }, /* @__PURE__ */ l.createElement("div", { + className: E.buttons + }, /* @__PURE__ */ l.createElement("button", { + className: W(E.button, F ? "sc-animate-pulse" : ""), + onClick: () => P() + }, /* @__PURE__ */ l.createElement($, { + type: F ? "pause" : "play" + })), /* @__PURE__ */ l.createElement("button", { + className: W(v ? E.button : E.buttonDisabled), + onClick: () => n() + }, /* @__PURE__ */ l.createElement($, { type: "refresh" - }))), u && /* @__PURE__ */ i.createElement("div", { - className: w.error - }, u.message)), /* @__PURE__ */ i.createElement("div", { - className: w.body - }, L && /* @__PURE__ */ i.createElement(fe, { - value: c, - onChange: l, - onViewChanged: S - })), a && /* @__PURE__ */ i.createElement("canvas", { - id: A, + }))), d && /* @__PURE__ */ l.createElement("div", { + className: E.error + }, d.message)), /* @__PURE__ */ l.createElement("div", { + className: E.body + }, B && /* @__PURE__ */ l.createElement(ge, { + value: i, + onChange: u, + onViewChanged: V + })), a && /* @__PURE__ */ l.createElement("canvas", { + id: b, className: "w-full pointer-events-none", - height: n, - ref: (h) => { - h && h.width !== h.clientWidth && (h.width = h.clientWidth); + height: o, + ref: (c) => { + c && c.width !== c.clientWidth && (c.width = c.clientWidth); } - })); + }), !!H.length && /* @__PURE__ */ l.createElement("div", { + className: "sc-bg-gray-800 sc-rounded-md sc-p-2" + }, H.map(({ message: c }, f) => /* @__PURE__ */ l.createElement("div", { + key: f + }, c)))); } -const Oe = (e) => U(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); +function Ae(e) { + De(ne.key, e); +} +function De(e, r, t = !1) { + _(() => (document.addEventListener(e, r, t), () => { + document.removeEventListener(e, r, t); + }), [r]); +} +const Ue = (e) => G(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); export { - fe as CodeMirror, - Be as MiniRepl, - j as cx, - le as flash, - me as useHighlighting, - Oe as useKeydown, - he as usePostMessage, - ve as useStrudel + ge as CodeMirror, + je as MiniRepl, + W as cx, + de as flash, + pe as useHighlighting, + Ue as useKeydown, + be as usePostMessage, + Ee as useStrudel }; diff --git a/packages/react/dist/style.css b/packages/react/dist/style.css index b28aca62..6e937139 100644 --- a/packages/react/dist/style.css +++ b/packages/react/dist/style.css @@ -1 +1 @@ -.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto} +.cm-editor{background-color:transparent!important;height:100%;z-index:11;font-size:18px}.cm-theme-light{width:100%}.cm-line>*{background:#00000095}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}.sc-rounded-md{border-radius:.375rem}.sc-bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity))}.sc-p-2{padding:.5rem}._container_3i85k_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(34 34 34 / var(--tw-bg-opacity))}._header_3i85k_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_3i85k_9{display:flex}._button_3i85k_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_3i85k_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_3i85k_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_3i85k_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_3i85k_25{position:relative;overflow:auto} diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index a7874e55..70ebe22c 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -1,6 +1,6 @@ import { pianoroll } from '@strudel.cycles/core'; import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; -import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react'; import { useInView } from 'react-hook-inview'; import 'tailwindcss/tailwind.css'; import cx from '../cx'; @@ -11,6 +11,7 @@ import CodeMirror6, { flash } from './CodeMirror6'; import { Icon } from './Icon'; import styles from './MiniRepl.module.css'; import './style.css'; +import { logger } from '@strudel.cycles/core'; const getTime = () => getAudioContext().currentTime; @@ -29,10 +30,14 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa togglePlay, stop, canvasId, + id: replId, } = useStrudel({ initialCode: tune, defaultOutput: webaudioOutput, getTime, + editPattern: (pat, id) => { + return pat.withContext((ctx) => ({ ...ctx, id })); + }, }); usePatternFrame({ @@ -86,6 +91,20 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa } }, [enableKeyboard, pattern, code, evaluate, stop, view]); + const [log, setLog] = useState([]); + useLogger( + useCallback((e) => { + const { data } = e.detail; + const logId = data?.hap?.context?.id; + // const logId = data?.pattern?.meta?.id; + if (logId === replId) { + setLog((l) => { + return l.concat([e.detail]).slice(-10); + }); + } + }, []), + ); + return (
@@ -114,6 +133,28 @@ export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCa }} > )} + {!!log.length && ( +
+ {log.map(({ message }, i) => ( +
{message}
+ ))} +
+ )}
); } + +// TODO: dedupe +function useLogger(onTrigger) { + useEvent(logger.key, onTrigger); +} + +// TODO: dedupe +function useEvent(name, onTrigger, useCapture = false) { + useEffect(() => { + document.addEventListener(name, onTrigger, useCapture); + return () => { + document.removeEventListener(name, onTrigger, useCapture); + }; + }, [onTrigger]); +} diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 5d475e1e..6308cf5e 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -12,6 +12,7 @@ function useStrudel({ autolink = false, beforeEval, afterEval, + editPattern, onEvalError, onToggle, canvasId, @@ -44,6 +45,7 @@ function useStrudel({ setCode(code); beforeEval?.(); }, + editPattern: editPattern ? (pat) => editPattern(pat, id) : undefined, afterEval: ({ pattern: _pattern, code }) => { setActiveCode(code); setPattern(_pattern); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 1696d132..af47a607 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -195,9 +195,14 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => { hap.value = { note: hap.value }; } */ if (typeof hap.value !== 'object') { - throw new Error( - `hap.value ${hap.value} is not supported by webaudio output. Hint: append .note() or .s() to the end`, + logger( + `hap.value "${hap.value}" is not supported by webaudio output. Hint: append .note() or .s() to the end`, + 'error', ); + /* throw new Error( + `hap.value "${hap.value}"" is not supported by webaudio output. Hint: append .note() or .s() to the end`, + ); */ + return; } // calculate correct time (tone.js workaround) let t = ac.currentTime + deadline; diff --git a/test/__snapshots__/examples.test.mjs.snap b/test/__snapshots__/examples.test.mjs.snap index 251cf6a9..7945ba1b 100644 --- a/test/__snapshots__/examples.test.mjs.snap +++ b/test/__snapshots__/examples.test.mjs.snap @@ -899,14 +899,14 @@ exports[`runs examples > example "cat" example index 0 1`] = ` [ "[ 0/1 → 1/2 | s:hh ]", "[ 1/2 → 1/1 | s:hh ]", - "[ 1/1 → 9/8 | n:c2 ]", - "[ 11/8 → 3/2 | n:c2 ]", - "[ 7/4 → 15/8 | n:c2 ]", + "[ 1/1 → 9/8 | note:c2 ]", + "[ 11/8 → 3/2 | note:c2 ]", + "[ 7/4 → 15/8 | note:c2 ]", "[ 2/1 → 5/2 | s:hh ]", "[ 5/2 → 3/1 | s:hh ]", - "[ 3/1 → 25/8 | n:c2 ]", - "[ 27/8 → 7/2 | n:c2 ]", - "[ 15/4 → 31/8 | n:c2 ]", + "[ 3/1 → 25/8 | note:c2 ]", + "[ 27/8 → 7/2 | note:c2 ]", + "[ 15/4 → 31/8 | note:c2 ]", ] `; @@ -920,6 +920,27 @@ exports[`runs examples > example "cat" example index 0 2`] = ` ] `; +exports[`runs examples > example "ceil" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:42 ]", + "[ 1/4 → 1/2 | note:43 ]", + "[ 1/2 → 3/4 | note:43 ]", + "[ 3/4 → 1/1 | note:43 ]", + "[ 1/1 → 5/4 | note:42 ]", + "[ 5/4 → 3/2 | note:43 ]", + "[ 3/2 → 7/4 | note:43 ]", + "[ 7/4 → 2/1 | note:43 ]", + "[ 2/1 → 9/4 | note:42 ]", + "[ 9/4 → 5/2 | note:43 ]", + "[ 5/2 → 11/4 | note:43 ]", + "[ 11/4 → 3/1 | note:43 ]", + "[ 3/1 → 13/4 | note:42 ]", + "[ 13/4 → 7/2 | note:43 ]", + "[ 7/2 → 15/4 | note:43 ]", + "[ 15/4 → 4/1 | note:43 ]", +] +`; + exports[`runs examples > example "chooseCycles" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:bd ]", @@ -1680,6 +1701,27 @@ exports[`runs examples > example "firstOf" example index 0 1`] = ` ] `; +exports[`runs examples > example "floor" example index 0 1`] = ` +[ + "[ 0/1 → 1/4 | note:42 ]", + "[ 1/4 → 1/2 | note:42 ]", + "[ 1/2 → 3/4 | note:42 ]", + "[ 3/4 → 1/1 | note:43 ]", + "[ 1/1 → 5/4 | note:42 ]", + "[ 5/4 → 3/2 | note:42 ]", + "[ 3/2 → 7/4 | note:42 ]", + "[ 7/4 → 2/1 | note:43 ]", + "[ 2/1 → 9/4 | note:42 ]", + "[ 9/4 → 5/2 | note:42 ]", + "[ 5/2 → 11/4 | note:42 ]", + "[ 11/4 → 3/1 | note:43 ]", + "[ 3/1 → 13/4 | note:42 ]", + "[ 13/4 → 7/2 | note:42 ]", + "[ 7/2 → 15/4 | note:42 ]", + "[ 15/4 → 4/1 | note:43 ]", +] +`; + exports[`runs examples > example "freq" example index 0 1`] = ` [ "[ 0/1 → 1/4 | freq:220 s:superzow ]", @@ -2312,6 +2354,52 @@ exports[`runs examples > example "perlin" example index 0 1`] = ` ] `; +exports[`runs examples > example "polymeter" example index 0 1`] = ` +[ + "[ 0/1 → 1/3 | note:c ]", + "[ 1/3 → 2/3 | note:eb ]", + "[ 2/3 → 1/1 | note:g ]", + "[ 1/1 → 4/3 | note:c ]", + "[ 4/3 → 5/3 | note:eb ]", + "[ 5/3 → 2/1 | note:g ]", + "[ 2/1 → 7/3 | note:c ]", + "[ 7/3 → 8/3 | note:eb ]", + "[ 8/3 → 3/1 | note:g ]", + "[ 3/1 → 10/3 | note:c ]", + "[ 10/3 → 11/3 | note:eb ]", + "[ 11/3 → 4/1 | note:g ]", + "[ 0/1 → 1/3 | note:c2 ]", + "[ 1/3 → 2/3 | note:g2 ]", + "[ 2/3 → 1/1 | note:c2 ]", + "[ 1/1 → 4/3 | note:g2 ]", + "[ 4/3 → 5/3 | note:c2 ]", + "[ 5/3 → 2/1 | note:g2 ]", + "[ 2/1 → 7/3 | note:c2 ]", + "[ 7/3 → 8/3 | note:g2 ]", + "[ 8/3 → 3/1 | note:c2 ]", + "[ 3/1 → 10/3 | note:g2 ]", + "[ 10/3 → 11/3 | note:c2 ]", + "[ 11/3 → 4/1 | note:g2 ]", +] +`; + +exports[`runs examples > example "polymeterSteps" example index 0 1`] = ` +[ + "[ 0/1 → 1/2 | note:c ]", + "[ 1/2 → 1/1 | note:d ]", + "[ 1/1 → 3/2 | note:e ]", + "[ 3/2 → 2/1 | note:f ]", + "[ 2/1 → 5/2 | note:g ]", + "[ 5/2 → 3/1 | note:f ]", + "[ 3/1 → 7/2 | note:e ]", + "[ 7/2 → 4/1 | note:d ]", + "[ 0/1 → 1/1 | s:bd ]", + "[ 1/1 → 2/1 | s:bd ]", + "[ 2/1 → 3/1 | s:bd ]", + "[ 3/1 → 4/1 | s:bd ]", +] +`; + exports[`runs examples > example "pure" example index 0 1`] = ` [ "[ 0/1 → 1/1 | e4 ]", @@ -2662,24 +2750,24 @@ exports[`runs examples > example "seq" example index 0 1`] = ` [ "[ 0/1 → 1/4 | s:hh ]", "[ 1/4 → 1/2 | s:hh ]", - "[ 1/2 → 9/16 | n:c2 ]", - "[ 11/16 → 3/4 | n:c2 ]", - "[ 7/8 → 15/16 | n:c2 ]", + "[ 1/2 → 9/16 | note:c2 ]", + "[ 11/16 → 3/4 | note:c2 ]", + "[ 7/8 → 15/16 | note:c2 ]", "[ 1/1 → 5/4 | s:hh ]", "[ 5/4 → 3/2 | s:hh ]", - "[ 3/2 → 25/16 | n:c2 ]", - "[ 27/16 → 7/4 | n:c2 ]", - "[ 15/8 → 31/16 | n:c2 ]", + "[ 3/2 → 25/16 | note:c2 ]", + "[ 27/16 → 7/4 | note:c2 ]", + "[ 15/8 → 31/16 | note:c2 ]", "[ 2/1 → 9/4 | s:hh ]", "[ 9/4 → 5/2 | s:hh ]", - "[ 5/2 → 41/16 | n:c2 ]", - "[ 43/16 → 11/4 | n:c2 ]", - "[ 23/8 → 47/16 | n:c2 ]", + "[ 5/2 → 41/16 | note:c2 ]", + "[ 43/16 → 11/4 | note:c2 ]", + "[ 23/8 → 47/16 | note:c2 ]", "[ 3/1 → 13/4 | s:hh ]", "[ 13/4 → 7/2 | s:hh ]", - "[ 7/2 → 57/16 | n:c2 ]", - "[ 59/16 → 15/4 | n:c2 ]", - "[ 31/8 → 63/16 | n:c2 ]", + "[ 7/2 → 57/16 | note:c2 ]", + "[ 59/16 → 15/4 | note:c2 ]", + "[ 31/8 → 63/16 | note:c2 ]", ] `; @@ -2733,6 +2821,8 @@ exports[`runs examples > example "shape" example index 0 1`] = ` ] `; +exports[`runs examples > example "silence" example index 0 1`] = `[]`; + exports[`runs examples > example "sine" example index 0 1`] = ` [ "[ 0/1 → 1/8 | note:Eb4 ]", @@ -2956,18 +3046,18 @@ exports[`runs examples > example "stack" example index 0 1`] = ` "[ 5/2 → 3/1 | s:hh ]", "[ 3/1 → 7/2 | s:hh ]", "[ 7/2 → 4/1 | s:hh ]", - "[ 0/1 → 1/8 | n:c2 ]", - "[ 3/8 → 1/2 | n:c2 ]", - "[ 3/4 → 7/8 | n:c2 ]", - "[ 1/1 → 9/8 | n:c2 ]", - "[ 11/8 → 3/2 | n:c2 ]", - "[ 7/4 → 15/8 | n:c2 ]", - "[ 2/1 → 17/8 | n:c2 ]", - "[ 19/8 → 5/2 | n:c2 ]", - "[ 11/4 → 23/8 | n:c2 ]", - "[ 3/1 → 25/8 | n:c2 ]", - "[ 27/8 → 7/2 | n:c2 ]", - "[ 15/4 → 31/8 | n:c2 ]", + "[ 0/1 → 1/8 | note:c2 ]", + "[ 3/8 → 1/2 | note:c2 ]", + "[ 3/4 → 7/8 | note:c2 ]", + "[ 1/1 → 9/8 | note:c2 ]", + "[ 11/8 → 3/2 | note:c2 ]", + "[ 7/4 → 15/8 | note:c2 ]", + "[ 2/1 → 17/8 | note:c2 ]", + "[ 19/8 → 5/2 | note:c2 ]", + "[ 11/4 → 23/8 | note:c2 ]", + "[ 3/1 → 25/8 | note:c2 ]", + "[ 27/8 → 7/2 | note:c2 ]", + "[ 15/4 → 31/8 | note:c2 ]", ] `; diff --git a/website/src/config.ts b/website/src/config.ts index 088782f1..c6cc9f59 100644 --- a/website/src/config.ts +++ b/website/src/config.ts @@ -49,19 +49,29 @@ export const SIDEBAR: Sidebar = { { text: 'Sounds', link: 'learn/sounds' }, { text: 'Coding syntax', link: 'learn/code' }, { text: 'Mini-Notation', link: 'learn/mini-notation' }, + ], + 'Making Sound': [ { text: 'Samples', link: 'learn/samples' }, { text: 'Synths', link: 'learn/synths' }, { text: 'Audio Effects', link: 'learn/effects' }, - { text: 'Functions', link: 'learn/functions' }, + ], + 'Pattern Functions': [ + { text: 'Introduction', link: 'functions/intro' }, + { text: 'Pattern Constructors', link: 'learn/factories' }, + { text: 'Time Modifiers', link: 'learn/time-modifiers' }, + { text: 'Control Parameters', link: 'functions/value-modifiers' }, { text: 'Signals', link: 'learn/signals' }, - { text: 'Tonal', link: 'learn/tonal' }, + { text: 'Conditional Modifiers', link: 'learn/conditional-modifiers' }, + { text: 'Tonal Modifiers', link: 'learn/tonal' }, + ], + More: [ + { text: 'Patterns', link: 'technical-manual/patterns' }, + { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, { text: 'MIDI & OSC', link: 'learn/input-output' }, { text: 'Strudel vs Tidal', link: 'learn/strudel-vs-tidal' }, ], - 'Technical Manual': [ - { text: 'Patterns', link: 'technical-manual/patterns' }, + Development: [ { text: 'REPL', link: 'technical-manual/repl' }, - { text: 'Pattern Alignment', link: 'technical-manual/alignment' }, { text: 'Docs', link: 'technical-manual/docs' }, { text: 'Testing', link: 'technical-manual/testing' }, ], diff --git a/website/src/docs/JsDoc.jsx b/website/src/docs/JsDoc.jsx index d75409c7..3c8dd1eb 100644 --- a/website/src/docs/JsDoc.jsx +++ b/website/src/docs/JsDoc.jsx @@ -2,7 +2,7 @@ import jsdoc from '../../../doc.json'; // doc.json is built with `npm run jsdoc- const docs = jsdoc.docs.reduce((acc, obj) => Object.assign(acc, { [obj.longname]: obj }), {}); import { MiniRepl } from './MiniRepl'; -export function JsDoc({ name, h = 3 }) { +export function JsDoc({ name, h = 3, hideDescription }) { const item = docs[name]; if (!item) { console.warn('Not found: ' + name); @@ -16,7 +16,7 @@ export function JsDoc({ name, h = 3 }) { return ( <> {!!h && {item.longname}} -
+ {!hideDescription &&
}
    {item.params?.map((param, i) => (
  • diff --git a/website/src/pages/functions/intro.mdx b/website/src/pages/functions/intro.mdx new file mode 100644 index 00000000..bed3cb37 --- /dev/null +++ b/website/src/pages/functions/intro.mdx @@ -0,0 +1,73 @@ +--- +title: Introduction +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Functional JavaScript API + +While the mini notation is powerful on its own, there is much more to discover. +Internally, the mini notation will expand to use the actual functional JavaScript API. + +For example, this Pattern in Mini Notation: + + + +is equivalent to this Pattern without Mini Notation: + + + +Similarly, there is an equivalent function for every aspect of the mini notation. + +Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API +to fit better for the larger context, while mini notation is more practical for individiual rhythms. + +## Limits of Mini Notation + +While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example: + + + +Here, we are using mini notation for the individual rhythms, while using the function `stack` to mix them. +While stack is also available as `,` in mini notation, we cannot use it here, because we have different types of sounds. + +## Combining Patterns + +You can freely mix JS patterns, mini patterns and values! For example, this pattern: + + + +...is equivalent to: + + + +... as well as: + +")`} /> + +While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @. +When using JS patterns, there is a lot more you can do. diff --git a/website/src/pages/functions/value-modifiers.mdx b/website/src/pages/functions/value-modifiers.mdx new file mode 100644 index 00000000..308dfd77 --- /dev/null +++ b/website/src/pages/functions/value-modifiers.mdx @@ -0,0 +1,154 @@ +--- +title: Control Parameters +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Control Parameters + +Besides functions that control time, we saw earlier that functions like `note` and `cutoff` control different parameters (short params) of an event. +Let's now look more closely at how these `param(eter) functions` work. + +# Parameter Functions + +A very powerful feature of tidal patterns is that each parameter can be controlled independently: + +") +.gain(.8) +.s('sawtooth') +.log()`} +/> + +In this example, the parameters `note`, `cutoff`, `gain` and `s` are controlled independently by either patterns or plain values (numbers / text). +After pressing play, we can observe the time and parameter values of each event (hap) in the output created by `.log()`. + +## Plain vs Parameterized Values + +Patterns that are not wrapped inside a param function will contain unlabeled `plain values`: + +".log()`} /> + +This will not generate any sound output, because Strudel could only guess which param is meant by these letters. + +Now compare that to the version wrapped in `note`: + +").log()`} /> + +Now it is clear that these letters are meant to be played as notes. +Under the hood, the `note` function (as well as all other param functions) +will wrap each plain value in an object. If the note function did not exist, we would need to write: + + + +This will have the same output, though it is rather unwieldy to read and write. + +## Wrapping Parameter Functions + +To avoid too much nesting, param functions can also be chained like this: + + + +This is equivalent to `note(cat('c','e','g')).log()`. + +You can use this with any function that declares a type (like `n`, `s`, `note`, `freq` etc), just make sure to leave the parens empty! + +## Plain Value Modification + +Patterns of plain values can be modified with any of the following operators: + +").log()`} /> + +Here, the add function modifies the numbers on the left. +Again, there is no output because these numbers have no meaning without a param. + +## Param Value Modification + +To modify a parameter value, you can either: + +- Use the operator on the plain value pattern, inside the param function: + + ")).room(.1).log()`} /> + +- Similarly, use the operator on the plain value pattern and wrap it later: + + ").note().room(.1).log()`} /> + +- Specify which param should be modified inside the operator function: + + ")).log()`} /> + +- Modify _all_ numeral params: + + ").log()`} /> + +Which of these 3 ways to use strongly depends on the context! +Note that the order of chaining param functions also matters! +In the last example, the `room` value would not have changed if it was applied later: + +").room(.1).log()`} /> + +This shows how the execution of the chained functions goes from left to right. +In this case, the `.add` will only modify what's on the left side. + +# Operators + +This group of functions allows to modify the value of events. + +## add + + + +## sub + + + +## mul + + + +## div + + + +## round + + + +## floor + + + +## ceil + + + +## range + + + +# Custom Parameters + +You can also create your own parameters: + + + +Multiple params can also be created in a more consice way, using `createParams`: + + + +Note that these params will not do anything until you give them meaning in your custom output! diff --git a/website/src/pages/learn/conditional-modifiers.mdx b/website/src/pages/learn/conditional-modifiers.mdx new file mode 100644 index 00000000..6e4966df --- /dev/null +++ b/website/src/pages/learn/conditional-modifiers.mdx @@ -0,0 +1,43 @@ +--- +title: Conditional Modifiers +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Conditional Modifiers + +## every + + + +## when + + + +# Accumulation Modifiers + +## stack + + + +## superimpose + + + +## layer + + + +## off + + + +## echo + + + +## echoWith + + diff --git a/website/src/pages/learn/factories.mdx b/website/src/pages/learn/factories.mdx new file mode 100644 index 00000000..d98629d2 --- /dev/null +++ b/website/src/pages/learn/factories.mdx @@ -0,0 +1,63 @@ +--- +title: Pattern Constructors +description: Strudel Tutorial +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Pattern Constructors + +The following functions will return a pattern. +These are the equivalents used by the Mini Notation: + +| function | mini | +| ------------------------------ | ---------------- | +| `cat(x, y)` | `""` | +| `seq(x, y)` | `"x y"` | +| `stack(x, y)` | `"x,y"` | +| `timeCat([3,x],[2,y])` | `"x@2 y@2"` | +| `polymeter([a, b, c], [x, y])` | `"{a b c, x y}"` | +| `polymeterSteps(2, x, y, z)` | `"{x y z}%2"` | +| `silence` | `"~"` | + +## cat + + + +You can also use cat as a chained function like this: + + + +## seq + + + +Or as a chained function: + + + +## stack + + + +As a chained function: + + + +## timeCat + + + +## polymeter + + + +## polymeterSteps + + + +## silence + + diff --git a/website/src/pages/learn/functions.mdx b/website/src/pages/learn/functions.mdx deleted file mode 100644 index 0cc83dc2..00000000 --- a/website/src/pages/learn/functions.mdx +++ /dev/null @@ -1,250 +0,0 @@ ---- -title: What is Strudel? -description: Strudel Tutorial -layout: ../../layouts/MainLayout.astro ---- - -import { MiniRepl } from '../../docs/MiniRepl'; -import { JsDoc } from '../../docs/JsDoc'; - -# Functional JavaScript API - -While the mini notation is powerful on its own, there is much more to discover. -Internally, the mini notation will expand to use the actual functional JavaScript API. - -For example, this Pattern in Mini Notation: - - - -is equivalent to this Pattern without Mini Notation: - - - -Similarly, there is an equivalent function for every aspect of the mini notation. - -Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API -to fit better for the larger context, while mini notation is more practical for individiual rhythms. - -## Limits of Mini Notation - -While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example: - - - -Here, we are using mini notation for the individual rhythms, while using the function `stack` to mix them. -While stack is also available as `,` in mini notation, we cannot use it here, because we have different types of sounds. - -## Notes - -Notes are automatically available as variables: - - - -An important difference to the mini notation: -For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name. - -The above is the same as: - - - -Using strings, you can also use "#". - -## Alternative Syntax - -In the above example, we are nesting a function inside a function, which makes reading the parens a little more difficult. -To avoid getting to many nested parens, there is an alternative syntax to add a type to a pattern: - - - -You can use this with any function that declares a type (like `n`, `s`, `note`, `freq` etc), just make sure to leave the parens empty! - -## Pattern Factories - -The following functions will return a pattern. - -### cat - - - -### seq - - - -### stack - - - -### timeCat - - - -## Combining Patterns - -You can freely mix JS patterns, mini patterns and values! For example, this pattern: - - - -...is equivalent to: - - - -... as well as: - -")`} /> - -While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @. -When using JS patterns, there is a lot more you can do. - -## Time Modifiers - -The following functions modify a pattern temporal structure in some way. - -### Pattern.slow - - - -### Pattern.fast - - - -### Pattern.early - - - -### Pattern.late - - - -### Pattern.legato - - - -### Pattern.struct - - - -### Pattern.euclid - - - -### Pattern.euclidLegato - - - -### Pattern.rev - - - -### Pattern.iter - - - -### Pattern.iterBack - - - -## Conditional Modifiers - -### Pattern.every - - - -### Pattern.when - - - -## Accumulation Modifiers - -### Pattern.stack - - - -### Pattern.superimpose - - - -### Pattern.layer - - - -### Pattern.off - - - -### Pattern.echo - - - -### Pattern.echoWith - - - -## Concat Modifiers - -### Pattern.seq - - - -### Pattern.cat - - - -## Value Modifiers - -### Pattern.add - - - -### Pattern.sub - - - -### Pattern.mul - - - -### Pattern.div - - - -### Pattern.round - - - -### Pattern.apply - - - -### Pattern.range - - - -### Pattern.chunk - - - -### Pattern.chunkBack - - diff --git a/website/src/pages/learn/notes.mdx b/website/src/pages/learn/notes.mdx index 6bfd3b3d..9fe8472f 100644 --- a/website/src/pages/learn/notes.mdx +++ b/website/src/pages/learn/notes.mdx @@ -10,16 +10,16 @@ import { JsDoc } from '../../docs/JsDoc'; # Notes Pitches are an essential building block for music. -In Strudel, there are three different ways to express a pitch, `note`, `n` and `freq`. +In Strudel, pitches can be expressed as note names, note numbers or frequencies. Here's the same pattern written in three different ways: - `note`: letter notation, good for those who are familiar with western music theory: -- `n`: number notation, good for those who want to use recognisable pitches, but don't care about music theory: +- `note`: number notation, good for those who want to use recognisable pitches, but don't care about music theory: - + - `freq`: frequency notation, good for those who want to go beyond standardised tuning systems: @@ -27,28 +27,28 @@ Here's the same pattern written in three different ways: Let's look at `note`, `n` and `freq` in more detail... -# `note` +## `note` names -Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`. +Notes names can be notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`. By the way, you can edit the contents of the player, and press "update" to hear your change! You can also press "play" on the next player without needing to stop the last one. -# `n` +## `note` numbers -If you prefer, you can also use numbers with `n` instead: +If you prefer, you can also use numbers with `note` instead: - + These numbers are interpreted as so called [MIDI numbers](https://www.inspiredacoustics.com/en/MIDI_note_numbers_and_center_frequencies), where adjacent whole numbers are one 'semitone' apart. You could also write decimal numbers to get 'microtonal' pitches (in between the black and white piano notes): - + -# `freq` +## `freq` To get maximum freedom, you can also use `freq` to directly control the frequency: @@ -76,13 +76,13 @@ The less distance we can hear between the frequencies! Why is this? [Human hearing operates logarithmically](https://www.audiocheck.net/soundtests_nonlinear.php). -# From notes to sounds +## From notes to sounds In this page, when we played a pattern of notes like this: -We heard a simple synthesised sound, in fact we heard a [square wave oscillator](https://en.wikipedia.org/wiki/Square_wave). +We heard a simple synthesised sound, in fact we heard a [triangle wave oscillator](https://en.wikipedia.org/wiki/Triangle_wave). This is the default synthesiser used by Strudel, but how do we then make different sounds in Strudel? diff --git a/website/src/pages/learn/signals.mdx b/website/src/pages/learn/signals.mdx index ee5614d1..0cf0f58e 100644 --- a/website/src/pages/learn/signals.mdx +++ b/website/src/pages/learn/signals.mdx @@ -56,54 +56,54 @@ These methods add random behavior to your Patterns. -## Pattern.degradeBy +## degradeBy -## Pattern.degrade +## degrade -## Pattern.undegradeBy +## undegradeBy -## Pattern.sometimesBy +## sometimesBy -## Pattern.sometimes +## sometimes -## Pattern.someCyclesBy +## someCyclesBy -## Pattern.someCycles +## someCycles -## Pattern.often +## often -## Pattern.rarely +## rarely -## Pattern.almostNever +## almostNever -## Pattern.almostAlways +## almostAlways -## Pattern.never +## never -## Pattern.always +## always diff --git a/website/src/pages/learn/time-modifiers.mdx b/website/src/pages/learn/time-modifiers.mdx new file mode 100644 index 00000000..c24d5961 --- /dev/null +++ b/website/src/pages/learn/time-modifiers.mdx @@ -0,0 +1,75 @@ +--- +title: Time Modifiers +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +# Time Modifiers + +The following functions modify a pattern temporal structure in some way. +Some of these have equivalent operators in the Mini Notation: + +| function | mini | +| ---------------------- | ------------ | +| `"x".slow(2)` | `"x/2"` | +| `"x".fast(2)` | `"x*2"` | +| `"x".euclid(3,8)` | `"x(3,8)"` | +| `"x".euclidRot(3,8,1)` | `"x(3,8,1)"` | + +## slow + + + +## fast + + + +## early + + + +## late + + + +## legato + + + +## struct + + + +## euclid + + + +### euclidRot + + + +### euclidLegato + + + +## rev + + + +## iter + + + +### iterBack + + + +## chunk + + + +### chunkBack + + diff --git a/website/src/pages/technical-manual/docs.mdx b/website/src/pages/technical-manual/docs.mdx index 229b9a63..b0417984 100644 --- a/website/src/pages/technical-manual/docs.mdx +++ b/website/src/pages/technical-manual/docs.mdx @@ -47,6 +47,7 @@ Usage: - `name`: function name, as named with `@name` in jsdoc - `h`: level of heading. `0` will hide the heading. Hiding it allows using a manual heading which results in a nav link being generated in the right sidebar. +- `hideDescription`: if set, the description will be hidden ### Writing jsdoc diff --git a/website/tailwind.config.cjs b/website/tailwind.config.cjs index 52707838..7d263336 100644 --- a/website/tailwind.config.cjs +++ b/website/tailwind.config.cjs @@ -1,4 +1,7 @@ /** @type {import('tailwindcss').Config} */ + +const defaultTheme = require('tailwindcss/defaultTheme'); + module.exports = { content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], theme: { @@ -17,6 +20,20 @@ module.exports = { // header: 'transparent', footer: '#00000050', }, + typography(theme) { + return { + DEFAULT: { + css: { + 'code::before': { + content: 'none', // don’t wrap code in backticks + }, + 'code::after': { + content: 'none', + }, + }, + }, + }; + }, }, }, plugins: [require('@tailwindcss/typography')],