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": { "dependencies": {
"compression": "^1.7.1", "compression": "^1.7.1",
"eases-jsnext": "^1.0.10",
"polka": "^0.4.0", "polka": "^0.4.0",
"sapper": "^0.14.0", "sapper": "^0.14.0",
"sirv": "^0.1.1" "sirv": "^0.1.1",
"svelte-transitions": "^1.2.0",
"yootils": "^0.0.9"
}, },
"devDependencies": { "devDependencies": {
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",
"svelte": "^2.0.0", "svelte": "^2.9.0",
"svelte-loader": "^2.9.0", "svelte-loader": "^2.9.0",
"webpack": "^4.7.0" "webpack": "^4.7.0"
} }

View File

@@ -2,13 +2,23 @@
<title>{post.title}</title> <title>{post.title}</title>
</svelte:head> </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'> <div transition:fade="{duration: 100}" class='content'>
{@html post.html} {@html post.html}
</div>
</div> </div>
<style> <style>
h1 {
display: inline-block;
will-change: transform;
}
/* /*
By default, CSS is locally scoped to the component, By default, CSS is locally scoped to the component,
and any unused styles are dead-code-eliminated. and any unused styles are dead-code-eliminated.
@@ -45,6 +55,9 @@
</style> </style>
<script> <script>
import { fade } from 'svelte-transitions';
import { send, receive } from './_transitions.js';
export default { export default {
async preload({ params, query }) { async preload({ params, query }) {
// the `slug` parameter is available because // the `slug` parameter is available because
@@ -57,6 +70,12 @@
} else { } else {
this.error(res.status, data.message); this.error(res.status, data.message);
} }
},
transitions: {
fade,
send,
receive
} }
}; };
</script> </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> <title>Blog</title>
</svelte:head> </svelte:head>
<h1>Recent posts</h1> <div style="position: absolute">
<h1 transition:fade="{duration: 100}">Recent posts</h1>
<ul> <ul>
{#each posts as post} {#each posts as post}
<!-- we're using the non-standard `rel=prefetch` attribute to <!-- we're using the non-standard `rel=prefetch` attribute to
tell Sapper to load the data for the page as soon as tell Sapper to load the data for the page as soon as
the user hovers over the link or taps it, instead of the user hovers over the link or taps it, instead of
waiting for the 'click' event --> waiting for the 'click' event -->
<li><a rel='prefetch' href='blog/{post.slug}'>{post.title}</a></li> <li out:fade={duration:100}>
{/each} <a
</ul> rel='prefetch'
href='blog/{post.slug}'
in:receive="{key: post.title}"
out:send="{key: post.title}"
>{post.title}</a>
</li>
{/each}
</ul>
</div>
<style> <style>
ul { ul {
margin: 0 0 1em 0; margin: 0 0 1em 0;
line-height: 1.5; line-height: 1.5;
} }
a {
display: inline-block;
will-change: transform;
}
</style> </style>
<script> <script>
import { fade } from 'svelte-transitions';
import { send, receive } from './_transitions.js';
export default { export default {
preload({ params, query }) { preload({ params, query }) {
return this.fetch(`blog.json`).then(r => r.json()).then(posts => { return this.fetch(`blog.json`).then(r => r.json()).then(posts => {
return { posts }; return { posts };
}); });
},
transitions: {
fade,
send,
receive
} }
}; };
</script> </script>

View File

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