feat(stage-tamagotchi): better transforming strategy for main-stage window

This commit is contained in:
Makito
2026-04-10 05:02:34 +09:00
parent f65a67f543
commit 2a1c47eab7
2 changed files with 87 additions and 35 deletions
@@ -1,5 +1,5 @@
import type { createContext } from '@moeru/eventa/adapters/electron/main'
import type { BrowserWindow } from 'electron'
import type { BrowserWindow, Rectangle } from 'electron'
import type { OnboardingWindowManager } from '../../../windows/onboarding'
@@ -12,26 +12,33 @@ import { computeAdjacentPosition } from '../../../windows/shared/display'
const ANIMATION_DURATION = 350
function animateWindowPosition(
function animateWindowBounds(
window: BrowserWindow,
targetX: number,
targetY: number,
target: Rectangle,
): ReturnType<typeof animate> | undefined {
if (window.isDestroyed())
return undefined
const bounds = window.getBounds()
const state = { x: bounds.x, y: bounds.y }
const current = window.getBounds()
const state = { x: current.x, y: current.y, w: current.width, h: current.height }
return animate(state, {
x: targetX,
y: targetY,
x: target.x,
y: target.y,
w: target.width,
h: target.height,
duration: ANIMATION_DURATION,
ease: 'outCubic',
modifier: utils.round(0),
onRender: () => {
if (!window.isDestroyed())
window.setPosition(Math.round(state.x), Math.round(state.y))
if (!window.isDestroyed()) {
window.setBounds({
x: Math.round(state.x),
y: Math.round(state.y),
width: Math.round(state.w),
height: Math.round(state.h),
})
}
},
})
}
@@ -51,14 +58,19 @@ export function createOnboardingService(params: {
const onboardingBounds = onboardingWindow.getBounds()
const display = screen.getDisplayMatching(onboardingBounds)
const newPosition = computeAdjacentPosition(
const adjacent = computeAdjacentPosition(
onboardingBounds,
{ width: savedBounds.width, height: savedBounds.height },
display.workArea,
)
currentAnimation?.pause()
currentAnimation = animateWindowPosition(params.mainWindow, newPosition.x, newPosition.y)
currentAnimation = animateWindowBounds(params.mainWindow, {
x: adjacent.x,
y: adjacent.y,
width: adjacent.width,
height: adjacent.height,
})
let userMovedManually = false
let ignoreNextMoves = true
@@ -70,6 +82,7 @@ export function createOnboardingService(params: {
}
params.mainWindow.on('move', moveListener)
params.mainWindow.on('resize', moveListener)
setTimeout(() => {
ignoreNextMoves = false
}, ANIMATION_DURATION + 50)
@@ -78,10 +91,11 @@ export function createOnboardingService(params: {
cleanupOnClosed = params.onboardingWindowManager.onClosed(() => {
params.mainWindow.removeListener('move', moveListener)
params.mainWindow.removeListener('resize', moveListener)
if (!userMovedManually && !params.mainWindow.isDestroyed()) {
currentAnimation?.pause()
currentAnimation = animateWindowPosition(params.mainWindow, savedBounds.x, savedBounds.y)
currentAnimation = animateWindowBounds(params.mainWindow, savedBounds)
}
cleanupOnClosed = undefined
@@ -141,39 +141,77 @@ export function widthFrom(bounds: Rectangle, sizeOptions: Size & { min?: Size, m
return val
}
export interface AdjacentPositionResult {
x: number
y: number
width: number
height: number
scale: number
}
/**
* Compute a position for `target` adjacent to `anchor`, staying within `workArea`.
*
* Compares available space on right, left, and bottom of the anchor and picks the
* side with the most room. Tie-breaking preference: right > left > bottom.
*
* If the target doesn't fit at full size on the best side, it is scaled down
* (preserving aspect ratio) to fit, respecting `minScale`.
*/
export function computeAdjacentPosition(
anchorBounds: Rectangle,
targetSize: { width: number, height: number },
workArea: Rectangle,
offset = 16,
): { x: number, y: number } {
const centerY = anchorBounds.y + Math.floor((anchorBounds.height - targetSize.height) / 2)
const clampY = (y: number) => Math.min(
Math.max(y, workArea.y),
workArea.y + workArea.height - targetSize.height,
)
options?: { margin?: number, minScale?: number },
): AdjacentPositionResult {
const margin = options?.margin ?? 16
const minScale = options?.minScale ?? 0.5
// Try right
const rightX = anchorBounds.x + anchorBounds.width + offset
if (rightX + targetSize.width <= workArea.x + workArea.width) {
return { x: rightX, y: clampY(centerY) }
const waRight = workArea.x + workArea.width
const waBottom = workArea.y + workArea.height
const rightSpace = { w: waRight - (anchorBounds.x + anchorBounds.width + margin), h: workArea.height }
const leftSpace = { w: anchorBounds.x - workArea.x - margin, h: workArea.height }
const bottomSpace = { w: workArea.width, h: waBottom - (anchorBounds.y + anchorBounds.height + margin) }
function maxScale(space: { w: number, h: number }): number {
if (space.w <= 0 || space.h <= 0)
return 0
return Math.min(space.w / targetSize.width, space.h / targetSize.height, 1)
}
// Try left
const leftX = anchorBounds.x - targetSize.width - offset
if (leftX >= workArea.x) {
return { x: leftX, y: clampY(centerY) }
}
const candidates: { side: 'right' | 'left' | 'bottom', scale: number }[] = [
{ side: 'right', scale: maxScale(rightSpace) },
{ side: 'left', scale: maxScale(leftSpace) },
{ side: 'bottom', scale: maxScale(bottomSpace) },
]
// Fall back to below
const belowY = anchorBounds.y + anchorBounds.height + offset
const belowX = anchorBounds.x + Math.floor((anchorBounds.width - targetSize.width) / 2)
return {
x: Math.min(Math.max(belowX, workArea.x), workArea.x + workArea.width - targetSize.width),
y: Math.min(belowY, workArea.y + workArea.height - targetSize.height),
candidates.sort((a, b) => b.scale - a.scale)
const best = candidates[0]!
const scale = Math.max(best.scale, minScale)
const w = Math.round(targetSize.width * scale)
const h = Math.round(targetSize.height * scale)
const clampX = (x: number) => Math.min(Math.max(x, workArea.x), waRight - w)
const clampY = (y: number) => Math.min(Math.max(y, workArea.y), waBottom - h)
const centerY = anchorBounds.y + Math.floor((anchorBounds.height - h) / 2)
switch (best.side) {
case 'right': {
const x = anchorBounds.x + anchorBounds.width + margin
return { x: clampX(x), y: clampY(centerY), width: w, height: h, scale }
}
case 'left': {
const x = anchorBounds.x - w - margin
return { x: clampX(x), y: clampY(centerY), width: w, height: h, scale }
}
case 'bottom': {
const y = anchorBounds.y + anchorBounds.height + margin
const x = anchorBounds.x + Math.floor((anchorBounds.width - w) / 2)
return { x: clampX(x), y: clampY(y), width: w, height: h, scale }
}
}
}