basic page transitions between /blog and /blog/[slug]

This commit is contained in:
Rich Harris
2018-07-01 10:59:41 -04:00
parent 1dcad25401
commit fd73119bda
7 changed files with 231 additions and 16 deletions

View File

@@ -13,13 +13,16 @@
},
"dependencies": {
"compression": "^1.7.1",
"eases-jsnext": "^1.0.10",
"polka": "^0.4.0",
"sapper": "^0.14.0",
"sirv": "^0.1.1"
"sirv": "^0.1.1",
"svelte-transitions": "^1.2.0",
"yootils": "^0.0.9"
},
"devDependencies": {
"npm-run-all": "^4.1.2",
"svelte": "^2.0.0",
"svelte": "^2.9.0",
"svelte-loader": "^2.9.0",
"webpack": "^4.7.0"
}

View File

@@ -2,13 +2,23 @@
<title>{post.title}</title>
</svelte:head>
<h1>{post.title}</h1>
<div style="position: absolute">
<h1
in:receive="{key: post.title}"
out:send="{key: post.title}"
>{post.title}</h1>
<div class='content'>
{@html post.html}
<div transition:fade="{duration: 100}" class='content'>
{@html post.html}
</div>
</div>
<style>
h1 {
display: inline-block;
will-change: transform;
}
/*
By default, CSS is locally scoped to the component,
and any unused styles are dead-code-eliminated.
@@ -45,6 +55,9 @@
</style>
<script>
import { fade } from 'svelte-transitions';
import { send, receive } from './_transitions.js';
export default {
async preload({ params, query }) {
// the `slug` parameter is available because
@@ -57,6 +70,12 @@
} else {
this.error(res.status, data.message);
}
},
transitions: {
fade,
send,
receive
}
};
</script>

82
routes/blog/_crossfade.js Normal file
View File

@@ -0,0 +1,82 @@
import * as eases from 'eases-jsnext';
import * as yootils from 'yootils';
export default function crossfade({ fallback }) {
let requested = new Map();
let provided = new Map();
function crossfade(from, node) {
const to = node.getBoundingClientRect();
console.log({ from, to });
const dx = from.left - to.left;
const dy = from.top - to.top;
const dsx = (from.right - from.left) / (to.right - to.left);
const dsy = (from.bottom - from.top) / (to.bottom - to.top);
console.log({ dsx, dsy });
const sx = yootils.linearScale([0, 1], [dsx, 1]);
const sy = yootils.linearScale([0, 1], [dsy, 1]);
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 4000,
easing: eases.quintOut,
css: (t, u) => `
opacity: ${t};
transform-origin: 0 0;
transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${sx(t)}, ${sy(t)});
`,
tick: (t, u) => {
// console.log({
// sx: 1 + u * dsx,
// sy: 1 + u * dsy,
// });
}
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return crossfade(rect, node);
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return crossfade(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

75
routes/blog/_move.js Normal file
View File

@@ -0,0 +1,75 @@
import * as eases from 'eases-jsnext';
import * as yootils from 'yootils';
export default function move({ fallback }) {
let requested = new Map();
let provided = new Map();
function move(from, node) {
const to = node.getBoundingClientRect();
const dx = from.left - to.left;
const dy = from.top - to.top;
const dsx = (from.right - from.left) / (to.right - to.left);
const dsy = (from.bottom - from.top) / (to.bottom - to.top);
const sx = yootils.linearScale([0, 1], [dsx, 1]);
const sy = yootils.linearScale([0, 1], [dsy, 1]);
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
return {
duration: 400,
easing: eases.quintOut,
css: (t, u) => `
transform-origin: 0 0;
transform: ${transform} translate(${u * dx}px,${u * dy}px) scale(${sx(t)}, ${sy(t)});
`
};
}
return {
send(node, params) {
provided.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (requested.has(params.key)) {
const { rect } = requested.get(params.key);
requested.delete(params.key);
return {
duration: 0,
css: () => `opacity: 0`
};
}
// if the node is disappearing altogether
// (i.e. wasn't claimed by the other list)
// then we need to supply an outro
provided.delete(params.key);
return fallback(node, params);
};
},
receive(node, params) {
requested.set(params.key, {
rect: node.getBoundingClientRect()
});
return () => {
if (provided.has(params.key)) {
const { rect } = provided.get(params.key);
provided.delete(params.key);
return move(rect, node);
}
requested.delete(params.key);
return fallback(node, params);
};
}
};
}

View File

@@ -0,0 +1,12 @@
import move from './_move.js';
const { send, receive } = move({
fallback(node) {
return {
duration: 0,
css: t => `opacity: ${t}`
};
}
});
export { send, receive };

View File

@@ -2,31 +2,54 @@
<title>Blog</title>
</svelte:head>
<h1>Recent posts</h1>
<div style="position: absolute">
<h1 transition:fade="{duration: 100}">Recent posts</h1>
<ul>
{#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li>
{/each}
</ul>
<ul>
{#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of
waiting for the 'click' event -->
<li out:fade={duration:100}>
<a
rel='prefetch'
href='blog/{post.slug}'
in:receive="{key: post.title}"
out:send="{key: post.title}"
>{post.title}</a>
</li>
{/each}
</ul>
</div>
<style>
ul {
margin: 0 0 1em 0;
line-height: 1.5;
}
a {
display: inline-block;
will-change: transform;
}
</style>
<script>
import { fade } from 'svelte-transitions';
import { send, receive } from './_transitions.js';
export default {
preload({ params, query }) {
return this.fetch(`blog.json`).then(r => r.json()).then(posts => {
return { posts };
});
},
transitions: {
fade,
send,
receive
}
};
</script>

View File

@@ -20,7 +20,8 @@ module.exports = {
options: {
dev: isDev,
hydratable: true,
hotReload: true
hotReload: true,
nestedTransitions: true
}
}
}