update frame
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user