Split the code into smaller more manageble files

This commit is contained in:
Mathias Malmqvist
2025-08-19 01:18:58 +02:00
committed by dualshock-tools
parent d4ba4a5fdd
commit 42fc94a9a2
30 changed files with 3529 additions and 3210 deletions

View File

@@ -0,0 +1,108 @@
<svg id="controller-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 518" style="width: 80%; height: auto; max-width: 80%;" stroke-width="1">
<g id="Button_infills">
<g id="Mute_infill" transform="translate(0,0.65) scale(0.70)">
<path id="Mute_infill" d="M454.47,468.7c-6.47,0-12.69.04-18.75.14-.92.01-2.18.28-2.96,1.48-.56.85-.83,2.14-.8,3.81.03,2.24.41,3.3,3.67,3.32,4.47.03,8.94.02,13.41.02l5.53-.03c1.83,0,3.67,0,5.5-.01,4.41-.01,8.97-.03,13.46.04,3.68.09,4.03-1.61,4.06-3.62.02-1.47-.23-2.58-.76-3.39-.67-1.03-1.83-1.6-3.26-1.62-6.64-.08-12.98-.12-19.1-.12Z"/>
</g>
<g id="Down_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M177.4,357.17c-1.88,2.05-4.45,3.18-7.24,3.18h-22.91c-2.79,0-5.36-1.13-7.25-3.18-1.88-2.05-2.79-4.71-2.55-7.49l1.25-14.78c.43-5.04,2.71-9.71,6.42-13.16l9.04-8.39c1.21-1.12,2.74-1.68,4.27-1.68s2.98.54,4.18,1.61l9.44,8.45c3.87,3.47,6.24,8.23,6.68,13.4l1.23,14.55c.24,2.78-.67,5.44-2.56,7.49Z"/>
</g>
<g id="PS_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M455.41,437.59c11.37.03,20.87-9.06,21-20.1.14-11.24-9.39-20.97-20.61-21.05-11.23-.08-21,9.57-20.99,20.72,0,11.16,9.32,20.4,20.6,20.43Z"/>
</g>
<g id="Right_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M223.51,284.09v22.91c0,2.79-1.13,5.36-3.18,7.24-2.05,1.89-4.71,2.8-7.49,2.56l-14.55-1.23c-5.17-.44-9.93-2.81-13.39-6.68l-8.46-9.44c-2.17-2.43-2.14-6.06.07-8.45l8.4-9.04c3.44-3.71,8.11-5.99,13.15-6.42l14.78-1.25c2.78-.24,5.44.67,7.49,2.56,2.05,1.88,3.18,4.45,3.18,7.24Z"/>
</g>
<g id="Left_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M140.97,299.45l-8.46,9.44c-3.46,3.87-8.22,6.24-13.39,6.68l-14.55,1.23c-2.78.24-5.44-.67-7.49-2.56-2.05-1.88-3.18-4.45-3.18-7.24v-22.91c0-2.79,1.13-5.36,3.18-7.24,1.84-1.69,4.17-2.6,6.63-2.6.28,0,.57.01.86.04l14.78,1.25c5.04.43,9.71,2.71,13.15,6.42l8.4,9.04c2.21,2.39,2.24,6.02.07,8.45Z"/>
</g>
<g id="Up_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M179.96,241.41l-1.23,14.55c-.44,5.17-2.81,9.93-6.68,13.39l-9.44,8.46c-2.43,2.17-6.06,2.14-8.45-.07l-9.04-8.4c-3.71-3.44-5.99-8.11-6.42-13.15l-1.25-14.78c-.24-2.78.67-5.44,2.55-7.49,1.89-2.05,4.46-3.18,7.25-3.18h22.91c2.79,0,5.36,1.13,7.24,3.18,1.89,2.05,2.8,4.71,2.56,7.49Z"/>
</g>
<g id="Triangle_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M751.45,261.08c15.08-.01,26.75-11.5,26.73-26.31-.02-14.47-12.19-26.62-26.73-26.69-14.57-.07-26.65,11.99-26.66,26.63-.01,14.84,11.65,26.37,26.66,26.36Z"/>
</g>
<g id="Cross_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M750.93,379.05c15.44-.06,27.38-11.64,27.24-26.43-.13-14.28-12.84-26.83-26.99-26.3-14.58.55-26.43,12.2-26.39,26.91.04,14.23,11.83,25.88,26.14,25.82Z"/>
</g>
<g id="Circle_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M787.29,293.47c-.03,14.49,11.88,26.68,26.15,26.75,14.74.08,26.99-11.95,26.95-26.46-.04-14.51-12.12-26.34-27.04-26.48-14.04-.14-26.03,11.91-26.06,26.18Z"/>
</g>
<g id="Square_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M689.1,320.22c14.61.11,27.23-12.13,27.17-26.35-.06-14.09-12.36-26.44-26.54-26.66-14.42-.22-26.52,11.67-26.66,26.18-.13,14.49,11.74,26.73,26.03,26.83Z"/>
</g>
<g id="Options_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M691.58,199.27l-4.27,17.92c-.91,3.82-4.76,6.19-8.58,5.28-3.82-.91-6.19-4.76-5.28-8.58l4.27-17.92c.78-3.27,3.71-5.48,6.93-5.48.54,0,1.1.06,1.65.19,1.85.44,3.42,1.58,4.42,3.2s1.3,3.53.86,5.38Z"/>
</g>
<g id="Create_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M224.03,191.39c.55-.13,1.11-.19,1.65-.19,3.22,0,6.15,2.21,6.93,5.47l4.27,17.93c.44,1.85.13,3.76-.86,5.38-1,1.62-2.57,2.76-4.42,3.2-3.82.91-7.67-1.46-8.58-5.28l-4.27-17.92c-.91-3.82,1.46-7.67,5.28-8.58Z"/>
</g>
<g id="R2_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M800.14,115.88c-1.06,1.16-2.71,1.73-5.05,1.73-.01,0-.03,0-.04-.01-24.46-6.24-52.88-10.2-84.5-11.76h-.12c-4.19.08-7.09-.8-8.55-2.62-1.41-1.74-1.67-4.53-.77-8.29l.03-.15,9.87-67.57c3.29-14.17,13.23-21.12,30.28-21.12,2.57,0,5.31.16,8.22.47,13.16,1.54,22.93,6.87,29.86,16.29,2.6,3.53,4.67,7.72,6.16,12.44,7.81,24.72,11.55,46.61,15.88,71.95l.15.88c.41,3.61-.06,6.25-1.42,7.76Z"/>
</g>
<g id="L2_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M208.41,103.22c-1.47,1.82-4.36,2.7-8.55,2.62h-.12c-31.62,1.56-60.04,5.52-84.5,11.76-.01.01-.03.01-.04.01-2.34,0-4-.57-5.05-1.73-1.36-1.51-1.84-4.15-1.42-7.76l.15-.88c4.33-25.34,8.07-47.23,15.87-71.95,1.49-4.72,3.57-8.91,6.17-12.44,6.93-9.42,16.7-14.75,29.86-16.29,2.9-.31,5.64-.47,8.22-.47,17.05,0,26.98,6.95,30.28,21.12l9.87,67.57.03.15c.89,3.76.63,6.55-.77,8.29Z"/>
</g>
<g id="R1_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M805.8,173.73c-45.97-8.48-86.34-15.03-123.26-19.99,2.9-8.22,9.5-13.32,20.48-15.9,26.02-3.4,53.76,2.2,87.27,17.61.98.44,1.96.95,2.94,1.51,9.2,5.27,12.41,9.42,12.57,16.77Z"/>
</g>
<g id="L1_infill" transform="translate(0,0.65) scale(0.70)">
<path d="M226.89,153.84c-42.76,6.62-84.3,13.1-123.56,19.27.76-6.19,2.09-12.15,27.86-22.29,28.7-10.58,37.26-12.24,70.44-13.64,2.49-.11,4.68,0,6.67.32,9.71,1.57,15.81,6.94,18.59,16.34Z"/>
</g>
</g>
<g id="Outline" transform="translate(0,0.65) scale(0.70)">
<g id="Controller">
<path id="Controller_outline" d="M804.61,172.5s.09.02.13.02c-45.03-8.29-84.58-14.71-120.82-19.6.06,0,.12.02.17.02-.09.21-.14.44-.23.66-1.95.4-4.06.27-6,.85-.01-.01-.04-.03-.06-.05-.04-.13-.09-.26-.14-.39.02,0,.04,0,.07,0-4.87-.52-9.74-1.01-14.62-1.5-4.79-.46-9.6-.91-14.4-1.34h-.01s-.37-.05-.37-.05c-9.11-.83-18.27-1.58-27.45-2.28,0,.04,0,.08,0,.11,6.85.92,13.16,2.58,18.67,6.93,0,0-.01-.01-.02-.02,1.13.1,2.24.2,3.36.3,5.18,1.42,9.39,5.36,10.93,10.49,1.6,5.35,1.3,12.24-.9,20.47l-21.05,109.2c-4.19,26.12-19.01,37.08-42.82,41.56l-143.01.03-128.89-.03c-23.72-4.39-39.07-18.19-42.83-41.64l-2.11-10.93c-.29-2.09-.65-4.23-1.12-6.43l-12.19-62.64-5.57-28.92-.04-.19c-.09-.35-.16-.68-.25-1.03l-1.25-6.42s0-.03-.01-.04c-.67-4.99-.48-9.34.61-12.98,1.44-4.81,5.22-8.56,9.95-10.19,1.68-.14,3.36-.28,5.03-.42-.01,0-.02.02-.04.03.13-.02.26-.04.39-.06,1.51-.96,3.05-1.79,4.52-2.46,4.51-2.11,9.69-3.99,14.78-4.58-8.71.66-17.56,1.37-26.29,2.11l-1.11.09c-9.48.82-18.77,1.68-27.61,2.55l-.46.05s0-.01,0-.02c-.02.03-.03.06-.04.09-1.99.06-3.94-.42-5.98-.72,0-.03,0-.06-.01-.09.03,0,.06,0,.08-.01-41.23,6.38-81.95,12.73-121.09,18.88,0-.03.01-.07.02-.1l-5.76,1.61c-1.39.76-2.77,1.68-4.11,2.75-6.4,5.15-12.04,13.69-17.24,26.11-27.39,64.19-47.54,129.24-59.9,193.34C4.64,462.34-.21,530.22,3.08,597.4c2.41,40.48,7.54,66.82,16.62,85.41,9.85,20.12,24.59,31.39,46.4,35.45l39.76,12.39.33.07h.1c.97.12,1.93.18,2.86.18,13.63,0,18.84-11.41,21.55-19.96,27.65-75.71,49.02-130.11,67.27-171.19,8.52-19.59,24.3-29.11,48.23-29.11,3.5,0,7.23.21,11.11.63h.17c60.68,2.54,123.31,3.82,186.16,3.82,70.29,0,142.26-1.6,213.94-4.76,2.31-.2,4.59-.3,6.77-.3,23.28,0,39.64,11.03,50,33.68,26.25,62.96,48.44,119.86,67.83,173.94,3.28,8.94,9.45,13.47,18.32,13.47,1.52,0,3.14-.14,4.82-.4h.06s45.5-13.11,45.5-13.11c2-.48,4.05-1.09,6.27-1.86l.13-.04.72-.25v-.02c15.47-5.75,26.84-17.21,34.75-35,7.77-17.48,12.58-41.33,15.58-77.35,4.35-57.49-1.58-134.16-16.29-210.35-15.8-81.82-40.06-154.86-68.3-205.65-2.37-4.62-4.05-7.26-5.79-9.11-2.01-2.17-4.26-3.46-7.64-4.35,0-.05-.03.03-.04-.02M654.75,157.28c1.32.12,2.27.2,2.26.19,4.84.46,9.76.95,14.92,1.48-3.75,2.27-6.89,5.18-9.51,8.8-.01-.02-.01-.04-.02-.06-.81,1.03-1.56,2.12-2.23,3.26-.18-2.07-.53-4.04-1.07-5.86-.89-2.98-2.4-5.62-4.35-7.82ZM251.1,157.47l.22-.17.34-.03c-2.05,2.16-3.62,4.8-4.53,7.83-.29.95-.5,1.95-.69,2.98-.3-.5-.5-.83-.51-.82-2.55-3.31-5.76-6.16-9.46-8.41,4.54-.44,9.37-.89,14.63-1.37ZM63.74,711.34c-1.7,0-3.53-.26-5.45-.77l-.46-.09c-15.03-4.97-25.58-14.54-33.18-30.09-8.75-17.89-13.71-43.59-16.08-83.29C2.18,466.43,27.04,334.31,82.48,204.42c5.96-14.26,12.4-22.95,19.68-26.57,36.97-5.81,77.23-12.09,119.66-18.67,12.6,2.24,22,10.48,24.57,21.52.29,2.12.73,4.32,1.3,6.61l5.63,28.91,13.52,70.16c3.04,22.28-4.43,36.44-19.51,54.89-33.86,35.52-63.96,83.16-92.02,145.65-22.57,50.27-43.21,108.54-64.97,183.45-3.79,15.91-8.97,32.96-18.74,38.81-2.41,1.45-4.98,2.16-7.85,2.16ZM804.28,725.31c-1.3.21-2.54.31-3.7.31-6.47,0-10.8-3.22-13.23-9.84-19.36-54.03-41.59-111.02-67.97-174.28-11.21-24.52-29.78-36.96-55.17-36.96-2.28,0-4.64.1-6.97.3-71.45,3.15-143.25,4.75-213.42,4.75-62.79,0-125.39-1.28-185.99-3.8-3.91-.42-7.72-.63-11.32-.63-26.2,0-44.22,10.9-53.58,32.37-18.31,41.24-39.73,95.76-67.43,171.64-3.62,11.38-8.49,16.23-16.28,16.23-.66,0-1.36-.03-2.07-.1l-34.11-10.63c.47-.24.93-.49,1.38-.76,11.51-6.9,17.19-25.2,21.22-42.14,31.82-109.48,78.28-245.56,155.72-326.77l.12-.14c10.32-12.6,15.94-21.99,18.94-31.77.01.02.02.03.03.05l.04-.29c.47-1.24.83-2.6,1.14-4.03,5.82,18.4,22.43,32.57,45.18,34.56l129.25.04,143.13-.03h.24c23.32-2.05,38.12-13.14,44.92-33.72.1.51.2,1.01.32,1.53,3.02,14.37,11.49,26.24,20.3,37l.17.19c81.44,78.96,128.1,212.33,162.99,332.38,5.91,20.1,10.54,31.1,19.1,35.05l-32.95,9.49ZM902.84,602.66c-4.81,57.88-14.43,96.11-47.36,107.83-4.19,1.09-7.37,1.59-9.99,1.59-9.87,0-14.41-6.75-22.08-32.85-22.39-77.03-43.38-136.08-66.05-185.82-29.41-64.53-61.55-113.22-98.27-148.86-16.5-20.18-23.88-35.21-18.4-61.3l20.03-98.96c4.49-15.34,12.4-22.67,27.28-25.27,35.69,4.91,74.62,11.29,119,19.5h.07c5.65,1.12,7.18,2.15,11.82,11.17,60.68,109.11,92.4,301.63,83.95,412.97Z"/>
<g id="Speaker_grill">
<circle cx="420.29" cy="357.71" r="4.82"/>
<circle cx="437.84" cy="357.71" r="4.82"/>
<circle cx="455.38" cy="357.71" r="4.82"/>
<circle cx="472.93" cy="357.71" r="4.82"/>
<circle cx="490.47" cy="358.68" r="4.82"/>
</g>
<path id="L3_surround" d="M340.92,373.72c-12.1-11.94-28.56-18.52-46.36-18.52-1.03,0-2.06.02-3.11.06-16.56.69-32.12,7.92-43.82,20.35-11.43,12.15-17.66,28.1-17.11,43.7-.63,16.4,5.79,32.86,17.63,45.16,12.39,12.87,29.14,19.96,47.17,19.96h.13c17.75-.03,34.16-6.86,46.21-19.24,12.14-12.46,18.58-29.31,18.14-47.45-.41-16.77-7.12-32.41-18.9-44.04ZM296.63,478.55c-.35,0-.7,0-1.06,0-16.24,0-31.34-6.14-42.53-17.29-11.18-11.15-17.33-26.19-17.32-42.36.02-27.96,22.56-58.15,58.94-58.15h.29c16.72.07,32,6.28,43.02,17.47,10.8,10.97,16.65,25.88,16.48,41.98-.4,36.53-29.71,58.36-57.82,58.36Z"/>
<path id="R3_surround" d="M617.17,355.14c-1.39,0-2.81.05-4.21.13-16.38,1.04-31.68,7.93-43.07,19.39-11.63,11.71-18.09,27.2-18.18,43.63-.1,18.38,6.27,34.57,18.42,46.8,12.21,12.3,29.46,19.38,47.3,19.41h.1c16.61,0,32.4-6.7,44.48-18.88,12.21-12.3,19.13-29.12,19-46.1.29-16.6-6.42-33.11-18.41-45.31-12.1-12.31-28.23-19.09-45.43-19.09ZM675.5,419.52c-.02,32.9-25.99,58.85-59.12,59.08h-.4c-15.55,0-30.85-6.54-41.96-17.95-11.2-11.5-17.15-26.51-16.74-42.27.84-32.25,26.93-57.61,59.4-57.74h.24c16.08,0,31.03,6.27,42.11,17.65,10.89,11.19,16.89,26.21,16.48,41.23Z"/>
</g>
<g id="Button_outlines">
<path id="L1" d="M131.54,151.76c28.58-10.54,37.1-12.19,70.13-13.58.62-.03,1.21-.04,1.78-.04,1.7,0,3.23.11,4.69.35,8.92,1.44,14.64,6.2,17.45,14.54-.03,0-.06,0-.08.01,0,.03,0,.06.01.09,2.04.3,3.99.78,5.98.72.02-.03.03-.06.04-.09-3.07-11.83-10.65-18.79-22.53-20.72-1.76-.27-3.6-.41-5.61-.41-.63,0-1.28,0-1.95.04-5.25.22-9.69.44-13.55.68-23.59,1.48-33.63,4.15-58.32,13.26-1.9.75-3.55,1.43-5.01,2.05l-.16.07c-23.9,10.2-24.72,17.05-25.6,24.3-.02.13-.04.25-.05.38l5.76-1.61c.79-5.14,3.38-10.75,27.02-20.05Z"/>
<path id="R1" d="M804.81,159.88c-2.27-2.33-5.34-4.53-9.35-6.83-1.04-.6-2.15-1.17-3.28-1.69-27.77-12.77-52.08-18.98-74.31-18.98-5.21,0-10.43.34-15.56,1.02l-.23.04c-13.31,3.1-21.29,9.84-24.35,20.58-.02,0-.04,0-.07,0,.05.13.1.26.14.39.02.02.05.03.06.05.49-.18,1-.3,1.52-.33,1.52-.11,3.01-.3,4.48-.51.01-.04.03-.08.05-.12.05-.18.11-.36.18-.54-.06,0-.12-.02-.17-.02,2.98-7.21,9.14-11.7,19.28-14.1,4.8-.62,9.73-.94,14.65-.94,21.43,0,44.99,6.05,72.03,18.48.91.41,1.84.89,2.85,1.47,8.44,4.83,11.54,8.54,12.01,14.69-.05,0-.09-.02-.13-.02.06.13.12.25.16.39.02.06.05.12.07.18,1.01.07,1.98.26,2.89.58.87-.31,1.74-.34,2.57-.15-.14-5.55-1.73-9.75-5.49-13.61Z"/>
<path id="L2_outline" d="M115.16,122.1h.4l.52-.07.06-.02c24.15-6.19,52.33-10.12,83.69-11.68h.47c5.39,0,9.3-1.44,11.61-4.29,2.31-2.88,2.86-6.94,1.67-11.99l-9.91-67.79c-3.78-16.37-15.49-24.66-34.81-24.66-2.7,0-5.6.17-8.61.49-14.28,1.66-25.37,7.75-32.97,18.08-2.9,3.94-5.2,8.57-6.83,13.75-7.91,25.05-11.67,47.06-16.02,72.55l-.18,1.03c-.57,5.06.28,8.9,2.55,11.39,1.92,2.12,4.73,3.2,8.34,3.2ZM109.72,108.29l.15-.89c4.32-25.3,8.06-47.16,15.84-71.81,1.46-4.61,3.48-8.7,6.02-12.15,6.76-9.19,16.3-14.38,29.16-15.89,2.88-.31,5.61-.46,8.11-.46,16.53,0,26.12,6.66,29.29,20.26l9.88,67.62.04.18c.81,3.44.62,5.94-.57,7.43-1.21,1.5-3.65,2.25-7.25,2.25h-.69c-31.62,1.56-60.09,5.52-84.61,11.76-1.98-.02-3.35-.48-4.19-1.4-1.15-1.27-1.55-3.69-1.18-6.92Z"/>
<path id="R2_outline" d="M709.98,110.34h.41c31.43,1.56,59.6,5.48,83.74,11.68l.06.02.53.07h.4c3.61,0,6.42-1.08,8.34-3.2,2.27-2.5,3.13-6.33,2.54-11.45l-.17-.97c-4.35-25.49-8.11-47.5-16.02-72.55-1.63-5.18-3.93-9.81-6.83-13.75-7.61-10.33-18.71-16.42-32.99-18.08-3-.33-5.89-.49-8.6-.49-19.32,0-31.03,8.3-34.82,24.74l-9.87,67.63c-1.21,5.13-.66,9.2,1.66,12.08,2.31,2.85,6.22,4.29,11.61,4.29ZM702.09,95.12l.04-.2,9.85-67.49c3.18-13.69,12.77-20.35,29.31-20.35,2.49,0,5.22.16,8.1.46,12.87,1.51,22.41,6.7,29.17,15.89,2.53,3.43,4.55,7.52,6.01,12.15,7.79,24.65,11.52,46.51,15.85,71.82l.14.83c.37,3.3-.03,5.71-1.17,6.97-.85.93-2.22,1.39-4.19,1.4-24.52-6.25-52.99-10.21-84.66-11.77h-.64c-3.56,0-6.07-.77-7.26-2.25-1.2-1.49-1.4-3.99-.56-7.47Z"/>
<path id="Create_outline" d="M218.64,218.94c1.25,5.26,5.9,8.93,11.31,8.93.92,0,1.83-.11,2.69-.32,3.02-.72,5.57-2.57,7.2-5.22,1.63-2.65,2.13-5.76,1.41-8.77l-4.26-17.93c-1.25-5.26-5.9-8.93-11.31-8.93-.9,0-1.81.11-2.7.32-3.02.71-5.57,2.57-7.2,5.21-1.63,2.65-2.13,5.76-1.42,8.78l4.27,17.93ZM220.47,195.11c.86-1.4,2.21-2.37,3.79-2.75.46-.11.93-.16,1.42-.16,2.85,0,5.3,1.93,5.96,4.7l4.27,17.93c.38,1.59.11,3.23-.74,4.62-.86,1.4-2.21,2.38-3.8,2.75-.47.11-.94.17-1.42.17-2.85,0-5.3-1.94-5.96-4.71l-4.27-17.93c-.38-1.59-.11-3.23.75-4.62Z"/>
<path id="Options_outline" d="M677.7,226.85c.91.21,1.82.32,2.69.32,5.4,0,10.05-3.68,11.3-8.94l4.27-17.92c.72-3.02.22-6.15-1.41-8.79-1.63-2.64-4.18-4.5-7.2-5.22-.88-.21-1.79-.31-2.69-.31-5.41,0-10.06,3.67-11.32,8.93l-4.27,17.93c-1.49,6.23,2.38,12.51,8.62,14.01ZM674.43,214.12l4.26-17.92c.66-2.77,3.11-4.71,5.96-4.71.5,0,.96.06,1.42.17,1.59.38,2.94,1.35,3.8,2.74.85,1.38,1.12,3.03.74,4.63l-4.26,17.92c-.66,2.77-3.11,4.71-5.96,4.71-.47,0-.95-.06-1.42-.17-1.59-.38-2.94-1.35-3.8-2.75-.86-1.39-1.12-3.04-.74-4.63Z"/>
<path id="Left_outline" d="M99.81,321.31c1.27.35,2.6.53,3.93.53.42,0,.83-.02,1.25-.06l14.55-1.23c5.53-.47,10.68-2.71,14.76-6.39.68-.6,1.32-1.25,1.94-1.94l8.45-9.44c3.91-4.36,3.85-10.88-.13-15.18l-8.39-9.04c-4.29-4.63-10.11-7.47-16.4-8l-14.78-1.25c-4.13-.36-8.24,1.05-11.29,3.85s-4.8,6.79-4.8,10.93v22.91c0,4.14,1.75,8.12,4.8,10.93,1.75,1.61,3.85,2.76,6.11,3.38ZM93.9,284.09c0-2.79,1.13-5.36,3.18-7.24,1.84-1.69,4.17-2.6,6.63-2.6.28,0,.57.01.86.04l14.78,1.25c5.04.43,9.71,2.71,13.15,6.42l8.4,9.04c2.21,2.39,2.24,6.02.07,8.45l-8.46,9.44c-3.46,3.87-8.22,6.24-13.39,6.68l-14.55,1.23c-2.78.24-5.44-.67-7.49-2.56-2.05-1.88-3.18-4.45-3.18-7.24v-22.91Z"/>
<path id="Right_outline" d="M172.85,287.6c-3.98,4.3-4.04,10.82-.14,15.18l8.27,9.23.19.21c4.32,4.83,10.25,7.78,16.7,8.33l14.55,1.23c.42.04.83.06,1.25.06,1.4,0,2.79-.2,4.12-.58,2.18-.64,4.22-1.76,5.92-3.33,3.05-2.81,4.8-6.79,4.8-10.93v-22.91c0-4.14-1.75-8.13-4.8-10.93-3.05-2.8-7.16-4.2-11.29-3.85l-14.78,1.25c-.74.06-1.48.16-2.2.28-2.7.47-5.27,1.36-7.65,2.64-2.43,1.31-4.64,3.01-6.55,5.08l-8.39,9.04ZM198.06,275.54l14.78-1.25c2.78-.24,5.44.67,7.49,2.56,2.05,1.88,3.18,4.45,3.18,7.24v22.91c0,2.79-1.13,5.36-3.18,7.24-2.05,1.89-4.71,2.8-7.49,2.56l-14.55-1.23c-5.17-.44-9.93-2.81-13.39-6.68l-8.46-9.44c-2.17-2.43-2.14-6.06.07-8.45l8.4-9.04c3.44-3.71,8.11-5.99,13.15-6.42Z"/>
<path id="Down_outline" d="M183.71,334.71c-.13-1.61-.42-3.18-.84-4.71-.05-.16-.1-.32-.15-.47-.22-.68-.45-1.34-.69-2-1.43-3.61-3.69-6.87-6.65-9.52l-9.44-8.46c-4.36-3.9-10.88-3.85-15.18.14l-9.04,8.39c-1.05.97-2,2.01-2.85,3.12-2.94,3.82-4.74,8.41-5.15,13.28l-1.25,14.78c-.15,1.83.03,3.65.54,5.38.62,2.18,1.75,4.21,3.31,5.91,2.8,3.05,6.79,4.8,10.93,4.8h22.91c4.14,0,8.12-1.75,10.93-4.8,2.8-3.05,4.2-7.16,3.85-11.29l-1.23-14.55ZM177.4,357.17c-1.88,2.05-4.45,3.18-7.24,3.18h-22.91c-2.79,0-5.36-1.13-7.25-3.18-1.88-2.05-2.79-4.71-2.55-7.49l1.25-14.78c.43-5.04,2.71-9.71,6.42-13.16l9.04-8.39c1.21-1.12,2.74-1.68,4.27-1.68s2.98.54,4.18,1.61l9.44,8.45c3.87,3.47,6.24,8.23,6.68,13.4l1.23,14.55c.24,2.78-.67,5.44-2.56,7.49Z"/>
<path id="Up_outline" d="M133.72,256.61c.48,5.73,2.88,11.07,6.81,15.22.38.41.78.8,1.19,1.18l9.04,8.39c2.18,2.02,4.92,3.03,7.67,3.03s5.36-.97,7.51-2.9l9.44-8.45c1.35-1.2,2.55-2.53,3.58-3.96,2.51-3.43,4.1-7.44,4.64-11.72.05-.34.08-.68.11-1.02l1.23-14.55c.35-4.13-1.05-8.24-3.85-11.29-2.81-3.05-6.79-4.8-10.93-4.8h-22.91c-4.14,0-8.13,1.75-10.93,4.8-1.53,1.66-2.64,3.64-3.27,5.76-.54,1.77-.74,3.65-.58,5.53l1.25,14.78ZM140,233.92c1.89-2.05,4.46-3.18,7.25-3.18h22.91c2.79,0,5.36,1.13,7.24,3.18,1.89,2.05,2.8,4.71,2.56,7.49l-1.23,14.55c-.44,5.17-2.81,9.93-6.68,13.39l-9.44,8.46c-2.43,2.17-6.06,2.14-8.45-.07l-9.04-8.4c-3.71-3.44-5.99-8.11-6.42-13.15l-1.25-14.78c-.24-2.78.67-5.44,2.55-7.49Z"/>
<path id="Cross_outline" d="M751.82,321.39h-.11c-8.53,0-16.94,3.65-23.08,10.01-5.91,6.13-9.01,13.94-8.72,21.98.63,17.53,13.54,30.1,31.38,30.57h.03c17.47,0,31.35-13.6,31.6-30.96.12-8.05-3.19-16.1-9.08-22.09-5.9-6.01-13.93-9.48-22.02-9.51ZM770.09,370.25c-4.93,5-11.74,7.77-19.16,7.8h-.1c-13.76,0-24.99-11.14-25.03-24.82-.04-13.99,11.13-25.37,25.43-25.91.28-.01.56-.02.84-.02,13.25,0,24.99,11.83,25.12,25.32.06,6.67-2.46,12.93-7.09,17.63Z"/>
<path id="Triangle_outline" d="M751.06,266c.21,0,.41,0,.62,0,8.36,0,16.79-3.45,22.54-9.22,5.71-5.73,8.72-13.49,8.69-22.44-.05-17.11-14.19-31.03-31.52-31.03h-.19c-17.15.1-31.24,14.15-31.4,31.3-.08,8.08,3.16,15.85,9.1,21.87,5.94,6.02,14.02,9.48,22.16,9.51ZM751.32,209.08h.12c13.93.07,25.71,11.83,25.73,25.69.01,6.89-2.62,13.27-7.4,17.98-4.79,4.71-11.3,7.31-18.35,7.32-6.99,0-13.47-2.6-18.25-7.33-4.77-4.72-7.4-11.13-7.39-18.03.01-14.13,11.47-25.63,25.53-25.63Z"/>
<path id="Circle_outline" d="M791.23,272.06c-5.72,5.82-8.79,13.5-8.65,21.62.31,17.85,13.17,30.72,31.27,31.3.3,0,.6.01.9.01,16.35,0,30.24-14.13,30.33-30.86.05-8.44-3.12-16.31-8.92-22.14-5.91-5.94-13.96-9.21-22.68-9.21-8.47,0-16.37,3.3-22.25,9.28ZM839.39,293.77c.02,6.64-2.62,12.96-7.44,17.78-4.93,4.95-11.46,7.67-18.37,7.67,0,0-.14,0-.14,0-13.66-.07-25.18-11.87-25.15-25.75.03-13.65,11.4-25.19,24.83-25.19h.23c14.33.14,26.01,11.57,26.05,25.49Z"/>
<path id="Square_outline" d="M691.99,262.62c-.79-.06-1.6-.09-2.38-.09-8.46,0-16.39,3.35-22.34,9.45-5.74,5.88-8.93,13.76-8.74,21.58-.29,17.51,12.78,31.02,30.42,31.42.26,0,.52,0,.78,0,17.09,0,30.37-12.98,30.9-30.18.51-16.54-12.34-30.97-28.64-32.18ZM707.82,311.38c-4.97,4.99-11.72,7.85-18.53,7.85,0,0-.18,0-.18,0-6.58-.05-12.82-2.74-17.59-7.59-4.87-4.95-7.51-11.43-7.45-18.24.13-13.89,11.46-25.19,25.26-25.19h.38c13.56.21,25.5,12.2,25.56,25.67.03,6.44-2.62,12.65-7.44,17.5Z"/>
<path id="PS_outline" d="M455.4,442.49h.21c14.28-.12,25.41-11.31,25.35-25.47-.06-14.05-11.44-25.51-25.42-25.54-13.74,0-25.39,11.7-25.44,25.55-.03,6.72,2.63,13.09,7.46,17.95,4.82,4.84,11.16,7.51,17.84,7.51ZM441.75,403.34c3.78-3.75,8.85-5.9,13.92-5.9h.13c5.08.04,10.12,2.23,13.84,6.02,3.73,3.8,5.84,8.91,5.78,14.02-.13,10.54-9.08,19.11-20,19.11h0c-10.81-.02-19.6-8.74-19.6-19.43,0-5.03,2.17-10.07,5.94-13.82Z"/>
<path id="Mute_outline" d="M474.24,464.3c-6.6-.06-13.03-.1-19.11-.1-6.51,0-12.67.04-18.82.11-2.55.03-4.67.99-6.15,2.77-1.4,1.7-2.12,4.04-2.07,6.78.07,3.52,1.54,7.73,8.15,7.76,2.64.01,5.27.02,7.91.02h11.06s1.74-.03,1.74-.03c0,0,7.44-.02,9.31-.02,2.62,0,5.25,0,7.86.04h.17c7.21,0,8.38-5.01,8.43-8,.05-2.57-.65-4.8-2.03-6.45-1.54-1.85-3.77-2.85-6.45-2.88ZM473.79,476.4h-2.19c-2.66-.05-2.76-.04-5.42-.04-1.84,0-10.97.03-10.97.03l-5.52.04h-5.9c-2.5,0-5,0-7.5-.02-.93,0-2.31-.66-2.33-2.29-.02-1.53-.18-2.63.18-3.31.57-1.06,1.65-1.24,2.25-1.25,6.08-.11,12.21-.17,18.74-.17,6.06,0,12.48.05,19.09.15,1.14.02,2.07.53,2.55,1.42.21.4.5,1.12.47,2.82-.02,1.47-.69,2.61-3.43,2.61Z"/>
</g>
</g>
<g transform="translate(0,0.65) scale(0.70)">
<g id="Trackpad_infill" >
<path d="M452.27,328.53v-.02c40.22,0,80.43.07,120.65-.07,6.47-.02,13.17.01,19.36-1.58,19.32-4.99,29.59-18.18,33.51-36.6,7.44-34.93,15.37-69.99,21.95-105.09,1.8-9.6-.91-20.7-9.35-26.91-6.53-4.8-15.07-5.38-22.94-6-15.84-1.24-31.65-1.69-47.52-2.33-25.29-1.01-52.32-3.71-77.62-4-59.86-.67-117.65,1.12-177.41,5.09-10.18.68-20.91.48-30.97,2.12-12.6,2.05-25.72,12.82-22.5,31.32,2.97,17.08,7.65,34.26,11,51.28,3.72,18.85,7.61,37.68,11.08,56.58,2.33,12.67,8.64,23.23,19.04,30.56,9.97,7.03,21.64,5.61,33.14,5.62,39.53.06,79.06.02,118.6.02Z"/>
</g>
<g id="Trackpad_outline" >
<path d="M272.25,153.56c-4.52,2.07-9.76,5.6-12.42,9.85-4.66,6.89-3.71,15.95-1.11,26.9l19.74,98.37c4.52,27.51,18.61,41.63,45.84,43.95h.12l235.94.06c-4.66-.08,4.33,0,9.56,0,35.3,0,49.5-11.1,56.07-37.73l20.44-101.35c4.26-16.93,3.93-29.59-6.87-37.71-6.24-4.92-13.49-6.41-21.4-7.26-52.97-4-107.12-6.03-161.4-6.03-.3,0-.6,0-.9,0-2.3-.01-4.58-.02-6.81-.02-.36,0-.72,0-1.08,0-.33,0-.66,0-1,0-3.06.02-6.18.05-9.34.09-48.92.43-98.66,2.47-148.15,6.11-5.85.15-12,2.31-17.24,4.76ZM641.07,192.4l-20.45,101.35c-6.01,24.35-18.98,33.44-50.41,33.44-3.02,0-6.24.2-9.64,0l-235.88-.06c-24.7-2.11-36.7-14.33-40.82-39.44l-19.79-98.56c-2.23-9.46-3.46-17.08.31-22.64,4.07-6.04,11.93-10.09,23.26-12.03,13.9-1.04,27.83-1.95,41.74-2.73,35.06-1.79,70.63-3.02,101.96-3.47.19,0,.38,0,.58,0,1.55-.02,3.08-.04,4.6-.06,6.53-.06,13.04-.09,19.54-.09,46.61.2,104.83,2.34,158.55,5.8,3.45.26,7.4.89,10.36,1.77,18.01,5.87,21.08,16.85,16.09,36.73Z"/>
</g>
</g>
<g id="L3" transform="translate(0,0.65) scale(0.70)">
<g id="L3_infill">
<path d="M295.63,461.03c23.36,0,41.53-17.87,41.57-40.86.03-23.53-18.65-42.27-42.16-42.27-23.09,0-41.82,18.55-41.83,41.45-.01,23.9,18.09,41.69,42.42,41.69Z"/>
</g>
<g id="L3_outline">
<path d="M295.14,466.67c-26.36-.18-47.62-21.47-47.16-47.19.51-28.21,21.76-46.51,47.28-47.04,25.52-.53,47.65,22.07,47.37,47.25-.29,26.05-21.63,47.15-47.5,46.98ZM295.63,461.03c23.36,0,41.53-17.87,41.57-40.86.03-23.53-18.65-42.27-42.16-42.27-23.09,0-41.82,18.55-41.83,41.45-.01,23.9,18.09,41.69,42.42,41.69Z"/>
</g>
</g>
<g id="R3" transform="translate(0,0.65) scale(0.70)">
<g id="R3_infill">
<path d="M658.23,419.7c.55-22.76-17.76-41.19-40.35-41.72-23.43-.55-42.68,18.19-43.29,40.74-.65,23.92,18.7,41.15,39.11,42.46,25.96,1.66,44.96-18.72,44.52-41.48Z"/>
</g>
<g id="R3_outline">
<path d="M664.07,419.78c-.01,26.37-21.16,47.39-47.6,47.32-26.19-.06-48.19-21.58-47.62-47.5.59-26.82,21.12-47.52,48.44-47.45,26.83.08,46.79,21.47,46.78,47.62ZM658.23,419.7c.55-22.76-17.76-41.19-40.35-41.72-23.43-.55-42.68,18.19-43.29,40.74-.65,23.92,18.7,41.15,39.11,42.46,25.96,1.66,44.96-18.72,44.52-41.48Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

