173 lines
11 KiB
HTML
173 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Cast a character — PayToLaugh</title>
|
|
<meta name="description" content="Draw a few candidates for a character in the looks the story needs, and choose the one who will be in every picture.">
|
|
<meta name="robots" content="noindex,nofollow">
|
|
<link rel="stylesheet" href="/assets/app.css">
|
|
<style>
|
|
.who{display:flex;gap:16px;align-items:flex-start;flex-wrap:wrap;margin:0 0 16px}
|
|
.who img{width:110px;border-radius:10px;border:1px solid var(--line)}
|
|
.who .ap{color:var(--muted);font-size:13.5px;max-width:720px;line-height:1.5}
|
|
.looks{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
@media (max-width:800px){.looks{grid-template-columns:1fr}}
|
|
.look{border:1px solid var(--line);border-radius:12px;padding:14px;background:var(--panel)}
|
|
.look h4{margin:0 0 6px;font-size:14px}
|
|
.look input,.look textarea{width:100%;box-sizing:border-box;font:inherit;font-size:14px;margin:0 0 8px}
|
|
.look textarea{min-height:88px}
|
|
.tiny{font-size:12px;color:var(--muted)}
|
|
.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:14px 0}
|
|
.grid{display:grid;gap:16px;margin-top:18px}
|
|
.cand{border:1px solid var(--line);border-radius:12px;padding:12px;background:var(--panel)}
|
|
.cand.chosen{border-color:#3fae74}
|
|
.cand .pics{display:flex;gap:10px;flex-wrap:wrap}
|
|
.cand .pic{flex:1 1 200px;max-width:320px}
|
|
.cand .pic img{width:100%;border-radius:8px;border:1px solid var(--line);cursor:zoom-in}
|
|
.cand .pic .cap{font-size:12px;color:var(--muted);margin-top:4px}
|
|
.cand .foot{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
|
|
.sheets{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
|
.sheets img{height:150px;border-radius:8px;border:1px solid var(--line)}
|
|
.btn.small{padding:7px 13px;font-size:13px}
|
|
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
|
|
.zoom{position:fixed;inset:0;background:rgba(0,0,0,.88);display:flex;align-items:center;justify-content:center;z-index:50;cursor:zoom-out}
|
|
.zoom img{max-height:96vh;max-width:96vw;border-radius:8px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap" style="max-width:1100px">
|
|
<header class="page">
|
|
<h1 id="h1">Cast a character</h1>
|
|
<p id="lede">Draw a few candidates in the looks the story needs. Each candidate is one person, drawn in every look. Choose one and they become the face every picture is drawn from.</p>
|
|
</header>
|
|
<div id="msg" class="msg"></div>
|
|
<div id="who" class="who"></div>
|
|
<div id="form">
|
|
<div class="looks" id="looks"></div>
|
|
<div class="row">
|
|
<label class="tiny">Candidates <select id="n"><option>2</option><option selected>3</option><option>4</option></select></label>
|
|
<button type="button" class="btn" id="draw">Draw the candidates</button>
|
|
<span class="tiny" id="cost"></span>
|
|
</div>
|
|
</div>
|
|
<div id="state" class="tiny"></div>
|
|
<div id="grid" class="grid"></div>
|
|
<p class="tiny" style="margin-top:22px"><a id="back" href="/trailer.html">← Back to the storyboard</a></p>
|
|
</div>
|
|
<script src="/assets/app.js"></script>
|
|
<script>
|
|
(function () {
|
|
navBar("create");
|
|
if (!Session.get()) { location.href = "/register.html?next=" + encodeURIComponent(location.pathname + location.search); return; }
|
|
var Q = new URLSearchParams(location.search);
|
|
var DID = Q.get("draft") || "", CID = Q.get("character") || "", RUN = Q.get("run") || "";
|
|
var C = null, POLL = null, PER_PICTURE = 0.068;
|
|
function $(id) { return document.getElementById(id); }
|
|
function msg(t, kind) { var m = $("msg"); m.className = "msg " + (kind || "err"); m.textContent = t; if (!t) m.className = "msg"; }
|
|
|
|
// The two looks the owner asked for on 22 Sep 2026 are the starting point
|
|
// for Dorothy; any other character starts from their own wardrobe.
|
|
var DOROTHY = [
|
|
{ name: "Coming off the court (the opening)",
|
|
wear: "a loose number 22 basketball jersey, loose basketball shorts, scuffed high-top basketball shoes, dark hair in one practical braid, a basketball under one arm, a little sweaty",
|
|
setting: "Late afternoon at the edge of an empty outdoor basketball court in a small Kansas town, walking off the court toward camera, sun low behind her, confident and unbothered, no interest in how she looks." },
|
|
{ name: "Getting ready for the dance (the ending)",
|
|
wear: "a fitted, glamorous dress for a high school dance, dark hair down and done, light make-up, and the silver shoes from Oz with their heels up - silver high-heeled shoes",
|
|
setting: "Her bedroom in the farmhouse in the evening, standing in front of a full-length mirror getting ready for the school dance, turning to see herself, a small surprised smile - the tomboy seeing who she can also be." } ];
|
|
|
|
function lookHtml(i, lk) {
|
|
return '<div class="look"><h4>Look ' + (i + 1) + '</h4>'
|
|
+ '<input data-k="name" data-i="' + i + '" placeholder="A name for this look" value="' + esc(lk.name || "") + '">'
|
|
+ '<textarea data-k="wear" data-i="' + i + '" placeholder="What they wear, hair, anything carried">' + esc(lk.wear || "") + '</textarea>'
|
|
+ '<textarea data-k="setting" data-i="' + i + '" placeholder="Where they are, the light, what they are doing, the mood">' + esc(lk.setting || "") + '</textarea>'
|
|
+ '<span class="tiny">Leave the clothes empty to skip this look.</span></div>';
|
|
}
|
|
function readLooks() {
|
|
var out = [];
|
|
[0, 1].forEach(function (i) {
|
|
var g = function (k) { return (document.querySelector('[data-k="' + k + '"][data-i="' + i + '"]') || {}).value || ""; };
|
|
if (g("wear").trim()) out.push({ name: g("name"), wear: g("wear"), setting: g("setting") });
|
|
});
|
|
return out;
|
|
}
|
|
function cost() {
|
|
var n = parseInt($("n").value, 10) || 2, looks = readLooks().length;
|
|
$("cost").textContent = looks ? "about $" + (n * looks * PER_PICTURE).toFixed(2) + " for " + (n * looks) + " pictures" : "";
|
|
}
|
|
|
|
async function load() {
|
|
var r = await api("get_draft", { draft_id: DID });
|
|
if (!r.ok) { msg(r.error || "That did not load."); return; }
|
|
var pkg = r.draft.package || JSON.parse(r.draft.package_json || "{}");
|
|
C = (pkg.characters || []).filter(function (c) { return String(c.id) === CID; })[0];
|
|
if (!C) { msg("That character is not in this storyboard."); return; }
|
|
$("back").href = "/trailer.html?run=" + encodeURIComponent(RUN || r.draft.run_id || "");
|
|
$("h1").textContent = "Cast " + (C.name || C.id);
|
|
$("who").innerHTML = (C.head_url ? '<img src="' + esc(C.head_url) + '" alt="">' : '')
|
|
+ '<div><b>' + esc(C.name || C.id) + '</b> <span class="tiny">' + esc(C.role || "") + (C.age ? " · " + esc(String(C.age)) : "") + '</span>'
|
|
+ '<div class="ap">' + esc(C.appearance || "") + '</div>'
|
|
+ (C.wardrobe ? '<div class="ap"><i>Wears in this chapter:</i> ' + esc(C.wardrobe) + '</div>' : '')
|
|
+ (C.look_chosen ? '<div class="tiny" style="margin-top:6px;color:#5fd38d">Cast from candidate ' + (C.look_chosen.index + 1) + ' of ' + C.look_chosen.candidate_of + ' by ' + esc(C.look_chosen.by || "") + ' · ' + esc(new Date(C.look_chosen.at).toLocaleString()) + '</div>' : '')
|
|
+ '</div>';
|
|
var lc = C.look_candidates || {};
|
|
if (!$("looks").innerHTML) {
|
|
var start = lc.looks && lc.looks.length ? lc.looks : (/dorothy/i.test(C.id + " " + C.name) ? DOROTHY : [{ name: "As written", wear: C.wardrobe || "", setting: "" }, { name: "", wear: "", setting: "" }]);
|
|
$("looks").innerHTML = lookHtml(0, start[0] || {}) + lookHtml(1, start[1] || {});
|
|
cost();
|
|
}
|
|
paint(lc);
|
|
var busy = lc.state === "working" || lc.state === "choosing";
|
|
$("draw").disabled = busy;
|
|
if (busy && !POLL) POLL = setInterval(load, 4000);
|
|
if (!busy && POLL) { clearInterval(POLL); POLL = null; }
|
|
}
|
|
|
|
function paint(lc) {
|
|
var items = lc.items || [], looks = lc.looks || [];
|
|
$("state").textContent = lc.state === "working" ? "Drawing " + (lc.asked || "") + " candidates…"
|
|
: lc.state === "choosing" ? "Drawing the chosen candidate's reference sheets…"
|
|
: (lc.error ? lc.error : (items.length ? items.length + " candidates, drawn " + new Date(lc.made_at).toLocaleString() : ""));
|
|
$("grid").innerHTML = items.map(function (it, i) {
|
|
var chosen = lc.chosen === i;
|
|
return '<div class="cand' + (chosen ? ' chosen' : '') + '"><b>Candidate ' + (i + 1) + '</b>' + (chosen ? ' <span class="tiny" style="color:#5fd38d">— cast</span>' : '')
|
|
+ '<div class="pics">' + (it.urls || []).map(function (u, k) {
|
|
return '<div class="pic">' + (u ? '<img src="' + esc(u) + '" data-zoom="1" alt="">' : '<div class="tiny">not drawn</div>')
|
|
+ '<div class="cap">' + esc((looks[k] || {}).name || ("Look " + (k + 1))) + '</div></div>'; }).join("")
|
|
+ '</div><div class="foot"><button type="button" class="btn small" data-pick="' + i + '"' + (lc.state === "choosing" ? ' disabled' : '') + '>'
|
|
+ (chosen ? 'Cast again from this one' : 'Cast this one') + '</button>'
|
|
+ '<span class="tiny">Draws the head, front, side and back sheets from this face (about $' + (4 * PER_PICTURE).toFixed(2) + ').</span></div>'
|
|
+ (chosen && C.head_url ? '<div class="sheets">' + [C.head_url, C.front_url, C.side_url, C.back_url].filter(Boolean).map(function (u) { return '<img src="' + esc(u) + '" data-zoom="1" alt="">'; }).join("") + '</div>' : '')
|
|
+ '</div>';
|
|
}).join("");
|
|
}
|
|
|
|
document.addEventListener("input", function (e) { if (e.target.closest("#form")) cost(); });
|
|
$("draw").addEventListener("click", async function () {
|
|
var looks = readLooks();
|
|
if (!looks.length) { msg("Say what they wear in at least one look."); return; }
|
|
msg("");
|
|
$("draw").disabled = true;
|
|
var r = await api("look_candidates", { draft_id: DID, character_id: CID, looks: looks, n: parseInt($("n").value, 10) || 2 });
|
|
if (!r.ok) { msg(r.error || "That did not start."); $("draw").disabled = false; return; }
|
|
msg("Drawing " + r.pictures + " pictures (about $" + r.usd + "). They appear below as they finish.", "ok");
|
|
load();
|
|
});
|
|
document.addEventListener("click", async function (e) {
|
|
var z = e.target.closest("[data-zoom]");
|
|
if (z) { var d = document.createElement("div"); d.className = "zoom"; d.innerHTML = '<img src="' + esc(z.src) + '" alt="">'; d.onclick = function () { d.remove(); }; document.body.appendChild(d); return; }
|
|
var p = e.target.closest("[data-pick]");
|
|
if (p) {
|
|
var r = await api("pick_look", { draft_id: DID, character_id: CID, index: parseInt(p.getAttribute("data-pick"), 10) });
|
|
if (!r.ok) { msg(r.error || "That did not start."); return; }
|
|
msg("Drawing the reference sheets from that face (about $" + r.usd + ").", "ok");
|
|
load();
|
|
}
|
|
});
|
|
|
|
if (!DID || !CID) { msg("Open this page from a character on the storyboard."); return; }
|
|
load();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|