Skip to content

Commit 73eb38f

Browse files
committed
Corrige tablero de Pinball en moviles
1 parent 8191b0e commit 73eb38f

4 files changed

Lines changed: 171 additions & 39 deletions

File tree

‎css/xp-apps.css‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3613,6 +3613,7 @@ body.xp-no-animations *::after {
36133613
}
36143614

36153615
.xp-pinball-canvas-stage {
3616+
grid-template-rows: minmax(0, 1fr);
36163617
padding-bottom: 6px;
36173618
box-sizing: border-box;
36183619
}
@@ -4287,6 +4288,20 @@ body.xp-no-animations *::after {
42874288
}
42884289
}
42894290

4291+
@media (max-height: 720px) and (max-width: 760px),
4292+
(max-height: 720px) and (pointer: coarse) {
4293+
.xp-pinball-panel {
4294+
grid-template-areas: "actions";
4295+
grid-template-rows: 44px;
4296+
}
4297+
4298+
.xp-pinball-panel .xp-pinball-stats,
4299+
.xp-pinball-panel .xp-pinball-mission,
4300+
.xp-pinball-panel .xp-pinball-meter {
4301+
display: none;
4302+
}
4303+
}
4304+
42904305
@media (prefers-reduced-motion: reduce) {
42914306
.xp-api-skeleton span,
42924307
.xp-playing-card,

‎index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
<link rel="stylesheet" href="css/about-me.css">
8181
<link rel="stylesheet" href="css/contacto.css">
8282
<link rel="stylesheet" href="css/proyectos-explorer.css?v=zaratexp-20260712-i18n2">
83-
<link rel="stylesheet" href="css/xp-apps.css?v=zaratexp-20260713-pinball-touch1">
83+
<link rel="stylesheet" href="css/xp-apps.css?v=zaratexp-20260713-pinball-mobile-fit1">
8484
<link rel="stylesheet" href="css/clippy.css?v=zaratexp-20260712-clippy-mobile1">
8585
<link rel="stylesheet" href="css/crtEffect.css">
8686

‎scripts/smoke-playwright.mjs‎

Lines changed: 152 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,9 +1066,21 @@ async function touchPinballControl(page, cdp, button, selector, control, touchId
10661066
return released;
10671067
}
10681068

1069-
async function exerciseMobileClippyAndPinball(browser, baseUrl) {
1069+
const PINBALL_BOARD_WIDTH = 520;
1070+
const PINBALL_BOARD_HEIGHT = 700;
1071+
const PINBALL_RATIO_TOLERANCE = 0.005;
1072+
const PINBALL_MOBILE_VIEWPORTS = [
1073+
{ width: 375, height: 600 },
1074+
{ width: 390, height: 664 },
1075+
{ width: 390, height: 745 },
1076+
{ width: 430, height: 739, exerciseTouch: true },
1077+
{ width: 430, height: 824 },
1078+
{ width: 390, height: 844 }
1079+
];
1080+
1081+
async function auditMobilePinballViewport(browser, baseUrl, viewportCase) {
10701082
const context = await browser.newContext({
1071-
viewport: { width: 390, height: 844 },
1083+
viewport: { width: viewportCase.width, height: viewportCase.height },
10721084
isMobile: true,
10731085
hasTouch: true,
10741086
deviceScaleFactor: 2
@@ -1087,15 +1099,17 @@ async function exerciseMobileClippyAndPinball(browser, baseUrl) {
10871099
await page.waitForSelector('.desktop', { state: 'visible', timeout: 12000 });
10881100
await page.waitForFunction(() => Boolean(window.zarateXP?.appManager?.windowManager), null, { timeout: 12000 });
10891101

1090-
const trayClippy = page.locator('.tray-clippy-icon');
1091-
ensure(await trayClippy.count() === 1 && await trayClippy.isHidden(), 'El icono de Clippy en la bandeja no quedo oculto en un dispositivo movil real');
1102+
if (viewportCase.exerciseTouch) {
1103+
const trayClippy = page.locator('.tray-clippy-icon');
1104+
ensure(await trayClippy.count() === 1 && await trayClippy.isHidden(), 'El icono de Clippy en la bandeja no quedo oculto en un dispositivo movil real');
10921105

1093-
await page.evaluate(() => window.zarateXP.clippyManager.showWelcome());
1094-
await page.waitForTimeout(650);
1095-
ensure(await page.locator('clippy-character').count() === 0, 'showWelcome creo un host de Clippy en movil');
1096-
await page.evaluate(() => window.zarateXP.clippyManager.showTip(0));
1097-
await page.waitForTimeout(650);
1098-
ensure(await page.locator('clippy-character').count() === 0, 'showTip creo un host de Clippy en movil');
1106+
await page.evaluate(() => window.zarateXP.clippyManager.showWelcome());
1107+
await page.waitForTimeout(650);
1108+
ensure(await page.locator('clippy-character').count() === 0, 'showWelcome creo un host de Clippy en movil');
1109+
await page.evaluate(() => window.zarateXP.clippyManager.showTip(0));
1110+
await page.waitForTimeout(650);
1111+
ensure(await page.locator('clippy-character').count() === 0, 'showTip creo un host de Clippy en movil');
1112+
}
10991113

11001114
const appWindow = await openApp(page, 'pinball');
11011115
const root = appWindow.locator('[data-pinball-root]');
@@ -1109,79 +1123,179 @@ async function exerciseMobileClippyAndPinball(browser, baseUrl) {
11091123
}, null, { timeout: 12000 });
11101124
await waitForWindowAnimation(page, 'pinball');
11111125

1112-
const mobileLayout = await appWindow.evaluate((windowNode) => {
1126+
const mobileLayout = await appWindow.evaluate((windowNode, board) => {
11131127
const rootNode = windowNode.querySelector('[data-pinball-root]');
11141128
const windowBody = windowNode.querySelector('.window-body');
1129+
const tableWrap = rootNode?.querySelector('.xp-pinball-table-wrap');
1130+
const canvasStage = rootNode?.querySelector('.xp-pinball-canvas-stage');
11151131
const canvasNode = rootNode?.querySelector('[data-pinball-canvas]');
11161132
const pad = rootNode?.querySelector('.xp-pinball-pad');
1133+
const app = rootNode?._pinballApp || rootNode?.closest('.window')?._pinballApp;
11171134
const requiredButtons = [
11181135
rootNode?.querySelector('[data-pinball-left]'),
11191136
rootNode?.querySelector('[data-pinball-plunger]'),
11201137
rootNode?.querySelector('[data-pinball-right]')
11211138
];
1122-
if (!rootNode || !windowBody || !canvasNode || !pad || requiredButtons.some((button) => !button)) {
1139+
if (!rootNode || !windowBody || !tableWrap || !canvasStage || !canvasNode || !pad || !app || requiredButtons.some((button) => !button)) {
11231140
return { complete: false };
11241141
}
11251142

11261143
const bodyRect = windowBody.getBoundingClientRect();
1144+
const wrapRect = tableWrap.getBoundingClientRect();
1145+
const stageRect = canvasStage.getBoundingClientRect();
11271146
const canvasRect = canvasNode.getBoundingClientRect();
11281147
const padRect = pad.getBoundingClientRect();
11291148
const taskbarTop = document.querySelector('.taskbar')?.getBoundingClientRect().top ?? window.innerHeight;
1130-
const usableBottom = Math.min(window.innerHeight, taskbarTop);
1131-
const inViewport = (rect) => rect.width > 0 && rect.height > 0
1132-
&& rect.left >= -1 && rect.right <= window.innerWidth + 1
1133-
&& rect.top >= -1 && rect.bottom <= usableBottom + 1;
1134-
const inWindowBody = (rect) => rect.left >= bodyRect.left - 1
1135-
&& rect.right <= bodyRect.right + 1
1136-
&& rect.top >= bodyRect.top - 1
1137-
&& rect.bottom <= bodyRect.bottom + 1;
1149+
const visualViewportRect = {
1150+
left: window.visualViewport?.offsetLeft ?? 0,
1151+
top: window.visualViewport?.offsetTop ?? 0,
1152+
right: (window.visualViewport?.offsetLeft ?? 0) + (window.visualViewport?.width ?? window.innerWidth),
1153+
bottom: (window.visualViewport?.offsetTop ?? 0) + (window.visualViewport?.height ?? window.innerHeight)
1154+
};
1155+
const usableViewportRect = {
1156+
...visualViewportRect,
1157+
bottom: Math.min(visualViewportRect.bottom, taskbarTop)
1158+
};
1159+
const contains = (outer, inner, tolerance = 1) => inner.width > 0 && inner.height > 0
1160+
&& inner.left >= outer.left - tolerance
1161+
&& inner.right <= outer.right + tolerance
1162+
&& inner.top >= outer.top - tolerance
1163+
&& inner.bottom <= outer.bottom + tolerance;
1164+
const clipRect = {
1165+
left: Math.max(stageRect.left, wrapRect.left, bodyRect.left, usableViewportRect.left),
1166+
top: Math.max(stageRect.top, wrapRect.top, bodyRect.top, usableViewportRect.top),
1167+
right: Math.min(stageRect.right, wrapRect.right, bodyRect.right, usableViewportRect.right),
1168+
bottom: Math.min(stageRect.bottom, wrapRect.bottom, bodyRect.bottom, usableViewportRect.bottom)
1169+
};
1170+
1171+
const canvasStyle = getComputedStyle(canvasNode);
1172+
const borderLeft = Number.parseFloat(canvasStyle.borderLeftWidth) || 0;
1173+
const borderRight = Number.parseFloat(canvasStyle.borderRightWidth) || 0;
1174+
const borderTop = Number.parseFloat(canvasStyle.borderTopWidth) || 0;
1175+
const borderBottom = Number.parseFloat(canvasStyle.borderBottomWidth) || 0;
1176+
const canvasContentRect = {
1177+
left: canvasRect.left + borderLeft,
1178+
top: canvasRect.top + borderTop,
1179+
right: canvasRect.right - borderRight,
1180+
bottom: canvasRect.bottom - borderBottom,
1181+
width: canvasRect.width - borderLeft - borderRight,
1182+
height: canvasRect.height - borderTop - borderBottom
1183+
};
1184+
const intrinsicRatio = canvasNode.width / canvasNode.height;
1185+
const renderedRatio = canvasContentRect.width / canvasContentRect.height;
1186+
const ratioError = Math.abs((renderedRatio / intrinsicRatio) - 1);
1187+
const visibleContentBottom = Math.min(canvasContentRect.bottom, clipRect.bottom);
1188+
const logicalVisibleBottom = Math.max(0, Math.min(
1189+
board.height,
1190+
((visibleContentBottom - canvasContentRect.top) / canvasContentRect.height) * board.height
1191+
));
1192+
const logicalToPixelRect = (bounds) => ({
1193+
left: canvasContentRect.left + (bounds.left / board.width) * canvasContentRect.width,
1194+
right: canvasContentRect.left + (bounds.right / board.width) * canvasContentRect.width,
1195+
top: canvasContentRect.top + (bounds.top / board.height) * canvasContentRect.height,
1196+
bottom: canvasContentRect.top + (bounds.bottom / board.height) * canvasContentRect.height,
1197+
width: ((bounds.right - bounds.left) / board.width) * canvasContentRect.width,
1198+
height: ((bounds.bottom - bounds.top) / board.height) * canvasContentRect.height
1199+
});
1200+
const flipperBounds = ['left', 'right'].map((side) => {
1201+
const segment = app.flipperSegment(side);
1202+
return logicalToPixelRect({
1203+
left: Math.min(segment.a.x, segment.b.x) - segment.thickness,
1204+
right: Math.max(segment.a.x, segment.b.x) + segment.thickness,
1205+
top: Math.min(segment.a.y, segment.b.y) - segment.thickness,
1206+
bottom: Math.max(segment.a.y, segment.b.y) + segment.thickness
1207+
});
1208+
});
11381209
const scrollingElement = document.scrollingElement;
11391210
const scrollSurfaces = [scrollingElement, windowBody, rootNode].filter(Boolean);
1211+
const containment = {
1212+
stage: contains(stageRect, canvasRect),
1213+
wrap: contains(wrapRect, canvasRect),
1214+
body: contains(bodyRect, canvasRect),
1215+
visualViewport: contains(usableViewportRect, canvasRect)
1216+
};
11401217

11411218
return {
11421219
complete: true,
1143-
viewport: { width: window.innerWidth, height: window.innerHeight, usableBottom },
1220+
viewport: {
1221+
width: window.innerWidth,
1222+
height: window.innerHeight,
1223+
visualWidth: window.visualViewport?.width ?? window.innerWidth,
1224+
visualHeight: window.visualViewport?.height ?? window.innerHeight,
1225+
usableBottom: usableViewportRect.bottom
1226+
},
11441227
hasTouch: navigator.maxTouchPoints > 0 && matchMedia('(pointer: coarse)').matches,
11451228
buttonCount: pad.querySelectorAll('button').length,
1146-
padVisible: getComputedStyle(pad).display !== 'none' && inViewport(padRect) && inWindowBody(padRect),
1147-
canvasVisible: getComputedStyle(canvasNode).display !== 'none' && inViewport(canvasRect) && inWindowBody(canvasRect),
1229+
padVisible: getComputedStyle(pad).display !== 'none'
1230+
&& contains(wrapRect, padRect)
1231+
&& contains(bodyRect, padRect)
1232+
&& contains(usableViewportRect, padRect),
1233+
canvasContained: Object.values(containment).every(Boolean),
1234+
containment,
1235+
ratioError,
1236+
ratioWithinTolerance: Number.isFinite(ratioError) && ratioError <= board.ratioTolerance,
1237+
logicalVisibleBottom,
1238+
bottomEdgeVisible: logicalVisibleBottom >= board.height - 1,
1239+
flippersComplete: flipperBounds.every((bounds) => contains(clipRect, bounds)),
11481240
targetsLargeEnough: requiredButtons.every((button) => {
11491241
const rect = button.getBoundingClientRect();
11501242
return rect.width >= 43.5 && rect.height >= 43.5;
11511243
}),
11521244
noVerticalScroll: scrollSurfaces.every((surface) => surface.scrollHeight <= surface.clientHeight + 1 && Math.abs(surface.scrollTop) <= 1),
11531245
canvas: { top: canvasRect.top, bottom: canvasRect.bottom, width: canvasRect.width, height: canvasRect.height },
1246+
canvasContent: canvasContentRect,
1247+
clip: clipRect,
11541248
pad: { top: padRect.top, bottom: padRect.bottom, width: padRect.width, height: padRect.height },
11551249
body: { top: bodyRect.top, bottom: bodyRect.bottom, scrollHeight: windowBody.scrollHeight, clientHeight: windowBody.clientHeight },
11561250
root: { scrollHeight: rootNode.scrollHeight, clientHeight: rootNode.clientHeight },
1251+
flippers: flipperBounds,
11571252
buttons: requiredButtons.map((button) => {
11581253
const rect = button.getBoundingClientRect();
11591254
return { width: rect.width, height: rect.height };
11601255
})
11611256
};
1257+
}, {
1258+
width: PINBALL_BOARD_WIDTH,
1259+
height: PINBALL_BOARD_HEIGHT,
1260+
ratioTolerance: PINBALL_RATIO_TOLERANCE
11621261
});
11631262

1164-
ensure(mobileLayout.complete && mobileLayout.hasTouch, `El contexto movil no expuso touch real (${JSON.stringify(mobileLayout)})`);
1165-
ensure(mobileLayout.buttonCount === 3 && mobileLayout.targetsLargeEnough, `El dock de Pinball no expuso tres controles de al menos 44 px (${JSON.stringify(mobileLayout)})`);
1166-
ensure(mobileLayout.padVisible && mobileLayout.canvasVisible && mobileLayout.noVerticalScroll, `Canvas y dock de Pinball no quedaron visibles simultaneamente sin scroll vertical (${JSON.stringify(mobileLayout)})`);
1167-
1168-
const cdp = await context.newCDPSession(page);
1169-
await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-left]'), '[data-pinball-left]', 'left', 11);
1170-
await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-right]'), '[data-pinball-right]', 'right', 12);
1171-
await root.evaluate((rootNode) => {
1172-
const app = rootNode._pinballApp || rootNode.closest('.window')?._pinballApp;
1173-
app.resetGame({ announce: false });
1174-
});
1175-
const plungerReleased = await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-plunger]'), '[data-pinball-plunger]', 'plunger', 13, 160);
1176-
ensure(plungerReleased.gameState === 'playing' && plungerReleased.launchPower >= 0.3 && plungerReleased.ballInLauncherLane, `El lanzador no disparo la bola al soltar el control tactil (${JSON.stringify(plungerReleased)})`);
1177-
ensure(await page.locator('clippy-character').count() === 0, 'Clippy reaparecio durante la sesion movil');
1263+
const caseLabel = `${viewportCase.width}x${viewportCase.height}`;
1264+
ensure(mobileLayout.complete && mobileLayout.hasTouch, `El contexto movil ${caseLabel} no expuso touch real (${JSON.stringify(mobileLayout)})`);
1265+
ensure(mobileLayout.buttonCount === 3 && mobileLayout.targetsLargeEnough, `El dock de Pinball no expuso tres controles de al menos 44 px en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1266+
ensure(mobileLayout.padVisible && mobileLayout.canvasContained, `Canvas o dock de Pinball quedaron recortados en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1267+
ensure(mobileLayout.ratioWithinTolerance, `El canvas de Pinball deformo la relacion 520:700 en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1268+
ensure(mobileLayout.logicalVisibleBottom >= PINBALL_BOARD_HEIGHT - 1 && mobileLayout.bottomEdgeVisible, `El borde inferior del tablero no quedo visible en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1269+
ensure(mobileLayout.flippersComplete, `Los flippers no quedaron completamente visibles en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1270+
ensure(mobileLayout.noVerticalScroll, `Pinball genero scroll vertical en ${caseLabel} (${JSON.stringify(mobileLayout)})`);
1271+
1272+
if (viewportCase.exerciseTouch) {
1273+
const cdp = await context.newCDPSession(page);
1274+
await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-left]'), '[data-pinball-left]', 'left', 11);
1275+
await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-right]'), '[data-pinball-right]', 'right', 12);
1276+
await root.evaluate((rootNode) => {
1277+
const app = rootNode._pinballApp || rootNode.closest('.window')?._pinballApp;
1278+
app.resetGame({ announce: false });
1279+
});
1280+
const plungerReleased = await touchPinballControl(page, cdp, appWindow.locator('[data-pinball-plunger]'), '[data-pinball-plunger]', 'plunger', 13, 160);
1281+
ensure(plungerReleased.gameState === 'playing' && plungerReleased.launchPower >= 0.3 && plungerReleased.ballInLauncherLane, `El lanzador no disparo la bola al soltar el control tactil (${JSON.stringify(plungerReleased)})`);
1282+
ensure(await page.locator('clippy-character').count() === 0, 'Clippy reaparecio durante la sesion movil');
1283+
}
11781284

1179-
return 'Movil real: Clippy deshabilitado y Pinball con dock tactil, touch press/release y lanzamiento';
1285+
return caseLabel;
11801286
} finally {
11811287
await context.close();
11821288
}
11831289
}
11841290

1291+
async function exerciseMobileClippyAndPinball(browser, baseUrl) {
1292+
const auditedViewports = [];
1293+
for (const viewportCase of PINBALL_MOBILE_VIEWPORTS) {
1294+
auditedViewports.push(await auditMobilePinballViewport(browser, baseUrl, viewportCase));
1295+
}
1296+
return `Movil real: Clippy deshabilitado y Pinball completo en ${auditedViewports.join(', ')}, con touch press/release y lanzamiento`;
1297+
}
1298+
11851299
async function main() {
11861300
const server = await createStaticServer();
11871301
const { port } = server.address();

‎scripts/verify-experience.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const source = [
3737
'css/xp-apps.css',
3838
'css/minesweeper.css',
3939
'css/proyectos-explorer.css',
40+
'scripts/smoke-playwright.mjs',
4041
'assets/images/hd-icons/minesweeper.svg',
4142
'assets/images/hd-icons/pinball.svg'
4243
].map(read).join('\n');
@@ -100,6 +101,8 @@ const checks = [
100101
['Pinball accessibility and cleanup', /toggleFullscreen[\s\S]*destroyPinballApp/],
101102
['Pinball responsive touch dock', /\.xp-pinball-pad\s*\{[\s\S]{0,500}display:\s*none[\s\S]*@media\s*\(max-width:\s*760px\)[\s\S]{0,2000}\.xp-pinball-pad\s*\{[\s\S]{0,300}display:\s*grid/],
102103
['Pinball touch controls and release', /data-pinball-left[\s\S]*data-pinball-plunger[\s\S]*data-pinball-right[\s\S]*bindPadButton[\s\S]*pointerdown[\s\S]*pointerup/],
104+
['Pinball mobile viewport regression matrix', /PINBALL_MOBILE_VIEWPORTS[\s\S]{0,500}375[\s\S]{0,100}600[\s\S]{0,500}430[\s\S]{0,100}739[\s\S]{0,500}390[\s\S]{0,100}844/],
105+
['Pinball complete mobile playfield audit', /canvasContained[\s\S]{0,5000}ratioWithinTolerance[\s\S]{0,5000}logicalVisibleBottom[\s\S]{0,5000}flippersComplete/],
103106
['Winamp real music order', /acdc-thunderstruck\.mp3[\s\S]*soda-stereo-tratame-suavemente\.mp3/],
104107
['Winamp media and Web Audio graph', /createBiquadFilter[\s\S]*createAnalyser[\s\S]*createMediaElementSource/],
105108
['Winamp custom equalizer controls', /data-eq-output="bass"[\s\S]*appearance:\s*none[\s\S]*slider-runnable-track[\s\S]*slider-thumb/],

0 commit comments

Comments
 (0)