update frame
This commit is contained in:
50
app.js
50
app.js
@@ -265,6 +265,56 @@ function setStatus(text) {
|
|||||||
|
|
||||||
statusEl.textContent = 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() {
|
function getConnectionSettings() {
|
||||||
return window.TarotAppConfig?.getConnectionSettings?.() || {
|
return window.TarotAppConfig?.getConnectionSettings?.() || {
|
||||||
|
|||||||
@@ -863,13 +863,16 @@
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-shell {
|
.tarot-frame-shell {
|
||||||
width: min(1480px, 100%);
|
width: min(1480px, 100%);
|
||||||
|
max-width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-header {
|
.tarot-frame-header {
|
||||||
@@ -988,6 +991,27 @@
|
|||||||
box-shadow: 0 18px 38px rgba(0, 0, 0, 0.3);
|
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 {
|
.tarot-frame-settings-group {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1099,11 +1123,17 @@
|
|||||||
|
|
||||||
.tarot-frame-board-grid {
|
.tarot-frame-board-grid {
|
||||||
display: block;
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-panel {
|
.tarot-frame-panel {
|
||||||
--frame-cell-size: clamp(34px, 3.1vw, 52px);
|
--frame-grid-zoom-scale: 1;
|
||||||
--frame-gap: clamp(2px, 0.3vw, 6px);
|
--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;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
@@ -1113,7 +1143,8 @@
|
|||||||
radial-gradient(circle at top, rgba(59, 130, 246, 0.08), transparent 34%),
|
radial-gradient(circle at top, rgba(59, 130, 246, 0.08), transparent 34%),
|
||||||
linear-gradient(180deg, #161622 0%, #0f0f17 100%);
|
linear-gradient(180deg, #161622 0%, #0f0f17 100%);
|
||||||
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.24);
|
box-shadow: 0 22px 54px rgba(0, 0, 0, 0.24);
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-panel-head {
|
.tarot-frame-panel-head {
|
||||||
@@ -1151,6 +1182,21 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.tarot-frame-legend {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -1178,18 +1224,19 @@
|
|||||||
|
|
||||||
.tarot-frame-grid {
|
.tarot-frame-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 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-size));
|
grid-template-rows: repeat(var(--frame-grid-size), var(--frame-cell-height));
|
||||||
gap: var(--frame-gap);
|
gap: var(--frame-gap);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
|
width: max-content;
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-slot {
|
.tarot-frame-slot {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--frame-cell-size);
|
width: var(--frame-cell-width);
|
||||||
height: var(--frame-cell-size);
|
height: var(--frame-cell-height);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
transition: transform 120ms ease, box-shadow 120ms ease, border-color 120ms ease;
|
||||||
}
|
}
|
||||||
@@ -1362,8 +1409,8 @@
|
|||||||
.tarot-frame-drag-ghost {
|
.tarot-frame-drag-ghost {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
width: 86px;
|
width: 90px;
|
||||||
height: 129px;
|
height: 135px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1434,7 +1481,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-panel {
|
.tarot-frame-panel {
|
||||||
--frame-cell-size: 28px;
|
--frame-base-cell-width: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tarot-frame-card-badge {
|
.tarot-frame-card-badge {
|
||||||
|
|||||||
@@ -26,11 +26,13 @@
|
|||||||
[18, 17, 15, 14, 13, 9, 8, 7, 6, 5, 4],
|
[18, 17, 15, 14, 13, 9, 8, 7, 6, 5, 4],
|
||||||
[11]
|
[11]
|
||||||
];
|
];
|
||||||
const HOUSE_TRUMP_GRID_ROWS = [1, 3, 5, 7, 9];
|
const HOUSE_TRUMP_GRID_ROWS = [1, 2, 3, 4, 5];
|
||||||
const HOUSE_BOTTOM_START_ROW = 12;
|
const HOUSE_BOTTOM_START_ROW = 8;
|
||||||
const HOUSE_LEFT_START_COLUMN = 2;
|
const HOUSE_LEFT_START_COLUMN = 2;
|
||||||
const HOUSE_MIDDLE_START_COLUMN = 8;
|
const HOUSE_MIDDLE_START_COLUMN = 6;
|
||||||
const HOUSE_RIGHT_START_COLUMN = 15;
|
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 = {
|
const ZODIAC_START_TOKEN_BY_SIGN_ID = {
|
||||||
aries: "03-21",
|
aries: "03-21",
|
||||||
taurus: "04-20",
|
taurus: "04-20",
|
||||||
@@ -45,8 +47,10 @@
|
|||||||
aquarius: "01-20",
|
aquarius: "01-20",
|
||||||
pisces: "02-19"
|
pisces: "02-19"
|
||||||
};
|
};
|
||||||
const MASTER_GRID_SIZE = 18;
|
const MASTER_GRID_SIZE = 14;
|
||||||
const EXPORT_SLOT_SIZE = 120;
|
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_CARD_INSET = 0;
|
||||||
const EXPORT_GRID_GAP = 10;
|
const EXPORT_GRID_GAP = 10;
|
||||||
const EXPORT_PADDING = 28;
|
const EXPORT_PADDING = 28;
|
||||||
@@ -65,9 +69,15 @@
|
|||||||
const FRAME_LAYOUT_GROUPS = [
|
const FRAME_LAYOUT_GROUPS = [
|
||||||
{
|
{
|
||||||
id: "extra-cards",
|
id: "extra-cards",
|
||||||
title: "Extra Row",
|
title: "Extra Band",
|
||||||
description: "Top row for aces, princesses, and the non-zodiac majors.",
|
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 })),
|
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) {
|
getOrderedCards(cards) {
|
||||||
return cards
|
return cards
|
||||||
.filter((card) => isExtraTopRowCard(card))
|
.filter((card) => isExtraTopRowCard(card))
|
||||||
@@ -78,7 +88,7 @@
|
|||||||
id: "small-cards",
|
id: "small-cards",
|
||||||
title: "Small Cards",
|
title: "Small Cards",
|
||||||
description: "Outer perimeter in chronological decan order.",
|
description: "Outer perimeter in chronological decan order.",
|
||||||
positions: buildPerimeterPath(10, 5, 5),
|
positions: buildPerimeterPath(10, 5, 3),
|
||||||
getOrderedCards(cards) {
|
getOrderedCards(cards) {
|
||||||
return cards
|
return cards
|
||||||
.filter((card) => isSmallCard(card))
|
.filter((card) => isSmallCard(card))
|
||||||
@@ -89,7 +99,7 @@
|
|||||||
id: "court-dates",
|
id: "court-dates",
|
||||||
title: "Court Dates",
|
title: "Court Dates",
|
||||||
description: "Inner left frame in chronological court-date order.",
|
description: "Inner left frame in chronological court-date order.",
|
||||||
positions: buildPerimeterPath(4, 8, 6),
|
positions: buildPerimeterPath(4, 8, 4),
|
||||||
getOrderedCards(cards) {
|
getOrderedCards(cards) {
|
||||||
return cards
|
return cards
|
||||||
.filter((card) => isCourtDateCard(card))
|
.filter((card) => isCourtDateCard(card))
|
||||||
@@ -100,7 +110,7 @@
|
|||||||
id: "zodiac-trumps",
|
id: "zodiac-trumps",
|
||||||
title: "Zodiac Trumps",
|
title: "Zodiac Trumps",
|
||||||
description: "Inner right frame in chronological zodiac order.",
|
description: "Inner right frame in chronological zodiac order.",
|
||||||
positions: buildPerimeterPath(4, 8, 10),
|
positions: buildPerimeterPath(4, 8, 8),
|
||||||
getOrderedCards(cards) {
|
getOrderedCards(cards) {
|
||||||
return cards
|
return cards
|
||||||
.filter((card) => isZodiacTrump(card))
|
.filter((card) => isZodiacTrump(card))
|
||||||
@@ -117,8 +127,8 @@
|
|||||||
{
|
{
|
||||||
id: "frames",
|
id: "frames",
|
||||||
label: "Frames",
|
label: "Frames",
|
||||||
title: "Master 18x18 Frame Grid",
|
title: "Master 14x14 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.",
|
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.",
|
statusMessage: "Frames layout applied to the master grid.",
|
||||||
legendItems: FRAME_LAYOUT_GROUPS.map((group) => ({
|
legendItems: FRAME_LAYOUT_GROUPS.map((group) => ({
|
||||||
title: group.title,
|
title: group.title,
|
||||||
@@ -136,7 +146,7 @@
|
|||||||
id: "house",
|
id: "house",
|
||||||
label: "House of Cards",
|
label: "House of Cards",
|
||||||
title: "House of Cards Layout",
|
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.",
|
statusMessage: "House of Cards layout applied to the master grid.",
|
||||||
legendItems: [
|
legendItems: [
|
||||||
{
|
{
|
||||||
@@ -175,7 +185,8 @@
|
|||||||
layoutMenuOpen: false,
|
layoutMenuOpen: false,
|
||||||
currentLayoutId: "frames",
|
currentLayoutId: "frames",
|
||||||
exportInProgress: false,
|
exportInProgress: false,
|
||||||
exportFormat: "webp"
|
exportFormat: "webp",
|
||||||
|
gridZoomStepIndex: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
@@ -216,6 +227,7 @@
|
|||||||
tarotFrameLayoutPanelEl: document.getElementById("tarot-frame-layout-panel"),
|
tarotFrameLayoutPanelEl: document.getElementById("tarot-frame-layout-panel"),
|
||||||
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
tarotFrameSettingsToggleEl: document.getElementById("tarot-frame-settings-toggle"),
|
||||||
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
tarotFrameSettingsPanelEl: document.getElementById("tarot-frame-settings-panel"),
|
||||||
|
tarotFrameGridZoomEl: document.getElementById("tarot-frame-grid-zoom"),
|
||||||
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
tarotFrameShowInfoEl: document.getElementById("tarot-frame-show-info"),
|
||||||
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
tarotFrameHouseSettingsEl: document.getElementById("tarot-frame-house-settings"),
|
||||||
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
tarotFrameHouseTopCardsVisibleEl: document.getElementById("tarot-frame-house-top-cards-visible"),
|
||||||
@@ -304,7 +316,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildReadyStatus(cards) {
|
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) {
|
function normalizeKey(value) {
|
||||||
@@ -859,7 +879,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowCardImage(card) {
|
function shouldShowCardImage(card) {
|
||||||
if (getLayoutPreset().id !== "house" || !card) {
|
if (!card) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,7 +891,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildCardTextFaceModel(card) {
|
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));
|
const displayName = normalizeLabelText(getDisplayCardName(card));
|
||||||
|
|
||||||
if (card?.arcana !== "Major" && label?.primary) {
|
if (card?.arcana !== "Major" && label?.primary) {
|
||||||
@@ -962,14 +982,42 @@
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getLayoutPreset().id === "house") {
|
const label = buildHouseLabel(card);
|
||||||
const label = buildHouseLabel(card);
|
const structuredLabel = normalizeLabelText([label?.primary, label?.secondary].filter(Boolean).join(" · "));
|
||||||
return normalizeLabelText([label?.primary, label?.secondary].filter(Boolean).join(" · "));
|
if (structuredLabel) {
|
||||||
|
return structuredLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getCardOverlayDate(card) || formatMonthDay(getRelation(card, "decan")?.data?.dateStart) || getDisplayCardName(card);
|
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) {
|
function createCardTextFaceElement(faceModel) {
|
||||||
const faceEl = document.createElement("span");
|
const faceEl = document.createElement("span");
|
||||||
faceEl.className = `tarot-frame-card-text-face${faceModel?.className ? ` ${faceModel.className}` : ""}`;
|
faceEl.className = `tarot-frame-card-text-face${faceModel?.className ? ` ${faceModel.className}` : ""}`;
|
||||||
@@ -1088,6 +1136,7 @@
|
|||||||
|
|
||||||
const panelEl = document.createElement("section");
|
const panelEl = document.createElement("section");
|
||||||
panelEl.className = "tarot-frame-panel tarot-frame-panel--master";
|
panelEl.className = "tarot-frame-panel tarot-frame-panel--master";
|
||||||
|
panelEl.style.setProperty("--frame-grid-zoom-scale", String(getGridZoomScale()));
|
||||||
|
|
||||||
const headEl = document.createElement("div");
|
const headEl = document.createElement("div");
|
||||||
headEl.className = "tarot-frame-panel-head";
|
headEl.className = "tarot-frame-panel-head";
|
||||||
@@ -1103,11 +1152,17 @@
|
|||||||
|
|
||||||
const countEl = document.createElement("span");
|
const countEl = document.createElement("span");
|
||||||
countEl.className = "tarot-frame-panel-count";
|
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);
|
headEl.append(titleWrapEl, countEl);
|
||||||
|
|
||||||
panelEl.append(headEl, createLegend(layoutPreset));
|
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");
|
const gridEl = document.createElement("div");
|
||||||
gridEl.className = "tarot-frame-grid tarot-frame-grid--master";
|
gridEl.className = "tarot-frame-grid tarot-frame-grid--master";
|
||||||
gridEl.classList.toggle("is-info-hidden", !state.showInfo);
|
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);
|
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() {
|
function syncControls() {
|
||||||
@@ -1129,6 +1211,7 @@
|
|||||||
tarotFrameLayoutPanelEl,
|
tarotFrameLayoutPanelEl,
|
||||||
tarotFrameSettingsToggleEl,
|
tarotFrameSettingsToggleEl,
|
||||||
tarotFrameSettingsPanelEl,
|
tarotFrameSettingsPanelEl,
|
||||||
|
tarotFrameGridZoomEl,
|
||||||
tarotFrameShowInfoEl,
|
tarotFrameShowInfoEl,
|
||||||
tarotFrameHouseSettingsEl,
|
tarotFrameHouseSettingsEl,
|
||||||
tarotFrameHouseTopCardsVisibleEl,
|
tarotFrameHouseTopCardsVisibleEl,
|
||||||
@@ -1147,7 +1230,6 @@
|
|||||||
tarotFrameExportWebpEl,
|
tarotFrameExportWebpEl,
|
||||||
} = getElements();
|
} = getElements();
|
||||||
const layoutPreset = getLayoutPreset();
|
const layoutPreset = getLayoutPreset();
|
||||||
const isHouseLayout = layoutPreset.id === "house";
|
|
||||||
|
|
||||||
if (tarotFrameLayoutToggleEl) {
|
if (tarotFrameLayoutToggleEl) {
|
||||||
tarotFrameLayoutToggleEl.setAttribute("aria-expanded", state.layoutMenuOpen ? "true" : "false");
|
tarotFrameLayoutToggleEl.setAttribute("aria-expanded", state.layoutMenuOpen ? "true" : "false");
|
||||||
@@ -1176,18 +1258,23 @@
|
|||||||
tarotFrameSettingsPanelEl.hidden = !state.settingsOpen;
|
tarotFrameSettingsPanelEl.hidden = !state.settingsOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tarotFrameGridZoomEl) {
|
||||||
|
tarotFrameGridZoomEl.value = String(state.gridZoomStepIndex);
|
||||||
|
tarotFrameGridZoomEl.disabled = Boolean(state.exportInProgress);
|
||||||
|
}
|
||||||
|
|
||||||
if (tarotFrameShowInfoEl) {
|
if (tarotFrameShowInfoEl) {
|
||||||
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
tarotFrameShowInfoEl.checked = Boolean(state.showInfo);
|
||||||
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
tarotFrameShowInfoEl.disabled = Boolean(state.exportInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tarotFrameHouseSettingsEl) {
|
if (tarotFrameHouseSettingsEl) {
|
||||||
tarotFrameHouseSettingsEl.hidden = !isHouseLayout;
|
tarotFrameHouseSettingsEl.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tarotFrameHouseTopCardsVisibleEl) {
|
if (tarotFrameHouseTopCardsVisibleEl) {
|
||||||
tarotFrameHouseTopCardsVisibleEl.checked = config.getHouseTopCardsVisible?.() !== false;
|
tarotFrameHouseTopCardsVisibleEl.checked = config.getHouseTopCardsVisible?.() !== false;
|
||||||
tarotFrameHouseTopCardsVisibleEl.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
tarotFrameHouseTopCardsVisibleEl.disabled = Boolean(state.exportInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -1207,12 +1294,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
checkbox.checked = Boolean(getter?.()?.[mode]);
|
checkbox.checked = Boolean(getter?.()?.[mode]);
|
||||||
checkbox.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
checkbox.disabled = Boolean(state.exportInProgress);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tarotFrameHouseBottomCardsVisibleEl) {
|
if (tarotFrameHouseBottomCardsVisibleEl) {
|
||||||
tarotFrameHouseBottomCardsVisibleEl.checked = config.getHouseBottomCardsVisible?.() !== false;
|
tarotFrameHouseBottomCardsVisibleEl.checked = config.getHouseBottomCardsVisible?.() !== false;
|
||||||
tarotFrameHouseBottomCardsVisibleEl.disabled = !isHouseLayout || Boolean(state.exportInProgress);
|
tarotFrameHouseBottomCardsVisibleEl.disabled = Boolean(state.exportInProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tarotFrameExportWebpEl) {
|
if (tarotFrameExportWebpEl) {
|
||||||
@@ -1607,10 +1694,10 @@
|
|||||||
context.drawImage(image, drawX, drawY, drawWidth, drawHeight);
|
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 primaryText = normalizeLabelText(faceModel?.primary || "Tarot");
|
||||||
const secondaryText = normalizeLabelText(faceModel?.secondary);
|
const secondaryText = normalizeLabelText(faceModel?.secondary);
|
||||||
const maxWidth = size - 12;
|
const maxWidth = width - 12;
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
const primaryFontSize = faceModel?.className === "is-top-hebrew" && primaryText.length <= 3 ? 14 : 10;
|
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 primaryLineHeight = faceModel?.className === "is-top-hebrew" && primaryText.length <= 3 ? 14 : 11;
|
||||||
const secondaryLineHeight = 9;
|
const secondaryLineHeight = 9;
|
||||||
const totalHeight = (primaryLines.length * primaryLineHeight) + (secondaryLines.length ? 4 + (secondaryLines.length * secondaryLineHeight) : 0);
|
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.textAlign = "center";
|
||||||
context.textBaseline = "alphabetic";
|
context.textBaseline = "alphabetic";
|
||||||
primaryLines.forEach((line) => {
|
primaryLines.forEach((line) => {
|
||||||
context.fillStyle = "#f8fafc";
|
context.fillStyle = "#f8fafc";
|
||||||
context.font = `700 ${primaryFontSize}px ${primaryFontFamily}`;
|
context.font = `700 ${primaryFontSize}px ${primaryFontFamily}`;
|
||||||
context.fillText(line, x + (size / 2), currentY, maxWidth);
|
context.fillText(line, x + (width / 2), currentY, maxWidth);
|
||||||
currentY += primaryLineHeight;
|
currentY += primaryLineHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1639,7 +1726,7 @@
|
|||||||
context.fillStyle = "rgba(248, 250, 252, 0.78)";
|
context.fillStyle = "rgba(248, 250, 252, 0.78)";
|
||||||
context.font = "500 7px 'Segoe UI', sans-serif";
|
context.font = "500 7px 'Segoe UI', sans-serif";
|
||||||
secondaryLines.forEach((line) => {
|
secondaryLines.forEach((line) => {
|
||||||
context.fillText(line, x + (size / 2), currentY, maxWidth);
|
context.fillText(line, x + (width / 2), currentY, maxWidth);
|
||||||
currentY += secondaryLineHeight;
|
currentY += secondaryLineHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1647,13 +1734,13 @@
|
|||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSlotToCanvas(context, x, y, size, card, image) {
|
function drawSlotToCanvas(context, x, y, width, height, card, image) {
|
||||||
if (!card) {
|
if (!card) {
|
||||||
context.save();
|
context.save();
|
||||||
context.setLineDash([6, 6]);
|
context.setLineDash([6, 6]);
|
||||||
context.lineWidth = 1.5;
|
context.lineWidth = 1.5;
|
||||||
context.strokeStyle = "rgba(148, 163, 184, 0.42)";
|
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.stroke();
|
||||||
context.restore();
|
context.restore();
|
||||||
return;
|
return;
|
||||||
@@ -1661,32 +1748,33 @@
|
|||||||
|
|
||||||
const cardX = x + EXPORT_CARD_INSET;
|
const cardX = x + EXPORT_CARD_INSET;
|
||||||
const cardY = y + 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);
|
const showImage = shouldShowCardImage(card);
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
drawRoundedRectPath(context, cardX, cardY, cardSize, cardSize, 0);
|
drawRoundedRectPath(context, cardX, cardY, cardWidth, cardHeight, 0);
|
||||||
context.clip();
|
context.clip();
|
||||||
if (showImage && image) {
|
if (showImage && image) {
|
||||||
drawImageContain(context, image, cardX, cardY, cardSize, cardSize);
|
drawImageContain(context, image, cardX, cardY, cardWidth, cardHeight);
|
||||||
} else if (showImage) {
|
} else if (showImage) {
|
||||||
context.fillStyle = EXPORT_PANEL;
|
context.fillStyle = EXPORT_PANEL;
|
||||||
context.fillRect(cardX, cardY, cardSize, cardSize);
|
context.fillRect(cardX, cardY, cardWidth, cardHeight);
|
||||||
context.fillStyle = "#f8fafc";
|
context.fillStyle = "#f8fafc";
|
||||||
context.textAlign = "center";
|
context.textAlign = "center";
|
||||||
context.textBaseline = "middle";
|
context.textBaseline = "middle";
|
||||||
context.font = "700 14px 'Segoe UI', sans-serif";
|
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;
|
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) => {
|
lines.forEach((line) => {
|
||||||
context.fillText(line, cardX + (cardSize / 2), currentY, cardSize - 18);
|
context.fillText(line, cardX + (cardWidth / 2), currentY, cardWidth - 18);
|
||||||
currentY += lineHeight;
|
currentY += lineHeight;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
context.fillStyle = EXPORT_PANEL;
|
context.fillStyle = EXPORT_PANEL;
|
||||||
context.fillRect(cardX, cardY, cardSize, cardSize);
|
context.fillRect(cardX, cardY, cardWidth, cardHeight);
|
||||||
drawTextFaceToCanvas(context, cardX, cardY, cardSize, buildCardTextFaceModel(card));
|
drawTextFaceToCanvas(context, cardX, cardY, cardWidth, cardHeight, buildCardTextFaceModel(card));
|
||||||
}
|
}
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
@@ -1695,8 +1783,8 @@
|
|||||||
if (overlayText) {
|
if (overlayText) {
|
||||||
const overlayHeight = 30;
|
const overlayHeight = 30;
|
||||||
const overlayX = cardX + 4;
|
const overlayX = cardX + 4;
|
||||||
const overlayY = cardY + cardSize - overlayHeight - 4;
|
const overlayY = cardY + cardHeight - overlayHeight - 4;
|
||||||
const overlayWidth = cardSize - 8;
|
const overlayWidth = cardWidth - 8;
|
||||||
drawRoundedRectPath(context, overlayX, overlayY, overlayWidth, overlayHeight, 8);
|
drawRoundedRectPath(context, overlayX, overlayY, overlayWidth, overlayHeight, 8);
|
||||||
context.fillStyle = EXPORT_BADGE_BACKGROUND;
|
context.fillStyle = EXPORT_BADGE_BACKGROUND;
|
||||||
context.fill();
|
context.fill();
|
||||||
@@ -1754,14 +1842,16 @@
|
|||||||
const cards = getCards();
|
const cards = getCards();
|
||||||
const cardMap = getCardMap(cards);
|
const cardMap = getCardMap(cards);
|
||||||
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
const exportFormat = EXPORT_FORMATS[format] || EXPORT_FORMATS.webp;
|
||||||
const contentSize = (MASTER_GRID_SIZE * EXPORT_SLOT_SIZE) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
const contentWidth = (MASTER_GRID_SIZE * EXPORT_SLOT_WIDTH) + ((MASTER_GRID_SIZE - 1) * EXPORT_GRID_GAP);
|
||||||
const canvasSize = contentSize + (EXPORT_PADDING * 2);
|
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 scale = Math.max(1.5, Math.min(2, Number(window.devicePixelRatio) || 1));
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = Math.ceil(canvasSize * scale);
|
canvas.width = Math.ceil(canvasWidth * scale);
|
||||||
canvas.height = Math.ceil(canvasSize * scale);
|
canvas.height = Math.ceil(canvasHeight * scale);
|
||||||
canvas.style.width = `${canvasSize}px`;
|
canvas.style.width = `${canvasWidth}px`;
|
||||||
canvas.style.height = `${canvasSize}px`;
|
canvas.style.height = `${canvasHeight}px`;
|
||||||
|
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
if (!context) {
|
if (!context) {
|
||||||
@@ -1772,7 +1862,7 @@
|
|||||||
context.imageSmoothingEnabled = true;
|
context.imageSmoothingEnabled = true;
|
||||||
context.imageSmoothingQuality = "high";
|
context.imageSmoothingQuality = "high";
|
||||||
context.fillStyle = EXPORT_BACKGROUND;
|
context.fillStyle = EXPORT_BACKGROUND;
|
||||||
context.fillRect(0, 0, canvasSize, canvasSize);
|
context.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
|
||||||
const imageCache = new Map();
|
const imageCache = new Map();
|
||||||
cards.forEach((card) => {
|
cards.forEach((card) => {
|
||||||
@@ -1793,9 +1883,9 @@
|
|||||||
for (let column = 1; column <= MASTER_GRID_SIZE; column += 1) {
|
for (let column = 1; column <= MASTER_GRID_SIZE; column += 1) {
|
||||||
const slotId = getSlotId(row, column);
|
const slotId = getSlotId(row, column);
|
||||||
const card = getAssignedCard(slotId, cardMap);
|
const card = getAssignedCard(slotId, cardMap);
|
||||||
const x = EXPORT_PADDING + ((column - 1) * (EXPORT_SLOT_SIZE + EXPORT_GRID_GAP));
|
const x = EXPORT_PADDING + ((column - 1) * (EXPORT_SLOT_WIDTH + EXPORT_GRID_GAP));
|
||||||
const y = EXPORT_PADDING + ((row - 1) * (EXPORT_SLOT_SIZE + EXPORT_GRID_GAP));
|
const y = EXPORT_PADDING + ((row - 1) * (EXPORT_SLOT_HEIGHT + EXPORT_GRID_GAP));
|
||||||
drawSlotToCanvas(context, x, y, EXPORT_SLOT_SIZE, card, card ? resolvedImages.get(getCardId(card)) : null);
|
drawSlotToCanvas(context, x, y, EXPORT_SLOT_WIDTH, EXPORT_SLOT_HEIGHT, card, card ? resolvedImages.get(getCardId(card)) : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1839,6 +1929,7 @@
|
|||||||
tarotFrameLayoutPanelEl,
|
tarotFrameLayoutPanelEl,
|
||||||
tarotFrameSettingsToggleEl,
|
tarotFrameSettingsToggleEl,
|
||||||
tarotFrameSettingsPanelEl,
|
tarotFrameSettingsPanelEl,
|
||||||
|
tarotFrameGridZoomEl,
|
||||||
tarotFrameShowInfoEl,
|
tarotFrameShowInfoEl,
|
||||||
tarotFrameHouseTopCardsVisibleEl,
|
tarotFrameHouseTopCardsVisibleEl,
|
||||||
tarotFrameHouseTopInfoHebrewEl,
|
tarotFrameHouseTopInfoHebrewEl,
|
||||||
@@ -1924,6 +2015,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tarotFrameGridZoomEl) {
|
||||||
|
tarotFrameGridZoomEl.addEventListener("change", () => {
|
||||||
|
setGridZoomStepIndex(tarotFrameGridZoomEl.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[
|
[
|
||||||
[tarotFrameHouseTopCardsVisibleEl, (checked) => config.setHouseTopCardsVisible?.(checked)],
|
[tarotFrameHouseTopCardsVisibleEl, (checked) => config.setHouseTopCardsVisible?.(checked)],
|
||||||
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
[tarotFrameHouseTopInfoHebrewEl, (checked) => config.setHouseTopInfoMode?.("hebrew", checked)],
|
||||||
|
|||||||
@@ -1542,6 +1542,19 @@
|
|||||||
event.preventDefault();
|
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) {
|
function isPointOnCard(clientX, clientY, targetImage = imageEl, targetFrame = null) {
|
||||||
const frameElForHitTest = targetFrame || targetImage;
|
const frameElForHitTest = targetFrame || targetImage;
|
||||||
if (!targetImage || !frameElForHitTest) {
|
if (!targetImage || !frameElForHitTest) {
|
||||||
@@ -2693,6 +2706,17 @@
|
|||||||
stepPrimaryCard(event.key === "ArrowRight" ? 1 : -1);
|
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", () => {
|
window.addEventListener("resize", () => {
|
||||||
if (!lightboxState.isOpen) {
|
if (!lightboxState.isOpen) {
|
||||||
return;
|
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-400.css">
|
||||||
<link rel="stylesheet" href="node_modules/@fontsource/amiri/arabic-700.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="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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
<div class="tarot-frame-header">
|
<div class="tarot-frame-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="tarot-frame-title">Tarot Frame</h2>
|
<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>
|
||||||
<div class="tarot-frame-actions">
|
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="tarot-frame-settings-panel" class="tarot-frame-settings-panel" role="dialog" aria-label="Tarot Frame settings" hidden>
|
<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">
|
<label class="tarot-frame-toggle" for="tarot-frame-show-info">
|
||||||
<input id="tarot-frame-show-info" type="checkbox" checked>
|
<input id="tarot-frame-show-info" type="checkbox" checked>
|
||||||
<span>Display Info</span>
|
<span>Display Info</span>
|
||||||
</label>
|
</label>
|
||||||
<div id="tarot-frame-house-settings" class="tarot-frame-settings-group" hidden>
|
<div id="tarot-frame-house-settings" class="tarot-frame-settings-group">
|
||||||
<div class="tarot-frame-settings-heading">House Layout Settings</div>
|
<div class="tarot-frame-settings-heading">Card Display 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 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">
|
<label class="tarot-frame-toggle" for="tarot-frame-house-top-cards-visible">
|
||||||
<input id="tarot-frame-house-top-cards-visible" type="checkbox" checked>
|
<input id="tarot-frame-house-top-cards-visible" type="checkbox" checked>
|
||||||
<span>Show top card images</span>
|
<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/data-service.js?v=20260319-word-dictionary-01"></script>
|
||||||
<script src="app/calendar-events.js"></script>
|
<script src="app/calendar-events.js"></script>
|
||||||
<script src="app/card-images.js?v=20260309-gate"></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-house.js?v=20260401-house-top-date-01"></script>
|
||||||
<script src="app/ui-tarot-relations.js"></script>
|
<script src="app/ui-tarot-relations.js"></script>
|
||||||
<script src="app/ui-now-helpers.js?v=20260314-now-planets-grid-01"></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-detail.js"></script>
|
||||||
<script src="app/ui-numbers.js"></script>
|
<script src="app/ui-numbers.js"></script>
|
||||||
<script src="app/ui-tarot-spread.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-settings.js?v=20260309-gate"></script>
|
||||||
<script src="app/ui-chrome.js?v=20260328-topbar-settings-01"></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>
|
<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-home-calendar.js"></script>
|
||||||
<script src="app/ui-section-state.js?v=20260401-tarot-frame-01"></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/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>
|
<script src="app/navigation-detail-test-harness.js?v=20260401-universal-detail-02"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user