Files
erickdavestech--ps5-dualsen…/assets/preview.html
T
erickdavestech ffcfa5d29b Normalize line endings to LF and enforce them with .gitattributes
CRLF line endings broke scripts/prepare_ps5_deps.sh in CI. Only line endings change; the build output is identical.

Signed-off-by: erickdavestech <54048831+erickdavestech@users.noreply.github.com>
2026-10-03 20:52:58 -04:00

176 lines
8.7 KiB
HTML

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pad Overlay Preview</title>
<style>
:root { --bg: #11151c; --panel: #1a2029; --text: #d8dde6; --muted: #8a93a3; --accent: #2f7bff; }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font: 14px/1.4 system-ui, "Segoe UI", sans-serif; }
header { display: flex; flex-wrap: wrap; gap: 12px 20px; align-items: center; padding: 12px 16px; background: var(--panel); }
header h1 { font-size: 15px; margin: 0 8px 0 0; }
label { color: var(--muted); }
select { background: #0d1117; color: var(--text); border: 1px solid #2c3442; border-radius: 6px; padding: 3px 6px; }
#src { margin-left: auto; padding: 3px 10px; border-radius: 999px; background: #0d1117; }
#src.pad { color: #2bd99f; }
#stage { display: grid; place-items: center; padding: 24px 16px; min-height: 60vh; overflow: auto; }
#stage.game { background: radial-gradient(ellipse at 30% 20%, #3b5b7a, #16222f 55%, #0b0f14); }
#stage.light { background: #c4c9be; }
#stage.checker { background: repeating-conic-gradient(#2a2f38 0 25%, #20242b 0 50%) 0 0 / 24px 24px; }
#ov { position: relative; flex: none; }
#ov img { position: absolute; display: block; user-select: none; pointer-events: none; }
#ov .off { visibility: hidden; }
.trig { position: absolute; overflow: hidden; }
.trig img { left: 0; bottom: 0; }
#help { padding: 0 16px 20px; color: var(--muted); max-width: 900px; margin: 0 auto; }
kbd { background: #0d1117; border: 1px solid #2c3442; border-radius: 4px; padding: 0 5px; color: var(--text); font-size: 12px; }
</style>
</head>
<body>
<header>
<h1>Overlay del mando — simulador</h1>
<label>Tamaño <select id="size"><option>1280</option><option selected>640</option><option>320</option></select></label>
<label>Fondo <select id="bgsel"><option value="game">juego</option><option value="checker">transparente</option><option value="light">claro</option></select></label>
<span id="src">teclado</span>
</header>
<div id="stage" class="game"><div id="ov"></div></div>
<div id="help">
Conecta un DualSense al PC (USB o Bluetooth) y pulsa un botón, o usa el teclado:
<kbd>←↑→↓</kbd> cruceta · <kbd>I</kbd><kbd>J</kbd><kbd>K</kbd><kbd>L</kbd> △ □ ✕ ○ ·
<kbd>Q</kbd>/<kbd>E</kbd> L1/R1 · <kbd>1</kbd>/<kbd>3</kbd> L2/R2 (analógico) ·
<kbd>WASD</kbd> stick izq · <kbd>TFGH</kbd> stick der · <kbd>Z</kbd>/<kbd>X</kbd> L3/R3 ·
<kbd>C</kbd> Create · <kbd>V</kbd> Options · <kbd>P</kbd> PS · <kbd>M</kbd> Mute · <kbd>Espacio</kbd> clic touchpad ·
ratón sobre el touchpad = dedo.<br>
Simula en el PC lo que dibujará el renderer en la PS5: mismos PNG, mismo layout.json.
</div>
<script src="layout.js"></script>
<script>
(() => {
const L = window.LAYOUT;
const qs = new URLSearchParams(location.search);
const ov = document.getElementById('ov');
const sizeSel = document.getElementById('size');
const bgSel = document.getElementById('bgsel');
const srcTag = document.getElementById('src');
if (L.sizes[qs.get('size')]) sizeSel.value = qs.get('size');
// Estado de entrada, idéntico al que mandará el ELF: botones + ejes en [-1,1] + gatillos [0,1] + toque.
const st = { btn: new Set(), lx: 0, ly: 0, rx: 0, ry: 0, l2: 0, r2: 0, touch: null };
let els = {};
function img(src, rect, s, extra) {
const i = document.createElement('img');
i.src = `png/${sizeSel.value}/${src}`;
i.style.left = rect[0] * s + 'px'; i.style.top = rect[1] * s + 'px';
i.style.width = rect[2] * s + 'px'; i.style.height = rect[3] * s + 'px';
if (extra) i.className = extra;
return i;
}
function build() {
const s = L.sizes[sizeSel.value];
ov.innerHTML = '';
ov.style.width = L.canvas[0] * s + 'px'; ov.style.height = L.canvas[1] * s + 'px';
els = { btn: {}, trig: {}, stick: {}, dot: null, s };
ov.append(img(L.base, [0, 0, ...L.canvas], s));
for (const [t, d] of Object.entries(L.triggers)) {
const box = document.createElement('div');
box.className = 'trig';
box.style.left = d.rect[0] * s + 'px'; box.style.width = d.rect[2] * s + 'px';
const fill = img(d.sprite, [0, 0, d.rect[2], d.rect[3]], s);
fill.style.top = ''; fill.style.bottom = '0';
box.append(fill); ov.append(box);
els.trig[t] = { box, d };
}
for (const [n, d] of Object.entries(L.buttons)) ov.append(els.btn[n] = img(d.sprite, d.rect, s, 'off'));
for (const [side, d] of Object.entries(L.sticks)) ov.append(els.stick[side] = img(d.sprite, [0, 0, ...d.size], s));
const tp = L.touchpad;
ov.append(els.dot = img(tp.dot, [0, 0, ...tp.dot_size], s, 'off'));
draw();
}
function draw() {
const s = els.s;
for (const [n, i] of Object.entries(els.btn)) i.classList.toggle('off', !st.btn.has(n));
for (const [t, { box, d }] of Object.entries(els.trig)) {
const v = Math.max(0, Math.min(1, st[t]));
box.style.top = (d.rect[1] + d.rect[3] * (1 - v)) * s + 'px';
box.style.height = d.rect[3] * v * s + 'px';
}
for (const [side, i] of Object.entries(els.stick)) {
const d = L.sticks[side];
const [x, y] = side === 'left' ? [st.lx, st.ly] : [st.rx, st.ry];
i.style.left = (d.center[0] + x * d.travel - d.size[0] / 2) * s + 'px';
i.style.top = (d.center[1] + y * d.travel - d.size[1] / 2) * s + 'px';
const want = `png/${sizeSel.value}/${st.btn.has(d.click) ? d.sprite_pressed : d.sprite}`;
if (!i.src.endsWith(want)) i.src = want;
}
const tp = L.touchpad, dot = els.dot;
dot.classList.toggle('off', !st.touch);
if (st.touch) {
dot.style.left = (tp.rect[0] + st.touch[0] * tp.rect[2] - tp.dot_size[0] / 2) * s + 'px';
dot.style.top = (tp.rect[1] + st.touch[1] * tp.rect[3] - tp.dot_size[1] / 2) * s + 'px';
}
}
// --- Teclado ---
const KEYS = {
ArrowUp: 'dpad_up', ArrowDown: 'dpad_down', ArrowLeft: 'dpad_left', ArrowRight: 'dpad_right',
KeyI: 'triangle', KeyJ: 'square', KeyK: 'cross', KeyL: 'circle', KeyQ: 'l1', KeyE: 'r1',
KeyZ: 'l3', KeyX: 'r3', KeyC: 'create', KeyV: 'options', KeyP: 'ps', KeyM: 'mute', Space: 'touchpad',
};
const held = new Set();
addEventListener('keydown', e => { if (KEYS[e.code] || /^(Key[WASDTFGH]|Digit[13])$/.test(e.code)) { held.add(e.code); e.preventDefault(); } });
addEventListener('keyup', e => held.delete(e.code));
addEventListener('blur', () => held.clear());
const axis = (neg, pos) => (held.has(pos) ? 1 : 0) - (held.has(neg) ? 1 : 0);
// --- Ratón sobre el touchpad = un dedo ---
ov.addEventListener('pointermove', e => {
const r = ov.getBoundingClientRect(), s = els.s, tp = L.touchpad.rect;
const u = ((e.clientX - r.left) / s - tp[0]) / tp[2], v = ((e.clientY - r.top) / s - tp[1]) / tp[3];
st.mouseTouch = u >= 0 && u <= 1 && v >= 0 && v <= 1 ? [u, v] : null;
});
ov.addEventListener('pointerleave', () => { st.mouseTouch = null; });
// --- Gamepad API (mapeo "standard": así ve Chrome al DualSense) ---
const GP = ['cross', 'circle', 'square', 'triangle', 'l1', 'r1', null, null, 'create', 'options',
'l3', 'r3', 'dpad_up', 'dpad_down', 'dpad_left', 'dpad_right', 'ps', 'touchpad'];
const dz = v => Math.abs(v) < 0.08 ? 0 : v;
let last = performance.now();
function tick(now) {
const dt = Math.min(0.1, (now - last) / 1000); last = now;
const pad = [...(navigator.getGamepads?.() || [])].find(p => p && p.connected);
st.btn.clear();
if (pad) {
srcTag.textContent = pad.id.replace(/\s*\(.*$/, '') || 'mando'; srcTag.className = 'pad';
GP.forEach((n, i) => { if (n && pad.buttons[i]?.pressed) st.btn.add(n); });
st.l2 = pad.buttons[6]?.value || 0; st.r2 = pad.buttons[7]?.value || 0;
[st.lx, st.ly, st.rx, st.ry] = [0, 1, 2, 3].map(i => dz(pad.axes[i] || 0));
} else {
srcTag.textContent = 'teclado'; srcTag.className = '';
for (const c of held) if (KEYS[c]) st.btn.add(KEYS[c]);
const ramp = (v, on) => on ? Math.min(1, v + dt * 4) : Math.max(0, v - dt * 6);
st.l2 = ramp(st.l2, held.has('Digit1')); st.r2 = ramp(st.r2, held.has('Digit3'));
const norm = (x, y) => { const m = Math.hypot(x, y) || 1; return [x / Math.max(1, m), y / Math.max(1, m)]; };
[st.lx, st.ly] = norm(axis('KeyA', 'KeyD'), axis('KeyW', 'KeyS'));
[st.rx, st.ry] = norm(axis('KeyF', 'KeyH'), axis('KeyT', 'KeyG'));
}
if (st.l2 > 0.05) st.btn.add('l2');
if (st.r2 > 0.05) st.btn.add('r2');
st.touch = st.mouseTouch;
draw();
requestAnimationFrame(tick);
}
sizeSel.addEventListener('change', build);
bgSel.addEventListener('change', () => { document.getElementById('stage').className = bgSel.value; });
build();
requestAnimationFrame(tick);
})();
</script>
</body>
</html>