solitaire: fly the cards home instead of blinking them there
A foundation only ever draws its top card, so the FLIP diff had nothing to animate for the fifty-one cards buried underneath: an auto-finish was every card vanishing at once, four kings stranded in the tableau row, and the foundation flashes firing on empty slots. Cards bound for a foundation now fly as ghosts, lifted out of the old DOM into a fixed layer so the board underneath can re-render in one go while they are still in the air. The card that lands is held invisible until its ghost arrives, so the destination doesn't show the ending first. The home counter was jumping straight to 52/52 over a board with fifty-one cards still travelling. It walks up behind them now, on the same step ladder as the ghosts, the flashes and the sound.
This commit is contained in:
@@ -240,20 +240,45 @@
|
||||
|
||||
// meter is what the board is worth. Every number in it comes off the server —
|
||||
// this file does no arithmetic about money, which is the point.
|
||||
function meter(v) {
|
||||
//
|
||||
// `home` overrides how many cards it says are home, and nothing else. It is
|
||||
// there so the count can walk up behind the cards instead of arriving with
|
||||
// them: see tally. It only ever names a count the board has genuinely passed
|
||||
// through on its way to v.home, so the meter is never quoting a board that
|
||||
// didn't exist.
|
||||
function meter(v, home) {
|
||||
if (!v) {
|
||||
homeEl.innerHTML = '0<span class="text-white/40">/' + FULL + "</span>";
|
||||
perCardEl.textContent = "—";
|
||||
breakEvenEl.textContent = "";
|
||||
return;
|
||||
}
|
||||
homeEl.innerHTML = v.home + '<span class="text-white/40">/' + FULL + "</span>";
|
||||
var n = home === undefined ? v.home : home;
|
||||
homeEl.innerHTML = n + '<span class="text-white/40">/' + FULL + "</span>";
|
||||
perCardEl.textContent = "+" + v.per_card.toFixed(1);
|
||||
breakEvenEl.textContent =
|
||||
v.home >= v.break_even
|
||||
n >= v.break_even
|
||||
? "You're ahead of the house"
|
||||
: v.break_even - v.home + " more to break even";
|
||||
meterEl.dataset.cold = v.home === 0 ? "1" : "0";
|
||||
: v.break_even - n + " more to break even";
|
||||
meterEl.dataset.cold = n === 0 ? "1" : "0";
|
||||
}
|
||||
|
||||
// tally walks the home count up one card at a time, in step with the cards
|
||||
// actually landing. The chips on this table are already held back until the
|
||||
// payout has physically swept home — a counter that pays you before the chips
|
||||
// arrive is a counter that has told you the ending — and the card count was
|
||||
// the one number still jumping straight to the answer while fifty-one cards
|
||||
// were visibly still in the air.
|
||||
function tally(v, arrivals, from) {
|
||||
if (!v || !arrivals.length) return;
|
||||
var order = arrivals.slice().sort(function (a, b) { return a.at - b.at; });
|
||||
meter(v, from);
|
||||
order.forEach(function (a, i) {
|
||||
setTimeout(function () { meter(v, from + i + 1); }, a.at);
|
||||
});
|
||||
// Whatever the steps added up to, the board is the board: the last word goes
|
||||
// to the server's own count rather than to this file's running total.
|
||||
setTimeout(function () { meter(v); }, order[order.length - 1].at);
|
||||
}
|
||||
|
||||
function controls(v) {
|
||||
@@ -300,12 +325,13 @@
|
||||
}
|
||||
|
||||
// animate plays every card from where it was to where it is.
|
||||
function animate(before, plan) {
|
||||
function animate(before, plan, flying) {
|
||||
if (reduced) return Promise.resolve();
|
||||
var waits = [];
|
||||
|
||||
root.querySelectorAll(".pete-card[data-key]").forEach(function (el) {
|
||||
var key = el.dataset.key;
|
||||
if (flying && flying[key]) return; // a ghost of it is already making the trip
|
||||
var now = el.getBoundingClientRect();
|
||||
var was = before[key];
|
||||
var delay = plan.delays[key] || 0;
|
||||
@@ -336,6 +362,86 @@
|
||||
return Promise.all(waits);
|
||||
}
|
||||
|
||||
// Cards going home are the one move the after-picture can't describe. A
|
||||
// foundation only ever draws its top card, so an auto-finish that sends eleven
|
||||
// cards home is ten cards blinking out of existence and one appearing — which
|
||||
// is exactly what a finished board used to look like.
|
||||
//
|
||||
// So they fly as ghosts: copies lifted out of the old DOM into a fixed layer,
|
||||
// where they can still be in the air while the board underneath them
|
||||
// re-renders all at once. The real card that lands on each foundation is held
|
||||
// invisible until its ghost gets there, so the destination doesn't give away
|
||||
// the ending before the journey is over.
|
||||
function homeOut(events) {
|
||||
var none = { flying: {}, arrivals: [], done: Promise.resolve() };
|
||||
if (reduced) return none;
|
||||
|
||||
var flights = [], at = 0;
|
||||
(events || []).forEach(function (e) {
|
||||
if (e.kind === "move") { at += STEP_MS; return; }
|
||||
if (e.kind !== "home") return;
|
||||
var delay = at;
|
||||
at += STEP_MS;
|
||||
if (!e.to) return;
|
||||
var dest = foundEl.querySelector('[data-pile="' + e.to + '"]');
|
||||
if (!dest) return;
|
||||
var to = dest.getBoundingClientRect();
|
||||
(e.cards || []).forEach(function (c) {
|
||||
var el = root.querySelector('.pete-card[data-key="' + c.label + '"]');
|
||||
if (!el) return;
|
||||
flights.push({ label: c.label, pile: e.to, delay: delay, from: el.getBoundingClientRect(), to: to, el: el });
|
||||
});
|
||||
});
|
||||
if (!flights.length) return none;
|
||||
|
||||
var layer = document.createElement("div");
|
||||
layer.className = "pete-flight-layer";
|
||||
document.body.appendChild(layer);
|
||||
|
||||
var flying = {}, arrivals = [], waits = [];
|
||||
flights.forEach(function (f) {
|
||||
flying[f.label] = true;
|
||||
arrivals.push({ label: f.label, pile: f.pile, at: f.delay + MOVE_MS });
|
||||
|
||||
var ghost = f.el.cloneNode(true);
|
||||
ghost.style.position = "fixed";
|
||||
ghost.style.left = f.from.left + "px";
|
||||
ghost.style.top = f.from.top + "px";
|
||||
ghost.style.width = f.from.width + "px";
|
||||
ghost.style.height = f.from.height + "px";
|
||||
ghost.style.margin = "0";
|
||||
ghost.style.animation = "none";
|
||||
layer.appendChild(ghost);
|
||||
|
||||
waits.push(
|
||||
ghost.animate(
|
||||
[
|
||||
{ transform: "none" },
|
||||
{ transform: "translate(" + (f.to.left - f.from.left) + "px," + (f.to.top - f.from.top) + "px)" },
|
||||
],
|
||||
{ duration: MOVE_MS, delay: f.delay, easing: "cubic-bezier(0.22, 1, 0.36, 1)", fill: "both" }
|
||||
).finished.catch(noop)
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
flying: flying,
|
||||
arrivals: arrivals,
|
||||
done: Promise.all(waits).then(function () { layer.remove(); }),
|
||||
};
|
||||
}
|
||||
|
||||
// land holds each freshly-rendered foundation card back until the ghost of it
|
||||
// has finished its flight, then swaps one for the other.
|
||||
function land(arrivals) {
|
||||
arrivals.forEach(function (a) {
|
||||
var el = foundEl.querySelector('[data-pile="' + a.pile + '"] .pete-card[data-key="' + a.label + '"]');
|
||||
if (!el) return;
|
||||
el.style.visibility = "hidden";
|
||||
setTimeout(function () { el.style.visibility = ""; }, a.at);
|
||||
});
|
||||
}
|
||||
|
||||
function slide(el, dx, dy, delay) {
|
||||
return el.animate(
|
||||
[{ transform: "translate(" + dx + "px," + dy + "px)" }, { transform: "none" }],
|
||||
@@ -407,8 +513,9 @@
|
||||
// noises is the board's soundtrack, walked off the *same* STEP_MS ladder the
|
||||
// animation is walked off. That matters more than which sound goes where: a
|
||||
// card you hear land a step before it lands is worse than a card that lands in
|
||||
// silence, so this counts its way through the script exactly as planOf and
|
||||
// flashHome do, and any change to their timing has to be made here too.
|
||||
// silence, so this counts its way through the script exactly as planOf,
|
||||
// homeOut and flashHome do, and any change to their timing has to be made here
|
||||
// too.
|
||||
function noises(events) {
|
||||
var at = 0;
|
||||
(events || []).forEach(function (e) {
|
||||
@@ -612,11 +719,15 @@
|
||||
return pre
|
||||
.then(function () {
|
||||
var before = snapshot();
|
||||
var wasHome = board ? board.home : 0;
|
||||
var home = homeOut(events);
|
||||
render(v);
|
||||
land(home.arrivals);
|
||||
tally(v, home.arrivals, wasHome);
|
||||
var plan = planOf(events);
|
||||
flashHome(events);
|
||||
noises(events);
|
||||
return animate(before, plan);
|
||||
return Promise.all([animate(before, plan, home.flying), home.done]);
|
||||
})
|
||||
.then(function () {
|
||||
if (!v) { money(); return; }
|
||||
|
||||
Reference in New Issue
Block a user