mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 22:55:38 -04:00
Compare commits
1525 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 | |||
| 18d7222804 | |||
| ff1c37d361 | |||
| 3abe6db3de | |||
| b9ca9d3b06 | |||
| 9bbc04843d | |||
| ddf61a6438 | |||
| ba35a81e9b | |||
| 13133583ca | |||
| e91b981740 | |||
| 1458ee64ec | |||
| 4e2a5e1663 | |||
| c98b0e9687 | |||
| 4e25990de9 | |||
| 4b51f1a3fc | |||
| 5490b01004 | |||
| 43176e1f1d | |||
| fffd3c07ca | |||
| 952bd38e70 | |||
| 8589f5cc89 | |||
| ec594d52f8 | |||
| 848cbf9c60 | |||
| e02be7dd9a | |||
| 33d35e47a6 | |||
| 18d62b32c2 | |||
| 58ed51b49c | |||
| 9f9fe529e4 | |||
| aad721890a | |||
| e63a5d10c8 | |||
| 15681262de | |||
| b5d3aa81f9 | |||
| a688061616 | |||
| f788d347ec | |||
| a33ce67dd4 | |||
| 8a22c5627a | |||
| 7b529a30f4 | |||
| 4910b34f26 | |||
| 74ad521fc9 | |||
| 6a73b43e3a | |||
| b59b26460d | |||
| aa324ae25d | |||
| ff412f6396 | |||
| ac148b2f32 | |||
| 6f5d096e6d | |||
| ff1e6714c6 | |||
| 41d82eaadf | |||
| be6b011204 | |||
| 65e48c05f0 | |||
| cee08ea67d | |||
| 35ef26c013 | |||
| b08a0b8102 | |||
| 220342154b | |||
| ef7182cd76 | |||
| 6059c69995 | |||
| 892d8ba152 | |||
| da11069f75 | |||
| bf72908dc9 | |||
| 81ab5c78e2 | |||
| de503c7d63 | |||
| 487ccb90a1 | |||
| 4bfeaa47bf | |||
| 9a4d8036a6 | |||
| a50bd6375d | |||
| 14508e91a9 | |||
| 2d5064bd5b | |||
| fdc4819e39 | |||
| efddbb616b | |||
| db4d0f26a8 | |||
| 06cffbc31b | |||
| 3cfb0f4c76 | |||
| 436ba067bc | |||
| f8724bf47e | |||
| 1f7e293204 | |||
| 86d2652258 | |||
| f5c0cc2e2d | |||
| 4463d4a417 | |||
| df35a5c9b1 | |||
| dda93b3c13 | |||
| 5de40b6bf5 | |||
| dcbe799d25 | |||
| a5641aef03 | |||
| 668f97a4c8 | |||
| e9d577496b | |||
| 66fba947ac | |||
| 18c7e02609 | |||
| 7e1351507a | |||
| 032acbd27c | |||
| 8b51f0fbd2 | |||
| ead5942ef0 | |||
| cfe369d9fe | |||
| 2750a3c130 | |||
| a7f6d6ecac | |||
| 3680282450 | |||
| 3b3435c999 | |||
| e2ae6278c2 | |||
| 37d6723e3c | |||
| 961bbf6f55 | |||
| fc71d349ed | |||
| 97c8ab0f71 | |||
| 28534ea547 | |||
| 1a75b79a33 | |||
| 75dfb0af35 | |||
| 073fa09648 | |||
| 3015e7b203 | |||
| 7ac0cdc0f9 | |||
| 49234183a3 | |||
| ed7763df92 | |||
| b818a02f76 | |||
| 886f8449fd | |||
| 5de6643604 | |||
| 6283273d81 | |||
| 5506d18bc5 | |||
| c8f8f02a46 | |||
| 4c5d6f1d6c | |||
| f349e36345 | |||
| 1574c4ab13 | |||
| 0dc9bc3ab6 | |||
| 0006d57a4a | |||
| 70f7e73b9a | |||
| b67b049802 | |||
| ff99dbcd22 | |||
| 0198697737 | |||
| a742a71f67 | |||
| 8f6c1531fa | |||
| d9f56f11cb | |||
| 7ff877464f | |||
| 7c367eb1e8 | |||
| 20848aac09 | |||
| 7716574076 | |||
| 89cd0c769b | |||
| a21b3d788f | |||
| 7994ba8b38 | |||
| 81b4cb2f16 | |||
| 14af1df6a3 | |||
| ac3bd7d7db | |||
| 632e8e9634 | |||
| 1e3f09f69b | |||
| f3f18ffca7 | |||
| ceb3aa0627 | |||
| 014555fe5d | |||
| fdb76867a7 | |||
| 57157fde9c | |||
| 4a3540cf2b | |||
| 540bd938f2 | |||
| c6fbebd996 | |||
| 18d3a7e23a | |||
| cbae355896 | |||
| 924e8a764a | |||
| 40e629cb0e | |||
| 99ec261f79 | |||
| 9daea5bbc2 | |||
| 4704c03d48 | |||
| 9dbf52bedb | |||
| cfd04ab7fa | |||
| 33dbf75e3b | |||
| 8ffaeeb171 | |||
| 451828e5d4 | |||
| 176877f2b8 | |||
| 61b0d08b5f | |||
| f50a6ce1ae | |||
| 8549d6ea8b | |||
| 90a90a87a5 | |||
| 760bdea521 | |||
| 0fd783e83f | |||
| 42caa9058b | |||
| 9ab315ac2c | |||
| a08ad07537 | |||
| c6ce763f7c | |||
| 70d1eb17a7 | |||
| 0393205eda | |||
| 5c05a5a63e | |||
| b01d9f849b | |||
| 0ab904275c | |||
| 5f82a86801 | |||
| 13a5d8de86 | |||
| f024b71808 |
@@ -18,3 +18,7 @@ vite.config.js
|
|||||||
**/*.json
|
**/*.json
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
**/dist
|
**/dist
|
||||||
|
/src-tauri/target/**/*
|
||||||
|
reverbGen.mjs
|
||||||
|
hydra.mjs
|
||||||
|
jsdoc-synonyms.js
|
||||||
@@ -21,13 +21,13 @@ jobs:
|
|||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 8.11.0
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: pnpm install
|
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
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [18]
|
node-version: [20]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: pnpm/action-setup@v2
|
- uses: pnpm/action-setup@v2
|
||||||
with:
|
with:
|
||||||
version: 7
|
version: 8.11.0
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|||||||
+87
@@ -40,3 +40,90 @@ repl/stats.html
|
|||||||
coverage
|
coverage
|
||||||
public/icons/apple-splash-*
|
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
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ paper
|
|||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
**/dev-dist
|
**/dev-dist
|
||||||
|
website/.astro
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
"jsxSingleQuote": false,
|
"jsxSingleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"jsxBracketSameLine": false,
|
"bracketSameLine": false,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"proseWrap": "preserve",
|
"proseWrap": "preserve",
|
||||||
"htmlWhitespaceSensitivity": "css",
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
|||||||
+34
-5
@@ -13,7 +13,7 @@ To get in touch with the contributors, either
|
|||||||
## Ask a Question
|
## Ask a Question
|
||||||
|
|
||||||
If you have any questions about strudel, make sure you've glanced through the
|
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!
|
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.
|
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
|
## 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.
|
you can edit each file directly on github via the "Edit this page" link located in the right sidebar.
|
||||||
|
|
||||||
## Propose a Feature
|
## 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/)
|
- [git](https://git-scm.com/)
|
||||||
- [node](https://nodejs.org/en/) >= 18
|
- [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:
|
then, do the following:
|
||||||
|
|
||||||
@@ -82,6 +82,10 @@ Please report any problems you've had with the setup instructions!
|
|||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
To make sure the code changes only where it should, we are using prettier to unify the code style.
|
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 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
|
If you use VSCode, you can
|
||||||
|
|
||||||
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
1. install [the prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||||
@@ -89,10 +93,28 @@ If you use VSCode, you can
|
|||||||
3. Choose "Configure Default Formatter..."
|
3. Choose "Configure Default Formatter..."
|
||||||
4. Select prettier
|
4. Select prettier
|
||||||
|
|
||||||
|
## ESLint
|
||||||
|
|
||||||
|
To prevent unwanted runtime errors, this project uses [eslint](https://eslint.org/).
|
||||||
|
|
||||||
|
- You can check for lint errors by running `pnpm lint`
|
||||||
|
|
||||||
|
There are also eslint extensions / plugins for most editors.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Run all tests with `pnpm test`
|
||||||
|
- Run all tests with UI using `pnpm test-ui`
|
||||||
|
|
||||||
|
## Running all CI Checks
|
||||||
|
|
||||||
|
When opening a PR, the CI runner will automatically check the code style and eslint, as well as run all tests.
|
||||||
|
You can run the same check with `pnpm check`
|
||||||
|
|
||||||
## Package Workflow
|
## Package Workflow
|
||||||
|
|
||||||
The project is split into multiple [packages](https://github.com/tidalcycles/strudel/tree/main/packages) with independent versioning.
|
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.
|
allowing to develop multiple packages at the same time.
|
||||||
|
|
||||||
## Package Publishing
|
## Package Publishing
|
||||||
@@ -101,7 +123,14 @@ To publish all packages that have been changed since the last release, run:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm login
|
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`.
|
To manually publish a single package, increase the version in the `package.json`, then run `pnpm publish`.
|
||||||
|
|||||||
@@ -2,20 +2,21 @@
|
|||||||
|
|
||||||
[](https://github.com/tidalcycles/strudel/actions)
|
[](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/>
|
- Try it here: <https://strudel.cc>
|
||||||
- Docs: <https://strudel.tidalcycles.org/learn/>
|
- Docs: <https://strudel.cc/learn>
|
||||||
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
- Technical Blog Post: <https://loophole-letters.vercel.app/strudel>
|
||||||
- 1 Year of Strudel Blog Post: <https://loophole-letters.vercel.app/strudel1year>
|
- 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
|
## Running Locally
|
||||||
|
|
||||||
After cloning the project, you can run the REPL locally:
|
After cloning the project, you can run the REPL locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run setup
|
pnpm run setup
|
||||||
npm run repl
|
pnpm run repl
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using Strudel In Your Project
|
## Using Strudel In Your Project
|
||||||
@@ -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
|
- [`core`](./packages/core/): tidal pattern engine
|
||||||
- [`mini`](./packages/mini): mini notation parser + core binding
|
- [`mini`](./packages/mini): mini notation parser + core binding
|
||||||
- [`eval`](./packages/eval): user code evaluator. syntax sugar + highlighting
|
- [`transpiler`](./packages/transpiler): user code transpiler
|
||||||
- [`tone`](./packages/tone): bindings for Tone.js instruments and effects
|
- [`webaudio`](./packages/webaudio): webaudio output
|
||||||
- [`osc`](./packages/osc): bindings to communicate via OSC
|
- [`osc`](./packages/osc): bindings to communicate via OSC
|
||||||
- [`midi`](./packages/midi): webmidi bindings
|
- [`midi`](./packages/midi): webmidi bindings
|
||||||
- [`serial`](./packages/serial): webserial bindings
|
- [`serial`](./packages/serial): webserial bindings
|
||||||
- [`tonal`](./packages/tonal): tonal functions
|
- [`tonal`](./packages/tonal): tonal functions
|
||||||
- [`xen`](./packages/xen): microtonal / xenharmonic functions
|
|
||||||
- ... [and there are more](./packages/)
|
- ... [and there are more](./packages/)
|
||||||
|
|
||||||
Click on the package names to find out more about each one.
|
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>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<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
|
Object.assign(window, strudel); // assign all strudel functions to global scope to use with eval
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
const getEvents = () => {
|
const getEvents = () => {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
/>
|
/>
|
||||||
<canvas id="canvas"></canvas>
|
<canvas id="canvas"></canvas>
|
||||||
<script type="module">
|
<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
|
// this adds all strudel functions to the global scope, to be used by eval
|
||||||
Object.assign(window, strudel);
|
Object.assign(window, strudel);
|
||||||
// setup elements
|
// 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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -16,34 +16,40 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="output"></div>
|
<div id="output"></div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel.cycles/core@0.6.8';
|
import { controls, repl, evalScope } from 'https://cdn.skypack.dev/@strudel/core@0.11.0';
|
||||||
import { mini } from 'https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0';
|
import { mini } from 'https://cdn.skypack.dev/@strudel/mini@0.11.0';
|
||||||
import { transpiler } from 'https://cdn.skypack.dev/@strudel.cycles/transpiler@0.6.0';
|
import { transpiler } from 'https://cdn.skypack.dev/@strudel/transpiler@0.11.0';
|
||||||
import {
|
import {
|
||||||
getAudioContext,
|
getAudioContext,
|
||||||
webaudioOutput,
|
webaudioOutput,
|
||||||
initAudioOnFirstClick,
|
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 ctx = getAudioContext();
|
||||||
const input = document.getElementById('text');
|
const input = document.getElementById('text');
|
||||||
input.innerHTML = getTune();
|
input.innerHTML = getTune();
|
||||||
|
|
||||||
evalScope(
|
const loadModules = evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/core@0.6.8'),
|
import('https://cdn.skypack.dev/@strudel/core@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/mini@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/mini@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/tonal@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/tonal@0.11.0'),
|
||||||
import('https://cdn.skypack.dev/@strudel.cycles/webaudio@0.6.0'),
|
import('https://cdn.skypack.dev/@strudel/webaudio@0.11.0'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const initAudio = Promise.all([initAudioOnFirstClick(), registerSynthSounds()]);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime: () => ctx.currentTime,
|
getTime: () => ctx.currentTime,
|
||||||
transpiler,
|
transpiler,
|
||||||
});
|
});
|
||||||
document.getElementById('start').addEventListener('click', () => evaluate(input.value));
|
document.getElementById('start').addEventListener('click', async () => {
|
||||||
|
await loadModules;
|
||||||
|
await initAudio;
|
||||||
|
evaluate(input.value);
|
||||||
|
});
|
||||||
|
|
||||||
function getTune() {
|
function getTune() {
|
||||||
return `await samples('github:tidalcycles/Dirt-Samples/master')
|
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> -->
|
<!-- <script src="./embed.js"></script> -->
|
||||||
<strudel-repl>
|
<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*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.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">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { controls, repl, evalScope } from '@strudel.cycles/core';
|
import { controls, repl, evalScope } from '@strudel/core';
|
||||||
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
|
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';
|
||||||
import { transpiler } from '@strudel.cycles/transpiler';
|
import { transpiler } from '@strudel/transpiler';
|
||||||
import tune from './tune.mjs';
|
import tune from './tune.mjs';
|
||||||
|
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
@@ -10,10 +10,10 @@ initAudioOnFirstClick();
|
|||||||
|
|
||||||
evalScope(
|
evalScope(
|
||||||
controls,
|
controls,
|
||||||
import('@strudel.cycles/core'),
|
import('@strudel/core'),
|
||||||
import('@strudel.cycles/mini'),
|
import('@strudel/mini'),
|
||||||
import('@strudel.cycles/webaudio'),
|
import('@strudel/webaudio'),
|
||||||
import('@strudel.cycles/tonal'),
|
import('@strudel/tonal'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { evaluate } = repl({
|
const { evaluate } = repl({
|
||||||
+6
-6
@@ -10,13 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.0"
|
"vite": "^5.0.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel/core": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "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/core/index.mjs';
|
||||||
export * from './packages/csound/index.mjs';
|
export * from './packages/csound/index.mjs';
|
||||||
export * from './packages/embed/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/midi/index.mjs';
|
||||||
export * from './packages/mini/index.mjs';
|
export * from './packages/mini/index.mjs';
|
||||||
export * from './packages/osc/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/serial/index.mjs';
|
||||||
export * from './packages/soundfonts/index.mjs';
|
export * from './packages/soundfonts/index.mjs';
|
||||||
export * from './packages/tonal/index.mjs';
|
export * from './packages/tonal/index.mjs';
|
||||||
export * from './packages/tone/index.mjs';
|
|
||||||
export * from './packages/transpiler/index.mjs';
|
export * from './packages/transpiler/index.mjs';
|
||||||
export * from './packages/webaudio/index.mjs';
|
export * from './packages/webaudio/index.mjs';
|
||||||
export * from './packages/webdirt/index.mjs';
|
|
||||||
export * from './packages/xen/index.mjs';
|
export * from './packages/xen/index.mjs';
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
evaluate.mjs - <short description TODO>
|
jsdoc-synonyms.js - Add support for @synonym tag
|
||||||
Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/eval/evaluate.mjs>
|
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/>.
|
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 { evaluate as _evaluate } from '@strudel.cycles/core';
|
function defineTags(dictionary) {
|
||||||
import shapeshifter from './shapeshifter.mjs';
|
dictionary.defineTag('synonyms', {
|
||||||
|
mustHaveValue: true,
|
||||||
|
onTagged: function (doclet, tag) {
|
||||||
|
doclet.synonyms_text = tag.value;
|
||||||
|
doclet.synonyms = doclet.synonyms_text.split(/[ ,]+/);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const evaluate = async (code) => {
|
module.exports = { defineTags: defineTags };
|
||||||
return _evaluate(code, shapeshifter);
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
"includePattern": ".+\\.(js(doc|x)?|mjs)$",
|
||||||
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
"excludePattern": "node_modules|shift-parser|shift-reducer|shift-traverser|dist"
|
||||||
},
|
},
|
||||||
"plugins": ["plugins/markdown"],
|
"plugins": ["plugins/markdown", "jsdoc/jsdoc-synonyms"],
|
||||||
"opts": {
|
"opts": {
|
||||||
"destination": "./out/",
|
"destination": "./out/",
|
||||||
"recurse": true
|
"recurse": true
|
||||||
@@ -65,7 +65,7 @@ async function getUndocumented(path, docs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// read doc.json file
|
// 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({
|
const paths = dependencyTree.toList({
|
||||||
filename: 'index.mjs',
|
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 paths = ['../packages/core/pattern.mjs', '../packages/core/hap.mjs'].map((rel) => resolve(__dirname, rel));
|
||||||
|
|
||||||
const undocumented = Object.fromEntries(
|
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));
|
console.log(JSON.stringify(undocumented, null, 2));
|
||||||
+2
-4
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"packages": [
|
"packages": ["packages/*"],
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"version": "independent",
|
"version": "independent",
|
||||||
"npmClient": "pnpm",
|
"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
|
## running locally
|
||||||
|
|
||||||
- install dependencies with `npm run setup`
|
- 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?
|
## tests fail?
|
||||||
|
|
||||||
|
|||||||
+23
-25
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/monorepo",
|
"name": "@strudel/monorepo",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Port of tidalcycles to javascript",
|
"description": "Port of tidalcycles to javascript",
|
||||||
@@ -18,13 +18,12 @@
|
|||||||
"build": "npm run prebuild && cd website && npm run build",
|
"build": "npm run prebuild && cd website && npm run build",
|
||||||
"preview": "cd website && npm run preview",
|
"preview": "cd website && npm run preview",
|
||||||
"osc": "cd packages/osc && npm run server",
|
"osc": "cd packages/osc && npm run server",
|
||||||
"deploy": "NODE_DEBUG=gh-pages gh-pages -d out",
|
"jsdoc": "jsdoc packages/ -c jsdoc/jsdoc.config.json",
|
||||||
"jsdoc": "jsdoc packages/ -c jsdoc.config.json",
|
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc/jsdoc.config.json",
|
||||||
"jsdoc-json": "jsdoc packages/ --template ./node_modules/jsdoc-json --destination doc.json -c jsdoc.config.json",
|
|
||||||
"lint": "eslint . --ext mjs,js --quiet",
|
"lint": "eslint . --ext mjs,js --quiet",
|
||||||
"codeformat": "prettier --write .",
|
"codeformat": "prettier --write .",
|
||||||
"format-check": "prettier --check .",
|
"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",
|
"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"
|
"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,30 +43,29 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dependency-tree": "^9.0.0",
|
"@strudel/core": "workspace:*",
|
||||||
"vitest": "^0.25.7",
|
"@strudel/mini": "workspace:*",
|
||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel/tonal": "workspace:*",
|
||||||
"@strudel.cycles/mini": "workspace:*",
|
"@strudel/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/tonal": "workspace:*",
|
"@strudel/webaudio": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel/xen": "workspace:*"
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
|
||||||
"@strudel.cycles/xen": "workspace:*"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitest/ui": "^0.25.7",
|
"dependency-tree": "^10.0.9",
|
||||||
"c8": "^7.12.0",
|
"@tauri-apps/cli": "^1.5.9",
|
||||||
"canvas": "^2.11.0",
|
"@vitest/ui": "^1.1.0",
|
||||||
"eslint": "^8.28.0",
|
"canvas": "^2.11.2",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"gh-pages": "^4.0.0",
|
"jsdoc": "^4.0.2",
|
||||||
"jsdoc": "^3.6.10",
|
|
||||||
"jsdoc-json": "^2.0.2",
|
"jsdoc-json": "^2.0.2",
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
"jsdoc-to-markdown": "^8.0.0",
|
||||||
"lerna": "^4.0.0",
|
"lerna": "^8.0.1",
|
||||||
"prettier": "^2.8.1",
|
"prettier": "^3.1.1",
|
||||||
"rollup-plugin-visualizer": "^5.8.1"
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
|
"vitest": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-11
@@ -1,15 +1,5 @@
|
|||||||
# Packages
|
# 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.
|
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.
|
||||||
+33
-26
@@ -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 getDocLabel = (doc) => doc.name || doc.longname;
|
||||||
const getInnerText = (html) => {
|
const getInnerText = (html) => {
|
||||||
@@ -8,21 +16,21 @@ const getInnerText = (html) => {
|
|||||||
return div.textContent || div.innerText || '';
|
return div.textContent || div.innerText || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Autocomplete({ doc }) {
|
export function Autocomplete({ doc, label }) {
|
||||||
return (
|
return h`<div class="prose dark:prose-invert max-h-[400px] overflow-auto p-2">
|
||||||
<div className="prose dark:prose-invert max-h-[400px] overflow-auto">
|
<h1 class="pt-0 mt-0">${label || getDocLabel(doc)}</h1>
|
||||||
<h3 className="pt-0 mt-0">{getDocLabel(doc)}</h3>
|
${doc.description}
|
||||||
<div dangerouslySetInnerHTML={{ __html: doc.description }} />
|
|
||||||
<ul>
|
<ul>
|
||||||
{doc.params?.map(({ name, type, description }, i) => (
|
${doc.params?.map(
|
||||||
<li key={i}>
|
({ name, type, description }) =>
|
||||||
{name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}</> : ''}
|
`<li>${name} : ${type.names?.join(' | ')} ${description ? ` - ${getInnerText(description)}` : ''}</li>`,
|
||||||
</li>
|
)}
|
||||||
))}
|
|
||||||
</ul>
|
</ul>
|
||||||
<div>
|
<div>
|
||||||
{doc.examples?.map((example, i) => (
|
${doc.examples?.map((example) => `<div><pre>${plaintext(example)}</pre></div>`)}
|
||||||
<div key={i}>
|
</div>
|
||||||
|
</div>`[0];
|
||||||
|
/*
|
||||||
<pre
|
<pre
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
@@ -33,11 +41,7 @@ export function Autocomplete({ doc }) {
|
|||||||
>
|
>
|
||||||
{example}
|
{example}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
*/
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsdocCompletions = jsdoc.docs
|
const jsdocCompletions = jsdoc.docs
|
||||||
@@ -52,13 +56,7 @@ const jsdocCompletions = jsdoc.docs
|
|||||||
.map((doc) /*: Completion */ => ({
|
.map((doc) /*: Completion */ => ({
|
||||||
label: getDocLabel(doc),
|
label: getDocLabel(doc),
|
||||||
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
// detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
|
||||||
info: () => {
|
info: () => Autocomplete({ doc }),
|
||||||
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;
|
|
||||||
},
|
|
||||||
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
|
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)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+102
-32
@@ -30,10 +30,22 @@ import {
|
|||||||
xcodeLight,
|
xcodeLight,
|
||||||
} from '@uiw/codemirror-themes-all';
|
} from '@uiw/codemirror-themes-all';
|
||||||
|
|
||||||
import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
|
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 = {
|
export const themes = {
|
||||||
strudelTheme,
|
strudelTheme,
|
||||||
|
bluescreen,
|
||||||
|
blackscreen,
|
||||||
|
whitescreen,
|
||||||
|
teletext,
|
||||||
|
algoboy,
|
||||||
|
terminal,
|
||||||
abcdef,
|
abcdef,
|
||||||
androidstudio,
|
androidstudio,
|
||||||
atomone,
|
atomone,
|
||||||
@@ -70,7 +82,7 @@ export const themes = {
|
|||||||
export const settings = {
|
export const settings = {
|
||||||
strudelTheme: {
|
strudelTheme: {
|
||||||
background: '#222',
|
background: '#222',
|
||||||
lineBackground: '#22222250',
|
lineBackground: '#22222299',
|
||||||
foreground: '#fff',
|
foreground: '#fff',
|
||||||
// foreground: '#75baff',
|
// foreground: '#75baff',
|
||||||
caret: '#ffcc00',
|
caret: '#ffcc00',
|
||||||
@@ -82,9 +94,15 @@ export const settings = {
|
|||||||
// gutterForeground: '#8a919966',
|
// gutterForeground: '#8a919966',
|
||||||
gutterForeground: '#8a919966',
|
gutterForeground: '#8a919966',
|
||||||
},
|
},
|
||||||
|
bluescreen: bluescreenSettings,
|
||||||
|
blackscreen: blackscreenSettings,
|
||||||
|
whitescreen: whitescreenSettings,
|
||||||
|
teletext: teletextSettings,
|
||||||
|
algoboy: algoboySettings,
|
||||||
|
terminal: terminalSettings,
|
||||||
abcdef: {
|
abcdef: {
|
||||||
background: '#0f0f0f',
|
background: '#0f0f0f',
|
||||||
lineBackground: '#0f0f0f50',
|
lineBackground: '#0f0f0f99',
|
||||||
foreground: '#defdef',
|
foreground: '#defdef',
|
||||||
caret: '#00FF00',
|
caret: '#00FF00',
|
||||||
selection: '#515151',
|
selection: '#515151',
|
||||||
@@ -95,7 +113,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
androidstudio: {
|
androidstudio: {
|
||||||
background: '#282b2e',
|
background: '#282b2e',
|
||||||
lineBackground: '#282b2e50',
|
lineBackground: '#282b2e99',
|
||||||
foreground: '#a9b7c6',
|
foreground: '#a9b7c6',
|
||||||
caret: '#00FF00',
|
caret: '#00FF00',
|
||||||
selection: '#343739',
|
selection: '#343739',
|
||||||
@@ -104,7 +122,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
atomone: {
|
atomone: {
|
||||||
background: '#272C35',
|
background: '#272C35',
|
||||||
lineBackground: '#272C3550',
|
lineBackground: '#272C3599',
|
||||||
foreground: '#9d9b97',
|
foreground: '#9d9b97',
|
||||||
caret: '#797977',
|
caret: '#797977',
|
||||||
selection: '#ffffff30',
|
selection: '#ffffff30',
|
||||||
@@ -116,7 +134,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
aura: {
|
aura: {
|
||||||
background: '#21202e',
|
background: '#21202e',
|
||||||
lineBackground: '#21202e50',
|
lineBackground: '#21202e99',
|
||||||
foreground: '#edecee',
|
foreground: '#edecee',
|
||||||
caret: '#a277ff',
|
caret: '#a277ff',
|
||||||
selection: '#3d375e7f',
|
selection: '#3d375e7f',
|
||||||
@@ -129,7 +147,7 @@ export const settings = {
|
|||||||
bbedit: {
|
bbedit: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#FFFFFF',
|
background: '#FFFFFF',
|
||||||
lineBackground: '#FFFFFF50',
|
lineBackground: '#FFFFFF99',
|
||||||
foreground: '#000000',
|
foreground: '#000000',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#FFD420',
|
selection: '#FFD420',
|
||||||
@@ -141,7 +159,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
bespin: {
|
bespin: {
|
||||||
background: '#28211c',
|
background: '#28211c',
|
||||||
lineBackground: '#28211c50',
|
lineBackground: '#28211c99',
|
||||||
foreground: '#9d9b97',
|
foreground: '#9d9b97',
|
||||||
caret: '#797977',
|
caret: '#797977',
|
||||||
selection: '#36312e',
|
selection: '#36312e',
|
||||||
@@ -152,7 +170,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
darcula: {
|
darcula: {
|
||||||
background: '#2B2B2B',
|
background: '#2B2B2B',
|
||||||
lineBackground: '#2B2B2B50',
|
lineBackground: '#2B2B2B99',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
@@ -164,7 +182,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
dracula: {
|
dracula: {
|
||||||
background: '#282a36',
|
background: '#282a36',
|
||||||
lineBackground: '#282a3650',
|
lineBackground: '#282a3699',
|
||||||
foreground: '#f8f8f2',
|
foreground: '#f8f8f2',
|
||||||
caret: '#f8f8f0',
|
caret: '#f8f8f0',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
@@ -177,7 +195,7 @@ export const settings = {
|
|||||||
duotoneLight: {
|
duotoneLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#faf8f5',
|
background: '#faf8f5',
|
||||||
lineBackground: '#faf8f550',
|
lineBackground: '#faf8f599',
|
||||||
foreground: '#b29762',
|
foreground: '#b29762',
|
||||||
caret: '#93abdc',
|
caret: '#93abdc',
|
||||||
selection: '#e3dcce',
|
selection: '#e3dcce',
|
||||||
@@ -189,7 +207,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
duotoneDark: {
|
duotoneDark: {
|
||||||
background: '#2a2734',
|
background: '#2a2734',
|
||||||
lineBackground: '#2a273450',
|
lineBackground: '#2a273499',
|
||||||
foreground: '#6c6783',
|
foreground: '#6c6783',
|
||||||
caret: '#ffad5c',
|
caret: '#ffad5c',
|
||||||
selection: 'rgba(255, 255, 255, 0.1)',
|
selection: 'rgba(255, 255, 255, 0.1)',
|
||||||
@@ -200,7 +218,7 @@ export const settings = {
|
|||||||
eclipse: {
|
eclipse: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff50',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#000',
|
foreground: '#000',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#d7d4f0',
|
selection: '#d7d4f0',
|
||||||
@@ -213,7 +231,7 @@ export const settings = {
|
|||||||
githubLight: {
|
githubLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff50',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#24292e',
|
foreground: '#24292e',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
@@ -222,7 +240,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
githubDark: {
|
githubDark: {
|
||||||
background: '#0d1117',
|
background: '#0d1117',
|
||||||
lineBackground: '#0d111750',
|
lineBackground: '#0d111799',
|
||||||
foreground: '#c9d1d9',
|
foreground: '#c9d1d9',
|
||||||
caret: '#c9d1d9',
|
caret: '#c9d1d9',
|
||||||
selection: '#003d73',
|
selection: '#003d73',
|
||||||
@@ -231,7 +249,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
gruvboxDark: {
|
gruvboxDark: {
|
||||||
background: '#282828',
|
background: '#282828',
|
||||||
lineBackground: '#28282850',
|
lineBackground: '#28282899',
|
||||||
foreground: '#ebdbb2',
|
foreground: '#ebdbb2',
|
||||||
caret: '#ebdbb2',
|
caret: '#ebdbb2',
|
||||||
selection: '#bdae93',
|
selection: '#bdae93',
|
||||||
@@ -243,7 +261,7 @@ export const settings = {
|
|||||||
gruvboxLight: {
|
gruvboxLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#fbf1c7',
|
background: '#fbf1c7',
|
||||||
lineBackground: '#fbf1c750',
|
lineBackground: '#fbf1c799',
|
||||||
foreground: '#3c3836',
|
foreground: '#3c3836',
|
||||||
caret: '#af3a03',
|
caret: '#af3a03',
|
||||||
selection: '#ebdbb2',
|
selection: '#ebdbb2',
|
||||||
@@ -255,7 +273,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
materialDark: {
|
materialDark: {
|
||||||
background: '#2e3235',
|
background: '#2e3235',
|
||||||
lineBackground: '#2e323550',
|
lineBackground: '#2e323599',
|
||||||
foreground: '#bdbdbd',
|
foreground: '#bdbdbd',
|
||||||
caret: '#a0a4ae',
|
caret: '#a0a4ae',
|
||||||
selection: '#d7d4f0',
|
selection: '#d7d4f0',
|
||||||
@@ -268,7 +286,7 @@ export const settings = {
|
|||||||
materialLight: {
|
materialLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#FAFAFA',
|
background: '#FAFAFA',
|
||||||
lineBackground: '#FAFAFA50',
|
lineBackground: '#FAFAFA99',
|
||||||
foreground: '#90A4AE',
|
foreground: '#90A4AE',
|
||||||
caret: '#272727',
|
caret: '#272727',
|
||||||
selection: '#80CBC440',
|
selection: '#80CBC440',
|
||||||
@@ -281,7 +299,7 @@ export const settings = {
|
|||||||
noctisLilac: {
|
noctisLilac: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#f2f1f8',
|
background: '#f2f1f8',
|
||||||
lineBackground: '#f2f1f850',
|
lineBackground: '#f2f1f899',
|
||||||
foreground: '#0c006b',
|
foreground: '#0c006b',
|
||||||
caret: '#5c49e9',
|
caret: '#5c49e9',
|
||||||
selection: '#d5d1f2',
|
selection: '#d5d1f2',
|
||||||
@@ -292,7 +310,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
nord: {
|
nord: {
|
||||||
background: '#2e3440',
|
background: '#2e3440',
|
||||||
lineBackground: '#2e344050',
|
lineBackground: '#2e344099',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#3b4252',
|
selection: '#3b4252',
|
||||||
@@ -304,7 +322,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
okaidia: {
|
okaidia: {
|
||||||
background: '#272822',
|
background: '#272822',
|
||||||
lineBackground: '#27282250',
|
lineBackground: '#27282299',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
caret: '#FFFFFF',
|
caret: '#FFFFFF',
|
||||||
selection: '#49483E',
|
selection: '#49483E',
|
||||||
@@ -316,7 +334,7 @@ export const settings = {
|
|||||||
solarizedLight: {
|
solarizedLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#fdf6e3',
|
background: '#fdf6e3',
|
||||||
lineBackground: '#fdf6e350',
|
lineBackground: '#fdf6e399',
|
||||||
foreground: '#657b83',
|
foreground: '#657b83',
|
||||||
caret: '#586e75',
|
caret: '#586e75',
|
||||||
selection: '#dfd9c8',
|
selection: '#dfd9c8',
|
||||||
@@ -327,7 +345,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
solarizedDark: {
|
solarizedDark: {
|
||||||
background: '#002b36',
|
background: '#002b36',
|
||||||
lineBackground: '#002b3650',
|
lineBackground: '#002b3699',
|
||||||
foreground: '#93a1a1',
|
foreground: '#93a1a1',
|
||||||
caret: '#839496',
|
caret: '#839496',
|
||||||
selection: '#173541',
|
selection: '#173541',
|
||||||
@@ -338,7 +356,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
sublime: {
|
sublime: {
|
||||||
background: '#303841',
|
background: '#303841',
|
||||||
lineBackground: '#30384150',
|
lineBackground: '#30384199',
|
||||||
foreground: '#FFFFFF',
|
foreground: '#FFFFFF',
|
||||||
caret: '#FBAC52',
|
caret: '#FBAC52',
|
||||||
selection: '#4C5964',
|
selection: '#4C5964',
|
||||||
@@ -350,7 +368,7 @@ export const settings = {
|
|||||||
tokyoNightDay: {
|
tokyoNightDay: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#e1e2e7',
|
background: '#e1e2e7',
|
||||||
lineBackground: '#e1e2e750',
|
lineBackground: '#e1e2e799',
|
||||||
foreground: '#3760bf',
|
foreground: '#3760bf',
|
||||||
caret: '#3760bf',
|
caret: '#3760bf',
|
||||||
selection: '#99a7df',
|
selection: '#99a7df',
|
||||||
@@ -362,7 +380,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
tokyoNightStorm: {
|
tokyoNightStorm: {
|
||||||
background: '#24283b',
|
background: '#24283b',
|
||||||
lineBackground: '#24283b50',
|
lineBackground: '#24283b99',
|
||||||
foreground: '#7982a9',
|
foreground: '#7982a9',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#6f7bb630',
|
selection: '#6f7bb630',
|
||||||
@@ -374,7 +392,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
tokyoNight: {
|
tokyoNight: {
|
||||||
background: '#1a1b26',
|
background: '#1a1b26',
|
||||||
lineBackground: '#1a1b2650',
|
lineBackground: '#1a1b2699',
|
||||||
foreground: '#787c99',
|
foreground: '#787c99',
|
||||||
caret: '#c0caf5',
|
caret: '#c0caf5',
|
||||||
selection: '#515c7e40',
|
selection: '#515c7e40',
|
||||||
@@ -386,7 +404,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
vscodeDark: {
|
vscodeDark: {
|
||||||
background: '#1e1e1e',
|
background: '#1e1e1e',
|
||||||
lineBackground: '#1e1e1e50',
|
lineBackground: '#1e1e1e99',
|
||||||
foreground: '#9cdcfe',
|
foreground: '#9cdcfe',
|
||||||
caret: '#c6c6c6',
|
caret: '#c6c6c6',
|
||||||
selection: '#6199ff2f',
|
selection: '#6199ff2f',
|
||||||
@@ -395,12 +413,11 @@ export const settings = {
|
|||||||
gutterBackground: '#1e1e1e',
|
gutterBackground: '#1e1e1e',
|
||||||
gutterForeground: '#838383',
|
gutterForeground: '#838383',
|
||||||
gutterActiveForeground: '#fff',
|
gutterActiveForeground: '#fff',
|
||||||
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
|
|
||||||
},
|
},
|
||||||
xcodeLight: {
|
xcodeLight: {
|
||||||
light: true,
|
light: true,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
lineBackground: '#ffffff50',
|
lineBackground: '#ffffff99',
|
||||||
foreground: '#3D3D3D',
|
foreground: '#3D3D3D',
|
||||||
selection: '#BBDFFF',
|
selection: '#BBDFFF',
|
||||||
selectionMatch: '#BBDFFF',
|
selectionMatch: '#BBDFFF',
|
||||||
@@ -410,7 +427,7 @@ export const settings = {
|
|||||||
},
|
},
|
||||||
xcodeDark: {
|
xcodeDark: {
|
||||||
background: '#292A30',
|
background: '#292A30',
|
||||||
lineBackground: '#292A3050',
|
lineBackground: '#292A3099',
|
||||||
foreground: '#CECFD0',
|
foreground: '#CECFD0',
|
||||||
caret: '#fff',
|
caret: '#fff',
|
||||||
selection: '#727377',
|
selection: '#727377',
|
||||||
@@ -455,3 +472,56 @@ function getCircularReplacer() {
|
|||||||
function stringifySafe(json) {
|
function stringifySafe(json) {
|
||||||
return JSON.stringify(json, getCircularReplacer());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: '#9bbc0f',
|
||||||
|
foreground: '#0f380f', // whats that?
|
||||||
|
caret: '#0f380f',
|
||||||
|
selection: '#306230',
|
||||||
|
selectionMatch: '#ffffff26',
|
||||||
|
lineHighlight: '#8bac0f',
|
||||||
|
lineBackground: '#9bbc0f50',
|
||||||
|
//lineBackground: 'transparent',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#0f380f',
|
||||||
|
light: true,
|
||||||
|
customStyle: '.cm-line { line-height: 1 }',
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'light',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: '#0f380f' },
|
||||||
|
{ tag: t.operator, color: '#0f380f' },
|
||||||
|
{ tag: t.special(t.variableName), color: '#0f380f' },
|
||||||
|
{ tag: t.typeName, color: '#0f380f' },
|
||||||
|
{ tag: t.atom, color: '#0f380f' },
|
||||||
|
{ tag: t.number, color: '#0f380f' },
|
||||||
|
{ tag: t.definition(t.variableName), color: '#0f380f' },
|
||||||
|
{ tag: t.string, color: '#0f380f' },
|
||||||
|
{ tag: t.special(t.string), color: '#0f380f' },
|
||||||
|
{ tag: t.comment, color: '#0f380f' },
|
||||||
|
{ tag: t.variableName, color: '#0f380f' },
|
||||||
|
{ tag: t.tagName, color: '#0f380f' },
|
||||||
|
{ tag: t.bracket, color: '#0f380f' },
|
||||||
|
{ tag: t.meta, color: '#0f380f' },
|
||||||
|
{ tag: t.attributeName, color: '#0f380f' },
|
||||||
|
{ tag: t.propertyName, color: '#0f380f' },
|
||||||
|
{ tag: t.className, color: '#0f380f' },
|
||||||
|
{ tag: t.invalid, color: '#0f380f' },
|
||||||
|
{ tag: [t.unit, t.punctuation], color: '#0f380f' },
|
||||||
|
],
|
||||||
|
});
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'black',
|
||||||
|
foreground: 'white', // whats that?
|
||||||
|
caret: 'white',
|
||||||
|
selection: '#ffffff20',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
lineHighlight: '#ffffff10',
|
||||||
|
lineBackground: '#00000050',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'white' },
|
||||||
|
{ tag: t.operator, color: 'white' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.typeName, color: 'white' },
|
||||||
|
{ tag: t.atom, color: 'white' },
|
||||||
|
{ tag: t.number, color: 'white' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.string, color: 'white' },
|
||||||
|
{ tag: t.special(t.string), color: 'white' },
|
||||||
|
{ tag: t.comment, color: 'white' },
|
||||||
|
{ tag: t.variableName, color: 'white' },
|
||||||
|
{ tag: t.tagName, color: 'white' },
|
||||||
|
{ tag: t.bracket, color: 'white' },
|
||||||
|
{ tag: t.meta, color: 'white' },
|
||||||
|
{ tag: t.attributeName, color: 'white' },
|
||||||
|
{ tag: t.propertyName, color: 'white' },
|
||||||
|
{ tag: t.className, color: 'white' },
|
||||||
|
{ tag: t.invalid, color: 'white' },
|
||||||
|
{ tag: [t.unit, t.punctuation], color: 'white' },
|
||||||
|
],
|
||||||
|
});
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: '#051DB5',
|
||||||
|
lineBackground: '#051DB550',
|
||||||
|
foreground: 'white', // whats that?
|
||||||
|
caret: 'white',
|
||||||
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
// lineHighlight: '#8a91991a', // original
|
||||||
|
lineHighlight: '#00000050',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
// gutterForeground: '#8a919966',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'white' },
|
||||||
|
{ tag: t.operator, color: 'white' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.typeName, color: 'white' },
|
||||||
|
{ tag: t.atom, color: 'white' },
|
||||||
|
{ tag: t.number, color: 'white' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'white' },
|
||||||
|
{ tag: t.string, color: 'white' },
|
||||||
|
{ tag: t.special(t.string), color: 'white' },
|
||||||
|
{ tag: t.comment, color: 'white' },
|
||||||
|
{ tag: t.variableName, color: 'white' },
|
||||||
|
{ tag: t.tagName, color: 'white' },
|
||||||
|
{ tag: t.bracket, color: 'white' },
|
||||||
|
{ tag: t.meta, color: 'white' },
|
||||||
|
{ tag: t.attributeName, color: 'white' },
|
||||||
|
{ 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.className, color: '#decb6b' },
|
||||||
{ tag: t.invalid, color: '#ffffff' },
|
{ 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 },
|
||||||
|
],
|
||||||
|
});
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'black',
|
||||||
|
foreground: '#41FF00', // whats that?
|
||||||
|
caret: '#41FF00',
|
||||||
|
selection: '#ffffff20',
|
||||||
|
selectionMatch: '#036dd626',
|
||||||
|
lineHighlight: '#ffffff10',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: '#8a919966',
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'dark',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: '#41FF00' },
|
||||||
|
{ tag: t.operator, color: '#41FF00' },
|
||||||
|
{ tag: t.special(t.variableName), color: '#41FF00' },
|
||||||
|
{ tag: t.typeName, color: '#41FF00' },
|
||||||
|
{ tag: t.atom, color: '#41FF00' },
|
||||||
|
{ tag: t.number, color: '#41FF00' },
|
||||||
|
{ tag: t.definition(t.variableName), color: '#41FF00' },
|
||||||
|
{ tag: t.string, color: '#41FF00' },
|
||||||
|
{ tag: t.special(t.string), color: '#41FF00' },
|
||||||
|
{ tag: t.comment, color: '#41FF00' },
|
||||||
|
{ tag: t.variableName, color: '#41FF00' },
|
||||||
|
{ tag: t.tagName, color: '#41FF00' },
|
||||||
|
{ tag: t.bracket, color: '#41FF00' },
|
||||||
|
{ tag: t.meta, color: '#41FF00' },
|
||||||
|
{ tag: t.attributeName, color: '#41FF00' },
|
||||||
|
{ tag: t.propertyName, color: '#41FF00' },
|
||||||
|
{ tag: t.className, color: '#41FF00' },
|
||||||
|
{ tag: t.invalid, color: '#41FF00' },
|
||||||
|
],
|
||||||
|
});
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { createTheme } from '@uiw/codemirror-themes';
|
||||||
|
export const settings = {
|
||||||
|
background: 'white',
|
||||||
|
foreground: 'black', // whats that?
|
||||||
|
caret: 'black',
|
||||||
|
selection: 'rgba(128, 203, 196, 0.5)',
|
||||||
|
selectionMatch: '#ffffff26',
|
||||||
|
lineHighlight: '#cccccc50',
|
||||||
|
lineBackground: '#ffffff50',
|
||||||
|
gutterBackground: 'transparent',
|
||||||
|
gutterForeground: 'black',
|
||||||
|
light: true,
|
||||||
|
};
|
||||||
|
export default createTheme({
|
||||||
|
theme: 'light',
|
||||||
|
settings,
|
||||||
|
styles: [
|
||||||
|
{ tag: t.keyword, color: 'black' },
|
||||||
|
{ tag: t.operator, color: 'black' },
|
||||||
|
{ tag: t.special(t.variableName), color: 'black' },
|
||||||
|
{ tag: t.typeName, color: 'black' },
|
||||||
|
{ tag: t.atom, color: 'black' },
|
||||||
|
{ tag: t.number, color: 'black' },
|
||||||
|
{ tag: t.definition(t.variableName), color: 'black' },
|
||||||
|
{ tag: t.string, color: 'black' },
|
||||||
|
{ tag: t.special(t.string), color: 'black' },
|
||||||
|
{ tag: t.comment, color: 'black' },
|
||||||
|
{ tag: t.variableName, color: 'black' },
|
||||||
|
{ tag: t.tagName, color: 'black' },
|
||||||
|
{ tag: t.bracket, color: 'black' },
|
||||||
|
{ tag: t.meta, color: 'black' },
|
||||||
|
{ tag: t.attributeName, color: 'black' },
|
||||||
|
{ tag: t.propertyName, color: 'black' },
|
||||||
|
{ tag: t.className, color: 'black' },
|
||||||
|
{ tag: t.invalid, color: 'black' },
|
||||||
|
],
|
||||||
|
});
|
||||||
@@ -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: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es'],
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [...Object.keys(dependencies)],
|
external: [...Object.keys(dependencies)],
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
# @strudel.cycles/core
|
# @strudel/core
|
||||||
|
|
||||||
This package contains the bare essence of strudel.
|
This package contains the bare essence of strudel.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm i @strudel.cycles/core --save
|
npm i @strudel/core --save
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { sequence } from '@strudel.cycles/core';
|
import { sequence } from '@strudel/core';
|
||||||
|
|
||||||
const pattern = sequence('a', ['b', 'c']);
|
const pattern = sequence('a', ['b', 'c']);
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ b: 3/2 - 7/4
|
|||||||
c: 7/4 - 2
|
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 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 repl example](https://raw.githack.com/tidalcycles/strudel/main/packages/core/examples/vanilla.html)
|
||||||
- [open minimal vite example](./examples/vite-vanilla-repl/)
|
- [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;
|
const { createParams } = controls;
|
||||||
|
|
||||||
let clearColor = '#22222210';
|
let clearColor = '#22222210';
|
||||||
|
|||||||
+981
-344
File diff suppressed because it is too large
Load Diff
+42
-19
@@ -10,50 +10,68 @@ import { logger } from './logger.mjs';
|
|||||||
export class Cyclist {
|
export class Cyclist {
|
||||||
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
constructor({ interval, onTrigger, onToggle, onError, getTime, latency = 0.1 }) {
|
||||||
this.started = false;
|
this.started = false;
|
||||||
this.cps = 1; // TODO
|
this.cps = 0.5;
|
||||||
this.phase = 0;
|
this.num_ticks_since_cps_change = 0;
|
||||||
this.getTime = getTime;
|
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.onToggle = onToggle;
|
||||||
this.latency = latency;
|
this.latency = latency; // fixed trigger time offset
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
|
||||||
this.clock = createClock(
|
this.clock = createClock(
|
||||||
getTime,
|
getTime,
|
||||||
|
// called slightly before each cycle
|
||||||
(phase, duration, tick) => {
|
(phase, duration, tick) => {
|
||||||
if (tick === 0) {
|
if (tick === 0) {
|
||||||
this.origin = phase;
|
this.origin = phase;
|
||||||
}
|
}
|
||||||
const begin = round(phase - this.origin);
|
if (this.num_ticks_since_cps_change === 0) {
|
||||||
this.phase = begin - latency;
|
this.num_cycles_since_last_cps_change = this.lastEnd;
|
||||||
const end = round(begin + duration);
|
}
|
||||||
const time = getTime();
|
this.num_ticks_since_cps_change++;
|
||||||
try {
|
try {
|
||||||
const haps = this.pattern.queryArc(begin, end); // get Haps
|
const time = getTime();
|
||||||
|
const begin = this.lastEnd;
|
||||||
|
this.lastBegin = begin;
|
||||||
|
|
||||||
|
//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;
|
||||||
|
|
||||||
|
// 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) => {
|
haps.forEach((hap) => {
|
||||||
if (hap.part.begin.equals(hap.whole.begin)) {
|
if (hap.part.begin.equals(hap.whole.begin)) {
|
||||||
const deadline = hap.whole.begin + this.origin - time + latency;
|
const deadline = (hap.whole.begin - begin) / this.cps + tickdeadline + latency;
|
||||||
const duration = hap.duration * 1;
|
const duration = hap.duration / this.cps;
|
||||||
onTrigger?.(hap, deadline, duration);
|
onTrigger?.(hap, deadline, duration, this.cps);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger(`[cyclist] error: ${e.message}`);
|
logger(`[cyclist] error: ${e.message}`);
|
||||||
onError?.(e);
|
onError?.(e);
|
||||||
}
|
}
|
||||||
}, // called slightly before each cycle
|
},
|
||||||
interval, // duration of each cycle
|
interval, // duration of each cycle
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
getPhase() {
|
|
||||||
return this.getTime() - this.origin - this.latency;
|
|
||||||
}
|
|
||||||
now() {
|
now() {
|
||||||
return this.getTime() - this.origin + this.clock.minLatency;
|
const secondsSinceLastTick = this.getTime() - this.lastTick - this.clock.duration;
|
||||||
|
return this.lastBegin + secondsSinceLastTick * this.cps; // + this.clock.minLatency;
|
||||||
}
|
}
|
||||||
setStarted(v) {
|
setStarted(v) {
|
||||||
this.started = v;
|
this.started = v;
|
||||||
this.onToggle?.(v);
|
this.onToggle?.(v);
|
||||||
}
|
}
|
||||||
start() {
|
start() {
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
|
this.num_cycles_since_last_cps_change = 0;
|
||||||
if (!this.pattern) {
|
if (!this.pattern) {
|
||||||
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
throw new Error('Scheduler: no pattern set! call .setPattern first.');
|
||||||
}
|
}
|
||||||
@@ -69,6 +87,7 @@ export class Cyclist {
|
|||||||
stop() {
|
stop() {
|
||||||
logger('[cyclist] stop');
|
logger('[cyclist] stop');
|
||||||
this.clock.stop();
|
this.clock.stop();
|
||||||
|
this.lastEnd = 0;
|
||||||
this.setStarted(false);
|
this.setStarted(false);
|
||||||
}
|
}
|
||||||
setPattern(pat, autostart = false) {
|
setPattern(pat, autostart = false) {
|
||||||
@@ -77,8 +96,12 @@ export class Cyclist {
|
|||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setCps(cps = 1) {
|
setCps(cps = 0.5) {
|
||||||
|
if (this.cps === cps) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.cps = cps;
|
this.cps = cps;
|
||||||
|
this.num_ticks_since_cps_change = 0;
|
||||||
}
|
}
|
||||||
log(begin, end, haps) {
|
log(begin, end, haps) {
|
||||||
const onsets = haps.filter((h) => h.hasOnset());
|
const onsets = haps.filter((h) => h.hasOnset());
|
||||||
|
|||||||
+116
-6
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
draw.mjs - <short description TODO>
|
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/>.
|
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') => {
|
export const getDrawContext = (id = 'test-canvas') => {
|
||||||
let canvas = document.querySelector('#' + id);
|
let canvas = document.querySelector('#' + id);
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
|
const scale = 2; // 2 = crisp on retina screens
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
canvas.id = id;
|
canvas.id = id;
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth * scale;
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight * scale;
|
||||||
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0;z-index:5';
|
canvas.style = 'pointer-events:none;width:100%;height:100%;position:fixed;top:0;left:0';
|
||||||
document.body.prepend(canvas);
|
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');
|
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) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -51,7 +63,7 @@ Pattern.prototype.draw = function (callback, { from, to, onQuery }) {
|
|||||||
|
|
||||||
export const cleanupDraw = (clearScreen = true) => {
|
export const cleanupDraw = (clearScreen = true) => {
|
||||||
const ctx = getDrawContext();
|
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) {
|
if (window.strudelAnimation) {
|
||||||
cancelAnimationFrame(window.strudelAnimation);
|
cancelAnimationFrame(window.strudelAnimation);
|
||||||
}
|
}
|
||||||
@@ -65,3 +77,101 @@ Pattern.prototype.onPaint = function (onPaint) {
|
|||||||
this.context = { onPaint };
|
this.context = { onPaint };
|
||||||
return this;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,15 +11,9 @@ This program is free software: you can redistribute it and/or modify it under th
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
import { Pattern, timeCat, register, silence } from './pattern.mjs';
|
||||||
import { rotate, flatten } from './util.mjs';
|
import { rotate, flatten, splitAt, zipWith } from './util.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
|
|
||||||
const splitAt = function (index, value) {
|
|
||||||
return [value.slice(0, index), value.slice(index)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const zipWith = (f, xs, ys) => xs.map((n, i) => f(n, ys[i]));
|
|
||||||
|
|
||||||
const left = function (n, x) {
|
const left = function (n, x) {
|
||||||
const [ons, offs] = n;
|
const [ons, offs] = n;
|
||||||
const [xs, ys] = x;
|
const [xs, ys] = x;
|
||||||
@@ -151,8 +145,10 @@ export const { euclidrot, euclidRot } = register(['euclidrot', 'euclidRot'], fun
|
|||||||
* so there will be no gaps.
|
* so there will be no gaps.
|
||||||
* @name euclidLegato
|
* @name euclidLegato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
|
* @param {number} pulses the number of onsets / beats
|
||||||
|
* @param {number} steps the number of steps to fill
|
||||||
* @example
|
* @example
|
||||||
* n("g2").decay(.1).sustain(.3).euclidLegato(3,8)
|
* note("c3").euclidLegato(3,8)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
const _euclidLegato = function (pulses, steps, rotation, pat) {
|
||||||
@@ -172,6 +168,18 @@ export const euclidLegato = register(['euclidLegato'], function (pulses, steps,
|
|||||||
return _euclidLegato(pulses, steps, 0, pat);
|
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) {
|
export const euclidLegatoRot = register(['euclidLegatoRot'], function (pulses, steps, rotation, pat) {
|
||||||
return _euclidLegato(pulses, steps, rotation, pat);
|
return _euclidLegato(pulses, steps, rotation, pat);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
evaluate.mjs - <short description TODO>
|
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/>.
|
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 { isPattern } from './index.mjs';
|
import { isPattern } from './index.mjs';
|
||||||
|
|
||||||
let scoped = false;
|
|
||||||
export const evalScope = async (...args) => {
|
export const evalScope = async (...args) => {
|
||||||
if (scoped) {
|
|
||||||
console.warn('evalScope was called more than once.');
|
|
||||||
}
|
|
||||||
scoped = true;
|
|
||||||
const results = await Promise.allSettled(args);
|
const results = await Promise.allSettled(args);
|
||||||
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value);
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, i) => {
|
||||||
@@ -42,19 +37,15 @@ function safeEval(str, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const evaluate = async (code, transpiler) => {
|
export const evaluate = async (code, transpiler) => {
|
||||||
if (!scoped) {
|
let meta = {};
|
||||||
await evalScope(); // at least scope Pattern.prototype.boostrap
|
|
||||||
}
|
|
||||||
if (transpiler) {
|
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)
|
// if no transpiler is given, we expect a single instruction (!wrapExpression)
|
||||||
const options = { wrapExpression: !!transpiler };
|
const options = { wrapExpression: !!transpiler };
|
||||||
let evaluated = await safeEval(code, options);
|
let evaluated = await safeEval(code, options);
|
||||||
if (!isPattern(evaluated)) {
|
return { mode: 'javascript', pattern: evaluated, meta };
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
|
||||||
+18
-1
@@ -32,7 +32,11 @@ export class Hap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get duration() {
|
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() {
|
wholeOrPart() {
|
||||||
@@ -126,6 +130,19 @@ export class Hap {
|
|||||||
setContext(context) {
|
setContext(context) {
|
||||||
return new Hap(this.whole, this.part, this.value, context);
|
return new Hap(this.whole, this.part, this.value, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureObjectValue() {
|
||||||
|
/* if (isNote(hap.value)) {
|
||||||
|
// supports primitive hap values that look like notes
|
||||||
|
hap.value = { note: hap.value };
|
||||||
|
} */
|
||||||
|
if (typeof this.value !== 'object') {
|
||||||
|
throw new Error(
|
||||||
|
`expected hap.value to be an object, but got "${this.value}". Hint: append .note() or .s() to the end`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Hap;
|
export default Hap;
|
||||||
|
|||||||
@@ -18,23 +18,24 @@ export * from './util.mjs';
|
|||||||
export * from './speak.mjs';
|
export * from './speak.mjs';
|
||||||
export * from './evaluate.mjs';
|
export * from './evaluate.mjs';
|
||||||
export * from './repl.mjs';
|
export * from './repl.mjs';
|
||||||
|
export * from './cyclist.mjs';
|
||||||
export * from './logger.mjs';
|
export * from './logger.mjs';
|
||||||
export * from './time.mjs';
|
export * from './time.mjs';
|
||||||
export * from './draw.mjs';
|
export * from './draw.mjs';
|
||||||
export * from './animate.mjs';
|
export * from './animate.mjs';
|
||||||
export * from './pianoroll.mjs';
|
export * from './pianoroll.mjs';
|
||||||
|
export * from './spiral.mjs';
|
||||||
export * from './ui.mjs';
|
export * from './ui.mjs';
|
||||||
export { default as drawLine } from './drawLine.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)
|
// below won't work with runtime.mjs (json import fails)
|
||||||
/* import * as p from './package.json';
|
/* import * as p from './package.json';
|
||||||
export const version = p.version; */
|
export const version = p.version; */
|
||||||
logger('🌀 @strudel.cycles/core loaded 🌀');
|
logger('🌀 @strudel/core loaded 🌀');
|
||||||
if (globalThis._strudelLoaded) {
|
if (globalThis._strudelLoaded) {
|
||||||
console.warn(
|
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.
|
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;
|
globalThis._strudelLoaded = true;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@strudel.cycles/core",
|
"name": "@strudel/core",
|
||||||
"version": "0.6.8",
|
"version": "1.0.1",
|
||||||
"description": "Port of Tidal Cycles to JavaScript",
|
"description": "Port of Tidal Cycles to JavaScript",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.mjs"
|
||||||
"module": "dist/index.mjs"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
@@ -29,13 +28,13 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/tidalcycles/strudel/issues"
|
"url": "https://github.com/tidalcycles/strudel/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://strudel.tidalcycles.org",
|
"homepage": "https://strudel.cc",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fraction.js": "^4.2.0"
|
"fraction.js": "^4.3.7"
|
||||||
},
|
},
|
||||||
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
"gitHead": "0e26d4e741500f5bae35b023608f062a794905c2",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^3.2.2",
|
"vite": "^5.0.10",
|
||||||
"vitest": "^0.25.7"
|
"vitest": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+364
-128
@@ -26,7 +26,7 @@ export class Pattern {
|
|||||||
/**
|
/**
|
||||||
* Create a pattern. As an end user, you will most likely not create a Pattern directly.
|
* 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
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
constructor(query) {
|
constructor(query) {
|
||||||
@@ -39,7 +39,7 @@ export class Pattern {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new pattern, with the function applied to the value of
|
* 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
|
* @synonyms fmap
|
||||||
* @param {Function} func to to apply to the value
|
* @param {Function} func to to apply to the value
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -51,7 +51,7 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* see {@link Pattern#withValue}
|
* see `withValue`
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
fmap(func) {
|
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
|
* but the timespan from the function of patterns that this method is called
|
||||||
* on. In practice, this means that the pattern structure, including onsets,
|
* on. In practice, this means that the pattern structure, including onsets,
|
||||||
* are preserved from the pattern of functions (often referred to as the left
|
* 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 of values, i.e. structure is preserved from the right hand/outer
|
||||||
* pattern.
|
* pattern.
|
||||||
* @param {Pattern} pat_val
|
* @param {Pattern} pat_val
|
||||||
@@ -340,9 +340,9 @@ export class Pattern {
|
|||||||
* silence
|
* silence
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
queryArc(begin, end) {
|
queryArc(begin, end, controls = {}) {
|
||||||
try {
|
try {
|
||||||
return this.query(new State(new TimeSpan(begin, end)));
|
return this.query(new State(new TimeSpan(begin, end), controls));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[query]: ${err.message}`, 'error');
|
logger(`[query]: ${err.message}`, 'error');
|
||||||
return [];
|
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.
|
* begin and end time of the query timespan.
|
||||||
* @param {Function} func the function to apply
|
* @param {Function} func the function to apply
|
||||||
* @returns Pattern
|
* @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
|
* of all haps returned by pattern queries (both `part` timespans, and where
|
||||||
* present, `whole` timespans).
|
* present, `whole` timespans).
|
||||||
* @param {Function} func
|
* @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.
|
* begin and end time of the hap timespans.
|
||||||
* @param {Function} func the function to apply
|
* @param {Function} func the function to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
@@ -427,11 +427,11 @@ export class Pattern {
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withHaps(func) {
|
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
|
* @param {Function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
@@ -472,15 +472,15 @@ export class Pattern {
|
|||||||
/**
|
/**
|
||||||
* Returns a new pattern with the given location information added to the
|
* Returns a new pattern with the given location information added to the
|
||||||
* context of every hap.
|
* context of every hap.
|
||||||
* @param {Number} start
|
* @param {Number} start start offset
|
||||||
* @param {Number} end
|
* @param {Number} end end offset
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
withLocation(start, end) {
|
withLoc(start, end) {
|
||||||
const location = {
|
const location = {
|
||||||
start: { line: start[0], column: start[1], offset: start[2] },
|
start,
|
||||||
end: { line: end[0], column: end[1], offset: end[2] },
|
end,
|
||||||
};
|
};
|
||||||
return this.withContext((context) => {
|
return this.withContext((context) => {
|
||||||
const locations = (context.locations || []).concat([location]);
|
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.
|
* 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
|
* @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.
|
* inside haps.
|
||||||
* @param {Function} value_test
|
* @param {Function} value_test
|
||||||
* @returns Pattern
|
* @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
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
get showFirstCycle() {
|
get showFirstCycle() {
|
||||||
@@ -717,13 +691,13 @@ export class Pattern {
|
|||||||
// Methods without corresponding toplevel functions
|
// 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
|
* @name layer
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @synonyms apply
|
* @synonyms apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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"))
|
* .layer(x=>x.add("0,2"))
|
||||||
* .scale('C minor').note()
|
* .scale('C minor').note()
|
||||||
*/
|
*/
|
||||||
@@ -737,7 +711,7 @@ export class Pattern {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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))
|
* .superimpose(x=>x.add(2))
|
||||||
* .scale('C minor').note()
|
* .scale('C minor').note()
|
||||||
*/
|
*/
|
||||||
@@ -753,8 +727,8 @@ export class Pattern {
|
|||||||
* @name stack
|
* @name stack
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
* s("hh*2").stack(
|
* s("hh*4").stack(
|
||||||
* note("c2(3,8)")
|
* note("c4(5,8)")
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
stack(...pats) {
|
stack(...pats) {
|
||||||
@@ -771,8 +745,8 @@ export class Pattern {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @synonyms sequence, fastcat
|
* @synonyms sequence, fastcat
|
||||||
* @example
|
* @example
|
||||||
* s("hh*2").seq(
|
* s("hh*4").seq(
|
||||||
* note("c2(3,8)")
|
* note("c4(5,8)")
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
seq(...pats) {
|
seq(...pats) {
|
||||||
@@ -785,8 +759,8 @@ export class Pattern {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @synonyms slowcat
|
* @synonyms slowcat
|
||||||
* @example
|
* @example
|
||||||
* s("hh*2").cat(
|
* s("hh*4").cat(
|
||||||
* note("c2(3,8)")
|
* note("c4(5,8)")
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
cat(...pats) {
|
cat(...pats) {
|
||||||
@@ -809,13 +783,14 @@ export class Pattern {
|
|||||||
hap.setContext({
|
hap.setContext({
|
||||||
...hap.context,
|
...hap.context,
|
||||||
onTrigger: (...args) => {
|
onTrigger: (...args) => {
|
||||||
if (!dominant && hap.context.onTrigger) {
|
// run previously set trigger, if it exists
|
||||||
hap.context.onTrigger(...args);
|
hap.context.onTrigger?.(...args);
|
||||||
}
|
|
||||||
onTrigger(...args);
|
onTrigger(...args);
|
||||||
},
|
},
|
||||||
// we need this to know later if the default trigger should still fire
|
// if dominantTrigger is set to true, the default output (webaudio) will be disabled
|
||||||
dominantTrigger: dominant,
|
// 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,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -840,7 +815,7 @@ export class Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// functions relating to chords/patterns of lists
|
// functions relating to chords/patterns of lists/lists of patterns
|
||||||
|
|
||||||
// returns Array<Hap[]> where each list of haps satisfies eq
|
// returns Array<Hap[]> where each list of haps satisfies eq
|
||||||
function groupHapsBy(eq, haps) {
|
function groupHapsBy(eq, haps) {
|
||||||
@@ -883,12 +858,41 @@ Pattern.prototype.arpWith = function (func) {
|
|||||||
* Selects indices in in stacked notes.
|
* Selects indices in in stacked notes.
|
||||||
* @example
|
* @example
|
||||||
* note("<[c,eb,g]!2 [c,f,ab] [d,f,ab]>")
|
* 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) {
|
Pattern.prototype.arp = function (pat) {
|
||||||
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
return this.arpWith((haps) => pat.fmap((i) => haps[i % haps.length]));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Takes a time duration followed by one or more patterns, and shifts the given patterns in time, so they are
|
||||||
|
* distributed equally over the given time duration. They are then combined with the pattern 'weave' is called on, after it has been stretched out (i.e. slowed down by) the time duration.
|
||||||
|
* @name weave
|
||||||
|
* @memberof Pattern
|
||||||
|
* @example pan(saw).weave(4, s("bd(3,8)"), s("~ sd"))
|
||||||
|
* @example n("0 1 2 3 4 5 6 7").weave(8, s("bd(3,8)"), s("~ sd"))
|
||||||
|
|
||||||
|
addToPrototype('weave', function (t, ...pats) {
|
||||||
|
return this.weaveWith(t, ...pats.map((x) => set.out(x)));
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
* Like 'weave', but accepts functions rather than patterns, which are applied to the pattern.
|
||||||
|
* @name weaveWith
|
||||||
|
* @memberof Pattern
|
||||||
|
|
||||||
|
addToPrototype('weaveWith', function (t, ...funcs) {
|
||||||
|
const pat = this;
|
||||||
|
const l = funcs.length;
|
||||||
|
t = Fraction(t);
|
||||||
|
if (l == 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
|
return stack(...funcs.map((func, i) => pat.inside(t, func).early(Fraction(i).div(l))))._slow(t);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// compose matrix functions
|
// compose matrix functions
|
||||||
|
|
||||||
@@ -925,14 +929,14 @@ function _composeOp(a, b, func) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
* // Here, the triad 0, 2, 4 is shifted by different amounts
|
* // 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:
|
* // 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
|
* @example
|
||||||
* // You can also use add with notes:
|
* // 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:
|
* // 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
|
add: [numeralArgs((a, b) => a + b)], // support string concatenation
|
||||||
/**
|
/**
|
||||||
@@ -941,7 +945,7 @@ function _composeOp(a, b, func) {
|
|||||||
* @name sub
|
* @name sub
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @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.
|
* // See add for more information.
|
||||||
*/
|
*/
|
||||||
sub: [numeralArgs((a, b) => a - b)],
|
sub: [numeralArgs((a, b) => a - b)],
|
||||||
@@ -951,7 +955,7 @@ function _composeOp(a, b, func) {
|
|||||||
* @name mul
|
* @name mul
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @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)],
|
mul: [numeralArgs((a, b) => a * b)],
|
||||||
/**
|
/**
|
||||||
@@ -1045,9 +1049,9 @@ function _composeOp(a, b, func) {
|
|||||||
* Applies the given structure to the pattern:
|
* Applies the given structure to the pattern:
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* note("c3,eb3,g3")
|
* note("c,eb,g")
|
||||||
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
* .struct("x ~ x ~ ~ x ~ x ~ ~ ~ x ~ x ~ ~")
|
||||||
* .slow(4)
|
* .slow(2)
|
||||||
*/
|
*/
|
||||||
Pattern.prototype.struct = function (...args) {
|
Pattern.prototype.struct = function (...args) {
|
||||||
return this.keepif.out(...args);
|
return this.keepif.out(...args);
|
||||||
@@ -1059,7 +1063,7 @@ function _composeOp(a, b, func) {
|
|||||||
* Returns silence when mask is 0 or "~"
|
* Returns silence when mask is 0 or "~"
|
||||||
*
|
*
|
||||||
* @example
|
* @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) {
|
Pattern.prototype.mask = function (...args) {
|
||||||
return this.keepif.in(...args);
|
return this.keepif.in(...args);
|
||||||
@@ -1071,7 +1075,7 @@ function _composeOp(a, b, func) {
|
|||||||
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
* Resets the pattern to the start of the cycle for each onset of the reset pattern.
|
||||||
*
|
*
|
||||||
* @example
|
* @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) {
|
Pattern.prototype.reset = function (...args) {
|
||||||
return this.keepif.trig(...args);
|
return this.keepif.trig(...args);
|
||||||
@@ -1084,7 +1088,7 @@ function _composeOp(a, b, func) {
|
|||||||
* While reset will only reset the current cycle, restart will start from cycle 0.
|
* While reset will only reset the current cycle, restart will start from cycle 0.
|
||||||
*
|
*
|
||||||
* @example
|
* @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) {
|
Pattern.prototype.restart = function (...args) {
|
||||||
return this.keepif.trigzero(...args);
|
return this.keepif.trigzero(...args);
|
||||||
@@ -1098,6 +1102,8 @@ function _composeOp(a, b, func) {
|
|||||||
export const polyrhythm = stack;
|
export const polyrhythm = stack;
|
||||||
export const pr = stack;
|
export const pr = stack;
|
||||||
|
|
||||||
|
export const pm = polymeter;
|
||||||
|
|
||||||
// methods that create patterns, which are added to patternified Pattern methods
|
// methods that create patterns, which are added to patternified Pattern methods
|
||||||
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
// TODO: remove? this is only used in old transpiler (shapeshifter)
|
||||||
Pattern.prototype.factories = {
|
Pattern.prototype.factories = {
|
||||||
@@ -1148,8 +1154,8 @@ export function isPattern(thing) {
|
|||||||
/* if (!thing instanceof Pattern) {
|
/* if (!thing instanceof Pattern) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Found Pattern that fails "instanceof Pattern" check.
|
`Found Pattern that fails "instanceof Pattern" check.
|
||||||
This may happen if you are using multiple versions of @strudel.cycles/core.
|
This may happen if you are using multiple versions of @strudel/core.
|
||||||
Please check by running "npm ls @strudel.cycles/core".`,
|
Please check by running "npm ls @strudel/core".`,
|
||||||
);
|
);
|
||||||
console.log(thing);
|
console.log(thing);
|
||||||
} */
|
} */
|
||||||
@@ -1172,7 +1178,8 @@ export function reify(thing) {
|
|||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @synonyms polyrhythm, pr
|
* @synonyms polyrhythm, pr
|
||||||
* @example
|
* @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) {
|
export function stack(...pats) {
|
||||||
// Array test here is to avoid infinite recursions..
|
// Array test here is to avoid infinite recursions..
|
||||||
@@ -1183,11 +1190,11 @@ export function stack(...pats) {
|
|||||||
|
|
||||||
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
/** Concatenation: combines a list of patterns, switching between them successively, one per cycle:
|
||||||
*
|
*
|
||||||
* synonyms: {@link cat}
|
* synonyms: `cat`
|
||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* slowcat(e5, b4, [d5, c5])
|
* slowcat("e5", "b4", ["d5", "c5"])
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function slowcat(...pats) {
|
export function slowcat(...pats) {
|
||||||
@@ -1231,17 +1238,19 @@ export function slowcatPrime(...pats) {
|
|||||||
* @synonyms slowcat
|
* @synonyms slowcat
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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) {
|
export function cat(...pats) {
|
||||||
return slowcat(...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}
|
* @return {Pattern}
|
||||||
* @example
|
* @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) {
|
export function timeCat(...timepats) {
|
||||||
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
const total = timepats.map((a) => a[0]).reduce((a, b) => a.add(b), Fraction(0));
|
||||||
@@ -1255,11 +1264,28 @@ export function timeCat(...timepats) {
|
|||||||
return stack(...pats);
|
return stack(...pats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to arrange multiple patterns together over multiple cycles.
|
||||||
|
* Takes a variable number of arrays with two elements specifying the number of cycles and the pattern to use.
|
||||||
|
*
|
||||||
|
* @return {Pattern}
|
||||||
|
* @example
|
||||||
|
* 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);
|
||||||
|
sections = sections.map(([cycles, section]) => [cycles, section.fast(cycles)]);
|
||||||
|
return timeCat(...sections).slow(total);
|
||||||
|
}
|
||||||
|
|
||||||
export function fastcat(...pats) {
|
export function fastcat(...pats) {
|
||||||
return slowcat(...pats)._fast(pats.length);
|
return slowcat(...pats)._fast(pats.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See {@link fastcat} */
|
/** See `fastcat` */
|
||||||
export function sequence(...pats) {
|
export function sequence(...pats) {
|
||||||
return fastcat(...pats);
|
return fastcat(...pats);
|
||||||
}
|
}
|
||||||
@@ -1267,7 +1293,8 @@ export function sequence(...pats) {
|
|||||||
/** Like **cat**, but the items are crammed into one cycle.
|
/** Like **cat**, but the items are crammed into one cycle.
|
||||||
* @synonyms fastcat, sequence
|
* @synonyms fastcat, sequence
|
||||||
* @example
|
* @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) {
|
export function seq(...pats) {
|
||||||
@@ -1293,9 +1320,9 @@ function _sequenceCount(x) {
|
|||||||
* @param {number} steps how many items are placed in one cycle
|
* @param {number} steps how many items are placed in one cycle
|
||||||
* @param {any[]} sequences one or more arrays of Patterns / values
|
* @param {any[]} sequences one or more arrays of Patterns / values
|
||||||
* @example
|
* @example
|
||||||
* polymeterSteps(2, ["c", "d", "e", "f", "g", "f", "e", "d"])
|
* polymeterSteps(4, ["c", "d", "e"])
|
||||||
* .note().stack(s("bd")) // 1 cycle = 1 bd = 2 notes
|
* .note().stack(s("bd"))
|
||||||
* // note("{c d e f g f e d}%2").stack(s("bd"))
|
* // note("{c d e}%4").stack(s("bd"))
|
||||||
*/
|
*/
|
||||||
export function polymeterSteps(steps, ...args) {
|
export function polymeterSteps(steps, ...args) {
|
||||||
const seqs = args.map((a) => _sequenceCount(a));
|
const seqs = args.map((a) => _sequenceCount(a));
|
||||||
@@ -1331,11 +1358,6 @@ export function polymeter(...args) {
|
|||||||
return polymeterSteps(0, ...args);
|
return polymeterSteps(0, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// alias
|
|
||||||
export function pm(...args) {
|
|
||||||
polymeter(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mask = curry((a, b) => reify(b).mask(a));
|
export const mask = curry((a, b) => reify(b).mask(a));
|
||||||
export const struct = curry((a, b) => reify(b).struct(a));
|
export const struct = curry((a, b) => reify(b).struct(a));
|
||||||
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
export const superimpose = curry((a, b) => reify(b).superimpose(...a));
|
||||||
@@ -1375,17 +1397,18 @@ export const func = curry((a, b) => reify(b).func(a));
|
|||||||
* @noAutocomplete
|
* @noAutocomplete
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function register(name, func) {
|
export function register(name, func, patternify = true) {
|
||||||
if (Array.isArray(name)) {
|
if (Array.isArray(name)) {
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const name_item of name) {
|
for (const name_item of name) {
|
||||||
result[name_item] = register(name_item, func);
|
result[name_item] = register(name_item, func, patternify);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
const arity = func.length;
|
const arity = func.length;
|
||||||
var pfunc; // the patternified function
|
var pfunc; // the patternified function
|
||||||
|
|
||||||
|
if (patternify) {
|
||||||
pfunc = function (...args) {
|
pfunc = function (...args) {
|
||||||
args = args.map(reify);
|
args = args.map(reify);
|
||||||
const pat = args[args.length - 1];
|
const pat = args[args.length - 1];
|
||||||
@@ -1405,9 +1428,14 @@ export function register(name, func) {
|
|||||||
mapFn = curry(mapFn, null, arity - 1);
|
mapFn = curry(mapFn, null, arity - 1);
|
||||||
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
return right.reduce((acc, p) => acc.appLeft(p), left.fmap(mapFn)).innerJoin();
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
pfunc = function (...args) {
|
||||||
|
args = args.map(reify);
|
||||||
|
return func(...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Pattern.prototype[name] = function (...args) {
|
Pattern.prototype[name] = function (...args) {
|
||||||
args = args.map(reify);
|
|
||||||
// For methods that take a single argument (plus 'this'), allow
|
// For methods that take a single argument (plus 'this'), allow
|
||||||
// multiple arguments but sequence them
|
// multiple arguments but sequence them
|
||||||
if (arity === 2 && args.length !== 1) {
|
if (arity === 2 && args.length !== 1) {
|
||||||
@@ -1415,6 +1443,7 @@ export function register(name, func) {
|
|||||||
} else if (arity !== args.length + 1) {
|
} else if (arity !== args.length + 1) {
|
||||||
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
throw new Error(`.${name}() expects ${arity - 1} inputs but got ${args.length}.`);
|
||||||
}
|
}
|
||||||
|
args = args.map(reify);
|
||||||
return pfunc(...args, this);
|
return pfunc(...args, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1441,7 +1470,7 @@ export function register(name, func) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const round = register('round', function (pat) {
|
||||||
return pat.asNumber().fmap((v) => Math.round(v));
|
return pat.asNumber().fmap((v) => Math.round(v));
|
||||||
@@ -1455,7 +1484,7 @@ export const round = register('round', function (pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "42 42.1 42.5 43".floor().note()
|
* note("42 42.1 42.5 43".floor())
|
||||||
*/
|
*/
|
||||||
export const floor = register('floor', function (pat) {
|
export const floor = register('floor', function (pat) {
|
||||||
return pat.asNumber().fmap((v) => Math.floor(v));
|
return pat.asNumber().fmap((v) => Math.floor(v));
|
||||||
@@ -1469,7 +1498,7 @@ export const floor = register('floor', function (pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* "42 42.1 42.5 43".ceil().note()
|
* note("42 42.1 42.5 43".ceil())
|
||||||
*/
|
*/
|
||||||
export const ceil = register('ceil', function (pat) {
|
export const ceil = register('ceil', function (pat) {
|
||||||
return pat.asNumber().fmap((v) => Math.ceil(v));
|
return pat.asNumber().fmap((v) => Math.ceil(v));
|
||||||
@@ -1502,7 +1531,8 @@ export const fromBipolar = register('fromBipolar', function (pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const range = register('range', function (min, max, pat) {
|
||||||
return pat.mul(max - min).add(min);
|
return pat.mul(max - min).add(min);
|
||||||
@@ -1516,7 +1546,8 @@ export const range = register('range', function (min, max, pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const rangex = register('rangex', function (min, max, pat) {
|
||||||
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
return pat._range(Math.log(min), Math.log(max)).fmap(Math.exp);
|
||||||
@@ -1529,12 +1560,32 @@ export const rangex = register('rangex', function (min, max, pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const range2 = register('range2', function (min, max, pat) {
|
||||||
return pat.fromBipolar()._range(min, max);
|
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
|
// Structural and temporal transformations
|
||||||
|
|
||||||
@@ -1596,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
|
* @example
|
||||||
* s("bd hh sd hh").focus(1/4, 3/4)
|
* s("bd hh sd hh").focus(1/4, 3/4)
|
||||||
*/
|
*/
|
||||||
@@ -1627,9 +1678,12 @@ export const ply = register('ply', function (factor, pat) {
|
|||||||
* @param {number | Pattern} factor speed up factor
|
* @param {number | Pattern} factor speed up factor
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const { fast, density } = register(['fast', 'density'], function (factor, pat) {
|
||||||
|
if (factor === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
factor = Fraction(factor);
|
factor = Fraction(factor);
|
||||||
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
const fastQuery = pat.withQueryTime((t) => t.mul(factor));
|
||||||
return fastQuery.withHapTime((t) => t.div(factor));
|
return fastQuery.withHapTime((t) => t.div(factor));
|
||||||
@@ -1653,9 +1707,12 @@ export const hurry = register('hurry', function (r, pat) {
|
|||||||
* @param {number | Pattern} factor slow down factor
|
* @param {number | Pattern} factor slow down factor
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const { slow, sparsity } = register(['slow', 'sparsity'], function (factor, pat) {
|
||||||
|
if (factor === 0) {
|
||||||
|
return silence;
|
||||||
|
}
|
||||||
return pat._fast(Fraction(1).div(factor));
|
return pat._fast(Fraction(1).div(factor));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1707,7 +1764,7 @@ export const lastOf = register('lastOf', function (n, func, pat) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An alias for {@link firstOf}
|
* An alias for `firstOf`
|
||||||
* @name every
|
* @name every
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @param {number} n how many cycles
|
* @param {number} n how many cycles
|
||||||
@@ -1739,9 +1796,9 @@ export const apply = register('apply', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* s("<bd sd>,hh*2").cpm(90) // = 90 bpm
|
* 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) {
|
export const cpm = register('cpm', function (cpm, pat) {
|
||||||
return pat._fast(cpm / 60);
|
return pat._fast(cpm / 60 / 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1814,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.
|
* 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
|
* @param {number} segments number of segments per cycle
|
||||||
* @example
|
* @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) {
|
export const segment = register('segment', function (rate, pat) {
|
||||||
return pat.struct(pure(true)._fast(rate));
|
return pat.struct(pure(true)._fast(rate));
|
||||||
@@ -1840,7 +1897,7 @@ export const { invert, inv } = register(['invert', 'inv'], function (pat) {
|
|||||||
* @param {function} func
|
* @param {function} func
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const when = register('when', function (on, func, pat) {
|
||||||
return on ? func(pat) : pat;
|
return on ? func(pat) : pat;
|
||||||
@@ -1877,7 +1934,7 @@ export const brak = register('brak', function (pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* note("c3 d3 e3 g3").rev()
|
* note("c d e g").rev()
|
||||||
*/
|
*/
|
||||||
export const rev = register('rev', function (pat) {
|
export const rev = register('rev', function (pat) {
|
||||||
const query = function (state) {
|
const query = function (state) {
|
||||||
@@ -1929,9 +1986,9 @@ export const press = register('press', function (pat) {
|
|||||||
* s("hh*3")
|
* s("hh*3")
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
export const hush = register('hush', function (pat) {
|
Pattern.prototype.hush = function () {
|
||||||
return silence;
|
return silence;
|
||||||
});
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
|
* Applies `rev` to a pattern every other cycle, so that the pattern alternates between forwards and backwards.
|
||||||
@@ -1939,7 +1996,7 @@ export const hush = register('hush', function (pat) {
|
|||||||
* note("c d e g").palindrome()
|
* note("c d e g").palindrome()
|
||||||
*/
|
*/
|
||||||
export const palindrome = register('palindrome', function (pat) {
|
export const palindrome = register('palindrome', function (pat) {
|
||||||
return pat.every(2, rev);
|
return pat.lastOf(2, rev);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1947,7 +2004,7 @@ export const palindrome = register('palindrome', function (pat) {
|
|||||||
* @name juxBy
|
* @name juxBy
|
||||||
* @synonyms juxby
|
* @synonyms juxby
|
||||||
* @example
|
* @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) {
|
export const { juxBy, juxby } = register(['juxBy', 'juxby'], function (by, func, pat) {
|
||||||
by /= 2;
|
by /= 2;
|
||||||
@@ -1966,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.
|
* The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.
|
||||||
* @example
|
* @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) {
|
export const jux = register('jux', function (func, pat) {
|
||||||
return pat._juxBy(1, func, pat);
|
return pat._juxBy(1, func, pat);
|
||||||
@@ -1982,7 +2043,7 @@ export const jux = register('jux', function (func, pat) {
|
|||||||
* @example
|
* @example
|
||||||
* "<0 [2 4]>"
|
* "<0 [2 4]>"
|
||||||
* .echoWith(4, 1/8, (p,n) => p.add(n*2))
|
* .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(
|
export const { echoWith, echowith, stutWith, stutwith } = register(
|
||||||
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
['echoWith', 'echowith', 'stutWith', 'stutwith'],
|
||||||
@@ -2055,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).
|
* Repeats each cycle the given number of times.
|
||||||
* @name chunk
|
* @name repeatCycles
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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);
|
const binary = Array(n - 1).fill(false);
|
||||||
binary.unshift(true);
|
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);
|
return pat.when(binary_pat, func);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const chunk = register('chunk', function (n, func, pat) {
|
const { chunk, slowchunk, slowChunk } = register(['chunk', 'slowchunk', 'slowChunk'], function (n, func, pat) {
|
||||||
return _chunk(n, func, pat, false);
|
return _chunk(n, func, pat, false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2080,12 +2162,29 @@ export const chunk = register('chunk', function (n, func, pat) {
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const { chunkBack, chunkback } = register(['chunkBack', 'chunkback'], function (n, func, pat) {
|
||||||
return _chunk(n, func, pat, true);
|
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
|
// TODO - redefine elsewhere in terms of mask
|
||||||
export const bypass = register('bypass', function (on, pat) {
|
export const bypass = register('bypass', function (on, pat) {
|
||||||
on = Boolean(parseInt(on));
|
on = Boolean(parseInt(on));
|
||||||
@@ -2097,6 +2196,8 @@ export const bypass = register('bypass', function (on, pat) {
|
|||||||
* @param {number} offset start point of loop in cycles
|
* @param {number} offset start point of loop in cycles
|
||||||
* @param {number} cycles loop length in cycles
|
* @param {number} cycles loop length in cycles
|
||||||
* @example
|
* @example
|
||||||
|
* note("<c d e f>").ribbon(1, 2).fast(2)
|
||||||
|
* @example
|
||||||
* // Looping a portion of randomness
|
* // Looping a portion of randomness
|
||||||
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
|
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
|
||||||
*/
|
*/
|
||||||
@@ -2108,8 +2209,19 @@ export const duration = register('duration', function (value, pat) {
|
|||||||
return pat.withHapSpan((span) => new TimeSpan(span.begin, span.begin.add(value)));
|
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.
|
* 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
|
// TODO: move this to controls https://github.com/tidalcycles/strudel/issues/288
|
||||||
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
|
export const { color, colour } = register(['color', 'colour'], function (color, pat) {
|
||||||
@@ -2132,6 +2244,7 @@ export const velocity = register('velocity', function (velocity, pat) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* Multiplies the hap duration with the given factor.
|
* 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
|
* @name legato
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @example
|
* @example
|
||||||
@@ -2158,7 +2271,7 @@ export const legato = register('legato', function (value, pat) {
|
|||||||
* s("rhodes")
|
* s("rhodes")
|
||||||
* .chop(4)
|
* .chop(4)
|
||||||
* .rev() // reverse order of chops
|
* .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) {
|
export const chop = register('chop', function (n, pat) {
|
||||||
@@ -2170,6 +2283,14 @@ export const chop = register('chop', function (n, pat) {
|
|||||||
return pat.squeezeBind(func);
|
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) {
|
export const striate = register('striate', function (n, pat) {
|
||||||
const slices = Array.from({ length: n }, (x, i) => i);
|
const slices = Array.from({ length: n }, (x, i) => i);
|
||||||
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
const slice_objects = slices.map((i) => ({ begin: i / n, end: (i + 1) / n }));
|
||||||
@@ -2184,19 +2305,102 @@ export const striate = register('striate', function (n, pat) {
|
|||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @example
|
||||||
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
* samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
|
||||||
* s("rhodes").loopAt(4)
|
* s("rhodes").loopAt(2)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
// TODO - global cps clock
|
||||||
const _loopAt = function (factor, pat, cps = 1) {
|
const _loopAt = function (factor, pat, cps = 0.5) {
|
||||||
return pat
|
return pat
|
||||||
.speed((1 / factor) * cps)
|
.speed((1 / factor) * cps)
|
||||||
.unit('c')
|
.unit('c')
|
||||||
.slow(factor);
|
.slow(factor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat) {
|
/**
|
||||||
return _loopAt(factor, pat, 1);
|
* 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(0.75)
|
||||||
|
* @example
|
||||||
|
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
* s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const slice = register(
|
||||||
|
'slice',
|
||||||
|
function (npat, ipat, opat) {
|
||||||
|
return npat.innerBind((n) =>
|
||||||
|
ipat.outerBind((i) =>
|
||||||
|
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 };
|
||||||
|
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 });
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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
|
||||||
|
* @example
|
||||||
|
* await samples('github:tidalcycles/Dirt-Samples/master')
|
||||||
|
* s("breaks165")
|
||||||
|
* .splice(8, "0 1 [2 3 0]@2 3 0@2 7")
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const splice = register(
|
||||||
|
'splice',
|
||||||
|
function (npat, ipat, opat) {
|
||||||
|
const sliced = slice(npat, ipat, opat);
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
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
|
* Makes the sample fit the given number of cycles and cps value, by
|
||||||
@@ -2211,6 +2415,38 @@ const { loopAt, loopat } = register(['loopAt', 'loopat'], function (factor, pat)
|
|||||||
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
* s("rhodes").loopAtCps(4,1.5).cps(1.5)
|
||||||
*/
|
*/
|
||||||
// TODO - global cps clock
|
// 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);
|
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);
|
||||||
|
};
|
||||||
|
|||||||
+132
-137
@@ -1,10 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
pianoroll.mjs - <short description TODO>
|
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/>.
|
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 { Pattern, toMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
import { Pattern, noteToMidi, getDrawContext, freqToMidi, isNote } from './index.mjs';
|
||||||
|
|
||||||
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
const scale = (normalized, min, max) => normalized * (max - min) + min;
|
||||||
const getValue = (e) => {
|
const getValue = (e) => {
|
||||||
@@ -18,7 +18,7 @@ const getValue = (e) => {
|
|||||||
}
|
}
|
||||||
note = note ?? n;
|
note = note ?? n;
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return toMidi(note);
|
return noteToMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -29,129 +29,26 @@ const getValue = (e) => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
Pattern.prototype.pianoroll = function ({
|
Pattern.prototype.pianoroll = function (options = {}) {
|
||||||
cycles = 4,
|
let { cycles = 4, playhead = 0.5, overscan = 1, hideNegative = false } = options;
|
||||||
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;
|
|
||||||
let from = -cycles * playhead;
|
let from = -cycles * playhead;
|
||||||
let to = cycles * (1 - 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(
|
this.draw(
|
||||||
(ctx, events, t) => {
|
(ctx, haps, t) => {
|
||||||
ctx.fillStyle = background;
|
|
||||||
ctx.globalAlpha = 1; // reset!
|
|
||||||
if (!smear) {
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
|
||||||
ctx.fillRect(0, 0, w, h);
|
|
||||||
}
|
|
||||||
const inFrame = (event) =>
|
const inFrame = (event) =>
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.whole.end >= t + from;
|
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= t + to && event.endClipped >= t + from;
|
||||||
events.filter(inFrame).forEach((event) => {
|
pianoroll({
|
||||||
const isActive = event.whole.begin <= t && event.whole.end > t;
|
...options,
|
||||||
ctx.fillStyle = event.context?.color || inactive;
|
time: t,
|
||||||
ctx.strokeStyle = event.context?.color || active;
|
ctx,
|
||||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
haps: haps.filter(inFrame),
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
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,
|
from: from - overscan,
|
||||||
to: to + 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;
|
return this;
|
||||||
@@ -159,6 +56,40 @@ Pattern.prototype.pianoroll = function ({
|
|||||||
|
|
||||||
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
// this function allows drawing a pianoroll without ties to Pattern.prototype
|
||||||
// it will probably replace the above in the future
|
// 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({
|
export function pianoroll({
|
||||||
time,
|
time,
|
||||||
haps,
|
haps,
|
||||||
@@ -181,6 +112,14 @@ export function pianoroll({
|
|||||||
timeframe: timeframeProp,
|
timeframe: timeframeProp,
|
||||||
fold = 0,
|
fold = 0,
|
||||||
vertical = 0,
|
vertical = 0,
|
||||||
|
labels = false,
|
||||||
|
fill = 1,
|
||||||
|
fillActive = false,
|
||||||
|
strokeActive = true,
|
||||||
|
stroke,
|
||||||
|
hideInactive = 0,
|
||||||
|
colorizeInactive = 1,
|
||||||
|
fontFamily,
|
||||||
ctx,
|
ctx,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const w = ctx.canvas.width;
|
const w = ctx.canvas.width;
|
||||||
@@ -221,33 +160,42 @@ export function pianoroll({
|
|||||||
maxMidi = max;
|
maxMidi = max;
|
||||||
valueExtent = maxMidi - minMidi + 1;
|
valueExtent = maxMidi - minMidi + 1;
|
||||||
}
|
}
|
||||||
// foldValues = values.sort((a, b) => a - b);
|
foldValues = values.sort((a, b) =>
|
||||||
foldValues = values.sort((a, b) => String(a).localeCompare(String(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;
|
barThickness = fold ? valueAxis / foldValues.length : valueAxis / valueExtent;
|
||||||
|
|
||||||
ctx.fillStyle = background;
|
ctx.fillStyle = background;
|
||||||
ctx.globalAlpha = 1; // reset!
|
ctx.globalAlpha = 1; // reset!
|
||||||
if (!smear) {
|
if (!smear) {
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
ctx.fillRect(0, 0, w, h);
|
ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
/* const inFrame = (event) =>
|
haps.forEach((event) => {
|
||||||
(!hideNegative || event.whole.begin >= 0) && event.whole.begin <= time + to && event.whole.end >= time + from; */
|
const isActive = event.whole.begin <= time && event.endClipped > time;
|
||||||
haps
|
let strokeCurrent = stroke ?? (strokeActive && isActive);
|
||||||
// .filter(inFrame)
|
let fillCurrent = (!isActive && fill) || (isActive && fillActive);
|
||||||
.forEach((event) => {
|
if (hideInactive && !isActive) {
|
||||||
const isActive = event.whole.begin <= time && event.whole.end > time;
|
return;
|
||||||
const color = event.value?.color || event.context?.color;
|
}
|
||||||
ctx.fillStyle = color || inactive;
|
let color = event.value?.color || event.context?.color;
|
||||||
ctx.strokeStyle = color || active;
|
active = color || active;
|
||||||
ctx.globalAlpha = event.context.velocity ?? 1;
|
inactive = colorizeInactive ? color || inactive : inactive;
|
||||||
const timePx = scale((event.whole.begin - (flipTime ? to : from)) / timeExtent, ...timeRange);
|
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);
|
let durationPx = scale(event.duration / timeExtent, 0, timeAxis);
|
||||||
const value = getValue(event);
|
const value = getValue(event);
|
||||||
const valuePx = scale(
|
const valueProgress = fold
|
||||||
fold ? foldValues.indexOf(value) / foldValues.length : (Number(value) - minMidi) / valueExtent,
|
? foldValues.indexOf(value) / foldValues.length
|
||||||
...valueRange,
|
: (Number(value) - minMidi) / valueExtent;
|
||||||
);
|
const valuePx = scale(valueProgress, ...valueRange);
|
||||||
let margin = 0;
|
let margin = 0;
|
||||||
const offset = scale(time / timeExtent, ...timeRange);
|
const offset = scale(time / timeExtent, ...timeRange);
|
||||||
let coords;
|
let coords;
|
||||||
@@ -266,7 +214,28 @@ export function pianoroll({
|
|||||||
barThickness - 2, // height
|
barThickness - 2, // height
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
isActive ? ctx.strokeRect(...coords) : ctx.fillRect(...coords);
|
/* 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!
|
ctx.globalAlpha = 1; // reset!
|
||||||
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
const playheadPosition = scale(-from / timeExtent, ...timeRange);
|
||||||
@@ -284,7 +253,7 @@ export function pianoroll({
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOptions(drawTime, options = {}) {
|
export function getDrawOptions(drawTime, options = {}) {
|
||||||
let [lookbehind, lookahead] = drawTime;
|
let [lookbehind, lookahead] = drawTime;
|
||||||
lookbehind = Math.abs(lookbehind);
|
lookbehind = Math.abs(lookbehind);
|
||||||
const cycles = lookahead + lookbehind;
|
const cycles = lookahead + lookbehind;
|
||||||
@@ -292,6 +261,32 @@ function getOptions(drawTime, options = {}) {
|
|||||||
return { fold: 1, ...options, cycles, playhead };
|
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) {
|
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 });
|
||||||
|
}
|
||||||
|
|||||||
+145
-32
@@ -2,6 +2,8 @@ import { Cyclist } from './cyclist.mjs';
|
|||||||
import { evaluate as _evaluate } from './evaluate.mjs';
|
import { evaluate as _evaluate } from './evaluate.mjs';
|
||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { setTime } from './time.mjs';
|
import { setTime } from './time.mjs';
|
||||||
|
import { evalScope } from './evaluate.mjs';
|
||||||
|
import { register, Pattern, isPattern, silence, stack } from './pattern.mjs';
|
||||||
|
|
||||||
export function repl({
|
export function repl({
|
||||||
interval,
|
interval,
|
||||||
@@ -14,49 +16,160 @@ export function repl({
|
|||||||
transpiler,
|
transpiler,
|
||||||
onToggle,
|
onToggle,
|
||||||
editPattern,
|
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({
|
const scheduler = new Cyclist({
|
||||||
interval,
|
interval,
|
||||||
onTrigger: async (hap, deadline, duration) => {
|
onTrigger: getTrigger({ defaultOutput, getTime }),
|
||||||
|
onError: onSchedulerError,
|
||||||
|
getTime,
|
||||||
|
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 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 {
|
||||||
|
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`);
|
||||||
|
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) {
|
||||||
|
logger(`[eval] error: ${err.message}`, 'error');
|
||||||
|
updateState({ evalError: err, pending: false });
|
||||||
|
onEvalError?.(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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 {
|
try {
|
||||||
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
if (!hap.context.onTrigger || !hap.context.dominantTrigger) {
|
||||||
await defaultOutput(hap, deadline, duration);
|
await defaultOutput(hap, deadline, duration, cps);
|
||||||
}
|
}
|
||||||
if (hap.context.onTrigger) {
|
if (hap.context.onTrigger) {
|
||||||
const cps = 1;
|
|
||||||
// call signature of output / onTrigger is different...
|
// call signature of output / onTrigger is different...
|
||||||
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
await hap.context.onTrigger(getTime() + deadline, hap, getTime(), cps);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger(`[cyclist] error: ${err.message}`, 'error');
|
logger(`[cyclist] error: ${err.message}`, 'error');
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onError: onSchedulerError,
|
|
||||||
getTime,
|
|
||||||
onToggle,
|
|
||||||
});
|
|
||||||
setTime(() => scheduler.now()); // TODO: refactor?
|
|
||||||
const evaluate = async (code, autostart = true) => {
|
|
||||||
if (!code) {
|
|
||||||
throw new Error('no code to evaluate');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
beforeEval?.({ code });
|
|
||||||
let { pattern } = await _evaluate(code, transpiler);
|
|
||||||
|
|
||||||
logger(`[eval] code updated`);
|
|
||||||
pattern = editPattern?.(pattern) || pattern;
|
|
||||||
scheduler.setPattern(pattern, autostart);
|
|
||||||
afterEval?.({ code, pattern });
|
|
||||||
return pattern;
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn(`[repl] eval error: ${err.message}`);
|
|
||||||
logger(`[eval] error: ${err.message}`, 'error');
|
|
||||||
onEvalError?.(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
const stop = () => scheduler.stop();
|
|
||||||
const start = () => scheduler.start();
|
|
||||||
const pause = () => scheduler.pause();
|
|
||||||
return { scheduler, evaluate, start, stop, pause };
|
|
||||||
}
|
|
||||||
|
|||||||
+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 { Hap } from './hap.mjs';
|
||||||
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
import { Pattern, fastcat, reify, silence, stack, register } from './pattern.mjs';
|
||||||
import Fraction from './fraction.mjs';
|
import Fraction from './fraction.mjs';
|
||||||
import { id } from './util.mjs';
|
import { id, _mod, clamp, objectMap } from './util.mjs';
|
||||||
|
|
||||||
export function steady(value) {
|
export function steady(value) {
|
||||||
// A continuous value
|
// A continuous value
|
||||||
@@ -27,9 +27,11 @@ export const isaw2 = isaw.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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
|
* @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);
|
export const saw = signal((t) => t % 1);
|
||||||
@@ -42,7 +44,8 @@ export const sine2 = signal((t) => Math.sin(Math.PI * 2 * t));
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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();
|
export const sine = sine2.fromBipolar();
|
||||||
@@ -52,7 +55,8 @@ export const sine = sine2.fromBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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));
|
export const cosine = sine._early(Fraction(1).div(4));
|
||||||
@@ -63,7 +67,7 @@ export const cosine2 = sine2._early(Fraction(1).div(4));
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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));
|
export const square = signal((t) => Math.floor((t * 2) % 2));
|
||||||
@@ -74,7 +78,7 @@ export const square2 = square.toBipolar();
|
|||||||
*
|
*
|
||||||
* @return {Pattern}
|
* @return {Pattern}
|
||||||
* @example
|
* @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);
|
export const tri = fastcat(isaw, saw);
|
||||||
@@ -101,6 +105,7 @@ const timeToRand = (x) => Math.abs(intSeedToRand(timeToIntSeed(x)));
|
|||||||
|
|
||||||
const timeToRandsPrime = (seed, n) => {
|
const timeToRandsPrime = (seed, n) => {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
// eslint-disable-next-line
|
||||||
for (let i = 0; i < n; ++n) {
|
for (let i = 0; i < n; ++n) {
|
||||||
result.push(intSeedToRand(seed));
|
result.push(intSeedToRand(seed));
|
||||||
seed = xorwise(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
|
* A discrete pattern of numbers from 0 to n-1
|
||||||
* @example
|
* @example
|
||||||
* run(4).scale('C4 major').note()
|
* n(run(4)).scale("C4:pentatonic")
|
||||||
* // "0 1 2 3".scale('C4 major').note()
|
* // n("0 1 2 3").scale("C4:pentatonic")
|
||||||
*/
|
*/
|
||||||
export const run = (n) => saw.range(0, n).floor().segment(n);
|
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
|
* @name rand
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // 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);
|
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)
|
* @param {number} n max value (exclusive)
|
||||||
* @example
|
* @example
|
||||||
* // randomly select scale notes from 0 - 7 (= C to C)
|
* // 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();
|
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) => {
|
export const __chooseWith = (pat, xs) => {
|
||||||
xs = xs.map(reify);
|
xs = xs.map(reify);
|
||||||
if (xs.length == 0) {
|
if (xs.length == 0) {
|
||||||
return silence;
|
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
|
* 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 {Pattern} pat
|
||||||
* @param {*} xs
|
* @param {*} xs
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
|
* @example
|
||||||
|
* note("c2 g2!2 d2 f1").s(chooseWith(sine.fast(2), ["sawtooth", "triangle", "bd:6"]))
|
||||||
*/
|
*/
|
||||||
export const chooseWith = (pat, xs) => {
|
export const chooseWith = (pat, xs) => {
|
||||||
return __chooseWith(pat, xs).outerJoin();
|
return __chooseWith(pat, xs).outerJoin();
|
||||||
@@ -216,9 +364,9 @@ Pattern.prototype.choose2 = function (...xs) {
|
|||||||
* Picks one of the elements at random each cycle.
|
* Picks one of the elements at random each cycle.
|
||||||
* @returns {Pattern}
|
* @returns {Pattern}
|
||||||
* @example
|
* @example
|
||||||
* chooseCycles("bd", "hh", "sd").s().fast(4)
|
* chooseCycles("bd", "hh", "sd").s().fast(8)
|
||||||
* @example
|
* @example
|
||||||
* "bd | hh | sd".s().fast(4)
|
* s("bd | hh | sd").fast(8)
|
||||||
*/
|
*/
|
||||||
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
export const chooseCycles = (...xs) => chooseInWith(rand.segment(1), xs);
|
||||||
|
|
||||||
@@ -261,7 +409,7 @@ export const perlinWith = (pat) => {
|
|||||||
* @name perlin
|
* @name perlin
|
||||||
* @example
|
* @example
|
||||||
* // randomly change the cutoff
|
* // 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)));
|
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));
|
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
|
* 0 = 100% chance of removal
|
||||||
* 1 = 0% 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).
|
* 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.
|
* Randomly applies the given function by the given probability.
|
||||||
* Similar to {@link Pattern#someCyclesBy}
|
* Similar to `someCyclesBy`
|
||||||
*
|
*
|
||||||
* @name sometimesBy
|
* @name sometimesBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -335,7 +483,7 @@ export const undegrade = register('undegrade', (pat) => pat._undegradeBy(0.5));
|
|||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
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
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const sometimes = register('sometimes', function (func, pat) {
|
||||||
return pat._sometimesBy(0.5, func);
|
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.
|
* 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
|
* @name someCyclesBy
|
||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
@@ -370,7 +518,7 @@ export const sometimes = register('sometimes', function (func, pat) {
|
|||||||
* @param {function} function - the transformation to apply
|
* @param {function} function - the transformation to apply
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const someCyclesBy = register('someCyclesBy', function (patx, func, pat) {
|
||||||
@@ -392,7 +540,7 @@ export const someCyclesBy = register('someCyclesBy', function (patx, func, pat)
|
|||||||
* @memberof Pattern
|
* @memberof Pattern
|
||||||
* @returns Pattern
|
* @returns Pattern
|
||||||
* @example
|
* @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) {
|
export const someCycles = register('someCycles', function (func, pat) {
|
||||||
return pat._someCyclesBy(0.5, func);
|
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 }));
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
controls.test.mjs - <short description TODO>
|
||||||
|
Copyright (C) 2023 Strudel contributors - see <https://github.com/tidalcycles/strudel/blob/main/packages/core/test/controls.test.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 controls from '../controls.mjs';
|
||||||
|
import { mini } from '../../mini/mini.mjs';
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('controls', () => {
|
||||||
|
it('should support controls', () => {
|
||||||
|
expect(controls.s('bd').firstCycleValues).toEqual([{ s: 'bd' }]);
|
||||||
|
});
|
||||||
|
it('should support compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3')).firstCycleValues).toEqual([{ s: 'bd', n: 3 }]);
|
||||||
|
expect(controls.s(mini('bd:3 sd:4:1.4')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3 },
|
||||||
|
{ s: 'sd', n: 4, gain: 1.4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should support ignore extra elements in compound controls', () => {
|
||||||
|
expect(controls.s(mini('bd:3:0.4 sd:4:0.5:3:17')).firstCycleValues).toEqual([
|
||||||
|
{ s: 'bd', n: 3, gain: 0.4 },
|
||||||
|
{ s: 'sd', n: 4, gain: 0.5 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
slowcat,
|
slowcat,
|
||||||
cat,
|
cat,
|
||||||
sequence,
|
sequence,
|
||||||
|
palindrome,
|
||||||
polymeter,
|
polymeter,
|
||||||
polymeterSteps,
|
polymeterSteps,
|
||||||
polyrhythm,
|
polyrhythm,
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
rev,
|
rev,
|
||||||
time,
|
time,
|
||||||
run,
|
run,
|
||||||
|
pick,
|
||||||
} from '../index.mjs';
|
} from '../index.mjs';
|
||||||
|
|
||||||
import { steady } from '../signal.mjs';
|
import { steady } from '../signal.mjs';
|
||||||
@@ -182,13 +184,17 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(26, 27, 36, 37),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(21, 22, 31, 32),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -230,13 +236,17 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(6, 7, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(1, 2, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -272,13 +282,17 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
it('can Trig() structure', () => {
|
it('can Trig() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(silence, silence, 6, 7),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('can Trigzero() structure', () => {
|
it('can Trigzero() structure', () => {
|
||||||
sameFirst(
|
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),
|
sequence(silence, silence, 1, 2),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -571,6 +585,18 @@ describe('Pattern', () => {
|
|||||||
expect(sequence(1, 2, 3).firstCycle()).toStrictEqual(fastcat(1, 2, 3).firstCycle());
|
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()', () => {
|
describe('polyrhythm()', () => {
|
||||||
it('Can layer up cycles', () => {
|
it('Can layer up cycles', () => {
|
||||||
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
expect(polyrhythm(['a', 'b'], ['c']).firstCycle()).toStrictEqual(
|
||||||
@@ -638,7 +664,11 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('struct()', () => {
|
describe('struct()', () => {
|
||||||
it('Can restructure a discrete pattern', () => {
|
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(0, third), ts(0, third), 'a'),
|
||||||
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
hap(ts(third, twothirds), ts(third, 0.5), 'a'),
|
||||||
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
hap(ts(third, twothirds), ts(0.5, twothirds), 'b'),
|
||||||
@@ -669,7 +699,11 @@ describe('Pattern', () => {
|
|||||||
});
|
});
|
||||||
describe('mask()', () => {
|
describe('mask()', () => {
|
||||||
it('Can fragment a pattern', () => {
|
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(0, third), 'a'),
|
||||||
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
hap(ts(0, 0.5), ts(third, 0.5), 'a'),
|
||||||
hap(ts(0.5, 1), ts(0.5, twothirds), 'b'),
|
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);
|
expect(stack(pure('a').mask(1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(1);
|
||||||
});
|
});
|
||||||
it('Doesnt merge two overlapping haps', () => {
|
it('Doesnt merge two overlapping haps', () => {
|
||||||
expect(stack(pure('a').mask(1, 1, 0), pure('a').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(
|
expect(
|
||||||
2,
|
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', () => {
|
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);
|
expect(stack(pure('a').mask(1, 0), pure('b').mask(0, 1)).defragmentHaps().firstCycle().length).toStrictEqual(2);
|
||||||
@@ -959,4 +995,126 @@ describe('Pattern', () => {
|
|||||||
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
sameFirst(s('a', 'b').hurry(2), s('a', 'b').fast(2).speed(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
/*describe('composable functions', () => {
|
||||||
|
it('Can compose functions', () => {
|
||||||
|
sameFirst(sequence(3, 4).fast(2).rev().fast(2), fast(2).rev().fast(2)(sequence(3, 4)));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
});
|
||||||
|
it('Can compose by method chaining operators and alignments with controls', () => {
|
||||||
|
sameFirst(s('bd').apply(set.in.n(3).fast(2)), s('bd').set.n(3).fast(2));
|
||||||
|
// sameFirst(s('bd').apply(set.squeeze.n(3).fast(2)), s('bd').set.squeeze.n(3).fast(2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('weave', () => {
|
||||||
|
it('Can distribute patterns along a pattern', () => {
|
||||||
|
sameFirst(n(0, 1).weave(2, s('bd', silence), s(silence, 'sd')), sequence(s('bd').n(0), s('sd').n(1)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
describe('slice', () => {
|
||||||
|
it('Can slice a sample', () => {
|
||||||
|
sameFirst(
|
||||||
|
s('break').slice(4, sequence(0, 1, 2, 3)),
|
||||||
|
sequence(
|
||||||
|
{ begin: 0, end: 0.25, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4 },
|
||||||
|
{ begin: 0.75, end: 1, s: 'break', _slices: 4 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('splice', () => {
|
||||||
|
it('Can splice a sample', () => {
|
||||||
|
sameFirst(
|
||||||
|
s('break').splice(4, sequence(0, 1, 2, 3)),
|
||||||
|
sequence(
|
||||||
|
{ begin: 0, end: 0.25, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.25, end: 0.5, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.5, end: 0.75, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
{ begin: 0.75, end: 1, s: 'break', _slices: 4, unit: 'c', speed: 1 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -8,8 +8,8 @@ import { pure } from '../pattern.mjs';
|
|||||||
import {
|
import {
|
||||||
isNote,
|
isNote,
|
||||||
tokenizeNote,
|
tokenizeNote,
|
||||||
toMidi,
|
noteToMidi,
|
||||||
fromMidi,
|
midiToFreq,
|
||||||
freqToMidi,
|
freqToMidi,
|
||||||
_mod,
|
_mod,
|
||||||
compose,
|
compose,
|
||||||
@@ -75,27 +75,27 @@ describe('isNote', () => {
|
|||||||
expect(tokenizeNote(123)).toStrictEqual([]);
|
expect(tokenizeNote(123)).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('toMidi', () => {
|
describe('noteToMidi', () => {
|
||||||
it('should turn notes into midi', () => {
|
it('should turn notes into midi', () => {
|
||||||
expect(toMidi('A4')).toEqual(69);
|
expect(noteToMidi('A4')).toEqual(69);
|
||||||
expect(toMidi('C4')).toEqual(60);
|
expect(noteToMidi('C4')).toEqual(60);
|
||||||
expect(toMidi('Db4')).toEqual(61);
|
expect(noteToMidi('Db4')).toEqual(61);
|
||||||
expect(toMidi('C3')).toEqual(48);
|
expect(noteToMidi('C3')).toEqual(48);
|
||||||
expect(toMidi('Cb3')).toEqual(47);
|
expect(noteToMidi('Cb3')).toEqual(47);
|
||||||
expect(toMidi('Cbb3')).toEqual(46);
|
expect(noteToMidi('Cbb3')).toEqual(46);
|
||||||
expect(toMidi('C#3')).toEqual(49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
expect(toMidi('C#3')).toEqual(49);
|
expect(noteToMidi('C#3')).toEqual(49);
|
||||||
expect(toMidi('C##3')).toEqual(50);
|
expect(noteToMidi('C##3')).toEqual(50);
|
||||||
});
|
});
|
||||||
it('should throw an error when given a non-note', () => {
|
it('should throw an error when given a non-note', () => {
|
||||||
expect(() => toMidi('Q')).toThrowError(`not a note: "Q"`);
|
expect(() => noteToMidi('Q')).toThrowError(`not a note: "Q"`);
|
||||||
expect(() => toMidi('Z')).toThrowError(`not a note: "Z"`);
|
expect(() => noteToMidi('Z')).toThrowError(`not a note: "Z"`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('fromMidi', () => {
|
describe('midiToFreq', () => {
|
||||||
it('should turn midi into frequency', () => {
|
it('should turn midi into frequency', () => {
|
||||||
expect(fromMidi(69)).toEqual(440);
|
expect(midiToFreq(69)).toEqual(440);
|
||||||
expect(fromMidi(57)).toEqual(220);
|
expect(midiToFreq(57)).toEqual(220);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('freqToMidi', () => {
|
describe('freqToMidi', () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
ui.mjs - <short description TODO>
|
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/>.
|
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');
|
const container = document.getElementById('code');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style = '';
|
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
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+133
-16
@@ -4,31 +4,36 @@ 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/>.
|
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
|
// returns true if the given string is a note
|
||||||
export const isNoteWithOctave = (name) => /^[a-gA-G][#bs]*[0-9]$/.test(name);
|
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) => {
|
export const tokenizeNote = (note) => {
|
||||||
if (typeof note !== 'string') {
|
if (typeof note !== 'string') {
|
||||||
return [];
|
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) {
|
if (!pc) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return [pc, acc, oct ? Number(oct) : undefined];
|
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
|
// turns the given note into its midi number representation
|
||||||
export const toMidi = (note) => {
|
export const noteToMidi = (note, defaultOctave = 3) => {
|
||||||
const [pc, acc, oct = 3] = tokenizeNote(note);
|
const [pc, acc, oct = defaultOctave] = tokenizeNote(note);
|
||||||
if (!pc) {
|
if (!pc) {
|
||||||
throw new Error('not a note: "' + note + '"');
|
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 chroma = chromas[pc.toLowerCase()];
|
||||||
const offset = acc?.split('').reduce((o, char) => o + { '#': 1, b: -1, s: 1 }[char], 0) || 0;
|
const offset = acc?.split('').reduce((o, char) => o + accs[char], 0) || 0;
|
||||||
return (Number(oct) + 1) * 12 + chroma + offset;
|
return (Number(oct) + 1) * 12 + chroma + offset;
|
||||||
};
|
};
|
||||||
export const fromMidi = (n) => {
|
export const midiToFreq = (n) => {
|
||||||
return Math.pow(2, (n - 69) / 12) * 440;
|
return Math.pow(2, (n - 69) / 12) * 440;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
return freqToMidi(freq);
|
return freqToMidi(freq);
|
||||||
}
|
}
|
||||||
if (typeof note === 'string') {
|
if (typeof note === 'string') {
|
||||||
return toMidi(note);
|
return noteToMidi(note);
|
||||||
}
|
}
|
||||||
if (typeof note === 'number') {
|
if (typeof note === 'number') {
|
||||||
return note;
|
return note;
|
||||||
@@ -62,24 +67,37 @@ export const valueToMidi = (value, fallbackValue) => {
|
|||||||
*/
|
*/
|
||||||
export const getFreq = (noteOrMidi) => {
|
export const getFreq = (noteOrMidi) => {
|
||||||
if (typeof noteOrMidi === 'number') {
|
if (typeof noteOrMidi === 'number') {
|
||||||
return fromMidi(noteOrMidi);
|
return midiToFreq(noteOrMidi);
|
||||||
}
|
}
|
||||||
return fromMidi(toMidi(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
|
* @noAutocomplete
|
||||||
*/
|
*/
|
||||||
export const midi2note = (n) => {
|
export const midi2note = (n) => {
|
||||||
const oct = Math.floor(n / 12) - 1;
|
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;
|
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)
|
// 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 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) => {
|
export const getPlayableNoteValue = (hap) => {
|
||||||
let { value, context } = hap;
|
let { value, context } = hap;
|
||||||
let note = value;
|
let note = value;
|
||||||
@@ -91,7 +109,7 @@ export const getPlayableNoteValue = (hap) => {
|
|||||||
}
|
}
|
||||||
// if value is number => interpret as midi number as long as its not marked as frequency
|
// if value is number => interpret as midi number as long as its not marked as frequency
|
||||||
if (typeof note === 'number' && context.type !== 'frequency') {
|
if (typeof note === 'number' && context.type !== 'frequency') {
|
||||||
note = fromMidi(hap.value);
|
note = midiToFreq(hap.value);
|
||||||
} else if (typeof note === 'number' && context.type === 'frequency') {
|
} else if (typeof note === 'number' && context.type === 'frequency') {
|
||||||
note = hap.value; // legacy workaround.. will be removed in the future
|
note = hap.value; // legacy workaround.. will be removed in the future
|
||||||
} else if (typeof note !== 'string' || !isNote(note)) {
|
} else if (typeof note !== 'string' || !isNote(note)) {
|
||||||
@@ -110,9 +128,9 @@ export const getFrequency = (hap) => {
|
|||||||
return getFreq(value.note || value.n || value.value);
|
return getFreq(value.note || value.n || value.value);
|
||||||
}
|
}
|
||||||
if (typeof value === 'number' && context.type !== 'frequency') {
|
if (typeof value === 'number' && context.type !== 'frequency') {
|
||||||
value = fromMidi(hap.value);
|
value = midiToFreq(hap.value);
|
||||||
} else if (typeof value === 'string' && isNote(value)) {
|
} else if (typeof value === 'string' && isNote(value)) {
|
||||||
value = fromMidi(toMidi(hap.value));
|
value = midiToFreq(noteToMidi(hap.value));
|
||||||
} else if (typeof value !== 'number') {
|
} else if (typeof value !== 'number') {
|
||||||
throw new Error('not a note or frequency: ' + value);
|
throw new Error('not a note or frequency: ' + value);
|
||||||
}
|
}
|
||||||
@@ -170,7 +188,7 @@ export function parseNumeral(numOrString) {
|
|||||||
return asNumber;
|
return asNumber;
|
||||||
}
|
}
|
||||||
if (isNote(numOrString)) {
|
if (isNote(numOrString)) {
|
||||||
return toMidi(numOrString);
|
return noteToMidi(numOrString);
|
||||||
}
|
}
|
||||||
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
throw new Error(`cannot parse as numeral: "${numOrString}"`);
|
||||||
}
|
}
|
||||||
@@ -206,3 +224,102 @@ export function parseFractional(numOrString) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
export const fractionalArgs = (fn) => mapArgs(fn, parseFractional);
|
||||||
|
|
||||||
|
export const splitAt = function (index, value) {
|
||||||
|
return [value.slice(0, index), value.slice(index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
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: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'index.mjs'),
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es'],
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
fileName: (ext) => ({ es: 'index.mjs' })[ext],
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [...Object.keys(dependencies)],
|
external: [...Object.keys(dependencies)],
|
||||||
|
|||||||
@@ -44,6 +44,6 @@ function createClock(
|
|||||||
};
|
};
|
||||||
const getPhase = () => phase;
|
const getPhase = () => phase;
|
||||||
// setCallback
|
// setCallback
|
||||||
return { setDuration, start, stop, pause, duration, getPhase, minLatency };
|
return { setDuration, start, stop, pause, duration, interval, getPhase, minLatency };
|
||||||
}
|
}
|
||||||
export default createClock;
|
export default createClock;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# @strudel/csound
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user