27
assets/icons.svg Normal file
View File

@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="paypal" viewBox="0 0 384 512">
<path fill="#ffffff" d="M111.4 295.9c-3.5 19.2-17.4 108.7-21.5 134-.3 1.8-1 2.5-3 2.5H12.3c-7.6 0-13.1-6.6-12.1-13.9L58.8 46.6c1.5-9.6 10.1-16.9 20-16.9 152.3 0 165.1-3.7 204 11.4 60.1 23.3 65.6 79.5 44 140.3-21.5 62.6-72.5 89.5-140.1 90.3-43.4 .7-69.5-7-75.3 24.2zM357.1 152c-1.8-1.3-2.5-1.8-3 1.3-2 11.4-5.1 22.5-8.8 33.6-39.9 113.8-150.5 103.9-204.5 103.9-6.1 0-10.1 3.3-10.9 9.4-22.6 140.4-27.1 169.7-27.1 169.7-1 7.1 3.5 12.9 10.6 12.9h63.5c8.6 0 15.7-6.3 17.4-14.9 .7-5.4-1.1 6.1 14.4-91.3 4.6-22 14.3-19.7 29.3-19.7 71 0 126.4-28.8 142.9-112.3 6.5-34.8 4.6-71.4-23.8-92.6z"/>
</symbol>
<symbol id="ethereum" viewBox="0 0 320 512">
<path fill="#ffffff" d="M311.9 260.8L160 353.6 8 260.8 160 0l151.9 260.8zM160 383.4L8 290.6 160 512l152-221.4-152 92.8z"/>
</symbol>
<symbol id="info" viewBox="0 -860 960 960">
<path d="M440-280h80v-240h-80v240Zm40-320q17 0 28.5-11.5T520-640q0-17-11.5-28.5T480-680q-17 0-28.5 11.5T440-640q0 17 11.5 28.5T480-600Zm0 520q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
</symbol>
<symbol id="discord" viewBox="0 0 640 512">
<path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/>
</symbol>
<symbol id="mail" viewBox="0 0 512 512">
<path d="M48 64C21.5 64 0 85.5 0 112c0 15.1 7.1 29.3 19.2 38.4L236.8 313.6c11.4 8.5 27 8.5 38.4 0L492.8 150.4c12.1-9.1 19.2-23.3 19.2-38.4c0-26.5-21.5-48-48-48H48zM0 176V384c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V176L294.4 339.2c-22.8 17.1-54 17.1-76.8 0L0 176z"/>
</symbol>
<symbol id="github" viewBox="0 0 496 512">
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/>
</symbol>
<symbol id="mug" viewBox="0 0 512 512">
<path fill="#ffffff" d="M88 0C74.7 0 64 10.7 64 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C120.5 112.3 128 119.9 128 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C119.5 47.7 112 40.1 112 24c0-13.3-10.7-24-24-24zM32 192c-17.7 0-32 14.3-32 32V416c0 53 43 96 96 96H288c53 0 96-43 96-96h16c61.9 0 112-50.1 112-112s-50.1-112-112-112H352 32zm352 64h16c26.5 0 48 21.5 48 48s-21.5 48-48 48H384V256zM224 24c0-13.3-10.7-24-24-24s-24 10.7-24 24c0 38.9 23.4 59.4 39.1 73.1l1.1 1C232.5 112.3 240 119.9 240 136c0 13.3 10.7 24 24 24s24-10.7 24-24c0-38.9-23.4-59.4-39.1-73.1l-1.1-1C231.5 47.7 224 40.1 224 24z"/>
</symbol>
<symbol id="lang" viewBox="0 0 640 512">
<path d="M0 128C0 92.7 28.7 64 64 64H256h48 16H576c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H320 304 256 64c-35.3 0-64-28.7-64-64V128zm320 0V384H576V128H320zM178.3 175.9c-3.2-7.2-10.4-11.9-18.3-11.9s-15.1 4.7-18.3 11.9l-64 144c-4.5 10.1 .1 21.9 10.2 26.4s21.9-.1 26.4-10.2l8.9-20.1h73.6l8.9 20.1c4.5 10.1 16.3 14.6 26.4 10.2s14.6-16.3 10.2-26.4l-64-144zM160 233.2L179 276H141l19-42.8zM448 164c11 0 20 9 20 20v4h44 16c11 0 20 9 20 20s-9 20-20 20h-2l-1.6 4.5c-8.9 24.4-22.4 46.6-39.6 65.4c.9 .6 1.8 1.1 2.7 1.6l18.9 11.3c9.5 5.7 12.5 18 6.9 27.4s-18 12.5-27.4 6.9l-18.9-11.3c-4.5-2.7-8.8-5.5-13.1-8.5c-10.6 7.5-21.9 14-34 19.4l-3.6 1.6c-10.1 4.5-21.9-.1-26.4-10.2s.1-21.9 10.2-26.4l3.6-1.6c6.4-2.9 12.6-6.1 18.5-9.8l-12.2-12.2c-7.8-7.8-7.8-20.5 0-28.3s20.5-7.8 28.3 0l14.6 14.6 .5 .5c12.4-13.1 22.5-28.3 29.8-45H448 376c-11 0-20-9-20-20s9-20 20-20h52v-4c0-11 9-20 20-20z"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

