Miyako Photo Studio
都写真館は、岐阜・伊奈波神社で永くご家族の大切な一日を見つめてきた写真館です。お宮参り、七五三、Studio Cachetteでの宣材プロフィール・成長記録・美肌撮影、Studio Cachette Babyのベビー撮影、Studio Cachette Kidsのキッズフォト・バースデー撮影まで、その日の空気と想いを丁寧に写真に残します。
Concept
大切にしているのは、本質の美しさ
本質を見つめると、昔から変わらない家族を愛する気持ちや人の想いが美しく写し出されます。
お宮参り、七五三などのお子さまの成長・長寿のお祝いなど、人生にはいくつものイベントがあり、それぞれに意味があります。
お祝い事を喜び、成長を感謝し、健康を願う、そこには家族を想う気持ちがあります。その想いを思い出として写真に残すこと。
その時々だけの家族の美しい瞬間をありのまま写すこと。それが都写真館の撮影です。
{
const values = [grid.dataset.pageSize, galleryData.pageSize]
.map((value) => Number.parseInt(value, 10))
.filter(Number.isFinite);
const value = values[0] || 12;
return Math.min(Math.max(value, 1), 24);
};
const normalizeCategories = (data) => {
if (!data || !Array.isArray(data.categories)) return [];
return data.categories.map((category) => {
if (!category || typeof category !== "object") return null;
const categoryId = cleanId(category.id);
const categoryTitle = cleanText(category.title, categoryId || "写真");
const items = Array.isArray(category.items) ? category.items : [];
const normalizedItems = items.map((item) => {
if (!item || typeof item !== "object") return null;
const src = typeof item.src === "string" ? item.src.trim() : "";
const rawThumbSrc = typeof item.thumbSrc === "string" ? item.thumbSrc.trim() : "";
const thumbSrc = rawThumbSrc ? rawThumbSrc : src;
if (!isAllowedImageUrl(src) || !isAllowedImageUrl(thumbSrc)) return null;
return {
src,
thumbSrc,
alt: cleanText(item.alt, categoryTitle),
familyId: cleanText(item.familyId || src, src).slice(0, 120),
categoryId,
categoryTitle
};
}).filter(Boolean);
return normalizedItems.length ? { id: categoryId, title: categoryTitle, items: normalizedItems } : null;
}).filter(Boolean);
};
const pageSize = parsePageSize();
const categories = normalizeCategories(galleryData);
const randomInt = (max) => Math.floor(Math.random() * max);
const shuffleArray = (sourceItems) => {
const result = sourceItems.slice();
for (let i = result.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[result[i], result[j]] = [result[j], result[i]];
}
return result;
};
const nonAdjacentShuffle = (sourceItems) => {
const groups = new Map();
sourceItems.forEach((item) => {
const key = item.familyId || item.src;
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
});
if (groups.size === sourceItems.length) return shuffleArray(sourceItems);
const buckets = Array.from(groups.entries()).map(([familyId, values]) => ({
familyId,
values: shuffleArray(values)
}));
const result = [];
let previousFamily = null;
while (buckets.some((bucket) => bucket.values.length > 0)) {
const eligibleBuckets = buckets.filter((bucket) => (
bucket.values.length > 0 ? bucket.familyId !== previousFamily : false
));
const candidates = eligibleBuckets.length
? eligibleBuckets
: buckets.filter((bucket) => bucket.values.length > 0);
const maxRemaining = Math.max(...candidates.map((bucket) => bucket.values.length));
const heaviestCandidates = candidates.filter((bucket) => bucket.values.length === maxRemaining);
const bucket = heaviestCandidates[randomInt(heaviestCandidates.length)];
result.push(bucket.values.pop());
previousFamily = bucket.familyId;
}
return result;
};
const buildBalancedTopGallery = (categories) => {
const oldShichigosanUrlPattern = /static\.wixstatic\.com\/media\/f91e02_/i;
const filterCategoryItems = (category) => (
category.items.filter((item) => (
category.id !== "shichigosan"
|| !oldShichigosanUrlPattern.test(`${item.src || ""} ${item.thumbSrc || ""}`)
))
);
// Current operation keeps randomized category balancing. Switch here if a fixed SEO order becomes the spec.
let queues = shuffleArray(categories.map((category) => ({
id: category.id,
title: category.title,
items: nonAdjacentShuffle(filterCategoryItems(category))
}))).filter((queue) => queue.items.length > 0);
const result = [];
while (queues.length) {
shuffleArray(queues).forEach((queue) => {
const item = queue.items.shift();
if (item) result.push(item);
});
queues = queues.filter((queue) => queue.items.length > 0);
}
return result;
};
const orderedItems = buildBalancedTopGallery(categories);
let shownCount = 0;
let activeIndex = 0;
let activeTriggerButton = null;
let previousFocus = null;
const lightbox = document.getElementById("top-gallery-lightbox");
const lightboxFrame = lightbox?.querySelector(".top-gallery-lightbox-frame");
const lightboxImage = document.getElementById("top-gallery-lightbox-image");
const lightboxImageWrap = lightboxImage?.closest(".top-gallery-lightbox-image-wrap");
const lightboxCaption = document.getElementById("top-gallery-lightbox-caption");
const lightboxCount = document.getElementById("top-gallery-lightbox-count");
const lightboxClose = lightbox?.querySelector("[data-top-gallery-close]");
const backgroundInertState = [];
if (lightbox) {
if (lightbox.parentElement !== document.body) {
document.body.appendChild(lightbox);
}
}
if (lightbox) {
lightbox.setAttribute("aria-hidden", "true");
}
if (!orderedItems.length) {
moreButton.hidden = true;
grid.dataset.galleryError = "empty";
return;
}
const clearTopGalleryExpandedFigure = () => {
document.querySelectorAll(".top-gallery-item.is-expanded").forEach((figure) => {
figure.classList.remove("is-expanded");
});
};
const markTopGalleryTrigger = (triggerButton) => {
const figure = triggerButton?.closest(".top-gallery-item");
clearTopGalleryExpandedFigure();
if (figure) figure.classList.add("is-expanded");
};
const setBackgroundInert = (enabled) => {
if (!lightbox) return;
if (enabled) {
backgroundInertState.length = 0;
Array.from(document.body.children).forEach((element) => {
if (element === lightbox || element.tagName === "SCRIPT" || element.tagName === "STYLE") return;
backgroundInertState.push({
element,
inert: element.inert,
ariaHidden: element.getAttribute("aria-hidden")
});
element.inert = true;
element.setAttribute("aria-hidden", "true");
});
return;
}
backgroundInertState.forEach(({ element, inert, ariaHidden }) => {
element.inert = inert;
if (ariaHidden === null) {
element.removeAttribute("aria-hidden");
} else {
element.setAttribute("aria-hidden", ariaHidden);
}
});
backgroundInertState.length = 0;
};
const updateMoreButton = () => {
const remaining = orderedItems.length - shownCount;
moreButton.hidden = remaining <= 0;
if (remaining > 0) {
moreButton.setAttribute("aria-label", `続きを見る。残り${remaining}枚`);
}
};
const updateLightbox = () => {
const item = orderedItems[activeIndex];
if (!item || !lightboxImage || !lightboxCaption || !lightboxCount) return;
lightboxImageWrap?.classList.remove("is-image-error");
lightboxImage.src = item.src;
lightboxImage.alt = item.alt;
lightboxImage.referrerPolicy = "strict-origin-when-cross-origin";
lightboxCaption.textContent = item.alt;
lightboxCount.textContent = `${activeIndex + 1} / ${Math.min(shownCount, orderedItems.length)}`;
};
lightboxImage?.addEventListener("error", () => {
lightboxImageWrap?.classList.add("is-image-error");
if (lightboxCaption) lightboxCaption.textContent = "画像を読み込めませんでした";
if (lightboxImage) lightboxImage.alt = "画像を読み込めませんでした";
});
const getLightboxFocusable = () => {
if (!lightbox) return [];
return Array.from(lightbox.querySelectorAll("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"))
.filter((element) => (!element.disabled ? element.offsetParent !== null : false));
};
const focusLightbox = () => {
(lightboxClose || lightboxFrame)?.focus({ preventScroll: true });
requestAnimationFrame(() => {
if (!lightbox?.contains(document.activeElement)) {
(lightboxClose || lightboxFrame)?.focus({ preventScroll: true });
}
});
};
const trapLightboxFocus = (event) => {
if (event.key !== "Tab" || !lightbox || lightbox.hidden) return;
const focusable = getLightboxFocusable();
if (!focusable.length) {
event.preventDefault();
lightboxFrame?.focus({ preventScroll: true });
return;
}
const first = focusable[0];
const last = focusable[focusable.length - 1];
const active = document.activeElement;
if (event.shiftKey) {
if (active === first || active === lightboxFrame) {
event.preventDefault();
last.focus({ preventScroll: true });
}
} else if (active === last) {
event.preventDefault();
first.focus({ preventScroll: true });
}
};
const openTopGalleryLightbox = (index, triggerButton) => {
if (!lightbox) return;
previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
activeIndex = index;
activeTriggerButton = triggerButton;
markTopGalleryTrigger(activeTriggerButton);
updateLightbox();
lightbox.hidden = false;
lightbox.setAttribute("aria-hidden", "false");
setBackgroundInert(true);
document.body.classList.add("top-gallery-lightbox-open");
focusLightbox();
};
const closeTopGalleryLightbox = () => {
if (!lightbox) return;
lightbox.hidden = true;
lightbox.setAttribute("aria-hidden", "true");
setBackgroundInert(false);
document.body.classList.remove("top-gallery-lightbox-open");
clearTopGalleryExpandedFigure();
if (lightboxImage) {
lightboxImage.removeAttribute("src");
lightboxImage.alt = "";
}
const focusTarget = activeTriggerButton || previousFocus;
if (focusTarget) {
if (typeof focusTarget.focus === "function") {
focusTarget.focus({ preventScroll: true });
}
}
activeTriggerButton = null;
previousFocus = null;
};
const moveTopGalleryLightbox = (delta) => {
const availableCount = Math.min(shownCount, orderedItems.length);
if (!availableCount) return;
activeIndex = (activeIndex + delta + availableCount) % availableCount;
updateLightbox();
};
const renderTopGallery = () => {
const nextItems = orderedItems.slice(shownCount, shownCount + pageSize);
const fragment = document.createDocumentFragment();
nextItems.forEach((item, offset) => {
const index = shownCount + offset;
const figure = document.createElement("figure");
const button = document.createElement("button");
const img = document.createElement("img");
figure.className = "top-gallery-item";
figure.dataset.category = item.categoryId || "";
button.className = "top-gallery-button";
button.type = "button";
button.setAttribute("aria-label", `${item.alt}を拡大表示`);
button.addEventListener("click", () => openTopGalleryLightbox(index, button));
img.src = item.thumbSrc || item.src;
img.alt = item.alt;
img.loading = "lazy";
img.decoding = "async";
img.referrerPolicy = "strict-origin-when-cross-origin";
img.addEventListener("error", () => {
figure.classList.add("is-image-error");
button.disabled = true;
button.setAttribute("aria-label", "画像を読み込めません");
}, { once: true });
button.appendChild(img);
figure.appendChild(button);
fragment.appendChild(figure);
});
grid.appendChild(fragment);
shownCount += nextItems.length;
updateMoreButton();
};
moreButton.addEventListener("click", renderTopGallery);
if (lightbox) {
lightboxClose?.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
closeTopGalleryLightbox();
});
lightbox.addEventListener("click", (event) => {
if (event.target === lightbox || event.target.hasAttribute("data-top-gallery-close")) {
closeTopGalleryLightbox();
}
});
lightbox.querySelector("[data-top-gallery-prev]")?.addEventListener("click", () => moveTopGalleryLightbox(-1));
lightbox.querySelector("[data-top-gallery-next]")?.addEventListener("click", () => moveTopGalleryLightbox(1));
}
document.addEventListener("keydown", (event) => {
if (!lightbox || lightbox.hidden) return;
if (event.key === "Escape") {
event.preventDefault();
closeTopGalleryLightbox();
} else if (event.key === "ArrowLeft") {
event.preventDefault();
moveTopGalleryLightbox(-1);
} else if (event.key === "ArrowRight") {
event.preventDefault();
moveTopGalleryLightbox(1);
} else {
trapLightboxFocus(event);
}
});
renderTopGallery();
})();
OMIYAMAIRI GUIDE
はじめての伊奈波神社
お宮参り撮影ガイド
当日の流れを、都写真館のカメラマンがご案内します
ガイドを読む