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:
@@ -1340,6 +1340,16 @@ html[data-phase="night"] {
|
|||||||
/* An empty place a card could be: the stock when it's spent, a foundation
|
/* An empty place a card could be: the stock when it's spent, a foundation
|
||||||
waiting for its ace, a column waiting for a king. Same footprint as a card,
|
waiting for its ace, a column waiting for a king. Same footprint as a card,
|
||||||
so nothing on the board reflows when one empties. */
|
so nothing on the board reflows when one empties. */
|
||||||
|
/* Where cards go home. Solitaire lifts a copy of every card bound for a
|
||||||
|
foundation into here so it can keep flying while the board underneath it
|
||||||
|
redraws in one go. It never takes a click and it never scrolls. */
|
||||||
|
.pete-flight-layer {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 60;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.pete-slot {
|
.pete-slot {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -240,20 +240,45 @@
|
|||||||
|
|
||||||
// meter is what the board is worth. Every number in it comes off the server —
|
// 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.
|
// 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) {
|
if (!v) {
|
||||||
homeEl.innerHTML = '0<span class="text-white/40">/' + FULL + "</span>";
|
homeEl.innerHTML = '0<span class="text-white/40">/' + FULL + "</span>";
|
||||||
perCardEl.textContent = "—";
|
perCardEl.textContent = "—";
|
||||||
breakEvenEl.textContent = "";
|
breakEvenEl.textContent = "";
|
||||||
return;
|
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);
|
perCardEl.textContent = "+" + v.per_card.toFixed(1);
|
||||||
breakEvenEl.textContent =
|
breakEvenEl.textContent =
|
||||||
v.home >= v.break_even
|
n >= v.break_even
|
||||||
? "You're ahead of the house"
|
? "You're ahead of the house"
|
||||||
: v.break_even - v.home + " more to break even";
|
: v.break_even - n + " more to break even";
|
||||||
meterEl.dataset.cold = v.home === 0 ? "1" : "0";
|
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) {
|
function controls(v) {
|
||||||
@@ -300,12 +325,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// animate plays every card from where it was to where it is.
|
// 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();
|
if (reduced) return Promise.resolve();
|
||||||
var waits = [];
|
var waits = [];
|
||||||
|
|
||||||
root.querySelectorAll(".pete-card[data-key]").forEach(function (el) {
|
root.querySelectorAll(".pete-card[data-key]").forEach(function (el) {
|
||||||
var key = el.dataset.key;
|
var key = el.dataset.key;
|
||||||
|
if (flying && flying[key]) return; // a ghost of it is already making the trip
|
||||||
var now = el.getBoundingClientRect();
|
var now = el.getBoundingClientRect();
|
||||||
var was = before[key];
|
var was = before[key];
|
||||||
var delay = plan.delays[key] || 0;
|
var delay = plan.delays[key] || 0;
|
||||||
@@ -336,6 +362,86 @@
|
|||||||
return Promise.all(waits);
|
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) {
|
function slide(el, dx, dy, delay) {
|
||||||
return el.animate(
|
return el.animate(
|
||||||
[{ transform: "translate(" + dx + "px," + dy + "px)" }, { transform: "none" }],
|
[{ 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
|
// 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
|
// 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
|
// 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
|
// silence, so this counts its way through the script exactly as planOf,
|
||||||
// flashHome do, and any change to their timing has to be made here too.
|
// homeOut and flashHome do, and any change to their timing has to be made here
|
||||||
|
// too.
|
||||||
function noises(events) {
|
function noises(events) {
|
||||||
var at = 0;
|
var at = 0;
|
||||||
(events || []).forEach(function (e) {
|
(events || []).forEach(function (e) {
|
||||||
@@ -612,11 +719,15 @@
|
|||||||
return pre
|
return pre
|
||||||
.then(function () {
|
.then(function () {
|
||||||
var before = snapshot();
|
var before = snapshot();
|
||||||
|
var wasHome = board ? board.home : 0;
|
||||||
|
var home = homeOut(events);
|
||||||
render(v);
|
render(v);
|
||||||
|
land(home.arrivals);
|
||||||
|
tally(v, home.arrivals, wasHome);
|
||||||
var plan = planOf(events);
|
var plan = planOf(events);
|
||||||
flashHome(events);
|
flashHome(events);
|
||||||
noises(events);
|
noises(events);
|
||||||
return animate(before, plan);
|
return Promise.all([animate(before, plan, home.flying), home.done]);
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
if (!v) { money(); return; }
|
if (!v) { money(); return; }
|
||||||
|
|||||||
Reference in New Issue
Block a user