1856
core.js

File diff suppressed because it is too large Load Diff

116
css/finetune.css Normal file
View File

@@ -0,0 +1,116 @@
/* Styles for fine-tuning interface */
/* Styling for coordinate labels - base state to prevent layout shift */
#finetuneStickCanvasLx-lbl,
#finetuneStickCanvasLy-lbl,
#finetuneStickCanvasRx-lbl,
#finetuneStickCanvasRy-lbl {
padding: 2px 4px !important;
border-radius: 3px !important;
background-color: transparent !important;
}
/* Styling for finetune input boxes - base state to prevent layout shift */
input[id^="finetune"] {
border: 1px solid transparent !important;
width: 90px !important;
min-width: 90px !important;
color: #969696 !important;
}
/* Styling for highlighted coordinate labels */
#finetuneStickCanvasLx-lbl.text-primary,
#finetuneStickCanvasLy-lbl.text-primary,
#finetuneStickCanvasRx-lbl.text-primary,
#finetuneStickCanvasRy-lbl.text-primary {
color: #0d6efd !important;
background-color: rgba(13, 110, 253, 0.1) !important;
}
/* CSS Grid layout for finetune inputs around canvas */
.finetune-grid {
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
". top ."
"left center right"
". bottom .";
justify-items: center;
align-items: center;
width: 100%;
margin: 0 auto;
max-width: fit-content;
}
.finetune-top {
grid-area: top;
}
.finetune-left {
grid-area: left;
}
.finetune-center {
grid-area: center;
}
.finetune-right {
grid-area: right;
}
.finetune-bottom {
grid-area: bottom;
}
/* Finetune mode visibility controls */
.finetune-center-mode {
display: block;
}
.finetune-circularity-mode {
display: none;
}
/* When circularity mode is active */
#finetuneModal.circularity-mode .finetune-center-mode {
display: none;
}
#finetuneModal.circularity-mode .finetune-circularity-mode {
display: block;
}
/* Hide raw numbers mode - hide input boxes when checkbox is unchecked */
#finetuneModal.hide-raw-numbers .finetune-top,
#finetuneModal.hide-raw-numbers .finetune-left,
#finetuneModal.hide-raw-numbers .finetune-right,
#finetuneModal.hide-raw-numbers .finetune-bottom {
display: none;
}
/* Adjust grid layout when raw numbers are hidden - center the canvas */
#finetuneModal.hide-raw-numbers .finetune-grid {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas: "center";
}
/* when element with id finetuneModal has class hide-raw-numbers, hide all elements with id finetuneStickCanvasL and finetuneStickCanvasR */
#finetuneModal.hide-raw-numbers #finetuneStickCanvasL,
#finetuneModal.hide-raw-numbers #finetuneStickCanvasR {
display: none;
}
#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasL,
#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasR {
display: block;
}
#finetuneModal.hide-raw-numbers #finetuneStickCanvasL_large,
#finetuneModal.hide-raw-numbers #finetuneStickCanvasR_large
{
display: block;
}
#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasL_large,
#finetuneModal:not(.hide-raw-numbers) #finetuneStickCanvasR_large {
display: none;
}

23
css/main.css Normal file
View File

@@ -0,0 +1,23 @@
/* Main styles for DualShock Calibration GUI */
dl.row dt {
font-weight: normal;
}
dl.row dd {
font-family: monospace;
}
#left-stick-card,
#right-stick-card {
cursor: pointer;
}
.stick-card-active {
border: 1px solid #0d6efd !important;
box-shadow: 0 0 10px rgba(13, 110, 253, 0.3) !important;
}
.stick-card-active .card-header {
background-color: #0d6efd !important;
color: white !important;
}

