diff --git a/ac-electron/main.js b/ac-electron/main.js index 922a69b87..13d5665e8 100644 --- a/ac-electron/main.js +++ b/ac-electron/main.js @@ -1038,16 +1038,49 @@ async function buildPalsTrayImage() { // live tint). Adding a same-color stroke to the path thickens the line art // in vector space, so it stays crisp at every size. const baseFill = accentHex || '#cd5c9b'; - const STROKE_W = 0.5; // extra outline weight, in the SVG's 24-unit viewBox - let svg = fs.readFileSync(svgPath, 'utf8'); - svg = svg.replace( + const fr = parseInt(baseFill.slice(1, 3), 16); + const fg = parseInt(baseFill.slice(3, 5), 16); + const fb = parseInt(baseFill.slice(5, 7), 16); + + // The pals vector is OUTLINE geometry (a closed band tracing the two + // figures), so a plain fill renders as line-art. Rasterize the outline as a + // mask, flood-fill the exterior from the border, then paint every + // non-exterior pixel solid — giving filled figures, recolored to the accent. + const svg = fs.readFileSync(svgPath, 'utf8'); + const outlineSvg = svg.replace( /fill="#[0-9a-fA-F]{3,8}"/g, - `fill="${baseFill}" stroke="${baseFill}" stroke-width="${STROKE_W}" stroke-linejoin="round"` + // Embolden a touch so anti-aliased gaps stay sealed for the flood fill. + `fill="#000000" stroke="#000000" stroke-width="0.7" stroke-linejoin="round"` ); - const svgBuf = Buffer.from(svg); - - // Rasterize the vector once at high resolution, trimmed to the figures. - const master = await sharp(svgBuf, { density: 1200 }).trim().png().toBuffer(); + const mask = await sharp(Buffer.from(outlineSvg), { density: 1200 }) + .trim() + .extend({ top: 4, bottom: 4, left: 4, right: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + const mW = mask.info.width, mH = mask.info.height, mBuf = mask.data; + const isWall = (i) => mBuf[i * 4 + 3] > 40; // outline pixel + const exterior = new Uint8Array(mW * mH); + const stk = []; + for (let x = 0; x < mW; x++) { stk.push(x); stk.push((mH - 1) * mW + x); } + for (let y = 0; y < mH; y++) { stk.push(y * mW); stk.push(y * mW + mW - 1); } + while (stk.length) { + const i = stk.pop(); + if (exterior[i] || isWall(i)) continue; + exterior[i] = 1; + const x = i % mW, y = (i / mW) | 0; + if (x > 0) stk.push(i - 1); + if (x < mW - 1) stk.push(i + 1); + if (y > 0) stk.push(i - mW); + if (y < mH - 1) stk.push(i + mW); + } + // Solid accent silhouette; a thin 1px black border is added per-rep below. + const solid = Buffer.alloc(mW * mH * 4, 0); + for (let i = 0; i < mW * mH; i++) { + if (!exterior[i]) { solid[i * 4] = fr; solid[i * 4 + 1] = fg; solid[i * 4 + 2] = fb; solid[i * 4 + 3] = 255; } + } + // Rasterized solid figures, trimmed to content. + const master = await sharp(solid, { raw: { width: mW, height: mH, channels: 4 } }).trim().png().toBuffer(); const SHRINK = 0.84; // logo height relative to the full menubar slot const SHADOW_ALPHA = 0.7; // hard drop-shadow opacity @@ -1089,19 +1122,29 @@ async function buildPalsTrayImage() { canvas[i + 3] = Math.round(oa * 255); }; - // Pass 1: tight hard drop shadow (black, offset 1pt down-right). - for (let y = 0; y < lh; y++) { - for (let x = 0; x < lw; x++) { - const a = logoBuf[(y * lw + x) * 4 + 3]; - if (a > 0) put(ox + x + shDX, oy + y + shDY, 0, 0, 0, a * SHADOW_ALPHA); - } - } - - // Pass 2: the crisp, accent-colored logo (sharp raw is RGBA). + // Accent fill + a black silhouette border (no drop shadow). The border + // thickness scales with the representation so it reads consistently at + // every DPI (thicker on retina reps where 1px would look hairline). + const AT = 128; // alpha threshold for "solidly inside the figure" + const BORDER = scale + 1; // black outline thickness, in this rep's px for (let y = 0; y < lh; y++) { for (let x = 0; x < lw; x++) { const j = (y * lw + x) * 4; - put(ox + x, oy + y, logoBuf[j + 2], logoBuf[j + 1], logoBuf[j], logoBuf[j + 3]); + const a = logoBuf[j + 3]; + if (a <= 0) continue; + // Edge = solid pixel within BORDER px of a (near-)transparent pixel. + let edge = false; + if (a >= AT) { + for (let dy = -BORDER; dy <= BORDER && !edge; dy++) { + for (let dx = -BORDER; dx <= BORDER; dx++) { + const nx = x + dx, ny = y + dy; + if (nx < 0 || ny < 0 || nx >= lw || ny >= lh) { edge = true; break; } + if (logoBuf[(ny * lw + nx) * 4 + 3] < AT) { edge = true; break; } + } + } + } + if (edge) put(ox + x, oy + y, 0, 0, 0, 255); + else put(ox + x, oy + y, logoBuf[j + 2], logoBuf[j + 1], logoBuf[j], a); } } @@ -1176,6 +1219,13 @@ async function createSystemTray() { } }) .catch(() => {}); + // Re-tint every AC window's chrome to the new accent. + const acc = getAccentRGB(); + if (acc) { + for (const w of BrowserWindow.getAllWindows()) { + try { w.webContents.send('ac-accent', acc); } catch (_) {} + } + } }); } catch (_) {} } else { @@ -1874,26 +1924,30 @@ function getOffsetWindowPosition(sourceWindow, index = 0) { async function openAcPaneWindowInternal(options = {}) { const { sourceWindow = null, index = 0 } = options; - // Start with a wide window - extra height for mode tags at bottom - const winWidth = 680; - const winHeight = 520; + // Start with a smaller, cuter window - extra height for mode tags at bottom + const winWidth = 330; + const winHeight = 290; // Calculate position to avoid overlap const { x, y } = getOffsetWindowPosition(sourceWindow, index); + // 🪟 Native macOS chrome: the standard OS title bar + traffic lights + native + // resize, with a full-bleed webview below it. Set false to fall back to the + // custom card chrome. + const NATIVE_CHROME = !isPaperWM; const winOpts = { width: winWidth, height: winHeight, x, y, - minWidth: 320, - minHeight: 240, - title: 'AC Pane', - frame: false, - transparent: !isPaperWM, - hasShadow: isPaperWM, + minWidth: 260, + minHeight: 220, + title: options.piece || 'prompt', + frame: NATIVE_CHROME ? true : false, + transparent: NATIVE_CHROME ? false : !isPaperWM, + hasShadow: NATIVE_CHROME ? true : isPaperWM, alwaysOnTop: !isPaperWM && preferences.alwaysOnTop, - backgroundColor: isPaperWM ? '#000000' : '#00000000', + backgroundColor: NATIVE_CHROME ? '#111114' : (isPaperWM ? '#000000' : '#00000000'), webPreferences: { nodeIntegration: true, contextIsolation: false, @@ -1904,6 +1958,9 @@ async function openAcPaneWindowInternal(options = {}) { // On PaperWM, use 'normal' type so the WM tiles it instead of floating if (isPaperWM) winOpts.type = 'normal'; const win = new BrowserWindow(winOpts); + // Keep the native title bar showing the current piece (driven by + // 'set-current-piece'), not the flip-view document ("AC Pane"). + if (NATIVE_CHROME) win.on('page-title-updated', (e) => e.preventDefault()); // Plumb the launch piece + environment base into the flip-view shell so // `--piece=NAME` (and `--dev`) actually route. The shell reads ?piece= and @@ -1914,6 +1971,11 @@ async function openAcPaneWindowInternal(options = {}) { const query = { base: baseUrl }; if (options.piece) query.piece = options.piece; if (isPaperWM) query.wm = 'paper'; + if (NATIVE_CHROME) query.chrome = 'native'; + // Tint the window chrome to the macOS system accent (kept in sync below via + // the 'accent-color-changed' broadcast). Backgrounds still follow light/dark. + const _acc = getAccentRGB(); + if (_acc) query.accent = [_acc.r, _acc.g, _acc.b].map((c) => c.toString(16).padStart(2, '0')).join(''); win.loadFile(getAppPath('renderer/flip-view.html'), { query }); // 🎮 Grant sticky user activation to the host page so navigator.getGamepads() @@ -2438,6 +2500,11 @@ ipcMain.handle('set-current-piece', (event, pieceName) => { currentPiece = pieceName || 'prompt'; console.log('[main] Current piece updated:', currentPiece); updateTrayTitle(); + // Reflect the current piece in the native window title bar. + try { + const win = BrowserWindow.fromWebContents(event.sender); + if (win && !win.isDestroyed()) win.setTitle(currentPiece); + } catch (_) {} return currentPiece; }); diff --git a/ac-electron/renderer/flip-view.html b/ac-electron/renderer/flip-view.html index baf7b56d2..a220b1b9c 100644 --- a/ac-electron/renderer/flip-view.html +++ b/ac-electron/renderer/flip-view.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="UTF-8"> - <script>if(new URLSearchParams(location.search).get('wm')==='paper')document.documentElement.classList.add('paperwm')</script> + <script>{const _q=new URLSearchParams(location.search);if(_q.get('wm')==='paper')document.documentElement.classList.add('paperwm');if(_q.get('chrome')==='native')document.documentElement.classList.add('native');}</script> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AC Pane @@ -72,10 +72,10 @@ /* Perspective container - static, provides 3D context */ .flip-perspective { position: fixed; - top: 40px; /* Extra space at top for flip animation headroom */ - left: 30px; /* Extra space for wider flip tabs */ - right: 30px; /* Extra space for wider flip tabs */ - bottom: 56px; /* Extra space at bottom for mode tags + flip headroom */ + top: 22px; /* Extra space at top for flip animation headroom */ + left: 14px; /* Extra space for wider flip tabs */ + right: 14px; /* Extra space for wider flip tabs */ + bottom: 36px; /* Extra space at bottom for mode tags + flip headroom */ perspective: 1200px; perspective-origin: center center; } @@ -264,13 +264,13 @@ top: 100%; transform: translateX(-50%); z-index: 240; - height: 24px; + height: 15px; display: flex; flex-direction: row; align-items: center; justify-content: center; - padding: 4px 10px; - border-radius: 0 0 10px 10px; + padding: 1px 6px; + border-radius: 0 0 7px 7px; background: var(--ac-border-solid); border: 1px solid var(--ac-border); border-top: none; @@ -286,7 +286,7 @@ #volume-slider { -webkit-appearance: none; appearance: none; - width: 110px; + width: 68px; height: 4px; background: var(--ac-border-solid); border-radius: 2px; @@ -335,56 +335,56 @@ /* Debug: background: rgba(255,0,0,0.2); */ } - /* Positions aligned with flip-perspective: top: 40px, left/right: 30px, bottom: 56px */ + /* Positions aligned with flip-perspective: top: 22px, left/right: 14px, bottom: 36px */ .resize-handle.top { - top: 34px; left: 24px; right: 24px; height: 12px; + top: 16px; left: 8px; right: 8px; height: 12px; cursor: n-resize; } - + .resize-handle.bottom { - bottom: 50px; left: 24px; right: 24px; height: 12px; + bottom: 30px; left: 8px; right: 8px; height: 12px; cursor: s-resize; } - + /* Left side - split into top and bottom sections, leaving gap for flip tab */ .resize-handle.left-top { - left: 24px; top: 46px; height: calc(50% - 90px); width: 12px; + left: 8px; top: 28px; height: calc(50% - 90px); width: 12px; cursor: w-resize; } - + .resize-handle.left-bottom { - left: 24px; bottom: 62px; height: calc(50% - 90px); width: 12px; + left: 8px; bottom: 42px; height: calc(50% - 90px); width: 12px; cursor: w-resize; } - + /* Right side - split into top and bottom sections, leaving gap for flip tab */ .resize-handle.right-top { - right: 24px; top: 46px; height: calc(50% - 90px); width: 12px; + right: 8px; top: 28px; height: calc(50% - 90px); width: 12px; cursor: e-resize; } - + .resize-handle.right-bottom { - right: 24px; bottom: 62px; height: calc(50% - 90px); width: 12px; + right: 8px; bottom: 42px; height: calc(50% - 90px); width: 12px; cursor: e-resize; } - + .resize-handle.top-left { - top: 34px; left: 24px; width: 16px; height: 16px; + top: 16px; left: 8px; width: 16px; height: 16px; cursor: nw-resize; } - + .resize-handle.top-right { - top: 34px; right: 24px; width: 16px; height: 16px; + top: 16px; right: 8px; width: 16px; height: 16px; cursor: ne-resize; } - + .resize-handle.bottom-left { - bottom: 50px; left: 24px; width: 16px; height: 16px; + bottom: 30px; left: 8px; width: 16px; height: 16px; cursor: sw-resize; } - + .resize-handle.bottom-right { - bottom: 50px; right: 24px; width: 16px; height: 16px; + bottom: 30px; right: 8px; width: 16px; height: 16px; cursor: se-resize; } @@ -447,9 +447,34 @@ height: 100% !important; } + /* Native macOS chrome: real OS title bar + traffic lights (hiddenInset), + an accent-tinted drag strip across the top, and a full-bleed webview. + The custom card frame / flip / resize handles / volume are suppressed. */ + html.native { background: var(--ac-front-bg, #111); } + html.native body { perspective: none; } + html.native .flip-perspective { + top: 0; left: 0; right: 0; bottom: 0; /* full-bleed webview under the native title bar */ + perspective: none; + } + html.native .flip-card { transform-style: flat; transition: none; } + html.native .card-face { border: none; border-radius: 0; box-shadow: none; } + html.native .card-back, + html.native .flip-tab, + html.native .mode-tags, + html.native .volume-control, + html.native .resize-handle, + html.native .resize-overlay { display: none !important; } + html.native .card-front { position: absolute; inset: 0; } + html.native .card-front webview { width: 100% !important; height: 100% !important; } + /* No custom strip — the standard macOS title bar handles the top. */ + .native-topbar { display: none !important; } + + +
aesthetic.computer
+
@@ -488,7 +513,7 @@
- +
@@ -499,7 +524,35 @@