collage
This commit is contained in:
142
src/templates.js
142
src/templates.js
@@ -41,6 +41,24 @@ function cappedPixelValue(value, cap) {
|
||||
return `${Math.min(number, cap)}px`;
|
||||
}
|
||||
|
||||
function collageCardWidth({ width, spacing, columns }) {
|
||||
const availableWidth = width - (spacing * 2) - (spacing * Math.max(columns - 1, 0));
|
||||
const columnWidth = Math.floor(availableWidth / columns);
|
||||
|
||||
return Math.max(Math.min(columnWidth, 500), 120);
|
||||
}
|
||||
|
||||
function shuffleItems(items) {
|
||||
const shuffled = [...items];
|
||||
|
||||
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
||||
const randomIndex = Math.floor(Math.random() * (index + 1));
|
||||
[shuffled[index], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[index]];
|
||||
}
|
||||
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
function addIframePermissions(tag) {
|
||||
const autoplayPermission = "autoplay";
|
||||
|
||||
@@ -323,9 +341,48 @@ export function homePage({ providersCount = 0 } = {}) {
|
||||
<label class="checkbox-label"><input name="transparent" value="1" type="checkbox" checked> transparent</label>
|
||||
</div>
|
||||
</form>
|
||||
<form class="collage-form" action="/collage" method="get">
|
||||
<label for="urls">Collage URLs</label>
|
||||
<textarea id="urls" name="urls" rows="6" placeholder="https://bsky.app/...\nhttps://x.com/...\nhttps://www.instagram.com/..."></textarea>
|
||||
<div class="controls collage-controls">
|
||||
<label>Width <input name="width" type="number" value="1920" min="320" max="3840"></label>
|
||||
<label>Height <input name="height" type="number" value="1080" min="240" max="2160"></label>
|
||||
<label>Spacing <input name="spacing" type="number" value="48" min="0" max="400"></label>
|
||||
<label>Fade <input name="fade" type="number" value="0" min="0" max="600"></label>
|
||||
<label>Columns <input name="columns" type="number" value="3" min="1" max="8"></label>
|
||||
<label>Duration <input name="duration" type="number" value="360" min="10" max="1200"></label>
|
||||
<label>Repeat <input name="repeat" type="number" value="4" min="2" max="12"></label>
|
||||
<label class="checkbox-label"><input name="shuffle" value="1" type="checkbox" checked> shuffle</label>
|
||||
<label class="checkbox-label"><input name="transparent" value="1" type="checkbox" checked> transparent</label>
|
||||
</div>
|
||||
<button type="submit">Open collage</button>
|
||||
</form>
|
||||
<p>${providersCount} provider URL patterns loaded. Use <code>/graphic?url=...</code> from CasparCG, OBS Browser Source, or OGraf.</p>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
document.querySelector(".collage-form").addEventListener("submit", (event) => {
|
||||
const form = event.currentTarget;
|
||||
const textarea = form.querySelector("textarea[name='urls']");
|
||||
const urls = textarea.value.split(/[\\n,]+/).map((url) => url.trim()).filter(Boolean);
|
||||
|
||||
if (!urls.length) {
|
||||
event.preventDefault();
|
||||
textarea.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new FormData(form);
|
||||
data.delete("urls");
|
||||
|
||||
for (const url of urls) {
|
||||
data.append("url", url);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
window.location.href = form.action + "?" + new URLSearchParams(data).toString();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -341,6 +398,76 @@ export function casparTemplatePage() {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function embedCardHtml({
|
||||
targetUrl,
|
||||
embed,
|
||||
autoplay,
|
||||
muted,
|
||||
className = "embed",
|
||||
widthCap = 500,
|
||||
includeHeight = true,
|
||||
}) {
|
||||
const providerClass = `provider-${slugify(embed.provider_name)}`;
|
||||
const embedWidth = cappedPixelValue(embed.width, widthCap);
|
||||
const embedHeight = numericPixelValue(embed.height);
|
||||
const embedStyle = [
|
||||
embedWidth ? `--embed-width:${embedWidth}` : "",
|
||||
includeHeight && embedHeight ? `--embed-height:${embedHeight}` : "",
|
||||
].filter(Boolean).join(";");
|
||||
const html = prepareEmbedHtml(embed.html || "", { autoplay, muted });
|
||||
const media = embed.type === "photo" && embed.url
|
||||
? `<img src="${escapeHtml(embed.url)}" alt="${escapeHtml(embed.title || embed.provider_name || "")}">`
|
||||
: html;
|
||||
|
||||
return `<section class="${escapeHtml(className)} ${escapeHtml(providerClass)}" data-source="${escapeHtml(targetUrl)}"${embedStyle ? ` style="${escapeHtml(embedStyle)}"` : ""}>
|
||||
${media}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
export function collagePage({
|
||||
items,
|
||||
width,
|
||||
height,
|
||||
transparent,
|
||||
chroma,
|
||||
autoplay = true,
|
||||
muted = true,
|
||||
spacing = 48,
|
||||
fade = 0,
|
||||
columns = 3,
|
||||
duration = 360,
|
||||
repeat = 4,
|
||||
shuffle = true,
|
||||
}) {
|
||||
const cardWidth = collageCardWidth({ width, spacing, columns });
|
||||
const orderedItems = shuffle ? shuffleItems(items) : items;
|
||||
const groupItems = Array.from({ length: repeat }, () => orderedItems).flat();
|
||||
const groupCards = groupItems
|
||||
.map((item) => embedCardHtml({
|
||||
targetUrl: item.targetUrl,
|
||||
embed: item.embed,
|
||||
autoplay,
|
||||
muted,
|
||||
className: "embed collage-card",
|
||||
widthCap: cardWidth,
|
||||
includeHeight: false,
|
||||
}))
|
||||
.join("\n");
|
||||
const groups = [groupCards, groupCards]
|
||||
.map((cards) => `<div class="collage-group">${cards}</div>`)
|
||||
.join("\n");
|
||||
|
||||
return `${commonHead({ title: "oEmbed Collage", htmlClass: transparent ? "graphic-document transparent" : "graphic-document" })}
|
||||
<body class="graphic collage-page ${transparent ? "transparent" : ""} is-ready" style="--stage-width:${width}px; --stage-height:${height}px; --chroma:${escapeHtml(chroma)}; --collage-spacing:${spacing}px; --collage-fade:${fade}px; --collage-columns:${columns}; --collage-card-width:${cardWidth}px; --collage-duration:${duration}s;">
|
||||
<main class="collage-stage">
|
||||
<div class="collage-track">
|
||||
${groups}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export function graphicPage({
|
||||
targetUrl,
|
||||
embed,
|
||||
@@ -357,24 +484,11 @@ export function graphicPage({
|
||||
maxWait = 10000,
|
||||
}) {
|
||||
const title = embed.title || embed.provider_name || "oEmbed Graphic";
|
||||
const providerClass = `provider-${slugify(embed.provider_name)}`;
|
||||
const embedWidth = cappedPixelValue(embed.width, 500);
|
||||
const embedHeight = numericPixelValue(embed.height);
|
||||
const embedStyle = [
|
||||
embedWidth ? `--embed-width:${embedWidth}` : "",
|
||||
embedHeight ? `--embed-height:${embedHeight}` : "",
|
||||
].filter(Boolean).join(";");
|
||||
const html = prepareEmbedHtml(embed.html || "", { autoplay, muted });
|
||||
const media = embed.type === "photo" && embed.url
|
||||
? `<img src="${escapeHtml(embed.url)}" alt="${escapeHtml(title)}">`
|
||||
: html;
|
||||
|
||||
return `${commonHead({ title, htmlClass: transparent ? "graphic-document transparent" : "graphic-document" })}
|
||||
<body class="graphic ${transparent ? "transparent" : ""} ${wait ? "is-loading" : "is-ready"}" style="--stage-width:${width}px; --stage-height:${height}px; --scale:${scale}; --chroma:${escapeHtml(chroma)};">
|
||||
<main class="stage fit-${escapeHtml(fit)}">
|
||||
<section class="embed ${escapeHtml(providerClass)}" data-source="${escapeHtml(targetUrl)}"${embedStyle ? ` style="${escapeHtml(embedStyle)}"` : ""}>
|
||||
${media}
|
||||
</section>
|
||||
${embedCardHtml({ targetUrl, embed, autoplay, muted })}
|
||||
</main>
|
||||
${wait ? revealScript({ readyDelay, maxWait }) : ""}
|
||||
${autoplay ? autoplayAssistScript({ muted }) : ""}
|
||||
|
||||
Reference in New Issue
Block a user