From b46330fc7a4f7fa88b3e5bf20aef19edbaf21663 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 24 Dec 2022 00:12:54 +0100 Subject: [PATCH 01/15] proper setup minirepl --- website/src/docs/MiniRepl.jsx | 12 +++++++----- website/src/repl/prebake.mjs | 11 ++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/website/src/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 961cb4c3..12d0771b 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -1,12 +1,9 @@ import { evalScope, controls } from '@strudel.cycles/core'; -import { samples } from '@strudel.cycles/webaudio'; +import { initAudioOnFirstClick } from '@strudel.cycles/webaudio'; import { useEffect, useState } from 'react'; +import { prebake } from '../repl/prebake'; if (typeof window !== 'undefined') { - fetch('https://strudel.tidalcycles.org/EmuSP12.json') - .then((res) => res.json()) - .then((json) => samples(json, 'https://strudel.tidalcycles.org/EmuSP12/')); - evalScope( controls, import('@strudel.cycles/core'), @@ -20,6 +17,11 @@ if (typeof window !== 'undefined') { ); } +if (typeof window !== 'undefined') { + initAudioOnFirstClick(); + prebake(); +} + export function MiniRepl({ tune }) { const [Repl, setRepl] = useState(); useEffect(() => { diff --git a/website/src/repl/prebake.mjs b/website/src/repl/prebake.mjs index d8fc199c..bdd7f8d3 100644 --- a/website/src/repl/prebake.mjs +++ b/website/src/repl/prebake.mjs @@ -1,17 +1,18 @@ import { Pattern, toMidi, valueToMidi } from '@strudel.cycles/core'; import { samples } from '@strudel.cycles/webaudio'; -export async function prebake({ baseDir = '.' } = {}) { +export async function prebake({ baseDir = '' } = {}) { // https://archive.org/details/SalamanderGrandPianoV3 // License: CC-by http://creativecommons.org/licenses/by/3.0/ Author: Alexander Holm return await Promise.all([ - samples('piano.json', `${baseDir}/piano/`), + samples('/piano.json', `${baseDir}/piano/`), // https://github.com/sgossner/VCSL/ // https://api.github.com/repositories/126427031/contents/ // LICENSE: CC0 general-purpose - samples('vcsl.json', 'github:sgossner/VCSL/master/'), - samples('tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'), - samples('EmuSP12.json', `${baseDir}/EmuSP12/`), + samples('/vcsl.json', 'github:sgossner/VCSL/master/'), + samples('/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/'), + samples('/EmuSP12.json', `${baseDir}/EmuSP12/`), + // samples('github:tidalcycles/Dirt-Samples/master'), ]); } From 3feed9003969666ed3985fd9779221a4daf88346 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Sat, 24 Dec 2022 00:14:26 +0100 Subject: [PATCH 02/15] translate parts of tidal "How-tos" --- website/src/pages/recipes/arpeggios.mdx | 69 +++++++++++++++++++ website/src/pages/recipes/rhythms.mdx | 88 +++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 website/src/pages/recipes/arpeggios.mdx create mode 100644 website/src/pages/recipes/rhythms.mdx diff --git a/website/src/pages/recipes/arpeggios.mdx b/website/src/pages/recipes/arpeggios.mdx new file mode 100644 index 00000000..06659ff3 --- /dev/null +++ b/website/src/pages/recipes/arpeggios.mdx @@ -0,0 +1,69 @@ +--- +title: Build Arpeggios +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; + +Note: This has been (partly) translated from https://tidalcycles.org/docs/patternlib/howtos/buildarpeggios + +# Build Arpeggios + +This page will teach you how to get started writing arpeggios using different techniques. It is a good way to learn Strudel in a more intuitive way. + +## Arpeggios from notes + +Start with a simple sequence of notes: + + + +Now, let's play one per cycle: + +").piano().slow(2)`} client:idle /> + +On top of that, put a copy of the sequence, offset in time and pitch: + +".off(1/8, add(7)) + .note().piano().slow(2)`} + client:idle +/> + +Add some structure to the original sequence: + +" + .off(1/8, add(7)) + .note().piano().slow(2)`} + client:idle +/> + +Reverse in one speaker: + +" + .off(1/8, add(7)) + .note().piano() + .jux(rev).slow(2)`} + client:idle +/> + +Let's add another layer: + +" + .off(1/8, add(7)) + .off(1/8, add(12)) + .note().piano() + .jux(rev).slow(2)`} + client:idle +/> + +- added slow(2) to approximate tidals cps +- n was replaced with note, because using n does not work as note for samples +- legato 2 was removed because it does not work in combination with rev (bug) + +## Arpeggios from chords + +TODO \ No newline at end of file diff --git a/website/src/pages/recipes/rhythms.mdx b/website/src/pages/recipes/rhythms.mdx new file mode 100644 index 00000000..08aef82a --- /dev/null +++ b/website/src/pages/recipes/rhythms.mdx @@ -0,0 +1,88 @@ +--- +title: Build Rhythms +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; +import { samples } from '@strudel.cycles/webaudio'; + +Note: + +- this has been (partly) translated from https://tidalcycles.org/docs/patternlib/howtos/buildrhythms +- this only sounds good with `samples('github:tidalcycles/Dirt-Samples/master')` in prebake + +# Build Rhythms + +This page will teach you how to get started writing rhythms using different techniques. It is a good way to learn Strudel in a more intuitive way. + +## From a simple to a complex rhythm + +Simple bass drum - snare: + + + +Let's pick a different snare sample: + + + +Now, we are going to change the rhythm: + + + +And add some toms: + + + +Start to transform, shift a quarter cycle every other cycle: + + + +Pattern the shift amount: + +")).slow(2)`} +/> + +Add some patterned effects: + +")) +.shape("<0 .5 .3>") +.room(saw.range(0,.2).slow(4)) +.slow(2)`} +/> + +More transformation: + +")) +.shape("<0 .5 .3>") +.room(saw.range(0,.2).slow(4)) +.jux(id, rev, x=>x.speed(2)) +.slow(2)`} +/> + +## Another rhythmic construction + +Let's start with a sequence: + + + +We add a bit of flavour: + + [2 0] [2 3]").s("feel").speed(1.5).slow(2)`} /> + +Swap the samples round every other cycle: + +TODO: implement `rot` From aaf3ae5f9ccd664443d8604ae2c7a2bccaf8c43a Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 11:25:27 +0100 Subject: [PATCH 03/15] microrhythm experiment --- website/src/pages/recipes/microrhythms.mdx | 78 ++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 website/src/pages/recipes/microrhythms.mdx diff --git a/website/src/pages/recipes/microrhythms.mdx b/website/src/pages/recipes/microrhythms.mdx new file mode 100644 index 00000000..daff4219 --- /dev/null +++ b/website/src/pages/recipes/microrhythms.mdx @@ -0,0 +1,78 @@ +--- +title: Microrhythms +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; +import { samples } from '@strudel.cycles/webaudio'; + +see https://strudel.tidalcycles.org?zMEo5kowGrFc + +# Microrhythms + +Inspired by this [Mini-Lecture on Microrhythm Notation](https://www.youtube.com/watch?v=or7B6vI3jOo), let's look at how we can express microrhythms with Strudel. + +The timestamps of the first rhythm are `0 1/5 1/2 2/3 1`. We could naively express this with a stack: + + + +While this works, it has two problems: + +- it is not very compact +- the durations are wrong, e.g. the first note takes up the whole cycle + +In the video, the duration of a timestamp is calculated by subtracting it from the next timestamp: + +- 1/5 - 0 = 1/5 = 6/30 +- 1/2 - 1/5 = 3/10 = 9/30 +- 2/3 - 1/2 = 1/6 = 5/30 +- 1 - 2/3 = 1/3 = 10/30 + +Using those, we can now express the rhythm much shorter: + + + +The problems of the first notation are now fixed: it is much shorter and the durations are correct. +Still, this notation involved calculating the durations by hand, which could be automated: + + { + const next = i < a.length-1 ? a[i+1] : 1; + return next - a[i] + }) + return this.struct(timeCat(...durations.map(d => [d, 1]))).late(timestamps[0]) +} +s('hh').micro(0, 1/5, 1/2, 2/3)`} +/> + +This notation is even shorter and it allows directly filling in the timestamps! + +This is the second example of the video: + + + { + const next = i < a.length-1 ? a[i+1] : 1; + return next - a[i] + }) + return this.struct(timeCat(...durations.map(d => [d, 1]))).late(timestamps[0]) +} +s('hh').micro(0, 1/6, 2/5, 2/3, 3/4)`} +/> + + +with bass: https://strudel.tidalcycles.org?sTglgJJCPIeY \ No newline at end of file From 0792d0d59d21b036d72f17f0b31a36eb31f52c0e Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 20:55:21 +0100 Subject: [PATCH 04/15] add scheduler.now to get phase starting from 0 --- packages/core/cyclist.mjs | 3 +++ packages/core/zyklus.mjs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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/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; From 2d1b62a97810955a3f0613b97ee61d6fc28b4f17 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 20:58:57 +0100 Subject: [PATCH 05/15] begin reimplementing draw logic for parallel use --- packages/core/pianoroll.mjs | 128 +++++ packages/react/dist/index.cjs.js | 2 +- packages/react/dist/index.es.js | 541 +++++++++++-------- packages/react/src/components/MiniRepl.jsx | 42 +- packages/react/src/hooks/useFrame.mjs | 43 ++ packages/react/src/hooks/usePatternFrame.mjs | 34 ++ packages/react/src/hooks/useStrudel.mjs | 4 + website/src/docs/MiniRepl.jsx | 4 +- website/src/pages/learn/notes.mdx | 2 +- 9 files changed, 550 insertions(+), 250 deletions(-) create mode 100644 packages/react/src/hooks/useFrame.mjs create mode 100644 packages/react/src/hooks/usePatternFrame.mjs diff --git a/packages/core/pianoroll.mjs b/packages/core/pianoroll.mjs index 378480ba..16a2dc89 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -153,3 +153,131 @@ 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; + ctx.fillStyle = event.context?.color || inactive; + ctx.strokeStyle = event.context?.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/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js index 771d8be0..1d12bd87 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"),b=require("@codemirror/view"),x=require("@codemirror/state"),ee=require("@codemirror/lang-javascript"),c=require("@lezer/highlight"),te=require("@uiw/codemirror-themes"),K=require("@strudel.cycles/core"),I=require("@strudel.cycles/webaudio"),re=require("react-hook-inview"),ae=require("@strudel.cycles/transpiler"),U=e=>e&&typeof e=="object"&&"default"in e?e:{default:e},u=U(t),oe=U(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 z=x.StateEffect.define(),se=x.StateField.define({create(){return b.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(z))if(a.value){const s=b.Decoration.mark({attributes:{style:"background-color: #FFCA2880"}});e=b.Decoration.set([s.range(0,r.newDoc.length)])}else e=b.Decoration.set([]);return e}catch(a){return console.warn("flash error",a),e}},provide:e=>b.EditorView.decorations.from(e)}),W=e=>{e.dispatch({effects:z.of(!0)}),setTimeout(()=>{e.dispatch({effects:z.of(!1)})},200)},A=x.StateEffect.define(),ce=x.StateField.define({create(){return b.Decoration.none},update(e,r){try{for(let a of r.effects)if(a.is(A)){const s=a.value.map(n=>(n.context.locations||[]).map(({start:i,end:l})=>{const f=n.context.color||"#FFCA28";let o=r.newDoc.line(i.line).from+i.column,d=r.newDoc.line(l.line).from+l.column;const m=r.newDoc.length;return o>m||d>m?void 0:b.Decoration.mark({attributes:{style:`outline: 1.5px solid ${f};`}}).range(o,d)})).flat().filter(Boolean)||[];e=b.Decoration.set(s,!0)}return e}catch{return b.Decoration.set([])}},provide:e=>b.EditorView.decorations.from(e)}),ie=[ee.javascript(),ne,ce,se];function $({value:e,onChange:r,onViewChanged:a,onSelectionChange:s,options:n,editorDidMount:i}){const l=t.useCallback(d=>{r?.(d)},[r]),f=t.useCallback(d=>{a?.(d)},[a]),o=t.useCallback(d=>{d.selectionSet&&s&&s?.(d.state.selection)},[s]);return u.default.createElement(u.default.Fragment,null,u.default.createElement(oe.default,{value:e,onChange:l,onCreateEditor:f,onUpdate:o,extensions:ie}))}function V(...e){return e.filter(Boolean).join(" ")}function J({view:e,pattern:r,active:a,getTime:s}){const n=t.useRef([]),i=t.useRef();t.useEffect(()=>{if(e)if(r&&a){let l=requestAnimationFrame(function f(){try{const o=s(),m=[Math.max(i.current||o,o-1/10,0),o+1/60];i.current=m[1],n.current=n.current.filter(w=>w.whole.end>o);const h=r.queryArc(...m).filter(w=>w.hasOnset());n.current=n.current.concat(h),e.dispatch({effects:A.of(n.current)})}catch{e.dispatch({effects:A.of([])})}l=requestAnimationFrame(f)});return()=>{cancelAnimationFrame(l)}}else n.current=[],e.dispatch({effects:A.of([])})},[r,a,e])}function le(e,r=!1){const a=t.useRef(),s=t.useRef(),n=f=>{if(s.current!==void 0){const o=f-s.current;e(f,o)}s.current=f,a.current=requestAnimationFrame(n)},i=()=>{a.current=requestAnimationFrame(n)},l=()=>{a.current&&cancelAnimationFrame(a.current),delete a.current};return t.useEffect(()=>{a.current&&(l(),i())},[e]),t.useEffect(()=>(r&&i(),l),[]),{start:i,stop:l}}function ue({pattern:e,started:r,getTime:a,onDraw:s}){let n=t.useRef([]),i=t.useRef(null);const{start:l,stop:f}=le(t.useCallback(()=>{const o=a();if(i.current===null){i.current=o;return}const d=e.queryArc(Math.max(i.current,o-1/10),o),m=4;i.current=o,n.current=(n.current||[]).filter(h=>h.whole.end>o-m).concat(d.filter(h=>h.hasOnset())),s(o,n.current)},[e]));t.useEffect(()=>{r?l():(n.current=[],f())},[r])}function G(e){return t.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),t.useCallback(r=>window.postMessage(r,"*"),[])}function Q({defaultOutput:e,interval:r,getTime:a,evalOnMount:s=!1,initialCode:n="",autolink:i=!1,beforeEval:l,afterEval:f,onEvalError:o,onToggle:d,canvasId:m}){const h=t.useMemo(()=>de(),[]);m=m||`canvas-${h}`;const[w,_]=t.useState(),[k,M]=t.useState(),[E,D]=t.useState(n),[F,R]=t.useState(),[T,P]=t.useState(),[C,q]=t.useState(!1),H=E!==F,{scheduler:g,evaluate:v,start:L,stop:j,pause:X}=t.useMemo(()=>K.repl({interval:r,defaultOutput:e,onSchedulerError:_,onEvalError:p=>{M(p),o?.(p)},getTime:a,transpiler:ae.transpiler,beforeEval:({code:p})=>{D(p),l?.()},afterEval:({pattern:p,code:N})=>{R(N),P(p),M(),_(),i&&(window.location.hash="#"+encodeURIComponent(btoa(N))),f?.()},onToggle:p=>{q(p),d?.(p)}}),[e,r,a]),Y=G(({data:{from:p,type:N}})=>{N==="start"&&p!==h&&j()}),S=t.useCallback(async(p=!0)=>{await v(E,p),Y({type:"start",from:h})},[v,E]),O=t.useRef();return t.useEffect(()=>{!O.current&&s&&E&&(O.current=!0,S())},[S,s,E]),t.useEffect(()=>()=>{g.stop()},[g]),{id:h,canvasId:m,code:E,setCode:D,error:w||k,schedulerError:w,scheduler:g,evalError:k,evaluate:v,activateCode:S,activeCode:F,isDirty:H,pattern:T,started:C,start:L,stop:j,pause:X,togglePlay:async()=>{C?g.pause():await S()}}}function de(){return Math.floor((1+Math.random())*65536).toString(16).substring(1)}function B({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 fe="_container_3i85k_1",ge="_header_3i85k_5",me="_buttons_3i85k_9",he="_button_3i85k_9",pe="_buttonDisabled_3i85k_17",ve="_error_3i85k_21",be="_body_3i85k_25",y={container:fe,header:ge,buttons:me,button:he,buttonDisabled:pe,error:ve,body:be},we=()=>I.getAudioContext().currentTime;function Ee({tune:e,hideOutsideView:r=!1,init:a,enableKeyboard:s,withCanvas:n=!1,canvasHeight:i=200}){const{code:l,setCode:f,evaluate:o,activateCode:d,error:m,isDirty:h,activeCode:w,pattern:_,started:k,scheduler:M,togglePlay:E,stop:D,canvasId:F}=Q({initialCode:e,defaultOutput:I.webaudioOutput,getTime:we});ue({pattern:_,started:k,getTime:()=>M.now(),onDraw:(g,v)=>{const L=document.querySelector("#"+F).getContext("2d");K.pianoroll({ctx:L,time:g,haps:v,autorange:1,fold:1,playhead:1})}});const[R,T]=t.useState(),[P,C]=re.useInView({threshold:.01}),q=t.useRef(),H=t.useMemo(()=>((C||!r)&&(q.current=!0),C||q.current),[C,r]);return J({view:R,pattern:_,active:k&&!w?.includes("strudel disable-highlighting"),getTime:()=>M.getPhase()}),t.useLayoutEffect(()=>{if(s){const g=async v=>{(v.ctrlKey||v.altKey)&&(v.code==="Enter"?(v.preventDefault(),W(R),await d()):v.code==="Period"&&(D(),v.preventDefault()))};return window.addEventListener("keydown",g,!0),()=>window.removeEventListener("keydown",g,!0)}},[s,_,l,o,D,R]),u.default.createElement("div",{className:y.container,ref:P},u.default.createElement("div",{className:y.header},u.default.createElement("div",{className:y.buttons},u.default.createElement("button",{className:V(y.button,k?"sc-animate-pulse":""),onClick:()=>E()},u.default.createElement(B,{type:k?"pause":"play"})),u.default.createElement("button",{className:V(h?y.button:y.buttonDisabled),onClick:()=>d()},u.default.createElement(B,{type:"refresh"}))),m&&u.default.createElement("div",{className:y.error},m.message)),u.default.createElement("div",{className:y.body},H&&u.default.createElement($,{value:l,onChange:f,onViewChanged:T})),n&&u.default.createElement("canvas",{id:F,className:"w-full pointer-events-none",height:i,ref:g=>{g&&g.width!==g.clientWidth&&(g.width=g.clientWidth)}}))}const ye=e=>t.useLayoutEffect(()=>(window.addEventListener("keydown",e,!0),()=>window.removeEventListener("keydown",e,!0)),[e]);exports.CodeMirror=$;exports.MiniRepl=Ee;exports.cx=V;exports.flash=W;exports.useHighlighting=J;exports.useKeydown=ye;exports.usePostMessage=G;exports.useStrudel=Q; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js index 5a00ac91..eb313a3f 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 { 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 l, { useCallback as M, useRef as E, useEffect as C, useMemo as B, useState as _, useLayoutEffect as W } from "react"; +import Y from "@uiw/react-codemirror"; +import { Decoration as y, EditorView as $ } from "@codemirror/view"; +import { StateEffect as G, StateField as J } from "@codemirror/state"; +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,364 @@ 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 O = G.define(), ie = J.define({ create() { - return E.none; + return y.none; }, update(e, t) { 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 r of t.effects) + if (r.is(O)) + if (r.value) { + const a = y.mark({ attributes: { style: "background-color: #FFCA2880" } }); + e = y.set([a.range(0, t.newDoc.length)]); } else - e = E.set([]); + e = y.set([]); return e; - } catch (o) { - return console.warn("flash error", o), e; + } catch (r) { + return console.warn("flash error", r), e; } }, - provide: (e) => U.decorations.from(e) -}), ce = (e) => { - e.dispatch({ effects: B.of(!0) }), setTimeout(() => { - e.dispatch({ effects: B.of(!1) }); + provide: (e) => $.decorations.from(e) +}), le = (e) => { + e.dispatch({ effects: O.of(!0) }), setTimeout(() => { + e.dispatch({ effects: O.of(!1) }); }, 200); -}, z = $.define(), ie = G.define({ +}, H = G.define(), ue = J.define({ create() { - return E.none; + return y.none; }, update(e, t) { 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; + for (let r of t.effects) + if (r.is(H)) { + const a = r.value.map( + (n) => (n.context.locations || []).map(({ start: c, end: i }) => { + const d = n.context.color || "#FFCA28"; + let o = t.newDoc.line(c.line).from + c.column, u = t.newDoc.line(i.line).from + i.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); + return o > m || u > m ? 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) => $.decorations.from(e) +}), de = [Z(), ce, ue, ie]; +function fe({ value: e, onChange: t, onViewChanged: r, onSelectionChange: a, options: n, editorDidMount: c }) { + const i = M( + (u) => { + t?.(u); }, [t] - ), f = _( - (i) => { - o?.(i); + ), d = M( + (u) => { + r?.(u); }, - [o] - ), c = _( - (i) => { - i.selectionSet && a && a?.(i.state.selection); + [r] + ), o = M( + (u) => { + u.selectionSet && a && a?.(u.state.selection); }, [a] ); - return /* @__PURE__ */ n.createElement(n.Fragment, null, /* @__PURE__ */ n.createElement(X, { + return /* @__PURE__ */ l.createElement(l.Fragment, null, /* @__PURE__ */ l.createElement(Y, { value: e, - onChange: d, - onCreateEditor: f, - onUpdate: c, - extensions: le + onChange: i, + onCreateEditor: d, + onUpdate: o, + extensions: de })); } -function K(...e) { +function I(...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: t, active: r, getTime: a }) { + const n = E([]), c = E(); + C(() => { if (e) - if (t && o) { - let d = requestAnimationFrame(function f() { + if (t && r) { + let i = 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(), m = [Math.max(c.current || o, o - 1 / 10, 0), o + 1 / 60]; + c.current = m[1], n.current = n.current.filter((v) => v.whole.end > o); + const g = t.queryArc(...m).filter((v) => v.hasOnset()); + n.current = n.current.concat(g), e.dispatch({ effects: H.of(n.current) }); } catch { - e.dispatch({ effects: z.of([]) }); + e.dispatch({ effects: H.of([]) }); } - d = requestAnimationFrame(f); + i = requestAnimationFrame(d); }); return () => { - cancelAnimationFrame(d); + cancelAnimationFrame(i); }; } else - s.current = [], e.dispatch({ effects: z.of([]) }); - }, [t, o, e]); + n.current = [], e.dispatch({ effects: H.of([]) }); + }, [t, r, 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, t = !1) { + const r = E(), a = E(), n = (d) => { + if (a.current !== void 0) { + const o = d - a.current; + e(d, o); + } + a.current = d, r.current = requestAnimationFrame(n); + }, c = () => { + r.current = requestAnimationFrame(n); + }, i = () => { + r.current && cancelAnimationFrame(r.current), delete r.current; + }; + return C(() => { + r.current && (i(), c()); + }, [e]), C(() => (t && c(), i), []), { + start: c, + stop: i + }; +} +function pe({ pattern: e, started: t, getTime: r, onDraw: a }) { + let n = E([]), c = E(null); + const { start: i, stop: d } = ge( + M(() => { + const o = r(); + if (c.current === null) { + c.current = o; + return; + } + const u = e.queryArc(Math.max(c.current, o - 1 / 10), o), m = 4; + c.current = o, n.current = (n.current || []).filter((g) => g.whole.end > o - m).concat(u.filter((g) => g.hasOnset())), a(o, n.current); + }, [e]) + ); + C(() => { + t ? i() : (n.current = [], d()); + }, [t]); +} +function he(e) { + return C(() => (window.addEventListener("message", e), () => window.removeEventListener("message", e)), [e]), M((t) => window.postMessage(t, "*"), []); +} +function ve({ + defaultOutput: e, + interval: t, + getTime: r, + evalOnMount: a = !1, + initialCode: n = "", + autolink: c = !1, + beforeEval: i, + afterEval: d, + onEvalError: o, + onToggle: u, + canvasId: m +}) { + const g = B(() => be(), []); + m = m || `canvas-${g}`; + const [v, F] = _(), [k, A] = _(), [b, D] = _(n), [x, R] = _(), [S, z] = _(), [N, T] = _(!1), L = b !== x, { scheduler: f, evaluate: h, start: V, stop: K, pause: Q } = B( + () => te({ + interval: t, + defaultOutput: e, + onSchedulerError: F, + onEvalError: (p) => { + A(p), o?.(p); + }, + getTime: r, + transpiler: se, + beforeEval: ({ code: p }) => { + D(p), i?.(); + }, + afterEval: ({ pattern: p, code: q }) => { + R(q), z(p), A(), F(), c && (window.location.hash = "#" + encodeURIComponent(btoa(q))), d?.(); + }, + onToggle: (p) => { + T(p), u?.(p); + } + }), + [e, t, r] + ), X = he(({ data: { from: p, type: q } }) => { + q === "start" && p !== g && K(); + }), P = M( + async (p = !0) => { + await h(b, p), X({ type: "start", from: g }); + }, + [h, b] + ), j = E(); + return C(() => { + !j.current && a && b && (j.current = !0, P()); + }, [P, a, b]), C(() => () => { + f.stop(); + }, [f]), { + id: g, + canvasId: m, + code: b, + setCode: D, + error: v || k, + schedulerError: v, + scheduler: f, + evalError: k, + evaluate: h, + activateCode: P, + activeCode: x, + isDirty: L, + pattern: S, + started: N, + start: V, + stop: K, + pause: Q, + togglePlay: async () => { + N ? f.pause() : await P(); + } + }; +} +function be() { + return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); +} +function U({ 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__ */ n.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__ */ n.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__ */ n.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]); } -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 we = "_container_3i85k_1", ye = "_header_3i85k_5", Ee = "_buttons_3i85k_9", ke = "_button_3i85k_9", _e = "_buttonDisabled_3i85k_17", Ce = "_error_3i85k_21", Fe = "_body_3i85k_25", w = { + container: we, + header: ye, + buttons: Ee, + button: ke, + buttonDisabled: _e, + error: Ce, + body: Fe +}, Ne = () => ne().currentTime; +function Be({ + tune: e, + hideOutsideView: t = !1, + init: r, + enableKeyboard: a, + withCanvas: n = !1, + canvasHeight: c = 200 }) { - 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 { - code: s, - setCode: u, - evaluate: d, - activateCode: f, - error: c, - isDirty: i, - activeCode: m, - pattern: h, - started: g, - scheduler: C, - togglePlay: N, - stop: p - } = we({ + code: i, + setCode: d, + evaluate: o, + activateCode: u, + error: m, + isDirty: g, + activeCode: v, + pattern: F, + started: k, + scheduler: A, + togglePlay: b, + stop: D, + canvasId: x + } = 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: F, + started: k, + getTime: () => A.now(), + onDraw: (f, h) => { + const V = document.querySelector("#" + x).getContext("2d"); + re({ ctx: V, time: f, haps: h, 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 [R, S] = _(), [z, N] = ae({ + threshold: 0.01 + }), T = E(), L = B(() => ((N || !t) && (T.current = !0), N || T.current), [N, t]); + return me({ + view: R, + pattern: F, + active: k && !v?.includes("strudel disable-highlighting"), + getTime: () => A.getPhase() + }), W(() => { + if (a) { + const f = async (h) => { + (h.ctrlKey || h.altKey) && (h.code === "Enter" ? (h.preventDefault(), le(R), await u()) : h.code === "Period" && (D(), h.preventDefault())); + }; + return window.addEventListener("keydown", f, !0), () => window.removeEventListener("keydown", f, !0); + } + }, [a, F, i, o, D, R]), /* @__PURE__ */ l.createElement("div", { + className: w.container, + ref: z + }, /* @__PURE__ */ l.createElement("div", { + className: w.header + }, /* @__PURE__ */ l.createElement("div", { + className: w.buttons + }, /* @__PURE__ */ l.createElement("button", { + className: I(w.button, k ? "sc-animate-pulse" : ""), + onClick: () => b() + }, /* @__PURE__ */ l.createElement(U, { + type: k ? "pause" : "play" + })), /* @__PURE__ */ l.createElement("button", { + className: I(g ? w.button : w.buttonDisabled), + onClick: () => u() + }, /* @__PURE__ */ l.createElement(U, { 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 - }))); + }))), m && /* @__PURE__ */ l.createElement("div", { + className: w.error + }, m.message)), /* @__PURE__ */ l.createElement("div", { + className: w.body + }, L && /* @__PURE__ */ l.createElement(fe, { + value: i, + onChange: d, + onViewChanged: S + })), n && /* @__PURE__ */ l.createElement("canvas", { + id: x, + className: "w-full pointer-events-none", + height: c, + ref: (f) => { + f && f.width !== f.clientWidth && (f.width = f.clientWidth); + } + })); } -const Te = (e) => j(() => (window.addEventListener("keydown", e, !0), () => window.removeEventListener("keydown", e, !0)), [e]); +const Oe = (e) => W(() => (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, + I 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..15c75e5c 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, + 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/docs/MiniRepl.jsx b/website/src/docs/MiniRepl.jsx index 12d0771b..9ea06151 100644 --- a/website/src/docs/MiniRepl.jsx +++ b/website/src/docs/MiniRepl.jsx @@ -22,7 +22,7 @@ if (typeof window !== 'undefined') { prebake(); } -export function MiniRepl({ tune }) { +export function MiniRepl({ tune, withCanvas }) { const [Repl, setRepl] = useState(); useEffect(() => { // we have to load this package on the client @@ -31,5 +31,5 @@ export function MiniRepl({ tune }) { setRepl(() => res.MiniRepl); }); }, []); - return Repl ? :
{tune}
; + return Repl ? :
{tune}
; } diff --git a/website/src/pages/learn/notes.mdx b/website/src/pages/learn/notes.mdx index 6bfd3b3d..f4410e90 100644 --- a/website/src/pages/learn/notes.mdx +++ b/website/src/pages/learn/notes.mdx @@ -15,7 +15,7 @@ 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: From b8f0a1dd82046a0e0367af7e5459e57027f53ff7 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 21:04:15 +0100 Subject: [PATCH 06/15] fix format --- website/src/pages/recipes/arpeggios.mdx | 2 +- website/src/pages/recipes/microrhythms.mdx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/website/src/pages/recipes/arpeggios.mdx b/website/src/pages/recipes/arpeggios.mdx index 06659ff3..48a8da04 100644 --- a/website/src/pages/recipes/arpeggios.mdx +++ b/website/src/pages/recipes/arpeggios.mdx @@ -66,4 +66,4 @@ Let's add another layer: ## Arpeggios from chords -TODO \ No newline at end of file +TODO diff --git a/website/src/pages/recipes/microrhythms.mdx b/website/src/pages/recipes/microrhythms.mdx index daff4219..7b2b2425 100644 --- a/website/src/pages/recipes/microrhythms.mdx +++ b/website/src/pages/recipes/microrhythms.mdx @@ -61,7 +61,6 @@ This notation is even shorter and it allows directly filling in the timestamps! This is the second example of the video: - - -with bass: https://strudel.tidalcycles.org?sTglgJJCPIeY \ No newline at end of file +with bass: https://strudel.tidalcycles.org?sTglgJJCPIeY From 36f837730a64fffd80b639d7296aaceaffd2c813 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 22:13:41 +0100 Subject: [PATCH 07/15] better color support --- packages/core/color.mjs | 175 +++++++++++++++++++++++++++++ packages/core/pianoroll.mjs | 9 +- website/src/docs/Colors.jsx | 20 ++++ website/src/pages/learn/colors.mdx | 12 ++ 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 packages/core/color.mjs create mode 100644 website/src/docs/Colors.jsx create mode 100644 website/src/pages/learn/colors.mdx 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/pianoroll.mjs b/packages/core/pianoroll.mjs index 16a2dc89..8e0a3877 100644 --- a/packages/core/pianoroll.mjs +++ b/packages/core/pianoroll.mjs @@ -5,6 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import { Pattern, toMidi, getDrawContext, freqToMidi } from './index.mjs'; +import { convertColorToNumber } from './color.mjs'; const scale = (normalized, min, max) => normalized * (max - min) + min; const getValue = (e) => { @@ -20,6 +21,9 @@ const getValue = (e) => { if (typeof value === 'string') { value = toMidi(value); } + if (typeof value === 'object' && value.color) { + return convertColorToNumber(value.color); + } return value; }; @@ -236,8 +240,9 @@ export function pianoroll({ // .filter(inFrame) .forEach((event) => { const isActive = event.whole.begin <= time && event.whole.end > time; - ctx.fillStyle = event.context?.color || inactive; - ctx.strokeStyle = event.context?.color || active; + 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); diff --git a/website/src/docs/Colors.jsx b/website/src/docs/Colors.jsx new file mode 100644 index 00000000..59f7bca0 --- /dev/null +++ b/website/src/docs/Colors.jsx @@ -0,0 +1,20 @@ +import { colorMap } from '@strudel.cycles/core/color.mjs'; +import React from 'react'; + +const Colors = () => { + return ( +
+ {Object.entries(colorMap).map(([name, hex]) => ( +
+
+
{name}
+
+
{name}
+
+
+ ))} +
+ ); +}; + +export default Colors; diff --git a/website/src/pages/learn/colors.mdx b/website/src/pages/learn/colors.mdx new file mode 100644 index 00000000..47a8001b --- /dev/null +++ b/website/src/pages/learn/colors.mdx @@ -0,0 +1,12 @@ +--- +title: Colors +layout: ../../layouts/MainLayout.astro +--- + +import { MiniRepl } from '../../docs/MiniRepl'; +import { JsDoc } from '../../docs/JsDoc'; +import Colors from '../../docs/Colors.jsx'; + +# Colors + + From c1d2bf9b9fcb10a987f5008abb09a7e43229b727 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 22:16:46 +0100 Subject: [PATCH 08/15] fix anchor links --- website/src/styles/index.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/src/styles/index.css b/website/src/styles/index.css index 86fdb74e..d439928d 100644 --- a/website/src/styles/index.css +++ b/website/src/styles/index.css @@ -8,6 +8,6 @@ body { display: none !important; } -.prose > h1 { - padding-top: 30px; +.prose > h1:not(:first-child) { + margin-top: 30px; } From d7ea37c917707494567104a22b29b444784aacdd Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 26 Dec 2022 22:43:02 +0100 Subject: [PATCH 09/15] add technical manual to docs --- .../components/LeftSidebar/LeftSidebar.astro | 2 +- website/src/config.ts | 5 + website/src/pages/technical-manual/about.mdx | 3 + .../src/pages/technical-manual/alignment.mdx | 47 +++++ .../src/pages/technical-manual/packages.mdx | 10 + .../src/pages/technical-manual/patterns.mdx | 39 ++++ website/src/pages/technical-manual/repl.mdx | 190 ++++++++++++++++++ 7 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 website/src/pages/technical-manual/about.mdx create mode 100644 website/src/pages/technical-manual/alignment.mdx create mode 100644 website/src/pages/technical-manual/packages.mdx create mode 100644 website/src/pages/technical-manual/patterns.mdx create mode 100644 website/src/pages/technical-manual/repl.mdx 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]) => (
  • -