update frame
This commit is contained in:
50
app.js
50
app.js
@@ -265,6 +265,56 @@ function setStatus(text) {
|
||||
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
function isBrowserZoomShortcut(event) {
|
||||
if (!(event?.ctrlKey || event?.metaKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = String(event.key || "").toLowerCase();
|
||||
return key === "+"
|
||||
|| key === "="
|
||||
|| key === "-"
|
||||
|| key === "_"
|
||||
|| key === "0"
|
||||
|| event.code === "NumpadAdd"
|
||||
|| event.code === "NumpadSubtract"
|
||||
|| event.code === "Digit0"
|
||||
|| event.code === "Numpad0";
|
||||
}
|
||||
|
||||
function preventBrowserZoom(event) {
|
||||
if (event.type === "wheel" && !event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "touchmove" && Number(event.touches?.length || 0) < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
document.addEventListener("wheel", preventBrowserZoom, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
document.addEventListener("touchmove", preventBrowserZoom, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
["gesturestart", "gesturechange", "gestureend"].forEach((eventName) => {
|
||||
document.addEventListener(eventName, preventBrowserZoom, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!isBrowserZoomShortcut(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
function getConnectionSettings() {
|
||||
return window.TarotAppConfig?.getConnectionSettings?.() || {
|
||||
|
||||
@@ -863,13 +863,16 @@
|
||||
min-height: 100%;
|
||||
padding: 18px;
|
||||
box-sizing: border-box;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.tarot-frame-shell {
|
||||
width: min(1480px, 100%);
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tarot-frame-header {
|
||||
@@ -988,6 +991,27 @@
|
||||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tarot-frame-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: #e2e8f0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tarot-frame-field select {
|
||||
width: 100%;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid rgba(99, 102, 241, 0.34);
|
||||
border-radius: 12px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tarot-frame-settings-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -1099,11 +1123,17 @@
|
||||
|
||||
.tarot-frame-board-grid {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tarot-frame-panel {
|
||||
--frame-cell-size: clamp(34px, 3.1vw, 52px);
|
||||
--frame-gap: clamp(2px, 0.3vw, 6px);
|
||||
--frame-grid-zoom-scale: 1;
|
||||
--frame-base-cell-width: clamp(32px, 2.6vw, 46px);
|
||||
--frame-cell-width: calc(var(--frame-base-cell-width) * var(--frame-grid-zoom-scale));
|
||||
--frame-cell-height: calc(var(--frame-cell-width) * 1.5);
|
||||
--frame-base-gap: clamp(2px, 0.3vw, 6px);
|
||||
--frame-gap: calc(var(--frame-base-gap) * var(--frame-grid-zoom-scale));
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px;
|
||||
@@ -1113,7 +1143,8 @@
|
||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.08), transparent 34%),
|
||||
linear-gradient(180deg, #161622 0%, #0f0f17 100%);
|
||||
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.24);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tarot-frame-panel-head {
|
||||
@@ -1151,6 +1182,21 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tarot-frame-grid-viewport {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tarot-frame-grid-track {
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tarot-frame-legend {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -1178,18 +1224,19 @@
|
||||
|
||||
.tarot-frame-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--frame-grid-size), var(--frame-cell-size));
|
||||
grid-template-rows: repeat(var(--frame-grid-size), var(--frame-cell-size));
|
||||
grid-template-columns: repeat(var(--frame-grid-size), var(--frame-cell-width));
|
||||
grid-template-rows: repeat(var(--frame-grid-size), var(--frame-cell-height));
|
||||
gap: var(--frame-gap);
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.tarot-frame-slot {
|
||||
position: relative;
|
||||
width: var(--frame-cell-size);
|
||||
height: var(--frame-cell-size);
|
||||
width: var(--frame-cell-width);
|
||||
height: var(--frame-cell-height);
|
||||
border-radius: 8px;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
@@ -1362,8 +1409,8 @@
|
||||
.tarot-frame-drag-ghost {
|
||||
position: fixed;
|
||||
z-index: 120;
|
||||
width: 86px;
|
||||
height: 129px;
|
||||
width: 90px;
|
||||
height: 135px;
|
||||
pointer-events: none;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
@@ -1434,7 +1481,7 @@
|
||||
}
|
||||
|
||||
.tarot-frame-panel {
|
||||
--frame-cell-size: 28px;
|
||||
--frame-base-cell-width: 26px;
|
||||
}
|
||||
|
||||
.tarot-frame-card-badge {
|
||||
|
||||
@@ -26,11 +26,13 @@
|
||||
[18, 17, 15, 14, 13, 9, 8, 7, 6, 5, 4],
|
||||
[11]
|
||||
];
|
||||
const HOUSE_TRUMP_GRID_ROWS = [1, 3, 5, 7, 9];
|
||||
const HOUSE_BOTTOM_START_ROW = 12;
|
||||
const HOUSE_TRUMP_GRID_ROWS = [1, 2, 3, 4, 5];
|
||||
const HOUSE_BOTTOM_START_ROW = 8;
|
||||
const HOUSE_LEFT_START_COLUMN = 2;
|
||||
const HOUSE_MIDDLE_START_COLUMN = 8;
|
||||
const HOUSE_RIGHT_START_COLUMN = 15;
|
||||
const HOUSE_MIDDLE_START_COLUMN = 6;
|
||||
const HOUSE_RIGHT_START_COLUMN = 11;
|
||||
const TAROT_CARD_WIDTH_RATIO = 2;
|
||||
const TAROT_CARD_HEIGHT_RATIO = 3;
|
||||
const ZODIAC_START_TOKEN_BY_SIGN_ID = {
|
||||
aries: "03-21",
|
||||
taurus: "04-20",
|
||||
@@ -45,8 +47,10 @@
|
||||
aquarius: "01-20",
|
||||
pisces: "02-19"
|
||||
};
|
||||
const MASTER_GRID_SIZE = 18;
|
||||
const EXPORT_SLOT_SIZE = 120;
|
||||
const MASTER_GRID_SIZE = 14;
|
||||
const FRAME_GRID_ZOOM_STEPS = [1, 1.2, 1.4, 1.7, 2];
|
||||
const EXPORT_SLOT_WIDTH = 120;
|
||||
const EXPORT_SLOT_HEIGHT = Math.round((EXPORT_SLOT_WIDTH * TAROT_CARD_HEIGHT_RATIO) / TAROT_CARD_WIDTH_RATIO);
|
||||
const EXPORT_CARD_INSET = 0;
|
||||
const EXPORT_GRID_GAP = 10;
|
||||
const EXPORT_PADDING = 28;
|
||||
@@ -65,9 +69,15 @@
|
||||
const FRAME_LAYOUT_GROUPS = [
|
||||
{
|
||||
id: "extra-cards",
|
||||
title: "Extra Row",
|
||||
description: "Top row for aces, princesses, and the non-zodiac majors.",
|
||||
positions: Array.from({ length: MASTER_GRID_SIZE }, (_, index) => ({ row: 1, column: index + 1 })),
|
||||
title: "Extra Band",
|
||||
description: "Two-row top band for aces, princesses, and the non-zodiac majors.",
|
||||
positions: [
|
||||
...Array.from({ length: MASTER_GRID_SIZE }, (_, index) => ({ row: 1, column: index + 1 })),
|
||||
{ row: 2, column: 6 },
|
||||
{ row: 2, column: 7 },
|
||||
{ row: 2, column: 8 },
|
||||
{ row: 2, column: 9 }
|
||||
],
|
||||
getOrderedCards(cards) {
|
||||
return cards
|
||||
.filter((card) => isExtraTopRowCard(card))
|
||||
@@ -78,7 +88,7 @@
|
||||
id: "small-cards",
|
||||
title: "Small Cards",
|
||||
description: "Outer perimeter in chronological decan order.",
|
||||
positions: buildPerimeterPath(10, 5, 5),
|
||||
positions: buildPerimeterPath(10, 5, 3),
|
||||
getOrderedCards(cards) {
|
||||
return cards
|
||||
.filter((card) => isSmallCard(card))
|
||||
@@ -89,7 +99,7 @@
|
||||
id: "court-dates",
|
||||
title: "Court Dates",
|
||||
description: "Inner left frame in chronological court-date order.",
|
||||
positions: buildPerimeterPath(4, 8, 6),
|
||||
positions: buildPerimeterPath(4, 8, 4),
|
||||
getOrderedCards(cards) {
|
||||
return cards
|
||||
.filter((card) => isCourtDateCard(card))
|
||||
@@ -100,7 +110,7 @@
|
||||
id: "zodiac-trumps",
|
||||
title: "Zodiac Trumps",
|
||||
description: "Inner right frame in chronological zodiac order.",
|
||||
positions: buildPerimeterPath(4, 8, 10),
|
||||
positions: buildPerimeterPath(4, 8, 8),
|
||||
getOrderedCards(cards) {
|
||||
return cards
|
||||
.filter((card) => isZodiacTrump(card))
|
||||
@@ -117,8 +127,8 @@
|
||||
{
|
||||
id: "frames",
|
||||
label: "Frames",
|
||||
title: "Master 18x18 Frame Grid",
|
||||
subtitle: "Top row holds the remaining 18 cards, while the centered frame keeps the small cards, court dates, and zodiac trumps grouped together. Every square on the grid is a snap target for custom layouts.",
|
||||
title: "Master 14x14 Frame Grid",
|
||||
subtitle: "A two-row top band holds the remaining 18 cards, while the centered frame keeps the small cards, court dates, and zodiac trumps grouped together. Every square on the grid is a snap target for custom layouts.",
|
||||
statusMessage: "Frames layout applied to the master grid.",
|
||||
legendItems: FRAME_LAYOUT_GROUPS.map((group) => ({
|
||||
title: group.title,
|
||||
@@ -136,7 +146,7 @@
|
||||
id: "house",
|
||||
label: "House of Cards",
|
||||
title: "House of Cards Layout",
|
||||
subtitle: "The legacy house composition now lives inside the same draggable 18x18 grid. Centered trump tiers sit above the three lower columns, while every square still remains available for custom rearranging.",
|
||||
subtitle: "The legacy house composition now lives inside the same draggable 14x14 grid. Centered trump tiers sit above the three lower columns, while every square still remains available for custom rearranging.",
|
||||
statusMessage: "House of Cards layout applied to the master grid.",
|
||||
legendItems: [
|
||||
{
|
||||
@@ -175,7 +185,8 @@
|
||||
layoutMenuOpen: false,
|
||||
currentLayoutId: "frames",
|
||||
exportInProgress: false,
|
||||
exportFormat: "webp"
|
||||
exportFormat: "webp",
|
||||
gridZoomStepIndex: 0
|
||||
};
|
||||
|
||||
let config = {
|
||||
@@ -216,6 +227,7 @@
|
||||
tarotFrameLayoutPanelEl: document.getElementById("tarot-frame-layout-panel"),
|
||||
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
||||
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
||||
tarotFrameGridZoomEl: document.getElementById("tarot-frame-grid-zoom"),
|
||||
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
||||
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
||||
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
||||
@@ -304,7 +316,15 @@
|
||||
}
|
||||
|
||||
function buildReadyStatus(cards) {
|
||||
return `${Array.isArray(cards) ? cards.length : 0} cards ready. Drag any card to any grid square and it will snap into that spot.`;
|
||||
return `${Array.isArray(cards) ? cards.length : 0} cards ready. Drag cards freely and use Settings to change the grid zoom for any layout.`;
|
||||
}
|
||||
|
||||
function getGridZoomScale() {
|
||||
return FRAME_GRID_ZOOM_STEPS[state.gridZoomStepIndex] || FRAME_GRID_ZOOM_STEPS[0];
|
||||
}
|
||||
|
||||
function buildPanelCountText(cards = getCards()) {
|
||||
return `${cards.length} cards / ${MASTER_GRID_SIZE * MASTER_GRID_SIZE} cells · Zoom ${Math.round(getGridZoomScale() * 100)}%`;
|
||||
}
|
||||
|
||||
function normalizeKey(value) {
|
||||
@@ -859,7 +879,7 @@
|
||||
}
|
||||
|
||||
function shouldShowCardImage(card) {
|
||||
if (getLayoutPreset().id !== "house" || !card) {
|
||||
if (!card) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -871,7 +891,7 @@
|
||||
}
|
||||
|
||||
function buildCardTextFaceModel(card) {
|
||||
const label = state.showInfo && getLayoutPreset().id === "house" ? buildHouseLabel(card) : null;
|
||||
const label = state.showInfo ? buildHouseLabel(card) : null;
|
||||
const displayName = normalizeLabelText(getDisplayCardName(card));
|
||||
|
||||
if (card?.arcana !== "Major" && label?.primary) {
|
||||
@@ -962,14 +982,42 @@
|
||||
return "";
|
||||
}
|
||||
|
||||
if (getLayoutPreset().id === "house") {
|
||||
const label = buildHouseLabel(card);
|
||||
return normalizeLabelText([label?.primary, label?.secondary].filter(Boolean).join(" · "));
|
||||
const label = buildHouseLabel(card);
|
||||
const structuredLabel = normalizeLabelText([label?.primary, label?.secondary].filter(Boolean).join(" · "));
|
||||
if (structuredLabel) {
|
||||
return structuredLabel;
|
||||
}
|
||||
|
||||
return getCardOverlayDate(card) || formatMonthDay(getRelation(card, "decan")?.data?.dateStart) || getDisplayCardName(card);
|
||||
}
|
||||
|
||||
function centerGridViewport() {
|
||||
const { tarotFrameBoardEl } = getElements();
|
||||
const gridViewportEl = tarotFrameBoardEl?.querySelector(".tarot-frame-grid-viewport");
|
||||
const gridTrackEl = tarotFrameBoardEl?.querySelector(".tarot-frame-grid-track");
|
||||
if (!(gridViewportEl instanceof HTMLElement) || !(gridTrackEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!(gridViewportEl instanceof HTMLElement) || !(gridTrackEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const overflowX = Math.max(0, gridTrackEl.offsetWidth - gridViewportEl.clientWidth);
|
||||
gridViewportEl.scrollLeft = overflowX > 0 ? overflowX / 2 : 0;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (!(gridViewportEl instanceof HTMLElement) || !(gridTrackEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextOverflowX = Math.max(0, gridTrackEl.offsetWidth - gridViewportEl.clientWidth);
|
||||
gridViewportEl.scrollLeft = nextOverflowX > 0 ? nextOverflowX / 2 : 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createCardTextFaceElement(faceModel) {
|
||||
const faceEl = document.createElement("span");
|
||||
faceEl.className = `tarot-frame-card-text-face${faceModel?.className ? ` ${faceModel.className}` : ""}`;
|
||||
@@ -1088,6 +1136,7 @@
|
||||
|
||||
const panelEl = document.createElement("section");
|
||||
panelEl.className = "tarot-frame-panel tarot-frame-panel--master";
|
||||
panelEl.style.setProperty("--frame-grid-zoom-scale", String(getGridZoomScale()));
|
||||
|
||||
const headEl = document.createElement("div");
|
||||
headEl.className = "tarot-frame-panel-head";
|
||||
@@ -1103,11 +1152,17 @@
|
||||
|
||||
const countEl = document.createElement("span");
|
||||
countEl.className = "tarot-frame-panel-count";
|
||||
countEl.textContent = `${cards.length} cards / ${MASTER_GRID_SIZE * MASTER_GRID_SIZE} cells`;
|
||||
countEl.textContent = buildPanelCountText(cards);
|
||||
headEl.append(titleWrapEl, countEl);
|
||||
|
||||
panelEl.append(headEl, createLegend(layoutPreset));
|
||||
|
||||
const gridViewportEl = document.createElement("div");
|
||||
gridViewportEl.className = "tarot-frame-grid-viewport";
|
||||
|
||||
const gridTrackEl = document.createElement("div");
|
||||
gridTrackEl.className = "tarot-frame-grid-track";
|
||||
|
||||
const gridEl = document.createElement("div");
|
||||
gridEl.className = "tarot-frame-grid tarot-frame-grid--master";
|
||||
gridEl.classList.toggle("is-info-hidden", !state.showInfo);
|
||||
@@ -1119,8 +1174,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
panelEl.appendChild(gridEl);
|
||||
gridTrackEl.appendChild(gridEl);
|
||||
gridViewportEl.appendChild(gridTrackEl);
|
||||
panelEl.appendChild(gridViewportEl);
|
||||
tarotFrameBoardEl.appendChild(panelEl);
|
||||
centerGridViewport();
|
||||
}
|
||||
|
||||
function applyGridZoomState() {
|
||||
const { tarotFrameBoardEl } = getElements();
|
||||
const panelEl = tarotFrameBoardEl?.querySelector(".tarot-frame-panel--master");
|
||||
if (!(panelEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelEl.style.setProperty("--frame-grid-zoom-scale", String(getGridZoomScale()));
|
||||
|
||||
const countEl = panelEl.querySelector(".tarot-frame-panel-count");
|
||||
if (countEl instanceof HTMLElement) {
|
||||
countEl.textContent = buildPanelCountText();
|
||||
}
|
||||
|
||||
centerGridViewport();
|
||||
}
|
||||
|
||||
function setGridZoomStepIndex(nextIndex) {
|
||||
const safeIndex = Math.max(0, Math.min(FRAME_GRID_ZOOM_STEPS.length - 1, Number(nextIndex) || 0));
|
||||
state.gridZoomStepIndex = safeIndex;
|
||||
applyGridZoomState();
|
||||
setStatus(`Frame grid zoom ${Math.round(getGridZoomScale() * 100)}%. This setting applies to every Frame layout.`);
|
||||
}
|
||||
|
||||
function syncControls() {
|
||||
@@ -1129,6 +1211,7 @@
|
||||
tarotFrameLayoutPanelEl,
|
||||
tarotFrameSettingsToggleEl,
|
||||
tarotFrameSettingsPanelEl,
|
||||
tarotFrameGridZoomEl,
|
||||
tarotFrameShowInfoEl,
|
||||
tarotFrameHouseSettingsEl,
|
||||
tarotFrameHouseTopCardsVisibleEl,
|
||||
@@ -1147,7 +1230,6 @@
|
||||
tarotFrameExportWebpEl,
|
||||
} = getElements();
|
||||
const layoutPreset = getLayoutPreset();
|
||||
const isHouseLayout = layoutPreset.id === "house";
|
||||
|
||||
if (tarotFrameLayoutToggleEl) {
|
||||
tarotFrameLayoutToggleEl.setAttribute("aria-expanded", state.layoutMenuOpen ? "true" : "false");
|
||||
@@ -1176,18 +1258,23 @@
|
||||
tarotFrameSettingsPanelEl.hidden = !state.settingsOpen;
|
||||
}
|
||||
|
||||
if (tarotFrameGridZoomEl) {
|
||||
tarotFrameGridZoomEl.value = String(state.gridZoomStepIndex);
|
||||
tarotFrameGridZoomEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
if (tarotFrameShowInfoEl) {
|
||||
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
||||
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
if (tarotFrameHouseSettingsEl) {
|
||||
tarotFrameHouseSettingsEl.hidden = !isHouseLayout;
|
||||
tarotFrameHouseSettingsEl.hidden = false;
|
||||
}
|
||||
|
||||
if (tarotFrameHouseTopCardsVisibleEl) {
|
||||
tarotFrameHouseTopCardsVisibleEl.checked = config.getHouseTopCardsVisible?.() !== false;
|
||||
tarotFrameHouseTopCardsVisibleEl.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
||||
tarotFrameHouseTopCardsVisibleEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
[
|
||||
@@ -1207,12 +1294,12 @@
|
||||
return;
|
||||
}
|
||||
checkbox.checked = Boolean(getter?.()?.[mode]);
|
||||
checkbox.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
||||
checkbox.disabled = Boolean(state.exportInProgress);
|
||||
});
|
||||
|
||||
if (tarotFrameHouseBottomCardsVisibleEl) {
|
||||
tarotFrameHouseBottomCardsVisibleEl.checked = config.getHouseBottomCardsVisible?.() !== false;
|
||||
tarotFrameHouseBottomCardsVisibleEl.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
||||
tarotFrameHouseBottomCardsVisibleEl.disabled = Boolean(state.exportInProgress);
|
||||
}
|
||||
|
||||
if (tarotFrameExportWebpEl) {
|
||||
@@ -1607,10 +1694,10 @@
|
||||
context.drawImage(image, drawX, drawY, drawWidth, drawHeight);
|
||||
}
|
||||
|
||||
function drawTextFaceToCanvas(context, x, y, size, faceModel) {
|
||||
function drawTextFaceToCanvas(context, x, y, width, height, faceModel) {
|
||||
const primaryText = normalizeLabelText(faceModel?.primary || "Tarot");
|
||||
const secondaryText = normalizeLabelText(faceModel?.secondary);
|
||||
const maxWidth = size - 12;
|
||||
const maxWidth = width - 12;
|
||||
|
||||
context.save();
|
||||
const primaryFontSize = faceModel?.className === "is-top-hebrew" && primaryText.length <= 3 ? 14 : 10;
|
||||
@@ -1623,14 +1710,14 @@
|
||||
const primaryLineHeight = faceModel?.className === "is-top-hebrew" && primaryText.length <= 3 ? 14 : 11;
|
||||
const secondaryLineHeight = 9;
|
||||
const totalHeight = (primaryLines.length * primaryLineHeight) + (secondaryLines.length ? 4 + (secondaryLines.length * secondaryLineHeight) : 0);
|
||||
let currentY = y + ((size - totalHeight) / 2) + primaryLineHeight;
|
||||
let currentY = y + ((height - totalHeight) / 2) + primaryLineHeight;
|
||||
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "alphabetic";
|
||||
primaryLines.forEach((line) => {
|
||||
context.fillStyle = "#f8fafc";
|
||||
context.font = `700 ${primaryFontSize}px ${primaryFontFamily}`;
|
||||
context.fillText(line, x + (size / 2), currentY, maxWidth);
|
||||
context.fillText(line, x + (width / 2), currentY, maxWidth);
|
||||
currentY += primaryLineHeight;
|
||||
});
|
||||
|
||||
@@ -1639,7 +1726,7 @@
|
||||
context.fillStyle = "rgba(248, 250, 252, 0.78)";
|
||||
context.font = "500 7px 'Segoe UI', sans-serif";
|
||||
secondaryLines.forEach((line) => {
|
||||
context.fillText(line, x + (size / 2), currentY, maxWidth);
|
||||
context.fillText(line, x + (width / 2), currentY, maxWidth);
|
||||
currentY += secondaryLineHeight;
|
||||
});
|
||||
}
|
||||
@@ -1647,13 +1734,13 @@
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawSlotToCanvas(context, x, y, size, card, image) {
|
||||
function drawSlotToCanvas(context, x, y, width, height, card, image) {
|
||||
if (!card) {
|
||||
context.save();
|
||||
context.setLineDash([6, 6]);
|
||||
context.lineWidth = 1.5;
|
||||
context.strokeStyle = "rgba(148, 163, 184, 0.42)";
|
||||
drawRoundedRectPath(context, x + 1, y + 1, size - 2, size - 2, 10);
|
||||
drawRoundedRectPath(context, x + 1, y + 1, width - 2, height - 2, 10);
|
||||
context.stroke();
|
||||
context.restore();
|
||||
return;
|
||||
@@ -1661,32 +1748,33 @@
|
||||
|
||||
const cardX = x + EXPORT_CARD_INSET;
|
||||
const cardY = y + EXPORT_CARD_INSET;
|
||||
const cardSize = size - (EXPORT_CARD_INSET * 2);
|
||||
const cardWidth = width - (EXPORT_CARD_INSET * 2);
|
||||
const cardHeight = height - (EXPORT_CARD_INSET * 2);
|
||||
const showImage = shouldShowCardImage(card);
|
||||
|
||||
context.save();
|
||||
drawRoundedRectPath(context, cardX, cardY, cardSize, cardSize, 0);
|
||||
drawRoundedRectPath(context, cardX, cardY, cardWidth, cardHeight, 0);
|
||||
context.clip();
|
||||
if (showImage && image) {
|
||||
drawImageContain(context, image, cardX, cardY, cardSize, cardSize);
|
||||
drawImageContain(context, image, cardX, cardY, cardWidth, cardHeight);
|
||||
} else if (showImage) {
|
||||
context.fillStyle = EXPORT_PANEL;
|
||||
context.fillRect(cardX, cardY, cardSize, cardSize);
|
||||
context.fillRect(cardX, cardY, cardWidth, cardHeight);
|
||||
context.fillStyle = "#f8fafc";
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
context.font = "700 14px 'Segoe UI', sans-serif";
|
||||
const lines = wrapCanvasText(context, getDisplayCardName(card), cardSize - 18, 4);
|
||||
const lines = wrapCanvasText(context, getDisplayCardName(card), cardWidth - 18, 4);
|
||||
const lineHeight = 18;
|
||||
let currentY = cardY + (cardSize / 2) - (((Math.max(1, lines.length) - 1) * lineHeight) / 2);
|
||||
let currentY = cardY + (cardHeight / 2) - (((Math.max(1, lines.length) - 1) * lineHeight) / 2);
|
||||
lines.forEach((line) => {
|
||||
context.fillText(line, cardX + (cardSize / 2), currentY, cardSize - 18);
|
||||
context.fillText(line, cardX + (cardWidth / 2), currentY, cardWidth - 18);
|
||||
currentY += lineHeight;
|
||||
});
|
||||
} else {
|
||||
context.fillStyle = EXPORT_PANEL;
|
||||
context.fillRect(cardX, cardY, cardSize, cardSize);
|
||||
drawTextFaceToCanvas(context, cardX, cardY, cardSize, buildCardTextFaceModel(card));
|
||||
context.fillRect(cardX, cardY, cardWidth, cardHeight);
|
||||
drawTextFaceToCanvas(context, cardX, cardY, cardWidth, cardHeight, buildCardTextFaceModel(card));
|
||||
}
|
||||
context.restore();
|
||||
|
||||
@@ -1695,8 +1783,8 @@
|
||||
if (overlayText) {
|
||||
const overlayHeight = 30;
|
||||
const overlayX = cardX + 4;
|
||||
const overlayY = cardY + cardSize - overlayHeight - 4;
|
||||
const overlayWidth = cardSize - 8;
|
||||
const overlayY = cardY + cardHeight - overlayHeight - 4;
|
||||
const overlayWidth = cardWidth - 8;
|
||||
drawRoundedRectPath(context, overlayX, overlayY, overlayWidth, overlayHeight, 8);
|
||||
context.fillStyle = EXPORT_BADGE_BACKGROUND;
|
||||
context.fill();
|
||||
@@ -1754,14 +1842,16 @@
|
||||
const cards = getCards();
|
||||
const cardMap = getCardMap(cards);
|
||||
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
||||
const contentSize = (MASTER_GRID_SIZE * EXPORT_SLOT_SIZE) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||
const canvasSize = contentSize + (EXPORT_PADDING * 2);
|
||||
const contentWidth = (MASTER_GRID_SIZE * EXPORT_SLOT_WIDTH) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||
const contentHeight = (MASTER_GRID_SIZE * EXPORT_SLOT_HEIGHT) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||
const canvasWidth = contentWidth + (EXPORT_PADDING * 2);
|
||||
const canvasHeight = contentHeight + (EXPORT_PADDING * 2);
|
||||
const scale = Math.max(1.5, Math.min(2, Number(window.devicePixelRatio) || 1));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.ceil(canvasSize * scale);
|
||||
canvas.height = Math.ceil(canvasSize * scale);
|
||||
canvas.style.width = `${canvasSize}px`;
|
||||
canvas.style.height = `${canvasSize}px`;
|
||||
canvas.width = Math.ceil(canvasWidth * scale);
|
||||
canvas.height = Math.ceil(canvasHeight * scale);
|
||||
canvas.style.width = `${canvasWidth}px`;
|
||||
canvas.style.height = `${canvasHeight}px`;
|
||||
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
@@ -1772,7 +1862,7 @@
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = "high";
|
||||
context.fillStyle = EXPORT_BACKGROUND;
|
||||
context.fillRect(0, 0, canvasSize, canvasSize);
|
||||
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
const imageCache = new Map();
|
||||
cards.forEach((card) => {
|
||||
@@ -1793,9 +1883,9 @@
|
||||
for (let column = 1; column <= MASTER_GRID_SIZE; column += 1) {
|
||||
const slotId = getSlotId(row, column);
|
||||
const card = getAssignedCard(slotId, cardMap);
|
||||
const x = EXPORT_PADDING + ((column - 1) * (EXPORT_SLOT_SIZE + EXPORT_GRID_GAP));
|
||||
const y = EXPORT_PADDING + ((row - 1) * (EXPORT_SLOT_SIZE + EXPORT_GRID_GAP));
|
||||
drawSlotToCanvas(context, x, y, EXPORT_SLOT_SIZE, card, card ? resolvedImages.get(getCardId(card)) : null);
|
||||
const x = EXPORT_PADDING + ((column - 1) * (EXPORT_SLOT_WIDTH + EXPORT_GRID_GAP));
|
||||
const y = EXPORT_PADDING + ((row - 1) * (EXPORT_SLOT_HEIGHT + EXPORT_GRID_GAP));
|
||||
drawSlotToCanvas(context, x, y, EXPORT_SLOT_WIDTH, EXPORT_SLOT_HEIGHT, card, card ? resolvedImages.get(getCardId(card)) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1839,6 +1929,7 @@
|
||||
tarotFrameLayoutPanelEl,
|
||||
tarotFrameSettingsToggleEl,
|
||||
tarotFrameSettingsPanelEl,
|
||||
tarotFrameGridZoomEl,
|
||||
tarotFrameShowInfoEl,
|
||||
tarotFrameHouseTopCardsVisibleEl,
|
||||
tarotFrameHouseTopInfoHebrewEl,
|
||||
@@ -1924,6 +2015,12 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (tarotFrameGridZoomEl) {
|
||||
tarotFrameGridZoomEl.addEventListener("change", () => {
|
||||
setGridZoomStepIndex(tarotFrameGridZoomEl.value);
|
||||
});
|
||||
}
|
||||
|
||||
[
|
||||
[tarotFrameHouseTopCardsVisibleEl, (checked) => config.setHouseTopCardsVisible?.(checked)],
|
||||
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
||||
|
||||
@@ -1542,6 +1542,19 @@
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function preventBrowserZoomGesture(event) {
|
||||
if (!lightboxState.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === "wheel" && !event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function isPointOnCard(clientX, clientY, targetImage = imageEl, targetFrame = null) {
|
||||
const frameElForHitTest = targetFrame || targetImage;
|
||||
if (!targetImage || !frameElForHitTest) {
|
||||
@@ -2693,6 +2706,17 @@
|
||||
stepPrimaryCard(event.key === "ArrowRight" ? 1 : -1);
|
||||
});
|
||||
|
||||
document.addEventListener("wheel", preventBrowserZoomGesture, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
["gesturestart", "gesturechange", "gestureend"].forEach((eventName) => {
|
||||
document.addEventListener(eventName, preventBrowserZoomGesture, {
|
||||
capture: true,
|
||||
passive: false
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
if (!lightboxState.isOpen) {
|
||||
return;
|
||||
|
||||
26
index.html
26
index.html
@@ -16,7 +16,7 @@
|
||||
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-400.css">
|
||||
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.css">
|
||||
<link rel="stylesheet" href="node_modules/@fontsource/noto-naskh-arabic/arabic-400.css">
|
||||
<link rel="stylesheet" href="app/styles.css?v=20260401-tarot-frame-12">
|
||||
<link rel="stylesheet" href="app/styles.css?v=20260402-frame-center-05">
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
@@ -313,7 +313,7 @@
|
||||
<div class="tarot-frame-header">
|
||||
<div>
|
||||
<h2 class="tarot-frame-title">Tarot Frame</h2>
|
||||
<p class="tarot-frame-copy">Arrange all 78 tarot cards inside one master 18x18 grid, then switch between the Frames and House of Cards presets without leaving the page.</p>
|
||||
<p class="tarot-frame-copy">Arrange all 78 tarot cards inside one master 14x14 grid, then switch between the Frames and House of Cards presets without leaving the page. Use the shared settings panel to change the grid zoom for any layout.</p>
|
||||
</div>
|
||||
<div class="tarot-frame-actions">
|
||||
<button id="tarot-frame-settings-toggle" class="tarot-frame-action-btn tarot-frame-settings-toggle" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="tarot-frame-settings-panel">Settings</button>
|
||||
@@ -329,13 +329,23 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="tarot-frame-settings-panel" class="tarot-frame-settings-panel" role="dialog" aria-label="Tarot Frame settings" hidden>
|
||||
<label class="tarot-frame-field" for="tarot-frame-grid-zoom">
|
||||
<span>Grid Zoom</span>
|
||||
<select id="tarot-frame-grid-zoom">
|
||||
<option value="0">100%</option>
|
||||
<option value="1">120%</option>
|
||||
<option value="2">140%</option>
|
||||
<option value="3">170%</option>
|
||||
<option value="4">200%</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="tarot-frame-toggle" for="tarot-frame-show-info">
|
||||
<input id="tarot-frame-show-info" type="checkbox" checked>
|
||||
<span>Display Info</span>
|
||||
</label>
|
||||
<div id="tarot-frame-house-settings" class="tarot-frame-settings-group" hidden>
|
||||
<div class="tarot-frame-settings-heading">House Layout Settings</div>
|
||||
<div class="tarot-frame-settings-note">These controls mirror the old House of Cards view and only apply while that layout is active.</div>
|
||||
<div id="tarot-frame-house-settings" class="tarot-frame-settings-group">
|
||||
<div class="tarot-frame-settings-heading">Card Display Settings</div>
|
||||
<div class="tarot-frame-settings-note">These controls apply to every Frame layout. Top settings affect major arcana, and lower settings affect minor and court cards.</div>
|
||||
<label class="tarot-frame-toggle" for="tarot-frame-house-top-cards-visible">
|
||||
<input id="tarot-frame-house-top-cards-visible" type="checkbox" checked>
|
||||
<span>Show top card images</span>
|
||||
@@ -1108,7 +1118,7 @@
|
||||
<script src="app/data-service.js?v=20260319-word-dictionary-01"></script>
|
||||
<script src="app/calendar-events.js"></script>
|
||||
<script src="app/card-images.js?v=20260309-gate"></script>
|
||||
<script src="app/ui-tarot-lightbox.js?v=20260328-lightbox-settings-01"></script>
|
||||
<script src="app/ui-tarot-lightbox.js?v=20260402-lightbox-pinch-zoom-01"></script>
|
||||
<script src="app/ui-tarot-house.js?v=20260401-house-top-date-01"></script>
|
||||
<script src="app/ui-tarot-relations.js"></script>
|
||||
<script src="app/ui-now-helpers.js?v=20260314-now-planets-grid-01"></script>
|
||||
@@ -1168,7 +1178,7 @@
|
||||
<script src="app/ui-numbers-detail.js"></script>
|
||||
<script src="app/ui-numbers.js"></script>
|
||||
<script src="app/ui-tarot-spread.js"></script>
|
||||
<script src="app/ui-tarot-frame.js?v=20260401-tarot-frame-11"></script>
|
||||
<script src="app/ui-tarot-frame.js?v=20260402-frame-center-05"></script>
|
||||
<script src="app/ui-settings.js?v=20260309-gate"></script>
|
||||
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></script>
|
||||
<script src="app/ui-navigation.js?v=20260401-tarot-frame-01"></script>
|
||||
@@ -1177,7 +1187,7 @@
|
||||
<script src="app/ui-home-calendar.js"></script>
|
||||
<script src="app/ui-section-state.js?v=20260401-tarot-frame-01"></script>
|
||||
<script src="app/app-runtime.js?v=20260309-gate"></script>
|
||||
<script src="app.js?v=20260401-tarot-frame-02"></script>
|
||||
<script src="app.js?v=20260402-global-zoom-01"></script>
|
||||
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user