feat: add cursor overlay pipeline
@@ -0,0 +1,46 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1_311)">
|
||||
<path d="M12.0455 7.54446C12.819 7.64044 13.5864 7.88965 14.3038 8.30386C17.1736 9.96071 18.1568 13.6303 16.5 16.5C14.8431 13.6303 11.1736 12.647 8.30384 14.3039C7.58745 14.7175 6.98862 15.2565 6.51907 15.8772C6.74376 12.2206 8.93355 9.09535 12.0455 7.54446Z" fill="url(#paint0_linear_1_311)"/>
|
||||
<path d="M6.51908 15.8772C6.98862 15.2565 7.58745 14.7175 8.30385 14.3038C11.1736 12.647 14.8431 13.6302 16.5 16.5C13.1863 16.5 10.5 19.1863 10.5 22.5C10.5 23.3277 10.6676 24.1162 10.9707 24.8336C8.27601 23.0421 6.5 19.9784 6.5 16.5C6.5 16.2908 6.50642 16.0832 6.51908 15.8772Z" fill="url(#paint1_linear_1_311)"/>
|
||||
<path d="M10.9707 24.8336C10.6676 24.1163 10.5 23.3277 10.5 22.5C10.5 19.1863 13.1863 16.5 16.5 16.5C14.8431 19.3698 15.8264 23.0393 18.6962 24.6962C19.4136 25.1104 20.181 25.3596 20.9545 25.4555C19.6131 26.124 18.1005 26.5 16.5 26.5C14.4556 26.5 12.5545 25.8865 10.9707 24.8336Z" fill="url(#paint2_linear_1_311)"/>
|
||||
<path d="M20.9545 25.4555C20.181 25.3596 19.4136 25.1104 18.6962 24.6962C15.8264 23.0393 14.8432 19.3698 16.5 16.5C18.1569 19.3698 21.8264 20.353 24.6962 18.6962C25.4126 18.2825 26.0114 17.7435 26.4809 17.1228C26.2562 20.7794 24.0665 23.9047 20.9545 25.4555Z" fill="url(#paint3_linear_1_311)"/>
|
||||
<path d="M26.4809 17.1228C26.0114 17.7435 25.4125 18.2825 24.6962 18.6961C21.8264 20.353 18.1569 19.3697 16.5 16.5C19.8137 16.5 22.5 13.8137 22.5 10.5C22.5 9.67229 22.3324 8.88374 22.0293 8.16641C24.724 9.95791 26.5 13.0215 26.5 16.5C26.5 16.7092 26.4936 16.9168 26.4809 17.1228Z" fill="url(#paint4_linear_1_311)"/>
|
||||
<path d="M22.0293 8.16642C22.3324 8.88375 22.5 9.6723 22.5 10.5C22.5 13.8137 19.8137 16.5 16.5 16.5C18.1569 13.6302 17.1736 9.9607 14.3038 8.30385C13.5864 7.88964 12.819 7.64043 12.0455 7.54445C13.3869 6.87599 14.8995 6.5 16.5 6.5C18.5444 6.5 20.4455 7.11349 22.0293 8.16642Z" fill="url(#paint5_linear_1_311)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1_311" x="4.5" y="5.5" width="24" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4049 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_311"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_311" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1_311" x1="545.808" y1="7.54446" x2="545.808" y2="903.099" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFD305" style="stop-color:#FFD305;stop-color:color(display-p3 1.0000 0.8275 0.0196);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#FDCF01" style="stop-color:#FDCF01;stop-color:color(display-p3 0.9922 0.8118 0.0039);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1_311" x1="506.5" y1="13.499" x2="506.5" y2="1146.96" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#52CF30" style="stop-color:#52CF30;stop-color:color(display-p3 0.3216 0.8118 0.1882);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#3BBD1C" style="stop-color:#3BBD1C;stop-color:color(display-p3 0.2314 0.7412 0.1098);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1_311" x1="533.223" y1="16.5" x2="533.223" y2="1016.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#14ADF6" style="stop-color:#14ADF6;stop-color:color(display-p3 0.0784 0.6784 0.9647);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#1191F4" style="stop-color:#1191F4;stop-color:color(display-p3 0.0667 0.5686 0.9569);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1_311" x1="554.984" y1="16.5" x2="554.984" y2="912.055" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#CA70E1" style="stop-color:#CA70E1;stop-color:color(display-p3 0.7922 0.4392 0.8824);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#B452CB" style="stop-color:#B452CB;stop-color:color(display-p3 0.7059 0.3216 0.7961);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_1_311" x1="516.5" y1="8.16641" x2="516.5" y2="1141.62" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF645D" style="stop-color:#FF645D;stop-color:color(display-p3 1.0000 0.3922 0.3647);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#FF4332" style="stop-color:#FF4332;stop-color:color(display-p3 1.0000 0.2627 0.1961);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_1_311" x1="534.769" y1="6.5" x2="534.769" y2="1006.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FBB114" style="stop-color:#FBB114;stop-color:color(display-p3 0.9843 0.6941 0.0784);stop-opacity:1;"/>
|
||||
<stop offset="1" stop-color="#FF9508" style="stop-color:#FF9508;stop-color:color(display-p3 1.0000 0.5843 0.0314);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5 9C17.328 9 18 9.672 18 10.5V15H22.5C23.2793 15 23.9204 15.5953 23.9931 16.3556L24 16.5C24 17.328 23.328 18 22.5 18H18V22.5C18 23.2793 17.4047 23.9204 16.6444 23.9931L16.5 24C15.672 24 15 23.328 15 22.5V18H10.5C9.72071 18 9.0796 17.4047 9.00687 16.6444L9 16.5C9 15.672 9.672 15 10.5 15H15V10.5C15 9.72071 15.5953 9.0796 16.3556 9.00687L16.5 9Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5 10C16.776 10 17 10.224 17 10.5V16H22.5C22.7453 16 22.9496 16.177 22.9919 16.4102L23 16.5C23 16.776 22.776 17 22.5 17H17V22.5C17 22.7453 16.823 22.9496 16.5898 22.9919L16.5 23C16.224 23 16 22.776 16 22.5V17H10.5C10.2547 17 10.0504 16.823 10.0081 16.5898L10 16.5C10 16.224 10.224 16 10.5 16H16V10.5C16 10.2547 16.177 10.0504 16.4102 10.0081L16.5 10Z" fill="#232020" style="fill:#232020;fill:color(display-p3 0.1373 0.1255 0.1255);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.501 13.8601L24.884 22.2611C25.937 23.3171 25.19 25.1191 23.699 25.1191L22.475 25.119L23.6908 28.0067C23.9038 28.5127 23.9068 29.0727 23.6998 29.5817C23.4918 30.0917 23.0978 30.4897 22.5898 30.7027C22.3338 30.8097 22.0658 30.8637 21.7918 30.8637C20.9608 30.8637 20.2158 30.3687 19.8938 29.6027L18.616 26.565L17.784 27.3031C16.703 28.2591 15 27.4921 15 26.0481V14.4811C15 13.6971 15.947 13.3051 16.501 13.8601Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9995 15.1292C15.9995 14.9982 16.1585 14.9322 16.2505 15.0252L24.1585 22.9502C24.5895 23.3822 24.2835 24.1192 23.6735 24.1192L20.9695 24.1177L22.7691 28.3936C22.9961 28.9336 22.7421 29.5546 22.2031 29.7806C21.6621 30.0076 21.0421 29.7546 20.8161 29.2156L18.9985 24.8917L17.1385 26.5392C16.7225 26.9072 16.0806 26.6507 16.0065 26.1274L15.9995 26.0262V15.1292Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7191 13.556C23.3671 13.299 22.9751 13.199 22.5421 13.258C22.3341 13.286 22.1621 13.347 22.0191 13.432C22.0241 13.273 22.0121 13.122 21.9761 12.98C21.9011 12.683 21.7611 12.439 21.5561 12.247C21.3511 12.055 21.1051 11.917 20.8181 11.832C20.5581 11.754 20.2931 11.731 20.0231 11.764C19.7521 11.797 19.5101 11.889 19.2951 12.042C19.2251 12.092 19.1731 12.156 19.1171 12.217C19.0781 12.148 19.0421 12.077 18.9871 12.013C18.8211 11.821 18.6101 11.673 18.3531 11.569C18.0961 11.465 17.8231 11.412 17.5371 11.412C17.2441 11.412 16.9681 11.465 16.7121 11.569C16.4551 11.673 16.2431 11.821 16.0771 12.013C16.0231 12.075 15.9891 12.145 15.9491 12.213C15.9121 12.171 15.8811 12.123 15.8381 12.086C15.6621 11.936 15.4551 11.834 15.2171 11.778C14.9801 11.723 14.7411 11.715 14.5001 11.754C14.1941 11.799 13.9191 11.914 13.6741 12.096C13.4301 12.278 13.2501 12.524 13.1321 12.833C13.0231 13.122 13.0101 13.464 13.0771 13.847C12.5071 13.777 11.9771 13.943 11.4921 14.356C10.9911 14.783 10.7431 15.455 10.7501 16.373C10.7561 17.857 11.0381 19.175 11.5941 20.328C12.1511 21.48 12.9341 22.381 13.9431 23.028C14.9521 23.676 16.1211 24 17.4491 24C18.6601 24 19.6951 23.787 20.5551 23.36C21.4141 22.934 22.1101 22.317 22.6401 21.509C23.1701 20.703 23.5561 19.703 23.7971 18.511C23.9271 17.881 24.0291 17.261 24.1051 16.656C24.1801 16.05 24.2241 15.41 24.2361 14.732C24.2431 14.205 24.0701 13.813 23.7191 13.556Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2635 14.9171L13.5095 16.1871C13.5555 16.4281 13.6755 16.5821 13.8705 16.6461C14.0665 16.7121 14.2435 16.6801 14.4035 16.5531C14.5625 16.4261 14.6165 16.2461 14.5645 16.0111L14.0565 13.5801C13.9975 13.3261 14.0715 13.1211 14.2765 12.9651C14.4815 12.8091 14.7085 12.7501 14.9595 12.7891C15.2105 12.8281 15.3715 12.9751 15.4435 13.2291L15.5895 13.7561C15.6415 13.9581 15.7635 14.0851 15.9555 14.1371C16.1485 14.1891 16.3315 14.1661 16.5075 14.0691C16.6835 13.9711 16.7715 13.8281 16.7715 13.6391V12.9651C16.7715 12.7891 16.8445 12.6481 16.9915 12.5401C17.1375 12.4331 17.3155 12.3791 17.5235 12.3791C17.7255 12.3791 17.8995 12.4331 18.0465 12.5401C18.1925 12.6481 18.2655 12.7891 18.2655 12.9651V13.6391C18.2655 13.8211 18.3535 13.9631 18.5295 14.0641C18.7055 14.1651 18.8885 14.1891 19.0815 14.1371C19.2735 14.0851 19.3955 13.9581 19.4475 13.7561L19.6035 13.2091C19.6695 12.9681 19.8255 12.8281 20.0725 12.7891C20.3195 12.7501 20.5465 12.8091 20.7515 12.9651C20.9565 13.1211 21.0325 13.3261 20.9805 13.5801L20.8245 14.4591C20.7785 14.6931 20.8245 14.8761 20.9615 15.0061C21.0975 15.1361 21.2635 15.1981 21.4595 15.1911C21.6545 15.1851 21.8015 15.1131 21.8985 14.9761L22.1525 14.5661C22.2435 14.4291 22.3795 14.3501 22.5575 14.3271C22.7365 14.3041 22.8975 14.3411 23.0415 14.4391C23.1845 14.5371 23.2565 14.6771 23.2565 14.8591C23.2565 15.4001 23.2175 15.9181 23.1385 16.4121C23.0605 16.9071 22.9465 17.4761 22.7975 18.1211L22.7385 18.3941C22.5115 19.4101 22.1845 20.2471 21.7625 20.9041C21.3395 21.5621 20.7775 22.0581 20.0775 22.3931C19.3775 22.7291 18.5125 22.8961 17.4845 22.8961C16.3445 22.8961 15.3455 22.6171 14.4865 22.0561C13.6265 21.4971 12.9655 20.7171 12.5035 19.7171C12.0415 18.7181 11.8105 17.5811 11.8105 16.3041C11.8105 15.8491 11.9095 15.4991 12.1085 15.2551C12.3065 15.0101 12.5445 14.8781 12.8215 14.8541C12.9845 14.8411 13.1285 14.8681 13.2635 14.9171Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5908 23.4186C16.6578 23.4186 15.8038 23.2506 15.0528 22.9196C14.2958 22.5856 13.6148 22.0696 13.0298 21.3876C12.4598 20.7226 11.9558 19.8716 11.5308 18.8596L9.61877 14.1546C9.47777 13.8136 9.46177 13.4866 9.57177 13.1866C9.69177 12.8636 9.92977 12.6346 10.2618 12.5236C10.3818 12.4806 10.5158 12.4556 10.6488 12.4556C10.8128 12.4556 10.9718 12.4926 11.1228 12.5656C11.3728 12.6876 11.5798 12.9046 11.7388 13.2116L13.3258 16.0706C13.4298 16.2506 13.5078 16.3166 13.5358 16.3366C13.5788 16.3656 13.6208 16.3776 13.6768 16.3776C13.7438 16.3776 13.7848 16.3586 13.8308 16.3066C13.8498 16.2846 13.8778 16.1896 13.8428 15.9836L12.7408 8.9466C12.6648 8.5056 12.7988 8.1986 12.9258 8.0206C13.1068 7.7656 13.3658 7.6026 13.6738 7.5486C13.7518 7.5386 13.8058 7.5346 13.8598 7.5346C14.1098 7.5346 14.3428 7.6086 14.5508 7.7526C14.8108 7.9326 14.9818 8.2066 15.0438 8.5456L16.1318 13.6776L16.1918 7.6486C16.1918 7.3076 16.3048 7.0056 16.5208 6.7776C16.7428 6.5416 17.0408 6.4166 17.3798 6.4166C17.7218 6.4166 18.0238 6.5376 18.2528 6.7676C18.4828 6.9966 18.6038 7.3026 18.6038 7.6516L18.5688 13.7016L19.5158 8.4556C19.5638 8.1296 19.7268 7.8486 19.9838 7.6576C20.1908 7.5036 20.4218 7.4256 20.6708 7.4256C20.7368 7.4256 20.8038 7.43159 20.8718 7.4426C21.1958 7.49259 21.4728 7.6556 21.6628 7.9116C21.7988 8.0936 21.9448 8.4066 21.8648 8.8606L20.9988 14.1766L22.2198 10.7096C22.3198 10.3946 22.4928 10.1486 22.7298 9.9916C22.9248 9.8616 23.1408 9.7956 23.3718 9.7956C23.4278 9.7956 23.4838 9.7996 23.5408 9.8076C23.8708 9.8646 24.1278 10.0286 24.3018 10.2856C24.4698 10.5356 24.5298 10.8336 24.4808 11.1696L23.3418 17.7446C23.0218 19.5926 22.4068 21.0076 21.5138 21.9506C20.5928 22.9246 19.2728 23.4186 17.5908 23.4186Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.8523 17.66L23.9893 11.097C24.0203 10.886 23.9873 10.708 23.8893 10.564C23.7923 10.419 23.6533 10.331 23.4733 10.3C23.3013 10.276 23.1453 10.312 23.0053 10.405C22.8643 10.499 22.7583 10.655 22.6883 10.874L21.4933 14.273C21.4383 14.422 21.3623 14.527 21.2643 14.589C21.1673 14.652 21.0513 14.672 20.9193 14.648C20.7943 14.625 20.6903 14.56 20.6083 14.455C20.5263 14.349 20.4933 14.23 20.5083 14.097L21.3763 8.776C21.4153 8.55 21.3783 8.36 21.2643 8.208C21.1513 8.056 20.9933 7.964 20.7903 7.933C20.6023 7.901 20.4323 7.942 20.2803 8.056C20.1283 8.169 20.0363 8.331 20.0053 8.542L19.0673 13.851C19.0443 13.984 18.9833 14.087 18.8853 14.161C18.7883 14.236 18.6693 14.261 18.5283 14.238C18.3723 14.214 18.2573 14.155 18.1823 14.062C18.1083 13.968 18.0713 13.847 18.0713 13.698L18.1063 7.651C18.1063 7.433 18.0383 7.255 17.9013 7.118C17.7643 6.982 17.5903 6.913 17.3803 6.913C17.1763 6.913 17.0103 6.982 16.8823 7.118C16.7533 7.255 16.6883 7.433 16.6883 7.651L16.6533 13.745C16.6533 13.886 16.6043 14.003 16.5073 14.097C16.4093 14.191 16.2943 14.238 16.1613 14.238C16.0283 14.238 15.9173 14.196 15.8273 14.114C15.7373 14.032 15.6763 13.921 15.6453 13.78L14.5553 8.636C14.5163 8.425 14.4213 8.267 14.2683 8.161C14.1163 8.056 13.9463 8.015 13.7583 8.038C13.5793 8.069 13.4363 8.159 13.3313 8.308C13.2253 8.456 13.1923 8.644 13.2313 8.87L14.3333 15.902C14.3873 16.23 14.3443 16.474 14.2043 16.634C14.0633 16.795 13.8873 16.875 13.6763 16.875C13.5203 16.875 13.3803 16.832 13.2553 16.746C13.1303 16.66 13.0083 16.515 12.8913 16.312L11.2983 13.441C11.1883 13.23 11.0573 13.087 10.9053 13.013C10.7533 12.939 10.5903 12.933 10.4193 12.995C10.2313 13.058 10.1043 13.179 10.0383 13.359C9.97126 13.538 9.98526 13.741 10.0793 13.968L11.9893 18.668C12.3953 19.636 12.8683 20.435 13.4073 21.064C13.9463 21.693 14.5613 22.16 15.2533 22.464C15.9443 22.769 16.7233 22.921 17.5903 22.921C19.1373 22.921 20.3253 22.484 21.1533 21.609C21.9813 20.734 22.5483 19.418 22.8523 17.66ZM11.0283 19.007L9.16526 14.414C9.03226 14.085 8.97926 13.773 9.00726 13.476C9.03426 13.179 9.12826 12.921 9.28826 12.702C9.44826 12.484 9.64926 12.312 9.89126 12.187C10.1333 12.069 10.3973 12.015 10.6823 12.023C10.9673 12.03 11.2433 12.112 11.5083 12.269C11.7743 12.425 11.9973 12.663 12.1763 12.984L13.1023 14.754C13.1263 14.8 13.1573 14.82 13.1963 14.812C13.2353 14.804 13.2513 14.773 13.2433 14.718L12.2473 9.046C12.1763 8.679 12.1923 8.351 12.2943 8.062C12.3953 7.773 12.5593 7.538 12.7863 7.359C13.0123 7.179 13.2743 7.066 13.5713 7.019C13.8683 6.972 14.1533 7.001 14.4263 7.107C14.7003 7.212 14.9343 7.386 15.1303 7.628C15.3253 7.87 15.4623 8.171 15.5403 8.53L15.6803 9.292V7.651C15.6803 7.136 15.8413 6.714 16.1613 6.386C16.4813 6.058 16.8873 5.894 17.3803 5.894C17.8953 5.894 18.3113 6.062 18.6283 6.398C18.9443 6.734 19.1023 7.167 19.1023 7.698L19.0793 8.202C19.1883 7.734 19.4173 7.384 19.7643 7.153C20.1123 6.923 20.5013 6.843 20.9303 6.913C21.2273 6.96 21.4913 7.073 21.7213 7.253C21.9523 7.433 22.1243 7.665 22.2373 7.95C22.3503 8.235 22.3833 8.562 22.3373 8.929L22.2373 9.85652C22.2179 9.76817 22.3253 9.65233 22.5593 9.509C22.9113 9.294 23.2983 9.222 23.7193 9.292C23.9933 9.347 24.2333 9.458 24.4403 9.626C24.6473 9.794 24.7993 10.019 24.8973 10.3C24.9953 10.581 25.0123 10.901 24.9503 11.261L23.8953 17.765C23.5673 19.789 22.8833 21.334 21.8443 22.4C20.8053 23.466 19.3763 24 17.5553 24C16.0163 24 14.7163 23.58 13.6533 22.74C12.5903 21.9 11.7153 20.656 11.0283 19.007Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.301 31.942C20.359 31.942 21.279 31.722 22.059 31.281C22.841 30.84 23.445 30.195 23.87 29.343C24.296 28.491 24.509 27.449 24.509 26.214V24.564C24.509 23.797 24.389 23.208 24.149 22.794C23.909 22.381 23.563 22.174 23.114 22.174V23.268C23.114 23.42 23.063 23.542 22.963 23.633C22.862 23.724 22.749 23.769 22.621 23.769C22.487 23.769 22.37 23.724 22.27 23.633C22.169 23.542 22.119 23.42 22.119 23.268V21.909C22.119 21.636 22.049 21.424 21.91 21.275C21.769 21.126 21.572 21.052 21.316 21.052C21.116 21.052 20.918 21.095 20.724 21.18V22.913C20.724 23.071 20.673 23.195 20.573 23.287C20.472 23.378 20.359 23.423 20.231 23.423C20.097 23.423 19.98 23.378 19.88 23.287C19.779 23.195 19.729 23.071 19.729 22.913V20.906C19.729 20.639 19.658 20.427 19.515 20.272C19.372 20.117 19.179 20.039 18.936 20.039C18.729 20.039 18.528 20.085 18.334 20.176V22.566C18.334 22.706 18.287 22.825 18.192 22.922C18.098 23.019 17.978 23.068 17.832 23.068C17.692 23.068 17.575 23.019 17.481 22.922C17.387 22.825 17.339 22.706 17.339 22.566V15.981C17.339 15.756 17.277 15.575 17.152 15.438C17.028 15.301 16.859 15.233 16.646 15.233C16.433 15.233 16.262 15.301 16.131 15.438C16 15.575 15.935 15.756 15.935 15.981V25.257C15.935 25.452 15.877 25.611 15.762 25.736C15.646 25.86 15.5 25.922 15.324 25.922C15.165 25.922 15.024 25.883 14.9 25.804C14.775 25.725 14.67 25.579 14.585 25.366L13.363 22.575C13.211 22.21 12.982 22.028 12.679 22.028C12.484 22.028 12.326 22.09 12.204 22.215C12.082 22.339 12.022 22.49 12.022 22.666C12.022 22.812 12.049 22.958 12.104 23.104L13.737 27.701C14.271 29.191 15.014 30.269 15.962 30.939C16.911 31.608 18.024 31.942 19.301 31.942ZM19.265 33C17.707 33 16.383 32.583 15.292 31.751C14.2 30.918 13.366 29.68 12.788 28.038L11.155 23.432C11.106 23.287 11.069 23.135 11.041 22.976C11.014 22.818 11 22.675 11 22.548C11 22.073 11.158 21.706 11.475 21.444C11.79 21.183 12.162 21.052 12.587 21.052C12.898 21.052 13.177 21.142 13.427 21.321C13.676 21.5 13.876 21.77 14.029 22.128L14.749 23.907C14.767 23.949 14.795 23.97 14.831 23.97C14.88 23.97 14.904 23.943 14.904 23.889V16.044C14.904 15.491 15.068 15.052 15.397 14.726C15.725 14.401 16.141 14.238 16.646 14.238C17.145 14.238 17.557 14.401 17.882 14.726C18.207 15.052 18.37 15.491 18.37 16.044V19.155C18.607 19.088 18.838 19.054 19.064 19.054C19.453 19.054 19.784 19.156 20.058 19.36C20.331 19.564 20.523 19.842 20.633 20.195C20.912 20.098 21.186 20.049 21.454 20.049C21.83 20.049 22.148 20.144 22.406 20.336C22.665 20.527 22.843 20.787 22.94 21.116C23.766 21.122 24.407 21.414 24.86 21.991C25.313 22.569 25.54 23.381 25.54 24.426V26.333C25.54 27.743 25.275 28.946 24.746 29.94C24.217 30.935 23.481 31.692 22.539 32.216C21.596 32.739 20.505 33 19.265 33Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.301 31.942C20.359 31.942 21.279 31.722 22.059 31.281C22.841 30.84 23.445 30.195 23.87 29.343C24.296 28.491 24.509 27.449 24.509 26.214V24.564C24.509 23.797 24.389 23.208 24.149 22.794C23.909 22.381 23.563 22.174 23.114 22.174V23.268C23.114 23.42 23.063 23.542 22.963 23.633C22.862 23.724 22.749 23.769 22.621 23.769C22.487 23.769 22.37 23.724 22.27 23.633C22.169 23.542 22.119 23.42 22.119 23.268V21.909C22.119 21.636 22.049 21.424 21.91 21.275C21.769 21.126 21.572 21.052 21.316 21.052C21.116 21.052 20.918 21.095 20.724 21.18V22.913C20.724 23.071 20.673 23.195 20.573 23.287C20.472 23.378 20.359 23.423 20.231 23.423C20.097 23.423 19.98 23.378 19.88 23.287C19.779 23.195 19.729 23.071 19.729 22.913V20.906C19.729 20.639 19.658 20.427 19.515 20.272C19.372 20.117 19.179 20.039 18.936 20.039C18.729 20.039 18.528 20.085 18.334 20.176V22.566C18.334 22.706 18.287 22.825 18.192 22.922C18.098 23.019 17.978 23.068 17.832 23.068C17.692 23.068 17.575 23.019 17.481 22.922C17.387 22.825 17.339 22.706 17.339 22.566V15.981C17.339 15.756 17.277 15.575 17.152 15.438C17.028 15.301 16.859 15.233 16.646 15.233C16.433 15.233 16.262 15.301 16.131 15.438C16 15.575 15.935 15.756 15.935 15.981V25.257C15.935 25.452 15.877 25.611 15.762 25.736C15.646 25.86 15.5 25.922 15.324 25.922C15.165 25.922 15.024 25.883 14.9 25.804C14.775 25.725 14.67 25.579 14.585 25.366L13.363 22.575C13.211 22.21 12.982 22.028 12.679 22.028C12.484 22.028 12.326 22.09 12.204 22.215C12.082 22.339 12.022 22.49 12.022 22.666C12.022 22.812 12.049 22.958 12.104 23.104L13.737 27.701C14.271 29.191 15.014 30.269 15.962 30.939C16.911 31.608 18.024 31.942 19.301 31.942Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="32" height="43" viewBox="0 0 32 43" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_302)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.501 13.8601L24.884 22.2611C25.937 23.3171 25.19 25.1191 23.699 25.1191L20.252 25.1181L17.784 27.3031C16.703 28.2591 15 27.4921 15 26.0481V14.4811C15 13.6971 15.947 13.3051 16.501 13.8601Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9995 15.1292V26.0262C15.9995 26.6162 16.6965 26.9302 17.1385 26.5392L19.8735 24.1182L23.6735 24.1192C24.2835 24.1192 24.5895 23.3822 24.1585 22.9502L16.2505 15.0252C16.1585 14.9322 15.9995 14.9982 15.9995 15.1292Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8888 42.1186H22.1098C20.9448 42.1186 19.9998 41.1736 19.9998 40.0076V29.2296C19.9998 28.0636 20.9448 27.1186 22.1098 27.1186H29.8888C31.0548 27.1186 31.9998 28.0636 31.9998 29.2296V40.0076C31.9998 41.1736 31.0548 42.1186 29.8888 42.1186Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.7495 41.1186H22.2495C21.5595 41.1186 20.9995 40.5586 20.9995 39.8686V29.3686C20.9995 28.6786 21.5595 28.1186 22.2495 28.1186H29.7495C30.4395 28.1186 30.9995 28.6786 30.9995 29.3686V39.8686C30.9995 40.5586 30.4395 41.1186 29.7495 41.1186Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.4995 31.1186H23.4995C23.2235 31.1186 22.9995 30.8946 22.9995 30.6186C22.9995 30.3426 23.2235 30.1186 23.4995 30.1186H28.4995C28.7755 30.1186 28.9995 30.3426 28.9995 30.6186C28.9995 30.8946 28.7755 31.1186 28.4995 31.1186Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.4995 39.1186H23.4995C23.2235 39.1186 22.9995 38.8946 22.9995 38.6186C22.9995 38.3426 23.2235 38.1186 23.4995 38.1186H28.4995C28.7755 38.1186 28.9995 38.3426 28.9995 38.6186C28.9995 38.8946 28.7755 39.1186 28.4995 39.1186Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.0608 36.1186H22.9378C22.4198 36.1186 21.9998 35.6986 21.9998 35.1796V34.0576C21.9998 33.5386 22.4198 33.1186 22.9378 33.1186H29.0608C29.5788 33.1186 29.9998 33.5386 29.9998 34.0576V35.1796C29.9998 35.6986 29.5788 36.1186 29.0608 36.1186Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.4995 35.1186H23.4995C23.2235 35.1186 22.9995 34.8946 22.9995 34.6186C22.9995 34.3426 23.2235 34.1186 23.4995 34.1186H26.4995C26.7755 34.1186 26.9995 34.3426 26.9995 34.6186C26.9995 34.8946 26.7755 35.1186 26.4995 35.1186Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_302">
|
||||
<rect width="16.9998" height="28.5186" fill="white" style="fill:white;fill-opacity:1;" transform="translate(15 13.6)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0764 8.5093C16.5864 7.9993 17.4134 7.9993 17.9234 8.5093L25.4904 16.0763C26.0004 16.5863 26.0004 17.4133 25.4904 17.9233L17.9234 25.4903C17.4134 26.0003 16.5864 26.0003 16.0764 25.4903L8.50939 17.9233C7.99939 17.4133 7.99939 16.5863 8.50939 16.0763L16.0764 8.5093Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0004 19.8444C13.0004 20.2704 12.4844 20.4844 12.1824 20.1824L9.4064 17.4064C9.1824 17.1824 9.1824 16.8184 9.4064 16.5934L12.1824 13.8174C12.4844 13.5154 13.0004 13.7294 13.0004 14.1554L13.001 16H16V13L14.1556 13.0004C13.758 13.0004 13.5451 12.5509 13.764 12.2454L13.8176 12.1824L16.5936 9.4064C16.8176 9.1824 17.1816 9.1824 17.4066 9.4064L20.1826 12.1824C20.4846 12.4844 20.2706 13.0004 19.8446 13.0004L18 13V16H21V14.1556C21 13.758 21.4495 13.5451 21.7542 13.764L21.817 13.8176L24.594 16.5936C24.818 16.8176 24.818 17.1816 24.594 17.4066L21.817 20.1826C21.516 20.4846 21 20.2706 21 19.8446V18H18V20.999L19.8444 20.9996C20.242 20.9996 20.4549 21.4491 20.236 21.7546L20.1824 21.8176L17.4064 24.5936C17.1824 24.8176 16.8184 24.8176 16.5934 24.5936L13.8174 21.8176C13.5154 21.5156 13.7294 20.9996 14.1554 20.9996L16 20.999V18H13.001L13.0004 19.8444Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23 16C23 17.103 22.103 18 21 18H11C9.897 18 9 17.103 9 16C9 14.897 9.897 14 11 14H21C22.103 14 23 14.897 23 16ZM19.7852 21.3564C19.7852 21.5684 19.7342 21.7824 19.6392 21.9734L17.2562 26.7354C17.0212 27.2064 16.5482 27.4994 16.0212 27.5004C15.4932 27.5004 15.0192 27.2074 14.7842 26.7364L12.4012 21.9744C12.3052 21.7834 12.2542 21.5694 12.2542 21.3564C12.2542 20.5934 12.8752 19.9734 13.6382 19.9734H18.4022C18.7812 19.9734 19.1352 20.1234 19.3972 20.3964C19.6472 20.6554 19.7852 20.9974 19.7852 21.3564Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 16C22 16.552 21.552 17 21 17H11C10.448 17 10 16.552 10 16C10 15.448 10.448 15 11 15H21C21.552 15 22 15.448 22 16ZM16.362 26.2884L18.744 21.5274C18.871 21.2734 18.686 20.9734 18.402 20.9734H13.638C13.353 20.9734 13.168 21.2734 13.295 21.5274L15.678 26.2884C15.819 26.5704 16.221 26.5704 16.362 26.2884Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0004 23C14.8974 23 14.0004 22.103 14.0004 21V11C14.0004 9.897 14.8974 9 16.0004 9C17.1034 9 18.0004 9.897 18.0004 11V21C18.0004 22.103 17.1034 23 16.0004 23ZM10.644 19.7852C10.432 19.7852 10.218 19.7342 10.027 19.6392L5.265 17.2562C4.794 17.0212 4.501 16.5482 4.5 16.0212C4.5 15.4932 4.793 15.0192 5.264 14.7842L10.026 12.4012C10.217 12.3052 10.431 12.2542 10.644 12.2542C11.407 12.2542 12.027 12.8752 12.027 13.6382V18.4022C12.027 18.7812 11.877 19.1352 11.604 19.3972C11.345 19.6472 11.003 19.7852 10.644 19.7852Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0004 22C15.4484 22 15.0004 21.552 15.0004 21V11C15.0004 10.448 15.4484 10 16.0004 10C16.5524 10 17.0004 10.448 17.0004 11V21C17.0004 21.552 16.5524 22 16.0004 22ZM5.712 16.362L10.473 18.744C10.727 18.871 11.027 18.686 11.027 18.402V13.638C11.027 13.353 10.727 13.168 10.473 13.295L5.712 15.678C5.43 15.819 5.43 16.221 5.712 16.362Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 23C14.8996 23 14.0047 22.103 14.0047 21V11C14.0047 9.897 14.8996 9 16 9C17.1004 9 17.9953 9.897 17.9953 11V21C17.9953 22.103 17.1004 23 16 23ZM21.3703 19.7852C20.6091 19.7852 19.9905 19.1652 19.9905 18.4022V13.6382C19.9905 12.8752 20.6091 12.2542 21.3703 12.2542C21.5828 12.2542 21.7973 12.3052 21.9878 12.4022L26.7368 14.7842C27.2077 15.0192 27.5 15.4932 27.5 16.0212C27.499 16.5482 27.2067 17.0212 26.7358 17.2572L21.9868 19.6392C21.7963 19.7342 21.5828 19.7852 21.3703 19.7852ZM10.6297 19.7852C10.4182 19.7852 10.2047 19.7342 10.0141 19.6392L5.26322 17.2562C4.79332 17.0212 4.501 16.5482 4.5 16.0212C4.5 15.4932 4.79232 15.0192 5.26222 14.7842L10.0132 12.4012C10.2037 12.3052 10.4172 12.2542 10.6297 12.2542C11.3909 12.2542 12.0095 12.8752 12.0095 13.6382V18.4022C12.0095 18.7812 11.8598 19.1352 11.5875 19.3972C11.3291 19.6472 10.9879 19.7852 10.6297 19.7852Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 22C15.4493 22 15.0023 21.552 15.0023 21V11C15.0023 10.448 15.4493 10 16 10C16.5507 10 16.9977 10.448 16.9977 11V21C16.9977 21.552 16.5507 22 16 22ZM26.2908 15.6775L21.5409 13.2955C21.2875 13.1685 20.9882 13.3535 20.9882 13.6375V18.4015C20.9882 18.6865 21.2875 18.8715 21.5409 18.7445L26.2908 16.3615C26.5722 16.2205 26.5722 15.8185 26.2908 15.6775ZM5.70918 16.362L10.4591 18.744C10.7125 18.871 11.0118 18.686 11.0118 18.402V13.638C11.0118 13.353 10.7125 13.168 10.4591 13.295L5.70918 15.678C5.42783 15.819 5.42783 16.221 5.70918 16.362Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 9C17.103 9 18 9.897 18 11V21C18 22.103 17.103 23 16 23C14.897 23 14 22.103 14 21V11C14 9.897 14.897 9 16 9ZM21.3564 12.2148C21.5684 12.2148 21.7824 12.2658 21.9734 12.3608L26.7354 14.7438C27.2064 14.9788 27.4994 15.4518 27.5004 15.9788C27.5004 16.5068 27.2074 16.9808 26.7364 17.2158L21.9744 19.5988C21.7834 19.6948 21.5694 19.7458 21.3564 19.7458C20.5934 19.7458 19.9734 19.1248 19.9734 18.3618V13.5978C19.9734 13.2188 20.1234 12.8648 20.3964 12.6028C20.6554 12.3528 20.9974 12.2148 21.3564 12.2148Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 10C16.552 10 17 10.448 17 11V21C17 21.552 16.552 22 16 22C15.448 22 15 21.552 15 21V11C15 10.448 15.448 10 16 10ZM26.2884 15.638L21.5274 13.256C21.2734 13.129 20.9734 13.314 20.9734 13.598V18.362C20.9734 18.647 21.2734 18.832 21.5274 18.705L26.2884 16.322C26.5704 16.181 26.5704 15.779 26.2884 15.638Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 16.0004C9 14.8974 9.897 14.0004 11 14.0004H21C22.103 14.0004 23 14.8974 23 16.0004C23 17.1034 22.103 18.0004 21 18.0004H11C9.897 18.0004 9 17.1034 9 16.0004ZM12.2148 10.644C12.2148 10.432 12.2658 10.218 12.3608 10.027L14.7438 5.265C14.9788 4.794 15.4518 4.501 15.9788 4.5C16.5068 4.5 16.9808 4.793 17.2158 5.264L19.5988 10.026C19.6948 10.217 19.7458 10.431 19.7458 10.644C19.7458 11.407 19.1248 12.027 18.3618 12.027H13.5978C13.2188 12.027 12.8648 11.877 12.6028 11.604C12.3528 11.345 12.2148 11.003 12.2148 10.644Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 16.0004C10 15.4484 10.448 15.0004 11 15.0004H21C21.552 15.0004 22 15.4484 22 16.0004C22 16.5524 21.552 17.0004 21 17.0004H11C10.448 17.0004 10 16.5524 10 16.0004ZM15.638 5.712L13.256 10.473C13.129 10.727 13.314 11.027 13.598 11.027H18.362C18.647 11.027 18.832 10.727 18.705 10.473L16.322 5.712C16.181 5.43 15.779 5.43 15.638 5.712Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23 16C23 17.1004 22.103 17.9953 21 17.9953H11C9.897 17.9953 9 17.1004 9 16C9 14.8996 9.897 14.0047 11 14.0047H21C22.103 14.0047 23 14.8996 23 16ZM19.7852 10.6297C19.7852 11.3909 19.1652 12.0095 18.4022 12.0095H13.6382C12.8752 12.0095 12.2542 11.3909 12.2542 10.6297C12.2542 10.4172 12.3052 10.2027 12.4022 10.0122L14.7842 5.26322C15.0192 4.79232 15.4932 4.5 16.0212 4.5C16.5482 4.501 17.0212 4.79332 17.2572 5.26422L19.6392 10.0132C19.7342 10.2037 19.7852 10.4172 19.7852 10.6297ZM19.7852 21.3703C19.7852 21.5818 19.7342 21.7953 19.6392 21.9859L17.2562 26.7368C17.0212 27.2067 16.5482 27.499 16.0212 27.5C15.4932 27.5 15.0192 27.2077 14.7842 26.7378L12.4012 21.9868C12.3052 21.7963 12.2542 21.5828 12.2542 21.3703C12.2542 20.6091 12.8752 19.9905 13.6382 19.9905H18.4022C18.7812 19.9905 19.1352 20.1402 19.3972 20.4125C19.6472 20.6709 19.7852 21.0121 19.7852 21.3703Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 16C22 16.5507 21.552 16.9977 21 16.9977H11C10.448 16.9977 10 16.5507 10 16C10 15.4493 10.448 15.0023 11 15.0023H21C21.552 15.0023 22 15.4493 22 16ZM15.6775 5.70918L13.2955 10.4591C13.1685 10.7125 13.3535 11.0118 13.6375 11.0118H18.4015C18.6865 11.0118 18.8715 10.7125 18.7445 10.4591L16.3615 5.70918C16.2205 5.42783 15.8185 5.42783 15.6775 5.70918ZM16.362 26.2908L18.744 21.5409C18.871 21.2875 18.686 20.9882 18.402 20.9882H13.638C13.353 20.9882 13.168 21.2875 13.295 21.5409L15.678 26.2908C15.819 26.5722 16.221 26.5722 16.362 26.2908Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.64 15.4658L16.5353 19.3601C16.8182 19.643 16.9701 20.0188 16.9621 20.4176C16.9551 20.7994 16.8042 21.1573 16.5353 21.4261C16.3754 21.5841 16.1785 21.707 15.9636 21.778L10.1241 23.7241C9.59533 23.9 9.02258 23.7651 8.62875 23.3713C8.23492 22.9774 8.09998 22.4037 8.2759 21.8759L10.2221 16.0366C10.293 15.8217 10.415 15.6238 10.5739 15.4648C11.1437 14.8961 12.0703 14.8961 12.64 15.4658ZM23.3713 8.62873C23.7651 9.02255 23.9 9.59529 23.7241 10.1231L21.781 15.9614C21.71 16.1763 21.587 16.3732 21.4281 16.5322C20.8584 17.1019 19.9318 17.1019 19.362 16.5322L15.4677 12.6379C14.8979 12.0692 14.8979 11.1426 15.4677 10.5718C15.6266 10.4129 15.8245 10.292 16.0394 10.22L21.8769 8.27589C22.4047 8.09997 22.9774 8.2349 23.3713 8.62873Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.80824 22.7821L15.6512 20.8351C15.9762 20.7271 16.0732 20.3141 15.8312 20.0721L11.9352 16.1761C11.6932 15.9331 11.2802 16.0311 11.1712 16.3561L9.22524 22.1991C9.10524 22.5591 9.44824 22.9021 9.80824 22.7821ZM22.199 9.22462L16.358 11.1696C16.033 11.2776 15.936 11.6906 16.178 11.9336L20.074 15.8286C20.316 16.0716 20.729 15.9736 20.837 15.6486L22.782 9.80762C22.902 9.44762 22.559 9.10462 22.199 9.22462Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.204 19.4693C20.203 19.6943 20.15 19.9203 20.049 20.1223L17.294 25.6303C17.045 26.1283 16.544 26.4383 15.987 26.4383C15.43 26.4383 14.929 26.1283 14.68 25.6303L11.925 20.1223C11.824 19.9203 11.77 19.6943 11.77 19.4693C11.77 18.6633 12.426 18.0083 13.232 18.0083H18.742C19.142 18.0083 19.517 18.1663 19.793 18.4543C20.058 18.7283 20.204 19.0893 20.204 19.4693ZM20.2031 12.546C20.2031 13.352 19.5481 14.008 18.7411 14.008H13.2321C12.4261 14.008 11.7701 13.353 11.7701 12.546C11.7701 12.321 11.8241 12.096 11.9251 11.893L14.6801 6.388C14.9291 5.89 15.4301 5.58 15.9871 5.58C16.5441 5.58 17.0451 5.89 17.2941 6.388L20.0491 11.893C20.1501 12.095 20.2031 12.321 20.2031 12.546Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3992 25.1833L19.1542 19.6753C19.3072 19.3683 19.0842 19.0083 18.7412 19.0083H13.2322C12.8892 19.0083 12.6662 19.3683 12.8192 19.6753L15.5742 25.1833C15.7442 25.5233 16.2292 25.5233 16.3992 25.1833ZM15.5743 6.8351L12.8193 12.3401C12.6663 12.6471 12.8893 13.0081 13.2323 13.0081H18.7413C19.0843 13.0081 19.3073 12.6471 19.1543 12.3401L16.3993 6.8351C16.2293 6.4951 15.7443 6.4951 15.5743 6.8351Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.36 15.4658C19.9297 14.8961 20.8563 14.8961 21.4261 15.4648C21.585 15.6238 21.707 15.8217 21.7779 16.0366L23.7241 21.8759C23.9 22.4037 23.7651 22.9774 23.3713 23.3713C22.9774 23.7651 22.4047 23.9 21.8759 23.7241L16.0364 21.778C15.8215 21.707 15.6246 21.5841 15.4647 21.4261C15.1958 21.1573 15.0449 20.7994 15.0379 20.4176C15.0299 20.0188 15.1818 19.643 15.4647 19.3601L19.36 15.4658ZM8.62873 8.62873C9.02256 8.2349 9.59532 8.09997 10.1231 8.27589L15.9606 10.22C16.1755 10.292 16.3734 10.4129 16.5323 10.5718C17.1021 11.1426 17.1021 12.0692 16.5323 12.6379L12.638 16.5322C12.0682 17.1019 11.1416 17.1019 10.5719 16.5322C10.413 16.3732 10.29 16.1763 10.219 15.9614L8.27589 10.1231C8.09996 9.59529 8.2349 9.02255 8.62873 8.62873Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.1926 22.7755L16.3521 20.8294C16.0272 20.7215 15.9303 20.3087 16.1722 20.0668L20.0665 16.1725C20.3084 15.9296 20.7212 16.0276 20.8302 16.3524L22.7753 22.1928C22.8953 22.5526 22.5524 22.8955 22.1926 22.7755ZM9.80703 9.22416L15.6455 11.1683C15.9704 11.2762 16.0683 11.689 15.8254 11.9319L11.9311 15.8252C11.6892 16.0681 11.2764 15.9701 11.1684 15.6453L9.22428 9.80689C9.10433 9.44706 9.44718 9.10421 9.80703 9.22416Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.7634 12C19.9884 12.001 20.2144 12.054 20.4164 12.155L25.9244 14.91C26.4224 15.159 26.7324 15.66 26.7324 16.217C26.7324 16.774 26.4224 17.275 25.9244 17.524L20.4164 20.279C20.2144 20.38 19.9884 20.434 19.7634 20.434C18.9574 20.434 18.3024 19.778 18.3024 18.972V13.462C18.3024 13.062 18.4604 12.687 18.7484 12.411C19.0224 12.146 19.3834 12 19.7634 12ZM12.84 12.001C13.646 12.001 14.302 12.656 14.302 13.463V18.972C14.302 19.778 13.647 20.434 12.84 20.434C12.615 20.434 12.39 20.38 12.187 20.279L6.68199 17.524C6.18399 17.275 5.87399 16.774 5.87399 16.217C5.87399 15.66 6.18399 15.159 6.68199 14.91L12.187 12.155C12.389 12.054 12.615 12.001 12.84 12.001Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.4773 15.8049L19.9693 13.0499C19.6623 12.8969 19.3023 13.1199 19.3023 13.4629V18.9719C19.3023 19.3149 19.6623 19.5379 19.9693 19.3849L25.4773 16.6299C25.8173 16.4599 25.8173 15.9749 25.4773 15.8049ZM7.12921 16.6298L12.6342 19.3848C12.9412 19.5378 13.3022 19.3148 13.3022 18.9718V13.4628C13.3022 13.1198 12.9412 12.8968 12.6342 13.0498L7.12921 15.8048C6.78921 15.9748 6.78921 16.4598 7.12921 16.6298Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.315 7.5046C20.315 8.2486 19.759 8.8886 19.023 8.9936C18.431 9.0786 17.984 9.5956 17.984 10.1956V20.4256C17.984 21.0266 18.43 21.5436 19.021 21.6286C19.756 21.7326 20.312 22.3736 20.312 23.1176C20.309 23.5606 20.118 23.9736 19.787 24.2566C19.458 24.5396 19.021 24.6676 18.59 24.6036C17.807 24.4916 17.078 24.1606 16.481 23.6586C15.883 24.1606 15.154 24.4916 14.367 24.6046C13.94 24.6676 13.504 24.5406 13.173 24.2566C12.842 23.9696 12.653 23.5576 12.65 23.1246C12.65 22.3736 13.206 21.7326 13.942 21.6276C14.536 21.5426 14.984 21.0256 14.984 20.4256V10.1956C14.984 9.5956 14.537 9.0786 13.944 8.9936C13.209 8.8886 12.654 8.2486 12.654 7.5046C12.655 7.0646 12.846 6.6506 13.177 6.3656C13.447 6.1326 13.796 6.0016 14.158 6.0016H14.239L14.39 6.0206C15.164 6.1316 15.889 6.4616 16.484 6.9636C17.083 6.4616 17.812 6.1296 18.598 6.0176C19.013 5.9506 19.455 6.0766 19.792 6.3656C20.123 6.6526 20.312 7.0646 20.315 7.4976V7.5016V7.5046Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.087 8.00285C15.169 8.15785 15.985 9.10085 15.985 10.1949V20.4249C15.985 21.5189 15.167 22.4619 14.083 22.6169C13.836 22.6519 13.65 22.8669 13.65 23.1169C13.651 23.2629 13.714 23.4009 13.824 23.4969C13.935 23.5909 14.08 23.6349 14.226 23.6139C15.148 23.4819 15.978 22.9429 16.481 22.1599C16.985 22.9429 17.814 23.4819 18.737 23.6139C18.879 23.6349 19.027 23.5909 19.137 23.4959C19.248 23.4009 19.311 23.2629 19.312 23.1169C19.312 22.8669 19.126 22.6519 18.879 22.6169C17.799 22.4619 16.985 21.5199 16.985 20.4249V10.1949C16.985 9.10085 17.801 8.15785 18.883 8.00285C19.13 7.96785 19.316 7.75285 19.316 7.50285C19.315 7.35785 19.251 7.21885 19.141 7.12285C19.03 7.02885 18.885 6.98285 18.74 7.00585C17.818 7.13885 16.988 7.67685 16.484 8.45985C15.98 7.67685 15.151 7.13885 14.229 7.00585C14.206 7.00285 14.182 7.00085 14.158 7.00085C14.038 7.00085 13.921 7.04385 13.829 7.12285C13.718 7.21885 13.655 7.35685 13.654 7.50385C13.654 7.75285 13.84 7.96785 14.087 8.00285Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 17C24 20.866 20.866 24 17 24C13.134 24 10 20.866 10 17C10 13.134 13.134 10 17 10C20.866 10 24 13.134 24 17Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.2532 17C22.2532 19.901 19.9012 22.253 17.0002 22.253C14.0982 22.253 11.7472 19.901 11.7472 17C11.7472 14.099 14.0982 11.747 17.0002 11.747C19.9012 11.747 22.2532 14.099 22.2532 17Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 20.5C16.448 20.5 16 20.052 16 19.5V14.5C16 13.948 16.448 13.5 17 13.5C17.552 13.5 18 13.948 18 14.5V19.5C18 20.052 17.552 20.5 17 20.5Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 17C13.5 16.448 13.948 16 14.5 16H19.5C20.052 16 20.5 16.448 20.5 17C20.5 17.552 20.052 18 19.5 18H14.5C13.948 18 13.5 17.552 13.5 17Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.9315 26.9316C27.3215 26.5416 27.3215 25.9076 26.9315 25.5176L22.1895 20.7756L20.7755 22.1896L25.5175 26.9316C25.9075 27.3216 26.5415 27.3216 26.9315 26.9316Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 17C24 20.866 20.866 24 17 24C13.134 24 10 20.866 10 17C10 13.134 13.134 10 17 10C20.866 10 24 13.134 24 17Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.2532 17C22.2532 19.901 19.9012 22.253 17.0002 22.253C14.0982 22.253 11.7472 19.901 11.7472 17C11.7472 14.099 14.0982 11.747 17.0002 11.747C19.9012 11.747 22.2532 14.099 22.2532 17Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5 17C13.5 16.448 13.948 16 14.5 16H19.5C20.052 16 20.5 16.448 20.5 17C20.5 17.552 20.052 18 19.5 18H14.5C13.948 18 13.5 17.552 13.5 17ZM26.9315 26.9316C27.3215 26.5416 27.3215 25.9076 26.9315 25.5176L22.1895 20.7756L20.7755 22.1896L25.5175 26.9316C25.9075 27.3216 26.5415 27.3216 26.9315 26.9316Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 1002 B |
@@ -537,6 +537,7 @@ export function SettingsPanel({
|
||||
},
|
||||
[cropRegion, videoWidth, videoHeight],
|
||||
);
|
||||
const [showCropDropdown, setShowCropDropdown] = useState(false);
|
||||
|
||||
const zoomEnabled = Boolean(selectedZoomDepth);
|
||||
const trimEnabled = Boolean(selectedTrimId);
|
||||
@@ -625,20 +626,6 @@ export function SettingsPanel({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCropToggle = () => {
|
||||
if (!showCropModal && cropRegion) {
|
||||
cropSnapshotRef.current = { ...cropRegion };
|
||||
}
|
||||
setShowCropModal(!showCropModal);
|
||||
};
|
||||
|
||||
const handleCropCancel = () => {
|
||||
if (cropSnapshotRef.current && onCropChange) {
|
||||
onCropChange(cropSnapshotRef.current);
|
||||
}
|
||||
setShowCropModal(false);
|
||||
};
|
||||
|
||||
// Find selected annotation
|
||||
const selectedAnnotation = selectedAnnotationId
|
||||
? annotationRegions.find((a) => a.id === selectedAnnotationId)
|
||||
@@ -1745,11 +1732,11 @@ export function SettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCropModal && cropRegion && onCropChange && (
|
||||
{showCropDropdown && cropRegion && onCropChange && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 animate-in fade-in duration-200"
|
||||
onClick={handleCropCancel}
|
||||
onClick={() => setShowCropDropdown(false)}
|
||||
/>
|
||||
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-[60] bg-[#09090b] rounded-2xl shadow-2xl border border-white/10 p-8 w-[90vw] max-w-5xl max-h-[90vh] overflow-auto animate-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
@@ -1760,7 +1747,7 @@ export function SettingsPanel({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleCropCancel}
|
||||
onClick={() => setShowCropDropdown(false)}
|
||||
className="hover:bg-white/10 text-slate-400 hover:text-white"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
@@ -1856,7 +1843,7 @@ export function SettingsPanel({
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={() => setShowCropModal(false)}
|
||||
onClick={() => setShowCropDropdown(false)}
|
||||
size="lg"
|
||||
className="bg-[#34B27B] hover:bg-[#34B27B]/90 text-white"
|
||||
>
|
||||
|
||||
@@ -67,6 +67,10 @@ import {
|
||||
DEFAULT_ANNOTATION_SIZE,
|
||||
DEFAULT_ANNOTATION_STYLE,
|
||||
DEFAULT_BLUR_DATA,
|
||||
DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_SIZE,
|
||||
DEFAULT_CURSOR_SMOOTHING,
|
||||
DEFAULT_FIGURE_DATA,
|
||||
DEFAULT_PLAYBACK_SPEED,
|
||||
DEFAULT_ZOOM_DEPTH,
|
||||
@@ -156,7 +160,13 @@ export default function VideoEditor() {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showCloseConfirmDialog, setShowCloseConfirmDialog] = useState(false);
|
||||
|
||||
const playerContainerRef = useRef<HTMLDivElement>(null);
|
||||
// Cursor & motion blur visual settings (non-undoable preferences)
|
||||
const [showCursor, setShowCursor] = useState(true);
|
||||
const [cursorSize, setCursorSize] = useState(DEFAULT_CURSOR_SIZE);
|
||||
const [cursorSmoothing, setCursorSmoothing] = useState(DEFAULT_CURSOR_SMOOTHING);
|
||||
const [cursorMotionBlur, setCursorMotionBlur] = useState(DEFAULT_CURSOR_MOTION_BLUR);
|
||||
const [cursorClickBounce, setCursorClickBounce] = useState(DEFAULT_CURSOR_CLICK_BOUNCE);
|
||||
|
||||
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
|
||||
|
||||
const nextZoomIdRef = useRef(1);
|
||||
@@ -208,12 +218,7 @@ export default function VideoEditor() {
|
||||
}
|
||||
|
||||
const project = candidate;
|
||||
const media = resolveProjectMedia(project);
|
||||
if (!media) {
|
||||
return false;
|
||||
}
|
||||
const sourcePath = fromFileUrl(media.screenVideoPath);
|
||||
const webcamSourcePath = media.webcamVideoPath ? fromFileUrl(media.webcamVideoPath) : null;
|
||||
const sourcePath = project.videoPath;
|
||||
const normalizedEditor = normalizeProjectEditor(project.editor);
|
||||
|
||||
try {
|
||||
@@ -391,11 +396,8 @@ export default function VideoEditor() {
|
||||
|
||||
const result = await window.electronAPI.getCurrentVideoPath();
|
||||
if (result.success && result.path) {
|
||||
const sourcePath = fromFileUrl(result.path);
|
||||
setVideoSourcePath(sourcePath);
|
||||
setVideoPath(toFileUrl(sourcePath));
|
||||
setWebcamVideoSourcePath(null);
|
||||
setWebcamVideoPath(null);
|
||||
setVideoSourcePath(result.path);
|
||||
setVideoPath(toFileUrl(result.path));
|
||||
setCurrentProjectPath(null);
|
||||
setLastSavedSnapshot(
|
||||
createProjectSnapshot({ screenVideoPath: sourcePath }, INITIAL_EDITOR_STATE),
|
||||
|
||||
@@ -36,7 +36,12 @@ import { AnnotationOverlay } from "./AnnotationOverlay";
|
||||
import {
|
||||
type AnnotationRegion,
|
||||
type BlurData,
|
||||
type CursorTelemetryPoint,
|
||||
computeRotation3DContainScale,
|
||||
DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
DEFAULT_CURSOR_MOTION_BLUR,
|
||||
DEFAULT_CURSOR_SIZE,
|
||||
DEFAULT_CURSOR_SMOOTHING,
|
||||
DEFAULT_ROTATION_3D,
|
||||
getZoomScale,
|
||||
isRotation3DIdentity,
|
||||
@@ -130,9 +135,14 @@ interface VideoPlaybackProps {
|
||||
onBlurSizeChange?: (id: string, size: { width: number; height: number }) => void;
|
||||
onBlurDataChange?: (id: string, blurData: BlurData) => void;
|
||||
onBlurDataCommit?: () => void;
|
||||
cursorTelemetry?: import("./types").CursorTelemetryPoint[];
|
||||
cursorTelemetry?: CursorTelemetryPoint[];
|
||||
cursorHighlight?: CursorHighlightConfig;
|
||||
cursorClickTimestamps?: number[];
|
||||
showCursor?: boolean;
|
||||
cursorSize?: number;
|
||||
cursorSmoothing?: number;
|
||||
cursorMotionBlur?: number;
|
||||
cursorClickBounce?: number;
|
||||
}
|
||||
|
||||
export interface VideoPlaybackRef {
|
||||
@@ -193,6 +203,11 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
cursorTelemetry = [],
|
||||
cursorHighlight = DEFAULT_CURSOR_HIGHLIGHT,
|
||||
cursorClickTimestamps = [],
|
||||
showCursor = false,
|
||||
cursorSize = DEFAULT_CURSOR_SIZE,
|
||||
cursorSmoothing = DEFAULT_CURSOR_SMOOTHING,
|
||||
cursorMotionBlur = DEFAULT_CURSOR_MOTION_BLUR,
|
||||
cursorClickBounce = DEFAULT_CURSOR_CLICK_BOUNCE,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -203,6 +218,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const videoSpriteRef = useRef<Sprite | null>(null);
|
||||
const videoContainerRef = useRef<Container | null>(null);
|
||||
const cameraContainerRef = useRef<Container | null>(null);
|
||||
const cursorContainerRef = useRef<Container | null>(null);
|
||||
const timeUpdateAnimationRef = useRef<number | null>(null);
|
||||
const [pixiReady, setPixiReady] = useState(false);
|
||||
const [videoReady, setVideoReady] = useState(false);
|
||||
@@ -217,7 +233,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const [webcamDimensions, setWebcamDimensions] = useState<Size | null>(null);
|
||||
const currentTimeRef = useRef(0);
|
||||
const zoomRegionsRef = useRef<ZoomRegion[]>([]);
|
||||
const cursorTelemetryRef = useRef<import("./types").CursorTelemetryPoint[]>([]);
|
||||
const cursorTelemetryRef = useRef<CursorTelemetryPoint[]>([]);
|
||||
const cursorHighlightRef = useRef<CursorHighlightConfig>(DEFAULT_CURSOR_HIGHLIGHT);
|
||||
const cursorClickTimestampsRef = useRef<number[]>([]);
|
||||
const cursorHighlightGraphicsRef = useRef<Graphics | null>(null);
|
||||
@@ -257,6 +273,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const trimRegionsRef = useRef<TrimRegion[]>([]);
|
||||
const speedRegionsRef = useRef<SpeedRegion[]>([]);
|
||||
const motionBlurAmountRef = useRef(motionBlurAmount);
|
||||
const cursorOverlayRef = useRef<PixiCursorOverlay | null>(null);
|
||||
const showCursorRef = useRef(showCursor);
|
||||
const cursorSizeRef = useRef(cursorSize);
|
||||
const cursorSmoothingRef = useRef(cursorSmoothing);
|
||||
const cursorMotionBlurRef = useRef(cursorMotionBlur);
|
||||
const cursorClickBounceRef = useRef(cursorClickBounce);
|
||||
const motionBlurStateRef = useRef<MotionBlurState>(createMotionBlurState());
|
||||
const onTimeUpdateRef = useRef(onTimeUpdate);
|
||||
const onPlayStateChangeRef = useRef(onPlayStateChange);
|
||||
@@ -581,6 +603,41 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
motionBlurAmountRef.current = motionBlurAmount;
|
||||
}, [motionBlurAmount]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorTelemetryRef.current = cursorTelemetry;
|
||||
}, [cursorTelemetry]);
|
||||
|
||||
useEffect(() => {
|
||||
showCursorRef.current = showCursor;
|
||||
}, [showCursor]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorSizeRef.current = cursorSize;
|
||||
}, [cursorSize]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorSmoothingRef.current = cursorSmoothing;
|
||||
}, [cursorSmoothing]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorMotionBlurRef.current = cursorMotionBlur;
|
||||
}, [cursorMotionBlur]);
|
||||
|
||||
useEffect(() => {
|
||||
cursorClickBounceRef.current = cursorClickBounce;
|
||||
}, [cursorClickBounce]);
|
||||
|
||||
// Sync cursor overlay config when settings change
|
||||
useEffect(() => {
|
||||
const overlay = cursorOverlayRef.current;
|
||||
if (!overlay) return;
|
||||
overlay.setDotRadius(DEFAULT_CURSOR_CONFIG.dotRadius * cursorSize);
|
||||
overlay.setSmoothingFactor(cursorSmoothing);
|
||||
overlay.setMotionBlur(cursorMotionBlur);
|
||||
overlay.setClickBounce(cursorClickBounce);
|
||||
overlay.reset();
|
||||
}, [cursorSize, cursorSmoothing, cursorMotionBlur, cursorClickBounce]);
|
||||
|
||||
useEffect(() => {
|
||||
onTimeUpdateRef.current = onTimeUpdate;
|
||||
}, [onTimeUpdate]);
|
||||
@@ -700,6 +757,13 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
let app: Application | null = null;
|
||||
|
||||
(async () => {
|
||||
let cursorOverlayEnabled = true;
|
||||
try {
|
||||
await preloadCursorAssets();
|
||||
} catch {
|
||||
cursorOverlayEnabled = false;
|
||||
}
|
||||
|
||||
app = new Application();
|
||||
|
||||
await app.init({
|
||||
@@ -735,12 +799,33 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
videoContainerRef.current = videoContainer;
|
||||
cameraContainer.addChild(videoContainer);
|
||||
|
||||
// Cursor container - rendered above video
|
||||
const cursorContainer = new Container();
|
||||
cursorContainerRef.current = cursorContainer;
|
||||
cameraContainer.addChild(cursorContainer);
|
||||
|
||||
// Cursor overlay - rendered above the masked video
|
||||
if (cursorOverlayEnabled) {
|
||||
const cursorOverlay = new PixiCursorOverlay({
|
||||
dotRadius: DEFAULT_CURSOR_CONFIG.dotRadius * cursorSizeRef.current,
|
||||
smoothingFactor: cursorSmoothingRef.current,
|
||||
motionBlur: cursorMotionBlurRef.current,
|
||||
clickBounce: cursorClickBounceRef.current,
|
||||
});
|
||||
cursorOverlayRef.current = cursorOverlay;
|
||||
cursorContainer.addChild(cursorOverlay.container);
|
||||
}
|
||||
|
||||
setPixiReady(true);
|
||||
})();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
setPixiReady(false);
|
||||
if (cursorOverlayRef.current) {
|
||||
cursorOverlayRef.current.destroy();
|
||||
cursorOverlayRef.current = null;
|
||||
}
|
||||
if (app && app.renderer) {
|
||||
app.destroy(true, {
|
||||
children: true,
|
||||
@@ -751,6 +836,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
appRef.current = null;
|
||||
cameraContainerRef.current = null;
|
||||
videoContainerRef.current = null;
|
||||
cursorContainerRef.current = null;
|
||||
videoSpriteRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
@@ -780,8 +866,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
const video = videoRef.current;
|
||||
const app = appRef.current;
|
||||
const videoContainer = videoContainerRef.current;
|
||||
const cursorContainer = cursorContainerRef.current;
|
||||
|
||||
if (!video || !app || !videoContainer) return;
|
||||
if (!video || !app || !videoContainer || !cursorContainer) return;
|
||||
if (video.videoWidth === 0 || video.videoHeight === 0) return;
|
||||
|
||||
const source = VideoSource.from(video);
|
||||
@@ -801,6 +888,9 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
videoContainer.addChild(maskGraphics);
|
||||
videoContainer.mask = maskGraphics;
|
||||
maskGraphicsRef.current = maskGraphics;
|
||||
if (cursorOverlayRef.current) {
|
||||
cursorContainer.addChild(cursorOverlayRef.current.container);
|
||||
}
|
||||
|
||||
const cursorHighlightGraphics = new Graphics();
|
||||
cursorHighlightGraphics.visible = false;
|
||||
@@ -1147,6 +1237,19 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
|
||||
}
|
||||
}
|
||||
|
||||
// Update cursor overlay
|
||||
const cursorOverlay = cursorOverlayRef.current;
|
||||
if (cursorOverlay) {
|
||||
const timeMs = currentTimeRef.current;
|
||||
cursorOverlay.update(
|
||||
cursorTelemetryRef.current,
|
||||
timeMs,
|
||||
baseMaskRef.current,
|
||||
showCursorRef.current,
|
||||
!isPlayingRef.current || isSeekingRef.current,
|
||||
);
|
||||
}
|
||||
|
||||
const composite3D = composite3DRef.current;
|
||||
const outerWrapper = outerWrapperRef.current;
|
||||
if (composite3D && outerWrapper) {
|
||||
|
||||
@@ -119,50 +119,21 @@ function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function isFileUrl(value: string): boolean {
|
||||
return /^file:\/\//i.test(value);
|
||||
}
|
||||
|
||||
function encodePathSegments(pathname: string, keepWindowsDrive = false): string {
|
||||
return pathname
|
||||
.split("/")
|
||||
.map((segment, index) => {
|
||||
if (!segment) return "";
|
||||
if (keepWindowsDrive && index === 1 && /^[a-zA-Z]:$/.test(segment)) {
|
||||
return segment;
|
||||
}
|
||||
return encodeURIComponent(segment);
|
||||
})
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export function toFileUrl(filePath: string): string {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
|
||||
// Windows drive path: C:/Users/...
|
||||
if (/^[a-zA-Z]:\//.test(normalized)) {
|
||||
return `file://${encodePathSegments(`/${normalized}`, true)}`;
|
||||
if (normalized.match(/^[a-zA-Z]:/)) {
|
||||
return `file:///${encodeURI(normalized)}`;
|
||||
}
|
||||
|
||||
// UNC path: //server/share/...
|
||||
if (normalized.startsWith("//")) {
|
||||
const [host, ...pathParts] = normalized.replace(/^\/+/, "").split("/");
|
||||
const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/");
|
||||
return encodedPath ? `file://${host}/${encodedPath}` : `file://${host}/`;
|
||||
}
|
||||
|
||||
const absolutePath = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
||||
return `file://${encodePathSegments(absolutePath)}`;
|
||||
return `file://${encodeURI(normalized)}`;
|
||||
}
|
||||
|
||||
export function fromFileUrl(fileUrl: string): string {
|
||||
const value = fileUrl.trim();
|
||||
if (!isFileUrl(value)) {
|
||||
if (!fileUrl.startsWith("file://")) {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
const url = new URL(fileUrl);
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
|
||||
if (url.host && url.host !== "localhost") {
|
||||
@@ -175,13 +146,7 @@ export function fromFileUrl(fileUrl: string): string {
|
||||
|
||||
return pathname;
|
||||
} catch {
|
||||
const rawFallbackPath = value.replace(/^file:\/\//i, "");
|
||||
let fallbackPath = rawFallbackPath;
|
||||
try {
|
||||
fallbackPath = decodeURIComponent(rawFallbackPath);
|
||||
} catch {
|
||||
// Keep raw best-effort path if percent decoding fails.
|
||||
}
|
||||
const fallbackPath = decodeURIComponent(fileUrl.replace(/^file:\/\//, ""));
|
||||
return fallbackPath.replace(/^\/([a-zA-Z]:)/, "$1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,8 +170,32 @@ export interface CursorTelemetryPoint {
|
||||
timeMs: number;
|
||||
cx: number;
|
||||
cy: number;
|
||||
interactionType?: "move" | "click" | "double-click" | "right-click" | "middle-click" | "mouseup";
|
||||
cursorType?:
|
||||
| "arrow"
|
||||
| "text"
|
||||
| "pointer"
|
||||
| "crosshair"
|
||||
| "open-hand"
|
||||
| "closed-hand"
|
||||
| "resize-ew"
|
||||
| "resize-ns"
|
||||
| "not-allowed";
|
||||
}
|
||||
|
||||
export interface CursorVisualSettings {
|
||||
size: number;
|
||||
smoothing: number;
|
||||
motionBlur: number;
|
||||
clickBounce: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CURSOR_SIZE = 3.0;
|
||||
export const DEFAULT_CURSOR_SMOOTHING = 0.67;
|
||||
export const DEFAULT_CURSOR_MOTION_BLUR = 0.35;
|
||||
export const DEFAULT_CURSOR_CLICK_BOUNCE = 2.5;
|
||||
export const DEFAULT_ZOOM_MOTION_BLUR = 0.35;
|
||||
|
||||
export interface TrimRegion {
|
||||
id: string;
|
||||
startMs: number;
|
||||
|
||||
@@ -0,0 +1,766 @@
|
||||
import { Assets, BlurFilter, Container, Graphics, Sprite, Texture } from "pixi.js";
|
||||
import { MotionBlurFilter } from "pixi-filters/motion-blur";
|
||||
import type { CursorTelemetryPoint } from "../types";
|
||||
import {
|
||||
createSpringState,
|
||||
getCursorSpringConfig,
|
||||
resetSpringState,
|
||||
stepSpringValue,
|
||||
} from "./motionSmoothing";
|
||||
import { UPLOADED_CURSOR_SAMPLE_SIZE, uploadedCursorAssets } from "./uploadedCursorAssets";
|
||||
|
||||
type CursorAssetKey = NonNullable<CursorTelemetryPoint["cursorType"]>;
|
||||
|
||||
/** System cursor asset from native helper (macOS only). */
|
||||
type SystemCursorAsset = {
|
||||
dataUrl: string;
|
||||
width: number;
|
||||
height: number;
|
||||
hotspotX: number;
|
||||
hotspotY: number;
|
||||
};
|
||||
|
||||
type LoadedCursorAsset = {
|
||||
texture: Texture;
|
||||
image: HTMLImageElement;
|
||||
aspectRatio: number;
|
||||
anchorX: number;
|
||||
anchorY: number;
|
||||
};
|
||||
|
||||
export interface CursorViewportRect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for cursor rendering.
|
||||
*/
|
||||
export interface CursorRenderConfig {
|
||||
/** Base cursor height in pixels (at reference width of 1920px) */
|
||||
dotRadius: number;
|
||||
/** Cursor fill color (hex number for PixiJS) */
|
||||
dotColor: number;
|
||||
/** Cursor opacity (0–1) */
|
||||
dotAlpha: number;
|
||||
/** Unused, kept for interface compatibility */
|
||||
trailLength: number;
|
||||
/** Smoothing factor for cursor interpolation (0–1, lower = smoother/slower) */
|
||||
smoothingFactor: number;
|
||||
/** Directional cursor motion blur amount. */
|
||||
motionBlur: number;
|
||||
/** Click bounce multiplier. */
|
||||
clickBounce: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CURSOR_CONFIG: CursorRenderConfig = {
|
||||
dotRadius: 28,
|
||||
dotColor: 0xffffff,
|
||||
dotAlpha: 0.95,
|
||||
trailLength: 0,
|
||||
smoothingFactor: 0.18,
|
||||
motionBlur: 0,
|
||||
clickBounce: 1,
|
||||
};
|
||||
|
||||
const REFERENCE_WIDTH = 1920;
|
||||
const MIN_CURSOR_VIEWPORT_SCALE = 0.55;
|
||||
const CLICK_ANIMATION_MS = 140;
|
||||
const CLICK_RING_FADE_MS = 240;
|
||||
const CURSOR_MOTION_BLUR_BASE_MULTIPLIER = 0.08;
|
||||
const CURSOR_TIME_DISCONTINUITY_MS = 100;
|
||||
const CURSOR_SVG_DROP_SHADOW_FILTER = "drop-shadow(0px 2px 3px rgba(0, 0, 0, 0.35))";
|
||||
const CURSOR_SHADOW_COLOR = 0x000000;
|
||||
const CURSOR_SHADOW_ALPHA = 0.35;
|
||||
const CURSOR_SHADOW_OFFSET_X = 0;
|
||||
const CURSOR_SHADOW_OFFSET_Y = 2;
|
||||
const CURSOR_SHADOW_BLUR = 3;
|
||||
const CURSOR_SHADOW_PADDING = 12;
|
||||
|
||||
let cursorAssetsPromise: Promise<void> | null = null;
|
||||
let loadedCursorAssets: Partial<Record<CursorAssetKey, LoadedCursorAsset>> = {};
|
||||
const SUPPORTED_CURSOR_KEYS: CursorAssetKey[] = [
|
||||
"arrow",
|
||||
"text",
|
||||
"pointer",
|
||||
"crosshair",
|
||||
"open-hand",
|
||||
"closed-hand",
|
||||
"resize-ew",
|
||||
"resize-ns",
|
||||
"not-allowed",
|
||||
];
|
||||
|
||||
function loadImage(dataUrl: string) {
|
||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () =>
|
||||
reject(new Error(`Failed to load cursor image: ${dataUrl.slice(0, 128)}`));
|
||||
image.src = dataUrl;
|
||||
});
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function getNormalizedAnchor(
|
||||
systemAsset: SystemCursorAsset | undefined,
|
||||
fallbackAnchor: { x: number; y: number },
|
||||
) {
|
||||
if (!systemAsset || systemAsset.width <= 0 || systemAsset.height <= 0) {
|
||||
return fallbackAnchor;
|
||||
}
|
||||
|
||||
return {
|
||||
x: clamp(systemAsset.hotspotX / systemAsset.width, 0, 1),
|
||||
y: clamp(systemAsset.hotspotY / systemAsset.height, 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an SVG at `sampleSize × sampleSize`, crops the trim region out of it,
|
||||
* and returns a PNG data-URL of the cropped result. This is required because
|
||||
* SVG files have their own natural pixel size (e.g. 32×32) which does not
|
||||
* match the 1024-sample coordinate space used by the trim measurements.
|
||||
*/
|
||||
async function rasterizeAndCropSvg(
|
||||
url: string,
|
||||
sampleSize: number,
|
||||
trimX: number,
|
||||
trimY: number,
|
||||
trimWidth: number,
|
||||
trimHeight: number,
|
||||
): Promise<{ dataUrl: string; width: number; height: number }> {
|
||||
const img = await loadImage(url);
|
||||
|
||||
// Draw at full sample size
|
||||
const srcCanvas = document.createElement("canvas");
|
||||
srcCanvas.width = sampleSize;
|
||||
srcCanvas.height = sampleSize;
|
||||
const srcCtx = srcCanvas.getContext("2d")!;
|
||||
srcCtx.drawImage(img, 0, 0, sampleSize, sampleSize);
|
||||
|
||||
// Crop to trim bounds
|
||||
const dstCanvas = document.createElement("canvas");
|
||||
dstCanvas.width = trimWidth;
|
||||
dstCanvas.height = trimHeight;
|
||||
const dstCtx = dstCanvas.getContext("2d")!;
|
||||
dstCtx.drawImage(srcCanvas, trimX, trimY, trimWidth, trimHeight, 0, 0, trimWidth, trimHeight);
|
||||
|
||||
return {
|
||||
dataUrl: dstCanvas.toDataURL("image/png"),
|
||||
width: dstCanvas.width,
|
||||
height: dstCanvas.height,
|
||||
};
|
||||
}
|
||||
|
||||
function getCursorAsset(key: CursorAssetKey): LoadedCursorAsset {
|
||||
const asset = loadedCursorAssets[key];
|
||||
if (!asset) {
|
||||
throw new Error(`Missing cursor asset for ${key}`);
|
||||
}
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
function getAvailableCursorKeys(): CursorAssetKey[] {
|
||||
const loadedKeys = Object.keys(loadedCursorAssets) as CursorAssetKey[];
|
||||
return loadedKeys.length > 0 ? loadedKeys : ["arrow"];
|
||||
}
|
||||
|
||||
export async function preloadCursorAssets() {
|
||||
if (!cursorAssetsPromise) {
|
||||
cursorAssetsPromise = (async () => {
|
||||
let systemCursors: Record<string, SystemCursorAsset> = {};
|
||||
|
||||
try {
|
||||
const api = window.electronAPI as Record<string, unknown>;
|
||||
if (typeof api.getSystemCursorAssets === "function") {
|
||||
const result = await (
|
||||
api.getSystemCursorAssets as () => Promise<{
|
||||
success: boolean;
|
||||
cursors?: Record<string, SystemCursorAsset>;
|
||||
}>
|
||||
)();
|
||||
if (result.success && result.cursors) {
|
||||
systemCursors = result.cursors;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[CursorRenderer] Failed to fetch system cursor assets:", error);
|
||||
}
|
||||
|
||||
const entries = await Promise.all(
|
||||
SUPPORTED_CURSOR_KEYS.map(async (key) => {
|
||||
const systemAsset = systemCursors[key];
|
||||
const uploadedAsset = uploadedCursorAssets[key];
|
||||
const assetUrl = uploadedAsset?.url ?? systemAsset?.dataUrl;
|
||||
|
||||
if (!assetUrl) {
|
||||
console.warn(`[CursorRenderer] No cursor image for: ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let finalUrl: string;
|
||||
let width: number;
|
||||
let height: number;
|
||||
let normalizedAnchor: { x: number; y: number };
|
||||
|
||||
if (uploadedAsset) {
|
||||
const { trim, fallbackAnchor } = uploadedAsset;
|
||||
const rasterized = await rasterizeAndCropSvg(
|
||||
assetUrl,
|
||||
UPLOADED_CURSOR_SAMPLE_SIZE,
|
||||
trim.x,
|
||||
trim.y,
|
||||
trim.width,
|
||||
trim.height,
|
||||
);
|
||||
finalUrl = rasterized.dataUrl;
|
||||
width = rasterized.width;
|
||||
height = rasterized.height;
|
||||
normalizedAnchor = {
|
||||
x: clamp((fallbackAnchor.x * trim.width) / width, 0, 1),
|
||||
y: clamp((fallbackAnchor.y * trim.height) / height, 0, 1),
|
||||
};
|
||||
} else {
|
||||
finalUrl = assetUrl;
|
||||
const img = await loadImage(finalUrl);
|
||||
width = img.naturalWidth;
|
||||
height = img.naturalHeight;
|
||||
normalizedAnchor = getNormalizedAnchor(systemAsset, { x: 0, y: 0 });
|
||||
}
|
||||
|
||||
await Assets.load(finalUrl);
|
||||
const image = await loadImage(finalUrl);
|
||||
const texture = Texture.from(finalUrl);
|
||||
|
||||
return [
|
||||
key,
|
||||
{
|
||||
texture,
|
||||
image,
|
||||
aspectRatio: height > 0 ? width / height : 1,
|
||||
anchorX: normalizedAnchor.x,
|
||||
anchorY: normalizedAnchor.y,
|
||||
} satisfies LoadedCursorAsset,
|
||||
] as const;
|
||||
} catch (error) {
|
||||
console.warn(`[CursorRenderer] Failed to load cursor image for: ${key}`, error);
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
loadedCursorAssets = Object.fromEntries(
|
||||
entries.filter(Boolean).map((entry) => entry!),
|
||||
) as Partial<Record<CursorAssetKey, LoadedCursorAsset>>;
|
||||
|
||||
if (!loadedCursorAssets.arrow) {
|
||||
throw new Error("Failed to initialize the fallback arrow cursor asset");
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return cursorAssetsPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates cursor position from telemetry samples at a given time.
|
||||
* Uses linear interpolation between the two nearest samples.
|
||||
*/
|
||||
export function interpolateCursorPosition(
|
||||
samples: CursorTelemetryPoint[],
|
||||
timeMs: number,
|
||||
): { cx: number; cy: number } | null {
|
||||
if (!samples || samples.length === 0) return null;
|
||||
|
||||
if (timeMs <= samples[0].timeMs) {
|
||||
return { cx: samples[0].cx, cy: samples[0].cy };
|
||||
}
|
||||
|
||||
if (timeMs >= samples[samples.length - 1].timeMs) {
|
||||
return { cx: samples[samples.length - 1].cx, cy: samples[samples.length - 1].cy };
|
||||
}
|
||||
|
||||
let lo = 0;
|
||||
let hi = samples.length - 1;
|
||||
while (lo < hi - 1) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (samples[mid].timeMs <= timeMs) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
|
||||
const a = samples[lo];
|
||||
const b = samples[hi];
|
||||
const span = b.timeMs - a.timeMs;
|
||||
if (span <= 0) return { cx: a.cx, cy: a.cy };
|
||||
|
||||
const t = (timeMs - a.timeMs) / span;
|
||||
return {
|
||||
cx: a.cx + (b.cx - a.cx) * t,
|
||||
cy: a.cy + (b.cy - a.cy) * t,
|
||||
};
|
||||
}
|
||||
|
||||
function findLatestSample(samples: CursorTelemetryPoint[], timeMs: number) {
|
||||
if (samples.length === 0) return null;
|
||||
|
||||
let lo = 0;
|
||||
let hi = samples.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = Math.ceil((lo + hi) / 2);
|
||||
if (samples[mid].timeMs <= timeMs) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return samples[lo]?.timeMs <= timeMs ? samples[lo] : null;
|
||||
}
|
||||
|
||||
function findLatestInteractionSample(samples: CursorTelemetryPoint[], timeMs: number) {
|
||||
for (let index = samples.length - 1; index >= 0; index -= 1) {
|
||||
const sample = samples[index];
|
||||
if (sample.timeMs > timeMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
sample.interactionType === "click" ||
|
||||
sample.interactionType === "double-click" ||
|
||||
sample.interactionType === "right-click" ||
|
||||
sample.interactionType === "middle-click"
|
||||
) {
|
||||
return sample;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findLatestStableCursorType(samples: CursorTelemetryPoint[], timeMs: number) {
|
||||
// Binary search to find position at timeMs, then scan backwards
|
||||
let lo = 0;
|
||||
let hi = samples.length - 1;
|
||||
while (lo < hi) {
|
||||
const mid = Math.ceil((lo + hi) / 2);
|
||||
if (samples[mid].timeMs <= timeMs) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan backwards from the position to find a sample with cursorType
|
||||
// Skip click events only (not mouseup) to avoid transient re-type during clicks
|
||||
for (let index = lo; index >= 0; index -= 1) {
|
||||
const sample = samples[index];
|
||||
if (sample.timeMs > timeMs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sample.cursorType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
sample.interactionType === "click" ||
|
||||
sample.interactionType === "double-click" ||
|
||||
sample.interactionType === "right-click" ||
|
||||
sample.interactionType === "middle-click"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return sample.cursorType;
|
||||
}
|
||||
|
||||
return findLatestSample(samples, timeMs)?.cursorType ?? "arrow";
|
||||
}
|
||||
|
||||
function getCursorViewportScale(viewport: CursorViewportRect) {
|
||||
return Math.max(MIN_CURSOR_VIEWPORT_SCALE, viewport.width / REFERENCE_WIDTH);
|
||||
}
|
||||
|
||||
function getCursorVisualState(samples: CursorTelemetryPoint[], timeMs: number) {
|
||||
const latestClick = findLatestInteractionSample(samples, timeMs);
|
||||
const interactionType = latestClick?.interactionType;
|
||||
const ageMs = latestClick ? Math.max(0, timeMs - latestClick.timeMs) : Number.POSITIVE_INFINITY;
|
||||
const isClickEvent =
|
||||
interactionType === "click" ||
|
||||
interactionType === "double-click" ||
|
||||
interactionType === "right-click" ||
|
||||
interactionType === "middle-click";
|
||||
const clickBounceProgress =
|
||||
latestClick && isClickEvent && ageMs <= CLICK_ANIMATION_MS ? 1 - ageMs / CLICK_ANIMATION_MS : 0;
|
||||
|
||||
return {
|
||||
cursorType: findLatestStableCursorType(samples, timeMs),
|
||||
clickBounceProgress,
|
||||
clickProgress:
|
||||
latestClick && isClickEvent && ageMs <= CLICK_RING_FADE_MS
|
||||
? 1 - ageMs / CLICK_RING_FADE_MS
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a smoothed cursor state that chases the interpolated target.
|
||||
*/
|
||||
export class SmoothedCursorState {
|
||||
public x = 0.5;
|
||||
public y = 0.5;
|
||||
public trail: Array<{ x: number; y: number }> = [];
|
||||
private smoothingFactor: number;
|
||||
private trailLength: number;
|
||||
private initialized = false;
|
||||
private lastTimeMs: number | null = null;
|
||||
private xSpring = createSpringState(0.5);
|
||||
private ySpring = createSpringState(0.5);
|
||||
|
||||
constructor(config: Pick<CursorRenderConfig, "smoothingFactor" | "trailLength">) {
|
||||
this.smoothingFactor = config.smoothingFactor;
|
||||
this.trailLength = config.trailLength;
|
||||
}
|
||||
|
||||
update(targetX: number, targetY: number, timeMs: number): void {
|
||||
if (!this.initialized) {
|
||||
this.x = targetX;
|
||||
this.y = targetY;
|
||||
this.initialized = true;
|
||||
this.lastTimeMs = timeMs;
|
||||
this.xSpring.value = targetX;
|
||||
this.ySpring.value = targetY;
|
||||
this.xSpring.velocity = 0;
|
||||
this.ySpring.velocity = 0;
|
||||
this.xSpring.initialized = true;
|
||||
this.ySpring.initialized = true;
|
||||
this.trail = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.smoothingFactor <= 0 || (this.lastTimeMs !== null && timeMs < this.lastTimeMs)) {
|
||||
this.snapTo(targetX, targetY, timeMs);
|
||||
return;
|
||||
}
|
||||
|
||||
this.trail.unshift({ x: this.x, y: this.y });
|
||||
if (this.trail.length > this.trailLength) {
|
||||
this.trail.length = this.trailLength;
|
||||
}
|
||||
|
||||
const deltaMs = this.lastTimeMs === null ? 1000 / 60 : Math.max(1, timeMs - this.lastTimeMs);
|
||||
this.lastTimeMs = timeMs;
|
||||
|
||||
const springConfig = getCursorSpringConfig(this.smoothingFactor);
|
||||
this.x = stepSpringValue(this.xSpring, targetX, deltaMs, springConfig);
|
||||
this.y = stepSpringValue(this.ySpring, targetY, deltaMs, springConfig);
|
||||
}
|
||||
|
||||
setSmoothingFactor(smoothingFactor: number): void {
|
||||
this.smoothingFactor = smoothingFactor;
|
||||
}
|
||||
|
||||
snapTo(targetX: number, targetY: number, timeMs: number): void {
|
||||
this.x = targetX;
|
||||
this.y = targetY;
|
||||
this.initialized = true;
|
||||
this.lastTimeMs = timeMs;
|
||||
this.xSpring.value = targetX;
|
||||
this.ySpring.value = targetY;
|
||||
this.xSpring.velocity = 0;
|
||||
this.ySpring.velocity = 0;
|
||||
this.xSpring.initialized = true;
|
||||
this.ySpring.initialized = true;
|
||||
this.trail = [];
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.initialized = false;
|
||||
this.lastTimeMs = null;
|
||||
this.trail = [];
|
||||
resetSpringState(this.xSpring, this.x);
|
||||
resetSpringState(this.ySpring, this.y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawClickRing(graphics: Graphics, px: number, py: number, h: number, progress: number) {
|
||||
void graphics;
|
||||
void px;
|
||||
void py;
|
||||
void h;
|
||||
void progress;
|
||||
}
|
||||
|
||||
export class PixiCursorOverlay {
|
||||
public readonly container: Container;
|
||||
private clickRingGraphics: Graphics;
|
||||
private cursorShadowSprites: Partial<Record<CursorAssetKey, Sprite>>;
|
||||
private cursorShadowFilters: Partial<Record<CursorAssetKey, BlurFilter>>;
|
||||
private cursorSprites: Partial<Record<CursorAssetKey, Sprite>>;
|
||||
private cursorMotionBlurFilter: MotionBlurFilter;
|
||||
private state: SmoothedCursorState;
|
||||
private config: CursorRenderConfig;
|
||||
private lastRenderedPoint: { px: number; py: number } | null = null;
|
||||
private lastRenderedTimeMs: number | null = null;
|
||||
|
||||
constructor(config: Partial<CursorRenderConfig> = {}) {
|
||||
this.config = { ...DEFAULT_CURSOR_CONFIG, ...config };
|
||||
this.state = new SmoothedCursorState(this.config);
|
||||
|
||||
this.container = new Container();
|
||||
this.container.label = "cursor-overlay";
|
||||
|
||||
this.clickRingGraphics = new Graphics();
|
||||
this.cursorShadowSprites = {};
|
||||
this.cursorShadowFilters = {};
|
||||
this.cursorSprites = {};
|
||||
for (const key of getAvailableCursorKeys()) {
|
||||
const asset = getCursorAsset(key);
|
||||
const shadowSprite = new Sprite(asset.texture);
|
||||
shadowSprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
shadowSprite.visible = false;
|
||||
shadowSprite.tint = CURSOR_SHADOW_COLOR;
|
||||
shadowSprite.alpha = CURSOR_SHADOW_ALPHA;
|
||||
const shadowFilter = new BlurFilter();
|
||||
shadowFilter.blur = CURSOR_SHADOW_BLUR;
|
||||
shadowFilter.quality = 4;
|
||||
shadowFilter.padding = CURSOR_SHADOW_PADDING;
|
||||
shadowSprite.filters = [shadowFilter];
|
||||
this.cursorShadowSprites[key] = shadowSprite;
|
||||
this.cursorShadowFilters[key] = shadowFilter;
|
||||
|
||||
const sprite = new Sprite(asset.texture);
|
||||
sprite.anchor.set(asset.anchorX, asset.anchorY);
|
||||
sprite.visible = false;
|
||||
this.cursorSprites[key] = sprite;
|
||||
}
|
||||
|
||||
this.cursorMotionBlurFilter = new MotionBlurFilter([0, 0], 5, 0);
|
||||
this.container.filters = null;
|
||||
|
||||
this.container.addChild(
|
||||
this.clickRingGraphics,
|
||||
...Object.values(this.cursorShadowSprites),
|
||||
...Object.values(this.cursorSprites),
|
||||
);
|
||||
this.setMotionBlur(this.config.motionBlur);
|
||||
}
|
||||
|
||||
setDotRadius(dotRadius: number) {
|
||||
this.config.dotRadius = dotRadius;
|
||||
}
|
||||
|
||||
setSmoothingFactor(smoothingFactor: number) {
|
||||
this.config.smoothingFactor = smoothingFactor;
|
||||
this.state.setSmoothingFactor(smoothingFactor);
|
||||
}
|
||||
|
||||
setMotionBlur(motionBlur: number) {
|
||||
this.config.motionBlur = Math.max(0, motionBlur);
|
||||
this.container.filters = this.config.motionBlur > 0 ? [this.cursorMotionBlurFilter] : null;
|
||||
if (this.config.motionBlur <= 0) {
|
||||
this.cursorMotionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
this.cursorMotionBlurFilter.kernelSize = 5;
|
||||
this.cursorMotionBlurFilter.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
setClickBounce(clickBounce: number) {
|
||||
this.config.clickBounce = Math.max(0, clickBounce);
|
||||
}
|
||||
|
||||
update(
|
||||
samples: CursorTelemetryPoint[],
|
||||
timeMs: number,
|
||||
viewport: CursorViewportRect,
|
||||
visible: boolean,
|
||||
freeze = false,
|
||||
): void {
|
||||
if (!visible || samples.length === 0 || viewport.width <= 0 || viewport.height <= 0) {
|
||||
this.container.visible = false;
|
||||
this.lastRenderedPoint = null;
|
||||
this.lastRenderedTimeMs = null;
|
||||
this.cursorMotionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
return;
|
||||
}
|
||||
|
||||
const target = interpolateCursorPosition(samples, timeMs);
|
||||
if (!target) {
|
||||
this.container.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const sameFrameTime =
|
||||
this.lastRenderedTimeMs !== null && Math.abs(this.lastRenderedTimeMs - timeMs) < 0.0001;
|
||||
const hasTimeDiscontinuity =
|
||||
this.lastRenderedTimeMs !== null &&
|
||||
Math.abs(timeMs - this.lastRenderedTimeMs) > CURSOR_TIME_DISCONTINUITY_MS;
|
||||
|
||||
if (freeze || hasTimeDiscontinuity) {
|
||||
if (!sameFrameTime || !this.lastRenderedPoint) {
|
||||
this.state.snapTo(target.cx, target.cy, timeMs);
|
||||
}
|
||||
} else {
|
||||
this.state.update(target.cx, target.cy, timeMs);
|
||||
}
|
||||
this.container.visible = true;
|
||||
|
||||
const px = viewport.x + this.state.x * viewport.width;
|
||||
const py = viewport.y + this.state.y * viewport.height;
|
||||
const h = this.config.dotRadius * getCursorViewportScale(viewport);
|
||||
const { cursorType, clickBounceProgress, clickProgress } = getCursorVisualState(
|
||||
samples,
|
||||
timeMs,
|
||||
);
|
||||
const spriteKey = (cursorType in this.cursorSprites ? cursorType : "arrow") as CursorAssetKey;
|
||||
const asset = getCursorAsset(spriteKey);
|
||||
const shadowSprite = this.cursorShadowSprites[spriteKey] ?? this.cursorShadowSprites.arrow!;
|
||||
const sprite = this.cursorSprites[spriteKey] ?? this.cursorSprites.arrow!;
|
||||
const bounceScale = Math.max(
|
||||
0.72,
|
||||
1 - Math.sin(clickBounceProgress * Math.PI) * (0.08 * this.config.clickBounce),
|
||||
);
|
||||
const scaledH = h;
|
||||
|
||||
this.clickRingGraphics.clear();
|
||||
drawClickRing(this.clickRingGraphics, px, py, h, clickProgress);
|
||||
|
||||
for (const [key, currentShadowSprite] of Object.entries(this.cursorShadowSprites) as Array<
|
||||
[CursorAssetKey, Sprite]
|
||||
>) {
|
||||
currentShadowSprite.visible = key === spriteKey;
|
||||
}
|
||||
|
||||
for (const [key, currentSprite] of Object.entries(this.cursorSprites) as Array<
|
||||
[CursorAssetKey, Sprite]
|
||||
>) {
|
||||
currentSprite.visible = key === spriteKey;
|
||||
}
|
||||
|
||||
if (shadowSprite) {
|
||||
shadowSprite.height = scaledH * bounceScale;
|
||||
shadowSprite.width = scaledH * bounceScale * asset.aspectRatio;
|
||||
shadowSprite.position.set(px + CURSOR_SHADOW_OFFSET_X, py + CURSOR_SHADOW_OFFSET_Y);
|
||||
}
|
||||
|
||||
if (sprite) {
|
||||
sprite.alpha = this.config.dotAlpha;
|
||||
sprite.height = scaledH * bounceScale;
|
||||
sprite.width = scaledH * bounceScale * asset.aspectRatio;
|
||||
sprite.position.set(px, py);
|
||||
}
|
||||
|
||||
this.applyCursorMotionBlur(px, py, timeMs, freeze);
|
||||
this.lastRenderedPoint = { px, py };
|
||||
this.lastRenderedTimeMs = timeMs;
|
||||
}
|
||||
|
||||
private applyCursorMotionBlur(px: number, py: number, timeMs: number, freeze: boolean) {
|
||||
if (
|
||||
freeze ||
|
||||
this.config.motionBlur <= 0 ||
|
||||
!this.lastRenderedPoint ||
|
||||
this.lastRenderedTimeMs === null
|
||||
) {
|
||||
this.cursorMotionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
this.cursorMotionBlurFilter.kernelSize = 5;
|
||||
this.cursorMotionBlurFilter.offset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaMs = Math.max(1, timeMs - this.lastRenderedTimeMs);
|
||||
const dx = px - this.lastRenderedPoint.px;
|
||||
const dy = py - this.lastRenderedPoint.py;
|
||||
const velocityScale =
|
||||
(1000 / deltaMs) * this.config.motionBlur * CURSOR_MOTION_BLUR_BASE_MULTIPLIER;
|
||||
const velocity = {
|
||||
x: dx * velocityScale,
|
||||
y: dy * velocityScale,
|
||||
};
|
||||
const magnitude = Math.hypot(velocity.x, velocity.y);
|
||||
|
||||
this.cursorMotionBlurFilter.velocity = magnitude > 0.05 ? velocity : { x: 0, y: 0 };
|
||||
this.cursorMotionBlurFilter.kernelSize = magnitude > 3 ? 9 : magnitude > 1 ? 7 : 5;
|
||||
this.cursorMotionBlurFilter.offset = magnitude > 0.5 ? -0.25 : 0;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.state.reset();
|
||||
this.clickRingGraphics.clear();
|
||||
for (const shadowSprite of Object.values(this.cursorShadowSprites)) {
|
||||
shadowSprite.visible = false;
|
||||
shadowSprite.scale.set(1);
|
||||
}
|
||||
for (const sprite of Object.values(this.cursorSprites)) {
|
||||
sprite.visible = false;
|
||||
sprite.scale.set(1);
|
||||
}
|
||||
this.container.visible = false;
|
||||
this.lastRenderedPoint = null;
|
||||
this.lastRenderedTimeMs = null;
|
||||
this.cursorMotionBlurFilter.velocity = { x: 0, y: 0 };
|
||||
this.cursorMotionBlurFilter.kernelSize = 5;
|
||||
this.cursorMotionBlurFilter.offset = 0;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.clickRingGraphics.destroy();
|
||||
for (const shadowFilter of Object.values(this.cursorShadowFilters)) {
|
||||
shadowFilter.destroy();
|
||||
}
|
||||
this.cursorMotionBlurFilter.destroy();
|
||||
this.container.destroy({ children: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function drawCursorOnCanvas(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
samples: CursorTelemetryPoint[],
|
||||
timeMs: number,
|
||||
viewport: CursorViewportRect,
|
||||
smoothedState: SmoothedCursorState,
|
||||
config: CursorRenderConfig = DEFAULT_CURSOR_CONFIG,
|
||||
): void {
|
||||
if (samples.length === 0 || viewport.width <= 0 || viewport.height <= 0) return;
|
||||
|
||||
const target = interpolateCursorPosition(samples, timeMs);
|
||||
if (!target) return;
|
||||
|
||||
smoothedState.update(target.cx, target.cy, timeMs);
|
||||
|
||||
const px = viewport.x + smoothedState.x * viewport.width;
|
||||
const py = viewport.y + smoothedState.y * viewport.height;
|
||||
const h = config.dotRadius * getCursorViewportScale(viewport);
|
||||
const { cursorType, clickBounceProgress } = getCursorVisualState(samples, timeMs);
|
||||
const spriteKey = (
|
||||
cursorType && loadedCursorAssets[cursorType] ? cursorType : "arrow"
|
||||
) as CursorAssetKey;
|
||||
const asset = getCursorAsset(spriteKey);
|
||||
const bounceScale = Math.max(
|
||||
0.72,
|
||||
1 - Math.sin(clickBounceProgress * Math.PI) * (0.08 * config.clickBounce),
|
||||
);
|
||||
|
||||
ctx.save();
|
||||
ctx.filter = CURSOR_SVG_DROP_SHADOW_FILTER;
|
||||
|
||||
const drawHeight = h * bounceScale;
|
||||
const drawWidth = drawHeight * asset.aspectRatio;
|
||||
const hotspotX = asset.anchorX * drawWidth;
|
||||
const hotspotY = asset.anchorY * drawHeight;
|
||||
ctx.globalAlpha = config.dotAlpha;
|
||||
ctx.drawImage(asset.image, px - hotspotX, py - hotspotY, drawWidth, drawHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { spring } from "motion";
|
||||
|
||||
export interface SpringState {
|
||||
value: number;
|
||||
velocity: number;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export interface SpringConfig {
|
||||
stiffness: number;
|
||||
damping: number;
|
||||
mass: number;
|
||||
restDelta?: number;
|
||||
restSpeed?: number;
|
||||
}
|
||||
|
||||
const CURSOR_SMOOTHING_MIN = 0;
|
||||
const CURSOR_SMOOTHING_MAX = 2;
|
||||
const CURSOR_SMOOTHING_LEGACY_MAX = 0.5;
|
||||
|
||||
export function createSpringState(initialValue = 0): SpringState {
|
||||
return {
|
||||
value: initialValue,
|
||||
velocity: 0,
|
||||
initialized: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetSpringState(state: SpringState, initialValue?: number) {
|
||||
if (typeof initialValue === "number") {
|
||||
state.value = initialValue;
|
||||
}
|
||||
|
||||
state.velocity = 0;
|
||||
state.initialized = false;
|
||||
}
|
||||
|
||||
export function clampDeltaMs(deltaMs: number, fallbackMs = 1000 / 60) {
|
||||
if (!Number.isFinite(deltaMs) || deltaMs <= 0) {
|
||||
return fallbackMs;
|
||||
}
|
||||
|
||||
return Math.min(80, Math.max(1, deltaMs));
|
||||
}
|
||||
|
||||
export function stepSpringValue(
|
||||
state: SpringState,
|
||||
target: number,
|
||||
deltaMs: number,
|
||||
config: SpringConfig,
|
||||
) {
|
||||
const safeDeltaMs = clampDeltaMs(deltaMs);
|
||||
|
||||
if (!state.initialized || !Number.isFinite(state.value)) {
|
||||
state.value = target;
|
||||
state.velocity = 0;
|
||||
state.initialized = true;
|
||||
return state.value;
|
||||
}
|
||||
|
||||
const restDelta = config.restDelta ?? 0.0005;
|
||||
const restSpeed = config.restSpeed ?? 0.02;
|
||||
|
||||
if (Math.abs(target - state.value) <= restDelta && Math.abs(state.velocity) <= restSpeed) {
|
||||
state.value = target;
|
||||
state.velocity = 0;
|
||||
return state.value;
|
||||
}
|
||||
|
||||
const previousValue = state.value;
|
||||
const generator = spring({
|
||||
keyframes: [state.value, target],
|
||||
velocity: state.velocity,
|
||||
stiffness: config.stiffness,
|
||||
damping: config.damping,
|
||||
mass: config.mass,
|
||||
restDelta,
|
||||
restSpeed,
|
||||
});
|
||||
|
||||
const result = generator.next(safeDeltaMs);
|
||||
state.value = result.done ? target : result.value;
|
||||
state.velocity = ((state.value - previousValue) / safeDeltaMs) * 1000;
|
||||
|
||||
if (result.done) {
|
||||
state.velocity = 0;
|
||||
}
|
||||
|
||||
return state.value;
|
||||
}
|
||||
|
||||
export function getCursorSpringConfig(smoothingFactor: number): SpringConfig {
|
||||
const clamped = Math.min(CURSOR_SMOOTHING_MAX, Math.max(CURSOR_SMOOTHING_MIN, smoothingFactor));
|
||||
|
||||
if (clamped <= 0) {
|
||||
return {
|
||||
stiffness: 1000,
|
||||
damping: 100,
|
||||
mass: 1,
|
||||
restDelta: 0.0001,
|
||||
restSpeed: 0.001,
|
||||
};
|
||||
}
|
||||
|
||||
if (clamped <= CURSOR_SMOOTHING_LEGACY_MAX) {
|
||||
const legacyNormalized = Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
0,
|
||||
(clamped - CURSOR_SMOOTHING_MIN) / (CURSOR_SMOOTHING_LEGACY_MAX - CURSOR_SMOOTHING_MIN),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
stiffness: 760 - legacyNormalized * 420,
|
||||
damping: 34 + legacyNormalized * 24,
|
||||
mass: 0.55 + legacyNormalized * 0.45,
|
||||
restDelta: 0.0002,
|
||||
restSpeed: 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
const extendedNormalized = Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
0,
|
||||
(clamped - CURSOR_SMOOTHING_LEGACY_MAX) /
|
||||
(CURSOR_SMOOTHING_MAX - CURSOR_SMOOTHING_LEGACY_MAX),
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
stiffness: 340 - extendedNormalized * 180,
|
||||
damping: 58 + extendedNormalized * 22,
|
||||
mass: 1 + extendedNormalized * 0.35,
|
||||
restDelta: 0.0002,
|
||||
restSpeed: 0.01,
|
||||
};
|
||||
}
|
||||
|
||||
export function getZoomSpringConfig(): SpringConfig {
|
||||
return {
|
||||
stiffness: 320,
|
||||
damping: 40,
|
||||
mass: 0.92,
|
||||
restDelta: 0.0005,
|
||||
restSpeed: 0.015,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import crosshairUrl from "../../../assets/cursors/Cursor=Cross.svg";
|
||||
import arrowUrl from "../../../assets/cursors/Cursor=Default.svg";
|
||||
import closedHandUrl from "../../../assets/cursors/Cursor=Hand-(Grabbing).svg";
|
||||
import openHandUrl from "../../../assets/cursors/Cursor=Hand-(Open).svg";
|
||||
import pointerUrl from "../../../assets/cursors/Cursor=Hand-(Pointing).svg";
|
||||
import resizeNsUrl from "../../../assets/cursors/Cursor=Resize-North-South.svg";
|
||||
import resizeEwUrl from "../../../assets/cursors/Cursor=Resize-West-East.svg";
|
||||
import textUrl from "../../../assets/cursors/Cursor=Text-Cursor.svg";
|
||||
import type { CursorTelemetryPoint } from "../types";
|
||||
|
||||
type CursorAssetKey = NonNullable<CursorTelemetryPoint["cursorType"]>;
|
||||
|
||||
export type UploadedCursorAsset = {
|
||||
url: string;
|
||||
trim: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
fallbackAnchor: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const UPLOADED_CURSOR_SAMPLE_SIZE = 1024;
|
||||
|
||||
export const uploadedCursorAssets: Partial<Record<CursorAssetKey, UploadedCursorAsset>> = {
|
||||
arrow: {
|
||||
url: arrowUrl,
|
||||
trim: { x: 480, y: 435, width: 333, height: 553 },
|
||||
fallbackAnchor: { x: 0.18, y: 0.1 },
|
||||
},
|
||||
text: {
|
||||
url: textUrl,
|
||||
trim: { x: 404, y: 192, width: 247, height: 596 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
pointer: {
|
||||
url: pointerUrl,
|
||||
trim: { x: 352, y: 441, width: 466, height: 583 },
|
||||
fallbackAnchor: { x: 0.37, y: 0.08 },
|
||||
},
|
||||
crosshair: {
|
||||
url: crosshairUrl,
|
||||
trim: { x: 288, y: 288, width: 480, height: 480 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
"open-hand": {
|
||||
url: openHandUrl,
|
||||
trim: { x: 288, y: 188, width: 512, height: 580 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.28 },
|
||||
},
|
||||
"closed-hand": {
|
||||
url: closedHandUrl,
|
||||
trim: { x: 344, y: 365, width: 432, height: 403 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.28 },
|
||||
},
|
||||
"resize-ew": {
|
||||
url: resizeEwUrl,
|
||||
trim: { x: 187, y: 384, width: 669, height: 270 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
"resize-ns": {
|
||||
url: resizeNsUrl,
|
||||
trim: { x: 376, y: 178, width: 271, height: 669 },
|
||||
fallbackAnchor: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
};
|
||||