export const CITY_MAP_BOOK_TOP_FORBIDDEN_BOTTOM = 32; export const CITY_MAP_BOOK_BOTTOM_FORBIDDEN_TOP_OFFSET = 56; export const CITY_MAP_BOOK_SAFE_HEIGHT = 264; export const CITY_MAP_BOOK_HOVER_SAFE_SCALE = 1.055; export const CITY_MAP_BOOK_FORBIDDEN_GAP = 2; export const CITY_MAP_SPECIAL_BOOK_LAYOUTS = { shanghai: { minWidth: 1200, marginX: 64, marginY: 132, slots: { "chn-shanghai--douban-25924323": { x: 0.1143, y: 0.4341 }, "chn-shanghai--manual-xinhualu": { x: 0.2254, y: 0.2208 }, "chn-shanghai--map-shanghai-1947-hanghao": { x: 0.3516, y: 0.2208 }, "chn-shanghai--douban-35299337": { x: 0.478, y: 0.18 }, "chn-shanghai--douban-20495120": { x: 0.6006, y: 0.2208 }, "chn-shanghai--manual-jiangnan-shanghai-gardens": { x: 0.7253, y: 0.2208 }, "chn-shanghai--douban-35408305": { x: 0.8568, y: 0.3114 }, "chn-shanghai--douban-36362358": { x: 0.2703, y: 0.7578, offsetY: 5 }, "chn-shanghai--douban-37442766": { x: 0.3944, y: 0.8185 }, "chn-shanghai--douban-36952719": { x: 0.6121, y: 0.8158 }, "chn-shanghai--douban-30254263": { x: 0.7253, y: 0.8168 }, "chn-shanghai--douban-26849413": { x: 0.8371, y: 0.7864, offsetY: -25 }, "chn-shanghai--douban-7055710": { x: 0.9499, y: 0.6581, offsetX: -5 }, }, }, beijing: { minWidth: 1200, marginX: 88, marginY: 132, safeHeight: 264, hoverSafeScale: CITY_MAP_BOOK_HOVER_SAFE_SCALE, topForbiddenBottom: 32, topSafeGap: CITY_MAP_BOOK_FORBIDDEN_GAP, bottomForbiddenTopOffset: 66, slots: { "chn-beijing--douban-1070711": { x: 0.075, y: 0.2786 }, "chn-beijing--douban-36159853": { x: 0.1964, y: 0.2521 }, "chn-beijing--homeshop-wear": { x: 0.3182, y: 0.2396 }, "chn-beijing--douban-37508268": { x: 0.4393, y: 0.322 }, "chn-beijing--douban-25751679": { x: 0.5604, y: 0.2869 }, "chn-beijing--douban-37131044": { x: 0.6817, y: 0.2744 }, "chn-beijing--douban-35279673": { x: 0.8036, y: 0.2883 }, "chn-beijing--douban-34995523": { x: 0.92, y: 0.3858, offsetY: -40 }, "chn-beijing--douban-30116399": { x: 0.075, y: 0.6643 }, "chn-beijing--douban-33395164": { x: 0.22, y: 0.7893 }, "chn-beijing--douban-30469133": { x: 0.36, y: 0.8257 }, "chn-beijing--douban-34991723": { x: 0.5, y: 0.8186 }, "chn-beijing--douban-37134106": { x: 0.64, y: 0.8247 }, "chn-beijing--isbn-9787540265052": { x: 0.7798, y: 0.7893 }, "chn-beijing--douban-26580056": { x: 0.92, y: 0.7893 }, }, }, tokyo: { minWidth: 1200, marginX: 88, marginY: 132, safeHeight: 264, hoverSafeScale: CITY_MAP_BOOK_HOVER_SAFE_SCALE, topForbiddenBottom: 32, topSafeGap: CITY_MAP_BOOK_FORBIDDEN_GAP, bottomForbiddenTopOffset: 66, slots: { "jpn-tokyo--douban-35928183": { x: 0.1508, y: 0.2744 }, "jpn-tokyo--douban-30438342": { x: 0.262, y: 0.2396 }, "jpn-tokyo--external-tsukuda-tsukishima-magazine": { x: 0.3831, y: 0.2396 }, "jpn-tokyo--map-qchizu-tokyo-3000": { x: 0.4952, y: 0.2396 }, "jpn-tokyo--douban-36448234": { x: 0.6385, y: 0.2396 }, "jpn-tokyo--douban-24720958": { x: 0.7691, y: 0.2883 }, "jpn-tokyo--douban-26819477": { x: 0.8639, y: 0.2883 }, "jpn-tokyo--douban-34950394": { x: 0.1446, y: 0.7503 }, "jpn-tokyo--douban-26945179": { x: 0.2573, y: 0.8134 }, "jpn-tokyo--douban-36096287": { x: 0.3745, y: 0.8069 }, "jpn-tokyo--douban-37090707": { x: 0.6924, y: 0.8045 }, "jpn-tokyo--douban-2389079": { x: 0.8056, y: 0.7337 }, "jpn-tokyo--douban-34663226": { x: 0.5703, y: 0.7995 }, }, }, "new-york": { minWidth: 1200, marginX: 88, marginY: 132, safeHeight: 245, slots: { "usa-new-york-city--douban-37199094": { x: 0.0747, y: 0.273 }, "usa-new-york-city--douban-36889885": { x: 0.1885, y: 0.2451 }, "usa-new-york-city--douban-35687602": { x: 0.3022, y: 0.273 }, "usa-new-york-city--map-mapping-historical-ny": { x: 0.4299, y: 0.2228 }, "usa-new-york-city--douban-37281194": { x: 0.6118, y: 0.2646 }, "usa-new-york-city--douban-30201329": { x: 0.086, y: 0.7159 }, "usa-new-york-city--douban-6124372": { x: 0.205, y: 0.7368 }, "usa-new-york-city--douban-30441545": { x: 0.3241, y: 0.7159 }, "usa-new-york-city--external-celluloid-skyline": { x: 0.4828, y: 0.7368 }, "usa-new-york-city--douban-35013441": { x: 0.5952, y: 0.7159 }, "usa-new-york-city--douban-35740229": { x: 0.7077, y: 0.7368 }, "usa-new-york-city--douban-26818878": { x: 0.8201, y: 0.7159 }, "usa-new-york-city--douban-1913467": { x: 0.9325, y: 0.7368 }, "usa-new-york-city--douban-27037182": { x: 0.787, y: 0.3203 }, }, }, "hong-kong": { minWidth: 1200, marginX: 88, marginY: 132, slots: { "chn-hong-kong--douban-36024491": { x: 0.119, y: 0.2354, offsetX: 40 }, "chn-hong-kong--manual-zhenzhong-hk": { x: 0.2639, y: 0.2549, offsetX: 40 }, "chn-hong-kong--manual-hong-kong-guide-2014": { x: 0.4944, y: 0.18, offsetX: -40, offsetY: -10 }, "chn-hong-kong--douban-27004436": { x: 0.69, y: 0.357, offsetY: -60 }, "chn-hong-kong--manual-power-generation-urban-renewal": { x: 0.805, y: 0.357, offsetY: -60 }, "chn-hong-kong--douban-30279653": { x: 0.075, y: 0.66 }, "chn-hong-kong--douban-35452941": { x: 0.1992, y: 0.765 }, "chn-hong-kong--manual-diwenzhi": { x: 0.3233, y: 0.765 }, "chn-hong-kong--douban-36285315": { x: 0.4475, y: 0.765 }, "chn-hong-kong--manual-xiangtu-hongkong": { x: 0.5717, y: 0.765 }, "chn-hong-kong--external-sham-shui-po-101": { x: 0.6958, y: 0.765 }, "chn-hong-kong--external-wanchai-wak-dongnin": { x: 0.82, y: 0.715 }, "chn-hong-kong--douban-27136081": { x: 0.92, y: 0.385 }, }, }, }; export function hasCompleteCityMapSpecialBookLayout(special, entries = []) { const layout = CITY_MAP_SPECIAL_BOOK_LAYOUTS[special?.id]; if (!layout || !entries.length) return false; return entries.every((entry) => Boolean(layout.slots?.[entry?.book?.id])); } export function applyCityMapSpecialBookLayout(special, entries, points, stageRect) { const layout = CITY_MAP_SPECIAL_BOOK_LAYOUTS[special?.id]; if (!layout || stageRect.width < (layout.minWidth || 0)) return points; const minCenterY = cityMapBookMinCenterY(layout); const maxCenterY = cityMapBookMaxCenterY(layout, stageRect, minCenterY); return points.map((point, index) => { const bookId = entries[index]?.book?.id; const slot = layout.slots[bookId]; if (!slot) return point; const marginX = layout.marginX ?? 72; const marginY = layout.marginY ?? 96; return { ...point, x: clamp(stageRect.width * slot.x, marginX, Math.max(marginX, stageRect.width - marginX)) + (slot.offsetX ?? 0), y: clamp(stageRect.height * slot.y, Math.max(marginY, minCenterY), maxCenterY) + (slot.offsetY ?? 0), }; }); } export function cityMapBookTopForbiddenBottom(special) { const layout = CITY_MAP_SPECIAL_BOOK_LAYOUTS[special?.id]; return layout?.topForbiddenBottom ?? CITY_MAP_BOOK_TOP_FORBIDDEN_BOTTOM; } function cityMapBookMinCenterY(layout) { const topForbiddenBottom = layout.topForbiddenBottom ?? CITY_MAP_BOOK_TOP_FORBIDDEN_BOTTOM; const safeHeight = layout.safeHeight ?? CITY_MAP_BOOK_SAFE_HEIGHT; const hoverSafeScale = layout.hoverSafeScale ?? 1; const topSafeGap = layout.topSafeGap ?? 0; return topForbiddenBottom + (safeHeight * hoverSafeScale) / 2 + topSafeGap; } function cityMapBookMaxCenterY(layout, stageRect, minCenterY) { const bottomOffset = layout.bottomForbiddenTopOffset ?? CITY_MAP_BOOK_BOTTOM_FORBIDDEN_TOP_OFFSET; const safeHeight = layout.safeHeight ?? CITY_MAP_BOOK_SAFE_HEIGHT; const bottomForbiddenTop = stageRect.height - bottomOffset; return Math.max(minCenterY, bottomForbiddenTop - safeHeight / 2); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); }