mirror of
https://github.com/dualshock-tools/dualshock-tools.github.io.git
synced 2026-03-01 11:19:54 +03:00
Split the code into smaller more manageble files
This commit is contained in:
committed by
dualshock-tools
parent
d4ba4a5fdd
commit
42fc94a9a2
108
assets/dualshock-controller.svg
Normal file
108
assets/dualshock-controller.svg
Normal 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
27
assets/icons.svg
Normal 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 |
116
css/finetune.css
Normal file
116
css/finetune.css
Normal 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
23
css/main.css
Normal 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
1315
index.html
File diff suppressed because it is too large
Load Diff
@@ -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(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
871
js/core.js
Normal 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 = ' <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 = ' <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;
|
||||
237
js/modals/calib-center-modal.js
Normal file
237
js/modals/calib-center-modal.js
Normal 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;
|
||||
62
js/modals/calib-range-modal.js
Normal file
62
js/modals/calib-range-modal.js
Normal 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
754
js/modals/finetune-modal.js
Normal 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
213
js/stick-renderer.js
Normal 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
69
js/template-loader.js
Normal 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);
|
||||
}
|
||||
}
|
||||
65
templates/calib-center-modal.html
Normal file
65
templates/calib-center-modal.html
Normal 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>
|
||||
19
templates/calibrate-modal.html
Normal file
19
templates/calibrate-modal.html
Normal 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>
|
||||
34
templates/donate-modal.html
Normal file
34
templates/donate-modal.html
Normal 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> 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> PayPal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
35
templates/edge-modal.html
Normal file
35
templates/edge-modal.html
Normal 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> <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>
|
||||
20
templates/edge-progress-modal.html
Normal file
20
templates/edge-progress-modal.html
Normal 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
168
templates/faq-modal.html
Normal 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>
|
||||
175
templates/finetune-modal.html
Normal file
175
templates/finetune-modal.html
Normal 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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
11
templates/popup-modal.html
Normal file
11
templates/popup-modal.html
Normal 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>
|
||||
17
templates/range-modal.html
Normal file
17
templates/range-modal.html
Normal 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>
|
||||
23
templates/welcome-modal.html
Normal file
23
templates/welcome-modal.html
Normal 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 & 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>
|
||||
Reference in New Issue
Block a user