1315
index.html

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
'use strict';
import { sleep, la } from '../utils.js';
import { sleep, la } from './utils.js';
/**
* Controller Manager - Manages the current controller instance and provides unified interface
@@ -8,7 +8,8 @@ import { sleep, la } from '../utils.js';
class ControllerManager {
constructor(uiDependencies = {}) {
this.currentController = null;
this.l = uiDependencies.l || ((text) => text); // fallback to identity function
this.l = uiDependencies.l;
this.handleNvStatusUpdate = uiDependencies.handleNvStatusUpdate;
this.has_changes_to_write = null;
this.inputHandler = null; // Callback function for input processing
@@ -66,6 +67,10 @@ class ControllerManager {
return await this.currentController.getInfo();
}
getFinetuneMaxValue() {
return this.currentController.getFinetuneMaxValue();
}
/**
* Set input report handler on the underlying device
* @param {Function|null} handler Input report handler function or null to clear
@@ -79,7 +84,9 @@ class ControllerManager {
* @returns {Promise<Object>} NVS status object
*/
async queryNvStatus() {
return await this.currentController.queryNvStatus();
const nv = await this.currentController.queryNvStatus();
this.handleNvStatusUpdate(nv);
return nv;
}
/**
@@ -98,8 +105,8 @@ class ControllerManager {
await this.currentController.writeFinetuneData(data);
}
controllerType() {
return this.currentController.getType();
getModel() {
return this.currentController.getModel();
}
/**
@@ -130,21 +137,19 @@ class ControllerManager {
/**
* Update NVS changes status and UI
* @param {boolean} new_value Changes status
* @param {boolean} hasChanges Changes status
*/
setHasChangesToWrite(new_value) {
if (new_value === this.has_changes_to_write)
setHasChangesToWrite(hasChanges) {
if (hasChanges === this.has_changes_to_write)
return;
if (new_value == true) {
$("#savechanges").prop("disabled", false);
$("#savechanges").addClass("btn-success").removeClass("btn-outline-secondary");
} else {
$("#savechanges").prop("disabled", true);
$("#savechanges").removeClass("btn-success").addClass("btn-outline-secondary");
}
const saveBtn = $("#savechanges");
saveBtn
.prop('disabled', !hasChanges)
.toggleClass('btn-success', hasChanges)
.toggleClass('btn-outline-secondary', !hasChanges);
this.has_changes_to_write = new_value;
this.has_changes_to_write = hasChanges;
}
// Unified controller operations that delegate to the current controller
@@ -170,6 +175,7 @@ class ControllerManager {
*/
async nvsUnlock() {
await this.currentController.nvsUnlock();
await this.queryNvStatus(); // Refresh NVS status
}
/**
@@ -181,6 +187,7 @@ class ControllerManager {
throw new Error(this.l("NVS Lock failed: ") + String(res.error));
}
await this.queryNvStatus(); // Refresh NVS status
return res;
}
@@ -193,7 +200,18 @@ class ControllerManager {
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
throw new Error(this.l("Stick calibration failed: ") + detail);
}
return true;
}
/**
* Sample stick position during calibration
*/
async calibrateSticksSample() {
const res = await this.currentController.calibrateSticksSample();
if (!res.ok) {
await sleep(500);
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
throw new Error(this.l("Stick calibration failed: ") + detail);
}
}
/**
@@ -208,20 +226,6 @@ class ControllerManager {
}
this.setHasChangesToWrite(true);
return true;
}
/**
* Sample stick position during calibration
*/
async calibrateSticksSample() {
const res = await this.currentController.calibrateSticksSample();
if (!res.ok) {
await sleep(500);
const detail = res.code ? (this.l("Error ") + String(res.code)) : String(res.error || "");
throw new Error(this.l("Stick calibration failed: ") + detail);
}
return true;
}
/**
@@ -233,7 +237,6 @@ class ControllerManager {
const detail = ret.code ? (this.l("Error ") + String(ret.code)) : String(ret.error || "");
throw new Error(this.l("Range calibration failed: ") + detail);
}
return true;
}
/**
@@ -270,22 +273,14 @@ class ControllerManager {
la("multi_calibrate_sticks");
progressCallback(20);
const okBegin = await this.calibrateSticksBegin();
if (!okBegin) {
return { success: false, message: this.l("Stick calibration failed to begin") };
}
await this.calibrateSticksBegin();
progressCallback(30);
// Sample multiple times during the process
const sampleCount = 5;
for (let i = 0; i < sampleCount; i++) {
await sleep(100);
const okSample = await this.calibrateSticksSample();
if (!okSample) {
return { success: false, message: this.l("Stick calibration sampling failed") };
}
await this.calibrateSticksSample();
// Progress from 30% to 80% during sampling
const sampleProgress = 30 + ((i + 1) / sampleCount) * 50;
@@ -293,13 +288,9 @@ class ControllerManager {
}
progressCallback(90);
const okEnd = await this.calibrateSticksEnd();
if (!okEnd) {
return { success: false, message: this.l("Stick calibration failed to complete") };
}
await this.calibrateSticksEnd();
progressCallback(100);
return { success: true, message: this.l("Stick calibration completed") };
} catch (e) {
la("multi_calibrate_sticks_failed", {"r": e});
@@ -310,7 +301,7 @@ class ControllerManager {
/**
* Helper function to check if stick positions have changed
*/
sticksChanged(current, newValues) {
_sticksChanged(current, newValues) {
return current.left.x !== newValues.left.x || current.left.y !== newValues.left.y ||
current.right.x !== newValues.right.x || current.right.y !== newValues.right.y;
}
@@ -319,7 +310,7 @@ class ControllerManager {
* Generic button processing for DS4/DS5
* Records button states and returns changes
*/
recordButtonStates(data, BUTTON_MAP, dpad_byte, l2_analog_byte, r2_analog_byte) {
_recordButtonStates(data, BUTTON_MAP, dpad_byte, l2_analog_byte, r2_analog_byte) {
const changes = {};
// Stick positions (always at bytes 0-3)
@@ -332,7 +323,7 @@ class ControllerManager {
right: { x: new_rx, y: new_ry }
};
if (this.sticksChanged(this.button_states.sticks, newSticks)) {
if (this._sticksChanged(this.button_states.sticks, newSticks)) {
this.button_states.sticks = newSticks;
changes.sticks = newSticks;
}
@@ -390,22 +381,22 @@ class ControllerManager {
const inputConfig = this.currentController.getInputConfig();
const { buttonMap, dpadByte, l2AnalogByte, r2AnalogByte } = inputConfig;
const { touchpadOffset, batteryByte, isDS4 } = inputConfig;
const { touchpadOffset } = inputConfig;
// Process button states using the device-specific configuration
const changes = this.recordButtonStates(data, buttonMap, dpadByte, l2AnalogByte, r2AnalogByte);
const changes = this._recordButtonStates(data, buttonMap, dpadByte, l2AnalogByte, r2AnalogByte);
// Parse and store touch points if touchpad data is available
if (touchpadOffset) {
this.touchPoints = this.parseTouchPoints(data, touchpadOffset);
this.touchPoints = this._parseTouchPoints(data, touchpadOffset);
}
// Parse and store battery status if battery data is available
this.batteryStatus = this.parseBatteryStatus(data, batteryByte, isDS4);
// Parse and store battery status
this.batteryStatus = this._parseBatteryStatus(data);
const result = {
changes,
inputConfig: { buttonMap, isDS4 },
inputConfig: { buttonMap },
touchPoints: this.touchPoints,
batteryStatus: this.batteryStatus,
};
@@ -419,7 +410,7 @@ class ControllerManager {
* @param {number} offset - Offset to touchpad data
* @returns {Array} Array of touch points with {active, id, x, y} properties
*/
parseTouchPoints(data, offset) {
_parseTouchPoints(data, offset) {
// Returns array of up to 2 points: {active, id, x, y}
const points = [];
for (let i = 0; i < 2; i++) {
@@ -442,88 +433,21 @@ class ControllerManager {
/**
* Parse battery status from input data
* @param {DataView} data - Input data view
* @param {number} byte - Byte offset for battery data
* @param {boolean} isDS4 - Whether this is a DS4 controller
* @returns {Object} Battery status object with bat_txt, changed, bat_capacity, etc.
*/
parseBatteryStatus(data, byte, isDS4 = false) {
const bat = data.getUint8(byte);
let bat_capacity = 0, cable_connected = false, is_charging = false, is_error = false;
_parseBatteryStatus(data) {
const batteryInfo = this.currentController.parseBatteryStatus(data);
const bat_txt = this._batteryPercentToText(batteryInfo);
if (isDS4) {
// DS4: bat_data = low 4 bits, bat_status = bit 4
const bat_data = bat & 0x0f;
const bat_status = (bat >> 4) & 1;
if (bat_status == 1) {
cable_connected = true;
if (bat_data < 10) {
bat_capacity = Math.min(bat_data * 10 + 5, 100);
is_charging = true;
} else if (bat_data == 10) {
bat_capacity = 100;
is_charging = true;
} else if (bat_data == 11) {
bat_capacity = 100;
// charged
} else {
bat_capacity = 0;
is_error = true;
}
} else {
cable_connected = false;
if (bat_data < 10) {
bat_capacity = bat_data * 10 + 5;
} else {
bat_capacity = 100;
}
}
} else {
// DS5: bat_charge = low 4 bits, bat_status = high 4 bits
const bat_charge = bat & 0x0f;
const bat_status = bat >> 4;
if (bat_status == 0) {
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
} else if (bat_status == 1) {
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
is_charging = true;
cable_connected = true;
} else if (bat_status == 2) {
bat_capacity = 100;
cable_connected = true;
} else {
is_error = true;
}
}
// Generate battery text with icons
const bat_txt = this.batteryPercentToText(bat_capacity, is_charging, is_error);
// Check if battery text has changed
const changed = bat_txt !== this._lastBatteryText;
this._lastBatteryText = bat_txt;
// Update internal battery status
const batteryStatus = {
bat_txt,
changed,
bat_capacity,
cable_connected,
is_charging,
is_error
};
return batteryStatus;
return { bat_txt, changed, ...batteryInfo };
}
/**
* Convert battery percentage to display text with icons
* @param {number} bat_charge - Battery charge percentage
* @param {boolean} is_charging - Whether battery is charging
* @param {boolean} is_error - Whether there's a battery error
* @returns {string} HTML string with battery status and icons
*/
batteryPercentToText(bat_charge, is_charging, is_error) {
_batteryPercentToText({bat_capacity, is_charging, is_error}) {
if (is_error) {
return '<font color="red">' + this.l("error") + '</font>';
}
@@ -535,10 +459,10 @@ class ControllerManager {
{ threshold: 80, icon: 'fa-battery-three-quarters' },
];
const icon_txt = batteryIcons.find(item => bat_charge < item.threshold)?.icon || 'fa-battery-full';
const icon_full = '<i class="fa-solid ' + icon_txt + '"></i>';
const icon_txt = batteryIcons.find(item => bat_capacity < item.threshold)?.icon || 'fa-battery-full';
const icon_full = `<i class="fa-solid ${icon_txt}"></i>`;
const bolt_txt = is_charging ? '<i class="fa-solid fa-bolt"></i>' : '';
return bat_charge + "%" + ' ' + bolt_txt + ' ' + icon_full;
return [`${bat_capacity}%`, icon_full, bolt_txt].join(' ');
}
/**

View File

@@ -6,14 +6,15 @@
class BaseController {
constructor(device, uiDependencies = {}) {
this.device = device;
this.type = "undefined"; // to be set by subclasses
this.model = "undefined"; // to be set by subclasses
this.finetuneMaxValue; // to be set by subclasses
// UI dependencies injected from core
this.l = uiDependencies.l;
}
getType() {
return this.type;
getModel() {
return this.model;
}
/**
@@ -28,6 +29,15 @@ class BaseController {
throw new Error('getInputConfig() must be implemented by subclass');
}
/**
* Get the maximum value for finetune data
* @returns {number} Maximum value for finetune adjustments
*/
getFinetuneMaxValue() {
if(!this.finetuneMaxValue) throw new Error('getFinetuneMaxValue() must be implemented by subclass');
return this.finetuneMaxValue;
}
/**
* Set input report handler
* @param {Function} handler Input report handler function
@@ -122,6 +132,10 @@ class BaseController {
async calibrateRangeEnd() {
throw new Error('calibrateRangeEnd() must be implemented by subclass');
}
parseBatteryStatus(data) {
throw new Error('parseBatteryStatus() must be implemented by subclass');
}
}
export default BaseController;

View File

@@ -28,16 +28,16 @@ class ControllerFactory {
switch (device.productId) {
case 0x05c4: // DS4 v1
case 0x09cc: // DS4 v2
return new DS4Controller(device, uiDependencies);
return new DS4Controller(device, uiDependencies);
case 0x0ce6: // DS5
return new DS5Controller(device, uiDependencies);
return new DS5Controller(device, uiDependencies);
case 0x0df2: // DS5 Edge
return new DS5EdgeController(device, uiDependencies);
return new DS5EdgeController(device, uiDependencies);
default:
throw new Error(`Unsupported device: ${dec2hex(device.vendorId)}:${dec2hex(device.productId)}`);
throw new Error(`Unsupported device: ${dec2hex(device.vendorId)}:${dec2hex(device.productId)}`);
}
}
@@ -49,15 +49,15 @@ class ControllerFactory {
static getDeviceName(productId) {
switch (productId) {
case 0x05c4:
return "Sony DualShock 4 V1";
return "Sony DualShock 4 V1";
case 0x09cc:
return "Sony DualShock 4 V2";
return "Sony DualShock 4 V2";
case 0x0ce6:
return "Sony DualSense";
return "Sony DualSense";
case 0x0df2:
return "Sony DualSense Edge";
return "Sony DualSense Edge";
default:
return "Unknown Device";
return "Unknown Device";
}
}
@@ -70,29 +70,29 @@ class ControllerFactory {
switch (productId) {
case 0x05c4: // DS4 v1
case 0x09cc: // DS4 v2
return {
showInfo: false,
showFinetune: false,
showMute: false,
showInfoTab: false
};
return {
showInfo: false,
showFinetune: false,
showMute: false,
showInfoTab: false
};
case 0x0ce6: // DS5
case 0x0df2: // DS5 Edge
return {
showInfo: true,
showFinetune: true,
showMute: true,
showInfoTab: true
};
return {
showInfo: true,
showFinetune: true,
showMute: true,
showInfoTab: true
};
default:
return {
showInfo: false,
showFinetune: false,
showMute: false,
showInfoTab: false
};
return {
showInfo: false,
showFinetune: false,
showMute: false,
showInfoTab: false
};
}
}
}

View File

@@ -40,8 +40,6 @@ const DS4_INPUT_CONFIG = {
l2AnalogByte: 7,
r2AnalogByte: 8,
touchpadOffset: 34,
batteryByte: 29,
isDS4: true
};
/**
@@ -50,7 +48,7 @@ const DS4_INPUT_CONFIG = {
class DS4Controller extends BaseController {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
this.type = "DS4";
this.model = "DS4";
}
getInputConfig() {
@@ -58,9 +56,11 @@ class DS4Controller extends BaseController {
}
async getInfo() {
const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
let deviceTypeText = this.l("unknown");
let deviceTypeText = l("unknown");
let is_clone = false;
const view = lf("ds4_info", await this.receiveFeatureReport(0xa3));
@@ -69,7 +69,7 @@ class DS4Controller extends BaseController {
if(cmd != 0xa3 || view.buffer.byteLength < 49) {
if(view.buffer.byteLength != 49) {
deviceTypeText = this.l("clone");
deviceTypeText = l("clone");
is_clone = true;
}
}
@@ -77,35 +77,35 @@ class DS4Controller extends BaseController {
const k1 = new TextDecoder().decode(view.buffer.slice(1, 0x10)).replace(/\0/g, '');
const k2 = new TextDecoder().decode(view.buffer.slice(0x10, 0x20)).replace(/\0/g, '');
const hw_ver_major= view.getUint16(0x21, true);
const hw_ver_minor= view.getUint16(0x23, true);
const sw_ver_major= view.getUint32(0x25, true);
const sw_ver_minor= view.getUint16(0x25+4, true);
const hw_ver_major = view.getUint16(0x21, true);
const hw_ver_minor = view.getUint16(0x23, true);
const sw_ver_major = view.getUint32(0x25, true);
const sw_ver_minor = view.getUint16(0x25+4, true);
try {
if(!is_clone) {
// If this feature report succeeds, it's an original device
await this.receiveFeatureReport(0x81);
deviceTypeText = this.l("original");
deviceTypeText = l("original");
}
} catch(e) {
la("clone");
is_clone = true;
deviceTypeText = this.l("clone");
deviceTypeText = l("clone");
}
const infoItems = [
{ key: this.l("Build Date"), value: k1 + " " + k2, cat: "fw" },
{ key: this.l("HW Version"), value: "" + dec2hex(hw_ver_major) + ":" + dec2hex(hw_ver_minor), cat: "hw" },
{ key: this.l("SW Version"), value: dec2hex32(sw_ver_major) + ":" + dec2hex(sw_ver_minor), cat: "fw" },
{ key: this.l("Device Type"), value: deviceTypeText, cat: "hw", severity: is_clone ? 'danger' : undefined },
{ key: l("Build Date"), value: `${k1} ${k2}`, cat: "fw" },
{ key: l("HW Version"), value: `${dec2hex(hw_ver_major)}:${dec2hex(hw_ver_minor)}`, cat: "hw" },
{ key: l("SW Version"), value: `${dec2hex32(sw_ver_major)}:${dec2hex(sw_ver_minor)}`, cat: "fw" },
{ key: l("Device Type"), value: deviceTypeText, cat: "hw", severity: is_clone ? 'danger' : undefined },
];
if(!is_clone) {
// Add Board Model (UI will append the info icon)
infoItems.push({ key: this.l("Board Model"), value: this.hwToBoardModel(hw_ver_minor), cat: "hw", addInfoIcon: 'board' });
infoItems.push({ key: l("Board Model"), value: this.hwToBoardModel(hw_ver_minor), cat: "hw", addInfoIcon: 'board' });
const bd_addr = await this.getBdAddr();
infoItems.push({ key: this.l("Bluetooth Address"), value: bd_addr, cat: "hw" });
infoItems.push({ key: l("Bluetooth Address"), value: bd_addr, cat: "hw" });
}
const nv = await this.queryNvStatus();
@@ -175,8 +175,7 @@ class DS4Controller extends BaseController {
// Assert
const data = await this.receiveFeatureReport(0x91);
const data2 = await this.receiveFeatureReport(0x92);
const d1 = data.getUint32(0, false);
const d2 = data2.getUint32(0, false);
const [d1, d2] = [data, data2].map(v => v.getUint32(0, false));
if(d1 != 0x91010201 || d2 != 0x920102ff) {
la("ds4_calibrate_range_begin_failed", {"d1": d1, "d2": d2});
return { ok: false, code: 1, d1, d2 };
@@ -197,8 +196,7 @@ class DS4Controller extends BaseController {
const data = await this.receiveFeatureReport(0x91);
const data2 = await this.receiveFeatureReport(0x92);
const d1 = data.getUint32(0, false);
const d2 = data2.getUint32(0, false);
const [d1, d2] = [data, data2].map(v => v.getUint32(0, false));
if(d1 != 0x91010202 || d2 != 0x92010201) {
la("ds4_calibrate_range_end_failed", {"d1": d1, "d2": d2});
return { ok: false, code: 3, d1, d2 };
@@ -221,8 +219,7 @@ class DS4Controller extends BaseController {
// Assert
const data = await this.receiveFeatureReport(0x91);
const data2 = await this.receiveFeatureReport(0x92);
const d1 = data.getUint32(0, false);
const d2 = data2.getUint32(0, false);
const [d1, d2] = [data, data2].map(v => v.getUint32(0, false));
if(d1 != 0x91010101 || d2 != 0x920101ff) {
la("ds4_calibrate_sticks_begin_failed", {"d1": d1, "d2": d2});
return { ok: false, code: 1, d1, d2 };
@@ -246,8 +243,7 @@ class DS4Controller extends BaseController {
const data = await this.receiveFeatureReport(0x91);
const data2 = await this.receiveFeatureReport(0x92);
if(data.getUint32(0, false) != 0x91010101 || data2.getUint32(0, false) != 0x920101ff) {
const d1 = dec2hex32(data.getUint32(0, false));
const d2 = dec2hex32(data2.getUint32(0, false));
const [d1, d2] = [data, data2].map(v => dec2hex32(v.getUint32(0, false)));
la("ds4_calibrate_sticks_sample_failed", {"d1": d1, "d2": d2});
return { ok: false, code: 2, d1, d2 };
}
@@ -267,8 +263,7 @@ class DS4Controller extends BaseController {
const data = await this.receiveFeatureReport(0x91);
const data2 = await this.receiveFeatureReport(0x92);
if(data.getUint32(0, false) != 0x91010102 || data2.getUint32(0, false) != 0x92010101) {
const d1 = dec2hex32(data.getUint32(0, false));
const d2 = dec2hex32(data2.getUint32(0, false));
const [d1, d2] = [data, data2].map(v => dec2hex32(v.getUint32(0, false)));
la("ds4_calibrate_sticks_end_failed", {"d1": d1, "d2": d2});
return { ok: false, code: 3, d1, d2 };
}
@@ -285,12 +280,14 @@ class DS4Controller extends BaseController {
await this.sendFeatureReport(0x08, [0xff,0, 12]);
const data = lf("ds4_nvstatus", await this.receiveFeatureReport(0x11));
const ret = data.getUint8(1, false);
if (ret === 1) {
return { device: 'ds4', status: 'locked', locked: true, mode: 'temporary', code: 1 };
} else if (ret === 0) {
return { device: 'ds4', status: 'unlocked', locked: false, mode: 'permanent', code: 0 };
} else {
return { device: 'ds4', status: 'unknown', locked: null, code: ret };
const res = { device: 'ds4', code: ret }
switch(ret) {
case 1:
return { ...res, status: 'locked', locked: true, mode: 'temporary' };
case 0:
return { ...res, status: 'unlocked', locked: false, mode: 'permanent' };
default:
return { ...res, status: 'unknown', locked: null };
}
} catch (e) {
return { device: 'ds4', status: 'error', locked: null, code: 2, error: e };
@@ -327,6 +324,42 @@ class DS4Controller extends BaseController {
const b = a >> 4;
return ((b == 7 && a > 0x74) || (b == 9 && a != 0x93 && a != 0x90));
}
/**
* Parse DS4 battery status from input data
*/
parseBatteryStatus(data) {
const bat = data.getUint8(29); // DS4 battery byte is at position 29
// DS4: bat_data = low 4 bits, bat_status = bit 4
const bat_data = bat & 0x0f;
const bat_status = (bat >> 4) & 1;
const cable_connected = bat_status === 1;
let bat_capacity = 0;
let is_charging = false;
let is_error = false;
if (cable_connected) {
if (bat_data < 10) {
bat_capacity = Math.min(bat_data * 10 + 5, 100);
is_charging = true;
} else if (bat_data === 10) {
bat_capacity = 100;
is_charging = true;
} else if (bat_data === 11) {
bat_capacity = 100; // Fully charged
} else {
bat_capacity = 0;
is_error = true;
}
} else {
// On battery power
bat_capacity = bat_data < 10 ? bat_data * 10 + 5 : 100;
}
return { bat_capacity, cable_connected, is_charging, is_error };
}
}
export default DS4Controller;

View File

@@ -43,8 +43,6 @@ const DS5_INPUT_CONFIG = {
l2AnalogByte: 4,
r2AnalogByte: 5,
touchpadOffset: 32,
batteryByte: 52,
isDS4: false
};
function ds5_color(x) {
@@ -81,7 +79,8 @@ function ds5_color(x) {
class DS5Controller extends BaseController {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
this.type = "DS5";
this.model = "DS5";
this.finetuneMaxValue = 65535; // 16-bit max value for DS5
}
getInputConfig() {
@@ -89,10 +88,11 @@ class DS5Controller extends BaseController {
}
async getInfo() {
return await this._getInfo(false);
return this._getInfo(false);
}
async _getInfo(is_edge) {
const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
const view = lf("ds5_info", await this.receiveFeatureReport(0x20));
@@ -118,30 +118,30 @@ class DS5Controller extends BaseController {
const serial_number = await this.getSystemInfo(1, 19, 17);
const color = ds5_color(serial_number);
const infoItems = [
{ key: this.l("Serial Number"), value: serial_number, cat: "hw" },
{ key: this.l("MCU Unique ID"), value: await this.getSystemInfo(1, 9, 9, false), cat: "hw", isExtra: true },
{ key: this.l("PCBA ID"), value: reverse_str(await this.getSystemInfo(1, 17, 14)), cat: "hw", isExtra: true },
{ key: this.l("Battery Barcode"), value: await this.getSystemInfo(1, 24, 23), cat: "hw", isExtra: true },
{ key: this.l("VCM Left Barcode"), value: await this.getSystemInfo(1, 26, 16), cat: "hw", isExtra: true },
{ key: this.l("VCM Right Barcode"), value: await this.getSystemInfo(1, 28, 16), cat: "hw", isExtra: true },
{ key: l("Serial Number"), value: serial_number, cat: "hw" },
{ key: l("MCU Unique ID"), value: await this.getSystemInfo(1, 9, 9, false), cat: "hw", isExtra: true },
{ key: l("PCBA ID"), value: reverse_str(await this.getSystemInfo(1, 17, 14)), cat: "hw", isExtra: true },
{ key: l("Battery Barcode"), value: await this.getSystemInfo(1, 24, 23), cat: "hw", isExtra: true },
{ key: l("VCM Left Barcode"), value: await this.getSystemInfo(1, 26, 16), cat: "hw", isExtra: true },
{ key: l("VCM Right Barcode"), value: await this.getSystemInfo(1, 28, 16), cat: "hw", isExtra: true },
{ key: this.l("Color"), value: this.l(color), cat: "hw", addInfoIcon: 'color' },
{ key: l("Color"), value: l(color), cat: "hw", addInfoIcon: 'color' },
...(is_edge ? [] : [{ key: this.l("Board Model"), value: this.hwToBoardModel(hwinfo), cat: "hw", addInfoIcon: 'board' }]),
...(is_edge ? [] : [{ key: l("Board Model"), value: this.hwToBoardModel(hwinfo), cat: "hw", addInfoIcon: 'board' }]),
{ key: this.l("FW Build Date"), value: build_date + " " + build_time, cat: "fw" },
{ key: this.l("FW Type"), value: "0x" + dec2hex(fwtype), cat: "fw", isExtra: true },
{ key: this.l("FW Series"), value: "0x" + dec2hex(swseries), cat: "fw", isExtra: true },
{ key: this.l("HW Model"), value: "0x" + dec2hex32(hwinfo), cat: "hw", isExtra: true },
{ key: this.l("FW Version"), value: "0x" + dec2hex32(fwversion), cat: "fw" },
{ key: this.l("FW Update"), value: "0x" + dec2hex(updversion), cat: "fw" },
{ key: this.l("FW Update Info"), value: "0x" + dec2hex8(unk), cat: "fw", isExtra: true },
{ key: this.l("SBL FW Version"), value: "0x" + dec2hex32(fwversion1), cat: "fw", isExtra: true },
{ key: this.l("Venom FW Version"), value: "0x" + dec2hex32(fwversion2), cat: "fw", isExtra: true },
{ key: this.l("Spider FW Version"), value: "0x" + dec2hex32(fwversion3), cat: "fw", isExtra: true },
{ key: l("FW Build Date"), value: build_date + " " + build_time, cat: "fw" },
{ key: l("FW Type"), value: "0x" + dec2hex(fwtype), cat: "fw", isExtra: true },
{ key: l("FW Series"), value: "0x" + dec2hex(swseries), cat: "fw", isExtra: true },
{ key: l("HW Model"), value: "0x" + dec2hex32(hwinfo), cat: "hw", isExtra: true },
{ key: l("FW Version"), value: "0x" + dec2hex32(fwversion), cat: "fw" },
{ key: l("FW Update"), value: "0x" + dec2hex(updversion), cat: "fw" },
{ key: l("FW Update Info"), value: "0x" + dec2hex8(unk), cat: "fw", isExtra: true },
{ key: l("SBL FW Version"), value: "0x" + dec2hex32(fwversion1), cat: "fw", isExtra: true },
{ key: l("Venom FW Version"), value: "0x" + dec2hex32(fwversion2), cat: "fw", isExtra: true },
{ key: l("Spider FW Version"), value: "0x" + dec2hex32(fwversion3), cat: "fw", isExtra: true },
{ key: this.l("Touchpad ID"), value: await this.getSystemInfo(5, 2, 8, false), cat: "hw", isExtra: true },
{ key: this.l("Touchpad FW Version"), value: await this.getSystemInfo(5, 4, 8, false), cat: "fw", isExtra: true },
{ key: l("Touchpad ID"), value: await this.getSystemInfo(5, 2, 8, false), cat: "hw", isExtra: true },
{ key: l("Touchpad FW Version"), value: await this.getSystemInfo(5, 4, 8, false), cat: "fw", isExtra: true },
];
const old_controller = build_date.search(/ 2020| 2021/);
@@ -153,7 +153,7 @@ class DS5Controller extends BaseController {
const nv = await this.queryNvStatus();
const bd_addr = await this.getBdAddr();
infoItems.push({ key: this.l("Bluetooth Address"), value: bd_addr, cat: "hw" });
infoItems.push({ key: l("Bluetooth Address"), value: bd_addr, cat: "hw" });
const pending_reboot = (nv?.status === 'pending_reboot');
@@ -218,13 +218,11 @@ class DS5Controller extends BaseController {
const pcba_id = lf("ds5_pcba_id", await this.receiveFeatureReport(129));
if(pcba_id.getUint8(1) != base || pcba_id.getUint8(2) != num || pcba_id.getUint8(3) != 2) {
return this.l("error");
} else {
if(decode)
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
else
return buf2hex(pcba_id.buffer.slice(4, 4+length));
}
return this.l("Unknown");
if(decode)
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
return buf2hex(pcba_id.buffer.slice(4, 4+length));
}
async calibrateSticksBegin() {
@@ -273,7 +271,7 @@ class DS5Controller extends BaseController {
// Write
await this.sendFeatureReport(0x82, [2,1,1]);
let data = await this.receiveFeatureReport(0x83);
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010102) {
const d1 = dec2hex32(data.getUint32(0, false));
@@ -315,7 +313,7 @@ class DS5Controller extends BaseController {
await this.sendFeatureReport(0x82, [2,1,2]);
// Assert
let data = await this.receiveFeatureReport(0x83);
const data = await this.receiveFeatureReport(0x83);
if(data.getUint32(0, false) != 0x83010202) {
const d1 = dec2hex32(data.getUint32(0, false));
@@ -376,9 +374,7 @@ class DS5Controller extends BaseController {
await sleep(100);
const data = await this.receiveFeatureReport(0x81);
const cmd = data.getUint8(0, true);
const p1 = data.getUint8(1, true);
const p2 = data.getUint8(2, true);
const p3 = data.getUint8(3, true);
const [p1, p2, p3] = [1, 2, 3].map(i => data.getUint8(i, true));
if(cmd != 129 || p1 != 12 || (p2 != 2 && p2 != 4) || p3 != 2)
return null;
@@ -390,6 +386,46 @@ class DS5Controller extends BaseController {
const pkg = data.reduce((acc, val) => acc.concat([val & 0xff, val >> 8]), [12, 1]);
await this.sendFeatureReport(0x80, pkg);
}
/**
* Parse DS5 battery status from input data
*/
parseBatteryStatus(data) {
const bat = data.getUint8(52); // DS5 battery byte is at position 52
// DS5: bat_charge = low 4 bits, bat_status = high 4 bits
const bat_charge = bat & 0x0f;
const bat_status = bat >> 4;
let bat_capacity = 0;
let cable_connected = false;
let is_charging = false;
let is_error = false;
switch (bat_status) {
case 0:
// On battery power
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
break;
case 1:
// Charging
bat_capacity = Math.min(bat_charge * 10 + 5, 100);
is_charging = true;
cable_connected = true;
break;
case 2:
// Fully charged
bat_capacity = 100;
cable_connected = true;
break;
default:
// Error state
is_error = true;
break;
}
return { bat_capacity, cable_connected, is_charging, is_error };
}
}
export default DS5Controller;

View File

@@ -1,33 +1,31 @@
'use strict';
import DS5Controller from './ds5-controller.js';
import {
sleep,
dec2hex32,
la,
lf
} from '../utils.js';
import { sleep, dec2hex32, la, lf } from '../utils.js';
/**
* DualSense Edge (DS5 Edge) Controller implementation
*/
class DS5EdgeController extends DS5Controller {
constructor(device) {
super(device);
this.type = "DS5Edge";
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
this.model = "DS5_Edge";
this.finetuneMaxValue = 4095; // 12-bit max value for DS5 Edge
}
async getInfo() {
const { l } = this;
// DS5 Edge uses the same info structure as DS5 but with is_edge=true
const result = await this._getInfo(true);
if (result.ok) {
// DS Edge extra module info
const empty = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00';
const empty = Array(17).fill('\x00').join('');
try {
const sticks_barcode = (await this.getBarcode()).map(barcode => barcode === empty ? this.l("Unknown") : barcode);
result.infoItems.push({ key: this.l("Left Module Barcode"), value: sticks_barcode[1], cat: "fw" });
result.infoItems.push({ key: this.l("Right Module Barcode"), value: sticks_barcode[0], cat: "fw" });
const sticks_barcode = (await this.getBarcode()).map(barcode => barcode === empty ? l("Unknown") : barcode);
result.infoItems.push({ key: l("Left Module Barcode"), value: sticks_barcode[1], cat: "fw" });
result.infoItems.push({ key: l("Right Module Barcode"), value: sticks_barcode[0], cat: "fw" });
} catch(_e) {
// ignore module read errors here
}
@@ -146,21 +144,25 @@ class DS5EdgeController extends DS5Controller {
}
async waitUntilWritten(expected) {
for(let it=0;it<10;it++) {
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
const data = await this.receiveFeatureReport(0x81);
let again = false
for(let i=0;i<expected.length;i++) {
if(data.getUint8(1+i, true) != expected[i]) {
again = true;
break;
}
}
if(!again) {
// Check if all expected bytes match
const allMatch = expected.every((expectedByte, i) =>
data.getUint8(1 + i, true) === expectedByte
);
if (allMatch) {
return true;
}
attempts++;
await sleep(50);
}
return false;
}
@@ -229,9 +231,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(100);
const data = await this.receiveFeatureReport(0x81);
const cmd = data.getUint8(0, true);
const p1 = data.getUint8(1, true);
const p2 = data.getUint8(2, true);
const p3 = data.getUint8(3, true);
const [p1, p2, p3] = [1, 2, 3].map(i => data.getUint8(i, true));
if(cmd != 129 || p1 != 12 || (p2 != 2 && p2 != 4) || p3 != 2)
return null;

871
js/core.js Normal file
View File

@@ -0,0 +1,871 @@
'use strict';
import { sleep, float_to_str, dec2hex, dec2hex32, lerp_color, la, createCookie, readCookie } from './utils.js';
import { initControllerManager } from './controller-manager.js';
import ControllerFactory from './controllers/controller-factory.js';
import { lang_init, l } from './translations.js';
import { loadAllTemplates } from './template-loader.js';
import { draw_stick_position, CIRCULARITY_DATA_SIZE } from './stick-renderer.js';
import { ds5_finetune, isFinetuneVisible, finetune_handle_controller_input } from './modals/finetune-modal.js';
import { calibrate_stick_centers, auto_calibrate_stick_centers } from './modals/calib-center-modal.js';
import { calibrate_range } from './modals/calib-range-modal.js';
// Application State - manages app-wide state and UI
const app = {
// Button disable state management
disable_btn: 0,
last_disable_btn: 0,
// Language and UI state
lang_orig_text: {},
lang_orig_text: {},
lang_cur: {},
lang_disabled: true,
lang_cur_direction: "ltr",
// Session tracking
gj: 0,
gu: 0
};
const ll_data = new Array(CIRCULARITY_DATA_SIZE);
const rr_data = new Array(CIRCULARITY_DATA_SIZE);
let controller = null;
function gboot() {
app.gu = crypto.randomUUID();
$("#infoshowall").hide();
async function initializeApp() {
await loadAllTemplates();
await init_svg_controller();
lang_init(app, handleLanguageChange, show_welcome_modal, la);
show_welcome_modal();
$("input[name='displayMode']").on('change', on_stick_mode_change);
window.addEventListener("error", (event) => {
console.error(event.error?.stack || event.message);
show_popup(event.error?.message || event.message);
});
window.addEventListener("unhandledRejection", (event) => {
console.error("Unhandled rejection:", event.reason?.stack || event.reason);
close_all_modals();
show_popup(event.reason?.message || event.reason);
// Prevent the default browser behavior (logging to console, again)
event.preventDefault();
});
}
// Since modules are deferred, DOM might already be loaded
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', initializeApp);
} else {
// DOM is already loaded, run immediately
initializeApp();
}
if (!("hid" in navigator)) {
$("#offlinebar").hide();
$("#onlinebar").hide();
$("#missinghid").show();
return;
}
$("#offlinebar").show();
navigator.hid.addEventListener("disconnect", handleDisconnectedDevice);
}
async function connect() {
app.gj = crypto.randomUUID();
// Initialize controller manager with translation function
controller = initControllerManager({ l, handleNvStatusUpdate });
controller.setInputHandler(handleControllerInput);
la("begin");
reset_circularity_mode();
try {
$("#btnconnect").prop("disabled", true);
$("#connectspinner").show();
await sleep(100);
const supportedModels = ControllerFactory.getSupportedModels();
const requestParams = { filters: supportedModels };
let devices = await navigator.hid.getDevices(); // Already connected?
if (devices.length == 0) {
devices = await navigator.hid.requestDevice(requestParams);
}
if (devices.length == 0) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
return;
}
if (devices.length > 1) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
throw new Error(l("Please connect only one controller at time."));
}
const device = devices[0];
if(device.opened) await device.close();
await device.open();
la("connect", {"p": device.productId, "v": device.vendorId});
device.oninputreport = continue_connection
} catch(error) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
throw new Error(l("Error: ") + error);
}
}
async function continue_connection({data, device}) {
try {
if (!controller || controller.isConnected()) {
controller?.setInputReportHandler(null);
return;
}
let connected = false;
// Detect if the controller is connected via USB
const reportLen = data.byteLength;
if(reportLen != 63) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
await disconnect();
throw new Error(l("Please connect the device using a USB cable."));
}
// Helper to apply basic UI visibility based on device type
function applyDeviceUI({ showInfo, showFinetune, showMute, showInfoTab }) {
$("#infoshowall").toggle(!!showInfo);
$("#ds5finetune").toggle(!!showFinetune);
$("#info-tab").toggle(!!showInfoTab);
set_mute_visibility(!!showMute);
}
let controllerInstance = null;
let info = null;
try {
// Create controller instance using factory
controllerInstance = ControllerFactory.createControllerInstance(device, { l });
controller.setControllerInstance(controllerInstance);
info = await controllerInstance.getInfo();
} catch (error) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
await disconnect();
if (device) {
throw new Error(l("Connected invalid device: ") + dec2hex(device.vendorId) + ":" + dec2hex(device.productId));
} else {
throw new Error(l("Failed to connect to device"));
}
}
if(!info?.ok) {
// Not connected/failed to fetch info
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
await disconnect();
if(info) console.error(info.error);
throw new Error(l("Connected invalid device: ") + l("Error 1"));
}
connected = true;
// Get UI configuration and device name
const ui = ControllerFactory.getUIConfig(device.productId);
applyDeviceUI(ui);
// Assign input processor for stream
device.oninputreport = controller.getInputHandler();
const deviceName = ControllerFactory.getDeviceName(device.productId);
$("#devname").text(deviceName + " (" + dec2hex(device.vendorId) + ":" + dec2hex(device.productId) + ")");
$("#offlinebar").hide();
$("#onlinebar").show();
$("#mainmenu").show();
$("#resetBtn").show();
$("#d-nvstatus").text = l("Unknown");
$("#d-bdaddr").text = l("Unknown");
$('#controller-tab').tab('show');
const model = controllerInstance.getModel();
// Edge-specific: pending reboot check (from nv)
if (model == "DS5_Edge" && info?.pending_reboot) {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
await disconnect();
throw new Error(l("A reboot is needed to continue using this DualSense Edge. Please disconnect and reconnect your controller."));
}
// Render info collected from device
render_info_to_dom(info.infoItems);
// Render NV status
if (info.nv) {
render_nvstatus_to_dom(info.nv);
// Optionally try to lock NVS if unlocked
if (info.nv.locked === false) {
await nvslock();
}
}
// Apply disable button flags
if (typeof info.disable_bits === 'number' && info.disable_bits) {
app.disable_btn |= info.disable_bits;
}
if(app.disable_btn != 0) update_disable_btn();
// DS4 rare notice
if (model == "DS4" && info?.rare) {
show_popup("Wow, this is a rare/weird controller! Please write me an email at ds4@the.al or contact me on Discord (the_al)");
}
// Edge onboarding modal
if(model == "DS5_Edge") {
show_edge_modal();
}
} finally {
$("#btnconnect").prop("disabled", false);
$("#connectspinner").hide();
}
}
async function disconnect() {
la("disconnect");
if(!controller?.isConnected()) {
controller = null;
return;
}
app.gj = 0;
app.disable_btn = 0;
await controller.disconnect();
controller = null; // Tear everything down
close_all_modals();
$("#offlinebar").show();
$("#onlinebar").hide();
$("#mainmenu").hide();
}
// Wrapper function for HTML onclick handlers
function disconnectSync() {
disconnect().catch(error => {
console.error("Error during disconnect:", error);
show_popup("Error during disconnect: " + error.message);
});
}
async function handleDisconnectedDevice(e) {
la("disconnected");
console.log("Disconnected: " + e.device.productName)
await disconnect();
}
function render_nvstatus_to_dom(nv) {
if(!nv?.status) {
throw new Error("Invalid NVS status data");
}
switch (nv.status) {
case 'locked':
$("#d-nvstatus").html("<font color='green'>" + l("locked") + "</font>");
break;
case 'unlocked':
$("#d-nvstatus").html("<font color='red'>" + l("unlocked") + "</font>");
break;
case 'pending_reboot':
// Keep consistent styling with unknown/purple, but indicate reboot pending if possible
const pendingTxt = nv.raw !== undefined ? ("0x" + dec2hex32(nv.raw)) : String(nv.code ?? '');
$("#d-nvstatus").html("<font color='purple'>unk " + pendingTxt + "</font>");
break;
case 'unknown':
const unknownTxt = nv.device === 'ds5' && nv.raw !== undefined ? ("0x" + dec2hex32(nv.raw)) : String(nv.code ?? '');
$("#d-nvstatus").html("<font color='purple'>unk " + unknownTxt + "</font>");
break;
case 'error':
$("#d-nvstatus").html("<font color='red'>" + l("error") + "</font>");
break;
}
}
async function refresh_nvstatus() {
if (!controller.isConnected()) {
return null;
}
return await controller.queryNvStatus();
}
function set_edge_progress(score) {
$("#dsedge-progress").css({ "width": score + "%" })
}
function show_welcome_modal() {
const already_accepted = readCookie("welcome_accepted");
if(already_accepted == "1")
return;
bootstrap.Modal.getOrCreateInstance('#welcomeModal').show();
}
function welcome_accepted() {
la("welcome_accepted");
createCookie("welcome_accepted", "1");
$("#welcomeModal").modal("hide");
}
async function init_svg_controller() {
const svgContainer = document.getElementById('controller-svg-placeholder');
const response = await fetch('assets/dualshock-controller.svg'); // load it from separate HTML file
if (!response.ok) {
throw new Error('Failed to load controller SVG');
}
const svgContent = await response.text();
svgContainer.innerHTML = svgContent;
const lightBlue = '#7ecbff';
const midBlue = '#3399cc';
const dualshock = document.getElementById('Controller');
set_svg_group_color(dualshock, lightBlue);
['Button_outlines', 'L3_outline', 'R3_outline', 'Trackpad_outline'].forEach(id => {
const group = document.getElementById(id);
set_svg_group_color(group, midBlue);
});
['Button_infills', 'L3_infill', 'R3_infill', 'Trackpad_infill'].forEach(id => {
const group = document.getElementById(id);
set_svg_group_color(group, 'white');
});
}
function set_mute_visibility(show) {
const muteOutline = document.getElementById('Mute_outline');
const muteInfill = document.getElementById('Mute_infill');
if (muteOutline) muteOutline.style.display = show ? '' : 'none';
if (muteInfill) muteInfill.style.display = show ? '' : 'none';
}
/**
* Collects circularity data for both analog sticks during testing mode.
* This function tracks the maximum distance reached at each angular position
* around the stick's circular range, creating a polar coordinate map of
* stick movement capabilities.
*/
function collectCircularityData(stickStates, leftData, rightData) {
const { left, right } = stickStates || {};
const MAX_N = CIRCULARITY_DATA_SIZE;
for(const [stick, data] of [[left, leftData], [right, rightData]]) {
if (!stick) return; // Skip if no stick changed position
const { x, y } = stick;
// Calculate distance from center (magnitude of stick position vector)
const distance = Math.sqrt(x * x + y * y);
// Convert cartesian coordinates to angular index (0 to MAX_N-1)
// atan2 gives angle in radians, convert to array index with proper wrapping
const angleIndex = (parseInt(Math.round(Math.atan2(y, x) * MAX_N / 2.0 / Math.PI)) + MAX_N) % MAX_N;
// Store maximum distance reached at this angle (for circularity analysis)
const oldValue = data[angleIndex] ?? 0;
data[angleIndex] = Math.max(oldValue, distance);
}
}
function clear_circularity() {
ll_data.fill(0);
rr_data.fill(0);
}
function reset_circularity_mode() {
clear_circularity();
$("#normalMode").prop('checked', true);
refresh_stick_pos();
}
function refresh_stick_pos() {
if(!controller) return;
const c = document.getElementById("stickCanvas");
const ctx = c.getContext("2d");
const sz = 60;
const hb = 20 + sz;
const yb = 15 + sz;
const w = c.width;
ctx.clearRect(0, 0, c.width, c.height);
const { left: { x: plx, y: ply }, right: { x: prx, y: pry } } = controller.button_states.sticks;
const enable_zoom_center = center_zoom_checked();
const enable_circ_test = circ_checked();
// Draw left stick
draw_stick_position(ctx, hb, yb, sz, plx, ply, {
circularity_data: enable_circ_test ? ll_data : null,
enable_zoom_center,
});
// Draw right stick
draw_stick_position(ctx, w-hb, yb, sz, prx, pry, {
circularity_data: enable_circ_test ? rr_data : null,
enable_zoom_center,
});
const precision = enable_zoom_center ? 3 : 2;
$("#lx-lbl").text(float_to_str(plx, precision));
$("#ly-lbl").text(float_to_str(ply, precision));
$("#rx-lbl").text(float_to_str(prx, precision));
$("#ry-lbl").text(float_to_str(pry, precision));
// Move L3 and R3 SVG elements according to stick position
try {
// These values are tuned for the SVG's coordinate system and visual effect
const max_stick_offset = 25;
// L3 center in SVG coordinates (from path: cx=295.63, cy=461.03)
const l3_cx = 295.63, l3_cy = 461.03;
// R3 center in SVG coordinates (from path: cx=662.06, cy=419.78)
const r3_cx = 662.06, r3_cy = 419.78;
const l3_x = l3_cx + plx * max_stick_offset;
const l3_y = l3_cy + ply * max_stick_offset;
const l3_group = document.querySelector('g#L3');
l3_group?.setAttribute('transform', `translate(${l3_x - l3_cx},${l3_y - l3_cy}) scale(0.70)`);
const r3_x = r3_cx + prx * max_stick_offset;
const r3_y = r3_cy + pry * max_stick_offset;
const r3_group = document.querySelector('g#R3');
r3_group?.setAttribute('transform', `translate(${r3_x - r3_cx},${r3_y - r3_cy}) scale(0.70)`);
} catch (e) {
// Fail silently if SVG not present
}
}
const circ_checked = () => $("#checkCircularityMode").is(':checked');
const center_zoom_checked = () => $("#centerZoomMode").is(':checked');
function resetStickDiagrams() {
clear_circularity();
refresh_stick_pos();
}
const on_stick_mode_change = () => resetStickDiagrams();
const throttled_refresh_sticks = (() => {
let delay = null;
return function(changes) {
if (!changes.sticks) return;
if (delay) return;
refresh_stick_pos();
delay = setTimeout(() => {
delay = null;
refresh_stick_pos();
}, 20);
};
})();
const update_stick_graphics = (changes) => throttled_refresh_sticks(changes);
function update_battery_status({/* bat_capacity, cable_connected, is_charging, is_error, */ bat_txt, changed}) {
if(changed) {
$("#d-bat").html(bat_txt);
}
}
function update_ds_button_svg(changes, BUTTON_MAP) {
if (!changes || Object.keys(changes).length === 0) return;
const pressedColor = '#1a237e'; // pleasing dark blue
// Update L2/R2 analog infill
for (const trigger of ['l2', 'r2']) {
const key = trigger + '_analog';
if (changes.hasOwnProperty(key)) {
const val = changes[key];
const t = val / 255;
const color = lerp_color('#ffffff', pressedColor, t);
const svg = trigger.toUpperCase() + '_infill';
const infill = document.getElementById(svg);
set_svg_group_color(infill, color);
}
}
// Update dpad buttons
for (const dir of ['up', 'right', 'down', 'left']) {
if (changes.hasOwnProperty(dir)) {
const pressed = changes[dir];
const group = document.getElementById(dir.charAt(0).toUpperCase() + dir.slice(1) + '_infill');
set_svg_group_color(group, pressed ? pressedColor : 'white');
}
}
// Update other buttons
for (const btn of BUTTON_MAP) {
if (['up', 'right', 'down', 'left'].includes(btn.name)) continue; // Dpad handled above
if (changes.hasOwnProperty(btn.name) && btn.svg) {
const pressed = changes[btn.name];
const group = document.getElementById(btn.svg + '_infill');
set_svg_group_color(group, pressed ? pressedColor : 'white');
}
}
}
function set_svg_group_color(group, color) {
if (group) {
const elements = group.querySelectorAll('path,rect,circle,ellipse,line,polyline,polygon');
elements.forEach(el => {
// Set up a smooth transition for fill and stroke if not already set
if (!el.style.transition) {
el.style.transition = 'fill 0.10s, stroke 0.10s';
}
el.setAttribute('fill', color);
el.setAttribute('stroke', color);
});
}
}
let hasActiveTouchPoints = false;
let trackpadBbox = undefined;
function update_touchpad_circles(points) {
const hasActivePointsNow = points.some(pt => pt.active);
if(!hasActivePointsNow && !hasActiveTouchPoints) return;
// Find the Trackpad_infill group in the SVG
const svg = document.getElementById('controller-svg');
const trackpad = svg?.querySelector('g#Trackpad_infill');
if (!trackpad) return;
// Remove the previous touch points, if any
trackpad.querySelectorAll('circle.ds-touch').forEach(c => c.remove());
hasActiveTouchPoints = hasActivePointsNow;
trackpadBbox = trackpadBbox ?? trackpad.querySelector('path')?.getBBox();
// Draw up to 2 circles
points.forEach((pt, idx) => {
if (!pt.active) return;
// Map raw x/y to SVG
// DS4/DS5 touchpad is 1920x943 units (raw values)
const RAW_W = 1920, RAW_H = 943;
const pointRadius = trackpadBbox.width * 0.05;
const cx = trackpadBbox.x + pointRadius + (pt.x / RAW_W) * (trackpadBbox.width - pointRadius*2);
const cy = trackpadBbox.y + pointRadius + (pt.y / RAW_H) * (trackpadBbox.height - pointRadius*2);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('class', 'ds-touch');
circle.setAttribute('cx', cx);
circle.setAttribute('cy', cy);
circle.setAttribute('r', pointRadius);
circle.setAttribute('fill', idx === 0 ? '#2196f3' : '#e91e63');
circle.setAttribute('fill-opacity', '0.5');
circle.setAttribute('stroke', '#3399cc');
circle.setAttribute('stroke-width', '4');
trackpad.appendChild(circle);
});
}
function get_current_main_tab() {
const mainTabs = document.getElementById('mainTabs');
const activeBtn = mainTabs?.querySelector('.nav-link.active');
return activeBtn?.id || 'controller-tab';
}
function get_current_test_tab() {
const testsList = document.getElementById('tests-list');
const activeBtn = testsList?.querySelector('.list-group-item.active');
return activeBtn?.id || 'haptic-test-tab';
}
// Callback function to handle UI updates after controller input processing
function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatus }) {
const { buttonMap } = inputConfig;
const current_active_tab = get_current_main_tab();
switch (current_active_tab) {
case 'controller-tab': // Main controller tab
collectCircularityData(changes.sticks, ll_data, rr_data);
if(isFinetuneVisible()) {
finetune_handle_controller_input(changes);
} else {
update_stick_graphics(changes);
update_ds_button_svg(changes, buttonMap);
update_touchpad_circles(touchPoints);
}
break;
case 'tests-tab':
handle_test_input(changes);
break;
}
update_battery_status(batteryStatus);
}
function handle_test_input(/* changes */) {
const current_test_tab = get_current_test_tab();
// Handle different test tabs
switch (current_test_tab) {
case 'haptic-test-tab':
// Handle L2/R2 for haptic feedback
const l2 = controller.button_states.l2_analog || 0;
const r2 = controller.button_states.r2_analog || 0;
if (l2 || r2) {
trigger_haptic_motors(l2, r2);
}
break;
// Add more test tabs here as needed
default:
console.log("Unknown test tab:", current_test_tab);
break;
}
}
function update_disable_btn() {
const { disable_btn, last_disable_btn } = app;
if(disable_btn == last_disable_btn)
return;
if(disable_btn == 0) {
$(".ds-btn").prop("disabled", false);
app.last_disable_btn = 0;
return;
}
$(".ds-btn").prop("disabled", true);
// show only one popup
if(disable_btn & 1 && !(last_disable_btn & 1)) {
show_popup(l("The device appears to be a DS4 clone. All functionalities are disabled."));
} else if(disable_btn & 2 && !(last_disable_btn & 2)) {
show_popup(l("This DualSense controller has outdated firmware.") + "<br>" + l("Please update the firmware and try again."), true);
} else if(disable_btn & 4 && !(last_disable_btn & 4)) {
show_popup(l("Please charge controller battery over 30% to use this tool."));
}
app.last_disable_btn = disable_btn;
}
async function handleLanguageChange() {
if(!controller) return;
const { infoItems } = await controller.getDeviceInfo();
render_info_to_dom(infoItems);
}
function handleNvStatusUpdate(nv) {
// Refresh NVS status display when it changes
render_nvstatus_to_dom(nv);
}
async function flash_all_changes() {
// For DS5 Edge controllers, pass the progress callback
const progressCallback = controller.getModel() == "DS5_Edge" ? set_edge_progress : null;
const result = await controller.flash(progressCallback);
if (result?.success) {
show_popup(result.message, result.isHtml);
}
}
async function reboot_controller() {
await controller.reset();
}
async function nvsunlock() {
await controller.nvsUnlock();
}
async function nvslock() {
return await controller.nvsLock();
}
function close_all_modals() {
$('.modal.show').modal('hide'); // Close any open modals
}
function set_progress(i) {
$(".progress-bar").css('width', '' + i + '%')
}
function render_info_to_dom(infoItems) {
// Clear all info sections
$("#fwinfo").html("");
$("#fwinfoextra-hw").html("");
$("#fwinfoextra-fw").html("");
if (!Array.isArray(infoItems)) return;
// Add new info items
infoItems.forEach(({key, value, addInfoIcon, severity, isExtra, cat}) => {
if (!key) return;
// Compose value with optional info icon
let valueHtml = String(value ?? "");
if (addInfoIcon === 'board') {
const icon = '&nbsp;<a class="link-body-emphasis" href="#" onclick="board_model_info()">' +
'<svg class="bi" width="1.3em" height="1.3em"><use xlink:href="#info"/></svg></a>';
valueHtml += icon;
} else if (addInfoIcon === 'color') {
const icon = '&nbsp;<a class="link-body-emphasis" href="#" onclick="edge_color_info()">' +
'<svg class="bi" width="1.3em" height="1.3em"><use xlink:href="#info"/></svg></a>';
valueHtml += icon;
}
// Apply severity formatting if requested
if (severity) {
const colors = { danger: 'red', success: 'green' }
const color = colors[severity] || 'black';
valueHtml = `<font color='${color}'><b>${valueHtml}</b></font>`;
}
if (isExtra) {
append_info_extra(key, valueHtml, cat || "hw");
} else {
append_info(key, valueHtml, cat || "hw");
}
});
}
function append_info_extra(key, value, cat) {
// TODO escape html
const s = '<dt class="text-muted col-sm-4 col-md-6 col-xl-5">' + key + '</dt><dd class="col-sm-8 col-md-6 col-xl-7" style="text-align: right;">' + value + '</dd>';
$("#fwinfoextra-" + cat).html($("#fwinfoextra-" + cat).html() + s);
}
function append_info(key, value, cat) {
// TODO escape html
const s = '<dt class="text-muted col-6">' + key + '</dt><dd class="col-6" style="text-align: right;">' + value + '</dd>';
$("#fwinfo").html($("#fwinfo").html() + s);
append_info_extra(key, value, cat);
}
function show_popup(text, is_html = false) {
if(is_html) {
$("#popupBody").html(text);
} else {
$("#popupBody").text(text);
}
bootstrap.Modal.getOrCreateInstance('#popupModal').show();
}
function show_faq_modal() {
la("faq_modal");
bootstrap.Modal.getOrCreateInstance('#faqModal').show();
}
function show_donate_modal() {
la("donate_modal");
bootstrap.Modal.getOrCreateInstance('#donateModal').show();
}
function show_edge_modal() {
la("edge_modal");
bootstrap.Modal.getOrCreateInstance('#edgeModal').show();
}
function show_info_tab() {
la("info_modal");
$('#info-tab').tab('show');
}
function discord_popup() {
la("discord_popup");
show_popup(l("My handle on discord is: the_al"));
}
function edge_color_info() {
la("cm_info");
const text = l("Color detection thanks to") + ' romek77 from Poland.';
show_popup(text, true);
}
function board_model_info() {
la("bm_info");
const l1 = l("This feature is experimental.");
const l2 = l("Please let me know if the board model of your controller is not detected correctly.");
const l3 = l("Board model detection thanks to") + ' <a href="https://battlebeavercustoms.com/">Battle Beaver Customs</a>.';
show_popup(l3 + "<br><br>" + l1 + " " + l2, true);
}
const trigger_haptic_motors = (() => {
let haptic_timeout = undefined;
let haptic_last_trigger = 0;
return async function(strong_motor /*left*/, weak_motor /*right*/) {
// The DS4 contoller has a strong (left) and a weak (right) motor.
// The DS5 emulates the same behavior, but the left and right motors are the same.
const now = Date.now();
if (now - haptic_last_trigger < 200) {
return; // Rate limited - ignore calls within 200ms
}
haptic_last_trigger = now;
try {
if (!controller.isConnected()) return;
const model = controller.getModel();
const device = controller.getDevice();
if (model == "DS4") {
const data = new Uint8Array([0x05, 0x00, 0, weak_motor, strong_motor]);
await device.sendReport(0x05, data);
} else if (model.startsWith("DS5")) {
const data = new Uint8Array([0x02, 0x00, weak_motor, strong_motor]);
await device.sendReport(0x02, data);
}
// Stop rumble after duration
clearTimeout(haptic_timeout);
haptic_timeout = setTimeout(stop_haptic_motors, 250);
} catch(e) {
throw new Error(l("Error triggering rumble: ") + e);
}
};
})();
async function stop_haptic_motors() {
if (!controller.isConnected()) return;
const model = controller.getModel();
const device = controller.getDevice();
if (model == "DS4") {
const data = new Uint8Array([0x05, 0x00, 0, 0, 0]);
await device.sendReport(0x05, data);
} else if (model.startsWith("DS5")) {
const data = new Uint8Array([0x02, 0x00, 0, 0]);
await device.sendReport(0x02, data);
}
}
// Export functions to global scope for HTML onclick handlers
window.gboot = gboot;
window.connect = connect;
window.disconnect = disconnectSync;
window.show_faq_modal = show_faq_modal;
window.show_info_tab = show_info_tab;
window.calibrate_range = () => calibrate_range(controller, { resetStickDiagrams, show_popup });
window.calibrate_stick_centers = () => calibrate_stick_centers(controller, { resetStickDiagrams, show_popup, set_progress });
window.auto_calibrate_stick_centers = () => auto_calibrate_stick_centers(controller, { resetStickDiagrams, show_popup, set_progress });
window.ds5_finetune = () => ds5_finetune(controller, { ll_data, rr_data, clear_circularity });
window.flash_all_changes = flash_all_changes;
window.reboot_controller = reboot_controller;
window.refresh_nvstatus = refresh_nvstatus;
window.nvsunlock = nvsunlock;
window.nvslock = nvslock;
window.welcome_accepted = welcome_accepted;
window.show_donate_modal = show_donate_modal;
window.board_model_info = board_model_info;
window.edge_color_info = edge_color_info;

View File

@@ -0,0 +1,237 @@
'use strict';
import { sleep, la } from '../utils.js';
import { l } from '../translations.js';
/**
* Calibration Center Modal Class
* Handles step-by-step manual stick center calibration
*/
export class CalibCenterModal {
constructor(controllerInstance, { resetStickDiagrams, show_popup, set_progress }) {
this.controller = controllerInstance;
this.resetStickDiagrams = resetStickDiagrams;
this.show_popup = show_popup;
this.set_progress = set_progress;
this._initEventListeners();
// Hide the spinner in case it's showing after prior failure
$("#calibNext").prop("disabled", false);
$("#btnSpinner").hide();
}
/**
* Initialize event listeners for the calibration modal
*/
_initEventListeners() {
$('#calibCenterModal').on('hidden.bs.modal', () => {
console.log("Closing calibration modal");
destroyCurrentInstance();
});
}
/**
* Remove event listeners
*/
removeEventListeners() {
$('#calibCenterModal').off('hidden.bs.modal');
}
/**
* Open the calibration modal
*/
async open() {
la("calib_open");
this.calibrationGenerator = this.calibrationSteps();
await this.next();
new bootstrap.Modal(document.getElementById('calibCenterModal'), {}).show();
}
/**
* Proceed to the next calibration step (legacy method)
*/
async next() {
la("calib_next");
const result = await this.calibrationGenerator.next();
if (result.done) {
this.calibrationGenerator = null;
}
}
/**
* Generator function for calibration steps
*/
async* calibrationSteps() {
// Step 1: Initial setup
la("calib_step", {"i": 1});
this._updateUI(1, "Stick center calibration", "Start", true);
yield 1;
// Step 2: Initialize calibration
la("calib_step", {"i": 2});
this._showSpinner("Initializing...");
await sleep(100);
await this._multiCalibSticksBegin();
await this._hideSpinner();
this._updateUI(2, "Calibration in progress", "Continue", false);
yield 2;
// Steps 3-5: Sample calibration data
for (let sampleStep = 3; sampleStep <= 5; sampleStep++) {
la("calib_step", {"i": sampleStep});
this._showSpinner("Sampling...");
await sleep(150);
await this._multiCalibSticksSample();
await this._hideSpinner();
this._updateUI(sampleStep, "Calibration in progress", "Continue", false);
yield sampleStep;
}
// Step 6: Final sampling and storage
la("calib_step", {"i": 6});
this._showSpinner("Sampling...");
await this._multiCalibSticksSample();
await sleep(200);
$("#calibNextText").text(l("Storing calibration..."));
await sleep(500);
await this._multiCalibSticksEnd();
await this._hideSpinner();
this._updateUI(6, "Stick center calibration", "Done", true);
yield 6;
this._close();
}
/**
* "Old" fully automatic stick center calibration
*/
async multiCalibrateSticks() {
if(!this.controller.isConnected())
return;
this.set_progress(0);
new bootstrap.Modal(document.getElementById('calibrateModal'), {}).show();
await sleep(1000);
// Use the controller manager's calibrateSticks method with UI progress updates
this.set_progress(10);
const result = await this.controller.calibrateSticks((progress) => {
this.set_progress(progress);
});
await sleep(500);
this._close();
this.resetStickDiagrams();
if (result?.message) {
this.show_popup(result.message);
}
}
/**
* Helper functions for step-by-step manual calibration UI
*/
async _multiCalibSticksBegin() {
await this.controller.calibrateSticksBegin();
}
async _multiCalibSticksEnd() {
await this.controller.calibrateSticksEnd();
}
async _multiCalibSticksSample() {
await this.controller.calibrateSticksSample();
}
/**
* Close the calibration modal
*/
_close() {
$(".modal.show").modal("hide");
}
/**
* Update the UI for a specific calibration step
*/
_updateUI(step, title, buttonText, allowDismiss) {
// Hide all step lists and remove active class
for (let j = 1; j < 7; j++) {
$("#list-" + j).hide();
$("#list-" + j + "-calib").removeClass("active");
}
// Show current step and mark as active
$("#list-" + step).show();
$("#list-" + step + "-calib").addClass("active");
// Update title and button text
$("#calibTitle").text(l(title));
$("#calibNextText").text(l(buttonText));
// Show/hide cross icon
if (allowDismiss) {
$("#calibCross").show();
} else {
$("#calibCross").hide();
}
}
/**
* Show spinner and disable button
*/
_showSpinner(text) {
$("#calibNextText").text(l(text));
$("#btnSpinner").show();
$("#calibNext").prop("disabled", true);
}
/**
* Hide spinner and enable button
*/
async _hideSpinner() {
await sleep(200);
$("#calibNext").prop("disabled", false);
$("#btnSpinner").hide();
}
}
// Global reference to the current calibration instance
let currentCalibCenterInstance = null;
/**
* Helper function to safely clear the current calibration instance
*/
function destroyCurrentInstance() {
if (currentCalibCenterInstance) {
console.log("Destroying current calibration instance");
currentCalibCenterInstance.removeEventListeners();
currentCalibCenterInstance = null;
}
}
// Legacy function exports for backward compatibility
export async function calibrate_stick_centers(controller, dependencies) {
currentCalibCenterInstance = new CalibCenterModal(controller, dependencies);
await currentCalibCenterInstance.open();
}
async function calib_next() {
if (currentCalibCenterInstance) {
await currentCalibCenterInstance.next();
}
}
// "Old" fully automatic stick center calibration
export async function auto_calibrate_stick_centers(controller, dependencies) {
currentCalibCenterInstance = new CalibCenterModal(controller, dependencies);
await currentCalibCenterInstance.multiCalibrateSticks();
}
// Legacy compatibility - expose functions to window for HTML onclick handlers
window.calib_next = calib_next;

View File

@@ -0,0 +1,62 @@
'use strict';
import { sleep } from '../utils.js';
/**
* Calibrate Stick Range Modal Class
* Handles stick range calibration
*/
export class CalibRangeModal {
constructor(controllerInstance, { resetStickDiagrams, show_popup }) {
// Dependencies
this.controller = controllerInstance;
this.resetStickDiagrams = resetStickDiagrams;
this.show_popup = show_popup;
}
async open() {
if(!this.controller.isConnected())
return;
bootstrap.Modal.getOrCreateInstance('#rangeModal').show();
await sleep(1000);
await this.controller.calibrateRangeBegin();
}
async onClose() {
bootstrap.Modal.getOrCreateInstance('#rangeModal').hide();
this.resetStickDiagrams();
const result = await this.controller.calibrateRangeOnClose();
if (result?.message) {
this.show_popup(result.message);
}
}
}
// Global reference to the current range calibration instance
let currentCalibRangeInstance = null;
/**
* Helper function to safely clear the current calibration instance
*/
function destroyCurrentInstance() {
currentCalibRangeInstance = null;
}
// Legacy function exports for backward compatibility
export async function calibrate_range(controller, dependencies) {
destroyCurrentInstance(); // Clean up any existing instance
currentCalibRangeInstance = new CalibRangeModal(controller, dependencies);
await currentCalibRangeInstance.open();
}
async function calibrate_range_on_close() {
if (currentCalibRangeInstance) {
await currentCalibRangeInstance.onClose();
}
}
// Legacy compatibility - expose functions to window for HTML onclick handlers
window.calibrate_range_on_close = calibrate_range_on_close;

754
js/modals/finetune-modal.js Normal file
View File

@@ -0,0 +1,754 @@
'use strict';
import { draw_stick_position } from '../stick-renderer.js';
import { dec2hex32, float_to_str } from '../utils.js';
const FINETUNE_INPUT_SUFFIXES = ["LL", "LT", "RL", "RT", "LR", "LB", "RR", "RB", "LX", "LY", "RX", "RY"];
/**
* DS5 Finetuning Class
* Handles controller stick calibration and fine-tuning operations
*/
export class Finetune {
constructor() {
this._mode = 'center'; // 'center' or 'circularity'
this.original_data = [];
this.last_written_data = [];
this.active_stick = null; // 'left', 'right', or null
// Dependencies
this.controller = null;
this.ll_data = null;
this.rr_data = null;
this.clearCircularity = null;
// Closure functions
this.refresh_finetune_sticks = this._createRefreshSticksThrottled();
this.update_finetune_warning_messages = this._createUpdateWarningMessagesClosure();
this.flash_finetune_warning = this._createFlashWarningClosure();
// Continuous adjustment state
this.continuous_adjustment = {
initial_delay: null,
repeat_delay: null,
};
}
get mode() {
return this._mode;
}
set mode(mode) {
if (mode !== 'center' && mode !== 'circularity') {
throw new Error(`Invalid finetune mode: ${mode}. Must be 'center' or 'circularity'`);
}
this._mode = mode;
this._updateUI();
}
async init(controllerInstance, { ll_data, rr_data, clear_circularity }) {
this.controller = controllerInstance;
this.ll_data = ll_data;
this.rr_data = rr_data;
this.clearCircularity = clear_circularity;
this._initEventListeners();
this._restoreShowRawNumbersCheckbox();
// Lock NVS before
const nv = await this.controller.queryNvStatus();
if(!nv.locked) {
const res = await this.controller.nvsLock();
if(!res.ok) {
return;
}
const nv2 = await this.controller.queryNvStatus();
if(!nv2.locked) {
const errTxt = "0x" + dec2hex32(nv2.raw);
throw new Error("ERROR: Cannot lock NVS (" + errTxt + ")");
}
} else if(nv.status !== 'locked') {
throw new Error("ERROR: Cannot read NVS status. Finetuning is not safe on this device.");
}
const data = await this._readFinetuneData();
const modal = new bootstrap.Modal(document.getElementById('finetuneModal'), {})
modal.show();
const maxValue = this.controller.getFinetuneMaxValue();
FINETUNE_INPUT_SUFFIXES.forEach((suffix, i) => {
const el = $("#finetune" + suffix);
el.attr('max', maxValue);
el.val(data[i]);
});
// Start in center mode
this.setMode('center');
this.setStickToFinetune('left');
// Initialize the raw numbers display state
this._showRawNumbersChanged();
this.original_data = data;
this.refresh_finetune_sticks();
}
/**
* Initialize event listeners for the finetune modal
*/
_initEventListeners() {
FINETUNE_INPUT_SUFFIXES.forEach((suffix) => {
$("#finetune" + suffix).on('change', () => this._onFinetuneChange());
});
// Set up mode toggle event listeners
$("#finetuneModeCenter").on('change', (e) => {
if (e.target.checked) {
this.setMode('center');
}
});
$("#finetuneModeCircularity").on('change', (e) => {
if (e.target.checked) {
this.setMode('circularity');
}
});
$("#showRawNumbersCheckbox").on('change', () => {
this._showRawNumbersChanged();
});
$("#left-stick-card").on('click', () => {
console.log("Left stick card clicked");
this.setStickToFinetune('left');
});
$("#right-stick-card").on('click', () => {
this.setStickToFinetune('right');
});
$('#finetuneModal').on('hidden.bs.modal', () => {
console.log("Finetune modal hidden event triggered");
destroyCurrentInstance();
});
}
/**
* Clean up event listeners for the finetune modal
*/
removeEventListeners() {
FINETUNE_INPUT_SUFFIXES.forEach((suffix) => {
$("#finetune" + suffix).off('change');
});
// Remove mode toggle event listeners
$("#finetuneModeCenter").off('change');
$("#finetuneModeCircularity").off('change');
// Remove other event listeners
$("#showRawNumbersCheckbox").off('change');
$("#left-stick-card").off('click');
$("#right-stick-card").off('click');
$('#finetuneModal').off('hidden.bs.modal');
}
/**
* Handle mode switching based on controller input
*/
handleModeSwitching(changes) {
if (changes.l1) {
this.setMode('center');
this._clearFinetuneAxisHighlights();
} else if (changes.r1) {
this.setMode('circularity');
this._clearFinetuneAxisHighlights();
}
}
/**
* Handle stick switching based on controller input
*/
handleStickSwitching(changes) {
if (changes.sticks) {
this._updateActiveStickBasedOnMovement();
}
}
/**
* Handle D-pad adjustments for finetuning
*/
handleDpadAdjustment(changes) {
if(!this.active_stick) return;
if (this._mode === 'center') {
this._handleCenterModeAdjustment(changes);
} else {
this._handleCircularityModeAdjustment(changes);
}
}
/**
* Save finetune changes
*/
save() {
// Unlock save button
this.controller.setHasChangesToWrite(true);
this._close();
}
/**
* Cancel finetune changes and restore original data
*/
async cancel() {
if(this.original_data.length == 12)
await this._writeFinetuneData(this.original_data)
this._close();
}
/**
* Set the finetune mode
*/
setMode(mode) {
this.mode = mode;
}
/**
* Set which stick to finetune
*/
setStickToFinetune(stick) {
if(this.active_stick === stick) {
return;
}
// Stop any continuous adjustments when switching sticks
this.stopContinuousDpadAdjustment();
this._clearFinetuneAxisHighlights();
this.active_stick = stick;
const other_stick = stick === 'left' ? 'right' : 'left';
$(`#${this.active_stick}-stick-card`).addClass("stick-card-active");
$(`#${other_stick}-stick-card`).removeClass("stick-card-active");
}
// Private methods
/**
* Restore the show raw numbers checkbox state from localStorage
*/
_restoreShowRawNumbersCheckbox() {
const savedState = localStorage.getItem('showRawNumbersCheckbox');
if (savedState) {
const isChecked = savedState === 'true';
$("#showRawNumbersCheckbox").prop('checked', isChecked);
}
}
/**
* Check if stick is in extreme position (close to edges)
* @param {Object} stick - Stick object with x and y properties
* @returns {boolean} True if stick is in extreme position
*/
_isStickInExtremePosition(stick) {
const primeAxis = Math.max(Math.abs(stick.x), Math.abs(stick.y));
const otherAxis = Math.min(Math.abs(stick.x), Math.abs(stick.y));
return primeAxis >= 0.5 && otherAxis < 0.2;
}
_updateUI() {
// Clear circularity data - we'll call this from core.js
this.clearCircularity();
const modal = $('#finetuneModal');
if (this._mode === 'center') {
$("#finetuneModeCenter").prop('checked', true);
modal.removeClass('circularity-mode');
} else if (this._mode === 'circularity') {
$("#finetuneModeCircularity").prop('checked', true);
modal.addClass('circularity-mode');
}
}
async _onFinetuneChange() {
const out = FINETUNE_INPUT_SUFFIXES.map((suffix) => {
const el = $("#finetune" + suffix);
const v = parseInt(el.val());
return isNaN(v) ? 0 : v;
});
await this._writeFinetuneData(out);
}
async _readFinetuneData() {
const data = await ds5_get_inmemory_module_data(); //mm there's also a missing await here
if(!data) {
throw new Error("ERROR: Cannot read calibration data");
}
this.last_written_data = data;
return data;
}
async _writeFinetuneData(data) {
if (data.length != 12) {
return;
}
// const deepEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
// if (deepEqual(data, this.last_written_data)) {
// if (data == this.last_written_data) { //mm this will never be true, but fixing it (per above) breaks Edge writes
// return;
// }
this.last_written_data = data
if (this.controller.isConnected()) {
await this.controller.writeFinetuneData(data);
}
}
_createRefreshSticksThrottled() {
let timeout = null;
return () => {
if (timeout) return;
timeout = setTimeout(() => {
const { left, right } = this.controller.button_states.sticks;
this._ds5FinetuneUpdate("finetuneStickCanvasL", left.x, left.y);
this._ds5FinetuneUpdate("finetuneStickCanvasR", right.x, right.y);
this.update_finetune_warning_messages();
this._highlightActiveFinetuneAxis();
timeout = null;
}, 10);
};
}
_createUpdateWarningMessagesClosure() {
let timeout = null; // to stop unnecessary flicker in center mode
return () => {
if(!this.active_stick) return;
const currentStick = this.controller.button_states.sticks[this.active_stick];
if (this._mode === 'center') {
const isNearCenter = Math.abs(currentStick.x) <= 0.5 && Math.abs(currentStick.y) <= 0.5;
if(!isNearCenter && timeout) return;
clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
if(this._mode !== 'center') return; // in case it changed during timeout
$('#finetuneCenterSuccess').toggle(isNearCenter);
$('#finetuneCenterWarning').toggle(!isNearCenter);
}, isNearCenter ? 0 : 200);
}
if (this._mode === 'circularity') {
// Check if stick is in extreme position (close to edges)
const isInExtremePosition = this._isStickInExtremePosition(currentStick);
$('#finetuneCircularitySuccess').toggle(isInExtremePosition);
$('#finetuneCircularityWarning').toggle(!isInExtremePosition);
}
};
}
_clearFinetuneAxisHighlights(to_clear = {center: true, circularity: true}) {
const { center, circularity } = to_clear;
if(this._mode === 'center' && center || this._mode === 'circularity' && circularity) {
// Clear label highlights
const labelIds = ["Lx-lbl", "Ly-lbl", "Rx-lbl", "Ry-lbl"];
labelIds.forEach(suffix => {
$(`#finetuneStickCanvas${suffix}`).removeClass("text-primary");
});
}
}
_highlightActiveFinetuneAxis(opts = {}) {
if(!this.active_stick) return;
if (this._mode === 'center') {
const { axis } = opts;
if(!axis) return;
this._clearFinetuneAxisHighlights({center: true});
const labelSuffix = `${this.active_stick === 'left' ? "L" : "R"}${axis.toLowerCase()}`;
$(`#finetuneStickCanvas${labelSuffix}-lbl`).addClass("text-primary");
} else {
this._clearFinetuneAxisHighlights({circularity: true});
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
// Only highlight if stick is moved significantly from center
const deadzone = 0.5;
if (Math.abs(currentStick.x) >= deadzone || Math.abs(currentStick.y) >= deadzone) {
const quadrant = this._getStickQuadrant(currentStick.x, currentStick.y);
const inputSuffix = this._getFinetuneInputSuffixForQuadrant(this.active_stick, quadrant);
if (inputSuffix) {
// Highlight the corresponding LX/LY label to observe
const labelId = `finetuneStickCanvas${
this.active_stick === 'left' ? 'L' : 'R'}${
quadrant === 'left' || quadrant === 'right' ? 'x' : 'y'}-lbl`;
$(`#${labelId}`).addClass("text-primary");
}
}
}
}
_ds5FinetuneUpdate(name, plx, ply) {
const showRawNumbers = $("#showRawNumbersCheckbox").is(":checked");
const canvasId = `${name}${showRawNumbers ? '' : '_large'}`;
const c = document.getElementById(canvasId);
if (!c) {
console.error(`Canvas element not found: ${canvasId}`);
return;
}
const ctx = c.getContext("2d");
const margins = showRawNumbers ? 15 : 5;
const radius = c.width / 2 - margins;
const sz = c.width/2 - margins;
const hb = radius + margins;
const yb = radius + margins;
ctx.clearRect(0, 0, c.width, c.height);
const isLeftStick = name === "finetuneStickCanvasL";
const highlight = this.active_stick == (isLeftStick ? 'left' : 'right') && this._isDpadAdjustmentActive();
if (this._mode === 'circularity') {
// Draw stick position with circle
draw_stick_position(ctx, hb, yb, sz, plx, ply, {
circularity_data: isLeftStick ? this.ll_data : this.rr_data,
highlight
});
} else {
// Draw stick position with crosshair
draw_stick_position(ctx, hb, yb, sz, plx, ply, {
enable_zoom_center: true,
highlight
});
}
$("#"+ name + "x-lbl").text(float_to_str(plx, 3));
$("#"+ name + "y-lbl").text(float_to_str(ply, 3));
}
_showRawNumbersChanged() {
const showRawNumbers = $("#showRawNumbersCheckbox").is(":checked");
const modal = $("#finetuneModal");
modal.toggleClass("hide-raw-numbers", !showRawNumbers);
localStorage.setItem('showRawNumbersCheckbox', showRawNumbers);
this.refresh_finetune_sticks();
}
_close() {
console.log("Closing finetune modal");
$("#finetuneModal").modal("hide");
}
_isStickAwayFromCenter(stick_pos, deadzone = 0.2) {
return Math.abs(stick_pos.x) >= deadzone || Math.abs(stick_pos.y) >= deadzone;
}
_updateActiveStickBasedOnMovement() {
const sticks = this.controller.button_states.sticks;
const deadzone = 0.2;
const left_is_away = this._isStickAwayFromCenter(sticks.left, deadzone);
const right_is_away = this._isStickAwayFromCenter(sticks.right, deadzone);
if (left_is_away && right_is_away) {
// Both sticks are away from center - clear highlighting
this._clearActiveStick();
} else if (left_is_away && !right_is_away) {
// Only left stick is away from center
this.setStickToFinetune('left');
} else if (right_is_away && !left_is_away) {
// Only right stick is away from center
this.setStickToFinetune('right');
}
// If both sticks are centered, keep current active stick (no change)
}
_clearActiveStick() {
// Remove active class from both cards
$("#left-stick-card").removeClass("stick-card-active");
$("#right-stick-card").removeClass("stick-card-active");
this.active_stick = null; // Clear active stick
this._clearFinetuneAxisHighlights();
}
_getStickQuadrant(x, y) {
// Determine which quadrant the stick is in based on x,y coordinates
// x and y are normalized values between -1 and 1
if (Math.abs(x) > Math.abs(y)) {
return x > 0 ? 'right' : 'left';
} else {
return y > 0 ? 'down' : 'up';
}
}
_getFinetuneInputSuffixForQuadrant(stick, quadrant) {
// This function should only be used in circularity mode
// In center mode, we don't care about quadrants - use direct axis mapping instead
if (this._mode === 'center') {
// This function shouldn't be called in center mode
console.warn('get_finetune_input_suffix_for_quadrant called in center mode - this should not happen');
return null;
}
// Circularity mode: map quadrants to specific calibration points
if (stick === 'left') {
switch (quadrant) {
case 'left': return "LL";
case 'up': return "LT";
case 'right': return "LR";
case 'down': return "LB";
}
} else if (stick === 'right') {
switch (quadrant) {
case 'left': return "RL";
case 'up': return "RT";
case 'right': return "RR";
case 'down': return "RB";
}
}
return null; // Invalid
}
_handleCenterModeAdjustment(changes) {
const adjustmentStep = 5; // Use consistent step size for center mode
// Define button mappings for center mode
const buttonMappings = [
{ buttons: ['left', 'square'], adjustment: adjustmentStep, axis: 'X' },
{ buttons: ['right', 'circle'], adjustment: -adjustmentStep, axis: 'X' },
{ buttons: ['up', 'triangle'], adjustment: adjustmentStep, axis: 'Y' },
{ buttons: ['down', 'cross'], adjustment: -adjustmentStep, axis: 'Y' }
];
// Check if any relevant button was released
const relevantButtons = ['left', 'right', 'square', 'circle', 'up', 'down', 'triangle', 'cross'];
if (relevantButtons.some(button => changes[button] === false)) {
this.stopContinuousDpadAdjustment();
return;
}
// Check for button presses
for (const mapping of buttonMappings) {
// Check if active stick is away from center (> 0.5)
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
const stickAwayFromCenter = Math.abs(currentStick.x) > 0.5 || Math.abs(currentStick.y) > 0.5;
if (stickAwayFromCenter && this._isNavigationKeyPressed()) {
this.flash_finetune_warning();
return;
}
if (mapping.buttons.some(button => changes[button])) {
this._highlightActiveFinetuneAxis({axis: mapping.axis});
this._startContinuousDpadAdjustmentCenterMode(this.active_stick, mapping.axis, mapping.adjustment);
return;
}
}
}
_isNavigationKeyPressed() {
const nav_buttons = ['left', 'right', 'up', 'down', 'square', 'circle', 'triangle', 'cross'];
return nav_buttons.some(button => this.controller.button_states[button] === true);
}
_createFlashWarningClosure() {
let timeout = null;
return () => {
function toggle() {
$("#finetuneCenterWarning").toggleClass(['alert-warning', 'alert-danger']);
$("#finetuneCircularityWarning").toggleClass(['alert-warning', 'alert-danger']);
}
if(timeout) return;
toggle(); // on
timeout = setTimeout(() => {
toggle(); // off
timeout = null;
}, 300);
};
}
_handleCircularityModeAdjustment({sticks: _, ...changes}) {
const sticks = this.controller.button_states.sticks;
const currentStick = sticks[this.active_stick];
// Only adjust if stick is moved significantly from center
const isInExtremePosition = this._isStickInExtremePosition(currentStick);
if (!isInExtremePosition) {
this.stopContinuousDpadAdjustment();
if(this._isNavigationKeyPressed()) {
this.flash_finetune_warning();
}
return;
}
const quadrant = this._getStickQuadrant(currentStick.x, currentStick.y);
// Use different step sizes based on quadrant - right/down values are much larger
const adjustmentStep = (quadrant === 'right' || quadrant === 'down') ? 15 : 3;
// Define button mappings for each quadrant type
const horizontalButtons = ['left', 'right', 'square', 'circle'];
const verticalButtons = ['up', 'down', 'triangle', 'cross'];
let adjustment = 0;
let relevantButtons = [];
if (quadrant === 'left' || quadrant === 'right') {
// Horizontal quadrants: left increases, right decreases
relevantButtons = horizontalButtons;
if (changes.left || changes.square) {
adjustment = adjustmentStep;
} else if (changes.right || changes.circle) {
adjustment = -adjustmentStep;
}
} else if (quadrant === 'up' || quadrant === 'down') {
// Vertical quadrants: up increases, down decreases
relevantButtons = verticalButtons;
if (changes.up || changes.triangle) {
adjustment = adjustmentStep;
} else if (changes.down || changes.cross) {
adjustment = -adjustmentStep;
}
}
// Check if any relevant button was released
if (relevantButtons.some(button => changes[button] === false)) {
this.stopContinuousDpadAdjustment();
return;
}
// Start continuous adjustment on button press
if (adjustment !== 0) {
this._startContinuousDpadAdjustment(this.active_stick, quadrant, adjustment);
}
}
_startContinuousDpadAdjustment(stick, quadrant, adjustment) {
const inputSuffix = this._getFinetuneInputSuffixForQuadrant(stick, quadrant);
this._startContinuousAdjustmentWithSuffix(inputSuffix, adjustment);
}
_startContinuousDpadAdjustmentCenterMode(stick, targetAxis, adjustment) {
// In center mode, directly map to X/Y axes
const inputSuffix = stick === 'left' ?
(targetAxis === 'X' ? 'LX' : 'LY') :
(targetAxis === 'X' ? 'RX' : 'RY');
this._startContinuousAdjustmentWithSuffix(inputSuffix, adjustment);
}
_startContinuousAdjustmentWithSuffix(inputSuffix, adjustment) {
this.stopContinuousDpadAdjustment();
const element = $(`#finetune${inputSuffix}`);
if (!element.length) return;
// Perform initial adjustment immediately...
this._performDpadAdjustment(element, adjustment);
this.clearCircularity();
// ...then prime continuous adjustment
this.continuous_adjustment.initial_delay = setTimeout(() => {
this.continuous_adjustment.repeat_delay = setInterval(() => {
this._performDpadAdjustment(element, adjustment);
this.clearCircularity();
}, 150);
}, 400); // Initial delay before continuous adjustment starts (400ms)
}
stopContinuousDpadAdjustment() {
clearInterval(this.continuous_adjustment.repeat_delay);
this.continuous_adjustment.repeat_delay = null;
clearTimeout(this.continuous_adjustment.initial_delay);
this.continuous_adjustment.initial_delay = null;
}
_isDpadAdjustmentActive() {
return !!this.continuous_adjustment.initial_delay;
}
async _performDpadAdjustment(element, adjustment) {
const currentValue = parseInt(element.val()) || 0;
const maxValue = this.controller.getFinetuneMaxValue();
const newValue = Math.max(0, Math.min(maxValue, currentValue + adjustment));
element.val(newValue);
// Trigger the change event to update the finetune data
await this._onFinetuneChange();
}
}
// Global reference to the current finetune instance
let currentFinetuneInstance = null;
/**
* Helper function to safely clear the current finetune instance
*/
function destroyCurrentInstance() {
if (currentFinetuneInstance) {
currentFinetuneInstance.stopContinuousDpadAdjustment();
currentFinetuneInstance.removeEventListeners();
currentFinetuneInstance = null;
}
}
// Function to create and initialize finetune instance
export async function ds5_finetune(controller, dependencies) {
// Create new instance
currentFinetuneInstance = new Finetune();
await currentFinetuneInstance.init(controller, dependencies);
}
export function finetune_handle_controller_input(changes) {
if (currentFinetuneInstance) {
currentFinetuneInstance.refresh_finetune_sticks();
currentFinetuneInstance.handleModeSwitching(changes);
currentFinetuneInstance.handleStickSwitching(changes);
currentFinetuneInstance.handleDpadAdjustment(changes);
}
}
function finetune_save() {
console.log("Saving finetune changes");
if (currentFinetuneInstance) {
currentFinetuneInstance.save();
}
}
async function finetune_cancel() {
console.log("Cancelling finetune changes");
if (currentFinetuneInstance) {
await currentFinetuneInstance.cancel();
}
}
export function isFinetuneVisible() {
return !!currentFinetuneInstance;
}
window.finetune_cancel = finetune_cancel;
window.finetune_save = finetune_save;

213
js/stick-renderer.js Normal file
View File

@@ -0,0 +1,213 @@
'use strict';
// Constants
export const CIRCULARITY_DATA_SIZE = 48; // Number of angular positions to sample
/**
* Draws analog stick position on a canvas with various visualization options.
* @param {CanvasRenderingContext2D} ctx - Canvas rendering context
* @param {number} center_x - X coordinate of stick center
* @param {number} center_y - Y coordinate of stick center
* @param {number} sz - Size/radius of the stick area
* @param {number} stick_x - Current stick X position (-1 to 1)
* @param {number} stick_y - Current stick Y position (-1 to 1)
* @param {Object} opts - Options object
* @param {number[]|null} opts.circularity_data - Array of circularity test data
* @param {boolean} opts.enable_zoom_center - Whether to apply center zoom transformation
* @param {boolean} opts.highlight - Whether to highlight the stick position
*/
export function draw_stick_position(ctx, center_x, center_y, sz, stick_x, stick_y, opts = {}) {
const { circularity_data = null, enable_zoom_center = false, highlight } = opts;
// Draw base circle
ctx.lineWidth = 1;
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#000000';
ctx.beginPath();
ctx.arc(center_x, center_y, sz, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Helper function for circularity visualization color
function cc_to_color(cc) {
const dd = Math.sqrt(Math.pow((1.0 - cc), 2));
let hh;
if(cc <= 1.0)
hh = 220 - 220 * Math.min(1.0, Math.max(0, (dd - 0.05)) / 0.1);
else
hh = (245 + (360-245) * Math.min(1.0, Math.max(0, (dd - 0.05)) / 0.15)) % 360;
return hh;
}
// Draw circularity visualization if data provided
if (circularity_data?.length > 0) {
const MAX_N = CIRCULARITY_DATA_SIZE;
for(let i = 0; i < MAX_N; i++) {
const kd = circularity_data[i];
const kd1 = circularity_data[(i+1) % CIRCULARITY_DATA_SIZE];
if (kd === undefined || kd1 === undefined) continue;
const ka = i * Math.PI * 2 / MAX_N;
const ka1 = ((i+1)%MAX_N) * 2 * Math.PI / MAX_N;
const kx = Math.cos(ka) * kd;
const ky = Math.sin(ka) * kd;
const kx1 = Math.cos(ka1) * kd1;
const ky1 = Math.sin(ka1) * kd1;
ctx.beginPath();
ctx.moveTo(center_x, center_y);
ctx.lineTo(center_x+kx*sz, center_y+ky*sz);
ctx.lineTo(center_x+kx1*sz, center_y+ky1*sz);
ctx.lineTo(center_x, center_y);
ctx.closePath();
const cc = (kd + kd1) / 2;
const hh = cc_to_color(cc);
ctx.fillStyle = 'hsla(' + parseInt(hh) + ', 100%, 50%, 0.5)';
ctx.fill();
}
}
// Draw circularity error text if enough data provided
if (circularity_data?.filter(n => n > 0.3).length > 10) {
const circularityError = calculateCircularityError(circularity_data);
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#444';
ctx.lineWidth = 3;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '24px Arial';
const text_y = center_y + sz * 0.5;
const text = `${circularityError.toFixed(1)} %`;
ctx.strokeText(text, center_x, text_y);
ctx.fillText(text, center_x, text_y);
}
// Draw crosshairs
ctx.strokeStyle = '#aaaaaa';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(center_x-sz, center_y);
ctx.lineTo(center_x+sz, center_y);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(center_x, center_y-sz);
ctx.lineTo(center_x, center_y+sz);
ctx.closePath();
ctx.stroke();
// Apply center zoom transformation if enabled
let display_x = stick_x;
let display_y = stick_y;
if (enable_zoom_center) {
const transformed = apply_center_zoom(stick_x, stick_y);
display_x = transformed.x;
display_y = transformed.y;
// Draw light gray circle at 50% radius to show border of zoomed center
ctx.strokeStyle = '#d3d3d3'; // light gray
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(center_x, center_y, sz * 0.5, 0, 2 * Math.PI);
ctx.stroke();
}
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
// Draw stick line with variable thickness
// Calculate distance from center
const stick_distance = Math.sqrt(display_x*display_x + display_y*display_y);
const boundary_radius = 0.5; // 50% radius
// Determine if we need to draw a two-segment line
const use_two_segments = enable_zoom_center && stick_distance > boundary_radius;
if (use_two_segments) {
// Calculate boundary point
const boundary_x = (display_x / stick_distance) * boundary_radius;
const boundary_y = (display_y / stick_distance) * boundary_radius;
// First segment: thicker line from center to boundary
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(center_x, center_y);
ctx.lineTo(center_x + boundary_x*sz, center_y + boundary_y*sz);
ctx.stroke();
// Second segment: thinner line from boundary to stick position
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(center_x + boundary_x*sz, center_y + boundary_y*sz);
ctx.lineTo(center_x + display_x*sz, center_y + display_y*sz);
ctx.stroke();
} else {
// Single line from center to stick position
ctx.lineWidth = enable_zoom_center ? 3 : 1;
ctx.beginPath();
ctx.moveTo(center_x, center_y);
ctx.lineTo(center_x + display_x*sz, center_y + display_y*sz);
ctx.stroke();
}
// Draw filled circle at stick position
ctx.beginPath();
ctx.arc(center_x+display_x*sz, center_y+display_y*sz, 3, 0, 2*Math.PI);
if (typeof highlight === 'boolean') {
ctx.fillStyle = highlight ? '#2989f7ff' : '#030b84ff';
}
ctx.fill();
}
/**
* Calculates circularity error for stick movement data.
* @param {number[]} data - Array of distance values at different angular positions
* @returns {number} RMS deviation as percentage
*/
function calculateCircularityError(data) {
// Sum of squared deviations from ideal distance of 1.0, only for values > 0.2
const sumSquaredDeviations = data.reduce((acc, val) =>
val > 0.2 ? acc + Math.pow(val - 1, 2) : acc, 0);
// Calculate RMS deviation as percentage
const validDataCount = data.filter(val => val > 0.2).length;
return validDataCount > 0 ? Math.sqrt(sumSquaredDeviations / validDataCount) * 100 : 0;
}
/**
* Applies center zoom transformation to stick coordinates.
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @returns {Object} Transformed coordinates {x, y}
*/
function apply_center_zoom(x, y) {
// Calculate distance from center
const distance = Math.sqrt(x * x + y * y);
// If distance is 0, return original values
if (distance === 0) {
return { x, y};
}
// Calculate angle
const angle = Math.atan2(y, x);
// Apply center zoom transformation
const new_distance =
distance <= 0.05
? (distance / 0.05) * 0.5 // 0 to 0.05 maps to 0 to 0.5 (half the radius)
: 0.5 + ((distance - 0.05) / 0.95) * 0.5 // 0.05 to 1.0 maps to 0.5 to 1.0 (other half)
// Convert back to x, y coordinates
return {
x: Math.cos(angle) * new_distance,
y: Math.sin(angle) * new_distance
};
}

69
js/template-loader.js Normal file
View File

@@ -0,0 +1,69 @@
'use strict';
// Cache for loaded templates
const templateCache = new Map();
/**
* Load a template from the templates directory
* @param {string} templateName - Name of the template file without extension
* @returns {Promise<string>} - Promise that resolves with the template HTML
*/
async function loadTemplate(templateName) {
// Check if template is already in cache
if (templateCache.has(templateName)) {
return templateCache.get(templateName);
}
try {
// Only append .html if the templateName doesn't already have an extension
const hasExtension = templateName.includes('.');
const templatePath = hasExtension ? `templates/${templateName}` : `templates/${templateName}.html`;
const response = await fetch(templatePath);
if (!response.ok) {
throw new Error(`Failed to load template: ${templateName}`);
}
const templateHtml = await response.text();
templateCache.set(templateName, templateHtml);
return templateHtml;
} catch (error) {
console.error(`Error loading template ${templateName}:`, error);
return '';
}
}
/**
* Load all templates and insert them into the DOM
*/
export async function loadAllTemplates() {
try {
// Load SVG icons
const iconsHtml = await loadTemplate('../assets/icons.svg');
const iconsContainer = document.createElement('div');
iconsContainer.innerHTML = iconsHtml;
document.body.prepend(iconsContainer);
// Load modals
const faqModalHtml = await loadTemplate('faq-modal');
const popupModalHtml = await loadTemplate('popup-modal');
const finetuneModalHtml = await loadTemplate('finetune-modal');
const calibCenterModalHtml = await loadTemplate('calib-center-modal');
const welcomeModalHtml = await loadTemplate('welcome-modal');
const calibrateModalHtml = await loadTemplate('calibrate-modal');
const rangeModalHtml = await loadTemplate('range-modal');
const edgeProgressModalHtml = await loadTemplate('edge-progress-modal');
const edgeModalHtml = await loadTemplate('edge-modal');
const donateModalHtml = await loadTemplate('donate-modal');
// Create modals container
const modalsContainer = document.createElement('div');
modalsContainer.id = 'modals-container';
modalsContainer.innerHTML = faqModalHtml + popupModalHtml + finetuneModalHtml + calibCenterModalHtml + welcomeModalHtml + calibrateModalHtml + rangeModalHtml + edgeProgressModalHtml + edgeModalHtml + donateModalHtml;
document.body.appendChild(modalsContainer);
console.log('All templates loaded successfully');
} catch (error) {
console.error('Error loading templates:', error);
}
}

View File

@@ -0,0 +1,65 @@
<!-- New calibrate modal -->
<div class="modal fade" id="calibCenterModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="calibTitle" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="calibTitle">Stick center calibration</h1>
<button type="button" id="calibCross" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-4">
<div class="list-group" id="list-tab">
<a class="ds-i18n list-group-item list-group-item-action active" id="list-1-calib">Welcome</a>
<a class="ds-i18n list-group-item list-group-item-action" id="list-2-calib">Step 1</a>
<a class="ds-i18n list-group-item list-group-item-action" id="list-3-calib">Step 2</a>
<a class="ds-i18n list-group-item list-group-item-action" id="list-4-calib">Step 3</a>
<a class="ds-i18n list-group-item list-group-item-action" id="list-5-calib">Step 4</a>
<a class="ds-i18n list-group-item list-group-item-action" id="list-6-calib">Completed</a>
</div>
</div>
<div class="col-8">
<div class="container" id="list-1">
<h4 class="ds-i18n">Welcome to the stick center-calibration wizard!</h4>
<p class="ds-i18n">This tool will guide you in re-centering the analog sticks of your controller. It consists in four steps: you will be asked to move both sticks in a direction and release them.</p>
<p class="ds-i18n">Please be aware that, <i>once the calibration is running, it cannot be canceled</i>. Do not close this page or disconnect your controller until is completed.</p>
<p class="ds-i18n">Press <b>Start</b> to begin calibration.</p>
</div>
<div class="container" style="display: none;" id="list-2">
<p class="ds-i18n">Please move both sticks to the <b>top-left corner</b> and release them.</p>
<p class="ds-i18n">When the sticks are back in the center, press <b>Continue</b>.</p>
</div>
<div class="container" style="display: none;" id="list-3">
<p class="ds-i18n">Please move both sticks to the <b>top-right corner</b> and release them.</p>
<p class="ds-i18n">When the sticks are back in the center, press <b>Continue</b>.</p>
</div>
<div class="container" style="display: none;" id="list-4">
<p class="ds-i18n">Please move both sticks to the <b>bottom-left corner</b> and release them.</p>
<p class="ds-i18n">When the sticks are back in the center, press <b>Continue</b>.</p>
</div>
<div class="container" style="display: none;" id="list-5">
<p class="ds-i18n">Please move both sticks to the <b>bottom-right corner</b> and release them.</p>
<p class="ds-i18n">When the sticks are back in the center, press <b>Continue</b>.</p>
</div>
<div class="container" style="display: none;" id="list-6">
<p class="ds-i18n">Calibration completed successfully!</p>
<p><span class="ds-i18n">You can check the calibration with the</span> <a href="https://hardwaretester.com/gamepad" target="_blank">gamepad tester</a>.</p>
<p class="ds-i18n">Have a nice day :)</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="calibNext" onclick="calib_next()">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" id="btnSpinner" style="display: none;"></span>
<span id="calibNextText" class="ds-i18n">Next</span>
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,19 @@
<!-- Modal -->
<div class="modal fade" id="calibrateModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">Calibrating center</h1>
</div>
<div class="modal-body">
<p class="ds-i18n">Recentering the controller sticks. </p>
<p class="ds-i18n">Please do not close this window and do not disconnect your controller. </p>
<div class="progress" role="progressbar" aria-label="Centering" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 0%"></div>
</div>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
<div class="modal fade" id="donateModal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4" id="donateBody">
<p class="ds-i18n">Hi, thank you for using this software.</p>
<p><span class="ds-i18n">If you're finding it helpful and you want to support my efforts, feel free to</span> <a href="https://paypal.me/alaincarlucci" target="_blank" class="text-body-secondary ds-i18n">buy me a coffee</a><span class="ds-i18n">! :)</span></p>
<p class="ds-i18n">Do you have any suggestion or issue? Drop me a message via email or discord.</p>
<p class="ds-i18n">Cheers!</p>
<div class="collapse" id="ethereumCollapse">
<div class="card card-body">
<h5 class="card-title">Ethereum Address</h5>
<center><img src="donate.png" width="128px" /></center>
<input type="text" class="form-control" value="0x27dDA2f15A6A477fcdFB3709Ed0760aEF0246D5D" readonly />
</div>
</div>
</div>
<div class="modal-footer">
<button data-bs-toggle="collapse" data-bs-target="#ethereumCollapse" aria-expanded="false" aria-controls="ethereumCollapse" type="button" class="btn btn-success">
<svg class="bi" width="18" height="18"><use xlink:href="#ethereum"/></svg>&nbsp;Ethereum
</button>
<button onclick="window.open('https://paypal.me/alaincarlucci')" type="button" class="btn btn-primary" data-bs-dismiss="modal">
<svg class="bi" width="18" height="18"><use xlink:href="#paypal"/></svg>&nbsp;&nbsp;PayPal
</button>
</div>
</div>
</div>
</div>

35
templates/edge-modal.html Normal file
View File

@@ -0,0 +1,35 @@
<div class="modal fade" id="edgeModal" tabindex="-1" aria-labelledby="modal-title" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title ds-i18n">DualSense Edge Calibration</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-4" id="donateBody">
<p class="ds-i18n">Support for calibrating DualSense Edge stick modules is now available as an <b>experimental feature</b>.</p>
<p class="ds-i18n">Please note: the stick modules on the DS Edge <b>cannot be calibrated via software alone</b>.</p>
<p class="ds-i18n">To store a custom calibration on the stick's internal memory, a <b>hardware modification</b> is required.</p>
<p class="ds-i18n">This involves temporarily disabling write protection by applying <b>+1.8V</b> to a specific test point on each module.</p>
<p></p>
<p><span class="ds-i18n">You can do this in two ways:</span>
<ul>
<li class="ds-i18n"><b>Internally</b>: by soldering a wire from a +1.8V source to the write-protect TP.</li>
<li class="ds-i18n"><b>Externally</b>: by applying +1.8V directly to the visible test point without opening the controller.</li>
</ul>
</p>
<p><b><span class="ds-i18n">This is only for advanced users. If you're not sure what you're doing, please do not attempt it.</span></b></p>
<p><span class="ds-i18n">More details and images</span>&nbsp;<a href="https://github.com/lewy20041/Dualsense_Edge_Modules_Callibration">here</a>.</p>
<p class="ds-i18n">We are not responsible for any damage caused by attempting this modification.</p>
<p class="ds-i18n">For more info or help, feel free to reach out on Discord.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary ds-i18n" data-bs-dismiss="modal">Understood</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
<!-- Edge in progress Modal -->
<div class="modal fade" id="edgeProgressModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="edgeProgressLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="edgeProgressLabel">Storing calibration...</h1>
</div>
<div class="modal-body">
<p class="ds-i18n">Calibration is being stored in the stick modules.</p>
<p class="ds-i18n">Please do not close this window and do not disconnect your controller. </p>
<div class="progress" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<div id="dsedge-progress" class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>

168
templates/faq-modal.html Normal file
View File

@@ -0,0 +1,168 @@
<!-- FAQ -->
<div class="modal fade" id="faqModal" tabindex="-1" role="dialog" aria-labelledby="faqModalTitle" aria-hidden="true">
<div class="modal-dialog modal-lg modal-fullscreen-md-down" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title ds-i18n" id="faqModalTitle">Frequently Asked Questions</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="p-3 ds-i18n">Welcome to the F.A.Q. section! Below, you'll find answers to some of the most commonly asked questions about this website. If you have any other inquiries or need further assistance, feel free to reach out to me directly. Your feedback and questions are always welcome!</div>
<div class="accordion accordion-flush" id="accordionFlushExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse3" aria-expanded="false" aria-controls="flush-collapse3">How does it work?</button>
</h2>
<div id="flush-collapse3" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">Behind the scenes, this website is the culmination of one year of dedicated effort in reverse-engineering DualShock controllers for fun/hobby from a random guy on the internet.</p>
<p><span class="ds-i18n">Through</span> <a class="ds-i18n" href='https://blog.the.al' target='_blank'>this research</a><span class="ds-i18n">, it was discovered that there exist some undocumented commands on DualShock controllers that can be sent via USB and are used during factory assembly process. If these commands are sent, the controller starts the recalibration of analog sticks.</span></p>
<p class="ds-i18n">While the primary focus of this research wasn't initially centered on recalibration, it became apparent that a service offering this capability could greatly benefit numerous individuals. And thus, here we are.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseOne" aria-expanded="false" aria-controls="flush-collapseOne">Does the calibration remain effective during gameplay on PS4/PS5?</button>
</h2>
<div id="flush-collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="ds-i18n accordion-body">Yes, if you tick the checkbox "Write changes permanently in the controller". In that case, the calibration is flashed directly in the controller firmware. This ensures that it remains in place regardless of the console it's connected to.</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapseTwo" aria-expanded="false" aria-controls="flush-collapseTwo">Is this an officially endorsed service?</button>
</h2>
<div id="flush-collapseTwo" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="ds-i18n accordion-body">No, this service is simply a creation by a DualShock enthusiast.</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse4" aria-expanded="false" aria-controls="flush-collapse4">Does this website detects if a controller is a clone?</button>
</h2>
<div id="flush-collapse4" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">Yes, only DualShock4 at the moment. This happened because I accidentally purchased some clones, spent time identifying the differences and added this functionality to prevent future deception.</p>
<p class="ds-i18n">Unfortunately, the clones cannot be calibrated anyway, because they only clone the behavior of a DualShock4 during a normal gameplay, not all the undocumented functionalities.</p>
<p class="ds-i18n">If you want to extend this detection functionality to DualSense, please ship me a fake DualSense and you'll see it in few weeks.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse5" aria-expanded="false" aria-controls="flush-collapse5">What development is in plan?</button>
</h2>
<div id="flush-collapse5" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">I maintain two separate to-do lists for this project, although the priority has yet to be established.</p>
<p class="ds-i18n">The first list is about enhancing support for DualShock4 and DualSense controllers:</p>
<ul>
<li class="ds-i18n">Implement calibration of L2/R2 triggers.</li>
<li class="ds-i18n">Improve detection of clones, particularly beneficial for those seeking to purchase used controllers with assurance of authenticity.</li>
<li class="ds-i18n">Enhance user interface (e.g. provide additional controller information)</li>
<li class="ds-i18n">Add support for recalibrating IMUs.</li>
<li class="ds-i18n">Additionally, explore the possibility of reviving non-functioning DualShock controllers (further discussion available on Discord for interested parties).</li>
</ul>
<p class="ds-i18n">The second list contains new controllers I aim to support:</p>
<ul>
<li class="ds-i18n">DualSense Edge</li>
<li class="ds-i18n">DualShock 3</li>
<li class="ds-i18n">XBox Controllers</li>
</ul>
<p class="ds-i18n">Each of these tasks presents both immense interest and significant time investment. To provide context, supporting a new controller typically demands 6-12 months of full-time research, alongside a stroke of good fortune.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse7" aria-expanded="false" aria-controls="flush-collapse7">Can I reset a permanent calibration to previous calibration?</button>
</h2>
<div id="flush-collapse7" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">No.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse8" aria-expanded="false" aria-controls="flush-collapse8">Can you overwrite a permanent calibration?</button>
</h2>
<div id="flush-collapse8" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">Yes. Simply do another permanent calibration.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse9" aria-expanded="false" aria-controls="flush-collapse9">Does this software resolve stickdrift?</button>
</h2>
<div id="flush-collapse9" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">Stickdrift is caused by a physical defect; namely dirt, worn potentiometer or in some cases a worn spring.</p>
<p class="ds-i18n">This software will not fix stick drift on its own if you already experience that. What it will help with, is ensuring the new joystick(s) will function properly after replacing the old one(s) to work well with.</p>
<p class="ds-i18n">I have noticed some controllers out of the box have worse factory calibration than if I would recalibrate them. Especially true for circularity of SCUF controllers with a unique shell.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse10" aria-expanded="false" aria-controls="flush-collapse10">(Dualsense) Will updating the firmware reset calibration?</button>
</h2>
<div id="flush-collapse10" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">No.</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse11" aria-expanded="false" aria-controls="flush-collapse11">After range calibration, joysticks always go in corners.</button>
</h2>
<div id="flush-collapse11" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">This issue happens because you have clicked "Done" immediately after starting a range calibration.</p>
<b><p class="ds-i18n">Please read the instructions.</p></b>
<p class="ds-i18n">You have to rotate the joysticks before you press "Done".</p>
<p class="ds-i18n">Make sure to touch the edges of the joystick frame and rotate slowly, preferably in each direction - clockwise and anti-clockwise.</p>
<p class="ds-i18n">Only after you have done that, you click on "Done".</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header">
<button class="ds-i18n accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#flush-collapse6" aria-expanded="false" aria-controls="flush-collapse6">I love this service, it helped me! How can I contribute?</button>
</h2>
<div id="flush-collapse6" class="accordion-collapse collapse" data-bs-parent="#accordionFlushExample">
<div class="accordion-body">
<p class="ds-i18n">I'm glad to hear that you found this helpful! If you're interested in contributing, here are a few ways you can help me:</p>
<ul>
<li><span class="ds-i18n">Consider making a</span> <a href="https://paypal.me/alaincarlucci" target="_blank" class="ds-i18n">donation</a> <span class="ds-i18n">to support my late-night caffeine-fueled reverse-engineering efforts.</span></li>
<li class="ds-i18n">Ship me a controller you would love to add (send me an email for organization).</li>
<li><a href="https://github.com/dualshock-tools/dualshock-tools.github.io/blob/main/TRANSLATIONS.md" class="ds-i18n" target="_blank">Translate this website in your language</a><span class="ds-i18n">, to help more people like you!</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary ds-i18n" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,175 @@
<div class="modal fade" id="finetuneModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="finetuneModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg modal-fullscreen-lg-down">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="finetuneModalLabel">Finetune stick calibration</h1>
<button type="button" class="btn-close" aria-label="Close" onclick="finetune_cancel()"></button>
</div>
<div class="modal-body">
<p class="ds-i18n">This screen allows to finetune raw calibration data on your controller</p>
<div class="modal-header border-top-0 pt-0">
<div class="btn-group w-100" role="group" aria-label="Finetune mode selection">
<input type="radio" class="btn-check" name="finetuneMode" id="finetuneModeCenter" autocomplete="off" checked>
<label class="btn btn-outline-primary ds-i18n" for="finetuneModeCenter">Center (L1)</label>
<input type="radio" class="btn-check" name="finetuneMode" id="finetuneModeCircularity" autocomplete="off">
<label class="btn btn-outline-primary ds-i18n" for="finetuneModeCircularity">Circularity (R1)</label>
</div>
</div>
<div class="alert alert-info finetune-center-mode" role="alert">
<i class="fas fa-info-circle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
Move the stick to select it for tuning, then without touching the stick use the D-pad buttons to adjust the center point. Flick it and adjust it again if it is off center or flickers.
</span>
</div>
<div class="alert alert-warning finetune-center-mode" role="alert" id="finetuneCenterWarning">
<i class="fas fa-exclamation-triangle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
Please release the stick to center position before adjusting with D-pad buttons.
</span>
</div>
<div class="alert alert-success finetune-center-mode" role="alert" id="finetuneCenterSuccess">
<i class="fas fa-check-circle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
Press the D-pad or face buttons in the direction you want the stick position to move.
</span>
</div>
<div class="alert alert-info finetune-circularity-mode" role="alert">
<i class="fas fa-info-circle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
While holding the stick to be adjusted straight up/down/left/right, <em>observe the highlighted value below the circle</em>,
then use the D-pad buttons to adjust the value to ±0.99 (just below ±1.00).
</span>
</div>
<div class="alert alert-warning finetune-circularity-mode" role="alert" id="finetuneCircularityWarning">
<i class="fas fa-exclamation-triangle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
Push the stick straight up/down/left/right as far as possible.
</span>
</div>
<div class="alert alert-success finetune-circularity-mode" role="alert" id="finetuneCircularitySuccess">
<i class="fas fa-check-circle"></i>&nbsp;&nbsp;
<span class="ds-i18n">
Press the D-pad or face buttons in the direction you want the stick position to move.
</span>
</div>
<div style="width: 100%; display: flex; justify-content: center;">
<div class="container-fluid">
<div class="row">
<div class="col col-lg-6 col-12">
<div class="card text-bg-light" id="left-stick-card" style="height: 340px;">
<div class="card-header"><span class="ds-i18n">Left stick</span></div>
<div class="card-body">
<div class="container-fluid">
<div class="finetune-grid">
<div class="finetune-top finetune-center-mode">
<input id="finetuneLY" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-top finetune-circularity-mode">
<input id="finetuneLT" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-left finetune-circularity-mode">
<input id="finetuneLL" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-left finetune-center-mode">
<input id="finetuneLX" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-center">
<canvas id="finetuneStickCanvasL" width="150px" height="150px"></canvas>
<canvas id="finetuneStickCanvasL_large" width="210px" height="210px"></canvas>
</div>
<div class="finetune-right finetune-circularity-mode">
<input id="finetuneLR" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-bottom finetune-circularity-mode">
<input id="finetuneLB" type="number" class="form-control" min="0" max="65535" value="0">
</div>
</div>
</div>
<div class="px-2">
<div class="hstack">
<div class="vstack" style="text-align: center;">
<span>LX:</span>
<strong><pre id="finetuneStickCanvasLx-lbl" style="min-width: 80px;"></pre></strong>
</div>
<div class="vstack" style="text-align: center;">
<span>LY:</span>
<strong><pre id="finetuneStickCanvasLy-lbl" style="min-width: 80px;"></pre></strong>
</div>
</div>
</div>
</div>
</div>
</div> <!-- col -->
<div class="col col-lg-6 col-12">
<div class="card text-bg-light" id="right-stick-card" style="height: 340px;">
<div class="card-header"><span class="ds-i18n">Right stick</span></div>
<div class="card-body">
<div class="container-fluid">
<div class="finetune-grid">
<div class="finetune-top finetune-center-mode">
<input id="finetuneRY" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-top finetune-circularity-mode">
<input id="finetuneRT" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-left finetune-circularity-mode">
<input id="finetuneRL" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-left finetune-center-mode">
<input id="finetuneRX" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-center">
<canvas id="finetuneStickCanvasR" width="150px" height="150px"></canvas>
<canvas id="finetuneStickCanvasR_large" width="210px" height="210px"></canvas>
</div>
<div class="finetune-right finetune-circularity-mode">
<input id="finetuneRR" type="number" class="form-control" min="0" max="65535" value="0">
</div>
<div class="finetune-bottom finetune-circularity-mode">
<input id="finetuneRB" type="number" class="form-control" min="0" max="65535" value="0">
</div>
</div>
<div class="px-2">
<div class="hstack">
<div class="vstack" style="text-align: center;">
<span>RX:</span>
<strong><pre id="finetuneStickCanvasRx-lbl" style="min-width: 80px;"></pre></strong>
</div>
<div class="vstack" style="text-align: center;">
<span>RY:</span>
<strong><pre id="finetuneStickCanvasRy-lbl" style="min-width: 80px;"></pre></strong>
</div>
</div>
</div>
</div>
</div>
</div> <!-- col -->
</div> <!-- row -->
</div>
</div>
</div>
<div class="modal-footer">
<div class="form-check me-auto">
<input class="form-check-input" type="checkbox" id="showRawNumbersCheckbox">
<label class="form-check-label ds-i18n" for="showRawNumbersCheckbox">Show raw numbers</label>
</div>
<button type="button" class="btn btn-secondary ds-i18n" onclick="finetune_cancel()">Cancel</button>
<button type="button" class="btn btn-primary ds-i18n" onclick="finetune_save()">Save</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<!-- Popup -->
<div class="modal fade" id="popupModal" tabindex="-1" aria-labelledby="popupTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body" id="popupBody"></div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
<!-- Modal -->
<div class="modal fade" id="rangeModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="staticBackdropLabel">Range calibration</h1>
</div>
<div class="modal-body">
<p class="ds-i18n"><b>The controller is now sampling data!</b></p>
<p class="ds-i18n">Rotate the sticks slowly to cover the whole range. Press "Done" when completed.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary ds-i18n" onclick="calibrate_range_on_close()">Done</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<!-- Welcome Modal -->
<div class="modal fade" id="welcomeModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="welcomeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5 ds-i18n" id="welcomeModalLabel">Welcome to the Calibration GUI</h1>
</div>
<div class="modal-body">
<p class="ds-i18n">Just few things to know before you can start:</p>
<ul>
<li class="ds-i18n">This website is not affiliated with Sony, PlayStation &amp; co.</li>
<li class="ds-i18n">This service is provided without warranty. Use at your own risk.</li>
<li class="ds-i18n">This website uses analytics to improve the service.</li>
<li class="ds-i18n">Keep the internal battery of the controller connected and ensure it is well charged. If the battery dies during operations, the controller will be damaged and rendered unusable.</li>
<li class="ds-i18n">Before doing the permanent calibration, try the temporary one to ensure that everything is working well.</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary ds-i18n" onclick="welcome_accepted();">Understood</button>
</div>
</div>
</div>
</div>