mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-14 06:43:47 -04:00
Compare commits
1351 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 051bdaccc7 | |||
| 5dc04d9574 | |||
| 60129413d3 | |||
| d2bf9370ea | |||
| fd11aa5eec | |||
| fb7c2b61d3 | |||
| dd64bf0fc9 | |||
| 434eb3b256 | |||
| 8fef75385a | |||
| c66b9219a8 | |||
| 2a1f8823e8 | |||
| 3da65f90e3 | |||
| a36cc24087 | |||
| 1829d8cadf | |||
| 02efc090ef | |||
| b184348f55 | |||
| 06743cb072 | |||
| 6fa66fb1d0 | |||
| 23c0ab9648 | |||
| 6422047cff | |||
| 46c2c72637 | |||
| c088590244 | |||
| 192ba69682 | |||
| 2cc428e968 | |||
| 15c451a6d9 | |||
| b46e8c07be | |||
| 961e931c31 | |||
| b1d49552ae | |||
| d31d154841 | |||
| d03892bbcf | |||
| 0cf0c1c585 | |||
| 0695b4bee9 | |||
| 0cfec3a7bd | |||
| 6224cd40d4 | |||
| 18ae82174d | |||
| aa1b9d11dc | |||
| 0a82471112 | |||
| 8052e10f0c | |||
| 1146f6ba52 | |||
| 738cea0025 | |||
| 6c28398d20 | |||
| 4fe75143d0 | |||
| 3cd59cdea5 | |||
| 9dca7eb878 | |||
| b45fd5ce3a | |||
| 229b1470f7 | |||
| 0a349e1707 | |||
| d176e9ccf0 | |||
| 710c3acb5d | |||
| a7f5d0e0c1 | |||
| 9928526dda | |||
| 2f8111be0e | |||
| 68113937ff | |||
| c598fa026b | |||
| 491c99c348 | |||
| d49aa8242a | |||
| 9ffbc06cf9 | |||
| af52ac30ba | |||
| 7aea1b5fc8 | |||
| 500ddf3946 | |||
| 3431eaf309 | |||
| 9a13c58583 | |||
| 77a48e8351 | |||
| 6cb156d876 | |||
| d8677c6261 | |||
| 6ef9ca18e9 | |||
| 6667738dc9 | |||
| 4f7415b0ec | |||
| c6f3a8b7c7 | |||
| 4229d673c9 | |||
| 98b7859605 | |||
| a8db707440 | |||
| 873a412179 | |||
| 0778b4809e | |||
| f85fb35c66 | |||
| 5ada0daf40 | |||
| 2ed5f5aa65 | |||
| ae5e6e3ab8 | |||
| 9ba7b9b4d4 | |||
| a45f1fba45 | |||
| e6b47d2cf2 | |||
| b04a262433 | |||
| 96bafa7f0b | |||
| b10612da5c | |||
| 171f4206a8 | |||
| bd83d19197 | |||
| 95cf201c6d | |||
| 4107711dae | |||
| 67f0758cb5 | |||
| 1c99944a32 | |||
| d3ea4959b4 | |||
| 70b27fbff2 | |||
| 96c2144857 | |||
| 0655af4d4f | |||
| 7adfe6f2df | |||
| 3fc0fdbe62 | |||
| fd73571744 | |||
| 80d861c12c | |||
| 45df7bfea3 | |||
| 106772e480 | |||
| 3506de8d1a | |||
| 32456d6966 | |||
| bdc2af9733 | |||
| a7daf41934 | |||
| fe6ff2528a | |||
| 7ceb8708b0 | |||
| 4d22d957a1 | |||
| 79f1a863b6 | |||
| 98db3d37ef | |||
| c02976782b | |||
| ceb4376eb8 | |||
| 0566b7bcb3 | |||
| 6d0ecb9f5e | |||
| 0901948564 | |||
| e6d028fd6b | |||
| 945e7082cd | |||
| 0d6bdfb514 | |||
| 70712bdab6 | |||
| 53771d7f95 | |||
| 6d32d4024d | |||
| f387589342 | |||
| 47a5080352 | |||
| e7e60d3ed8 | |||
| 5314c83534 | |||
| 204c964050 | |||
| 1ede99f71e | |||
| 3c1844046a | |||
| eefa90a2bc | |||
| efa0737660 | |||
| 6232560273 | |||
| e051d383a0 | |||
| 4fb5c1828d | |||
| 6f3b2fa66b | |||
| f17459fdc3 | |||
| a053f40e59 | |||
| 9a0459fdd3 | |||
| 46a859736a | |||
| e4524a2be9 | |||
| f3ee1cc947 | |||
| 86bc28dfb3 | |||
| 1bc0934228 | |||
| f26fd18c7a | |||
| c68dba8c21 | |||
| f768e1eece | |||
| f99557cc05 | |||
| f785823e00 | |||
| 62cd2263c2 | |||
| 7157634db0 | |||
| 78f6fdf169 | |||
| e01c6dc18d | |||
| 51dd408733 | |||
| 1ff7548b52 | |||
| f7fe44063a | |||
| 0355c358b8 | |||
| 4e0a60db00 | |||
| 3604d3940c | |||
| 02b9bd82cf | |||
| 86442adb20 | |||
| 569137eb22 | |||
| 5e3a37c9e8 | |||
| 4651066472 | |||
| 8d325b96ea | |||
| b52cb198b5 | |||
| bcee6632af | |||
| 150a1b8eed | |||
| 4c49b84213 | |||
| b27f177138 | |||
| b2f63897f9 | |||
| 2ee392be9b | |||
| f37652edb3 | |||
| 27049eb706 | |||
| a2974099c1 | |||
| 041a809b07 | |||
| 2ecc6b30aa | |||
| d582bbbb60 | |||
| ca3eda8fc7 | |||
| 1bc86c359e | |||
| 1e2c26123b | |||
| 4d25e3b90b | |||
| 83c820a18c | |||
| 3eface706c | |||
| 8d143f4bf1 | |||
| 88fbd78492 | |||
| 9a235a7c15 | |||
| d99607ea14 | |||
| 5150ec06dc | |||
| b9f190562e | |||
| eb189d077d | |||
| 9756d63cf1 | |||
| a800b32529 | |||
| 8f56ef6507 | |||
| 92e0d09c28 | |||
| 64d80f98db | |||
| 01391d316b | |||
| 1d32da075d | |||
| 9b32900b96 | |||
| ecf837636b | |||
| 5a76bc93b8 | |||
| b14e5577b4 | |||
| 4121ac91ee | |||
| c44f202c44 | |||
| 9059b16c7c | |||
| 3760f51c3c | |||
| aa18293f1d | |||
| 63066937e9 | |||
| 34f0d631d9 | |||
| b3522fea6a | |||
| 2ae3ce53f3 | |||
| 2adacbd27b | |||
| 45eed1b7e7 | |||
| b36d334f78 | |||
| bd648185bc | |||
| 67f1dc32a5 | |||
| d47d9dadf4 | |||
| b52f8c2925 | |||
| 59ce624521 | |||
| 70b9fbfcf6 | |||
| 74b9279416 | |||
| c3ba8ee0fa | |||
| 4f5ca0767d | |||
| d0a1374d2f | |||
| a747c976fe | |||
| 94fcea1040 | |||
| 7db684e308 | |||
| 1981e1c34c | |||
| 5274508665 | |||
| 6f68bdcf5b | |||
| cf20f1059f | |||
| 307c69edb9 | |||
| 9a07b15952 | |||
| a178d310fd | |||
| 12451ba5fc | |||
| 69554c0762 | |||
| 3152bc123a | |||
| 4871abbd71 | |||
| bd2333377f | |||
| 76aaedba66 | |||
| b0f316eecd | |||
| 49903abed7 | |||
| d106875605 | |||
| c13feccdcd | |||
| 6aebf99ebc | |||
| 5d6ccf56d7 | |||
| 0323f75bae | |||
| f31fdb0988 | |||
| 92024f68cf | |||
| f9c1e46c01 | |||
| a5af6d5f4c | |||
| b1d06c4eb7 | |||
| 89bf3b2817 | |||
| ea8aabceed | |||
| 9fd2d3d640 | |||
| 967dea346f | |||
| 57bc0b7032 | |||
| 9974311344 | |||
| b0bdd09032 | |||
| f477273d8a | |||
| 014303b0d5 | |||
| 201fe726b6 | |||
| 2ed3a5c582 | |||
| 975a198ee9 | |||
| 837f8f318d | |||
| f96827d052 | |||
| a9dc0912d0 | |||
| b5039ecce8 | |||
| 624affe092 | |||
| 9b5842b20e | |||
| edfa0c65f9 | |||
| 39e81aa77c | |||
| 8bde87246b | |||
| e2a082f748 | |||
| d7c2cf562f | |||
| f44ceb46cd | |||
| f524dde5ae | |||
| 47aa4cf198 | |||
| 1b55a5e3e0 | |||
| cb57d8f497 | |||
| e12421cee8 | |||
| 53484db768 | |||
| 4c337a5114 | |||
| 63ae95186b | |||
| 8c0065f563 | |||
| 5c41d6789c | |||
| e19f799046 | |||
| 7dac1e275d | |||
| d74d50c40a | |||
| 8b4be4d100 | |||
| de4460a047 | |||
| 0ed615a312 | |||
| e8e8f888dd | |||
| 3776791239 | |||
| 77fee0b866 | |||
| 5671b40707 | |||
| dec039ead3 | |||
| fb81f6f268 | |||
| deb973afa5 | |||
| 2dea3911ba | |||
| e0a7fb8290 | |||
| 30e402b9f1 | |||
| f6d9ad51c6 | |||
| 34ba81a841 | |||
| d7fae2620e | |||
| 9979a08cb0 | |||
| 17dd48f2a4 | |||
| 355ac2ea29 | |||
| 000e1293db | |||
| 48fa04f164 | |||
| ed8f5bf24f | |||
| e17df79133 | |||
| 9519d286bd | |||
| b462f19ef1 | |||
| 1cdb5964c6 | |||
| 592e54a53f | |||
| fe60f34015 | |||
| b5dc65d52a | |||
| 93a4efcf6d | |||
| 40f3212efa | |||
| 53d6ef04b8 | |||
| c29d032027 | |||
| 7fcd9d8a83 | |||
| fc034830d0 | |||
| 48e06bd213 | |||
| 96e0cce2d2 | |||
| 88f677e910 | |||
| 9f50f6ef0e | |||
| 20e8a30583 | |||
| 89c06046b5 | |||
| 9c13f6bb53 | |||
| b5edcde638 | |||
| 93e8186388 | |||
| 8e35079fd1 | |||
| 9663c2ec85 | |||
| 7da7554493 | |||
| 372e93e8f8 | |||
| 9b58cf9890 | |||
| 9fac2c3c33 | |||
| 505ad17447 | |||
| 129327077a | |||
| 034df01e1f | |||
| be1e5a7d0e | |||
| 9872a89a29 | |||
| cf445633c2 | |||
| 2cec7bc1a6 | |||
| 46bfd74e8d | |||
| f6b1e13113 | |||
| f0c3d38ea7 | |||
| d4afbc63e2 | |||
| 466574a856 | |||
| a8d0d39055 | |||
| 1b04243af6 | |||
| 29082808cb | |||
| d23cac506c | |||
| 8959c630ae | |||
| 35a4f9808d | |||
| 49906e98b7 | |||
| 7b6f63db05 | |||
| 37f97e1c17 | |||
| 60e559a910 | |||
| f6f5ee0375 | |||
| 72d1f129cf | |||
| 9474ed7d85 | |||
| 99194814a9 | |||
| 87feac447b | |||
| 0a070a2084 | |||
| 1d3f87a431 | |||
| f38d1c272e | |||
| b0bbd58f66 | |||
| 73fd801907 | |||
| bc59f20af4 | |||
| daf9187572 | |||
| 783c856fd7 | |||
| f939e00a84 | |||
| b31faa9bbc | |||
| 853a31b638 | |||
| faaed61384 | |||
| b50f686a96 | |||
| f45a3933de | |||
| 98a1786c10 | |||
| 1f16ebc5b2 | |||
| 79e8e430f1 | |||
| 6440621d5d | |||
| 59a4cd9cf8 | |||
| 6238be7f2c | |||
| 4051276d74 | |||
| 145510266a | |||
| 4d361a2b3d | |||
| c4fe664215 | |||
| ace71693b7 | |||
| f19ac748e6 | |||
| a7713213f9 | |||
| 845571a716 | |||
| 8b1dfab077 | |||
| 6969800192 | |||
| 37a13d1a00 | |||
| 8c89938eaf | |||
| 5116bf74ad | |||
| a6f969614e | |||
| c1b7476f43 | |||
| 0ca4c1de0f | |||
| ec44103d20 | |||
| 89fe0f11ae | |||
| 63a01088a0 | |||
| c3e48f1b4d | |||
| 31cd626820 | |||
| 8dd0e9600d | |||
| 10d86ff835 | |||
| cec8553fb5 | |||
| b03bf9ec3d | |||
| a750490eea | |||
| 5281c61f99 | |||
| 52628920d9 | |||
| ac2e450e38 | |||
| f8d1e9e004 | |||
| e551dfeddb | |||
| 2f9244e90d | |||
| 056fae518b | |||
| a45d87667f | |||
| 7225b0b18d | |||
| e72c9cbebf | |||
| c787bb26c7 | |||
| 20f3ddd277 | |||
| 5cac182b26 | |||
| 78b2b5ccc3 | |||
| 60cee7fd2f | |||
| 2bd21df762 | |||
| a32ed02745 | |||
| 93c5eeb99e | |||
| ff45baba2c | |||
| cc4d38b4a5 | |||
| bf30587180 | |||
| 1b372a9a7b | |||
| d9f78e1283 | |||
| 2ddd2d1f39 | |||
| 6b31160bbd | |||
| 74aa85b38b | |||
| 8e9b607fa1 | |||
| 5332f99a5a | |||
| e1a8bd333d | |||
| 018a45e2d4 | |||
| 2c43fb2512 | |||
| 3e5664a877 | |||
| ac41426a20 | |||
| 8a0cf14fd7 | |||
| 362f0df117 | |||
| 1190f6e091 | |||
| ebf93c06dd | |||
| fe8debe6e8 | |||
| b8866cf575 | |||
| 2ce8576ab2 | |||
| 1bca05e349 | |||
| 8146725f89 | |||
| 34c44a29c6 | |||
| 7c948dd812 | |||
| 61919a1b3e | |||
| 9be10890e1 | |||
| f51109cb26 | |||
| 9aec18b3a0 | |||
| b374cdf47c | |||
| b16f235e1c | |||
| c26e5bbf0d | |||
| 020c326458 | |||
| dacd1e6146 | |||
| 25b2eb5783 | |||
| 7c3dbac083 | |||
| 54b5d69c8d | |||
| 407510e265 | |||
| f7a283f87d | |||
| 508a6c92b2 | |||
| 9045ce05da | |||
| 456be920d3 | |||
| 71b4e14dd3 | |||
| fb7a9e2083 | |||
| 69e99fa902 | |||
| 5e7e1ff0e8 | |||
| 18e9ceb501 | |||
| 458c97b927 | |||
| 4fd3e6e99c | |||
| c606f86e9d | |||
| e7376c4fc8 | |||
| 886b16d6fd | |||
| 64c2e7c1e8 | |||
| 88e933ee25 | |||
| 1f62d85d8b | |||
| 3fd5fdf1ee | |||
| e419733716 | |||
| ecc8e0a108 | |||
| 2868a5ff4f | |||
| d71877f915 | |||
| e0e896d4b5 | |||
| 0a280e9399 | |||
| a58ac967a6 | |||
| ebc6723f55 | |||
| 66fd20ace5 | |||
| de00212bb6 | |||
| f0b458cd17 | |||
| 7aa217828b | |||
| b27f8e45dc | |||
| 3e02f0a0ba | |||
| f759999826 | |||
| 049aa35286 | |||
| dd8be657c2 | |||
| 09752241ba | |||
| 83561a0ceb | |||
| b8ca83ddd1 | |||
| 4cae6c051b | |||
| e5f9467329 | |||
| 34c1f3c7de | |||
| c0f56be2a9 | |||
| c6f89431d9 | |||
| 80601451b6 | |||
| f74d1beb62 | |||
| b73ea7f086 | |||
| a3bf6975c4 | |||
| 08ebb52e9b | |||
| 47a6af2b25 | |||
| a84cfb5677 | |||
| 6e92c4915a | |||
| 2a18df61cd | |||
| fb28083ce6 | |||
| 476bda812e | |||
| 265f01eb06 | |||
| cb9ef3ee6a | |||
| a4487179a5 | |||
| f43f627037 | |||
| a7b799f154 | |||
| ee7cd8cd4c | |||
| 2d1fcf4a8d | |||
| ef14074164 | |||
| 49aa9cf6da | |||
| 8d5ef45e2b | |||
| 9ef177e735 | |||
| 7f1919ebc6 | |||
| 1aaaaba619 | |||
| b7dd9b7d35 | |||
| ff0af15ac7 | |||
| 8a7ff9e01d | |||
| e52e257d9b | |||
| 2d6654b234 | |||
| 1b0427e943 | |||
| 24e1881876 | |||
| bfff38a262 | |||
| 2aff31fe06 | |||
| 6ba2b767b0 | |||
| bbc292561f | |||
| e4b00117c3 | |||
| d25851ac17 | |||
| d02450c0d9 | |||
| 400b2109fb | |||
| 2a09f9ef0b | |||
| 0052d349d9 | |||
| 7c8a8b8b70 | |||
| 85835087ba | |||
| 5aac042c80 | |||
| 9dbd073073 | |||
| 035f6c8098 | |||
| d2a06654c3 | |||
| c028686298 | |||
| 858a02634e | |||
| 2682fd38f8 | |||
| 3ecb701504 | |||
| ac359a0e9e | |||
| df636cf4f6 | |||
| 791a363694 | |||
| 06eb8b5be8 | |||
| 4b4290e087 | |||
| c89f91215c | |||
| 65d07dbff2 | |||
| 2e4d28b910 | |||
| 51c63fa996 | |||
| f0a582dc92 | |||
| bee36a7a4f | |||
| 0edd54dee3 | |||
| 4a16cd891f | |||
| 23dd9d4c01 | |||
| df277d6b7b | |||
| eed2c304e4 | |||
| bd5953317e | |||
| b88f58cc21 | |||
| 9cbf3079f3 | |||
| 682ac25111 | |||
| 07da9a9dd2 | |||
| 395ae4d05a | |||
| 2ef35a8583 | |||
| 5807dbfdac | |||
| c4f26c3cad | |||
| 26d87b691c | |||
| 52e2b90eef | |||
| 62eb9ce598 | |||
| a9efda3bc3 | |||
| 8fe4dca4c2 | |||
| a05d3a47c6 | |||
| e9265eff35 | |||
| 8fa7bf795a | |||
| f045fb44de | |||
| 0d06840a1b | |||
| 624e540cb4 | |||
| 353333653f | |||
| 83fec57400 | |||
| 7ad0ec1600 | |||
| ec611aac45 | |||
| d7752961b8 | |||
| 8c5837e78a | |||
| b3f8df1783 | |||
| 0ecc6c196f | |||
| 77686547eb | |||
| 419cf6c12b | |||
| 326571b797 | |||
| c25c2693f5 | |||
| 8d8a1f552d | |||
| 638f5c7323 | |||
| 9ec5d33840 | |||
| 8d3ba2a8cd | |||
| 0c2838cc90 | |||
| 1e5edfca26 | |||
| b141e1ee68 | |||
| 2fd4836228 | |||
| 9ceab0f294 | |||
| d4c12dcef4 | |||
| 16bb920011 | |||
| 158fa85c46 | |||
| 34bd0e11e4 | |||
| 9e649a18c4 | |||
| 2bb45afe5f | |||
| 41a9098c80 | |||
| 36ef03a406 | |||
| fd6c713119 | |||
| 4a57d3ada1 | |||
| 53160c638a | |||
| a2077f7be1 | |||
| b245d44220 | |||
| b15f7bcf45 | |||
| f16cb8f7d2 | |||
| af915be142 | |||
| 2633f4fcef | |||
| 6c2330984e | |||
| 1e1d5ef442 | |||
| 248757ac3f | |||
| cdbfb2c08c | |||
| d84c917f19 | |||
| 1c7c82358d | |||
| c085694a8b | |||
| a57c2b9fe5 | |||
| 9fc87fe20f | |||
| 4761e25fe1 | |||
| 3ce50d80de | |||
| 8754b023d5 | |||
| 22c9829fea | |||
| 0f7b3e9f84 | |||
| 1a456828df | |||
| 5958c69d64 | |||
| b14aa901ba | |||
| 535dfb5099 | |||
| 19f1aad88c | |||
| 9009eccc8e | |||
| 7f3cdedef9 | |||
| 05095805f1 | |||
| 5430147572 | |||
| 9b9f031fe1 | |||
| 2ad63247c2 | |||
| 87f37e3d87 | |||
| cb8edd9b8f | |||
| 072ae7280e | |||
| e99c299c6f | |||
| 46cdbd514f | |||
| a3e0189f0b | |||
| 8f4344f413 | |||
| 7e28de1d24 | |||
| 624e685212 | |||
| a23765a1cb | |||
| 048502ecd1 | |||
| 1b072448c4 | |||
| 86997488fb | |||
| db6efec417 | |||
| 2cc60c6ee2 | |||
| 80c98b6a35 | |||
| f51c01dc88 | |||
| 7b8a35a311 | |||
| d9db5263e8 | |||
| 912a661c04 | |||
| d3edc62fc7 | |||
| 6b5de5fec8 | |||
| fed4c51aa4 | |||
| 1721209f85 | |||
| 289820188e | |||
| 4fa0a9064b | |||
| f751d97c29 | |||
| d7028252c2 | |||
| a2ee70a964 | |||
| 57b166d13a | |||
| 5c76f2b9a2 | |||
| e59f06ff92 | |||
| d3b96dd048 | |||
| 6544e8135c | |||
| 54ed415d75 | |||
| f34937d4ae | |||
| a4fc40875b | |||
| 56c1441494 | |||
| 08ce8213fc | |||
| 0a3692e459 | |||
| 341546508f | |||
| 444d583a49 | |||
| 130ad4451b | |||
| d24cf8e7ec | |||
| 2d750a14b9 | |||
| b25b9d815b | |||
| daf965f119 | |||
| 296cd83a55 | |||
| b297888eb4 | |||
| e2569ed6dd | |||
| 4718bfac25 | |||
| 2f0798de32 | |||
| 11b0a00593 | |||
| 1d4eb59e94 | |||
| d6c8838fe2 | |||
| 0b888ac54d | |||
| 75099abbc1 | |||
| 3c4f835d8b | |||
| ce842f7561 | |||
| 508af7eb72 | |||
| a4386a617a | |||
| 767ba713a6 | |||
| 020e85906d | |||
| d1ffdd57a5 | |||
| 9c9323e040 | |||
| 376cf09565 | |||
| 047129223e | |||
| 4b64168faa | |||
| 2bc6d08410 | |||
| 484bb6b11f | |||
| a0884e2a03 | |||
| 1e352fdf80 | |||
| 83dfbb84c2 | |||
| 9043064543 | |||
| a3649148c1 | |||
| 28966739f6 | |||
| 13cb32903a | |||
| dcd75e0384 | |||
| dfdd9e02ca | |||
| 275796c241 | |||
| 6ca99e33ab | |||
| 1909caf769 | |||
| 2d07eeb518 | |||
| 1c0da7cd49 | |||
| fd316c81c0 | |||
| edbd437d7b | |||
| 9be1f9c18b | |||
| 96f06d8026 | |||
| 935d8e8aea | |||
| 21b99b3810 | |||
| c051a1249d | |||
| 564697e175 | |||
| d4bf358eae | |||
| f84d5ba3a0 | |||
| b550ff038c | |||
| ac9c629c0b | |||
| 755e24315e | |||
| c6ecd31ea1 | |||
| e600b91a85 | |||
| abff279707 | |||
| fd18508266 | |||
| d7bc309eeb | |||
| 33c40e5ef8 | |||
| 062d926900 | |||
| 0ad0046a76 | |||
| b36cee93f7 | |||
| 2731e70fb7 | |||
| 276cf858fc | |||
| b2acff40c4 | |||
| e3333e716f | |||
| bb7b8c2fab | |||
| 389c7be264 | |||
| c2481e460b | |||
| c93e4a951a | |||
| aa094bf930 | |||
| 8a741952df | |||
| 0dcc55ee16 | |||
| 6c3a3b9e29 | |||
| e4eff8c837 | |||
| 527b895e3c | |||
| 4eb0a7b7c0 | |||
| f11462bf41 | |||
| dea1c31701 | |||
| 68ab43b3ab | |||
| cf72e3bba5 | |||
| 62743edf45 | |||
| eec3752b5a | |||
| 006fd6c733 | |||
| 7078e20200 | |||
| 52c01abbe9 | |||
| 6e26f39751 | |||
| 08aab9d4d8 | |||
| dc254bd562 | |||
| 6c3173f45b | |||
| 30cba7b935 | |||
| 9e27c0fe2c | |||
| 05c09eff19 | |||
| 258eb88684 | |||
| 9c73ef770f | |||
| de9a52366f | |||
| 57265b9e33 | |||
| b7ef2d97e2 | |||
| 4fb364195c | |||
| 578bede46b | |||
| 38a51324b2 | |||
| 0e7c9a4001 | |||
| 32fee6313b | |||
| c6a74c040e | |||
| a97384cec1 | |||
| f052adb93a | |||
| c2560e0cf8 | |||
| cd6d1fb2d2 | |||
| c354ee32e3 | |||
| 5c5df74860 | |||
| 887fb6a84c | |||
| 80ea9e8de4 | |||
| 3879b01ddd | |||
| 4490681fa3 | |||
| b98e24fabf | |||
| 2deafe214a | |||
| d0258756e5 | |||
| 4fecad16e4 | |||
| a82eeb44c0 | |||
| 7ec5ab0c20 | |||
| 872e10fc95 | |||
| e3b7d0f84d | |||
| e0c424eb92 | |||
| 536427310d | |||
| a7eb0bb04a | |||
| af6a5b85f5 | |||
| b96e8093a0 | |||
| 989dc0aabe | |||
| 735f892a08 | |||
| e959c275fd | |||
| 7dbcc90450 | |||
| 7f86eac8f1 | |||
| 40de717cc9 | |||
| df211c994d | |||
| ee314e9b03 | |||
| 9049a8b964 | |||
| 88c029202c | |||
| 9142fb753a | |||
| 4688c0bf10 | |||
| 7e55258bbf | |||
| 828e475921 | |||
| 9d5f919023 | |||
| b8932390f7 | |||
| 445881992e | |||
| 30d96dcb65 | |||
| 3a69fd50bb | |||
| 0f72729f0d | |||
| 0a8874180c | |||
| a6501474bb | |||
| 4cb93ddcde | |||
| 7f1918ae8c | |||
| 78adaaedef | |||
| 4a65113bff | |||
| 843d9d52c3 | |||
| 7a74189771 | |||
| 6de04f7781 | |||
| ba27f2ba28 | |||
| f09b89ed63 | |||
| bac8594c5f | |||
| 3582ae9163 | |||
| 7a95bafa54 | |||
| 1383af6208 | |||
| bea9a3c2dc | |||
| 78ccec5d7b | |||
| 534e3f7f81 | |||
| 6a427d6f89 | |||
| b4297580a7 | |||
| 26b0e2ae43 | |||
| e75ac7b3c0 | |||
| 74442b0d76 | |||
| 3431803980 | |||
| c5fb247de6 | |||
| 18640d59da | |||
| 48753b2539 | |||
| 606dcc8cc3 | |||
| 9e4c548c69 | |||
| 75a974643b | |||
| e2711ba911 | |||
| 7605198774 | |||
| fc9525e7d8 | |||
| e597b6473d | |||
| 16c8dcf788 | |||
| 37842d4fbd | |||
| 133a1d2e8f | |||
| 15c15a9c36 | |||
| d233d50de3 | |||
| a9d8e7ca82 | |||
| 4001cc9531 | |||
| 4319df0cb6 | |||
| 5be7898522 | |||
| 009760c31a | |||
| 77349526e4 | |||
| 3ba195c2d9 | |||
| 60f5032b12 | |||
| fdc201a799 | |||
| ec8616a515 | |||
| 349d11e76d | |||
| 62e3774ff7 | |||
| abaeb52b7c | |||
| 60e600c381 | |||
| 97f84ee87f | |||
| c5738aeed9 | |||
| 192adccb10 | |||
| defcacb59b | |||
| e4ea786956 | |||
| c9d98bf4d7 | |||
| 8dd08c360f | |||
| d6c4bb0e4e | |||
| c54916cdfd | |||
| e29cf6fcc1 | |||
| 5b3cd166b0 | |||
| a7246c6518 | |||
| b48fd78cf0 | |||
| 66c895ee3c | |||
| 37c450dd83 | |||
| 4c744da3e2 | |||
| 388e7b3c2f | |||
| 6ec5377381 | |||
| 24ec47e4ff | |||
| 4a122523d9 | |||
| 1241911ec0 | |||
| 0f54bb1938 | |||
| a3b55aa5cc | |||
| 4fa02a483b | |||
| a786c642f1 | |||
| 1bade7bee8 | |||
| d399df8798 | |||
| 749b11a5ea | |||
| 328af5f560 | |||
| 876f97e9a2 | |||
| 26d7a32f11 | |||
| 3057773009 | |||
| 6ca89cb907 | |||
| 88b5b9b21c | |||
| 6bcf8b91cc | |||
| 7cf7ebc555 | |||
| ee0aaca2b0 | |||
| b1b084452c | |||
| 213b1a2dae | |||
| b991cfc493 | |||
| 90cdea3656 | |||
| 1463f27a0f | |||
| afb8ab44cf | |||
| 282f36c47c | |||
| c3f27d1ad9 | |||
| ad230a8dc7 | |||
| c29c4c0a8b | |||
| 284f5615cc | |||
| c625096f59 | |||
| c3cd5d08c9 | |||
| c87c3c6672 | |||
| 8fc7688585 | |||
| 94bcfb11de | |||
| ebeaac3c5f | |||
| 942ffc038d | |||
| 9de344e48d | |||
| 5efea045d2 | |||
| 6bd3274a4b | |||
| a179313db2 | |||
| 289376840e | |||
| 0e3d84e5ca | |||
| cb643f8fd9 | |||
| 1faa81099c | |||
| 2dc079b950 | |||
| f6cf740507 | |||
| 338c866d83 | |||
| 038e6c312b | |||
| 6f6def34f7 | |||
| 88651149d3 | |||
| e301090704 | |||
| d0f8a612ad | |||
| a7728e3d81 | |||
| 4dadc18a3a | |||
| 43efe6921d | |||
| 2e31c8695a | |||
| 988bd8ccdc | |||
| 965794712e | |||
| 68ea086e2a | |||
| 7370f41fa0 | |||
| 5cbe30095b | |||
| c75c3f1ab1 | |||
| 281ad5fac9 | |||
| eb0301d74e | |||
| 4a76757dd2 | |||
| 1855dae35e | |||
| f20c5a1bcb | |||
| 02979926fd | |||
| b08337af45 | |||
| 980c1e4812 | |||
| f43dd8db1b | |||
| badda165fe | |||
| f7d17875c0 | |||
| 0c124bcca3 | |||
| 3eb63f1730 | |||
| abb0b3b4c0 | |||
| 42662748d3 | |||
| b5b866a894 | |||
| 4e7e715f10 | |||
| 5be1004292 | |||
| 01cccc6462 | |||
| 9287b8ea42 | |||
| 5fabc5c7f9 | |||
| 61dc6f980d | |||
| 84328cfb90 | |||
| 37a35022f5 | |||
| 3afe540111 | |||
| 35687b1ee0 | |||
| 195e6b61f9 | |||
| dabc0331cb | |||
| 824abb0406 | |||
| f6789aaace | |||
| 10bce85561 | |||
| cd0f1b6c5c | |||
| 834bb7d777 | |||
| 5c1d7baf72 | |||
| 8bdbc9ec4c | |||
| 0c6a0720af | |||
| c6095fd92a | |||
| 7d0fb0de83 | |||
| 75281afe05 | |||
| 738e714f45 | |||
| 898bfaeecf | |||
| 7f44d9cdd2 | |||
| 8d9be21c8a | |||
| 82cb4c8c11 | |||
| 2778e4d697 | |||
| 30fe2dd502 | |||
| 1170ff58ee | |||
| 2483bf812f | |||
| 7a999de602 | |||
| 1929c27225 | |||
| 0cf5c9c7d6 | |||
| c20b691683 | |||
| 5c2101de1a | |||
| 2ec2e81511 | |||
| ac3d8b832c | |||
| ad174957e8 | |||
| 9b3fa83516 | |||
| 52b8d9d361 | |||
| d231ded7a2 | |||
| 5f57951e3d | |||
| b1a4788442 | |||
| 6377378a3d | |||
| 45b91cdea5 | |||
| d5647c6760 | |||
| f419559bde | |||
| 4cfaa39419 | |||
| d6b1762adf | |||
| 45c3293385 | |||
| 92d574605e | |||
| cec7b90631 | |||
| aa3cb35f09 | |||
| 7373138e3d | |||
| 2d8176f213 | |||
| 8589d72aee | |||
| 626b50087a | |||
| 1e62554dfb | |||
| bcf800482b | |||
| 08f94d2122 | |||
| 559cfd11db | |||
| ce820c2314 | |||
| 8583ed0e5c | |||
| f2c16a06a1 | |||
| 584d469a73 | |||
| 4298e582fb | |||
| a3fdfbbc16 | |||
| b8929a0d18 | |||
| 0fa5832b63 | |||
| bbb29ebd7c | |||
| 0fd9ac6da3 | |||
| e53098b179 | |||
| 78dcab2778 | |||
| e05f74cef1 | |||
| 3d8724c90d | |||
| ffeda19ce5 | |||
| 70fe8f14e3 | |||
| a639d9f49f | |||
| e5251422fe | |||
| d8f80c058d | |||
| 4cf3655c08 | |||
| 58fc786462 | |||
| 81b142704c | |||
| b0cbeda796 | |||
| 0b3a8a5f65 | |||
| d7d8d2a3e6 | |||
| d5c0309885 | |||
| 36fa9d81fb | |||
| 35f5a5d11a | |||
| d1fe50e271 | |||
| 2d08b31f9a | |||
| 27643f4a48 | |||
| 110e179e9e | |||
| fe6d99d4f1 | |||
| 06fbcbe24b | |||
| f40b57ee48 | |||
| 9438c69bed | |||
| 70d6f3000a | |||
| d47fd0cf18 | |||
| 40117fbe54 | |||
| 7466d8a1de | |||
| ce09443e48 | |||
| 1718dbe94e | |||
| 69894db206 | |||
| 6e4c873248 | |||
| 64693ffd26 | |||
| 66f8ca72c1 | |||
| c82f7bd8fe | |||
| 34176ab5f8 | |||
| 7f12ce9b45 | |||
| 9e1b9f7f5c | |||
| 23e949de1c | |||
| 00bd60a855 | |||
| dacd9afac0 | |||
| 3fc5bb31d0 | |||
| 78770888a5 | |||
| f5b092acf2 | |||
| ba9562f000 | |||
| aded178ab7 | |||
| 0b5d905120 | |||
| 08abec8fd5 | |||
| 63c23736ad | |||
| f7bd373ce6 | |||
| 8e717d2ea1 | |||
| 5f271ed127 | |||
| d686b65dbe | |||
| c7e882e001 | |||
| f07859996e | |||
| fbc73bc78a | |||
| bd8ad1ed1b | |||
| dc2ff83fa5 | |||
| 13545d147b | |||
| 1b85aa713b | |||
| 90a58858ca | |||
| 8149b85a55 | |||
| e79b6c514a | |||
| aaa894d74b | |||
| b55721f16f | |||
| b39d948d71 | |||
| 0fee279e3d | |||
| c7d8a9cb82 | |||
| b50075f102 | |||
| fb3ca9f960 | |||
| 058b971870 | |||
| 610ea414c1 | |||
| cc6997baec | |||
| d126c28b85 | |||
| c1649b9dc0 | |||
| b0a8d12112 | |||
| b5160d38cb | |||
| 360e9b5936 | |||
| 85a6cd828e | |||
| e698f19a64 | |||
| 83c1b580dd | |||
| 9273e1f8ed | |||
| 5287825279 | |||
| 5567959afd | |||
| eaab36b051 | |||
| 9a4f3af580 | |||
| b83664dfce | |||
| 349f7ae965 | |||
| 6f41716ab5 | |||
| abeb62dea8 | |||
| 7cfed80443 | |||
| 36aa7d9b25 | |||
| 2a449ad3c0 | |||
| a71c5957cc | |||
| eef6c7d55e | |||
| 3872322da1 | |||
| f1e850a62a | |||
| 8d9278419d | |||
| 0862827a51 | |||
| f37a220a05 | |||
| aa51c2b6dd | |||
| 92da70a7ae | |||
| a55862fc25 | |||
| 09b14a5702 | |||
| 260b831815 | |||
| 665cf6ac65 | |||
| 5e90021698 | |||
| 16aeb7e12c | |||
| b4cbec173b | |||
| 8c9e06c329 | |||
| e4a35fdd72 | |||
| 370d82e66c | |||
| 876e851517 | |||
| 190fe37a9f | |||
| 623661f8d2 | |||
| f31bc3f8c5 | |||
| 12fc48544c | |||
| 3b4e44a9fd | |||
| 7850fb727e | |||
| 9b3122837e | |||
| d4c59cc73b | |||
| fe36bf54b2 | |||
| 4d8308a763 | |||
| 11f26c1e2c | |||
| e10104d401 | |||
| 5e09c3c318 | |||
| 576b88221d | |||
| 7dc2a0091b | |||
| 89a4a85589 | |||
| a3baf07a5c | |||
| 8e6909478c | |||
| 2d2d0cabb3 | |||
| 6f04a1b8fa | |||
| a9ad3a56ad | |||
| 3069b14e9c | |||
| ae868c6372 | |||
| c287ff554c | |||
| 3ccbd35fad | |||
| 51cc69208f | |||
| 1fec2f7a46 | |||
| ffffdd2d7d | |||
| f8c3e3d5aa | |||
| eccce6fa84 | |||
| 44be6cee2c | |||
| 799afd511a | |||
| 37c4541468 | |||
| 119a66acd0 | |||
| 105193a452 | |||
| 02d793e9e0 | |||
| 90647a8ba8 | |||
| 06950a6bd8 | |||
| 0c2147d9c5 | |||
| 51e8174689 | |||
| 185318a70c | |||
| a88759ba94 | |||
| 96ac054392 | |||
| bd7f4dd73f | |||
| 2e22152089 | |||
| 98a89aea11 | |||
| 372e33c066 | |||
| 52641db590 | |||
| f9ad5f0d01 | |||
| 3db1c7a521 | |||
| b6d1f96930 | |||
| 61a9b01062 | |||
| f0faf5b42e | |||
| 11733d5e05 | |||
| c4a38d9008 | |||
| 0edd7e14b7 | |||
| 8b7bb7b6ae | |||
| 39d5955e58 | |||
| 536327f403 | |||
| 8679dc63be | |||
| 1e9979ae18 | |||
| d2dffe3186 | |||
| 9971867e2f | |||
| 8c93e578a0 | |||
| 1ba5d2e1ca | |||
| ed792fc0d4 | |||
| 4e575c44b3 | |||
| fc06181217 | |||
| 0d6fcf78d8 | |||
| 82225f0b81 | |||
| 2177e13b49 | |||
| b63f4eb503 | |||
| cd7bc09f9c | |||
| ccf775e976 | |||
| 811346e83d | |||
| d3240a75d0 | |||
| d93e396ce5 | |||
| d74cc234c4 | |||
| 0fda83d8de | |||
| a1ec8545fb | |||
| 087f574355 | |||
| 1de784b817 | |||
| db79c05d9a | |||
| 1fdb1f0da6 | |||
| e2bfaa6233 | |||
| 6089849d35 | |||
| 6e02bf59e9 | |||
| 12228c56d9 | |||
| 3b631cb6af | |||
| b27a58df7e | |||
| 308f139193 | |||
| 692fc4e11d | |||
| b52f65c4d7 | |||
| 299275e368 | |||
| bb71c519db | |||
| 9ec7109dc8 | |||
| 5b67fccb1b | |||
| 9807c4b7a1 | |||
| 9039600f6d | |||
| b5b0156e08 | |||
| 352e647d37 | |||
| e6b66f2525 | |||
| f5075906e2 | |||
| a6f57bced8 | |||
| d35bf9591c | |||
| d96f3f9dd4 | |||
| 1f4c2f8c5a | |||
| 95719654f3 | |||
| 4b921c47f5 | |||
| f069f53fae | |||
| 1494cc38fc | |||
| a035412956 | |||
| 08662ea831 | |||
| d97103268b | |||
| a959e2e34d | |||
| 602bfbdd79 | |||
| 0785bdd37a | |||
| 8b62b6e1b1 | |||
| d1c8ebd15b | |||
| 13088e4001 | |||
| 5c6fa1c82b | |||
| fd81867e7b | |||
| 78293e64dd | |||
| 930fc10aec | |||
| 0b2a4fda44 | |||
| efdce6d4cd | |||
| c0b0ea4493 | |||
| 0fcec0a0ca | |||
| c0701d8a35 | |||
| ae51092044 | |||
| b7fcccb8db | |||
| b21a52f5f3 | |||
| 2bdcfe13e6 | |||
| 12a97ec429 | |||
| 99298ce9f2 | |||
| a0ed25d373 | |||
| dc6c29c668 | |||
| 3a85f28b5c | |||
| 95c44b1ce2 | |||
| b88e0ecb78 | |||
| c26f6b72d9 | |||
| 6cd3cc96a3 | |||
| c138cf0b76 | |||
| 8edb086178 | |||
| 0f74b5a4b1 | |||
| 6de2feff05 | |||
| a9811ee955 | |||
| 98b6c11a82 | |||
| 3df2929d12 | |||
| 30d1f423cb | |||
| e0c5566f35 | |||
| c5a2ae47d7 | |||
| 17654b885b | |||
| acaf0ca0af | |||
| 6a201be1fd | |||
| 8617b9164e | |||
| 991e5f1b3c | |||
| 55c533c947 | |||
| d17543d5d9 | |||
| 73169563c4 | |||
| d351cb9f7f | |||
| 251be60630 | |||
| 74ad521fc9 |
+5
-1
@@ -17,4 +17,8 @@ vite.config.js
|
||||
**/*.ts
|
||||
**/*.json
|
||||
**/dev-dist
|
||||
**/dist
|
||||
**/dist
|
||||
/src-tauri/target/**/*
|
||||
reverbGen.mjs
|
||||
hydra.mjs
|
||||
jsdoc-synonyms.js
|
||||
@@ -21,13 +21,13 @@ jobs:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8.11.0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Tauri Builder
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust_target: x86_64-unknown-linux-gnu
|
||||
- os: macos-latest
|
||||
rust_target: x86_64-apple-darwin
|
||||
- os: macos-latest
|
||||
rust_target: aarch64-apple-darwin
|
||||
- os: windows-latest
|
||||
rust_target: x86_64-pc-windows-msvc
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8.6.2
|
||||
|
||||
- name: Node.js setup
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: latest
|
||||
# node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install Rust (Stable)
|
||||
run:
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf libasound2-dev
|
||||
|
||||
- name: Install app dependencies from lockfile and build web
|
||||
run: pnpm install
|
||||
|
||||
- name: Build the app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# tauri-action replaces \_\_VERSION\_\_ with the app version
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Strudel v__VERSION__"
|
||||
releaseBody: |
|
||||
See the assets to download this version and install.
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
@@ -7,13 +7,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8.11.0
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
+88
-1
@@ -39,4 +39,91 @@ server/samples/old
|
||||
repl/stats.html
|
||||
coverage
|
||||
public/icons/apple-splash-*
|
||||
dev-dist
|
||||
dev-dist
|
||||
Dirt-Samples
|
||||
tidal-drum-machines
|
||||
webaudiofontdata
|
||||
src-tauri/target
|
||||
|
||||
# BEGIN JetBrains -> END JetBrains
|
||||
|
||||
# for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# END JetBrains -> BEGIN JetBrains
|
||||
|
||||
+2
-1
@@ -9,4 +9,5 @@ packages/xen/tunejs.js
|
||||
paper
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
**/dev-dist
|
||||
**/dev-dist
|
||||
website/.astro
|
||||
|
||||
+13
-6
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
|
||||
## Ask a Question
|
||||
|
||||
If you have any questions about strudel, make sure you've glanced through the
|
||||
[docs](https://strudel.tidalcycles.org/learn/) to find out if it answers your question.
|
||||
[docs](https://strudel.cc/learn/) to find out if it answers your question.
|
||||
If not, use one of the Communication Channels above!
|
||||
|
||||
Don't be afraid to ask! Your question might be of great value for other people too.
|
||||
@@ -31,7 +31,7 @@ Use one of the Communication Channels listed above.
|
||||
|
||||
## Improve the Docs
|
||||
|
||||
If you find some weak spots in the [docs](https://strudel.tidalcycles.org/learn/getting-started),
|
||||
If you find some weak spots in the [docs](https://strudel.cc/workshop/getting-started/),
|
||||
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||
|
||||
## Propose a Feature
|
||||
@@ -66,7 +66,7 @@ To get the project up and running for development, make sure you have installed:
|
||||
|
||||
- [git](https://git-scm.com/)
|
||||
- [node](https://nodejs.org/en/) >= 18
|
||||
- [pnpm](https://pnpm.io/) (`npm i pnpm -g`)
|
||||
- [pnpm](https://pnpm.io/) (`curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=8.11.0 sh -`)
|
||||
|
||||
then, do the following:
|
||||
|
||||
@@ -83,7 +83,7 @@ Please report any problems you've had with the setup instructions!
|
||||
|
||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
||||
|
||||
- You can format all files at once by running `pnpm prettier` from the project root
|
||||
- You can format all files at once by running `pnpm codeformat` from the project root
|
||||
- Run `pnpm format-check` from the project root to check if all files are well formatted
|
||||
|
||||
If you use VSCode, you can
|
||||
@@ -114,7 +114,7 @@ You can run the same check with `pnpm check`
|
||||
## Package Workflow
|
||||
|
||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
||||
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel.cycles/<package-name>` to get the local version,
|
||||
When you run `pnpm i` on the root folder, [pnpm workspaces](https://pnpm.io/workspaces) will install all dependencies of all subpackages. This will allow any js file to import `@strudel/<package-name>` to get the local version,
|
||||
allowing to develop multiple packages at the same time.
|
||||
|
||||
## Package Publishing
|
||||
@@ -123,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
|
||||
|
||||
```sh
|
||||
npm login
|
||||
npx lerna publish
|
||||
|
||||
# this will increment all the versions in package.json files of non private packages to selected versions
|
||||
npx lerna version --no-private
|
||||
|
||||
# publish all packages inside /packages using pnpm! don't use lerna to publish!!
|
||||
pnpm --filter "./packages/**" publish --dry-run
|
||||
|
||||
# the last command was only a dry-run, make sure everything looks ok, if yes, run the same command without flag
|
||||
```
|
||||
|
||||
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
[](https://github.com/tidalcycles/strudel/actions)
|
||||
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is slowly stabilising, but please continue to tread carefully.
|
||||
An experiment in making a [Tidal](https://github.com/tidalcycles/tidal/) using web technologies. This software is a bit more stable now, but please continue to tread carefully.
|
||||
|
||||
- Try it here: <https://strudel.tidalcycles.org/>
|
||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
||||
- Try it here: <https://strudel.cc>
|
||||
- Docs: <https://strudel.cc/learn>
|
||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
||||
- 2 Years of Strudel Blog Post: <https://strudel.cc/blog/#year-2>
|
||||
|
||||
## Running Locally
|
||||
|
||||
@@ -24,13 +25,12 @@ There are multiple npm packages you can use to use strudel, or only parts of it,
|
||||
|
||||
- [`core`](./packages/core/): tidal pattern engine
|
||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
||||
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
||||
- [`transpiler`](./packages/transpiler): user code transpiler
|
||||
- [`webaudio`](./packages/webaudio): webaudio output
|
||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||
- [`midi`](./packages/midi): webmidi bindings
|
||||
- [`serial`](./packages/serial): webserial bindings
|
||||
- [`tonal`](./packages/tonal): tonal functions
|
||||
- [`xen`](./packages/xen): microtonal / xenharmonic functions
|
||||
- ... [and there are more](./packages/)
|
||||
|
||||
Click on the package names to find out more about each one.
|
||||
|
||||
-1806
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,3 @@
|
||||
# examples
|
||||
|
||||
This folder contains usage examples for different scenarios.
|
||||
@@ -0,0 +1,5 @@
|
||||
# buildless examples
|
||||
|
||||
These examples show you how strudel can be used in a regular html file, without the need for a build tool.
|
||||
|
||||
Most examples are using [skypack](https://www.skypack.dev/)
|
||||
@@ -7,7 +7,7 @@
|
||||
/>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
|
||||
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||
const input = document.getElementById('text');
|
||||
const getEvents = () => {
|
||||
@@ -8,7 +8,7 @@
|
||||
/>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8');
|
||||
const strudel = await import('https://cdn.skypack.dev/@strudel/core@0.6.8');
|
||||
// this adds all strudel functions to the global scope, to be used by eval
|
||||
Object.assign(window, strudel);
|
||||
// setup elements
|
||||
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<button id="play">play</button>
|
||||
<button id="stop">stop</button>
|
||||
<script type="module">
|
||||
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||
|
||||
initStrudel();
|
||||
document.getElementById('play').addEventListener('click', () => evaluate('note("c a f e").jux(rev)'));
|
||||
document.getElementById('play').addEventListener('stop', () => hush());
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<button id="a">A</button>
|
||||
<button id="b">B</button>
|
||||
<button id="c">C</button>
|
||||
<button id="stop">stop</button>
|
||||
<script type="module">
|
||||
import { initStrudel } from 'https://cdn.skypack.dev/@strudel/web@0.8.2';
|
||||
initStrudel({
|
||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
});
|
||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
||||
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
|
||||
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
|
||||
click('stop', () => hush());
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -16,34 +16,40 @@
|
||||
</div>
|
||||
<div id="output"></div>
|
||||
<script type="module">
|
||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
|
||||
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
|
||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
|
||||
import {
|
||||
getAudioContext,
|
||||
webaudioOutput,
|
||||
initAudioOnFirstClick,
|
||||
} from 'https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0';
|
||||
registerSynthSounds,
|
||||
} from 'https://cdn.skypack.dev/@strudel/webaudio@0.11.0';
|
||||
|
||||
initAudioOnFirstClick();
|
||||
const ctx = getAudioContext();
|
||||
const input = document.getElementById('text');
|
||||
input.innerHTML = getTune();
|
||||
|
||||
evalScope(
|
||||
const loadModules = evalScope(
|
||||
controls,
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
||||
import('https://cdn.skypack.dev/@strudel/core@0.11.0'),
|
||||
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'),
|
||||
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'),
|
||||
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'),
|
||||
);
|
||||
|
||||
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
|
||||
|
||||
const { evaluate } = repl({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => ctx.currentTime,
|
||||
transpiler,
|
||||
});
|
||||
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
||||
document.getElementById('start').addEventListener('click', async () => {
|
||||
await loadModules;
|
||||
await initAudio;
|
||||
evaluate(input.value);
|
||||
});
|
||||
|
||||
function getTune() {
|
||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
@@ -1,4 +1,4 @@
|
||||
<script src="https://unpkg.com/@strudel.cycles/embed@latest"></script>
|
||||
<script src="https://unpkg.com/@strudel/embed@0.11.0"></script>
|
||||
<!-- <script src="./embed.js"></script> -->
|
||||
<strudel-repl>
|
||||
<!--
|
||||
@@ -0,0 +1,31 @@
|
||||
<script src="https://unpkg.com/@strudel/repl@0.9.4"></script>
|
||||
<strudel-editor>
|
||||
<!--
|
||||
// @date 23-08-15
|
||||
// "golf rolf" @by froos @license CC BY-NC-SA 4.0
|
||||
|
||||
setcps(1)
|
||||
stack(
|
||||
s("bd*2, ~ rim*<1!3 2>, hh*4").bank('RolandTR909')
|
||||
.off(-1/8, set(speed("1.5").gain(.25)))
|
||||
.mask("<0!16 1!64>")
|
||||
,
|
||||
note("g1(3,8)")
|
||||
.s("gm_synth_bass_2:<0 2>")
|
||||
.delay(".8:.25:.25")
|
||||
.clip("<.5!16 2!32>")
|
||||
.off(1/8, add(note("12?0.7")))
|
||||
.lpf(sine.range(500,2000).slow(32)).lpq(8)
|
||||
.add(note("0,.05"))
|
||||
.mask("<0!8 1!32>")
|
||||
,
|
||||
n("<0 1 2 3 4>*8").scale('G4 minor')
|
||||
.s("gm_lead_6_voice")
|
||||
.clip(sine.range(.2,.8).slow(8))
|
||||
.jux(rev)
|
||||
.room(2)
|
||||
.sometimes(add(note("12")))
|
||||
.lpf(perlin.range(200,20000).slow(4))
|
||||
).reset("<x@15 x(5,8)>")
|
||||
-->
|
||||
</strudel-editor>
|
||||
@@ -0,0 +1,8 @@
|
||||
# codemirror-repl example
|
||||
|
||||
This folder demonstrates how to set up a full strudel repl with the `@strudel/codemirror` package. Run it using:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
pnpm dev
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite Vanilla Strudel REPL</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav>
|
||||
<button id="play">eval</button>
|
||||
<button id="stop">stop</button>
|
||||
</nav>
|
||||
<div class="container">
|
||||
<div id="editor"></div>
|
||||
<div id="output"></div>
|
||||
</div>
|
||||
<canvas id="roll"></canvas>
|
||||
</main>
|
||||
<script type="module" src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { StrudelMirror } from '@strudel/codemirror';
|
||||
import { funk42 } from './tunes';
|
||||
import { drawPianoroll, evalScope, controls } from '@strudel/core';
|
||||
import './style.css';
|
||||
import { initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import { getAudioContext, webaudioOutput, registerSynthSounds } from '@strudel/webaudio';
|
||||
import { registerSoundfonts } from '@strudel/soundfonts';
|
||||
|
||||
// init canvas
|
||||
const canvas = document.getElementById('roll');
|
||||
canvas.width = canvas.width * 2;
|
||||
canvas.height = canvas.height * 2;
|
||||
const drawContext = canvas.getContext('2d');
|
||||
const drawTime = [-2, 2]; // time window of drawn haps
|
||||
|
||||
const editor = new StrudelMirror({
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime: () => getAudioContext().currentTime,
|
||||
transpiler,
|
||||
root: document.getElementById('editor'),
|
||||
initialCode: funk42,
|
||||
drawTime,
|
||||
onDraw: (haps, time) => drawPianoroll({ haps, time, ctx: drawContext, drawTime, fold: 0 }),
|
||||
prebake: async () => {
|
||||
initAudioOnFirstClick(); // needed to make the browser happy (don't await this here..)
|
||||
const loadModules = evalScope(
|
||||
controls,
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/tonal'),
|
||||
import('@strudel/webaudio'),
|
||||
);
|
||||
await Promise.all([loadModules, registerSynthSounds(), registerSoundfonts()]);
|
||||
},
|
||||
});
|
||||
|
||||
document.getElementById('play').addEventListener('click', () => editor.evaluate());
|
||||
document.getElementById('stop').addEventListener('click', () => editor.stop());
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "vite-vanilla-repl-cm6",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel/codemirror": "workspace:*",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/soundfonts": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
main {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
max-height: 100%;
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#editor {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#roll {
|
||||
height: 300px;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
export const bumpStreet = `// froos - "22 bump street", licensed with CC BY-NC-SA 4.0
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
"<[0,<6 7 9>,13,<17 20 22 26>]!2>/2"
|
||||
// make it 22 edo
|
||||
.fmap(v => Math.pow(2,v/22))
|
||||
// mess with the base frequency
|
||||
.mul("<300 [300@3 200]>/8").freq()
|
||||
.layer(
|
||||
// chords
|
||||
x=>x.div(freq(2)).s("flute").euclidLegato("<3 2>",8)
|
||||
.shape(.4).lpf(sine.range(800,4000).slow(8)),
|
||||
// adlibs
|
||||
x=>x.arp("{0 3 2 [1 3]}%1.5")
|
||||
.s('xylo').mul(freq(2))
|
||||
.delay(.5).delayfeedback(.4).juxBy(.5, rev)
|
||||
.hpf(sine.range(200,3000).slow(8)),
|
||||
// bass
|
||||
x=>x.arp("[0 [2 1?]](5,8)").s('sawtooth').div(freq(4))
|
||||
.lpf(sine.range(400,2000).slow(8)).lpq(8).shape(.4)
|
||||
.off(1/8, x=>x.mul(freq(2)).degradeBy(.5)).gain(.3)
|
||||
).clip(1).release(.2)
|
||||
.stack(
|
||||
// drums
|
||||
s("bd sd:<2 1>, [~ hh]*2, [~ rim]").bank('RolandTR909')
|
||||
.off(1/8, x=>x.speed(2).gain(.4)).sometimes(ply(2)).gain(.8)
|
||||
.mask("<0@4 1@12>/4")
|
||||
.reset("<x@15 [x(3,8) x*[4 8]]>")
|
||||
// wait for it...
|
||||
).fast(2/3)
|
||||
//.crush(6) // remove "//" if you dare`;
|
||||
|
||||
export const trafficFlam = `// froos - "traffic flam", licensed with CC BY-NC-SA 4.0
|
||||
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
addVoicings('hip', {
|
||||
m11: ['2M 3m 4P 7m'],
|
||||
'^7#11': ['3M 4A 5P 7M'],
|
||||
}, ['C4', 'C6'])
|
||||
|
||||
stack(
|
||||
stack(
|
||||
"<Bbm11 A^7#11>/2".voicings('hip').note()
|
||||
.s("gm_epiano1:2")
|
||||
.arp("[<[0 1 2 3] [3 2 1 0]> ~@5]/2")
|
||||
.release(2).late(.25).lpf(2000),
|
||||
"<Bb1 A1>/2".note().s('gm_acoustic_bass'),
|
||||
n("<0 2 3>(3,8)".off(1/8, add(4)))
|
||||
.scale("<Bb4:minor A4:lydian>/2")
|
||||
.s('gm_electric_guitar_jazz')
|
||||
.decay(sine.range(.05, .2).slow(32)).sustain(0)
|
||||
.delay(.5).lpf(sine.range(100,5000).slow(64))
|
||||
.gain(.7).room(.5).pan(sine.range(0,1).slow(11))
|
||||
).add(perlin.range(0,.25).note()),
|
||||
stack(
|
||||
s("bd:1(3,8) rim").bank('RolandTR707').slow(2).room("<0 <.1 .6>>")
|
||||
.when("<0@7 1>",x=>x.echoWith(3, .0625, (x,i) => x.speed(1+i*.24))),
|
||||
s("rim*4").end(.05).bank('RolandTR808').speed(.8).room(.2)
|
||||
)
|
||||
)
|
||||
.late("[0 .05]*2").late(12)
|
||||
|
||||
`;
|
||||
|
||||
export const funk42 = `// froos - how to funk in 42 lines of code
|
||||
// adapted from "how to funk in two minutes" by marc rebillet https://www.youtube.com/watch?v=3vBwRfQbXkg
|
||||
// thanks to peach for the transcription: https://www.youtube.com/watch?v=8eiPXvIgda4
|
||||
|
||||
await samples('github:felixroos/samples/main')
|
||||
await samples('https://strudel.cc/tidal-drum-machines.json', 'github:ritchse/tidal-drum-machines/main/machines/')
|
||||
|
||||
setcps(.5)
|
||||
|
||||
let drums = stack(
|
||||
s("bd*2, ~ sd").bank('RolandTR707').room("0 .1"),
|
||||
s("hh*4").begin(.2).release(.02).end(.25).release(.02)
|
||||
.gain(.3).bank('RolandTR707').late(.02).room(.5),
|
||||
//s("shaker_small").struct("[x x*2]*2").speed(".8,.9").release(.02)
|
||||
).fast(2)
|
||||
|
||||
let wurli = note(\`<
|
||||
[[a2,g3,[b3 c4],e4] ~ [g3,c4,e4](3,8)@4 ~@2]!3
|
||||
[[e2,e3,a3,b3,e4]@3 [e2,e3,ab3,b3,e4]@5]>\`)
|
||||
.s("gm_epiano1:5").decay(.2).sustain("<[1 0@7]!3 1>")
|
||||
.gain("<[.8@2 .4@14]!3 .7>").room(.3)
|
||||
|
||||
let organ = note("<[~@3 [a3,d4,f#4]@2 [[a3,c4,e4]@2 ~] ~@2]!3 ~>".add(12))
|
||||
.s("gm_percussive_organ:2").gain(.6).lpf(1800).pan(.2).room(.3);
|
||||
|
||||
let clav = note(\`<
|
||||
[~@3 a2 [g3,[b3 c4],e4]@2 ~ a2 [g3,b3,e4] ~@2 [g3,c4,e4] ~@4]!3
|
||||
[~@3 e3 [[a3 b3],c3,e3]@2 ~ e2 [e3,a3]@3 [b3,e3] ~@2 [b3,e3]@2]>\`)
|
||||
.s("gm_clavinet:1").decay("<.25!3 [.25 .4]>").sustain(0)
|
||||
.gain(.7).pan(.8).room(.2);
|
||||
|
||||
let bass = note(\`<
|
||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||
[a1 [~ [g2 a2]] [e3 d3] [c3 [g3 a3]]]
|
||||
[a1 [~ [g2 a2]] [g1 g#1] [a1 [g2 a2]]]
|
||||
[e2@6 e1@5 e1 [[d2 e3] g1]@4]
|
||||
>\`).s("gm_electric_bass_pick:1").release(.1)
|
||||
|
||||
stack(
|
||||
drums
|
||||
,wurli
|
||||
,organ
|
||||
,clav
|
||||
,bass
|
||||
)`;
|
||||
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# headless-repl demo
|
||||
|
||||
This demo shows how to use strudel in "headless mode".
|
||||
Buttons A / B / C will switch between different patterns.
|
||||
It showcases the usage of the `@strudel/web` package, using [vite](https://vitejs.dev/) as the dev server.
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
pnpm i && cd examples/headless-repl
|
||||
pnpm dev
|
||||
# open http://localhost:5173/
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="https://strudel.cc/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>@strudel/web REPL Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<button id="a">A</button>
|
||||
<button id="b">B</button>
|
||||
<button id="c">C</button>
|
||||
<button id="stop">stop</button>
|
||||
<script type="module">
|
||||
import { initStrudel } from '@strudel/web';
|
||||
initStrudel({
|
||||
prebake: () => samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
});
|
||||
|
||||
const click = (id, action) => document.getElementById(id).addEventListener('click', action);
|
||||
click('a', () => evaluate(`s('bd,jvbass(3,8)').jux(rev)`));
|
||||
click('b', () => s('bd*2,hh(3,4),jvbass(5,8,1)').jux(rev).play());
|
||||
click('c', () => s('bd*2,hh(3,4),jvbass:[0 4](5,8,1)').jux(rev).stack(s('~ sd')).play());
|
||||
click('stop', () => hush());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "repl-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel/web": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
@@ -0,0 +1,10 @@
|
||||
# minimal repl
|
||||
|
||||
This folder demonstrates how to set up a minimal strudel repl using vite and vanilla JS. Run it using:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you're looking for a more feature rich alternative, have a look at the [../codemirror-repl](codemirror-repl example)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -1,6 +1,6 @@
|
||||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
||||
import { transpiler } from '@strudel.cycles/transpiler';
|
||||
import { controls, repl, evalScope } from '@strudel/core';
|
||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||
import { transpiler } from '@strudel/transpiler';
|
||||
import tune from './tune.mjs';
|
||||
|
||||
const ctx = getAudioContext();
|
||||
@@ -10,10 +10,10 @@ initAudioOnFirstClick();
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel/core'),
|
||||
import('@strudel/mini'),
|
||||
import('@strudel/webaudio'),
|
||||
import('@strudel/tonal'),
|
||||
);
|
||||
|
||||
const { evaluate } = repl({
|
||||
+6
-6
@@ -10,13 +10,13 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.0"
|
||||
"vite": "^5.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*"
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,11 @@
|
||||
# superdough demo
|
||||
|
||||
This demo shows how to use [superdough](https://www.npmjs.com/package/superdough) with [vite](https://vitejs.dev/).
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
pnpm i && cd examples/headless-repl
|
||||
pnpm dev
|
||||
# open http://localhost:5173/
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Superdough Example</title>
|
||||
<meta charset="UTF-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<button id="play">PLAAAAAAAY</button>
|
||||
<script type="module">
|
||||
import { superdough, samples, initAudioOnFirstClick, registerSynthSounds } from 'superdough';
|
||||
|
||||
const init = Promise.all([
|
||||
initAudioOnFirstClick(),
|
||||
samples('github:tidalcycles/Dirt-Samples/master'),
|
||||
registerSynthSounds(),
|
||||
]);
|
||||
|
||||
const loop = (t = 0) => {
|
||||
// superdough(value, time, duration)
|
||||
superdough({ s: 'bd', delay: 0.5 }, t);
|
||||
superdough({ note: 'g1', s: 'sawtooth', cutoff: 600, resonance: 8 }, t, 0.125);
|
||||
superdough({ note: 'g2', s: 'sawtooth', cutoff: 600, resonance: 8 }, t + 0.25, 0.125);
|
||||
superdough({ s: 'hh' }, t + 0.25);
|
||||
superdough({ s: 'sd', room: 0.5 }, t + 0.5);
|
||||
superdough({ s: 'hh' }, t + 0.75);
|
||||
};
|
||||
|
||||
document.getElementById('play').addEventListener('click', async () => {
|
||||
await init;
|
||||
let t = 0.1;
|
||||
while (t < 16) {
|
||||
loop(t++);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "superdough-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"superdough": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
export * from './packages/core/index.mjs';
|
||||
export * from './packages/csound/index.mjs';
|
||||
export * from './packages/embed/index.mjs';
|
||||
export * from './packages/eval/index.mjs';
|
||||
export * from './packages/desktopbridge/index.mjs';
|
||||
export * from './packages/midi/index.mjs';
|
||||
export * from './packages/mini/index.mjs';
|
||||
export * from './packages/osc/index.mjs';
|
||||
@@ -10,8 +10,6 @@ export * from './packages/react/index.mjs';
|
||||
export * from './packages/serial/index.mjs';
|
||||
export * from './packages/soundfonts/index.mjs';
|
||||
export * from './packages/tonal/index.mjs';
|
||||
export * from './packages/tone/index.mjs';
|
||||
export * from './packages/transpiler/index.mjs';
|
||||
export * from './packages/webaudio/index.mjs';
|
||||
export * from './packages/webdirt/index.mjs';
|
||||
export * from './packages/xen/index.mjs';
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/*
|
||||
usePostMessage.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/repl/src/usePostMessage.mjs>
|
||||
jsdoc-synonyms.js - Add support for @synonym tag
|
||||
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/midi/midi.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useCallback } from 'react';
|
||||
|
||||
function usePostMessage(listener) {
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', listener);
|
||||
return () => window.removeEventListener('message', listener);
|
||||
}, [listener]);
|
||||
return useCallback((data) => window.postMessage(data, '*'), []);
|
||||
function defineTags(dictionary) {
|
||||
dictionary.defineTag('synonyms', {
|
||||
mustHaveValue: true,
|
||||
onTagged: function (doclet, tag) {
|
||||
doclet.synonyms_text = tag.value;
|
||||
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default usePostMessage;
|
||||
module.exports = { defineTags: defineTags };
|
||||
@@ -3,7 +3,7 @@
|
||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||
},
|
||||
"plugins": ["plugins/markdown"],
|
||||
"plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"],
|
||||
"opts": {
|
||||
"destination": "./out/",
|
||||
"recurse": true
|
||||
@@ -65,7 +65,7 @@ async function getUndocumented(path, docs) {
|
||||
}
|
||||
|
||||
// read doc.json file
|
||||
const { docs } = JSON.parse(await readFile(resolve(__dirname, 'doc.json'), 'utf8'));
|
||||
const { docs } = JSON.parse(await readFile(resolve(__dirname, '..', 'doc.json'), 'utf8'));
|
||||
|
||||
const paths = dependencyTree.toList({
|
||||
filename: 'index.mjs',
|
||||
@@ -76,7 +76,9 @@ const paths = dependencyTree.toList({
|
||||
// const paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel));
|
||||
|
||||
const undocumented = Object.fromEntries(
|
||||
await Promise.all(paths.map(async (path) => [path, await getUndocumented(path, docs)])),
|
||||
await Promise.all(
|
||||
paths.map(async (path) => [path.replace(resolve(__dirname, '..'), ''), await getUndocumented(path, docs)]),
|
||||
),
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(undocumented, null, 2));
|
||||
+2
-4
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"packages": ["packages/*"],
|
||||
"version": "independent",
|
||||
"npmClient": "pnpm",
|
||||
"useWorkspaces": true
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ If you want to automatically deploy your site on push, go to `deploy.yml` and ch
|
||||
## running locally
|
||||
|
||||
- install dependencies with `npm run setup`
|
||||
- run dev server with `npm run repl` and open `http://localhost:3000/strudel/swatch/`
|
||||
- run dev server with `npm run repl` and open `http://localhost:4321/strudel/swatch/`
|
||||
|
||||
## tests fail?
|
||||
|
||||
|
||||
+23
-26
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@strudel.cycles/monorepo",
|
||||
"name": "@strudel/monorepo",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"description": "Port of tidalcycles to javascript",
|
||||
@@ -18,13 +18,12 @@
|
||||
"build": "npm run prebuild && cd website && npm run build",
|
||||
"preview": "cd website && npm run preview",
|
||||
"osc": "cd packages/osc && npm run server",
|
||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
||||
"jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json",
|
||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc/jsdoc.config.json",
|
||||
"lint": "eslint . --ext mjs,js --quiet",
|
||||
"codeformat": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"report-undocumented": "npm run jsdoc-json && node undocumented.mjs > undocumented.json",
|
||||
"report-undocumented": "npm run jsdoc-json && node jsdoc/undocumented.mjs > undocumented.json",
|
||||
"check": "npm run format-check && npm run lint && npm run test",
|
||||
"iclc": "cd paper && pandoc --template=pandoc/iclc.html --citeproc --number-sections iclc2023.md -o iclc2023.html && pandoc --template=pandoc/iclc.latex --citeproc --number-sections iclc2023.md -o iclc2023.pdf"
|
||||
},
|
||||
@@ -44,31 +43,29 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"homepage": "https://strudel.cc",
|
||||
"dependencies": {
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/mini": "workspace:*",
|
||||
"@strudel.cycles/tonal": "workspace:*",
|
||||
"@strudel.cycles/transpiler": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*",
|
||||
"@strudel.cycles/xen": "workspace:*",
|
||||
"acorn": "^8.8.1",
|
||||
"dependency-tree": "^9.0.0",
|
||||
"vitest": "^0.25.7"
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/mini": "workspace:*",
|
||||
"@strudel/tonal": "workspace:*",
|
||||
"@strudel/transpiler": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*",
|
||||
"@strudel/xen": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/ui": "^0.25.7",
|
||||
"c8": "^7.12.0",
|
||||
"canvas": "^2.11.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"dependency-tree": "^10.0.9",
|
||||
"@tauri-apps/cli": "^1.5.9",
|
||||
"@vitest/ui": "^1.1.0",
|
||||
"canvas": "^2.11.2",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"events": "^3.3.0",
|
||||
"gh-pages": "^4.0.0",
|
||||
"jsdoc": "^3.6.10",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsdoc-json": "^2.0.2",
|
||||
"jsdoc-to-markdown": "^7.1.1",
|
||||
"lerna": "^6.5.1",
|
||||
"prettier": "^2.8.1",
|
||||
"rollup-plugin-visualizer": "^5.8.1"
|
||||
"jsdoc-to-markdown": "^8.0.0",
|
||||
"lerna": "^8.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-11
@@ -1,15 +1,5 @@
|
||||
# Packages
|
||||
|
||||
Each folder represents one of the @strudel.cycles/* packages [published to npm](https://www.npmjs.com/org/strudel.cycles).
|
||||
Each folder represents one of the @strudel/* packages [published to npm](https://www.npmjs.com/org/strudel).
|
||||
|
||||
To understand how those pieces connect, refer to the [Technical Manual](https://github.com/tidalcycles/strudel/wiki/Technical-Manual) or the individual READMEs.
|
||||
|
||||
This is a graphical view of all the packages: [full screen](https://raw.githubusercontent.com/tidalcycles/strudel/main/dependencies.svg)
|
||||
|
||||

|
||||
|
||||
Generated with
|
||||
|
||||
```sh
|
||||
npx depcruise --include-only "^packages" -X "node_modules" --output-type dot packages | dot -T svg > dependencygraph.svg
|
||||
```
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/codemirror
|
||||
|
||||
This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
|
||||
+46
-39
@@ -1,5 +1,13 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import jsdoc from '../../../../doc.json';
|
||||
import jsdoc from '../../doc.json';
|
||||
// import { javascriptLanguage } from '@codemirror/lang-javascript';
|
||||
import { autocompletion } from '@codemirror/autocomplete';
|
||||
import { h } from './html';
|
||||
|
||||
function plaintext(str) {
|
||||
const div = document.createElement('div');
|
||||
div.innerText = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
const getInnerText = (html) => {
|
||||
@@ -8,36 +16,32 @@ const getInnerText = (html) => {
|
||||
return div.textContent || div.innerText || '';
|
||||
};
|
||||
|
||||
export function Autocomplete({ doc }) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
||||
<ul>
|
||||
{doc.params?.map(({ name, type, description }, i) => (
|
||||
<li key={i}>
|
||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
{doc.examples?.map((example, i) => (
|
||||
<div key={i}>
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
export function Autocomplete({ doc, label }) {
|
||||
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||
${doc.description}
|
||||
<ul>
|
||||
${doc.params?.map(
|
||||
({ name, type, description }) =>
|
||||
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||
)}
|
||||
</ul>
|
||||
<div>
|
||||
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||
</div>
|
||||
</div>`[0];
|
||||
/*
|
||||
<pre
|
||||
className="cursor-pointer"
|
||||
onMouseDown={(e) => {
|
||||
console.log('ola!');
|
||||
navigator.clipboard.writeText(example);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{example}
|
||||
</pre>
|
||||
*/
|
||||
}
|
||||
|
||||
const jsdocCompletions = jsdoc.docs
|
||||
@@ -52,13 +56,7 @@ const jsdocCompletions = jsdoc.docs
|
||||
.map((doc) /*: Completion */ => ({
|
||||
label: getDocLabel(doc),
|
||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||
info: () => {
|
||||
const node = document.createElement('div');
|
||||
// if Autocomplete is non-interactive, it could also be rendered at build time..
|
||||
// .. using renderToStaticMarkup
|
||||
createRoot(node).render(<Autocomplete doc={doc} />);
|
||||
return node;
|
||||
},
|
||||
info: () => Autocomplete({ doc }),
|
||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
||||
}));
|
||||
|
||||
@@ -75,3 +73,12 @@ export const strudelAutocomplete = (context /* : CompletionContext */) => {
|
||||
], */
|
||||
};
|
||||
};
|
||||
|
||||
export function isAutoCompletionEnabled(on) {
|
||||
return on
|
||||
? [
|
||||
autocompletion({ override: [strudelAutocomplete] }),
|
||||
//javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
|
||||
]
|
||||
: []; // autocompletion({ override: [] })
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
import { closeBrackets } from '@codemirror/autocomplete';
|
||||
// import { search, highlightSelectionMatches } from '@codemirror/search';
|
||||
import { history } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { Compartment, EditorState, Prec } from '@codemirror/state';
|
||||
import {
|
||||
EditorView,
|
||||
highlightActiveLineGutter,
|
||||
highlightActiveLine,
|
||||
keymap,
|
||||
lineNumbers,
|
||||
drawSelection,
|
||||
} from '@codemirror/view';
|
||||
import { Pattern, Drawer, repl, cleanupDraw } from '@strudel/core';
|
||||
import { isAutoCompletionEnabled } from './autocomplete.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { flash, isFlashEnabled } from './flash.mjs';
|
||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||
import { keybindings } from './keybindings.mjs';
|
||||
import { initTheme, activateTheme, theme } from './themes.mjs';
|
||||
import { updateWidgets, sliderPlugin } from './slider.mjs';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
|
||||
const extensions = {
|
||||
isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
|
||||
isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
|
||||
theme,
|
||||
isAutoCompletionEnabled,
|
||||
isTooltipEnabled,
|
||||
isPatternHighlightingEnabled,
|
||||
isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
|
||||
isFlashEnabled,
|
||||
keybindings,
|
||||
};
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
|
||||
export const defaultSettings = {
|
||||
keybindings: 'codemirror',
|
||||
isLineNumbersDisplayed: true,
|
||||
isActiveLineHighlighted: false,
|
||||
isAutoCompletionEnabled: false,
|
||||
isPatternHighlightingEnabled: true,
|
||||
isFlashEnabled: true,
|
||||
isTooltipEnabled: false,
|
||||
isLineWrappingEnabled: false,
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
};
|
||||
|
||||
export const codemirrorSettings = persistentAtom('codemirror-settings', defaultSettings, {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse,
|
||||
});
|
||||
|
||||
// https://codemirror.net/docs/guide/
|
||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root }) {
|
||||
const settings = codemirrorSettings.get();
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
);
|
||||
initTheme(settings.theme);
|
||||
let state = EditorState.create({
|
||||
doc: initialCode,
|
||||
extensions: [
|
||||
/* search(),
|
||||
highlightSelectionMatches(), */
|
||||
...initialSettings,
|
||||
javascript(),
|
||||
sliderPlugin,
|
||||
// indentOnInput(), // works without. already brought with javascript extension?
|
||||
// bracketMatching(), // does not do anything
|
||||
closeBrackets(),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
history(),
|
||||
EditorView.updateListener.of((v) => onChange(v)),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Ctrl-Enter',
|
||||
run: () => onEvaluate?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-Enter',
|
||||
run: () => onEvaluate?.(),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-.',
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-.',
|
||||
run: (_, e) => {
|
||||
e.preventDefault();
|
||||
onStop?.();
|
||||
},
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
},
|
||||
{
|
||||
key: 'Ctrl-Shift-Enter',
|
||||
run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
|
||||
}, */
|
||||
]),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
return new EditorView({
|
||||
state,
|
||||
parent: root,
|
||||
});
|
||||
}
|
||||
|
||||
export class StrudelMirror {
|
||||
constructor(options) {
|
||||
const {
|
||||
root,
|
||||
id,
|
||||
initialCode = '',
|
||||
onDraw,
|
||||
drawTime = [0, 0],
|
||||
autodraw,
|
||||
prebake,
|
||||
bgFill = true,
|
||||
...replOptions
|
||||
} = options;
|
||||
this.code = initialCode;
|
||||
this.root = root;
|
||||
this.miniLocations = [];
|
||||
this.widgets = [];
|
||||
this.painters = [];
|
||||
this.drawTime = drawTime;
|
||||
this.onDraw = onDraw;
|
||||
const self = this;
|
||||
this.id = id || s4();
|
||||
|
||||
this.drawer = new Drawer((haps, time) => {
|
||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||
this.highlight(currentFrame, time);
|
||||
this.onDraw?.(haps, time, currentFrame, this.painters);
|
||||
}, drawTime);
|
||||
|
||||
// this approach does not work with multiple repls on screen
|
||||
// TODO: refactor onPaint usages + find fix, maybe remove painters here?
|
||||
Pattern.prototype.onPaint = function (onPaint) {
|
||||
self.painters.push(onPaint);
|
||||
return this;
|
||||
};
|
||||
|
||||
this.prebaked = prebake();
|
||||
autodraw && this.drawFirstFrame();
|
||||
|
||||
this.repl = repl({
|
||||
...replOptions,
|
||||
onToggle: (started) => {
|
||||
replOptions?.onToggle?.(started);
|
||||
if (started) {
|
||||
this.adjustDrawTime();
|
||||
this.drawer.start(this.repl.scheduler);
|
||||
// stop other repls when this one is started
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('start-repl', {
|
||||
detail: this.id,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.drawer.stop();
|
||||
updateMiniLocations(this.editor, []);
|
||||
cleanupDraw(false);
|
||||
}
|
||||
},
|
||||
beforeEval: async () => {
|
||||
cleanupDraw();
|
||||
this.painters = [];
|
||||
await this.prebaked;
|
||||
await replOptions?.beforeEval?.();
|
||||
},
|
||||
afterEval: (options) => {
|
||||
// remember for when highlighting is toggled on
|
||||
this.miniLocations = options.meta?.miniLocations;
|
||||
this.widgets = options.meta?.widgets;
|
||||
updateWidgets(this.editor, this.widgets);
|
||||
updateMiniLocations(this.editor, this.miniLocations);
|
||||
replOptions?.afterEval?.(options);
|
||||
this.adjustDrawTime();
|
||||
this.drawer.invalidate();
|
||||
},
|
||||
});
|
||||
this.editor = initEditor({
|
||||
root,
|
||||
initialCode,
|
||||
onChange: (v) => {
|
||||
if (v.docChanged) {
|
||||
this.code = v.state.doc.toString();
|
||||
this.repl.setCode?.(this.code);
|
||||
}
|
||||
},
|
||||
onEvaluate: () => this.evaluate(),
|
||||
onStop: () => this.stop(),
|
||||
});
|
||||
const cmEditor = this.root.querySelector('.cm-editor');
|
||||
if (cmEditor) {
|
||||
this.root.style.display = 'block';
|
||||
if (bgFill) {
|
||||
this.root.style.backgroundColor = 'var(--background)';
|
||||
}
|
||||
cmEditor.style.backgroundColor = 'transparent';
|
||||
}
|
||||
const settings = codemirrorSettings.get();
|
||||
this.setFontSize(settings.fontSize);
|
||||
this.setFontFamily(settings.fontFamily);
|
||||
|
||||
// stop this repl when another repl is started
|
||||
this.onStartRepl = (e) => {
|
||||
if (e.detail !== this.id) {
|
||||
this.stop();
|
||||
}
|
||||
};
|
||||
document.addEventListener('start-repl', this.onStartRepl);
|
||||
}
|
||||
// adjusts draw time depending on if there are painters
|
||||
adjustDrawTime() {
|
||||
// when no painters are set, [0,0] is enough (just highlighting)
|
||||
this.drawer.setDrawTime(this.painters.length ? this.drawTime : [0, 0]);
|
||||
}
|
||||
async drawFirstFrame() {
|
||||
if (!this.onDraw) {
|
||||
return;
|
||||
}
|
||||
// draw first frame instantly
|
||||
await this.prebaked;
|
||||
try {
|
||||
await this.repl.evaluate(this.code, false);
|
||||
this.drawer.invalidate(this.repl.scheduler, -0.001);
|
||||
// draw at -0.001 to avoid haps at 0 to be visualized as active
|
||||
this.onDraw?.(this.drawer.visibleHaps, -0.001, [], this.painters);
|
||||
} catch (err) {
|
||||
console.warn('first frame could not be painted');
|
||||
}
|
||||
}
|
||||
async evaluate() {
|
||||
this.flash();
|
||||
await this.repl.evaluate(this.code);
|
||||
}
|
||||
async stop() {
|
||||
this.repl.scheduler.stop();
|
||||
}
|
||||
async toggle() {
|
||||
if (this.repl.scheduler.started) {
|
||||
this.repl.stop();
|
||||
} else {
|
||||
this.evaluate();
|
||||
}
|
||||
}
|
||||
flash(ms) {
|
||||
flash(this.editor, ms);
|
||||
}
|
||||
highlight(haps, time) {
|
||||
highlightMiniLocations(this.editor, time, haps);
|
||||
}
|
||||
setFontSize(size) {
|
||||
this.root.style.fontSize = size + 'px';
|
||||
}
|
||||
setFontFamily(family) {
|
||||
this.root.style.fontFamily = family;
|
||||
const scroller = this.root.querySelector('.cm-scroller');
|
||||
if (scroller) {
|
||||
scroller.style.fontFamily = family;
|
||||
}
|
||||
}
|
||||
reconfigureExtension(key, value) {
|
||||
if (!extensions[key]) {
|
||||
console.warn(`extension ${key} is not known`);
|
||||
return;
|
||||
}
|
||||
value = parseBooleans(value);
|
||||
const newValue = extensions[key](value, this);
|
||||
this.editor.dispatch({
|
||||
effects: compartments[key].reconfigure(newValue),
|
||||
});
|
||||
if (key === 'theme') {
|
||||
activateTheme(value);
|
||||
}
|
||||
}
|
||||
setLineWrappingEnabled(enabled) {
|
||||
this.reconfigureExtension('isLineWrappingEnabled', enabled);
|
||||
}
|
||||
setLineNumbersDisplayed(enabled) {
|
||||
this.reconfigureExtension('isLineNumbersDisplayed', enabled);
|
||||
}
|
||||
setTheme(theme) {
|
||||
this.reconfigureExtension('theme', theme);
|
||||
}
|
||||
setAutocompletionEnabled(enabled) {
|
||||
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
|
||||
}
|
||||
updateSettings(settings) {
|
||||
this.setFontSize(settings.fontSize);
|
||||
this.setFontFamily(settings.fontFamily);
|
||||
for (let key in extensions) {
|
||||
this.reconfigureExtension(key, settings[key]);
|
||||
}
|
||||
const updated = { ...codemirrorSettings.get(), ...settings };
|
||||
codemirrorSettings.set(updated);
|
||||
}
|
||||
changeSetting(key, value) {
|
||||
if (extensions[key]) {
|
||||
this.reconfigureExtension(key, value);
|
||||
return;
|
||||
} else if (key === 'fontFamily') {
|
||||
this.setFontFamily(value);
|
||||
} else if (key === 'fontSize') {
|
||||
this.setFontSize(value);
|
||||
}
|
||||
}
|
||||
setCode(code) {
|
||||
const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
|
||||
this.editor.dispatch({ changes });
|
||||
}
|
||||
clear() {
|
||||
this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
|
||||
}
|
||||
}
|
||||
|
||||
function parseBooleans(value) {
|
||||
return { true: true, false: false }[value] ?? value;
|
||||
}
|
||||
|
||||
// helper function to generate repl ids
|
||||
function s4() {
|
||||
return Math.floor((1 + Math.random()) * 0x10000)
|
||||
.toString(16)
|
||||
.substring(1);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setFlash = StateEffect.define();
|
||||
export const flashField = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(flash, tr) {
|
||||
try {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setFlash)) {
|
||||
if (e.value && tr.newDoc.length > 0) {
|
||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||
} else {
|
||||
flash = Decoration.set([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return flash;
|
||||
} catch (err) {
|
||||
console.warn('flash error', err);
|
||||
return flash;
|
||||
}
|
||||
},
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
export const flash = (view, ms = 200) => {
|
||||
view.dispatch({ effects: setFlash.of(true) });
|
||||
setTimeout(() => {
|
||||
view.dispatch({ effects: setFlash.of(false) });
|
||||
}, ms);
|
||||
};
|
||||
|
||||
export const isFlashEnabled = (on) => (on ? flashField : []);
|
||||
@@ -0,0 +1,137 @@
|
||||
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||
import { Decoration, EditorView } from '@codemirror/view';
|
||||
|
||||
export const setMiniLocations = StateEffect.define();
|
||||
export const showMiniLocations = StateEffect.define();
|
||||
export const updateMiniLocations = (view, locations) => {
|
||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
||||
};
|
||||
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||
};
|
||||
|
||||
const miniLocations = StateField.define({
|
||||
create() {
|
||||
return Decoration.none;
|
||||
},
|
||||
update(locations, tr) {
|
||||
if (tr.docChanged) {
|
||||
locations = locations.map(tr.changes);
|
||||
}
|
||||
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setMiniLocations)) {
|
||||
// this is called on eval, with the mini locations obtained from the transpiler
|
||||
// codemirror will automatically remap the marks when the document is edited
|
||||
// create a mark for each mini location, adding the range to the spec to find it later
|
||||
const marks = e.value
|
||||
.filter(([from]) => from < tr.newDoc.length)
|
||||
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||
.map(
|
||||
(range) =>
|
||||
Decoration.mark({
|
||||
id: range.join(':'),
|
||||
// this green is only to verify that the decoration moves when the document is edited
|
||||
// it will be removed later, so the mark is not visible by default
|
||||
attributes: { style: `background-color: #00CA2880` },
|
||||
}).range(...range), // -> Decoration
|
||||
);
|
||||
|
||||
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
||||
}
|
||||
}
|
||||
|
||||
return locations;
|
||||
},
|
||||
});
|
||||
|
||||
const visibleMiniLocations = StateField.define({
|
||||
create() {
|
||||
return { atTime: 0, haps: new Map() };
|
||||
},
|
||||
update(visible, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(showMiniLocations)) {
|
||||
// this is called every frame to show the locations that are currently active
|
||||
// we can NOT create new marks because the context.locations haven't changed since eval time
|
||||
// this is why we need to find a way to update the existing decorations, showing the ones that have an active range
|
||||
const haps = new Map();
|
||||
for (let hap of e.value.haps) {
|
||||
if (!hap.context?.locations || !hap.whole) {
|
||||
continue;
|
||||
}
|
||||
for (let { start, end } of hap.context.locations) {
|
||||
let id = `${start}:${end}`;
|
||||
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
||||
haps.set(id, hap);
|
||||
}
|
||||
}
|
||||
}
|
||||
visible = { atTime: e.value.atTime, haps };
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
},
|
||||
});
|
||||
|
||||
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
||||
const iterator = state.field(miniLocations).iter();
|
||||
const { haps } = state.field(visibleMiniLocations);
|
||||
const builder = new RangeSetBuilder();
|
||||
|
||||
while (iterator.value) {
|
||||
const {
|
||||
from,
|
||||
to,
|
||||
value: {
|
||||
spec: { id },
|
||||
},
|
||||
} = iterator;
|
||||
|
||||
if (haps.has(id)) {
|
||||
const hap = haps.get(id);
|
||||
const color = hap.context.color ?? 'var(--foreground)';
|
||||
// Get explicit channels for color values
|
||||
/*
|
||||
const swatch = document.createElement('div');
|
||||
swatch.style.color = color;
|
||||
document.body.appendChild(swatch);
|
||||
let channels = getComputedStyle(swatch)
|
||||
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||
.slice(1)
|
||||
.map((c) => parseFloat(c || 1));
|
||||
document.body.removeChild(swatch);
|
||||
|
||||
// Get percentage of event
|
||||
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||
channels[3] *= percent;
|
||||
*/
|
||||
|
||||
builder.add(
|
||||
from,
|
||||
to,
|
||||
Decoration.mark({
|
||||
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||
attributes: { style: `outline: solid 2px ${color}` },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
});
|
||||
|
||||
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||
|
||||
export const isPatternHighlightingEnabled = (on, config) => {
|
||||
on &&
|
||||
config &&
|
||||
setTimeout(() => {
|
||||
updateMiniLocations(config.editor, config.miniLocations);
|
||||
}, 100);
|
||||
return on ? highlightExtension : [];
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
const parser = typeof DOMParser !== 'undefined' ? new DOMParser() : null;
|
||||
export let html = (string) => {
|
||||
return parser?.parseFromString(string, 'text/html').querySelectorAll('*');
|
||||
};
|
||||
let parseChunk = (chunk) => {
|
||||
if (Array.isArray(chunk)) return chunk.flat().join('');
|
||||
if (chunk === undefined) return '';
|
||||
return chunk;
|
||||
};
|
||||
export let h = (strings, ...vars) => {
|
||||
let string = '';
|
||||
for (let i in strings) {
|
||||
string += parseChunk(strings[i]);
|
||||
string += parseChunk(vars[i]);
|
||||
}
|
||||
return html(string);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './codemirror.mjs';
|
||||
export * from './highlight.mjs';
|
||||
export * from './flash.mjs';
|
||||
export * from './slider.mjs';
|
||||
export * from './themes.mjs';
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Prec } from '@codemirror/state';
|
||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { defaultKeymap, historyKeymap } from '@codemirror/commands';
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
constructor() {}
|
||||
},
|
||||
{
|
||||
provide: () => {
|
||||
return Prec.highest(keymap.of([...vscodeKeymap]));
|
||||
},
|
||||
},
|
||||
);
|
||||
const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
|
||||
|
||||
const keymaps = {
|
||||
vim,
|
||||
emacs,
|
||||
vscode: vscodeExtension,
|
||||
};
|
||||
|
||||
export function keybindings(name) {
|
||||
const active = keymaps[name];
|
||||
return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
|
||||
// keymap.of(searchKeymap),
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@strudel/codemirror",
|
||||
"version": "1.0.1",
|
||||
"description": "Codemirror Extensions for Strudel",
|
||||
"main": "index.mjs",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/tidalcycles/strudel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"tidalcycles",
|
||||
"strudel",
|
||||
"pattern",
|
||||
"livecoding",
|
||||
"algorave"
|
||||
],
|
||||
"author": "Felix Roos <flix91@gmail.com>",
|
||||
"contributors": [
|
||||
"Alex McLean <alex@slab.org>"
|
||||
],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.11.1",
|
||||
"@codemirror/commands": "^6.3.3",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/language": "^6.10.0",
|
||||
"@codemirror/search": "^6.5.5",
|
||||
"@codemirror/state": "^6.4.0",
|
||||
"@codemirror/view": "^6.23.0",
|
||||
"@lezer/highlight": "^1.2.0",
|
||||
"@nanostores/persistent": "^0.9.1",
|
||||
"@replit/codemirror-emacs": "^6.0.1",
|
||||
"@replit/codemirror-vim": "^6.1.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@uiw/codemirror-themes": "^4.21.21",
|
||||
"@uiw/codemirror-themes-all": "^4.21.21",
|
||||
"nanostores": "^0.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { ref, pure } from '@strudel/core';
|
||||
import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
|
||||
import { StateEffect, StateField } from '@codemirror/state';
|
||||
|
||||
export let sliderValues = {};
|
||||
const getSliderID = (from) => `slider_${from}`;
|
||||
|
||||
export class SliderWidget extends WidgetType {
|
||||
constructor(value, min, max, from, to, step, view) {
|
||||
super();
|
||||
this.value = value;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.from = from;
|
||||
this.originalFrom = from;
|
||||
this.to = to;
|
||||
this.step = step;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
eq() {
|
||||
return false;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
let wrap = document.createElement('span');
|
||||
wrap.setAttribute('aria-hidden', 'true');
|
||||
wrap.className = 'cm-slider'; // inline-flex items-center
|
||||
let slider = wrap.appendChild(document.createElement('input'));
|
||||
slider.type = 'range';
|
||||
slider.min = this.min;
|
||||
slider.max = this.max;
|
||||
slider.step = this.step ?? (this.max - this.min) / 1000;
|
||||
slider.originalValue = this.value;
|
||||
// to make sure the code stays in sync, let's save the original value
|
||||
// becuase .value automatically clamps values so it'll desync with the code
|
||||
slider.value = slider.originalValue;
|
||||
slider.from = this.from;
|
||||
slider.originalFrom = this.originalFrom;
|
||||
slider.to = this.to;
|
||||
slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
|
||||
this.slider = slider;
|
||||
slider.addEventListener('input', (e) => {
|
||||
const next = e.target.value;
|
||||
let insert = next;
|
||||
//let insert = next.toFixed(2);
|
||||
const to = slider.from + slider.originalValue.length;
|
||||
let change = { from: slider.from, to, insert };
|
||||
slider.originalValue = insert;
|
||||
slider.value = insert;
|
||||
this.view.dispatch({ changes: change });
|
||||
const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
|
||||
window.postMessage({ type: 'cm-slider', value: Number(next), id });
|
||||
});
|
||||
return wrap;
|
||||
}
|
||||
|
||||
ignoreEvent(e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const setWidgets = StateEffect.define();
|
||||
|
||||
export const updateWidgets = (view, widgets) => {
|
||||
view.dispatch({ effects: setWidgets.of(widgets) });
|
||||
};
|
||||
|
||||
function getWidgets(widgetConfigs, view) {
|
||||
return widgetConfigs.map(({ from, to, value, min, max, step }) => {
|
||||
return Decoration.widget({
|
||||
widget: new SliderWidget(value, min, max, from, to, step, view),
|
||||
side: 0,
|
||||
}).range(from /* , to */);
|
||||
});
|
||||
}
|
||||
|
||||
export const sliderPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations; //: DecorationSet
|
||||
|
||||
constructor(view /* : EditorView */) {
|
||||
this.decorations = Decoration.set([]);
|
||||
}
|
||||
|
||||
update(update /* : ViewUpdate */) {
|
||||
update.transactions.forEach((tr) => {
|
||||
if (tr.docChanged) {
|
||||
this.decorations = this.decorations.map(tr.changes);
|
||||
const iterator = this.decorations.iter();
|
||||
while (iterator.value) {
|
||||
// when the widgets are moved, we need to tell the dom node the current position
|
||||
// this is important because the updateSliderValue function has to work with the dom node
|
||||
if (iterator.value?.widget?.slider) {
|
||||
iterator.value.widget.slider.from = iterator.from;
|
||||
iterator.value.widget.slider.to = iterator.to;
|
||||
}
|
||||
iterator.next();
|
||||
}
|
||||
}
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setWidgets)) {
|
||||
this.decorations = Decoration.set(getWidgets(e.value, update.view));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays a slider widget to allow the user manipulate a value
|
||||
*
|
||||
* @name slider
|
||||
* @param {number} value Initial value
|
||||
* @param {number} min Minimum value - optional, defaults to 0
|
||||
* @param {number} max Maximum value - optional, defaults to 1
|
||||
* @param {number} step Step size - optional
|
||||
*/
|
||||
export let slider = (value) => {
|
||||
console.warn('slider will only work when the transpiler is used... passing value as is');
|
||||
return pure(value);
|
||||
};
|
||||
// function transpiled from slider = (value, min, max)
|
||||
export let sliderWithID = (id, value, min, max) => {
|
||||
sliderValues[id] = value; // sync state at eval time (code -> state)
|
||||
return ref(() => sliderValues[id]); // use state at query time
|
||||
};
|
||||
// update state when sliders are moved
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data.type === 'cm-slider') {
|
||||
if (sliderValues[e.data.id] !== undefined) {
|
||||
// update state when slider is moved
|
||||
sliderValues[e.data.id] = e.data.value;
|
||||
} else {
|
||||
console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+92
-36
@@ -30,18 +30,20 @@ import {
|
||||
xcodeLight,
|
||||
} from '@uiw/codemirror-themes-all';
|
||||
|
||||
import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
|
||||
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
|
||||
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
|
||||
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
|
||||
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
|
||||
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal';
|
||||
import strudelTheme from './themes/strudel-theme';
|
||||
import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
|
||||
import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
|
||||
import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
|
||||
import teletext, { settings as teletextSettings } from './themes/teletext';
|
||||
import algoboy, { settings as algoboySettings } from './themes/algoboy';
|
||||
import terminal, { settings as terminalSettings } from './themes/terminal';
|
||||
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
bluescreen,
|
||||
blackscreen,
|
||||
whitescreen,
|
||||
teletext,
|
||||
algoboy,
|
||||
terminal,
|
||||
abcdef,
|
||||
@@ -80,7 +82,7 @@ export const themes = {
|
||||
export const settings = {
|
||||
strudelTheme: {
|
||||
background: '#222',
|
||||
lineBackground: '#22222250',
|
||||
lineBackground: '#22222299',
|
||||
foreground: '#fff',
|
||||
// foreground: '#75baff',
|
||||
caret: '#ffcc00',
|
||||
@@ -95,11 +97,12 @@ export const settings = {
|
||||
bluescreen: bluescreenSettings,
|
||||
blackscreen: blackscreenSettings,
|
||||
whitescreen: whitescreenSettings,
|
||||
teletext: teletextSettings,
|
||||
algoboy: algoboySettings,
|
||||
terminal: terminalSettings,
|
||||
abcdef: {
|
||||
background: '#0f0f0f',
|
||||
lineBackground: '#0f0f0f50',
|
||||
lineBackground: '#0f0f0f99',
|
||||
foreground: '#defdef',
|
||||
caret: '#00FF00',
|
||||
selection: '#515151',
|
||||
@@ -110,7 +113,7 @@ export const settings = {
|
||||
},
|
||||
androidstudio: {
|
||||
background: '#282b2e',
|
||||
lineBackground: '#282b2e50',
|
||||
lineBackground: '#282b2e99',
|
||||
foreground: '#a9b7c6',
|
||||
caret: '#00FF00',
|
||||
selection: '#343739',
|
||||
@@ -119,7 +122,7 @@ export const settings = {
|
||||
},
|
||||
atomone: {
|
||||
background: '#272C35',
|
||||
lineBackground: '#272C3550',
|
||||
lineBackground: '#272C3599',
|
||||
foreground: '#9d9b97',
|
||||
caret: '#797977',
|
||||
selection: '#ffffff30',
|
||||
@@ -131,7 +134,7 @@ export const settings = {
|
||||
},
|
||||
aura: {
|
||||
background: '#21202e',
|
||||
lineBackground: '#21202e50',
|
||||
lineBackground: '#21202e99',
|
||||
foreground: '#edecee',
|
||||
caret: '#a277ff',
|
||||
selection: '#3d375e7f',
|
||||
@@ -144,7 +147,7 @@ export const settings = {
|
||||
bbedit: {
|
||||
light: true,
|
||||
background: '#FFFFFF',
|
||||
lineBackground: '#FFFFFF50',
|
||||
lineBackground: '#FFFFFF99',
|
||||
foreground: '#000000',
|
||||
caret: '#FBAC52',
|
||||
selection: '#FFD420',
|
||||
@@ -156,7 +159,7 @@ export const settings = {
|
||||
},
|
||||
bespin: {
|
||||
background: '#28211c',
|
||||
lineBackground: '#28211c50',
|
||||
lineBackground: '#28211c99',
|
||||
foreground: '#9d9b97',
|
||||
caret: '#797977',
|
||||
selection: '#36312e',
|
||||
@@ -167,7 +170,7 @@ export const settings = {
|
||||
},
|
||||
darcula: {
|
||||
background: '#2B2B2B',
|
||||
lineBackground: '#2B2B2B50',
|
||||
lineBackground: '#2B2B2B99',
|
||||
foreground: '#f8f8f2',
|
||||
caret: '#FFFFFF',
|
||||
selection: 'rgba(255, 255, 255, 0.1)',
|
||||
@@ -179,7 +182,7 @@ export const settings = {
|
||||
},
|
||||
dracula: {
|
||||
background: '#282a36',
|
||||
lineBackground: '#282a3650',
|
||||
lineBackground: '#282a3699',
|
||||
foreground: '#f8f8f2',
|
||||
caret: '#f8f8f0',
|
||||
selection: 'rgba(255, 255, 255, 0.1)',
|
||||
@@ -192,7 +195,7 @@ export const settings = {
|
||||
duotoneLight: {
|
||||
light: true,
|
||||
background: '#faf8f5',
|
||||
lineBackground: '#faf8f550',
|
||||
lineBackground: '#faf8f599',
|
||||
foreground: '#b29762',
|
||||
caret: '#93abdc',
|
||||
selection: '#e3dcce',
|
||||
@@ -204,7 +207,7 @@ export const settings = {
|
||||
},
|
||||
duotoneDark: {
|
||||
background: '#2a2734',
|
||||
lineBackground: '#2a273450',
|
||||
lineBackground: '#2a273499',
|
||||
foreground: '#6c6783',
|
||||
caret: '#ffad5c',
|
||||
selection: 'rgba(255, 255, 255, 0.1)',
|
||||
@@ -215,7 +218,7 @@ export const settings = {
|
||||
eclipse: {
|
||||
light: true,
|
||||
background: '#fff',
|
||||
lineBackground: '#ffffff50',
|
||||
lineBackground: '#ffffff99',
|
||||
foreground: '#000',
|
||||
caret: '#FFFFFF',
|
||||
selection: '#d7d4f0',
|
||||
@@ -228,7 +231,7 @@ export const settings = {
|
||||
githubLight: {
|
||||
light: true,
|
||||
background: '#fff',
|
||||
lineBackground: '#ffffff50',
|
||||
lineBackground: '#ffffff99',
|
||||
foreground: '#24292e',
|
||||
selection: '#BBDFFF',
|
||||
selectionMatch: '#BBDFFF',
|
||||
@@ -237,7 +240,7 @@ export const settings = {
|
||||
},
|
||||
githubDark: {
|
||||
background: '#0d1117',
|
||||
lineBackground: '#0d111750',
|
||||
lineBackground: '#0d111799',
|
||||
foreground: '#c9d1d9',
|
||||
caret: '#c9d1d9',
|
||||
selection: '#003d73',
|
||||
@@ -246,7 +249,7 @@ export const settings = {
|
||||
},
|
||||
gruvboxDark: {
|
||||
background: '#282828',
|
||||
lineBackground: '#28282850',
|
||||
lineBackground: '#28282899',
|
||||
foreground: '#ebdbb2',
|
||||
caret: '#ebdbb2',
|
||||
selection: '#bdae93',
|
||||
@@ -258,7 +261,7 @@ export const settings = {
|
||||
gruvboxLight: {
|
||||
light: true,
|
||||
background: '#fbf1c7',
|
||||
lineBackground: '#fbf1c750',
|
||||
lineBackground: '#fbf1c799',
|
||||
foreground: '#3c3836',
|
||||
caret: '#af3a03',
|
||||
selection: '#ebdbb2',
|
||||
@@ -270,7 +273,7 @@ export const settings = {
|
||||
},
|
||||
materialDark: {
|
||||
background: '#2e3235',
|
||||
lineBackground: '#2e323550',
|
||||
lineBackground: '#2e323599',
|
||||
foreground: '#bdbdbd',
|
||||
caret: '#a0a4ae',
|
||||
selection: '#d7d4f0',
|
||||
@@ -283,7 +286,7 @@ export const settings = {
|
||||
materialLight: {
|
||||
light: true,
|
||||
background: '#FAFAFA',
|
||||
lineBackground: '#FAFAFA50',
|
||||
lineBackground: '#FAFAFA99',
|
||||
foreground: '#90A4AE',
|
||||
caret: '#272727',
|
||||
selection: '#80CBC440',
|
||||
@@ -296,7 +299,7 @@ export const settings = {
|
||||
noctisLilac: {
|
||||
light: true,
|
||||
background: '#f2f1f8',
|
||||
lineBackground: '#f2f1f850',
|
||||
lineBackground: '#f2f1f899',
|
||||
foreground: '#0c006b',
|
||||
caret: '#5c49e9',
|
||||
selection: '#d5d1f2',
|
||||
@@ -307,7 +310,7 @@ export const settings = {
|
||||
},
|
||||
nord: {
|
||||
background: '#2e3440',
|
||||
lineBackground: '#2e344050',
|
||||
lineBackground: '#2e344099',
|
||||
foreground: '#FFFFFF',
|
||||
caret: '#FFFFFF',
|
||||
selection: '#3b4252',
|
||||
@@ -319,7 +322,7 @@ export const settings = {
|
||||
},
|
||||
okaidia: {
|
||||
background: '#272822',
|
||||
lineBackground: '#27282250',
|
||||
lineBackground: '#27282299',
|
||||
foreground: '#FFFFFF',
|
||||
caret: '#FFFFFF',
|
||||
selection: '#49483E',
|
||||
@@ -331,7 +334,7 @@ export const settings = {
|
||||
solarizedLight: {
|
||||
light: true,
|
||||
background: '#fdf6e3',
|
||||
lineBackground: '#fdf6e350',
|
||||
lineBackground: '#fdf6e399',
|
||||
foreground: '#657b83',
|
||||
caret: '#586e75',
|
||||
selection: '#dfd9c8',
|
||||
@@ -342,7 +345,7 @@ export const settings = {
|
||||
},
|
||||
solarizedDark: {
|
||||
background: '#002b36',
|
||||
lineBackground: '#002b3650',
|
||||
lineBackground: '#002b3699',
|
||||
foreground: '#93a1a1',
|
||||
caret: '#839496',
|
||||
selection: '#173541',
|
||||
@@ -353,7 +356,7 @@ export const settings = {
|
||||
},
|
||||
sublime: {
|
||||
background: '#303841',
|
||||
lineBackground: '#30384150',
|
||||
lineBackground: '#30384199',
|
||||
foreground: '#FFFFFF',
|
||||
caret: '#FBAC52',
|
||||
selection: '#4C5964',
|
||||
@@ -365,7 +368,7 @@ export const settings = {
|
||||
tokyoNightDay: {
|
||||
light: true,
|
||||
background: '#e1e2e7',
|
||||
lineBackground: '#e1e2e750',
|
||||
lineBackground: '#e1e2e799',
|
||||
foreground: '#3760bf',
|
||||
caret: '#3760bf',
|
||||
selection: '#99a7df',
|
||||
@@ -377,7 +380,7 @@ export const settings = {
|
||||
},
|
||||
tokyoNightStorm: {
|
||||
background: '#24283b',
|
||||
lineBackground: '#24283b50',
|
||||
lineBackground: '#24283b99',
|
||||
foreground: '#7982a9',
|
||||
caret: '#c0caf5',
|
||||
selection: '#6f7bb630',
|
||||
@@ -389,7 +392,7 @@ export const settings = {
|
||||
},
|
||||
tokyoNight: {
|
||||
background: '#1a1b26',
|
||||
lineBackground: '#1a1b2650',
|
||||
lineBackground: '#1a1b2699',
|
||||
foreground: '#787c99',
|
||||
caret: '#c0caf5',
|
||||
selection: '#515c7e40',
|
||||
@@ -401,7 +404,7 @@ export const settings = {
|
||||
},
|
||||
vscodeDark: {
|
||||
background: '#1e1e1e',
|
||||
lineBackground: '#1e1e1e50',
|
||||
lineBackground: '#1e1e1e99',
|
||||
foreground: '#9cdcfe',
|
||||
caret: '#c6c6c6',
|
||||
selection: '#6199ff2f',
|
||||
@@ -414,7 +417,7 @@ export const settings = {
|
||||
xcodeLight: {
|
||||
light: true,
|
||||
background: '#fff',
|
||||
lineBackground: '#ffffff50',
|
||||
lineBackground: '#ffffff99',
|
||||
foreground: '#3D3D3D',
|
||||
selection: '#BBDFFF',
|
||||
selectionMatch: '#BBDFFF',
|
||||
@@ -424,7 +427,7 @@ export const settings = {
|
||||
},
|
||||
xcodeDark: {
|
||||
background: '#292A30',
|
||||
lineBackground: '#292A3050',
|
||||
lineBackground: '#292A3099',
|
||||
foreground: '#CECFD0',
|
||||
caret: '#fff',
|
||||
selection: '#727377',
|
||||
@@ -469,3 +472,56 @@ function getCircularReplacer() {
|
||||
function stringifySafe(json) {
|
||||
return JSON.stringify(json, getCircularReplacer());
|
||||
}
|
||||
|
||||
export const theme = (theme) => themes[theme] || themes.strudelTheme;
|
||||
|
||||
// css style injection helpers
|
||||
export function injectStyle(rule) {
|
||||
const newStyle = document.createElement('style');
|
||||
document.head.appendChild(newStyle);
|
||||
const styleSheet = newStyle.sheet;
|
||||
const ruleIndex = styleSheet.insertRule(rule, 0);
|
||||
return () => styleSheet.deleteRule(ruleIndex);
|
||||
}
|
||||
|
||||
let currentTheme,
|
||||
resetThemeStyle,
|
||||
themeStyle,
|
||||
styleID = 'strudel-theme-vars';
|
||||
export function initTheme(theme) {
|
||||
if (!document.getElementById(styleID)) {
|
||||
themeStyle = document.createElement('style');
|
||||
themeStyle.id = styleID;
|
||||
document.head.append(themeStyle);
|
||||
}
|
||||
activateTheme(theme);
|
||||
}
|
||||
|
||||
export function activateTheme(name) {
|
||||
if (currentTheme === name) {
|
||||
return;
|
||||
}
|
||||
currentTheme = name;
|
||||
if (!settings[name]) {
|
||||
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||
}
|
||||
const themeSettings = settings[name] || settings.strudelTheme;
|
||||
// set css variables
|
||||
themeStyle.innerHTML = `:root {
|
||||
${Object.entries(themeSettings)
|
||||
// important to override fallback
|
||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||
.join('\n')}
|
||||
}`;
|
||||
// tailwind dark mode
|
||||
if (themeSettings.light) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
resetThemeStyle?.();
|
||||
resetThemeStyle = undefined;
|
||||
if (themeSettings.customStyle) {
|
||||
resetThemeStyle = injectStyle(themeSettings.customStyle);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export const settings = {
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#0f380f',
|
||||
light: true,
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
export default createTheme({
|
||||
theme: 'light',
|
||||
@@ -35,5 +36,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: '#0f380f' },
|
||||
{ tag: t.className, color: '#0f380f' },
|
||||
{ tag: t.invalid, color: '#0f380f' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
||||
],
|
||||
});
|
||||
+1
@@ -33,5 +33,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
+1
@@ -36,5 +36,6 @@ export default createTheme({
|
||||
{ tag: t.propertyName, color: 'white' },
|
||||
{ tag: t.className, color: 'white' },
|
||||
{ tag: t.invalid, color: 'white' },
|
||||
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||
],
|
||||
});
|
||||
+1
@@ -40,5 +40,6 @@ export default createTheme({
|
||||
|
||||
{ tag: t.className, color: '#decb6b' },
|
||||
{ tag: t.invalid, color: '#ffffff' },
|
||||
{ tag: [t.unit, t.punctuation], color: '#82aaff' },
|
||||
],
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
|
||||
let colorA = '#6edee4';
|
||||
//let colorB = 'magenta';
|
||||
let colorB = 'white';
|
||||
let colorC = 'red';
|
||||
let colorD = '#f8fc55';
|
||||
|
||||
export const settings = {
|
||||
background: '#000000',
|
||||
foreground: colorA, // whats that?
|
||||
caret: colorC,
|
||||
selection: colorD,
|
||||
selectionMatch: colorA,
|
||||
lineHighlight: '#6edee440', // panel bg
|
||||
lineBackground: '#00000040',
|
||||
gutterBackground: 'transparent',
|
||||
gutterForeground: '#8a919966',
|
||||
customStyle: '.cm-line { line-height: 1 }',
|
||||
};
|
||||
|
||||
let punctuation = colorD;
|
||||
let mini = colorB;
|
||||
|
||||
export default createTheme({
|
||||
theme: 'dark',
|
||||
settings,
|
||||
styles: [
|
||||
{ tag: t.keyword, color: colorA },
|
||||
{ tag: t.operator, color: mini },
|
||||
{ tag: t.special(t.variableName), color: colorA },
|
||||
{ tag: t.typeName, color: colorA },
|
||||
{ tag: t.atom, color: colorA },
|
||||
{ tag: t.number, color: mini },
|
||||
{ tag: t.definition(t.variableName), color: colorA },
|
||||
{ tag: t.string, color: mini },
|
||||
{ tag: t.special(t.string), color: mini },
|
||||
{ tag: t.comment, color: punctuation },
|
||||
{ tag: t.variableName, color: colorA },
|
||||
{ tag: t.tagName, color: colorA },
|
||||
{ tag: t.bracket, color: punctuation },
|
||||
{ tag: t.meta, color: colorA },
|
||||
{ tag: t.attributeName, color: colorA },
|
||||
{ tag: t.propertyName, color: colorA }, // methods
|
||||
{ tag: t.className, color: colorA },
|
||||
{ tag: t.invalid, color: colorC },
|
||||
{ tag: [t.unit, t.punctuation], color: punctuation },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { hoverTooltip } from '@codemirror/view';
|
||||
import jsdoc from '../../doc.json';
|
||||
import { Autocomplete } from './autocomplete.mjs';
|
||||
|
||||
const getDocLabel = (doc) => doc.name || doc.longname;
|
||||
|
||||
let ctrlDown = false;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// Record Control key event to trigger or block the tooltip depending on the state
|
||||
window.addEventListener(
|
||||
'keyup',
|
||||
function (e) {
|
||||
if (e.key == 'Control') {
|
||||
ctrlDown = false;
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'keydown',
|
||||
function (e) {
|
||||
if (e.key == 'Control') {
|
||||
ctrlDown = true;
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
export const strudelTooltip = hoverTooltip(
|
||||
(view, pos, side) => {
|
||||
// Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
|
||||
if (!ctrlDown) {
|
||||
return null;
|
||||
}
|
||||
let { from, to, text } = view.state.doc.lineAt(pos);
|
||||
let start = pos,
|
||||
end = pos;
|
||||
while (start > from && /\w/.test(text[start - from - 1])) {
|
||||
start--;
|
||||
}
|
||||
while (end < to && /\w/.test(text[end - from])) {
|
||||
end++;
|
||||
}
|
||||
if ((start == pos && side < 0) || (end == pos && side > 0)) {
|
||||
return null;
|
||||
}
|
||||
let word = text.slice(start - from, end - from);
|
||||
// Get entry from Strudel documentation
|
||||
let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
|
||||
if (!entry) {
|
||||
// Try for synonyms
|
||||
entry = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pos: start,
|
||||
end,
|
||||
above: false,
|
||||
arrow: true,
|
||||
create(view) {
|
||||
let dom = document.createElement('div');
|
||||
dom.className = 'strudel-tooltip';
|
||||
const ac = Autocomplete({ doc: entry, label: word });
|
||||
dom.appendChild(ac);
|
||||
return { dom };
|
||||
},
|
||||
};
|
||||
},
|
||||
{ hoverTime: 10 },
|
||||
);
|
||||
|
||||
export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
|
||||
@@ -8,8 +8,8 @@ export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
@@ -1,17 +1,17 @@
|
||||
# @strudel.cycles/core
|
||||
# @strudel/core
|
||||
|
||||
This package contains the bare essence of strudel.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm i @strudel.cycles/core --save
|
||||
npm i @strudel/core --save
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { sequence } from '@strudel.cycles/core';
|
||||
import { sequence } from '@strudel/core';
|
||||
|
||||
const pattern = sequence('a', ['b', 'c']);
|
||||
|
||||
@@ -33,7 +33,7 @@ b: 3/2 - 7/4
|
||||
c: 7/4 - 2
|
||||
```
|
||||
|
||||
- [play with @strudel.cycles/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
||||
- [play with @strudel/core on codesandbox](https://codesandbox.io/s/strudel-core-test-forked-9ywhv7?file=/src/index.js).
|
||||
- [open color pattern example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/canvas.html)
|
||||
- [open minimal repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
||||
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
||||
@@ -1,4 +1,5 @@
|
||||
import { controls, Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||
import { Pattern, getDrawContext, silence, register, pure } from './index.mjs';
|
||||
import controls from './controls.mjs'; // do not import from index.mjs as it breaks for some reason..
|
||||
const { createParams } = controls;
|
||||
|
||||
let clearColor = '#22222210';
|
||||
|
||||
+744
-64
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,15 @@ import { logger } from './logger.mjs';
|
||||
export class Cyclist {
|
||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||
this.started = false;
|
||||
this.cps = 1;
|
||||
this.cps = 0.5;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.lastTick = 0; // absolute time when last tick (clock callback) happened
|
||||
this.lastBegin = 0; // query begin of last tick
|
||||
this.lastEnd = 0; // query end of last tick
|
||||
this.getTime = getTime; // get absolute time
|
||||
this.num_cycles_since_last_cps_change = 0;
|
||||
this.onToggle = onToggle;
|
||||
this.latency = latency; // fixed trigger time offset
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
this.clock = createClock(
|
||||
getTime,
|
||||
// called slightly before each cycle
|
||||
@@ -25,14 +26,24 @@ export class Cyclist {
|
||||
if (tick === 0) {
|
||||
this.origin = phase;
|
||||
}
|
||||
if (this.num_ticks_since_cps_change === 0) {
|
||||
this.num_cycles_since_last_cps_change = this.lastEnd;
|
||||
}
|
||||
this.num_ticks_since_cps_change++;
|
||||
try {
|
||||
const time = getTime();
|
||||
const begin = this.lastEnd;
|
||||
this.lastBegin = begin;
|
||||
const end = round(begin + duration * this.cps);
|
||||
|
||||
//convert ticks to cycles, so you can query the pattern for events
|
||||
const eventLength = duration * this.cps;
|
||||
const end = this.num_cycles_since_last_cps_change + this.num_ticks_since_cps_change * eventLength;
|
||||
this.lastEnd = end;
|
||||
const haps = this.pattern.queryArc(begin, end);
|
||||
const tickdeadline = phase - time; // time left till phase begins
|
||||
|
||||
// query the pattern for events
|
||||
const haps = this.pattern.queryArc(begin, end, { _cps: this.cps });
|
||||
|
||||
const tickdeadline = phase - time; // time left until the phase is a whole number
|
||||
this.lastTick = time + tickdeadline;
|
||||
|
||||
haps.forEach((hap) => {
|
||||
@@ -59,6 +70,8 @@ export class Cyclist {
|
||||
this.onToggle?.(v);
|
||||
}
|
||||
start() {
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
this.num_cycles_since_last_cps_change = 0;
|
||||
if (!this.pattern) {
|
||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||
}
|
||||
@@ -74,6 +87,7 @@ export class Cyclist {
|
||||
stop() {
|
||||
logger('[cyclist] stop');
|
||||
this.clock.stop();
|
||||
this.lastEnd = 0;
|
||||
this.setStarted(false);
|
||||
}
|
||||
setPattern(pat, autostart = false) {
|
||||
@@ -82,8 +96,12 @@ export class Cyclist {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
setCps(cps = 1) {
|
||||
setCps(cps = 0.5) {
|
||||
if (this.cps === cps) {
|
||||
return;
|
||||
}
|
||||
this.cps = cps;
|
||||
this.num_ticks_since_cps_change = 0;
|
||||
}
|
||||
log(begin, end, haps) {
|
||||
const onsets = haps.filter((h) => h.hasOnset());
|
||||
|
||||
+116
-6
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
draw.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/draw.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/draw.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -9,17 +9,29 @@ import { Pattern, getTime, State, TimeSpan } from './index.mjs';
|
||||
export const getDrawContext = (id = 'test-canvas') => {
|
||||
let canvas = document.querySelector('#' + id);
|
||||
if (!canvas) {
|
||||
const scale = 2; // 2 = crisp on retina screens
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.id = id;
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||
document.body.prepend(canvas);
|
||||
let timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
canvas.width = window.innerWidth * scale;
|
||||
canvas.height = window.innerHeight * scale;
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
Pattern.prototype.draw = function (callback, { from, to, onQuery } = {}) {
|
||||
if (typeof window === 'undefined') {
|
||||
return this;
|
||||
}
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
@@ -51,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
||||
|
||||
export const cleanupDraw = (clearScreen = true) => {
|
||||
const ctx = getDrawContext();
|
||||
clearScreen && ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
||||
clearScreen && ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.width);
|
||||
if (window.strudelAnimation) {
|
||||
cancelAnimationFrame(window.strudelAnimation);
|
||||
}
|
||||
@@ -65,3 +77,101 @@ Pattern.prototype.onPaint = function (onPaint) {
|
||||
this.context = { onPaint };
|
||||
return this;
|
||||
};
|
||||
|
||||
// const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
// encapsulates starting and stopping animation frames
|
||||
export class Framer {
|
||||
constructor(onFrame, onError) {
|
||||
this.onFrame = onFrame;
|
||||
this.onError = onError;
|
||||
}
|
||||
start() {
|
||||
const self = this;
|
||||
let frame = requestAnimationFrame(function updateHighlights(time) {
|
||||
try {
|
||||
self.onFrame(time);
|
||||
} catch (err) {
|
||||
self.onError(err);
|
||||
}
|
||||
frame = requestAnimationFrame(updateHighlights);
|
||||
});
|
||||
self.cancel = () => {
|
||||
cancelAnimationFrame(frame);
|
||||
};
|
||||
}
|
||||
stop() {
|
||||
if (this.cancel) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncs animation frames to a cyclist scheduler
|
||||
// see vite-vanilla-repl-cm6 for an example
|
||||
export class Drawer {
|
||||
constructor(onDraw, drawTime) {
|
||||
this.visibleHaps = [];
|
||||
this.lastFrame = null;
|
||||
this.drawTime = drawTime;
|
||||
this.framer = new Framer(
|
||||
() => {
|
||||
if (!this.scheduler) {
|
||||
console.warn('Drawer: no scheduler');
|
||||
return;
|
||||
}
|
||||
const lookbehind = Math.abs(this.drawTime[0]);
|
||||
const lookahead = this.drawTime[1];
|
||||
// calculate current frame time (think right side of screen for pianoroll)
|
||||
const phase = this.scheduler.now() + lookahead;
|
||||
// first frame just captures the phase
|
||||
if (this.lastFrame === null) {
|
||||
this.lastFrame = phase;
|
||||
return;
|
||||
}
|
||||
// query haps from last frame till now. take last 100ms max
|
||||
const haps = this.scheduler.pattern.queryArc(Math.max(this.lastFrame, phase - 1 / 10), phase);
|
||||
this.lastFrame = phase;
|
||||
this.visibleHaps = (this.visibleHaps || [])
|
||||
// filter out haps that are too far in the past (think left edge of screen for pianoroll)
|
||||
.filter((h) => h.whole?.end >= phase - lookbehind - lookahead)
|
||||
// add new haps with onset (think right edge bars scrolling in)
|
||||
.concat(haps.filter((h) => h.hasOnset()));
|
||||
const time = phase - lookahead;
|
||||
onDraw(this.visibleHaps, time, this);
|
||||
},
|
||||
(err) => {
|
||||
console.warn('draw error', err);
|
||||
},
|
||||
);
|
||||
}
|
||||
setDrawTime(drawTime) {
|
||||
this.drawTime = drawTime;
|
||||
}
|
||||
invalidate(scheduler = this.scheduler, t) {
|
||||
if (!scheduler) {
|
||||
return;
|
||||
}
|
||||
// TODO: scheduler.now() seems to move even when it's stopped, this hints at a bug...
|
||||
t = t ?? scheduler.now();
|
||||
this.scheduler = scheduler;
|
||||
let [_, lookahead] = this.drawTime;
|
||||
const [begin, end] = [Math.max(t, 0), t + lookahead + 0.1];
|
||||
// remove all future haps
|
||||
this.visibleHaps = this.visibleHaps.filter((h) => h.whole.begin < t);
|
||||
// query future haps
|
||||
const futureHaps = scheduler.pattern.queryArc(begin, end); // +0.1 = workaround for weird holes in query..
|
||||
// append future haps
|
||||
this.visibleHaps = this.visibleHaps.concat(futureHaps);
|
||||
}
|
||||
start(scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
this.invalidate();
|
||||
this.framer.start();
|
||||
}
|
||||
stop() {
|
||||
if (this.framer) {
|
||||
this.framer.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,8 +145,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
||||
* so there will be no gaps.
|
||||
* @name euclidLegato
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @example
|
||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
||||
* note("c3").euclidLegato(3,8)
|
||||
*/
|
||||
|
||||
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||
@@ -166,6 +168,18 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
||||
return _euclidLegato(pulses, steps, 0, pat);
|
||||
});
|
||||
|
||||
/**
|
||||
* Similar to `euclid`, but each pulse is held until the next pulse,
|
||||
* so there will be no gaps, and has an additional parameter for 'rotating'
|
||||
* the resulting sequence
|
||||
* @name euclidLegatoRot
|
||||
* @memberof Pattern
|
||||
* @param {number} pulses the number of onsets / beats
|
||||
* @param {number} steps the number of steps to fill
|
||||
* @param {number} rotation offset in steps
|
||||
* @example
|
||||
* note("c3").euclidLegatoRot(3,5,2)
|
||||
*/
|
||||
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||
return _euclidLegato(pulses, steps, rotation, pat);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
evaluate.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/evaluate.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -37,16 +37,15 @@ function safeEval(str, options = {}) {
|
||||
}
|
||||
|
||||
export const evaluate = async (code, transpiler) => {
|
||||
let meta = {};
|
||||
if (transpiler) {
|
||||
code = transpiler(code); // transform syntactically correct js code to semantically usable code
|
||||
// transform syntactically correct js code to semantically usable code
|
||||
const transpiled = transpiler(code);
|
||||
code = transpiled.output;
|
||||
meta = transpiled;
|
||||
}
|
||||
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||
const options = { wrapExpression: !!transpiler };
|
||||
let evaluated = await safeEval(code, options);
|
||||
if (!isPattern(evaluated)) {
|
||||
console.log('evaluated', evaluated);
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
return { mode: 'javascript', pattern: evaluated };
|
||||
return { mode: 'javascript', pattern: evaluated, meta };
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
!dist
|
||||
@@ -1,10 +0,0 @@
|
||||
# vite-vanilla-repl
|
||||
|
||||
This folder demonstrates how to set up a strudel repl using vite and vanilla JS. Run it using:
|
||||
|
||||
```sh
|
||||
npm i
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or view it [live on githack](https://rawcdn.githack.com/tidalcycles/strudel/5fb36acb046ead7cd6ad3cd10f532e7f585f536a/packages/core/examples/vite-vanilla-repl/dist/index.html)
|
||||
@@ -1 +0,0 @@
|
||||
import{b as s,h as i,m,a as n,c as p,p as t}from"./index.4cbc0a10.js";export{s as SyntaxError,i as h,m as mini,n as minify,p as parse,t as patternifyAST};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
import{g as s,f as d,i as t,n as r,l as u,j as o,d as f,k as p,r as g,s as i,w as l,e as m}from"./index.4cbc0a10.js";export{s as getAudioContext,d as getCachedBuffer,t as getLoadedBuffer,r as getLoadedSamples,u as loadBuffer,o as loadGithubSamples,f as panic,p as resetLoadedSamples,g as reverseBuffer,i as samples,l as webaudioOutput,m as webaudioOutputTrigger};
|
||||
@@ -1,36 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite Vanilla Strudel REPL</title>
|
||||
<script type="module" crossorigin src="/tidalcycles/strudel/use-acorn/packages/core/examples/vite-vanilla-repl/dist/assets/index.4cbc0a10.js"></script>
|
||||
</head>
|
||||
<body style="margin: 0; background: #222">
|
||||
<div style="display: grid; height: 100vh">
|
||||
<textarea
|
||||
id="text"
|
||||
style="font-size: 2em; border: 0; color: white; background: transparent; outline: none; padding: 20px"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
id="start"
|
||||
style="
|
||||
position: absolute;
|
||||
border-radius: 10px;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 20px;
|
||||
border: 2px solid white;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
evaluate
|
||||
</button>
|
||||
<div id="output"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +0,0 @@
|
||||
/*
|
||||
gist.js - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/gist.js>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// this is a shortcut to eval code from a gist
|
||||
// why? to be able to shorten strudel code + e.g. be able to change instruments after links have been generated
|
||||
export default (route, cache = true) =>
|
||||
fetch(`https://gist.githubusercontent.com/${route}?cachebust=${cache ? '' : Date.now()}`)
|
||||
.then((res) => res.text())
|
||||
.then((code) => eval(code));
|
||||
@@ -32,7 +32,11 @@ export class Hap {
|
||||
}
|
||||
|
||||
get duration() {
|
||||
return this.whole.end.sub(this.whole.begin);
|
||||
return this.whole.end.sub(this.whole.begin).mul(typeof this.value?.clip === 'number' ? this.value?.clip : 1);
|
||||
}
|
||||
|
||||
get endClipped() {
|
||||
return this.whole.begin.add(this.duration);
|
||||
}
|
||||
|
||||
wholeOrPart() {
|
||||
|
||||
@@ -18,23 +18,24 @@ export * from './util.mjs';
|
||||
export * from './speak.mjs';
|
||||
export * from './evaluate.mjs';
|
||||
export * from './repl.mjs';
|
||||
export * from './cyclist.mjs';
|
||||
export * from './logger.mjs';
|
||||
export * from './time.mjs';
|
||||
export * from './draw.mjs';
|
||||
export * from './animate.mjs';
|
||||
export * from './pianoroll.mjs';
|
||||
export * from './spiral.mjs';
|
||||
export * from './ui.mjs';
|
||||
export { default as drawLine } from './drawLine.mjs';
|
||||
export { default as gist } from './gist.js';
|
||||
// below won't work with runtime.mjs (json import fails)
|
||||
/* import * as p from './package.json';
|
||||
export const version = p.version; */
|
||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
||||
logger('🌀 @strudel/core loaded 🌀');
|
||||
if (globalThis._strudelLoaded) {
|
||||
console.warn(
|
||||
`@strudel.cycles/core was loaded more than once...
|
||||
`@strudel/core was loaded more than once...
|
||||
This might happen when you have multiple versions of strudel installed.
|
||||
Please check with "npm ls @strudel.cycles/core".`,
|
||||
Please check with "npm ls @strudel/core".`,
|
||||
);
|
||||
}
|
||||
globalThis._strudelLoaded = true;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/core",
|
||||
"version": "0.7.2",
|
||||
"name": "@strudel/core",
|
||||
"version": "1.0.1",
|
||||
"description": "Port of Tidal Cycles to JavaScript",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
@@ -29,13 +28,13 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||
},
|
||||
"homepage": "https://strudel.tidalcycles.org",
|
||||
"homepage": "https://strudel.cc",
|
||||
"dependencies": {
|
||||
"fraction.js": "^4.2.0"
|
||||
"fraction.js": "^4.3.7"
|
||||
},
|
||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2",
|
||||
"vitest": "^0.25.7"
|
||||
"vite": "^5.0.10",
|
||||
"vitest": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
+273
-139
@@ -26,7 +26,7 @@ export class Pattern {
|
||||
/**
|
||||
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
||||
*
|
||||
* @param {function} query - The function that maps a {@link State} to an array of {@link Hap}.
|
||||
* @param {function} query - The function that maps a `State` to an array of `Hap`.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
constructor(query) {
|
||||
@@ -39,7 +39,7 @@ export class Pattern {
|
||||
|
||||
/**
|
||||
* Returns a new pattern, with the function applied to the value of
|
||||
* each hap. It has the alias {@link Pattern#fmap}.
|
||||
* each hap. It has the alias `fmap`.
|
||||
* @synonyms fmap
|
||||
* @param {Function} func to to apply to the value
|
||||
* @returns Pattern
|
||||
@@ -51,7 +51,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* see {@link Pattern#withValue}
|
||||
* see `withValue`
|
||||
* @noAutocomplete
|
||||
*/
|
||||
fmap(func) {
|
||||
@@ -115,7 +115,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#appBoth}, but the `whole` timespan is not the intersection,
|
||||
* As with `appBoth`, but the `whole` timespan is not the intersection,
|
||||
* but the timespan from the function of patterns that this method is called
|
||||
* on. In practice, this means that the pattern structure, including onsets,
|
||||
* are preserved from the pattern of functions (often referred to as the left
|
||||
@@ -148,7 +148,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#appLeft}, but `whole` timespans are instead taken from the
|
||||
* As with `appLeft`, but `whole` timespans are instead taken from the
|
||||
* pattern of values, i.e. structure is preserved from the right hand/outer
|
||||
* pattern.
|
||||
* @param {Pattern} pat_val
|
||||
@@ -340,9 +340,9 @@ export class Pattern {
|
||||
* silence
|
||||
* @noAutocomplete
|
||||
*/
|
||||
queryArc(begin, end) {
|
||||
queryArc(begin, end, controls = {}) {
|
||||
try {
|
||||
return this.query(new State(new TimeSpan(begin, end)));
|
||||
return this.query(new State(new TimeSpan(begin, end), controls));
|
||||
} catch (err) {
|
||||
logger(`[query]: ${err.message}`, 'error');
|
||||
return [];
|
||||
@@ -387,7 +387,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#withQuerySpan}, but the function is applied to both the
|
||||
* As with `withQuerySpan`, but the function is applied to both the
|
||||
* begin and end time of the query timespan.
|
||||
* @param {Function} func the function to apply
|
||||
* @returns Pattern
|
||||
@@ -398,7 +398,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link Pattern#withQuerySpan}, but the function is applied to the timespans
|
||||
* Similar to `withQuerySpan`, but the function is applied to the timespans
|
||||
* of all haps returned by pattern queries (both `part` timespans, and where
|
||||
* present, `whole` timespans).
|
||||
* @param {Function} func
|
||||
@@ -410,7 +410,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#withHapSpan}, but the function is applied to both the
|
||||
* As with `withHapSpan`, but the function is applied to both the
|
||||
* begin and end time of the hap timespans.
|
||||
* @param {Function} func the function to apply
|
||||
* @returns Pattern
|
||||
@@ -427,11 +427,11 @@ export class Pattern {
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withHaps(func) {
|
||||
return new Pattern((state) => func(this.query(state)));
|
||||
return new Pattern((state) => func(this.query(state), state));
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#withHaps}, but applies the function to every hap, rather than every list of haps.
|
||||
* As with `withHaps`, but applies the function to every hap, rather than every list of haps.
|
||||
* @param {Function} func
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
@@ -472,15 +472,15 @@ export class Pattern {
|
||||
/**
|
||||
* Returns a new pattern with the given location information added to the
|
||||
* context of every hap.
|
||||
* @param {Number} start
|
||||
* @param {Number} end
|
||||
* @param {Number} start start offset
|
||||
* @param {Number} end end offset
|
||||
* @returns Pattern
|
||||
* @noAutocomplete
|
||||
*/
|
||||
withLocation(start, end) {
|
||||
withLoc(start, end) {
|
||||
const location = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
start,
|
||||
end,
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
const locations = (context.locations || []).concat([location]);
|
||||
@@ -488,32 +488,6 @@ export class Pattern {
|
||||
});
|
||||
}
|
||||
|
||||
withMiniLocation(start, end) {
|
||||
const offset = {
|
||||
start: { line: start[0], column: start[1], offset: start[2] },
|
||||
end: { line: end[0], column: end[1], offset: end[2] },
|
||||
};
|
||||
return this.withContext((context) => {
|
||||
let locations = context.locations || [];
|
||||
locations = locations.map(({ start, end }) => {
|
||||
const colOffset = start.line === 1 ? offset.start.column : 0;
|
||||
return {
|
||||
start: {
|
||||
...start,
|
||||
line: start.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: start.column - 1 + colOffset,
|
||||
},
|
||||
end: {
|
||||
...end,
|
||||
line: end.line - 1 + (offset.start.line - 1) + 1,
|
||||
column: end.column - 1 + colOffset,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { ...context, locations };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Pattern, which only returns haps that meet the given test.
|
||||
* @param {Function} hap_test - a function which returns false for haps to be removed from the pattern
|
||||
@@ -525,7 +499,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* As with {@link Pattern#filterHaps}, but the function is applied to values
|
||||
* As with `filterHaps`, but the function is applied to values
|
||||
* inside haps.
|
||||
* @param {Function} value_test
|
||||
* @returns Pattern
|
||||
@@ -647,7 +621,7 @@ export class Pattern {
|
||||
}
|
||||
|
||||
/**
|
||||
* More human-readable version of the {@link Pattern#firstCycleValues} accessor.
|
||||
* More human-readable version of the `firstCycleValues` accessor.
|
||||
* @noAutocomplete
|
||||
*/
|
||||
get showFirstCycle() {
|
||||
@@ -717,13 +691,13 @@ export class Pattern {
|
||||
// Methods without corresponding toplevel functions
|
||||
|
||||
/**
|
||||
* Layers the result of the given function(s). Like {@link Pattern.superimpose}, but without the original pattern:
|
||||
* Layers the result of the given function(s). Like `superimpose`, but without the original pattern:
|
||||
* @name layer
|
||||
* @memberof Pattern
|
||||
* @synonyms apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
|
||||
* .layer(x=>x.add("0,2"))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
@@ -737,7 +711,7 @@ export class Pattern {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*4"
|
||||
* "<0 2 4 6 ~ 4 ~ 2 0!3 ~!5>*8"
|
||||
* .superimpose(x=>x.add(2))
|
||||
* .scale('C minor').note()
|
||||
*/
|
||||
@@ -753,8 +727,8 @@ export class Pattern {
|
||||
* @name stack
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* s("hh*2").stack(
|
||||
* note("c2(3,8)")
|
||||
* s("hh*4").stack(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
stack(...pats) {
|
||||
@@ -771,8 +745,8 @@ export class Pattern {
|
||||
* @memberof Pattern
|
||||
* @synonyms sequence, fastcat
|
||||
* @example
|
||||
* s("hh*2").seq(
|
||||
* note("c2(3,8)")
|
||||
* s("hh*4").seq(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
seq(...pats) {
|
||||
@@ -785,8 +759,8 @@ export class Pattern {
|
||||
* @memberof Pattern
|
||||
* @synonyms slowcat
|
||||
* @example
|
||||
* s("hh*2").cat(
|
||||
* note("c2(3,8)")
|
||||
* s("hh*4").cat(
|
||||
* note("c4(5,8)")
|
||||
* )
|
||||
*/
|
||||
cat(...pats) {
|
||||
@@ -809,13 +783,14 @@ export class Pattern {
|
||||
hap.setContext({
|
||||
...hap.context,
|
||||
onTrigger: (...args) => {
|
||||
if (!dominant && hap.context.onTrigger) {
|
||||
hap.context.onTrigger(...args);
|
||||
}
|
||||
// run previously set trigger, if it exists
|
||||
hap.context.onTrigger?.(...args);
|
||||
onTrigger(...args);
|
||||
},
|
||||
// we need this to know later if the default trigger should still fire
|
||||
dominantTrigger: dominant,
|
||||
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
||||
// when using multiple triggers, you cannot flip this flag to false again!
|
||||
// example: x.csound('CooLSynth').log() as well as x.log().csound('CooLSynth') should work the same
|
||||
dominantTrigger: hap.context.dominantTrigger || dominant,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -883,7 +858,7 @@ Pattern.prototype.arpWith = function (func) {
|
||||
* Selects indices in in stacked notes.
|
||||
* @example
|
||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
||||
* .arp("0 [0,2] 1 [0,2]").slow(2)
|
||||
* .arp("0 [0,2] 1 [0,2]")
|
||||
* */
|
||||
Pattern.prototype.arp = function (pat) {
|
||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||
@@ -954,14 +929,14 @@ function _composeOp(a, b, func) {
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
||||
* "0 2 4".add("<0 3 4 0>").scale('C major').note()
|
||||
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
|
||||
* // Without add, the equivalent would be:
|
||||
* // "<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>".scale('C major').note()
|
||||
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
|
||||
* @example
|
||||
* // You can also use add with notes:
|
||||
* "c3 e3 g3".add("<0 5 7 0>").note()
|
||||
* note("c3 e3 g3".add("<0 5 7 0>"))
|
||||
* // Behind the scenes, the notes are converted to midi numbers:
|
||||
* // "48 52 55".add("<0 5 7 0>").note()
|
||||
* // note("48 52 55".add("<0 5 7 0>"))
|
||||
*/
|
||||
add: [numeralArgs((a, b) => a + b)], // support string concatenation
|
||||
/**
|
||||
@@ -970,7 +945,7 @@ function _composeOp(a, b, func) {
|
||||
* @name sub
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "0 2 4".sub("<0 1 2 3>").scale('C4 minor').note()
|
||||
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
|
||||
* // See add for more information.
|
||||
*/
|
||||
sub: [numeralArgs((a, b) => a - b)],
|
||||
@@ -980,7 +955,7 @@ function _composeOp(a, b, func) {
|
||||
* @name mul
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
* "1 1.5 [1.66, <2 2.33>]".mul(150).freq()
|
||||
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
|
||||
*/
|
||||
mul: [numeralArgs((a, b) => a * b)],
|
||||
/**
|
||||
@@ -1074,9 +1049,9 @@ function _composeOp(a, b, func) {
|
||||
* Applies the given structure to the pattern:
|
||||
*
|
||||
* @example
|
||||
* note("c3,eb3,g3")
|
||||
* note("c,eb,g")
|
||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||
* .slow(4)
|
||||
* .slow(2)
|
||||
*/
|
||||
Pattern.prototype.struct = function (...args) {
|
||||
return this.keepif.out(...args);
|
||||
@@ -1088,7 +1063,7 @@ function _composeOp(a, b, func) {
|
||||
* Returns silence when mask is 0 or "~"
|
||||
*
|
||||
* @example
|
||||
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>").slow(2)
|
||||
* note("c [eb,g] d [eb,g]").mask("<1 [0 1]>")
|
||||
*/
|
||||
Pattern.prototype.mask = function (...args) {
|
||||
return this.keepif.in(...args);
|
||||
@@ -1100,7 +1075,7 @@ function _composeOp(a, b, func) {
|
||||
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
||||
*
|
||||
* @example
|
||||
* s("<bd lt> sd, hh*4").reset("<x@3 x(3,8)>")
|
||||
* s("[<bd lt> sd]*2, hh*8").reset("<x@3 x(5,8)>")
|
||||
*/
|
||||
Pattern.prototype.reset = function (...args) {
|
||||
return this.keepif.trig(...args);
|
||||
@@ -1113,7 +1088,7 @@ function _composeOp(a, b, func) {
|
||||
* While reset will only reset the current cycle, restart will start from cycle 0.
|
||||
*
|
||||
* @example
|
||||
* s("<bd lt> sd, hh*4").restart("<x@3 x(3,8)>")
|
||||
* s("[<bd lt> sd]*2, hh*8").restart("<x@3 x(5,8)>")
|
||||
*/
|
||||
Pattern.prototype.restart = function (...args) {
|
||||
return this.keepif.trigzero(...args);
|
||||
@@ -1179,8 +1154,8 @@ export function isPattern(thing) {
|
||||
/* if (!thing instanceof Pattern) {
|
||||
console.warn(
|
||||
`Found Pattern that fails "instanceof Pattern" check.
|
||||
This may happen if you are using multiple versions of @strudel.cycles/core.
|
||||
Please check by running "npm ls @strudel.cycles/core".`,
|
||||
This may happen if you are using multiple versions of @strudel/core.
|
||||
Please check by running "npm ls @strudel/core".`,
|
||||
);
|
||||
console.log(thing);
|
||||
} */
|
||||
@@ -1203,7 +1178,8 @@ export function reify(thing) {
|
||||
* @return {Pattern}
|
||||
* @synonyms polyrhythm, pr
|
||||
* @example
|
||||
* stack(g3, b3, [e4, d4]).note() // "g3,b3,[e4,d4]".note()
|
||||
* stack("g3", "b3", ["e4", "d4"]).note()
|
||||
* // "g3,b3,[e4,d4]".note()
|
||||
*/
|
||||
export function stack(...pats) {
|
||||
// Array test here is to avoid infinite recursions..
|
||||
@@ -1214,11 +1190,11 @@ export function stack(...pats) {
|
||||
|
||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||
*
|
||||
* synonyms: {@link cat}
|
||||
* synonyms: `cat`
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* slowcat(e5, b4, [d5, c5])
|
||||
* slowcat("e5", "b4", ["d5", "c5"])
|
||||
*
|
||||
*/
|
||||
export function slowcat(...pats) {
|
||||
@@ -1262,17 +1238,19 @@ export function slowcatPrime(...pats) {
|
||||
* @synonyms slowcat
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* cat(e5, b4, [d5, c5]).note() // "<e5 b4 [d5 c5]>".note()
|
||||
* cat("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "<e5 b4 [d5 c5]>".note()
|
||||
*
|
||||
*/
|
||||
export function cat(...pats) {
|
||||
return slowcat(...pats);
|
||||
}
|
||||
|
||||
/** Like {@link Pattern.seq}, but each step has a length, relative to the whole.
|
||||
/** Like `seq`, but each step has a length, relative to the whole.
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* timeCat([3,e3],[1, g3]).note() // "e3@3 g3".note()
|
||||
* timeCat([3,"e3"],[1, "g3"]).note()
|
||||
* // "e3@3 g3".note()
|
||||
*/
|
||||
export function timeCat(...timepats) {
|
||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||
@@ -1292,7 +1270,10 @@ export function timeCat(...timepats) {
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* arrange([4, "<c a f e>(3,8)"],[2, "<g a>(5,8)"]).note()
|
||||
* arrange(
|
||||
* [4, "<c a f e>(3,8)"],
|
||||
* [2, "<g a>(5,8)"]
|
||||
* ).note()
|
||||
*/
|
||||
export function arrange(...sections) {
|
||||
const total = sections.reduce((sum, [cycles]) => sum + cycles, 0);
|
||||
@@ -1304,7 +1285,7 @@ export function fastcat(...pats) {
|
||||
return slowcat(...pats)._fast(pats.length);
|
||||
}
|
||||
|
||||
/** See {@link fastcat} */
|
||||
/** See `fastcat` */
|
||||
export function sequence(...pats) {
|
||||
return fastcat(...pats);
|
||||
}
|
||||
@@ -1312,7 +1293,8 @@ export function sequence(...pats) {
|
||||
/** Like **cat**, but the items are crammed into one cycle.
|
||||
* @synonyms fastcat, sequence
|
||||
* @example
|
||||
* seq(e5, b4, [d5, c5]).note() // "e5 b4 [d5 c5]".note()
|
||||
* seq("e5", "b4", ["d5", "c5"]).note()
|
||||
* // "e5 b4 [d5 c5]".note()
|
||||
*
|
||||
*/
|
||||
export function seq(...pats) {
|
||||
@@ -1338,9 +1320,9 @@ function _sequenceCount(x) {
|
||||
* @param {number} steps how many items are placed in one cycle
|
||||
* @param {any[]} sequences one or more arrays of Patterns / values
|
||||
* @example
|
||||
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
|
||||
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
|
||||
* // note("{c d e f g f e d}%2").stack(s("bd"))
|
||||
* polymeterSteps(4, ["c", "d", "e"])
|
||||
* .note().stack(s("bd"))
|
||||
* // note("{c d e}%4").stack(s("bd"))
|
||||
*/
|
||||
export function polymeterSteps(steps, ...args) {
|
||||
const seqs = args.map((a) => _sequenceCount(a));
|
||||
@@ -1488,7 +1470,7 @@ export function register(name, func, patternify = true) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0.5 1.5 2.5".round().scale('C major').note()
|
||||
* n("0.5 1.5 2.5".round()).scale("C:major")
|
||||
*/
|
||||
export const round = register('round', function (pat) {
|
||||
return pat.asNumber().fmap((v) => Math.round(v));
|
||||
@@ -1502,7 +1484,7 @@ export const round = register('round', function (pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "42 42.1 42.5 43".floor().note()
|
||||
* note("42 42.1 42.5 43".floor())
|
||||
*/
|
||||
export const floor = register('floor', function (pat) {
|
||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
||||
@@ -1516,7 +1498,7 @@ export const floor = register('floor', function (pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "42 42.1 42.5 43".ceil().note()
|
||||
* note("42 42.1 42.5 43".ceil())
|
||||
*/
|
||||
export const ceil = register('ceil', function (pat) {
|
||||
return pat.asNumber().fmap((v) => Math.ceil(v));
|
||||
@@ -1549,7 +1531,8 @@ export const fromBipolar = register('fromBipolar', function (pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(sine.range(500,2000).slow(4))
|
||||
* s("[bd sd]*2,hh*8")
|
||||
* .cutoff(sine.range(500,4000))
|
||||
*/
|
||||
export const range = register('range', function (min, max, pat) {
|
||||
return pat.mul(max - min).add(min);
|
||||
@@ -1563,7 +1546,8 @@ export const range = register('range', function (min, max, pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(sine.rangex(500,2000).slow(4))
|
||||
* s("[bd sd]*2,hh*8")
|
||||
* .cutoff(sine.rangex(500,4000))
|
||||
*/
|
||||
export const rangex = register('rangex', function (min, max, pat) {
|
||||
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||
@@ -1576,12 +1560,32 @@ export const rangex = register('rangex', function (min, max, pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("bd sd,hh*4").cutoff(sine2.range2(500,2000).slow(4))
|
||||
* s("[bd sd]*2,hh*8")
|
||||
* .cutoff(sine2.range2(500,4000))
|
||||
*/
|
||||
export const range2 = register('range2', function (min, max, pat) {
|
||||
return pat.fromBipolar()._range(min, max);
|
||||
});
|
||||
|
||||
/**
|
||||
* Allows dividing numbers via list notation using ":".
|
||||
* Returns a new pattern with just numbers.
|
||||
* @name ratio
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* ratio("1, 5:4, 3:2").mul(110)
|
||||
* .freq().s("piano")
|
||||
*/
|
||||
export const ratio = register('ratio', (pat) =>
|
||||
pat.fmap((v) => {
|
||||
if (!Array.isArray(v)) {
|
||||
return v;
|
||||
}
|
||||
return v.slice(1).reduce((acc, n) => acc / n, v[0]);
|
||||
}),
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Structural and temporal transformations
|
||||
|
||||
@@ -1643,7 +1647,7 @@ export const { fastGap, fastgap } = register(['fastGap', 'fastgap'], function (f
|
||||
});
|
||||
|
||||
/**
|
||||
* Similar to compress, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
|
||||
* Similar to `compress`, but doesn't leave gaps, and the 'focus' can be bigger than a cycle
|
||||
* @example
|
||||
* s("bd hh sd hh").focus(1/4, 3/4)
|
||||
*/
|
||||
@@ -1674,9 +1678,12 @@ export const ply = register('ply', function (factor, pat) {
|
||||
* @param {number | Pattern} factor speed up factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("<bd sd> hh").fast(2) // s("[<bd sd> hh]*2")
|
||||
* s("bd hh sd hh").fast(2) // s("[bd hh sd hh]*2")
|
||||
*/
|
||||
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
factor = Fraction(factor);
|
||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||
return fastQuery.withHapTime((t) => t.div(factor));
|
||||
@@ -1700,9 +1707,12 @@ export const hurry = register('hurry', function (r, pat) {
|
||||
* @param {number | Pattern} factor slow down factor
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("<bd sd> hh").slow(2) // s("[<bd sd> hh]/2")
|
||||
* s("bd hh sd hh").slow(2) // s("[bd hh sd hh]/2")
|
||||
*/
|
||||
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||
if (factor === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat._fast(Fraction(1).div(factor));
|
||||
});
|
||||
|
||||
@@ -1754,7 +1764,7 @@ export const lastOf = register('lastOf', function (n, func, pat) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* An alias for {@link firstOf}
|
||||
* An alias for `firstOf`
|
||||
* @name every
|
||||
* @memberof Pattern
|
||||
* @param {number} n how many cycles
|
||||
@@ -1786,9 +1796,9 @@ export const apply = register('apply', function (func, pat) {
|
||||
* @example
|
||||
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
|
||||
*/
|
||||
// TODO - global clock
|
||||
// this is redefined in repl.mjs, using the current cps as divisor
|
||||
export const cpm = register('cpm', function (cpm, pat) {
|
||||
return pat._fast(cpm / 60);
|
||||
return pat._fast(cpm / 60 / 1);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1861,7 +1871,7 @@ export const linger = register('linger', function (t, pat) {
|
||||
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
|
||||
* @param {number} segments number of segments per cycle
|
||||
* @example
|
||||
* note(saw.range(0,12).segment(24)).add(40)
|
||||
* note(saw.range(40,52).segment(24))
|
||||
*/
|
||||
export const segment = register('segment', function (rate, pat) {
|
||||
return pat.struct(pure(true)._fast(rate));
|
||||
@@ -1887,7 +1897,7 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
|
||||
* @param {function} func
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub(5)).note()
|
||||
* "c3 eb3 g3".when("<0 1>/2", x=>x.sub("5")).note()
|
||||
*/
|
||||
export const when = register('when', function (on, func, pat) {
|
||||
return on ? func(pat) : pat;
|
||||
@@ -1924,7 +1934,7 @@ export const brak = register('brak', function (pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* note("c3 d3 e3 g3").rev()
|
||||
* note("c d e g").rev()
|
||||
*/
|
||||
export const rev = register('rev', function (pat) {
|
||||
const query = function (state) {
|
||||
@@ -1976,9 +1986,9 @@ export const press = register('press', function (pat) {
|
||||
* s("hh*3")
|
||||
* )
|
||||
*/
|
||||
export const hush = register('hush', function (pat) {
|
||||
Pattern.prototype.hush = function () {
|
||||
return silence;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
|
||||
@@ -1986,7 +1996,7 @@ export const hush = register('hush', function (pat) {
|
||||
* note("c d e g").palindrome()
|
||||
*/
|
||||
export const palindrome = register('palindrome', function (pat) {
|
||||
return pat.every(2, rev);
|
||||
return pat.lastOf(2, rev);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -1994,7 +2004,7 @@ export const palindrome = register('palindrome', function (pat) {
|
||||
* @name juxBy
|
||||
* @synonyms juxby
|
||||
* @example
|
||||
* s("lt ht mt ht hh").juxBy("<0 .5 1>/2", rev)
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)
|
||||
*/
|
||||
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
|
||||
by /= 2;
|
||||
@@ -2013,7 +2023,11 @@ export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func,
|
||||
/**
|
||||
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||
* @example
|
||||
* s("lt ht mt ht hh").jux(rev)
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").jux(press)
|
||||
* @example
|
||||
* s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))
|
||||
*/
|
||||
export const jux = register('jux', function (func, pat) {
|
||||
return pat._juxBy(1, func, pat);
|
||||
@@ -2029,7 +2043,7 @@ export const jux = register('jux', function (func, pat) {
|
||||
* @example
|
||||
* "<0 [2 4]>"
|
||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
||||
* .scale('C minor').note().legato(.2)
|
||||
* .scale("C:minor").note()
|
||||
*/
|
||||
export const { echoWith, echowith, stutWith, stutwith } = register(
|
||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||
@@ -2102,22 +2116,43 @@ export const { iterBack, iterback } = register(['iterBack', 'iterback'], functio
|
||||
});
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||
* @name chunk
|
||||
* Repeats each cycle the given number of times.
|
||||
* @name repeatCycles
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunk(4, x=>x.add(7)).scale('A minor').note()
|
||||
* note(irand(12).add(34)).segment(4).repeatCycles(2).s("gm_acoustic_guitar_nylon")
|
||||
*/
|
||||
const _chunk = function (n, func, pat, back = false) {
|
||||
const _repeatCycles = function (n, pat) {
|
||||
return slowcat(...Array(n).fill(pat));
|
||||
};
|
||||
|
||||
const { repeatCycles } = register('repeatCycles', _repeatCycles);
|
||||
|
||||
/**
|
||||
* Divides a pattern into a given number of parts, then cycles through those parts in turn, applying the given function to each part in turn (one part per cycle).
|
||||
* @name chunk
|
||||
* @synonyms slowChunk, slowchunk
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunk(4, x=>x.add(7))
|
||||
* .scale("A:minor").note()
|
||||
*/
|
||||
const _chunk = function (n, func, pat, back = false, fast = false) {
|
||||
const binary = Array(n - 1).fill(false);
|
||||
binary.unshift(true);
|
||||
const binary_pat = _iter(n, sequence(...binary), back);
|
||||
// Invert the 'back' because we want to shift the pattern forwards,
|
||||
// and so time backwards
|
||||
const binary_pat = _iter(n, sequence(...binary), !back);
|
||||
if (!fast) {
|
||||
pat = pat.repeatCycles(n);
|
||||
}
|
||||
return pat.when(binary_pat, func);
|
||||
};
|
||||
|
||||
export const chunk = register('chunk', function (n, func, pat) {
|
||||
return _chunk(n, func, pat, false);
|
||||
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
||||
return _chunk(n, func, pat, false, false);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2127,12 +2162,29 @@ export const chunk = register('chunk', function (n, func, pat) {
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "0 1 2 3".chunkBack(4, x=>x.add(7)).scale('A minor').note()
|
||||
* "0 1 2 3".chunkBack(4, x=>x.add(7))
|
||||
* .scale("A:minor").note()
|
||||
*/
|
||||
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
|
||||
return _chunk(n, func, pat, true);
|
||||
});
|
||||
|
||||
/**
|
||||
* Like `chunk`, but the cycles of the source pattern aren't repeated
|
||||
* for each set of chunks.
|
||||
* @name fastChunk
|
||||
* @synonyms fastchunk
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* "<0 8> 1 2 3 4 5 6 7"
|
||||
* .fastChunk(4, x => x.color('red')).slow(2)
|
||||
* .scale("C2:major").note()
|
||||
*/
|
||||
export const { fastchunk, fastChunk } = register(['fastchunk', 'fastChunk'], function (n, func, pat) {
|
||||
return _chunk(n, func, pat, false, true);
|
||||
});
|
||||
|
||||
// TODO - redefine elsewhere in terms of mask
|
||||
export const bypass = register('bypass', function (on, pat) {
|
||||
on = Boolean(parseInt(on));
|
||||
@@ -2144,6 +2196,8 @@ export const bypass = register('bypass', function (on, pat) {
|
||||
* @param {number} offset start point of loop in cycles
|
||||
* @param {number} cycles loop length in cycles
|
||||
* @example
|
||||
* note("<c d e f>").ribbon(1, 2).fast(2)
|
||||
* @example
|
||||
* // Looping a portion of randomness
|
||||
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
|
||||
*/
|
||||
@@ -2155,8 +2209,19 @@ export const duration = register('duration', function (value, pat) {
|
||||
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
||||
});
|
||||
|
||||
export const hsla = register('hsla', (h, s, l, a, pat) => {
|
||||
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);
|
||||
});
|
||||
|
||||
export const hsl = register('hsl', (h, s, l, pat) => {
|
||||
return pat.color(`hsl(${h}turn,${s * 100}%,${l * 100}%)`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets the color of the hap in visualizations like pianoroll or highlighting.
|
||||
* @name color
|
||||
* @synonyms colour
|
||||
* @param {string} color Hexadecimal or CSS color name
|
||||
*/
|
||||
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
|
||||
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
|
||||
@@ -2179,6 +2244,7 @@ export const velocity = register('velocity', function (velocity, pat) {
|
||||
/**
|
||||
*
|
||||
* Multiplies the hap duration with the given factor.
|
||||
* With samples, `clip` might be a better function to use ([more info](https://github.com/tidalcycles/strudel/pull/598))
|
||||
* @name legato
|
||||
* @memberof Pattern
|
||||
* @example
|
||||
@@ -2205,7 +2271,7 @@ export const legato = register('legato', function (value, pat) {
|
||||
* s("rhodes")
|
||||
* .chop(4)
|
||||
* .rev() // reverse order of chops
|
||||
* .loopAt(4) // fit sample into 4 cycles
|
||||
* .loopAt(2) // fit sample into 2 cycles
|
||||
*
|
||||
*/
|
||||
export const chop = register('chop', function (n, pat) {
|
||||
@@ -2217,6 +2283,14 @@ export const chop = register('chop', function (n, pat) {
|
||||
return pat.squeezeBind(func);
|
||||
});
|
||||
|
||||
/**
|
||||
* Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.
|
||||
* @name striate
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)
|
||||
*/
|
||||
export const striate = register('striate', function (n, pat) {
|
||||
const slices = Array.from({ length: n }, (x, i) => i);
|
||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||
@@ -2231,27 +2305,31 @@ export const striate = register('striate', function (n, pat) {
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes").loopAt(4)
|
||||
* s("rhodes").loopAt(2)
|
||||
*/
|
||||
// TODO - global cps clock
|
||||
const _loopAt = function (factor, pat, cps = 1) {
|
||||
const _loopAt = function (factor, pat, cps = 0.5) {
|
||||
return pat
|
||||
.speed((1 / factor) * cps)
|
||||
.unit('c')
|
||||
.slow(factor);
|
||||
};
|
||||
|
||||
/*
|
||||
/**
|
||||
* Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers.
|
||||
* Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.
|
||||
* @name slice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(1.5)
|
||||
* s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
|
||||
*/
|
||||
|
||||
const slice = register(
|
||||
export const slice = register(
|
||||
'slice',
|
||||
function (npat, ipat, opat) {
|
||||
return npat.innerBind((n) =>
|
||||
@@ -2259,9 +2337,9 @@ const slice = register(
|
||||
opat.outerBind((o) => {
|
||||
// If it's not an object, assume it's a string and make it a 's' control parameter
|
||||
o = o instanceof Object ? o : { s: o };
|
||||
// Remember we must stay pure and avoid editing the object directly
|
||||
const toAdd = { begin: i / n, end: (i + 1) / n, _slices: n };
|
||||
return pure({ ...toAdd, ...o });
|
||||
const begin = Array.isArray(n) ? n[i] : i / n;
|
||||
const end = Array.isArray(n) ? n[i + 1] : (i + 1) / n;
|
||||
return pure({ begin, end, _slices: n, ...o });
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -2269,37 +2347,61 @@ const slice = register(
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
/*
|
||||
/**
|
||||
* Works the same as slice, but changes the playback speed of each slice to match the duration of its step.
|
||||
* @name splice
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||
* s("breaks165").splice(8, "0 1 [2 3 0]@2 3 0@2 7").hurry(0.65)
|
||||
* s("breaks165")
|
||||
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
|
||||
*/
|
||||
|
||||
const splice = register(
|
||||
export const splice = register(
|
||||
'splice',
|
||||
function (npat, ipat, opat) {
|
||||
const sliced = slice(npat, ipat, opat);
|
||||
return sliced.withHap(function (hap) {
|
||||
return hap.withValue((v) => ({
|
||||
...{
|
||||
speed: (1 / v._slices / hap.whole.duration) * (v.speed || 1),
|
||||
unit: 'c',
|
||||
},
|
||||
...v,
|
||||
}));
|
||||
return new Pattern((state) => {
|
||||
// TODO - default cps to 0.5
|
||||
const cps = state.controls._cps || 1;
|
||||
const haps = sliced.query(state);
|
||||
return haps.map((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...{
|
||||
speed: (cps / v._slices / hap.whole.duration) * (v.speed || 1),
|
||||
unit: 'c',
|
||||
},
|
||||
...v,
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
false, // turns off auto-patternification
|
||||
);
|
||||
|
||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return _loopAt(factor, pat, 1);
|
||||
export const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
||||
return new Pattern((state) => _loopAt(factor, pat, state.controls._cps).query(state));
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes the sample fit its event duration. Good for rhythmical loops like drum breaks.
|
||||
* Similar to `loopAt`.
|
||||
* @name fit
|
||||
* @example
|
||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||
* s("rhodes/2").fit()
|
||||
*/
|
||||
export const fit = register('fit', (pat) =>
|
||||
pat.withHaps((haps, state) =>
|
||||
haps.map((hap) =>
|
||||
hap.withValue((v) => ({
|
||||
...v,
|
||||
speed: (state.controls._cps || 1) / hap.whole.duration,
|
||||
unit: 'c',
|
||||
})),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Makes the sample fit the given number of cycles and cps value, by
|
||||
* changing the speed. Please note that at some point cps will be
|
||||
@@ -2313,6 +2415,38 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
|
||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||
*/
|
||||
// TODO - global cps clock
|
||||
const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||
export const { loopAtCps, loopatcps } = register(['loopAtCps', 'loopatcps'], function (factor, cps, pat) {
|
||||
return _loopAt(factor, pat, cps);
|
||||
});
|
||||
|
||||
/** exposes a custom value at query time. basically allows mutating state without evaluation */
|
||||
export const ref = (accessor) =>
|
||||
pure(1)
|
||||
.withValue(() => reify(accessor()))
|
||||
.innerJoin();
|
||||
|
||||
let fadeGain = (p) => (p < 0.5 ? 1 : 1 - (p - 0.5) / 0.5);
|
||||
|
||||
/**
|
||||
* Cross-fades between left and right from 0 to 1:
|
||||
* - 0 = (full left, no right)
|
||||
* - .5 = (both equal)
|
||||
* - 1 = (no left, full right)
|
||||
*
|
||||
* @name xfade
|
||||
* @example
|
||||
* xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))
|
||||
*/
|
||||
export let xfade = (a, pos, b) => {
|
||||
pos = reify(pos);
|
||||
a = reify(a);
|
||||
b = reify(b);
|
||||
let gaina = pos.fmap((v) => ({ gain: fadeGain(v) }));
|
||||
let gainb = pos.fmap((v) => ({ gain: fadeGain(1 - v) }));
|
||||
return stack(a.mul(gaina), b.mul(gainb));
|
||||
};
|
||||
|
||||
// the prototype version is actually flipped so left/right makes sense
|
||||
Pattern.prototype.xfade = function (pos, b) {
|
||||
return xfade(this, pos, b);
|
||||
};
|
||||
|
||||
+151
-156
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
pianoroll.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/pianoroll.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/pianoroll.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -29,129 +29,26 @@ const getValue = (e) => {
|
||||
return value;
|
||||
};
|
||||
|
||||
Pattern.prototype.pianoroll = function ({
|
||||
cycles = 4,
|
||||
playhead = 0.5,
|
||||
overscan = 1,
|
||||
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,
|
||||
} = {}) {
|
||||
const ctx = getDrawContext();
|
||||
const w = ctx.canvas.width;
|
||||
const h = ctx.canvas.height;
|
||||
Pattern.prototype.pianoroll = function (options = {}) {
|
||||
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
||||
|
||||
let from = -cycles * playhead;
|
||||
let to = cycles * (1 - playhead);
|
||||
|
||||
if (timeframeProp) {
|
||||
console.warn('timeframe is deprecated! use from/to instead');
|
||||
from = 0;
|
||||
to = timeframeProp;
|
||||
}
|
||||
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();
|
||||
|
||||
this.draw(
|
||||
(ctx, events, t) => {
|
||||
ctx.fillStyle = background;
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
if (!smear) {
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
(ctx, haps, t) => {
|
||||
const inFrame = (event) =>
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
||||
events.filter(inFrame).forEach((event) => {
|
||||
const isActive = event.whole.begin <= t && event.whole.end > t;
|
||||
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(t / 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);
|
||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||
pianoroll({
|
||||
...options,
|
||||
time: t,
|
||||
ctx,
|
||||
haps: haps.filter(inFrame),
|
||||
});
|
||||
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();
|
||||
},
|
||||
{
|
||||
from: from - overscan,
|
||||
to: to + overscan,
|
||||
onQuery: (events) => {
|
||||
const { min, max, values } = events.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) => String(a).localeCompare(String(b)));
|
||||
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||
},
|
||||
},
|
||||
);
|
||||
return this;
|
||||
@@ -159,6 +56,40 @@ Pattern.prototype.pianoroll = function ({
|
||||
|
||||
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||
// it will probably replace the above in the future
|
||||
|
||||
/**
|
||||
* Displays a midi-style piano roll
|
||||
*
|
||||
* @name pianoroll
|
||||
* @param {Object} options Object containing all the optional following parameters as key value pairs:
|
||||
* @param {integer} cycles number of cycles to be displayed at the same time - defaults to 4
|
||||
* @param {number} playhead location of the active notes on the time axis - 0 to 1, defaults to 0.5
|
||||
* @param {boolean} vertical displays the roll vertically - 0 by default
|
||||
* @param {boolean} labels displays labels on individual notes (see the label function) - 0 by default
|
||||
* @param {boolean} flipTime reverse the direction of the roll - 0 by default
|
||||
* @param {boolean} flipValues reverse the relative location of notes on the value axis - 0 by default
|
||||
* @param {number} overscan lookup X cycles outside of the cycles window to display notes in advance - 1 by default
|
||||
* @param {boolean} hideNegative hide notes with negative time (before starting playing the pattern) - 0 by default
|
||||
* @param {boolean} smear notes leave a solid trace - 0 by default
|
||||
* @param {boolean} fold notes takes the full value axis width - 0 by default
|
||||
* @param {string} active hexadecimal or CSS color of the active notes - defaults to #FFCA28
|
||||
* @param {string} inactive hexadecimal or CSS color of the inactive notes - defaults to #7491D2
|
||||
* @param {string} background hexadecimal or CSS color of the background - defaults to transparent
|
||||
* @param {string} playheadColor hexadecimal or CSS color of the line representing the play head - defaults to white
|
||||
* @param {boolean} fill notes are filled with color (otherwise only the label is displayed) - 0 by default
|
||||
* @param {boolean} fillActive active notes are filled with color - 0 by default
|
||||
* @param {boolean} stroke notes are shown with colored borders - 0 by default
|
||||
* @param {boolean} strokeActive active notes are shown with colored borders - 0 by default
|
||||
* @param {boolean} hideInactive only active notes are shown - 0 by default
|
||||
* @param {boolean} colorizeInactive use note color for inactive notes - 1 by default
|
||||
* @param {string} fontFamily define the font used by notes labels - defaults to 'monospace'
|
||||
* @param {integer} minMidi minimum note value to display on the value axis - defaults to 10
|
||||
* @param {integer} maxMidi maximum note value to display on the value axis - defaults to 90
|
||||
* @param {boolean} autorange automatically calculate the minMidi and maxMidi parameters - 0 by default
|
||||
*
|
||||
* @example
|
||||
* note("C2 A2 G2").euclid(5,8).s('piano').clip(1).color('salmon').pianoroll({vertical:1, labels:1})
|
||||
*/
|
||||
export function pianoroll({
|
||||
time,
|
||||
haps,
|
||||
@@ -181,6 +112,14 @@ export function pianoroll({
|
||||
timeframe: timeframeProp,
|
||||
fold = 0,
|
||||
vertical = 0,
|
||||
labels = false,
|
||||
fill = 1,
|
||||
fillActive = false,
|
||||
strokeActive = true,
|
||||
stroke,
|
||||
hideInactive = 0,
|
||||
colorizeInactive = 1,
|
||||
fontFamily,
|
||||
ctx,
|
||||
} = {}) {
|
||||
const w = ctx.canvas.width;
|
||||
@@ -221,53 +160,83 @@ export function pianoroll({
|
||||
maxMidi = max;
|
||||
valueExtent = maxMidi - minMidi + 1;
|
||||
}
|
||||
// foldValues = values.sort((a, b) => a - b);
|
||||
foldValues = values.sort((a, b) => String(a).localeCompare(String(b)));
|
||||
foldValues = values.sort((a, b) =>
|
||||
typeof a === 'number' && typeof b === 'number'
|
||||
? a - b
|
||||
: typeof a === 'number'
|
||||
? 1
|
||||
: String(a).localeCompare(String(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);
|
||||
});
|
||||
haps.forEach((event) => {
|
||||
const isActive = event.whole.begin <= time && event.endClipped > time;
|
||||
let strokeCurrent = stroke ?? (strokeActive && isActive);
|
||||
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
|
||||
if (hideInactive && !isActive) {
|
||||
return;
|
||||
}
|
||||
let color = event.value?.color || event.context?.color;
|
||||
active = color || active;
|
||||
inactive = colorizeInactive ? color || inactive : inactive;
|
||||
color = isActive ? active : inactive;
|
||||
ctx.fillStyle = fillCurrent ? color : 'transparent';
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = event.context.velocity ?? event.value?.gain ?? 1;
|
||||
const timeProgress = (event.whole.begin - (flipTime ? to : from)) / timeExtent;
|
||||
const timePx = scale(timeProgress, ...timeRange);
|
||||
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||
const value = getValue(event);
|
||||
const valueProgress = fold
|
||||
? foldValues.indexOf(value) / foldValues.length
|
||||
: (Number(value) - minMidi) / valueExtent;
|
||||
const valuePx = scale(valueProgress, ...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
|
||||
];
|
||||
}
|
||||
/* const xFactor = Math.sin(performance.now() / 500) + 1;
|
||||
coords[0] *= xFactor; */
|
||||
|
||||
if (strokeCurrent) {
|
||||
ctx.strokeRect(...coords);
|
||||
}
|
||||
if (fillCurrent) {
|
||||
ctx.fillRect(...coords);
|
||||
}
|
||||
//ctx.ellipse(...ellipseFromRect(...coords))
|
||||
if (labels) {
|
||||
const defaultLabel = event.value.note ?? event.value.s + (event.value.n ? `:${event.value.n}` : '');
|
||||
const { label: inactiveLabel, activeLabel } = event.value;
|
||||
const customLabel = isActive ? activeLabel || inactiveLabel : inactiveLabel;
|
||||
const label = customLabel ?? defaultLabel;
|
||||
let measure = vertical ? durationPx : barThickness * 0.75;
|
||||
ctx.font = `${measure}px ${fontFamily || 'monospace'}`;
|
||||
// font color
|
||||
ctx.fillStyle = /* isActive && */ !fillCurrent ? color : 'black';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(label, ...coords);
|
||||
}
|
||||
});
|
||||
ctx.globalAlpha = 1; // reset!
|
||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||
// draw playhead
|
||||
@@ -284,7 +253,7 @@ export function pianoroll({
|
||||
return this;
|
||||
}
|
||||
|
||||
function getOptions(drawTime, options = {}) {
|
||||
export function getDrawOptions(drawTime, options = {}) {
|
||||
let [lookbehind, lookahead] = drawTime;
|
||||
lookbehind = Math.abs(lookbehind);
|
||||
const cycles = lookahead + lookbehind;
|
||||
@@ -292,6 +261,32 @@ function getOptions(drawTime, options = {}) {
|
||||
return { fold: 1, ...options, cycles, playhead };
|
||||
}
|
||||
|
||||
export const getPunchcardPainter =
|
||||
(options = {}) =>
|
||||
(ctx, time, haps, drawTime, paintOptions = {}) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { ...paintOptions, ...options }) });
|
||||
|
||||
Pattern.prototype.punchcard = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) => pianoroll({ ctx, time, haps, ...getOptions(drawTime, options) }));
|
||||
return this.onPaint(getPunchcardPainter(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a vertical pianoroll with event labels.
|
||||
* Supports all the same options as pianoroll.
|
||||
*
|
||||
* @name wordfall
|
||||
*/
|
||||
Pattern.prototype.wordfall = function (options) {
|
||||
return this.punchcard({ vertical: 1, labels: 1, stroke: 0, fillActive: 1, active: 'white', ...options });
|
||||
};
|
||||
|
||||
/* Pattern.prototype.pianoroll = function (options) {
|
||||
return this.onPaint((ctx, time, haps, drawTime) =>
|
||||
pianoroll({ ctx, time, haps, ...getDrawOptions(drawTime, { fold: 0, ...options }) }),
|
||||
);
|
||||
}; */
|
||||
|
||||
export function drawPianoroll(options) {
|
||||
const { drawTime, ...rest } = options;
|
||||
pianoroll({ ...getDrawOptions(drawTime), ...rest });
|
||||
}
|
||||
|
||||
+139
-42
@@ -3,6 +3,7 @@ import { evaluate as _evaluate } from './evaluate.mjs';
|
||||
import { logger } from './logger.mjs';
|
||||
import { setTime } from './time.mjs';
|
||||
import { evalScope } from './evaluate.mjs';
|
||||
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||
|
||||
export function repl({
|
||||
interval,
|
||||
@@ -15,64 +16,160 @@ export function repl({
|
||||
transpiler,
|
||||
onToggle,
|
||||
editPattern,
|
||||
onUpdateState,
|
||||
}) {
|
||||
const state = {
|
||||
schedulerError: undefined,
|
||||
evalError: undefined,
|
||||
code: '// LOADING',
|
||||
activeCode: '// LOADING',
|
||||
pattern: undefined,
|
||||
miniLocations: [],
|
||||
widgets: [],
|
||||
pending: false,
|
||||
started: false,
|
||||
};
|
||||
|
||||
const updateState = (update) => {
|
||||
Object.assign(state, update);
|
||||
state.isDirty = state.code !== state.activeCode;
|
||||
state.error = state.evalError || state.schedulerError;
|
||||
onUpdateState?.(state);
|
||||
};
|
||||
|
||||
const scheduler = new Cyclist({
|
||||
interval,
|
||||
onTrigger: async (hap, deadline, duration, cps) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
}
|
||||
},
|
||||
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||
onError: onSchedulerError,
|
||||
getTime,
|
||||
onToggle,
|
||||
onToggle: (started) => {
|
||||
updateState({ started });
|
||||
onToggle?.(started);
|
||||
},
|
||||
});
|
||||
let pPatterns = {};
|
||||
let allTransform;
|
||||
|
||||
const hush = function () {
|
||||
pPatterns = {};
|
||||
allTransform = undefined;
|
||||
return silence;
|
||||
};
|
||||
|
||||
const setPattern = (pattern, autostart = true) => {
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
};
|
||||
setTime(() => scheduler.now()); // TODO: refactor?
|
||||
const evaluate = async (code, autostart = true) => {
|
||||
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
const toggle = () => scheduler.toggle();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
const setCpm = (cpm) => scheduler.setCps(cpm / 60);
|
||||
const all = function (transform) {
|
||||
allTransform = transform;
|
||||
return silence;
|
||||
};
|
||||
|
||||
// set pattern methods that use this repl via closure
|
||||
const injectPatternMethods = () => {
|
||||
Pattern.prototype.p = function (id) {
|
||||
pPatterns[id] = this;
|
||||
return this;
|
||||
};
|
||||
Pattern.prototype.q = function (id) {
|
||||
return silence;
|
||||
};
|
||||
try {
|
||||
for (let i = 1; i < 10; ++i) {
|
||||
Object.defineProperty(Pattern.prototype, `d${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(Pattern.prototype, `p${i}`, {
|
||||
get() {
|
||||
return this.p(i);
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
Pattern.prototype[`q${i}`] = silence;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('injectPatternMethods: error:', err);
|
||||
}
|
||||
const cpm = register('cpm', function (cpm, pat) {
|
||||
return pat._fast(cpm / 60 / scheduler.cps);
|
||||
});
|
||||
evalScope({
|
||||
all,
|
||||
hush,
|
||||
cpm,
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
setCpm,
|
||||
setcpm: setCpm,
|
||||
});
|
||||
};
|
||||
|
||||
const evaluate = async (code, autostart = true, shouldHush = true) => {
|
||||
if (!code) {
|
||||
throw new Error('no code to evaluate');
|
||||
}
|
||||
try {
|
||||
beforeEval?.({ code });
|
||||
scheduler.setCps(1); // reset cps in case the code does not contain a setCps call
|
||||
// problem: when the code does contain a setCps after an awaited promise,
|
||||
// the cps will be 1 until the promise resolves
|
||||
// example:
|
||||
/*
|
||||
await new Promise(resolve => setTimeout(resolve,1000))
|
||||
setCps(.5)
|
||||
note("c a f e")
|
||||
*/
|
||||
// to make sure the setCps inside the code is called immediately,
|
||||
// it has to be placed first
|
||||
let { pattern } = await _evaluate(code, transpiler);
|
||||
|
||||
updateState({ code, pending: true });
|
||||
injectPatternMethods();
|
||||
await beforeEval?.({ code });
|
||||
shouldHush && hush();
|
||||
let { pattern, meta } = await _evaluate(code, transpiler);
|
||||
if (Object.keys(pPatterns).length) {
|
||||
pattern = stack(...Object.values(pPatterns));
|
||||
}
|
||||
if (allTransform) {
|
||||
pattern = allTransform(pattern);
|
||||
}
|
||||
if (!isPattern(pattern)) {
|
||||
const message = `got "${typeof evaluated}" instead of pattern`;
|
||||
throw new Error(message + (typeof evaluated === 'function' ? ', did you forget to call a function?' : '.'));
|
||||
}
|
||||
logger(`[eval] code updated`);
|
||||
pattern = editPattern?.(pattern) || pattern;
|
||||
scheduler.setPattern(pattern, autostart);
|
||||
afterEval?.({ code, pattern });
|
||||
setPattern(pattern, autostart);
|
||||
updateState({
|
||||
miniLocations: meta?.miniLocations || [],
|
||||
widgets: meta?.widgets || [],
|
||||
activeCode: code,
|
||||
pattern,
|
||||
evalError: undefined,
|
||||
schedulerError: undefined,
|
||||
pending: false,
|
||||
});
|
||||
afterEval?.({ code, pattern, meta });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
updateState({ evalError: err, pending: false });
|
||||
onEvalError?.(err);
|
||||
}
|
||||
};
|
||||
const stop = () => scheduler.stop();
|
||||
const start = () => scheduler.start();
|
||||
const pause = () => scheduler.pause();
|
||||
const setCps = (cps) => scheduler.setCps(cps);
|
||||
evalScope({
|
||||
setCps,
|
||||
setcps: setCps,
|
||||
});
|
||||
return { scheduler, evaluate, start, stop, pause, setCps };
|
||||
const setCode = (code) => updateState({ code });
|
||||
return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state };
|
||||
}
|
||||
|
||||
export const getTrigger =
|
||||
({ getTime, defaultOutput }) =>
|
||||
async (hap, deadline, duration, cps) => {
|
||||
try {
|
||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||
await defaultOutput(hap, deadline, duration, cps);
|
||||
}
|
||||
if (hap.context.onTrigger) {
|
||||
// call signature of output / onTrigger is different...
|
||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||
}
|
||||
} catch (err) {
|
||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
+170
-22
@@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
import { Hap } from './hap.mjs';
|
||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||
import Fraction from './fraction.mjs';
|
||||
import { id } from './util.mjs';
|
||||
import { id, _mod, clamp, objectMap } from './util.mjs';
|
||||
|
||||
export function steady(value) {
|
||||
// A continuous value
|
||||
@@ -27,9 +27,11 @@ export const isaw2 = isaw.toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* "c3 [eb3,g3] g2 [g3,bb3]".legato(saw.slow(4)).note()
|
||||
* note("<c3 [eb3,g3] g2 [g3,bb3]>*8")
|
||||
* .clip(saw.slow(2))
|
||||
* @example
|
||||
* saw.range(0,8).segment(8).scale('C major').slow(4).note()
|
||||
* n(saw.range(0,8).segment(8))
|
||||
* .scale('C major')
|
||||
*
|
||||
*/
|
||||
export const saw = signal((t) => t % 1);
|
||||
@@ -42,7 +44,8 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* sine.segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
* n(sine.segment(16).range(0,15))
|
||||
* .scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const sine = sine2.fromBipolar();
|
||||
@@ -52,7 +55,8 @@ export const sine = sine2.fromBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* stack(sine,cosine).segment(16).range(0,15).slow(2).scale('C minor').note()
|
||||
* n(stack(sine,cosine).segment(16).range(0,15))
|
||||
* .scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const cosine = sine._early(Fraction(1).div(4));
|
||||
@@ -63,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* square.segment(2).range(0,7).scale('C minor').note()
|
||||
* n(square.segment(4).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||
@@ -74,7 +78,7 @@ export const square2 = square.toBipolar();
|
||||
*
|
||||
* @return {Pattern}
|
||||
* @example
|
||||
* tri.segment(8).range(0,7).scale('C minor').note()
|
||||
* n(tri.segment(8).range(0,7)).scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const tri = fastcat(isaw, saw);
|
||||
@@ -101,6 +105,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
||||
|
||||
const timeToRandsPrime = (seed, n) => {
|
||||
const result = [];
|
||||
// eslint-disable-next-line
|
||||
for (let i = 0; i < n; ++n) {
|
||||
result.push(intSeedToRand(seed));
|
||||
seed = xorwise(seed);
|
||||
@@ -117,8 +122,8 @@ const timeToRands = (t, n) => timeToRandsPrime(timeToIntSeed(t), n);
|
||||
/**
|
||||
* A discrete pattern of numbers from 0 to n-1
|
||||
* @example
|
||||
* run(4).scale('C4 major').note()
|
||||
* // "0 1 2 3".scale('C4 major').note()
|
||||
* n(run(4)).scale("C4:pentatonic")
|
||||
* // n("0 1 2 3").scale("C4:pentatonic")
|
||||
*/
|
||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||
|
||||
@@ -128,7 +133,7 @@ export const run = (n) => saw.range(0, n).floor().segment(n);
|
||||
* @name rand
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(rand.range(500,2000))
|
||||
* s("bd*4,hh*8").cutoff(rand.range(500,8000))
|
||||
*
|
||||
*/
|
||||
export const rand = signal(timeToRand);
|
||||
@@ -150,17 +155,158 @@ export const _irand = (i) => rand.fmap((x) => Math.trunc(x * i));
|
||||
* @param {number} n max value (exclusive)
|
||||
* @example
|
||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
||||
* irand(8).struct("x(3,8)").scale('C minor').note()
|
||||
* n(irand(8)).struct("x x*2 x x*3").scale("C:minor")
|
||||
*
|
||||
*/
|
||||
export const irand = (ipat) => reify(ipat).fmap(_irand).innerJoin();
|
||||
|
||||
const _pick = function (lookup, pat, modulo = true) {
|
||||
const array = Array.isArray(lookup);
|
||||
const len = Object.keys(lookup).length;
|
||||
|
||||
lookup = objectMap(lookup, reify);
|
||||
|
||||
if (len === 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.fmap((i) => {
|
||||
let key = i;
|
||||
if (array) {
|
||||
key = modulo ? Math.round(key) % len : clamp(Math.round(key), 0, lookup.length - 1);
|
||||
}
|
||||
return lookup[key];
|
||||
});
|
||||
};
|
||||
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `inhabit`, but maintains the structure of the original patterns.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("<0 1 2!2 3>".pick(["g a", "e f", "f g f g" , "g c d"]))
|
||||
* @example
|
||||
* sound("<0 1 [2,0]>".pick(["bd sd", "cp cp", "hh hh"]))
|
||||
* @example
|
||||
* sound("<0!2 [0,1] 1>".pick(["bd(3,8)", "sd sd"]))
|
||||
* @example
|
||||
* s("<a!2 [a,b] b>".pick({a: "bd(3,8)", b: "sd sd"}))
|
||||
*/
|
||||
|
||||
export const pick = function (lookup, pat) {
|
||||
// backward compatibility - the args used to be flipped
|
||||
if (Array.isArray(pat)) {
|
||||
[pat, lookup] = [lookup, pat];
|
||||
}
|
||||
return __pick(lookup, pat);
|
||||
};
|
||||
|
||||
const __pick = register('pick', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).innerJoin();
|
||||
});
|
||||
|
||||
/** * The same as `pick`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const pickmod = register('pickmod', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).innerJoin();
|
||||
});
|
||||
|
||||
/** * pickF lets you use a pattern of numbers to pick which function to apply to another pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* s("bd [rim hh]").pickF("<0 1 2>", [rev,jux(rev),fast(2)])
|
||||
* @example
|
||||
* note("<c2 d2>(3,8)").s("square")
|
||||
* .pickF("<0 2> 1", [jux(rev),fast(2),x=>x.lpf(800)])
|
||||
*/
|
||||
export const pickF = register('pickF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pick(lookup, funcs));
|
||||
});
|
||||
|
||||
/** * The same as `pickF`, but if you pick a number greater than the size of the functions list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* @param {Pattern} pat
|
||||
* @param {Pattern} lookup a pattern of indices
|
||||
* @param {function[]} funcs the array of functions from which to pull
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
export const pickmodF = register('pickmodF', function (lookup, funcs, pat) {
|
||||
return pat.apply(pickmod(lookup, funcs));
|
||||
});
|
||||
|
||||
/**
|
||||
/** * Picks patterns (or plain values) either from a list (by index) or a lookup table (by name).
|
||||
* Similar to `pick`, but cycles are squeezed into the target ('inhabited') pattern.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* "<a b [a,b]>".inhabit({a: s("bd(3,8)"),
|
||||
b: s("cp sd")
|
||||
})
|
||||
* @example
|
||||
* s("a@2 [a b] a".inhabit({a: "bd(3,8)", b: "sd sd"})).slow(4)
|
||||
*/
|
||||
export const inhabit = register('inhabit', function (lookup, pat) {
|
||||
return _pick(lookup, pat, true).squeezeJoin();
|
||||
});
|
||||
|
||||
/** * The same as `inhabit`, but if you pick a number greater than the size of the list,
|
||||
* it wraps around, rather than sticking at the maximum value.
|
||||
* For example, if you pick the fifth pattern of a list of three, you'll get the
|
||||
* second one.
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
*/
|
||||
|
||||
export const inhabitmod = register('inhabit', function (lookup, pat) {
|
||||
return _pick(lookup, pat, false).squeezeJoin();
|
||||
});
|
||||
|
||||
/**
|
||||
* Pick from the list of values (or patterns of values) via the index using the given
|
||||
* pattern of integers. The selected pattern will be compressed to fit the duration of the selecting event
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note(squeeze("<0@2 [1!2] 2>", ["g a", "f g f g" , "g a c d"]))
|
||||
*/
|
||||
|
||||
export const squeeze = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat
|
||||
.fmap((i) => {
|
||||
const key = _mod(Math.round(i), xs.length);
|
||||
return xs[key];
|
||||
})
|
||||
.squeezeJoin();
|
||||
};
|
||||
|
||||
export const __chooseWith = (pat, xs) => {
|
||||
xs = xs.map(reify);
|
||||
if (xs.length == 0) {
|
||||
return silence;
|
||||
}
|
||||
return pat.range(0, xs.length).fmap((i) => xs[Math.floor(i)]);
|
||||
|
||||
return pat.range(0, xs.length).fmap((i) => {
|
||||
const key = Math.min(Math.max(Math.floor(i), 0), xs.length - 1);
|
||||
return xs[key];
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Choose from the list of values (or patterns of values) using the given
|
||||
@@ -168,6 +314,8 @@ export const __chooseWith = (pat, xs) => {
|
||||
* @param {Pattern} pat
|
||||
* @param {*} xs
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
|
||||
*/
|
||||
export const chooseWith = (pat, xs) => {
|
||||
return __chooseWith(pat, xs).outerJoin();
|
||||
@@ -216,9 +364,9 @@ Pattern.prototype.choose2 = function (...xs) {
|
||||
* Picks one of the elements at random each cycle.
|
||||
* @returns {Pattern}
|
||||
* @example
|
||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
||||
* chooseCycles("bd", "hh", "sd").s().fast(8)
|
||||
* @example
|
||||
* "bd | hh | sd".s().fast(4)
|
||||
* s("bd | hh | sd").fast(8)
|
||||
*/
|
||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||
|
||||
@@ -261,7 +409,7 @@ export const perlinWith = (pat) => {
|
||||
* @name perlin
|
||||
* @example
|
||||
* // randomly change the cutoff
|
||||
* s("bd sd,hh*4").cutoff(perlin.range(500,2000))
|
||||
* s("bd*4,hh*8").cutoff(perlin.range(500,8000))
|
||||
*
|
||||
*/
|
||||
export const perlin = perlinWith(time.fmap((v) => Number(v)));
|
||||
@@ -303,7 +451,7 @@ export const degradeBy = register('degradeBy', function (x, pat) {
|
||||
export const degrade = register('degrade', (pat) => pat._degradeBy(0.5));
|
||||
|
||||
/**
|
||||
* Inverse of {@link Pattern#degradeBy}: Randomly removes events from the pattern by a given amount.
|
||||
* Inverse of `degradeBy`: Randomly removes events from the pattern by a given amount.
|
||||
* 0 = 100% chance of removal
|
||||
* 1 = 0% chance of removal
|
||||
* Events that would be removed by degradeBy are let through by undegradeBy and vice versa (see second example).
|
||||
@@ -327,7 +475,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability.
|
||||
* Similar to {@link Pattern#someCyclesBy}
|
||||
* Similar to `someCyclesBy`
|
||||
*
|
||||
* @name sometimesBy
|
||||
* @memberof Pattern
|
||||
@@ -335,7 +483,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").sometimesBy(.4, x=>x.speed("0.5"))
|
||||
* s("hh*8").sometimesBy(.4, x=>x.speed("0.5"))
|
||||
*/
|
||||
|
||||
export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
@@ -353,7 +501,7 @@ export const sometimesBy = register('sometimesBy', function (patx, func, pat) {
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh*4").sometimes(x=>x.speed("0.5"))
|
||||
* s("hh*8").sometimes(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const sometimes = register('sometimes', function (func, pat) {
|
||||
return pat._sometimesBy(0.5, func);
|
||||
@@ -362,7 +510,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
||||
/**
|
||||
*
|
||||
* Randomly applies the given function by the given probability on a cycle by cycle basis.
|
||||
* Similar to {@link Pattern#sometimesBy}
|
||||
* Similar to `sometimesBy`
|
||||
*
|
||||
* @name someCyclesBy
|
||||
* @memberof Pattern
|
||||
@@ -370,7 +518,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
||||
* @param {function} function - the transformation to apply
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
* s("bd,hh*8").someCyclesBy(.3, x=>x.speed("0.5"))
|
||||
*/
|
||||
|
||||
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||
@@ -392,7 +540,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
||||
* @memberof Pattern
|
||||
* @returns Pattern
|
||||
* @example
|
||||
* s("hh(3,8)").someCycles(x=>x.speed("0.5"))
|
||||
* s("bd,hh*8").someCycles(x=>x.speed("0.5"))
|
||||
*/
|
||||
export const someCycles = register('someCycles', function (func, pat) {
|
||||
return pat._someCyclesBy(0.5, func);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Pattern } from './index.mjs';
|
||||
|
||||
// polar coords -> xy
|
||||
function fromPolar(angle, radius, cx, cy) {
|
||||
const radians = ((angle - 90) * Math.PI) / 180;
|
||||
return [cx + Math.cos(radians) * radius, cy + Math.sin(radians) * radius];
|
||||
}
|
||||
|
||||
const xyOnSpiral = (angle, margin, cx, cy, rotate = 0) => fromPolar((angle + rotate) * 360, margin * angle, cx, cy); // TODO: logSpiral
|
||||
|
||||
// draw spiral / segment of spiral
|
||||
function spiralSegment(options) {
|
||||
let {
|
||||
ctx,
|
||||
from = 0,
|
||||
to = 3,
|
||||
margin = 50,
|
||||
cx = 100,
|
||||
cy = 100,
|
||||
rotate = 0,
|
||||
thickness = margin / 2,
|
||||
color = '#0000ff30',
|
||||
cap = 'round',
|
||||
stretch = 1,
|
||||
fromOpacity = 1,
|
||||
toOpacity = 1,
|
||||
} = options;
|
||||
from *= stretch;
|
||||
to *= stretch;
|
||||
rotate *= stretch;
|
||||
ctx.lineWidth = thickness;
|
||||
ctx.lineCap = cap;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = fromOpacity;
|
||||
|
||||
ctx.beginPath();
|
||||
let [sx, sy] = xyOnSpiral(from, margin, cx, cy, rotate);
|
||||
ctx.moveTo(sx, sy);
|
||||
|
||||
const increment = 1 / 60;
|
||||
let angle = from;
|
||||
while (angle <= to) {
|
||||
const [x, y] = xyOnSpiral(angle, margin, cx, cy, rotate);
|
||||
//ctx.lineWidth = angle*thickness;
|
||||
ctx.globalAlpha = ((angle - from) / (to - from)) * toOpacity;
|
||||
ctx.lineTo(x, y);
|
||||
angle += increment;
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Pattern.prototype.spiral = function (options = {}) {
|
||||
const {
|
||||
stretch = 1,
|
||||
size = 80,
|
||||
thickness = size / 2,
|
||||
cap = 'butt', // round butt squar,
|
||||
inset = 3, // start angl,
|
||||
playheadColor = '#ffffff90',
|
||||
playheadLength = 0.02,
|
||||
playheadThickness = thickness,
|
||||
padding = 0,
|
||||
steady = 1,
|
||||
inactiveColor = '#ffffff20',
|
||||
colorizeInactive = 0,
|
||||
fade = true,
|
||||
// logSpiral = true,
|
||||
} = options;
|
||||
|
||||
function spiral({ ctx, time, haps, drawTime }) {
|
||||
const [w, h] = [ctx.canvas.width, ctx.canvas.height];
|
||||
ctx.clearRect(0, 0, w * 2, h * 2);
|
||||
const [cx, cy] = [w / 2, h / 2];
|
||||
const settings = {
|
||||
margin: size / stretch,
|
||||
cx,
|
||||
cy,
|
||||
stretch,
|
||||
cap,
|
||||
thickness,
|
||||
};
|
||||
|
||||
const playhead = {
|
||||
...settings,
|
||||
thickness: playheadThickness,
|
||||
from: inset - playheadLength,
|
||||
to: inset,
|
||||
color: playheadColor,
|
||||
};
|
||||
|
||||
const [min] = drawTime;
|
||||
const rotate = steady * time;
|
||||
haps.forEach((hap) => {
|
||||
const isActive = hap.whole.begin <= time && hap.endClipped > time;
|
||||
const from = hap.whole.begin - time + inset;
|
||||
const to = hap.endClipped - time + inset - padding;
|
||||
const { color } = hap.context;
|
||||
const opacity = fade ? 1 - Math.abs((hap.whole.begin - time) / min) : 1;
|
||||
spiralSegment({
|
||||
ctx,
|
||||
...settings,
|
||||
from,
|
||||
to,
|
||||
rotate,
|
||||
color: colorizeInactive || isActive ? color : inactiveColor,
|
||||
fromOpacity: opacity,
|
||||
toOpacity: opacity,
|
||||
});
|
||||
});
|
||||
spiralSegment({
|
||||
ctx,
|
||||
...playhead,
|
||||
rotate,
|
||||
});
|
||||
}
|
||||
|
||||
return this.onPaint((ctx, time, haps, drawTime) => spiral({ ctx, time, haps, drawTime }));
|
||||
};
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
slowcat,
|
||||
cat,
|
||||
sequence,
|
||||
palindrome,
|
||||
polymeter,
|
||||
polymeterSteps,
|
||||
polyrhythm,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
rev,
|
||||
time,
|
||||
run,
|
||||
pick,
|
||||
} from '../index.mjs';
|
||||
|
||||
import { steady } from '../signal.mjs';
|
||||
@@ -182,13 +184,17 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trig(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.add.trig(20, 30)
|
||||
.early(2),
|
||||
sequence(26, 27, 36, 37),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).add.trigzero(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.add.trigzero(20, 30)
|
||||
.early(2),
|
||||
sequence(21, 22, 31, 32),
|
||||
);
|
||||
});
|
||||
@@ -230,13 +236,17 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trig(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.keep.trig(20, 30)
|
||||
.early(2),
|
||||
sequence(6, 7, 6, 7),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keep.trigzero(20, 30).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.keep.trigzero(20, 30)
|
||||
.early(2),
|
||||
sequence(1, 2, 1, 2),
|
||||
);
|
||||
});
|
||||
@@ -272,13 +282,17 @@ describe('Pattern', () => {
|
||||
});
|
||||
it('can Trig() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trig(false, true).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.keepif.trig(false, true)
|
||||
.early(2),
|
||||
sequence(silence, silence, 6, 7),
|
||||
);
|
||||
});
|
||||
it('can Trigzero() structure', () => {
|
||||
sameFirst(
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10).keepif.trigzero(false, true).early(2),
|
||||
slowcat(sequence(1, 2, 3, 4), 5, sequence(6, 7, 8, 9), 10)
|
||||
.keepif.trigzero(false, true)
|
||||
.early(2),
|
||||
sequence(silence, silence, 1, 2),
|
||||
);
|
||||
});
|
||||
@@ -571,6 +585,18 @@ describe('Pattern', () => {
|
||||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
||||
});
|
||||
});
|
||||
describe('palindrome()', () => {
|
||||
it('Can create palindrome', () => {
|
||||
expect(
|
||||
fastcat('a', 'b', 'c')
|
||||
.palindrome()
|
||||
.fast(2)
|
||||
.firstCycle()
|
||||
.sort((a, b) => a.part.begin.sub(b.part.begin))
|
||||
.map((a) => a.value),
|
||||
).toStrictEqual(['a', 'b', 'c', 'c', 'b', 'a']);
|
||||
});
|
||||
});
|
||||
describe('polyrhythm()', () => {
|
||||
it('Can layer up cycles', () => {
|
||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||
@@ -638,7 +664,11 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('struct()', () => {
|
||||
it('Can restructure a discrete pattern', () => {
|
||||
expect(sequence('a', 'b').struct(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
expect(
|
||||
sequence('a', 'b')
|
||||
.struct(sequence(true, true, true))
|
||||
.firstCycle(),
|
||||
).toStrictEqual([
|
||||
hap(ts(0, third), ts(0, third), 'a'),
|
||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||
@@ -669,7 +699,11 @@ describe('Pattern', () => {
|
||||
});
|
||||
describe('mask()', () => {
|
||||
it('Can fragment a pattern', () => {
|
||||
expect(sequence('a', 'b').mask(sequence(true, true, true)).firstCycle()).toStrictEqual([
|
||||
expect(
|
||||
sequence('a', 'b')
|
||||
.mask(sequence(true, true, true))
|
||||
.firstCycle(),
|
||||
).toStrictEqual([
|
||||
hap(ts(0, 0.5), ts(0, third), 'a'),
|
||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
||||
@@ -938,9 +972,11 @@ describe('Pattern', () => {
|
||||
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||
});
|
||||
it('Doesnt merge two overlapping haps', () => {
|
||||
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
|
||||
2,
|
||||
);
|
||||
expect(
|
||||
stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1))
|
||||
.defragmentHaps()
|
||||
.firstCycle().length,
|
||||
).toStrictEqual(2);
|
||||
});
|
||||
it('Doesnt merge two touching haps with different values', () => {
|
||||
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||
@@ -1003,4 +1039,82 @@ describe('Pattern', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('chunk', () => {
|
||||
it('Processes each cycle of the source pattern multiple times, once for each chunk', () => {
|
||||
expect(sequence(0, 1, 2, 3).slow(2).chunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
||||
10, 1, 0, 11, 12, 3, 2, 13,
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('fastChunk', () => {
|
||||
it('Unlike chunk, cycles of the source pattern proceed cycle-by-cycle', () => {
|
||||
expect(sequence(0, 1, 2, 3).slow(2).fastChunk(2, add(10)).fast(4).firstCycleValues).toStrictEqual([
|
||||
10, 1, 2, 13, 10, 1, 2, 13,
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('repeatCycles', () => {
|
||||
it('Repeats each cycle of the source pattern the given number of times', () => {
|
||||
expect(slowcat(0, 1).repeatCycles(2).fast(6).firstCycleValues).toStrictEqual([0, 0, 1, 1, 0, 0]);
|
||||
});
|
||||
});
|
||||
describe('inhabit', () => {
|
||||
it('Can pattern named patterns', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence('a', 'b', stack('a', 'b')).inhabit({ a: sequence(1, 2), b: sequence(10, 20, 30) }),
|
||||
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('Can pattern indexed patterns', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence('0', '1', stack('0', '1')).inhabit([sequence(1, 2), sequence(10, 20, 30)]),
|
||||
sequence([1, 2], [10, 20, 30], stack([1, 2], [10, 20, 30])),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('pick', () => {
|
||||
it('Can pattern named patterns', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence('a', 'b', 'a', stack('a', 'b')).pick({ a: sequence(1, 2, 3, 4), b: sequence(10, 20, 30, 40) }),
|
||||
sequence(1, 20, 3, stack(4, 40)),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('Can pattern indexed patterns', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(0, 1, 0, stack(0, 1)).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
|
||||
sequence(1, 20, 3, stack(4, 40)),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('Clamps indexes', () => {
|
||||
expect(
|
||||
sameFirst(sequence(0, 1, 2, 3).pick([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]), sequence(1, 20, 30, 40)),
|
||||
);
|
||||
});
|
||||
it('Is backwards compatible', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
pick([sequence('a', 'b'), sequence('c', 'd')], sequence(0, 1)),
|
||||
pick(sequence(0, 1), [sequence('a', 'b'), sequence('c', 'd')]),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('pickmod', () => {
|
||||
it('Wraps indexes', () => {
|
||||
expect(
|
||||
sameFirst(
|
||||
sequence(0, 1, 2, 3).pickmod([sequence(1, 2, 3, 4), sequence(10, 20, 30, 40)]),
|
||||
sequence(1, 20, 3, 40),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*test for issue 302 support alternative solmization types */
|
||||
import { sol2note } from '../util.mjs';
|
||||
import { test } from 'vitest';
|
||||
import assert from 'assert';
|
||||
|
||||
test('solmization - letters', () => {
|
||||
const result = sol2note(60, 'letters');
|
||||
const expected = 'C4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - solfeggio', () => {
|
||||
const result = sol2note(60, 'solfeggio');
|
||||
const expected = 'Do4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - indian', () => {
|
||||
const result = sol2note(60, 'indian');
|
||||
const expected = 'Sa4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - german', () => {
|
||||
const result = sol2note(60, 'german');
|
||||
const expected = 'C4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - byzantine', () => {
|
||||
const result = sol2note(60, 'byzantine');
|
||||
const expected = 'Ni4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test('solmization - japanese', () => {
|
||||
const result = sol2note(60, 'japanese');
|
||||
const expected = 'I4';
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
ui.mjs - <short description TODO>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/tone/ui.mjs>
|
||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/ui.mjs>
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -46,7 +46,5 @@ export const cleanupUi = () => {
|
||||
const container = document.getElementById('code');
|
||||
if (container) {
|
||||
container.style = '';
|
||||
// TODO: find a way to remove that duplication..
|
||||
container.className = 'grow flex text-gray-100 relative overflow-auto cursor-text pb-0'; // has to match App.tsx
|
||||
}
|
||||
};
|
||||
|
||||
+119
-8
@@ -4,28 +4,33 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { logger } from './logger.mjs';
|
||||
|
||||
// returns true if the given string is a note
|
||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bs]*[0-9]?$/.test(name);
|
||||
export const isNote = (name) => /^[a-gA-G][#bsf]*[0-9]?$/.test(name);
|
||||
export const tokenizeNote = (note) => {
|
||||
if (typeof note !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bs]*)([0-9])?$/)?.slice(1) || [];
|
||||
const [pc, acc = '', oct] = note.match(/^([a-gA-G])([#bsf]*)([0-9]*)$/)?.slice(1) || [];
|
||||
if (!pc) {
|
||||
return [];
|
||||
}
|
||||
return [pc, acc, oct ? Number(oct) : undefined];
|
||||
};
|
||||
|
||||
const chromas = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
||||
const accs = { '#': 1, b: -1, s: 1, f: -1 };
|
||||
|
||||
// turns the given note into its midi number representation
|
||||
export const noteToMidi = (note) => {
|
||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
||||
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||
if (!pc) {
|
||||
throw new Error('not a note: "' + note + '"');
|
||||
}
|
||||
const chroma = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 }[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
||||
const chroma = chromas[pc.toLowerCase()];
|
||||
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||
};
|
||||
export const midiToFreq = (n) => {
|
||||
@@ -67,19 +72,32 @@ export const getFreq = (noteOrMidi) => {
|
||||
return midiToFreq(noteToMidi(noteOrMidi));
|
||||
};
|
||||
|
||||
const pcs = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
/**
|
||||
* @deprecated does not appear to be referenced or invoked anywhere in the codebase
|
||||
* @deprecated only used in workshop (first-notes)
|
||||
* @noAutocomplete
|
||||
*/
|
||||
export const midi2note = (n) => {
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
const pc = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'][n % 12];
|
||||
const pc = pcs[n % 12];
|
||||
return pc + oct;
|
||||
};
|
||||
|
||||
// modulo that works with negative numbers e.g. _mod(-1, 3) = 2. Works on numbers (rather than patterns of numbers, as @mod@ from pattern.mjs does)
|
||||
export const _mod = (n, m) => ((n % m) + m) % m;
|
||||
|
||||
export function nanFallback(value, fallback = 0) {
|
||||
if (isNaN(Number(value))) {
|
||||
logger(`"${value}" is not a number, falling back to ${fallback}`, 'warning');
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// round to nearest int, negative numbers will output a subtracted index
|
||||
export const getSoundIndex = (n, numSounds) => {
|
||||
return _mod(Math.round(nanFallback(n ?? 0, 0)), numSounds);
|
||||
};
|
||||
|
||||
export const getPlayableNoteValue = (hap) => {
|
||||
let { value, context } = hap;
|
||||
let note = value;
|
||||
@@ -212,3 +230,96 @@ export const splitAt = function (index, value) {
|
||||
};
|
||||
|
||||
export const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
||||
|
||||
export const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
||||
|
||||
/* solmization, not used yet */
|
||||
const solfeggio = ['Do', 'Reb', 'Re', 'Mib', 'Mi', 'Fa', 'Solb', 'Sol', 'Lab', 'La', 'Sib', 'Si']; /*solffegio notes*/
|
||||
const indian = [
|
||||
'Sa',
|
||||
'Re',
|
||||
'Ga',
|
||||
'Ma',
|
||||
'Pa',
|
||||
'Dha',
|
||||
'Ni',
|
||||
]; /*indian musical notes, seems like they do not use flats or sharps*/
|
||||
const german = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Hb', 'H']; /*german & dutch musical notes*/
|
||||
const byzantine = [
|
||||
'Ni',
|
||||
'Pab',
|
||||
'Pa',
|
||||
'Voub',
|
||||
'Vou',
|
||||
'Ga',
|
||||
'Dib',
|
||||
'Di',
|
||||
'Keb',
|
||||
'Ke',
|
||||
'Zob',
|
||||
'Zo',
|
||||
]; /*byzantine musical notes*/
|
||||
const japanese = [
|
||||
'I',
|
||||
'Ro',
|
||||
'Ha',
|
||||
'Ni',
|
||||
'Ho',
|
||||
'He',
|
||||
'To',
|
||||
]; /*traditional japanese musical notes, seems like they do not use falts or sharps*/
|
||||
|
||||
const english = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
||||
|
||||
export const sol2note = (n, notation = 'letters') => {
|
||||
const pc =
|
||||
notation === 'solfeggio'
|
||||
? solfeggio /*check if its is any of the following*/
|
||||
: notation === 'indian'
|
||||
? indian
|
||||
: notation === 'german'
|
||||
? german
|
||||
: notation === 'byzantine'
|
||||
? byzantine
|
||||
: notation === 'japanese'
|
||||
? japanese
|
||||
: english; /*if not use standard version*/
|
||||
const note = pc[n % 12]; /*calculating the midi value to the note*/
|
||||
const oct = Math.floor(n / 12) - 1;
|
||||
return note + oct;
|
||||
};
|
||||
|
||||
// code hashing helpers
|
||||
|
||||
export function unicodeToBase64(text) {
|
||||
const utf8Bytes = new TextEncoder().encode(text);
|
||||
const base64String = btoa(String.fromCharCode(...utf8Bytes));
|
||||
return base64String;
|
||||
}
|
||||
|
||||
export function base64ToUnicode(base64String) {
|
||||
const utf8Bytes = new Uint8Array(
|
||||
atob(base64String)
|
||||
.split('')
|
||||
.map((char) => char.charCodeAt(0)),
|
||||
);
|
||||
const decodedText = new TextDecoder().decode(utf8Bytes);
|
||||
return decodedText;
|
||||
}
|
||||
|
||||
export function code2hash(code) {
|
||||
return encodeURIComponent(unicodeToBase64(code));
|
||||
//return '#' + encodeURIComponent(btoa(code));
|
||||
}
|
||||
|
||||
export function hash2code(hash) {
|
||||
return base64ToUnicode(decodeURIComponent(hash));
|
||||
//return atob(decodeURIComponent(codeParam || ''));
|
||||
}
|
||||
|
||||
export function objectMap(obj, fn) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(fn);
|
||||
}
|
||||
return Object.fromEntries(Object.entries(obj).map(([k, v], i) => [k, fn(v, k, i)]));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @strudel/csound
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getFrequency, logger, register } from '@strudel.cycles/core';
|
||||
import { getAudioContext } from '@strudel.cycles/webaudio';
|
||||
import { getFrequency, logger, register } from '@strudel/core';
|
||||
import { getAudioContext } from '@strudel/webaudio';
|
||||
import csd from './project.csd?raw';
|
||||
// import livecodeOrc from './livecode.orc?raw';
|
||||
import presetsOrc from './presets.orc?raw';
|
||||
@@ -137,7 +137,7 @@ export async function loadOrc(url) {
|
||||
export const csoundm = register('csoundm', (instrument, pat) => {
|
||||
let p1 = instrument;
|
||||
if (typeof instrument === 'string') {
|
||||
p1 = `"{instrument}"`;
|
||||
p1 = `"${instrument}"`;
|
||||
}
|
||||
init(); // not async to support csound inside other patterns + to be able to call pattern methods after it
|
||||
return pat.onTrigger((tidal_time, hap) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@strudel.cycles/csound",
|
||||
"version": "0.7.1",
|
||||
"name": "@strudel/csound",
|
||||
"version": "1.0.1",
|
||||
"description": "csound bindings for strudel",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs"
|
||||
"main": "dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
@@ -32,11 +32,11 @@
|
||||
},
|
||||
"homepage": "https://github.com/tidalcycles/strudel#readme",
|
||||
"dependencies": {
|
||||
"@csound/browser": "6.18.5",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/webaudio": "workspace:*"
|
||||
"@csound/browser": "6.18.7",
|
||||
"@strudel/core": "workspace:*",
|
||||
"@strudel/webaudio": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.2.2"
|
||||
"vite": "^5.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'index.mjs'),
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||
formats: ['es'],
|
||||
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [...Object.keys(dependencies)],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# @strudel/desktopbridge
|
||||
|
||||
This package contains utilities used to communicate with the Tauri backend
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user