diff --git a/packages/core/color.mjs b/packages/core/color.mjs new file mode 100644 index 00000000..faab900f --- /dev/null +++ b/packages/core/color.mjs @@ -0,0 +1,175 @@ +export const colorMap = { + aliceblue: '#f0f8ff', + antiquewhite: '#faebd7', + aqua: '#00ffff', + aquamarine: '#7fffd4', + azure: '#f0ffff', + beige: '#f5f5dc', + bisque: '#ffe4c4', + black: '#000000', + blanchedalmond: '#ffebcd', + blue: '#0000ff', + blueviolet: '#8a2be2', + brown: '#a52a2a', + burlywood: '#deb887', + cadetblue: '#5f9ea0', + chartreuse: '#7fff00', + chocolate: '#d2691e', + coral: '#ff7f50', + cornflowerblue: '#6495ed', + cornsilk: '#fff8dc', + crimson: '#dc143c', + cyan: '#00ffff', + darkblue: '#00008b', + darkcyan: '#008b8b', + darkgoldenrod: '#b8860b', + darkgray: '#a9a9a9', + darkgreen: '#006400', + darkgrey: '#a9a9a9', + darkkhaki: '#bdb76b', + darkmagenta: '#8b008b', + darkolivegreen: '#556b2f', + darkorange: '#ff8c00', + darkorchid: '#9932cc', + darkred: '#8b0000', + darksalmon: '#e9967a', + darkseagreen: '#8fbc8f', + darkslateblue: '#483d8b', + darkslategray: '#2f4f4f', + darkslategrey: '#2f4f4f', + darkturquoise: '#00ced1', + darkviolet: '#9400d3', + deeppink: '#ff1493', + deepskyblue: '#00bfff', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1e90ff', + firebrick: '#b22222', + floralwhite: '#fffaf0', + forestgreen: '#228b22', + fuchsia: '#ff00ff', + gainsboro: '#dcdcdc', + ghostwhite: '#f8f8ff', + gold: '#ffd700', + goldenrod: '#daa520', + gray: '#808080', + green: '#008000', + greenyellow: '#adff2f', + grey: '#808080', + honeydew: '#f0fff0', + hotpink: '#ff69b4', + indianred: '#cd5c5c', + indigo: '#4b0082', + ivory: '#fffff0', + khaki: '#f0e68c', + lavender: '#e6e6fa', + lavenderblush: '#fff0f5', + lawngreen: '#7cfc00', + lemonchiffon: '#fffacd', + lightblue: '#add8e6', + lightcoral: '#f08080', + lightcyan: '#e0ffff', + lightgoldenrodyellow: '#fafad2', + lightgray: '#d3d3d3', + lightgreen: '#90ee90', + lightgrey: '#d3d3d3', + lightpink: '#ffb6c1', + lightsalmon: '#ffa07a', + lightseagreen: '#20b2aa', + lightskyblue: '#87cefa', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#b0c4de', + lightyellow: '#ffffe0', + lime: '#00ff00', + limegreen: '#32cd32', + linen: '#faf0e6', + magenta: '#ff00ff', + maroon: '#800000', + mediumaquamarine: '#66cdaa', + mediumblue: '#0000cd', + mediumorchid: '#ba55d3', + mediumpurple: '#9370db', + mediumseagreen: '#3cb371', + mediumslateblue: '#7b68ee', + mediumspringgreen: '#00fa9a', + mediumturquoise: '#48d1cc', + mediumvioletred: '#c71585', + midnightblue: '#191970', + mintcream: '#f5fffa', + mistyrose: '#ffe4e1', + moccasin: '#ffe4b5', + navajowhite: '#ffdead', + navy: '#000080', + oldlace: '#fdf5e6', + olive: '#808000', + olivedrab: '#6b8e23', + orange: '#ffa500', + orangered: '#ff4500', + orchid: '#da70d6', + palegoldenrod: '#eee8aa', + palegreen: '#98fb98', + paleturquoise: '#afeeee', + palevioletred: '#db7093', + papayawhip: '#ffefd5', + peachpuff: '#ffdab9', + peru: '#cd853f', + pink: '#ffc0cb', + plum: '#dda0dd', + powderblue: '#b0e0e6', + purple: '#800080', + red: '#ff0000', + rosybrown: '#bc8f8f', + royalblue: '#4169e1', + saddlebrown: '#8b4513', + salmon: '#fa8072', + sandybrown: '#f4a460', + seagreen: '#2e8b57', + seashell: '#fff5ee', + sienna: '#a0522d', + silver: '#c0c0c0', + skyblue: '#87ceeb', + slateblue: '#6a5acd', + slategray: '#708090', + slategrey: '#708090', + snow: '#fffafa', + springgreen: '#00ff7f', + steelblue: '#4682b4', + tan: '#d2b48c', + teal: '#008080', + thistle: '#d8bfd8', + tomato: '#ff6347', + turquoise: '#40e0d0', + violet: '#ee82ee', + wheat: '#f5deb3', + white: '#ffffff', + whitesmoke: '#f5f5f5', + yellow: '#ffff00', + yellowgreen: '#9acd32', +}; + +export function convertColorToNumber(color) { + // Convert color to lowercase for easier matching + color = color.toLowerCase(); + + // If the color is a hex code, convert it to a number + if (color[0] === '#') { + return convertHexToNumber(color); + } + + // If the color is a named color, return the corresponding number + if (colorMap[color] !== undefined) { + return convertHexToNumber(colorMap[color]); + } + + // If the color is not recognized, return null + return -1; +} + +export function convertHexToNumber(hex) { + // Remove the leading '#' from the hex code + hex = hex.slice(1); + + // Convert the hex code to a number + return parseInt(hex, 16); +} diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 562a91b0..59755a44 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -49,6 +49,9 @@ export class Cyclist { getPhase() { return this.getTime() - this.origin - this.latency; } + now() { + return this.getTime() - this.origin + this.clock.minLatency; + } setStarted(v) { this.started = v; this.onToggle?.(v); diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 378480ba..38c915b7 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -153,3 +153,132 @@ Pattern.prototype.pianoroll = function ({ ); return this; }; + +// this function allows drawing a pianoroll without ties to Pattern.prototype +// it will probably replace the above in the future +export function pianoroll({ + time, + haps, + cycles = 4, + playhead = 0.5, + flipTime = 0, + flipValues = 0, + hideNegative = false, + // inactive = '#C9E597', + // inactive = '#FFCA28', + inactive = '#7491D2', + active = '#FFCA28', + // background = '#2A3236', + background = 'transparent', + smear = 0, + playheadColor = 'white', + minMidi = 10, + maxMidi = 90, + autorange = 0, + timeframe: timeframeProp, + fold = 0, + vertical = 0, + ctx, +} = {}) { + const w = ctx.canvas.width; + const h = ctx.canvas.height; + let from = -cycles * playhead; + let to = cycles * (1 - playhead); + + if (timeframeProp) { + console.warn('timeframe is deprecated! use from/to instead'); + from = 0; + to = timeframeProp; + } + if (!autorange && fold) { + console.warn('disabling autorange has no effect when fold is enabled'); + } + const timeAxis = vertical ? h : w; + const valueAxis = vertical ? w : h; + let timeRange = vertical ? [timeAxis, 0] : [0, timeAxis]; // pixel range for time + const timeExtent = to - from; // number of seconds that fit inside the canvas frame + const valueRange = vertical ? [0, valueAxis] : [valueAxis, 0]; // pixel range for values + let valueExtent = maxMidi - minMidi + 1; // number of "slots" for values, overwritten if autorange true + let barThickness = valueAxis / valueExtent; // pixels per value, overwritten if autorange true + let foldValues = []; + flipTime && timeRange.reverse(); + flipValues && valueRange.reverse(); + + // onQuery + const { min, max, values } = haps.reduce( + ({ min, max, values }, e) => { + const v = getValue(e); + return { + min: v < min ? v : min, + max: v > max ? v : max, + values: values.includes(v) ? values : [...values, v], + }; + }, + { min: Infinity, max: -Infinity, values: [] }, + ); + if (autorange) { + minMidi = min; + maxMidi = max; + valueExtent = maxMidi - minMidi + 1; + } + foldValues = values.sort((a, b) => a - b); + barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent; + + ctx.fillStyle = background; + ctx.globalAlpha = 1; // reset! + if (!smear) { + ctx.clearRect(0, 0, w, h); + ctx.fillRect(0, 0, w, h); + } + /* const inFrame = (event) => + (!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */ + haps + // .filter(inFrame) + .forEach((event) => { + const isActive = event.whole.begin <= time && event.whole.end > time; + const color = event.value?.color || event.context?.color; + ctx.fillStyle = color || inactive; + ctx.strokeStyle = color || active; + ctx.globalAlpha = event.context.velocity ?? 1; + const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange); + let durationPx = scale(event.duration / timeExtent, 0, timeAxis); + const value = getValue(event); + const valuePx = scale( + fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent, + ...valueRange, + ); + let margin = 0; + const offset = scale(time / timeExtent, ...timeRange); + let coords; + if (vertical) { + coords = [ + valuePx + 1 - (flipValues ? barThickness : 0), // x + timeAxis - offset + timePx + margin + 1 - (flipTime ? 0 : durationPx), // y + barThickness - 2, // width + durationPx - 2, // height + ]; + } else { + coords = [ + timePx - offset + margin + 1 - (flipTime ? durationPx : 0), // x + valuePx + 1 - (flipValues ? 0 : barThickness), // y + durationPx - 2, // widith + barThickness - 2, // height + ]; + } + isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords); + }); + ctx.globalAlpha = 1; // reset! + const playheadPosition = scale(-from / timeExtent, ...timeRange); + // draw playhead + ctx.strokeStyle = playheadColor; + ctx.beginPath(); + if (vertical) { + ctx.moveTo(0, playheadPosition); + ctx.lineTo(valueAxis, playheadPosition); + } else { + ctx.moveTo(playheadPosition, 0); + ctx.lineTo(playheadPosition, valueAxis); + } + ctx.stroke(); + return this; +} diff --git a/packages/core/zyklus.mjs b/packages/core/zyklus.mjs index 3d45b741..e66d8e2e 100644 --- a/packages/core/zyklus.mjs +++ b/packages/core/zyklus.mjs @@ -44,6 +44,6 @@ function createClock( }; const getPhase = () => phase; // setCallback - return { setDuration, start, stop, pause, duration, getPhase }; + return { setDuration, start, stop, pause, duration, getPhase, minLatency }; } export default createClock; diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index 771d8be0..bdcd41d6 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"),X=require("@uiw/react-codemirror"),p=require("@codemirror/view"),A=require("@codemirror/state"),Y=require("@codemirror/lang-javascript"),o=require("@lezer/highlight"),Z=require("@uiw/codemirror-themes"),ee=require("react-hook-inview"),B=require("@strudel.cycles/webaudio"),te=require("@strudel.cycles/core"),re=require("@strudel.cycles/transpiler"),I=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},n=I(t),oe=I(X),ae=Z.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:o.tags.keyword,color:"#c792ea"},{tag:o.tags.operator,color:"#89ddff"},{tag:o.tags.special(o.tags.variableName),color:"#eeffff"},{tag:o.tags.typeName,color:"#c3e88d"},{tag:o.tags.atom,color:"#f78c6c"},{tag:o.tags.number,color:"#c3e88d"},{tag:o.tags.definition(o.tags.variableName),color:"#82aaff"},{tag:o.tags.string,color:"#c3e88d"},{tag:o.tags.special(o.tags.string),color:"#c3e88d"},{tag:o.tags.comment,color:"#7d8799"},{tag:o.tags.variableName,color:"#c792ea"},{tag:o.tags.tagName,color:"#c3e88d"},{tag:o.tags.bracket,color:"#525154"},{tag:o.tags.meta,color:"#ffcb6b"},{tag:o.tags.attributeName,color:"#c792ea"},{tag:o.tags.propertyName,color:"#c792ea"},{tag:o.tags.className,color:"#decb6b"},{tag:o.tags.invalid,color:"#ffffff"}]});const L=A.StateEffect.define(),ne=A.StateField.define({create(){return p.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(L))if(a.value){const s=p.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=p.Decoration.set([s.range(0,r.newDoc.length)])}else e=p.Decoration.set([]);return e}catch(a){return console.warn("flash error",a),e}},provide:e=>p.EditorView.decorations.from(e)}),K=e=>{e.dispatch({effects:L.of(!0)}),setTimeout(()=>{e.dispatch({effects:L.of(!1)})},200)},x=A.StateEffect.define(),se=A.StateField.define({create(){return p.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(x)){const s=a.value.map(c=>(c.context.locations||[]).map(({start:f,end:d})=>{const g=c.context.color||"#FFCA28";let i=r.newDoc.line(f.line).from+f.column,l=r.newDoc.line(d.line).from+d.column;const m=r.newDoc.length;return i>m||l>m?void 0:p.Decoration.mark({attributes:{style:`outline: 1.5px solid ${g};`}}).range(i,l)})).flat().filter(Boolean)||[];e=p.Decoration.set(s,!0)}return e}catch{return p.Decoration.set([])}},provide:e=>p.EditorView.decorations.from(e)}),ce=[Y.javascript(),ae,se,ne];function O({value:e,onChange:r,onViewChanged:a,onSelectionChange:s,options:c,editorDidMount:f}){const d=t.useCallback(l=>{r?.(l)},[r]),g=t.useCallback(l=>{a?.(l)},[a]),i=t.useCallback(l=>{l.selectionSet&&s&&s?.(l.state.selection)},[s]);return n.default.createElement(n.default.Fragment,null,n.default.createElement(oe.default,{value:e,onChange:d,onCreateEditor:g,onUpdate:i,extensions:ce}))}function T(...e){return e.filter(Boolean).join(" ")}function U({view:e,pattern:r,active:a,getTime:s}){const c=t.useRef([]),f=t.useRef();t.useEffect(()=>{if(e)if(r&&a){let d=requestAnimationFrame(function g(){try{const i=s(),m=[Math.max(f.current||i,i-1/10,0),i+1/60];f.current=m[1],c.current=c.current.filter(h=>h.whole.end>i);const v=r.queryArc(...m).filter(h=>h.hasOnset());c.current=c.current.concat(v),e.dispatch({effects:x.of(c.current)})}catch{e.dispatch({effects:x.of([])})}d=requestAnimationFrame(g)});return()=>{cancelAnimationFrame(d)}}else c.current=[],e.dispatch({effects:x.of([])})},[r,a,e])}const ie="_container_3i85k_1",le="_header_3i85k_5",ue="_buttons_3i85k_9",de="_button_3i85k_9",fe="_buttonDisabled_3i85k_17",ge="_error_3i85k_21",me="_body_3i85k_25",E={container:ie,header:le,buttons:ue,button:de,buttonDisabled:fe,error:ge,body:me};function j({type:e}){return n.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:n.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:n.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:n.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])}function J(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(r=>window.postMessage(r,"*"),[])}function $({defaultOutput:e,interval:r,getTime:a,evalOnMount:s=!1,initialCode:c="",autolink:f=!1,beforeEval:d,afterEval:g,onEvalError:i,onToggle:l}){const m=t.useMemo(()=>he(),[]),[v,h]=t.useState(),[_,C]=t.useState(),[b,y]=t.useState(c),[M,P]=t.useState(),[k,D]=t.useState(),[S,N]=t.useState(!1),w=b!==M,{scheduler:R,evaluate:H,start:G,stop:z,pause:Q}=t.useMemo(()=>te.repl({interval:r,defaultOutput:e,onSchedulerError:h,onEvalError:u=>{C(u),i?.(u)},getTime:a,transpiler:re.transpiler,beforeEval:({code:u})=>{y(u),d?.()},afterEval:({pattern:u,code:F})=>{P(F),D(u),C(),h(),f&&(window.location.hash="#"+encodeURIComponent(btoa(F))),g?.()},onToggle:u=>{N(u),l?.(u)}}),[e,r,a]),W=J(({data:{from:u,type:F}})=>{F==="start"&&u!==m&&z()}),q=t.useCallback(async(u=!0)=>{await H(b,u),W({type:"start",from:m})},[H,b]),V=t.useRef();return t.useEffect(()=>{!V.current&&s&&b&&(V.current=!0,q())},[q,s,b]),t.useEffect(()=>()=>{R.stop()},[R]),{code:b,setCode:y,error:v||_,schedulerError:v,scheduler:R,evalError:_,evaluate:H,activateCode:q,activeCode:M,isDirty:w,pattern:k,started:S,start:G,stop:z,pause:Q,togglePlay:async()=>{S?R.pause():await q()}}}function he(){return Math.floor((1+Math.random())*65536).toString(16).substring(1)}const pe=()=>B.getAudioContext().currentTime;function be({tune:e,hideOutsideView:r=!1,init:a,enableKeyboard:s}){const{code:c,setCode:f,evaluate:d,activateCode:g,error:i,isDirty:l,activeCode:m,pattern:v,started:h,scheduler:_,togglePlay:C,stop:b}=$({initialCode:e,defaultOutput:B.webaudioOutput,getTime:pe}),[y,M]=t.useState(),[P,k]=ee.useInView({threshold:.01}),D=t.useRef(),S=t.useMemo(()=>((k||!r)&&(D.current=!0),k||D.current),[k,r]);return U({view:y,pattern:v,active:h&&!m?.includes("strudel disable-highlighting"),getTime:()=>_.getPhase()}),t.useLayoutEffect(()=>{if(s){const N=async w=>{(w.ctrlKey||w.altKey)&&(w.code==="Enter"?(w.preventDefault(),K(y),await g()):w.code==="Period"&&(b(),w.preventDefault()))};return window.addEventListener("keydown",N,!0),()=>window.removeEventListener("keydown",N,!0)}},[s,v,c,d,b,y]),n.default.createElement("div",{className:E.container,ref:P},n.default.createElement("div",{className:E.header},n.default.createElement("div",{className:E.buttons},n.default.createElement("button",{className:T(E.button,h?"sc-animate-pulse":""),onClick:()=>C()},n.default.createElement(j,{type:h?"pause":"play"})),n.default.createElement("button",{className:T(l?E.button:E.buttonDisabled),onClick:()=>g()},n.default.createElement(j,{type:"refresh"}))),i&&n.default.createElement("div",{className:E.error},i.message)),n.default.createElement("div",{className:E.body},S&&n.default.createElement(O,{value:c,onChange:f,onViewChanged:M})))}const ve=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=O;exports.MiniRepl=be;exports.cx=T;exports.flash=K;exports.useHighlighting=U;exports.useKeydown=ve;exports.usePostMessage=J;exports.useStrudel=$; +"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; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index 5a00ac91..6545e0a7 100644 --- a/packages/react/dist/index.es.js +++ b/packages/react/dist/index.es.js @@ -1,15 +1,15 @@ -import n, { useCallback as _, useRef as H, useEffect as L, useMemo as V, useState as w, useLayoutEffect as j } from "react"; -import X from "@uiw/react-codemirror"; -import { Decoration as E, EditorView as U } from "@codemirror/view"; +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 Y } from "@codemirror/lang-javascript"; -import { tags as r } from "@lezer/highlight"; -import { createTheme as Z } from "@uiw/codemirror-themes"; -import { useInView as ee } from "react-hook-inview"; -import { webaudioOutput as te, getAudioContext as re } from "@strudel.cycles/webaudio"; -import { repl as oe } from "@strudel.cycles/core"; -import { transpiler as ne } from "@strudel.cycles/transpiler"; -const ae = Z({ +import { javascript as Z } 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({ theme: "dark", settings: { background: "#222", @@ -22,299 +22,357 @@ const ae = Z({ gutterForeground: "#8a919966" }, styles: [ - { tag: r.keyword, color: "#c792ea" }, - { tag: r.operator, color: "#89ddff" }, - { tag: r.special(r.variableName), color: "#eeffff" }, - { tag: r.typeName, color: "#c3e88d" }, - { tag: r.atom, color: "#f78c6c" }, - { tag: r.number, color: "#c3e88d" }, - { tag: r.definition(r.variableName), color: "#82aaff" }, - { tag: r.string, color: "#c3e88d" }, - { tag: r.special(r.string), color: "#c3e88d" }, - { tag: r.comment, color: "#7d8799" }, - { tag: r.variableName, color: "#c792ea" }, - { tag: r.tagName, color: "#c3e88d" }, - { tag: r.bracket, color: "#525154" }, - { tag: r.meta, color: "#ffcb6b" }, - { tag: r.attributeName, color: "#c792ea" }, - { tag: r.propertyName, color: "#c792ea" }, - { tag: r.className, color: "#decb6b" }, - { tag: r.invalid, color: "#ffffff" } + { tag: s.keyword, color: "#c792ea" }, + { tag: s.operator, color: "#89ddff" }, + { tag: s.special(s.variableName), color: "#eeffff" }, + { tag: s.typeName, color: "#c3e88d" }, + { tag: s.atom, color: "#f78c6c" }, + { tag: s.number, color: "#c3e88d" }, + { tag: s.definition(s.variableName), color: "#82aaff" }, + { tag: s.string, color: "#c3e88d" }, + { tag: s.special(s.string), color: "#c3e88d" }, + { tag: s.comment, color: "#7d8799" }, + { tag: s.variableName, color: "#c792ea" }, + { tag: s.tagName, color: "#c3e88d" }, + { tag: s.bracket, color: "#525154" }, + { tag: s.meta, color: "#ffcb6b" }, + { tag: s.attributeName, color: "#c792ea" }, + { tag: s.propertyName, color: "#c792ea" }, + { tag: s.className, color: "#decb6b" }, + { tag: s.invalid, color: "#ffffff" } ] }); -const B = $.define(), se = G.define({ +const B = $.define(), ie = G.define({ create() { - return E.none; + return y.none; }, - update(e, t) { + update(e, r) { try { - for (let o of t.effects) - if (o.is(B)) - if (o.value) { - const a = E.mark({ attributes: { style: "background-color: #FFCA2880" } }); - e = E.set([a.range(0, t.newDoc.length)]); + for (let t of r.effects) + if (t.is(B)) + if (t.value) { + const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } }); + e = y.set([a.range(0, r.newDoc.length)]); } else - e = E.set([]); + e = y.set([]); return e; - } catch (o) { - return console.warn("flash error", o), e; + } catch (t) { + return console.warn("flash error", t), e; } }, - provide: (e) => U.decorations.from(e) -}), ce = (e) => { + provide: (e) => W.decorations.from(e) +}), le = (e) => { e.dispatch({ effects: B.of(!0) }), setTimeout(() => { e.dispatch({ effects: B.of(!1) }); }, 200); -}, z = $.define(), ie = G.define({ +}, H = $.define(), ue = G.define({ create() { - return E.none; + return y.none; }, - update(e, t) { + update(e, r) { try { - for (let o of t.effects) - if (o.is(z)) { - const a = o.value.map( - (s) => (s.context.locations || []).map(({ start: u, end: d }) => { - const f = s.context.color || "#FFCA28"; - let c = t.newDoc.line(u.line).from + u.column, i = t.newDoc.line(d.line).from + d.column; - const m = t.newDoc.length; - return c > m || i > m ? void 0 : E.mark({ attributes: { style: `outline: 1.5px solid ${f};` } }).range(c, i); + for (let t of r.effects) + if (t.is(H)) { + 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); }) ).flat().filter(Boolean) || []; - e = E.set(a, !0); + e = y.set(a, !0); } return e; } catch { - return E.set([]); + return y.set([]); } }, - provide: (e) => U.decorations.from(e) -}), le = [Y(), ae, ie, se]; -function de({ value: e, onChange: t, onViewChanged: o, onSelectionChange: a, options: s, editorDidMount: u }) { - const d = _( - (i) => { - t?.(i); + 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); + }, + [r] + ), d = N( + (u) => { + t?.(u); }, [t] - ), f = _( - (i) => { - o?.(i); - }, - [o] - ), c = _( - (i) => { - i.selectionSet && a && a?.(i.state.selection); + ), o = N( + (u) => { + u.selectionSet && a && a?.(u.state.selection); }, [a] ); - return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, { + return /* @__PURE__ */ i.createElement(i.Fragment, null, /* @__PURE__ */ i.createElement(Y, { value: e, - onChange: d, - onCreateEditor: f, - onUpdate: c, - extensions: le + onChange: l, + onCreateEditor: d, + onUpdate: o, + extensions: de })); } -function K(...e) { +function j(...e) { return e.filter(Boolean).join(" "); } -function ue({ view: e, pattern: t, active: o, getTime: a }) { - const s = H([]), u = H(); - L(() => { +function me({ view: e, pattern: r, active: t, getTime: a }) { + const n = E([]), c = E(); + F(() => { if (e) - if (t && o) { - let d = requestAnimationFrame(function f() { + if (r && t) { + let l = requestAnimationFrame(function d() { try { - const c = a(), m = [Math.max(u.current || c, c - 1 / 10, 0), c + 1 / 60]; - u.current = m[1], s.current = s.current.filter((g) => g.whole.end > c); - const h = t.queryArc(...m).filter((g) => g.hasOnset()); - s.current = s.current.concat(h), e.dispatch({ effects: z.of(s.current) }); + 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) }); } catch { - e.dispatch({ effects: z.of([]) }); + e.dispatch({ effects: H.of([]) }); } - d = requestAnimationFrame(f); + l = requestAnimationFrame(d); }); return () => { - cancelAnimationFrame(d); + cancelAnimationFrame(l); }; } else - s.current = [], e.dispatch({ effects: z.of([]) }); - }, [t, o, e]); + n.current = [], e.dispatch({ effects: H.of([]) }); + }, [r, t, e]); } -const fe = "_container_3i85k_1", me = "_header_3i85k_5", ge = "_buttons_3i85k_9", pe = "_button_3i85k_9", he = "_buttonDisabled_3i85k_17", be = "_error_3i85k_21", ve = "_body_3i85k_25", v = { - container: fe, - header: me, - buttons: ge, - button: pe, - buttonDisabled: he, - error: be, - body: ve -}; -function O({ type: e }) { - return /* @__PURE__ */ n.createElement("svg", { +function ge(e, r = !1) { + const t = E(), a = E(), n = (d) => { + if (a.current !== void 0) { + const o = d - a.current; + e(d, o); + } + a.current = d, t.current = requestAnimationFrame(n); + }, c = () => { + t.current = requestAnimationFrame(n); + }, l = () => { + t.current && cancelAnimationFrame(t.current), delete t.current; + }; + return F(() => { + t.current && (l(), c()); + }, [e]), F(() => (r && c(), l), []), { + start: c, + stop: l + }; +} +function pe({ pattern: e, started: r, getTime: t, onDraw: a }) { + let n = E([]), c = E(null); + const { start: l, stop: d } = ge( + N(() => { + const o = t(); + if (c.current === null) { + c.current = o; + 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); + }, [e]) + ); + F(() => { + r ? l() : (n.current = [], d()); + }, [r]); +} +function he(e) { + return F(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), N((r) => window.postMessage(r, "*"), []); +} +function ve({ + defaultOutput: e, + interval: r, + getTime: t, + evalOnMount: a = !1, + initialCode: n = "", + autolink: c = !1, + beforeEval: l, + afterEval: d, + onEvalError: o, + onToggle: u, + canvasId: f +}) { + 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({ + interval: r, + defaultOutput: e, + onSchedulerError: k, + onEvalError: (p) => { + T(p), o?.(p); + }, + getTime: t, + transpiler: se, + beforeEval: ({ code: p }) => { + A(p), l?.(); + }, + afterEval: ({ pattern: p, code: q }) => { + S(q), D(p), T(), k(), c && (window.location.hash = "#" + encodeURIComponent(btoa(q))), d?.(); + }, + onToggle: (p) => { + L(p), u?.(p); + } + }), + [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 }); + }, + [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, + 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, + togglePlay: async () => { + x ? g.pause() : await P(); + } + }; +} +function be() { + return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); +} +function I({ type: e }) { + return /* @__PURE__ */ i.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__ */ n.createElement("path", { + refresh: /* @__PURE__ */ i.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__ */ n.createElement("path", { + play: /* @__PURE__ */ i.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__ */ n.createElement("path", { + pause: /* @__PURE__ */ i.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]); } -function Ee(e) { - return L(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), _((t) => window.postMessage(t, "*"), []); -} -function we({ - defaultOutput: e, - interval: t, - getTime: o, - evalOnMount: a = !1, - initialCode: s = "", - autolink: u = !1, - beforeEval: d, - afterEval: f, - onEvalError: c, - onToggle: i -}) { - const m = V(() => ye(), []), [h, g] = w(), [C, N] = w(), [p, y] = w(s), [M, S] = w(), [k, D] = w(), [F, x] = w(!1), b = p !== M, { scheduler: A, evaluate: T, start: J, stop: q, pause: Q } = V( - () => oe({ - interval: t, - defaultOutput: e, - onSchedulerError: g, - onEvalError: (l) => { - N(l), c?.(l); - }, - getTime: o, - transpiler: ne, - beforeEval: ({ code: l }) => { - y(l), d?.(); - }, - afterEval: ({ pattern: l, code: P }) => { - S(P), D(l), N(), g(), u && (window.location.hash = "#" + encodeURIComponent(btoa(P))), f?.(); - }, - onToggle: (l) => { - x(l), i?.(l); - } - }), - [e, t, o] - ), W = Ee(({ data: { from: l, type: P } }) => { - P === "start" && l !== m && q(); - }), R = _( - async (l = !0) => { - await T(p, l), W({ type: "start", from: m }); - }, - [T, p] - ), I = H(); - return L(() => { - !I.current && a && p && (I.current = !0, R()); - }, [R, a, p]), L(() => () => { - A.stop(); - }, [A]), { - code: p, - setCode: y, - error: h || C, - schedulerError: h, - scheduler: A, - evalError: C, - evaluate: T, - activateCode: R, - activeCode: M, - isDirty: b, - pattern: k, - started: F, - start: J, - stop: q, - pause: Q, - togglePlay: async () => { - F ? A.pause() : await R(); - } - }; -} -function ye() { - return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); -} -const ke = () => re().currentTime; -function Se({ tune: e, hideOutsideView: t = !1, init: o, enableKeyboard: a }) { +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 { - code: s, - setCode: u, + code: c, + setCode: l, evaluate: d, - activateCode: f, - error: c, - isDirty: i, + activateCode: o, + error: u, + isDirty: f, activeCode: m, - pattern: h, - started: g, - scheduler: C, - togglePlay: N, - stop: p - } = we({ + pattern: v, + started: k, + scheduler: M, + togglePlay: T, + stop: b, + canvasId: A + } = ve({ initialCode: e, - defaultOutput: te, - getTime: ke - }), [y, M] = w(), [S, k] = ee({ - threshold: 0.01 - }), D = H(), F = V(() => ((k || !t) && (D.current = !0), k || D.current), [k, t]); - return ue({ - view: y, - pattern: h, - active: g && !m?.includes("strudel disable-highlighting"), - getTime: () => C.getPhase() - }), j(() => { - if (a) { - const x = async (b) => { - (b.ctrlKey || b.altKey) && (b.code === "Enter" ? (b.preventDefault(), ce(y), await f()) : b.code === "Period" && (p(), b.preventDefault())); - }; - return window.addEventListener("keydown", x, !0), () => window.removeEventListener("keydown", x, !0); + defaultOutput: oe, + getTime: Ne + }); + 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 }); } - }, [a, h, s, d, p, y]), /* @__PURE__ */ n.createElement("div", { - className: v.container, - ref: S - }, /* @__PURE__ */ n.createElement("div", { - className: v.header - }, /* @__PURE__ */ n.createElement("div", { - className: v.buttons - }, /* @__PURE__ */ n.createElement("button", { - className: K(v.button, g ? "sc-animate-pulse" : ""), - onClick: () => N() - }, /* @__PURE__ */ n.createElement(O, { - type: g ? "pause" : "play" - })), /* @__PURE__ */ n.createElement("button", { - className: K(i ? v.button : v.buttonDisabled), - onClick: () => f() - }, /* @__PURE__ */ n.createElement(O, { + }); + const [C, S] = _(), [z, D] = ae({ + 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(() => { + 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())); + }; + return window.addEventListener("keydown", h, !0), () => window.removeEventListener("keydown", h, !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, { type: "refresh" - }))), c && /* @__PURE__ */ n.createElement("div", { - className: v.error - }, c.message)), /* @__PURE__ */ n.createElement("div", { - className: v.body - }, F && /* @__PURE__ */ n.createElement(de, { - value: s, - onChange: u, - onViewChanged: M - }))); + }))), 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, + className: "w-full pointer-events-none", + height: n, + ref: (h) => { + h && h.width !== h.clientWidth && (h.width = h.clientWidth); + } + })); } -const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); +const Oe = (e) => U(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); export { - de as CodeMirror, - Se as MiniRepl, - K as cx, - ce as flash, - ue as useHighlighting, - Te as useKeydown, - Ee as usePostMessage, - we as useStrudel + fe as CodeMirror, + Be as MiniRepl, + j as cx, + le as flash, + me as useHighlighting, + Oe as useKeydown, + he as usePostMessage, + ve as useStrudel }; diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index ab035b6a..a7874e55 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -1,18 +1,20 @@ -import React, { useState, useMemo, useRef, useEffect, useLayoutEffect } from 'react'; +import { pianoroll } from '@strudel.cycles/core'; +import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useInView } from 'react-hook-inview'; +import 'tailwindcss/tailwind.css'; import cx from '../cx'; import useHighlighting from '../hooks/useHighlighting.mjs'; -import CodeMirror6, { flash } from './CodeMirror6'; -import 'tailwindcss/tailwind.css'; -import './style.css'; -import styles from './MiniRepl.module.css'; -import { Icon } from './Icon'; -import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; +import usePatternFrame from '../hooks/usePatternFrame.mjs'; import useStrudel from '../hooks/useStrudel.mjs'; +import CodeMirror6, { flash } from './CodeMirror6'; +import { Icon } from './Icon'; +import styles from './MiniRepl.module.css'; +import './style.css'; const getTime = () => getAudioContext().currentTime; -export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }) { +export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, withCanvas = false, canvasHeight = 200 }) { const { code, setCode, @@ -26,11 +28,23 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard } scheduler, togglePlay, stop, + canvasId, } = useStrudel({ initialCode: tune, defaultOutput: webaudioOutput, getTime, }); + + usePatternFrame({ + pattern, + started: withCanvas && started, + getTime: () => scheduler.now(), + onDraw: (time, haps) => { + const ctx = document.querySelector('#' + canvasId).getContext('2d'); + pianoroll({ ctx, time, haps, autorange: 1, fold: 1, playhead: 1 }); + }, + }); + /* useEffect(() => { init && activateCode(); }, [init, activateCode]); */ @@ -88,6 +102,18 @@ export function MiniRepl({ tune, hideOutsideView = false, init, enableKeyboard }
{show && }
+ {withCanvas && ( + { + if (el && el.width !== el.clientWidth) { + el.width = el.clientWidth; + } + }} + > + )} ); } diff --git a/packages/react/src/hooks/useFrame.mjs b/packages/react/src/hooks/useFrame.mjs new file mode 100644 index 00000000..130609d0 --- /dev/null +++ b/packages/react/src/hooks/useFrame.mjs @@ -0,0 +1,43 @@ +import { useEffect, useRef } from 'react'; + +function useFrame(callback, autostart = false) { + const requestRef = useRef(); + const previousTimeRef = useRef(); + + const animate = (time) => { + if (previousTimeRef.current !== undefined) { + const deltaTime = time - previousTimeRef.current; + callback(time, deltaTime); + } + previousTimeRef.current = time; + requestRef.current = requestAnimationFrame(animate); + }; + + const start = () => { + requestRef.current = requestAnimationFrame(animate); + }; + const stop = () => { + requestRef.current && cancelAnimationFrame(requestRef.current); + delete requestRef.current; + }; + useEffect(() => { + if (requestRef.current) { + stop(); + start(); + } + }, [callback]); + + useEffect(() => { + if (autostart) { + start(); + } + return stop; + }, []); // Make sure the effect only runs once + + return { + start, + stop, + }; +} + +export default useFrame; diff --git a/packages/react/src/hooks/usePatternFrame.mjs b/packages/react/src/hooks/usePatternFrame.mjs new file mode 100644 index 00000000..53981ca5 --- /dev/null +++ b/packages/react/src/hooks/usePatternFrame.mjs @@ -0,0 +1,34 @@ +import { useCallback, useEffect, useRef } from 'react'; +import 'tailwindcss/tailwind.css'; +import useFrame from '../hooks/useFrame.mjs'; + +function usePatternFrame({ pattern, started, getTime, onDraw }) { + let visibleHaps = useRef([]); + let lastFrame = useRef(null); + const { start: startFrame, stop: stopFrame } = useFrame( + useCallback(() => { + const phase = getTime(); + if (lastFrame.current === null) { + lastFrame.current = phase; + return; + } + const haps = pattern.queryArc(Math.max(lastFrame.current, phase - 1 / 10), phase); + const cycles = 4; + lastFrame.current = phase; + visibleHaps.current = (visibleHaps.current || []) + .filter((h) => h.whole.end > phase - cycles) // in frame + .concat(haps.filter((h) => h.hasOnset())); + onDraw(phase, visibleHaps.current); + }, [pattern]), + ); + useEffect(() => { + if (started) { + startFrame(); + } else { + visibleHaps.current = []; + stopFrame(); + } + }, [started]); +} + +export default usePatternFrame; diff --git a/packages/react/src/hooks/useStrudel.mjs b/packages/react/src/hooks/useStrudel.mjs index 82326415..5d475e1e 100644 --- a/packages/react/src/hooks/useStrudel.mjs +++ b/packages/react/src/hooks/useStrudel.mjs @@ -14,8 +14,10 @@ function useStrudel({ afterEval, onEvalError, onToggle, + canvasId, }) { const id = useMemo(() => s4(), []); + canvasId = canvasId || `canvas-${id}`; // scheduler const [schedulerError, setSchedulerError] = useState(); const [evalError, setEvalError] = useState(); @@ -97,6 +99,8 @@ function useStrudel({ }; const error = schedulerError || evalError; return { + id, + canvasId, code, setCode, error, diff --git a/website/src/components/LeftSidebar/LeftSidebar.astro b/website/src/components/LeftSidebar/LeftSidebar.astro index a7804785..741e3f0c 100644 --- a/website/src/components/LeftSidebar/LeftSidebar.astro +++ b/website/src/components/LeftSidebar/LeftSidebar.astro @@ -17,7 +17,7 @@ const sidebar = SIDEBAR[langCode]; { Object.entries(sidebar).map(([header, children]) => (
  • -