|
4 | 4 | <meta charset="UTF-8" /> |
5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
6 | 6 | <title>CPS Tester — Both Buttons</title> |
| 7 | + |
| 8 | + <link rel="icon" type="image/svg+xml" id="favicon-link"> |
7 | 9 | <link |
8 | 10 | href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" |
9 | 11 | rel="stylesheet" |
@@ -836,5 +838,97 @@ <h1>CPS Tester</h1> |
836 | 838 | } |
837 | 839 | } |
838 | 840 | </script> |
| 841 | + |
| 842 | + |
| 843 | + <script> |
| 844 | + // FAVICON ANIMATION |
| 845 | + const SVG_W = 32; |
| 846 | + const SVG_H = 32; |
| 847 | + |
| 848 | + /* Mouse geometry (all coords in 32×32 space) */ |
| 849 | + const BODY_PATH = "M7,14 Q7,4 16,4 Q25,4 25,14 L25,24 Q25,28 16,28 Q7,28 7,24 Z"; |
| 850 | + const DIV_LINE = "M16,4 L16,14"; /* centre divider between buttons */ |
| 851 | + const SCROLL_RX = 1.5, SCROLL_RY = 3; /* scroll-wheel ellipse */ |
| 852 | + |
| 853 | + /* Colours */ |
| 854 | + const BG = "transparent"; |
| 855 | + const BODY_F = "#1a1a1a"; |
| 856 | + const BODY_S = "#888"; |
| 857 | + const BTN_IDLE = "#1a1a1a"; |
| 858 | + const BTN_FLASH= "#4af"; /* bright blue-white flash */ |
| 859 | + const LINE_C = "#666"; |
| 860 | + const WHEEL_F = "#555"; |
| 861 | + |
| 862 | + function makeSVG(leftOn, rightOn) { |
| 863 | + /* clip-paths for left / right button regions */ |
| 864 | + return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32"> |
| 865 | + <defs> |
| 866 | + <clipPath id="cl"><rect x="0" y="0" width="16" height="14"/></clipPath> |
| 867 | + <clipPath id="cr"><rect x="16" y="0" width="16" height="14"/></clipPath> |
| 868 | + </defs> |
| 869 | +
|
| 870 | + <!-- body outline --> |
| 871 | + <path d="${BODY_PATH}" fill="${BODY_F}" stroke="${BODY_S}" stroke-width="1.2"/> |
| 872 | +
|
| 873 | + <!-- left button flash --> |
| 874 | + <path d="${BODY_PATH}" fill="${leftOn ? BTN_FLASH : BTN_IDLE}" clip-path="url(#cl)"/> |
| 875 | +
|
| 876 | + <!-- right button flash --> |
| 877 | + <path d="${BODY_PATH}" fill="${rightOn ? BTN_FLASH : BTN_IDLE}" clip-path="url(#cr)"/> |
| 878 | +
|
| 879 | + <!-- body outline on top so the stroke is always visible --> |
| 880 | + <path d="${BODY_PATH}" fill="none" stroke="${BODY_S}" stroke-width="1.2"/> |
| 881 | +
|
| 882 | + <!-- centre divider --> |
| 883 | + <line x1="16" y1="4" x2="16" y2="14" stroke="${LINE_C}" stroke-width="0.8"/> |
| 884 | +
|
| 885 | + <!-- scroll wheel --> |
| 886 | + <ellipse cx="16" cy="13" rx="${SCROLL_RX}" ry="${SCROLL_RY}" fill="${WHEEL_F}" stroke="${LINE_C}" stroke-width="0.6"/> |
| 887 | +</svg>`; |
| 888 | + } |
| 889 | + |
| 890 | + function setFavicon(leftOn, rightOn) { |
| 891 | + const svg = makeSVG(leftOn, rightOn); |
| 892 | + const blob = new Blob([svg], {type: "image/svg+xml"}); |
| 893 | + const url = URL.createObjectURL(blob); |
| 894 | + const link = document.getElementById("favicon-link"); |
| 895 | + const old = link.href; |
| 896 | + link.href = url; |
| 897 | + if (old) URL.revokeObjectURL(old); |
| 898 | + } |
| 899 | + |
| 900 | + /* |
| 901 | + Animation schedule over 2 s (2000 ms), looping: |
| 902 | + 0 ms — LEFT on |
| 903 | + 200 ms — LEFT off |
| 904 | + 450 ms — LEFT on (second flash) |
| 905 | + 650 ms — LEFT off |
| 906 | + 1000 ms — RIGHT on |
| 907 | + 1200 ms — RIGHT off |
| 908 | + 2000 ms — loop |
| 909 | + */ |
| 910 | + const schedule = [ |
| 911 | + { t: 0, L: true, R: false }, |
| 912 | + { t: 200, L: false, R: false }, |
| 913 | + { t: 450, L: true, R: false }, |
| 914 | + { t: 650, L: false, R: false }, |
| 915 | + { t: 1000, L: false, R: true }, |
| 916 | + { t: 1200, L: false, R: false }, |
| 917 | + ]; |
| 918 | + const CYCLE = 2000; |
| 919 | + |
| 920 | + function tick() { |
| 921 | + const now = Date.now() % CYCLE; |
| 922 | + let frame = schedule[0]; |
| 923 | + for (const s of schedule) { |
| 924 | + if (now >= s.t) frame = s; |
| 925 | + } |
| 926 | + setFavicon(frame.L, frame.R); |
| 927 | + } |
| 928 | + |
| 929 | + /* initial frame immediately, then drive at ~30 fps */ |
| 930 | + setFavicon(false, false); |
| 931 | + setInterval(tick, 33); |
| 932 | + </script> |
839 | 933 | </body> |
840 | 934 | </html> |
0 commit comments