paytolaugh/looks.html

210 lines
14 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="look" style="margin:0 0 14px"><h4>Who they are (the words every picture is drawn from)</h4>
<textarea id="appearance" placeholder="Build, face, hair, colouring, age"></textarea>
<input id="wardrobe" placeholder="What they wear in this chapter">
<div class="row" style="margin:0"><button type="button" class="btn small ghost" id="save">Save the description</button><span class="tiny" id="saved"></span></div>
</div>
<div class="look" style="margin:0 0 14px"><h4>Or use your own picture of them</h4>
<span class="tiny">A picture you made or a photograph you hold the rights to. A side-by-side (two looks in one picture) is split into its halves and both are used. The reference sheets are drawn from it at once (about $0.27).</span>
<div class="row" style="margin:8px 0 0"><input type="file" id="own" accept="image/*" style="width:auto;font-size:14px"><span class="tiny" id="ownstate"></span></div>
</div>
<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">&larr; 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) {
$("appearance").value = C.appearance || ""; $("wardrobe").value = C.wardrobe || "";
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>' + (it.supplied ? ' <span class="tiny">— your own picture' + (it.note ? ' (' + esc(it.note) + ')' : '') + '</span>' : '') + (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(it.supplied ? (u ? "as supplied" : "") : ((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(); });
async function saveWho() {
var ap = $("appearance").value.trim(), wd = $("wardrobe").value.trim();
if (ap === (C.appearance || "") && wd === (C.wardrobe || "")) return true;
var r = await api("set_character", { draft_id: DID, character_id: CID, appearance: ap, wardrobe: wd });
if (!r.ok) { msg(r.error || "The description did not save."); return false; }
C.appearance = ap; C.wardrobe = wd;
$("saved").textContent = "Saved to " + r.drafts + " drafts of this chapter.";
return true;
}
$("save").addEventListener("click", saveWho);
$("own").addEventListener("change", async function () {
var file = $("own").files[0];
if (!file) return;
if (!/^image\//.test(file.type)) { msg("Please choose an image file."); $("own").value = ""; return; }
$("ownstate").textContent = "preparing…";
var img;
try { img = await resizeImage(file, 2400, 0.92); }
catch (err) { $("ownstate").textContent = ""; msg(err.message || "Could not read that picture."); $("own").value = ""; return; }
if (!(await saveWho())) return;
$("ownstate").textContent = "uploading…";
var r = await api("cast_from_picture", { draft_id: DID, character_id: CID, data_b64: img.b64, note: file.name });
$("own").value = ""; $("ownstate").textContent = "";
if (!r.ok) { msg(r.error || "That upload did not work."); return; }
msg("Your picture is candidate " + (r.index + 1) + (r.pieces > 1 ? " (split in " + r.pieces + ")" : "") + "; drawing the reference sheets from it (about $" + r.usd + ").", "ok");
load();
});
$("draw").addEventListener("click", async function () {
var looks = readLooks();
if (!looks.length) { msg("Say what they wear in at least one look."); return; }
msg("");
if (!(await saveWho())) return;
$("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>